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:
@@ -50,10 +50,14 @@ export function AbortTurnBody({
|
||||
mode,
|
||||
onClose,
|
||||
onConfirm,
|
||||
busy = false,
|
||||
}: {
|
||||
mode: AbortMode;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
/** The dismiss doubles as the abort when a turn is still running
|
||||
* (owner, 2026-10-04): the line says so. */
|
||||
busy?: boolean;
|
||||
}) {
|
||||
const copy = COPY[mode];
|
||||
return (
|
||||
@@ -75,7 +79,10 @@ export function AbortTurnBody({
|
||||
<h2 className="text-sm font-semibold text-card-foreground" data-testid="abort-turn-title">
|
||||
{copy.title}
|
||||
</h2>
|
||||
<p className="mt-1.5 text-xs text-muted-foreground">{copy.line}</p>
|
||||
<p className="mt-1.5 text-xs text-muted-foreground" data-testid="abort-turn-line">
|
||||
{copy.line}
|
||||
{busy && mode !== "turn" ? " The working turn stops too." : ""}
|
||||
</p>
|
||||
<div className="mt-4 flex justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
@@ -104,11 +111,13 @@ export function AbortTurnDialog({
|
||||
mode,
|
||||
onClose,
|
||||
onConfirm,
|
||||
busy = false,
|
||||
}: {
|
||||
/** What Esc is aborting; null = closed (nothing rendered). */
|
||||
mode: AbortMode | null;
|
||||
onClose: () => void;
|
||||
onConfirm: (mode: AbortMode) => void;
|
||||
busy?: boolean;
|
||||
}) {
|
||||
// Esc cancels the popup (claimed — never a chained abort).
|
||||
useEffect(() => {
|
||||
@@ -125,7 +134,7 @@ export function AbortTurnDialog({
|
||||
|
||||
if (mode === null || typeof document === "undefined") return null;
|
||||
return createPortal(
|
||||
<AbortTurnBody mode={mode} onClose={onClose} onConfirm={() => onConfirm(mode)} />,
|
||||
<AbortTurnBody mode={mode} onClose={onClose} onConfirm={() => onConfirm(mode)} busy={busy} />,
|
||||
document.body,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user