Gateway: review fixes for stale summaries, history cursors that survive a moving window, timeouts on T3 calls, push size, T3's snooze rules, and input checks
This commit is contained in:
1 parent
edc4b88990
commit
99b300855e
13 files changed
+208
-58
No files matched your search
+11
-4
@@ -3,7 +3,7 @@
|
||||
// thread already waiting counts as a change, so each request notifies once. Nothing notifies while
|
||||
// a host loads its first snapshot, so a restart never replays old news.
|
||||
|
||||
import { threadPhase, type Phase } from "./awareness.ts";
|
||||
import { isSnoozed, threadPhase, type Phase } from "./awareness.ts";
|
||||
import type { PendingRequest, PushCategory, PushPayload } from "./contract.ts";
|
||||
import type { HostRow } from "./db.ts";
|
||||
import type { T3Host, ThreadChange } from "./t3/host.ts";
|
||||
@@ -25,6 +25,9 @@ export type SendPush = (push: Push) => Promise<void>;
|
||||
|
||||
const NOTIFYING: ReadonlySet<Phase> = new Set(["waiting_for_approval", "waiting_for_input", "completed", "failed"]);
|
||||
const BODY_CHARS = 180;
|
||||
const TITLE_CHARS = 80;
|
||||
/** An option value longer than this can't be sent whole, so the question goes without buttons. */
|
||||
const OPTION_VALUE_CHARS = 200;
|
||||
const SUMMARY_WAIT_MS = 3_000;
|
||||
const TOKEN_WARNING_MS = 7 * 24 * 60 * 60_000;
|
||||
|
||||
@@ -51,6 +54,8 @@ export class Notifier {
|
||||
const before = this.keys.get(id);
|
||||
this.keys.set(id, key);
|
||||
if (initial || phase === null || !NOTIFYING.has(phase) || key === before) return;
|
||||
// T3 keeps a snoozed thread asleep through background wakes, so its finishing again is not news.
|
||||
if (phase === "completed" && isSnoozed(next)) return;
|
||||
void this.notify(host, next, phase).catch((error: unknown) =>
|
||||
this.log(`push for ${id} failed: ${error instanceof Error ? error.message : String(error)}`),
|
||||
);
|
||||
@@ -97,9 +102,9 @@ export class Notifier {
|
||||
const parent = thread.delegatedFrom?.title;
|
||||
const text = body(phase, thread.lastError ?? null, summary);
|
||||
await this.send({
|
||||
title: parent ?? thread.title,
|
||||
title: trimText(parent ?? thread.title, TITLE_CHARS),
|
||||
subtitle: project ? `${host.label} · ${project}` : host.label,
|
||||
body: parent ? `Subagent ${thread.title}: ${text}` : text,
|
||||
body: parent ? `Subagent ${trimText(thread.title, TITLE_CHARS)}: ${text}` : text,
|
||||
category: category(phase),
|
||||
fromSubagent: parent !== undefined,
|
||||
data: { hostId: host.id, threadId: thread.id, ...(requestId ? { requestId } : {}), ...pushQuestion(phase, summary) },
|
||||
@@ -117,10 +122,12 @@ function pushQuestion(phase: Phase, summary: ThreadSummary | undefined): Pick<Pu
|
||||
const question = questions[0];
|
||||
if (phase !== "waiting_for_input" || questions.length !== 1 || !question || question.multiSelect) return {};
|
||||
if (question.options.length === 0 && !question.allowCustomAnswer) return {};
|
||||
// A cut value would answer with a string that matches no option.
|
||||
if (question.options.some(({ value }) => value.length > OPTION_VALUE_CHARS)) return {};
|
||||
return {
|
||||
question: {
|
||||
id: question.id,
|
||||
options: question.options.slice(0, 4).map(({ label, value }) => ({ label: trimText(label, 40), value: value.slice(0, 200) })),
|
||||
options: question.options.slice(0, 4).map(({ label, value }) => ({ label: trimText(label, 40), value })),
|
||||
allowCustomAnswer: question.allowCustomAnswer,
|
||||
},
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user