question dismissal, A2: the silent write + the dismissal pause (owner design 2026-10-03)

- {type:"dismiss"} input event: parse + pure appends with the answer-event
  orphan discipline (dangling-call 400, kind match, answer must be dismissed)
- POST /api/chats/[id]/dismiss: the no-turn composition — write-ahead append,
  409 while a turn runs, emits record-changed (new bus word; idle views sync)
- needsContinuation: an all-dismissed trailing batch demands nothing;
  endsInDismissal = the derived pause predicate (tail-based, no latch)
- wake pipeline: while the record ends in a dismissal the pump holds every
  delivery in the steer queue — the user's next send is the wake (until the
  text-turn steer claim lands, settlement re-pump delivers right after it)
- session-core.dismissQuestion: optimistic skipped card + silent POST;
  chat.tsx confirmAbort question-mode swaps submitAnswer for it; ESC-abort
  popup everywhere (AbortTurnDialog) with popup-Esc claimed via state gate
- parallel question calls dismiss call-by-call (results bind by id)
This commit is contained in:
amosblomqvist
2026-10-03 01:51:13 +03:00
parent 3e5b857c63
commit 673a430d6e
12 changed files with 409 additions and 12 deletions
+24
View File
@@ -513,6 +513,30 @@ export class SessionModel {
return this.send({ type: "answer", toolCallId, answer: output });
}
/** The dismissal path (owner design 2026-10-03, "A2"): the skipped
* toolResult lands optimistically, then the SILENT route persists it
* — no turn, no stream, nothing wakes. The session pauses at the
* user's choice; the next plain send is the wake. On failure the
* durable record wins: the error surfaces, the record-changed sync
* (or any later refetch) replaces the optimistic append with truth. */
async dismissQuestion(toolCallId: string, answer: QuestionAnswerData): Promise<void> {
if (!this.readyRef) return;
const tool = pendingCallTool([...this.base, ...this.tail], toolCallId) ?? "quiz";
this.mutate(() => {
this.tail = [...this.tail, { ...newAnswerResult(toolCallId, tool, answer), id: localId() }];
});
try {
const res = await this.fetch(`/api/chats/${this.chatId}/dismiss`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ input: { type: "dismiss", toolCallId, answer } }),
});
if (!res.ok) throw new Error(`The dismissal failed (${res.status}).`);
} catch (err) {
this.setError((err as Error).message);
}
}
/** Local detach: cancel the consume, keep the partial tail. The page's
* settle watch adopts the record when the server turn finishes. */
stop(): void {