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:
amosblomqvist
2026-09-05 21:33:13 +03:00
parent e6409036a9
commit 2f8cf71f52
5 changed files with 287 additions and 10 deletions
+14 -2
View File
@@ -87,8 +87,20 @@ export function setJobRespawn(respawn: JobRespawn): void {
const POLL_MS = 400;
/** After the gates open, wait a beat before firing — a client that just
* answered a question is about to send its continuation; don't 409 it. */
const GRACE_MS = 1500;
* answered a question is about to send its continuation; don't 409 it.
*
* Evidence for 750: the race window is exactly the learner's answer→POST
* flight — sendAutomaticallyWhen dispatches synchronously on addToolOutput,
* the answer hits the record only at the write-ahead persist, and beginTurn
* flips the turn registry immediately after, all before the grace can fire
* (measured localhost RTT ~10ms; typical LAN/production RTT <300ms). 750ms
* is 2.5× the worst realistic flight. A miss self-heals: the pump rechecks
* the gates after the grace AND the wake pipeline retries 409s with a
* backoff ramp — and since the transport's merge-retry (chat-transport.ts)
* made client 409s non-fatal, even a pathological RTT degrades gracefully.
* (Was 1500 — pure guess; 750 claws back 750ms of post-answer delivery
* latency without weakening the guard.) */
const GRACE_MS = 750;
/** 409 backoff: capped ramp so a busy client can't spin the pump. */
const RETRY_BACKOFF_MS = [500, 1000, 2000, 5000] as const;