fix: the handoff freeze + the free-form mermaid pass
Two user-reported problems from the muscle-protein-synthesis session, both diagnosed from the live data (chat_mtxeml9e_6118e59f). The freeze: after probe_done, the open session never showed the teacher's wake turn — only navigating away and back loaded it. Not an agent-switch problem: the server-side handoff worked perfectly (write-ahead persist, liveness registry, the sidebar's busy dot saw the wake the whole time). The gap was client attach: the only mid-session wake detector the open page has is the jobs poll, and probe-phase sessions have zero job traffic, so that poll is OFF; the post-turn adoption poll read turnInFlight=false at ~0ms (the wake fires at ~0.75-1.2s, after the pump's grace), adopted, and stopped. Fix: the post-turn adoption effect now keeps a short WATCH WINDOW after adopting (~4s, 8x500ms) — if turnInFlight flips true, it hands off to the existing wake-adoption poll (busy shimmer, adopt at finish). The mermaid failures: four failed plan_init calls, all caused by ONE hidden rule — underscores are forbidden in node ids (mps_translation, net_balance, per_meal). The id regex rejected '_' BEFORE the slug check, so the specific, actionable message never fired; the generic 'can't read the edge — expected a[title] --> b[title]' lied (the syntax was exactly that), and the teacher spent three attempts hunting punctuation it suspected (colons, commas, multi-word labels) — visible in its own reasoning trail. Per the user's principle — enforce only the slugs, free everything else: underscores are first-class slug chars; the id CANDIDATE class is broad so every bad id reaches the specific slug message; chained edges (a --> b --> c) expand to pairs; ==>, -.-> are edge synonyms; any direction header accepted (we layout ourselves); id["title"] quote pairs are stripped. The tool description, zod describe, and the teacher's planning prompt now state the one strict rule instead of the old subset liturgy. Verified: tsc, 305/305 (the exact muscle-protein mermaid is now a passing test), lint, build; real-browser puppeteer smoke on live GLM — the teacher's wake turn appeared in the open page ~50s after the probe concluded with ZERO navigation, verdict gate + first teach check + plan window all live in-page (the single console 404 is the app's missing favicon, environmental noise).
This commit is contained in:
+50
-3
@@ -238,17 +238,64 @@ describe("parseMermaidPlan (the constrained mermaid subset)", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("rejects: non-slug ids, self-loops, chained edges, subgraphs, missing header, empty", () => {
|
||||
it("rejects: non-slug ids (with the SPECIFIC message), self-loops, subgraphs, missing header, empty", () => {
|
||||
// The muscle-protein post-mortem (2026-09-11): underscore ids died at
|
||||
// the regex layer with a generic message, and the teacher spent four
|
||||
// plan_init calls hunting punctuation the parser didn't care about.
|
||||
// Now EVERY bad id reaches the slug check — the message names the id.
|
||||
assert.ok(!parse("graph TD\n Foo Bar[x]\n").ok, "id with a space");
|
||||
assert.match(parse("graph TD\n Foo Bar[x]\n").problems[0]!, /line 2.*slug/);
|
||||
assert.match(parse("graph TD\n mps.translation[x]\n").problems[0]!, /node id `mps.translation` must be a lowercase slug/);
|
||||
assert.match(parse("graph TD\n MPS[x]\n").problems[0]!, /node id `MPS` must be a lowercase slug/);
|
||||
assert.match(
|
||||
parse("graph TD\n a[Alpha] --> mps.translation[Beta]\n").problems[0]!,
|
||||
/node id `mps.translation` must be a lowercase slug/,
|
||||
"bad id inside an edge also gets the specific message",
|
||||
);
|
||||
assert.ok(!parse("graph TD\n a --> a\n").ok, "self-loop");
|
||||
assert.ok(!parse("graph TD\n a --> b --> c\n").ok, "chained edge");
|
||||
assert.match(parse("graph TD\n a --> b --> c\n").problems[0]!, /one edge per line/);
|
||||
assert.ok(!parse("graph TD\n subgraph cluster\n").ok, "subgraph");
|
||||
assert.ok(!parse("digraph G\n a\n").ok, "wrong header");
|
||||
assert.ok(!parse("%% only a comment\n").ok, "empty plan");
|
||||
});
|
||||
|
||||
it("free-form pass (2026-09-11): underscores, chains, arrow synonyms, any direction, quoted titles", () => {
|
||||
// Underscores are first-class slug chars — kept verbatim, referenced
|
||||
// verbatim by quiz nodeId.
|
||||
const us = parse("graph TD\n mps_translation[Ribosomes build protein] --> net_balance[Net balance]\n");
|
||||
assert.ok(us.ok, "underscore ids");
|
||||
assert.deepEqual(us.plan!.nodes, [
|
||||
{ id: "mps_translation", title: "Ribosomes build protein" },
|
||||
{ id: "net_balance", title: "Net balance" },
|
||||
]);
|
||||
// The exact shape the muscle-protein teacher wrote (attempt 1):
|
||||
// quoted titles with colons, commas, equals signs — must just work.
|
||||
const quoted = parse(
|
||||
'graph TD\n mps_translation["MPS = ribosomes building protein"] --> per_meal["Per-meal dose: ~20-40 g saturates, excess oxidized"]\n',
|
||||
);
|
||||
assert.ok(quoted.ok, "quoted punctuation-rich titles");
|
||||
assert.deepEqual(quoted.plan!.nodes, [
|
||||
{ id: "mps_translation", title: "MPS = ribosomes building protein" },
|
||||
{ id: "per_meal", title: "Per-meal dose: ~20-40 g saturates, excess oxidized" },
|
||||
]);
|
||||
// Chains expand to consecutive pairs.
|
||||
const chain = parse("graph TD\n a[Alpha] --> b[Bee] --> c[See]\n");
|
||||
assert.ok(chain.ok, "chained edges");
|
||||
assert.deepEqual(chain.plan!.edges, [
|
||||
{ from: "a", to: "b" },
|
||||
{ from: "b", to: "c" },
|
||||
]);
|
||||
// Arrow synonyms ==>, -.-> mean the same as -->.
|
||||
for (const arrow of ["==>", "-.->"]) {
|
||||
const res = parse(`graph TD\n a ${arrow} b\n`);
|
||||
assert.ok(res.ok, arrow);
|
||||
assert.deepEqual(res.plan!.edges, [{ from: "a", to: "b" }]);
|
||||
}
|
||||
// Any direction header (we layout ourselves).
|
||||
for (const header of ["graph LR", "flowchart RL", "graph BT"]) {
|
||||
assert.ok(parse(`${header}\n a --> b\n`).ok, header);
|
||||
}
|
||||
});
|
||||
|
||||
it("the first label wins when a node line repeats (relabeling mid-block is ignored)", () => {
|
||||
const res = parse("graph TD\n a[First label]\n a[Second label]\n");
|
||||
assert.deepEqual(res.plan!.nodes, [{ id: "a", title: "First label" }]);
|
||||
|
||||
Reference in New Issue
Block a user