Wave 2: 409 merge-retry transport, dead pendingJob code, evidence-based timing, prompt-size guard
- components/chat-transport.ts (NEW): createChatTransport(chatId) — the chat.tsx seam is now a one-line swap. Its fetch turns a 409 on POST /api/chat (a learner send racing the wake pipeline's ~1s adoption window) into: wait out the in-flight turn (poll turnInFlight, 30s cap), refetch the record, MERGE the outgoing messages onto the fresh server list, re-POST. A bare retry would clobber the steer + wake messages via the write-ahead persist (steerDelivered makes that loss permanent) — merging is the whole point. Unit-tested: pass-through, wait+merge+reorder, dedupe by id, repeated-409 loop, bounded give-up. - src/core/pending.ts: findPendingJobCall deleted (dead since wave 1 — job tools resolve server-side with an ack, nothing pends client-side). JOB_TOOLS kept (runner.ts still uses it). Header doc rewritten. - src/server/jobs.ts: GRACE_MS 1500 → 750, with the evidence in the comment: the race window is only the learner's answer→POST flight (sendAutomaticallyWhen dispatches synchronously; ~10ms localhost, <300ms realistic RTT — 750 is 2.5× worst case) and misses self-heal via the pump's gate recheck, the 409 backoff ramp, and the new transport merge-retry. POLL_MS=400 kept (sampling granularity only). - tests/jobs.test.ts: prompt-size guard — every fragment ≤8k chars, composed prompt ≤60k — the README-leak class (815 bytes ≈ 200 tokens per teacher turn before wave 1's fix) can't silently recur. Gates: typecheck, 279/279 tests, lint, build — all green.
This commit is contained in:
+14
-1
@@ -700,7 +700,20 @@ describe("prompt composition", () => {
|
||||
assert.ok(instructions.startsWith(base), "base prompt first");
|
||||
for (const f of fs.readdirSync("src/agent/prompts").filter((f) => /^\d+-[\w.-]+\.md$/.test(f)).sort()) {
|
||||
const body = fs.readFileSync(`src/agent/prompts/${f}`, "utf-8").trim();
|
||||
assert.ok(instructions.includes(body), `fragment ${f} must compose verbatim`);
|
||||
assert.ok(instructions.includes(body), `fragment ${f} must compose verbatim`);
|
||||
}
|
||||
});
|
||||
|
||||
it("guards prompt size — no fragment can silently balloon (the README-leak class)", () => {
|
||||
// The wave-1 leak: README.md (815 bytes / ~200 tokens) rode into every
|
||||
// teacher turn until the numeric-prefix filter stopped it. This guard
|
||||
// catches the sibling failure mode — an accidental dump pasted into a
|
||||
// fragment. Thresholds: largest real fragment is 40-demos.md at ~3.2k
|
||||
// chars; 8k is generous headroom. Composed total ≤ 60k chars.
|
||||
for (const f of fs.readdirSync("src/agent/prompts").filter((f) => /^\d+-[\w.-]+\.md$/.test(f))) {
|
||||
const body = fs.readFileSync(`src/agent/prompts/${f}`, "utf-8").trim();
|
||||
assert.ok(body.length <= 8_000, `fragment ${f} is ${body.length} chars — over the 8k guard`);
|
||||
}
|
||||
assert.ok(composeInstructions().length <= 60_000, "composed system prompt over the 60k guard");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user