parallel question gate: an answer that leaves a sibling dangling defers its wake (deepseek batch incident fix, owner 2026-10-03)
The incident: deepseek batched quiz + ask_user_question in one assistant message; the quiz executed first (sequential) and terminated the turn; the answer to the quiz IMMEDIATELY woke a reaction turn (fork-1 design assumed one dangling question max), the model complained about the unanswered ask, and the ask became permanently unanswerable — its toolResult could only append AFTER the intervening assistant message, a shape DeepSeek rejects. The gate: for answer inputs, if a question call still dangles after the append, persist write-ahead + emit record-changed + return the settled terminal — no turn. The dock advances to the sibling; the LAST answer fires the one reaction turn with every result adjacent to its calling message. Nothing else generates while a question dangles (job wakes wait, continuation folds refuse, the composer gates), so adjacency holds by construction. Also repaired the stranded session data (ask call stripped from the batch; backup at /tmp/chat_murkbeq2_backup.json).
This commit is contained in:
@@ -28,7 +28,9 @@ import { modelsNotConfigured, providerKeyMissing as providerKeyMissingEnvelope,
|
||||
import { beginTurn, endTurn } from "./turn-liveness.ts";
|
||||
import { registerTurn, unregisterTurn } from "./turn-runs.ts";
|
||||
import { persistTurn, triggerExtraction } from "./turn-service.ts";
|
||||
import { hasProbeDone } from "../core/native-messages.ts";
|
||||
import { hasProbeDone, danglingQuestionOf } from "../core/native-messages.ts";
|
||||
import { emit } from "../core/events.ts";
|
||||
import { settledTurnResponse } from "./turn-runs.ts";
|
||||
import { asAgentMessages } from "../core/native-messages.ts";
|
||||
import {
|
||||
ChatInputError,
|
||||
@@ -228,6 +230,23 @@ export async function handleChatInput(
|
||||
throw err;
|
||||
}
|
||||
|
||||
// THE PARALLEL-QUESTION GATE (owner, 2026-10-03 — the deepseek parallel
|
||||
// batch incident): an answer that leaves a SIBLING question dangling
|
||||
// defers its wake. The dock advances to the next question (the client's
|
||||
// optimistic result + record-changed carry the view); the LAST answer
|
||||
// fires the one reaction turn with every result ADJACENT to its calling
|
||||
// assistant message — the provider shape law (a tool result appended
|
||||
// after an intervening assistant message is rejected, which stranded
|
||||
// the sibling question permanently pre-fix). Nothing else can generate
|
||||
// while a question dangles (job wakes wait, continuation folds refuse,
|
||||
// the composer gates), so adjacency holds by construction.
|
||||
if (input.type === "answer" && danglingQuestionOf([...record.messages, ...appends]) !== null) {
|
||||
const merged = [...record.messages, ...appends];
|
||||
stores.chatStore.saveMessages(chatId, merged);
|
||||
emit("record-changed", { chatId });
|
||||
return settledTurnResponse(); // the cheap terminal — no beginTurn, no turn
|
||||
}
|
||||
|
||||
// One turn per chat, ever. A second request while a turn runs means a
|
||||
// stale/remounted client raced a live one — letting it through would
|
||||
// double-run the teacher and clobber the first turn's persist.
|
||||
|
||||
Reference in New Issue
Block a user