From ddce919f9ba7a278e8dd16f4de4659a9fcc85e8b Mon Sep 17 00:00:00 2001 From: amosblomqvist Date: Tue, 29 Sep 2026 21:03:46 +0300 Subject: [PATCH] =?UTF-8?q?practice=20live-test=20fixes=20=E2=80=94=20scro?= =?UTF-8?q?ll=20containers,=20session=20math=20delimiters,=20header=20guid?= =?UTF-8?q?ance,=20sweep=20heartbeat=20(owner=20live=20test=202026-09-29)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/practice-exam.tsx | 4 ++- components/practice-list.tsx | 4 ++- src/agent/subagents/practice-pipeline.ts | 32 +++++++++++++++++++++--- src/agent/subagents/practice.ts | 4 +-- tests/practice-agents.test.ts | 4 +-- 5 files changed, 38 insertions(+), 10 deletions(-) diff --git a/components/practice-exam.tsx b/components/practice-exam.tsx index d661403..0a87f76 100644 --- a/components/practice-exam.tsx +++ b/components/practice-exam.tsx @@ -85,7 +85,8 @@ export function PracticeExamBody({ const answerable = set.problems.filter((p) => !p.grade); const anyDraft = answerable.some((p) => (drafts[p.id] ?? p.answer ?? "") !== ""); return ( -
+
+

{set.title}

@@ -177,6 +178,7 @@ export function PracticeExamBody({ )}

)} +
); } diff --git a/components/practice-list.tsx b/components/practice-list.tsx index 338b2c3..d996afa 100644 --- a/components/practice-list.tsx +++ b/components/practice-list.tsx @@ -35,7 +35,8 @@ export function PracticeListBody({ onNew: () => void; }) { return ( -
+
+

Practice

+
); } diff --git a/src/agent/subagents/practice-pipeline.ts b/src/agent/subagents/practice-pipeline.ts index fa1feef..88e269d 100644 --- a/src/agent/subagents/practice-pipeline.ts +++ b/src/agent/subagents/practice-pipeline.ts @@ -232,10 +232,11 @@ export function createPracticePipeline(deps: PracticePipelineDeps) { // 1. ONCE PER PROCESS (globalThis flag) — an instance could otherwise // evaluate against a registry whose fresh row it can't observe // (dev route bundles, restart paths). - // 2. GRACE WINDOW (120s over updatedAt ?? createdAt) — a live run - // saves at every phase/problem transition, so its heartbeat is - // structurally never 120s stale; a true orphan is minutes stale at - // the next boot. + // 2. GRACE WINDOW (120s over updatedAt ?? createdAt) — a live run's + // WATCHDOG re-stamps the file every 30s (cross-worker liveness — + // the registry is NOT shared between dev route workers), so a + // live run is structurally never 120s stale; a true crash stops + // the beats and is minutes stale at the next boot. // 3. Live-row check (globalThis-shared registry) as before. const sweepFlag = globalThis as { __lhPracticeSweepDone?: boolean }; if (!sweepFlag.__lhPracticeSweepDone) { @@ -275,6 +276,26 @@ export function createPracticePipeline(deps: PracticePipelineDeps) { ): Promise { const controller = partitionRow.controller; + // PERSISTENT HEARTBEAT (owner live test #2, 2026-09-29 — the sweep + // stamped LIVE runs again): dev serves routes from workers that do + // NOT share this process's registry, so memory liveness cannot + // protect a cross-worker sweep, and a long-running last problem can + // exceed the 120s grace between store writes. The watchdog + // re-stamps updatedAt on the FILE every 30s while THIS run is live + // (4-beat tolerance inside the grace window) — any process's sweep + // then sees a fresh heartbeat; a true crash stops the beats and the + // orphan goes stale honestly. + const beat = setInterval(() => { + void withSetLock(set.id, () => { + const cur = deps.practiceStore.getSet(set.id); + if (!cur) return; + if (cur.status !== "partitioning" && cur.status !== "generating") return; + deps.practiceStore.saveSet(cur); + }); + }, 30_000); + beat.unref?.(); + + try { // Stage 1 — the partitioner (its OWN dock row; acquires a shared // permit like every background LLM job). let partitions: Array<{ chains: string[] }>; @@ -393,6 +414,9 @@ export function createPracticePipeline(deps: PracticePipelineDeps) { cur.error = undefined; deps.practiceStore.saveSet(cur); }); + } finally { + clearInterval(beat); + } } /** diff --git a/src/agent/subagents/practice.ts b/src/agent/subagents/practice.ts index 0ef6eb1..45f7227 100644 --- a/src/agent/subagents/practice.ts +++ b/src/agent/subagents/practice.ts @@ -188,7 +188,7 @@ INPUTS (in the user message): THE CUE DISTANCE: 0 Lesson context · 1 Adjacent context · 2 Far context — how far the situation sits from the cues the learner has already met: the lesson's own examples AND every previous problem's situation. Far context is the edge of the recognizable band — the proposition must still genuinely apply, and the learner must be able to find it by looking. THE PRESENTATION: Theory (the subject's own terms — symbols, formal objects, native vocabulary) or Application (a concrete worldly affair in which the proposition's structure appears in the wild). It dresses the situation; it never changes the rung or the cue distance. Apply Part II step 2: degrade honestly if the target cannot support them — the degradation shows in the problem's own shape, never in meta-commentary. -- FORMATTING: your statement, solution, and rubric are rendered as full Markdown — use everything it offers. Mathematics is always LaTeX: inline as $…$, display as $$…$$ on its own lines. Code, pseudocode, and formal systems live in fenced code blocks with a language tag; identifiers and symbols inline as \`code\`. Use tables for parallel structure and comparisons, lists for enumerations. Prose-only renderings of mathematical or structured content are a defect: if a sentence carries an equation, a derivation, or a piece of code, format it as what it is. +- FORMATTING: your statement, solution, and rubric are rendered as full Markdown — use everything it offers. Mathematics is always LaTeX: inline as \\(…\\), display as \\[…\\] on its own lines — a bare $ is literal currency and never starts math. Inside math, use LaTeX commands for symbols, not unicode stand-ins (they fail to render). Code, pseudocode, and formal systems live in fenced code blocks with a language tag; identifiers and symbols inline as \`code\`. Use tables for parallel structure and comparisons, lists for enumerations. Skip headers — at this size they are pointless decoration; structure with paragraphs, bold labels, and lists (a genuinely long derivation may earn one). Prose-only renderings of mathematical or structured content are a defect: if a sentence carries an equation, a derivation, or a piece of code, format it as what it is. - PREVIOUS PROBLEMS: problems the learner has already answered, with the chains they exercised. Do not rebuild one; keep your situation distant from theirs — cue distance counts previous problems' situations (Part I, "Cue distance"). OUTPUT — your entire deliverable is ONE submit_problem call: @@ -228,7 +228,7 @@ THE VERDICT (exactly one): - "partial": some of the target came to mind; the answer is materially incomplete or partly wrong. - "missed": the targeted proposition(s) did not come to mind — the answer is empty or on the wrong track. -Feedback renders as full Markdown. Mathematics is always LaTeX — inline as $…$, display as $$…$$ on its own lines. Code in fenced blocks with a language tag, identifiers inline as \`code\`. Lists and tables wherever they carry structure. Name the failed recall and teach the boundary in properly formatted prose, not plain text. +Feedback renders as full Markdown. Mathematics is always LaTeX — inline as \\(…\\), display as \\[…\\] on its own lines (a bare $ is literal currency). Inside math, LaTeX commands for symbols, not unicode stand-ins. Code in fenced blocks with a language tag, identifiers inline as \`code\`. Lists and tables wherever they carry structure; no headers — feedback is short-form, structure it with paragraphs and bold labels. Name the failed recall and teach the boundary in properly formatted prose, not plain text. OUTPUT — your entire deliverable is ONE submit_grade call: "verdict" and "feedback" (non-empty; the boundary-teaching text above).`; diff --git a/tests/practice-agents.test.ts b/tests/practice-agents.test.ts index cb8cd3a..7d49128 100644 --- a/tests/practice-agents.test.ts +++ b/tests/practice-agents.test.ts @@ -294,7 +294,7 @@ describe("formatting doctrine pins", () => { it("GENERATOR_DOCTRINE carries the FORMATTING bullet verbatim (Markdown + LaTeX + fenced code + tables/lists)", () => { assert.ok( GENERATOR_DOCTRINE.includes( - "- FORMATTING: your statement, solution, and rubric are rendered as full Markdown — use everything it offers. Mathematics is always LaTeX: inline as $…$, display as $$…$$ on its own lines. Code, pseudocode, and formal systems live in fenced code blocks with a language tag; identifiers and symbols inline as `code`. Use tables for parallel structure and comparisons, lists for enumerations. Prose-only renderings of mathematical or structured content are a defect: if a sentence carries an equation, a derivation, or a piece of code, format it as what it is.", + "- FORMATTING: your statement, solution, and rubric are rendered as full Markdown — use everything it offers. Mathematics is always LaTeX: inline as \\(…\\), display as \\[…\\] on its own lines — a bare $ is literal currency and never starts math. Inside math, use LaTeX commands for symbols, not unicode stand-ins (they fail to render). Code, pseudocode, and formal systems live in fenced code blocks with a language tag; identifiers and symbols inline as `code`. Use tables for parallel structure and comparisons, lists for enumerations. Skip headers — at this size they are pointless decoration; structure with paragraphs, bold labels, and lists (a genuinely long derivation may earn one). Prose-only renderings of mathematical or structured content are a defect: if a sentence carries an equation, a derivation, or a piece of code, format it as what it is.", ), "the exact FORMATTING sentence", ); @@ -312,7 +312,7 @@ describe("formatting doctrine pins", () => { it("GRADER_INSTRUCTIONS carries the formatting paragraph before the OUTPUT section", () => { assert.ok( GRADER_INSTRUCTIONS.includes( - "Feedback renders as full Markdown. Mathematics is always LaTeX — inline as $…$, display as $$…$$ on its own lines. Code in fenced blocks with a language tag, identifiers inline as `code`. Lists and tables wherever they carry structure. Name the failed recall and teach the boundary in properly formatted prose, not plain text.", + "Feedback renders as full Markdown. Mathematics is always LaTeX — inline as \\(…\\), display as \\[…\\] on its own lines (a bare $ is literal currency). Inside math, LaTeX commands for symbols, not unicode stand-ins. Code in fenced blocks with a language tag, identifiers inline as `code`. Lists and tables wherever they carry structure; no headers — feedback is short-form, structure it with paragraphs and bold labels. Name the failed recall and teach the boundary in properly formatted prose, not plain text.", ), "the exact grader paragraph", );