tools: the standard argument heal — every registry, both seams
The whitehead incident (2026-10-09): glm-5.3 emitted quiz type with
quotes baked in ('"predict"') — rejected ten times, byte-identical
retries, the lesson never started — and correctAnswer with the JSON
key-value colon glued on ([':parmenides']). Third GLM wobble incident,
and each prior heal was wired only where its incident landed; this one
landed precisely where the practice heal never reached.
Layer 1 — healEncodedArguments moves to core/argument-heal (the display
seam needs it too — the tool-parts law) with withArgumentHeal +
healToolRecord (agent/tools/heal) wired at EVERY registry: prober,
teacher, practice, bareQuestionTools (it had no prepareArguments at
all), demo-builder, and submit-agent (composing after
healStringifiedArrays; the auditor's extraTools wrapped).
Layer 2 — stripModelValueDebris (core/value-debris): leading/trailing
colons, quotes, backticks, whitespace strip at the MEMBERSHIP seams —
coerceCorrectAnswer (boundary check + display + grading in one),
normalizeQuizType (persisted wobbled records keep their role),
resolveTargetId (copy-syntax on targets).
Layer 3 — schema description hygiene: the quiz type and practice
mode/outcome/rung/missClass values now read as bare words — the quoted
JSON-key style invited quote-copying.
Display mirror — parseQuizInput heals the raw record on read, so a
boundary-healed quiz whose record keeps raw args renders instead of
becoming an invisible pending quiz (learner lockout).
Pins: tests/tool-heal.test.ts (the incident shapes end-to-end through
pi-ai validation, the wiring, the strips, the mirror). The stringy-
options pins flip to boundary-parity per tool-parts' own law; the
chat-flow garbage pin keeps its honest-reject intent with an unhealable
shape. 1471 tests, typecheck, lint green.
This commit is contained in:
+10
-10
@@ -689,20 +689,20 @@ describe("teacher chat flow (scripted model)", () => {
|
||||
assert.deepEqual(pending?.input, QUIZ_INPUT);
|
||||
});
|
||||
|
||||
it("a structurally invalid quiz (the live double-encoded options string) never reaches the dock and retries clean", async () => {
|
||||
// The exact live modal-realism crash shape: GLM occasionally serializes
|
||||
// a nested array as a JSON STRING inside the arguments. The structural
|
||||
// layer (JSON Schema: options must be array) rejects the call at the
|
||||
// boundary — it must surface as tool-input-error, never as a pending
|
||||
// question, and the model's retry must become the pending quiz.
|
||||
it("structurally invalid quiz input (unhealable garbage) never reaches the dock and retries clean", async () => {
|
||||
// The two-tier world (owner, 2026-10-09 — the universal argument
|
||||
// heal): a JSON-ENCODED stand-in for the schema shape (the live
|
||||
// string-options habit) now heals at EVERY registry — see
|
||||
// tests/tool-heal.test.ts for that pin — so the structural reject
|
||||
// this test holds is the OTHER tier: garbage no mechanical rule
|
||||
// could rescue (not JSON, not an array) rejects honestly at the
|
||||
// boundary — tool-input-error, never a pending question — and the
|
||||
// model's clean retry becomes the pending quiz.
|
||||
const STRINGY_QUIZ_INPUT = {
|
||||
question: "Lewis's indexical analysis of 'actual' — which is it?",
|
||||
type: "check",
|
||||
nodeId: "indexicals",
|
||||
options: JSON.stringify([
|
||||
{ label: "Like 'I'", value: "indexical" },
|
||||
{ label: "Like a title", value: "unique" },
|
||||
]), // ← the bug: a string, not an array
|
||||
options: "definitely not a JSON array — denim all the way down", // ← unhealable garbage
|
||||
correctAnswer: "indexical",
|
||||
explanation: "Indexical: every world is actual to its own inhabitants.",
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user