practice hardening: weak-model argument heal + parked dismissals mid-turn
The GLM incident (2026-10-04): glm-5.3 sent mode as a JSON-string-encoded
enum ('"recall"') on 19 consecutive problem calls; TypeBox's error never
named the expected values, so the model re-emitted the identical payload
until the dev server was killed. And mid-turn the learner was trapped:
Esc's question/problem rungs only dismissed — the dismiss route 409'd
against the turn-owned record and nothing could stop the running turn.
- healEncodedArguments (practice.ts, the prepareArguments seam): a STRING
where the schema expects a literal union / boolean / null / number /
array is JSON.parse-d and kept when it satisfies the schema. Valid args
pass through untouched (same reference); unhealable values stay for
validation to reject honestly. Wired on BOTH problem and conclude.
- dismissal-park.ts: the dismiss route parks a mid-turn dismissal (202);
the turn's settle path (onFinish, after persistTurn, before endTurn —
the same ordering law as the record write itself) applies it against
the persisted record, only if the dismissed thing really is pending.
- confirmAbort: question/problem discard while busy now ALSO aborts the
in-flight turn; the dialog line says so (busy prop).
No error-result cap (owner ruling). 16 new pins (heal incl. pi-ai
validateToolArguments end-to-end; park registry, apply paths, the 202
route contract).
This commit is contained in:
+18
-8
@@ -358,24 +358,34 @@ export function Chat({
|
||||
abortInFlightTurn();
|
||||
return;
|
||||
}
|
||||
// From a pending question there is no turn to kill — abort IS
|
||||
// dismissal (A2, 2026-10-03): the skipped toolResult lands via the
|
||||
// SILENT route — no turn, no wake, nothing generates. The session
|
||||
// pauses at the user's choice; the next plain send is the wake.
|
||||
// From a pending question the dismiss is silent (A2, 2026-10-03):
|
||||
// the skipped toolResult lands via the SILENT route, no turn, no
|
||||
// wake — the next plain send is the wake. MID-TURN (owner,
|
||||
// 2026-10-04 — the GLM loop): the question can only dangle because a
|
||||
// turn is STILL RUNNING, so the discard ALSO kills that turn; the
|
||||
// route parks the dismissal (202) and the settle applies it.
|
||||
const p = pendingRef.current;
|
||||
if (p) {
|
||||
const question = (p.input as { question?: string } | null)?.question ?? "";
|
||||
const reading = readingThinkingRef.current.trim() || undefined;
|
||||
clearReadingThinking();
|
||||
const answer = dismissedAnswer(toolQuestionKind(p.tool), question);
|
||||
void dismissQuestion(p.toolCallId, reading ? { ...answer, readingThinking: reading } : answer);
|
||||
void dismissQuestion(
|
||||
p.toolCallId,
|
||||
reading ? { ...answer, readingThinking: reading } : answer,
|
||||
);
|
||||
if (busyRef.current) abortInFlightTurn();
|
||||
return;
|
||||
}
|
||||
// The practice discard (owner, 2026-10-04): ask-style — the
|
||||
// silent marker route; the conclude synthesis rides the user's
|
||||
// next input. The composer returns immediately.
|
||||
// next input. The composer returns immediately. Mid-turn the
|
||||
// discard also kills the running turn (same park + settle law).
|
||||
const problem = activeProblemRef.current;
|
||||
if (problem) void dismissProblem(problem.toolCallId);
|
||||
if (problem) {
|
||||
void dismissProblem(problem.toolCallId);
|
||||
if (busyRef.current) abortInFlightTurn();
|
||||
}
|
||||
},
|
||||
[abortInFlightTurn, dismissQuestion, dismissProblem],
|
||||
);
|
||||
@@ -1444,7 +1454,7 @@ export function Chat({
|
||||
<MermaidOverlay chart={mermaidFocus} onClose={() => setMermaidFocus(null)} />
|
||||
)}
|
||||
{planOpen && <PlanPopup messages={messages} onClose={() => setPlanOpen(false)} />}
|
||||
<AbortTurnDialog mode={abortAsk} onClose={() => setAbortAsk(null)} onConfirm={confirmAbort} />
|
||||
<AbortTurnDialog mode={abortAsk} onClose={() => setAbortAsk(null)} onConfirm={confirmAbort} busy={busy} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user