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:
amosblomqvist
2026-10-04 23:57:00 +03:00
parent 4640117ba7
commit 85022ae143
9 changed files with 544 additions and 17 deletions
+11 -2
View File
@@ -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,
);
}