Commit Graph
3 Commits
Author SHA1 Message Date
amosblomqvist 79e7c8648b Stream reattachment client half: resume via same rendering path
Reattach to a turn that outlived its view through the SDK's native
resumeStream() so replay burst + live deltas flow through the exact
same processUIMessageStream chunk pipeline as first-subscriber
streaming — no second rendering dialect.

chat-transport.ts: ReattachingChatTransport subclasses
DefaultChatTransport and repoints reconnectToStream at GET
/api/chats/[id]/stream (the server's reattach endpoint, not the SDK
default /api/chat/{id}/stream). It peeks the first SSE event:
- turn-settled terminal: cancel body, fire onSettled (refetch), return
  null — the SDK treats null as no active stream, status stays ready.
- start chunk whose messageId equals the seeded last assistant message
  id (continuation case): rewritten to a fresh id so the resumed run
  mounts beside the seeded message instead of replacing its parts —
  settle-time record sync merges to the server's assembled message.

chat.tsx: mount effect resumes when initiallyInFlight (no once-ref:
React StrictMode's dev double-mount aborts the first attempt at
dispose — the retry must run). Mid-session discovery sites (jobs
poll, handoff watch) funnel into attachToRunningTurn().
turnInFlight is demoted to fallback liveness: the 1500ms record poll
self-heals a failed attach and clears a stale error banner on settle.
A status watcher clears the flag once the local stream represents
the turn. Stop/Esc unified in abortInFlightTurn: local stop() when a
stream runs here, always POST /api/activity/abort {kind:'turn'} with
409 not_running treated as a benign raced settle. Composer busy now
covers reattached turns (Stop square shows, send gated).

Live-verified on :3001 with real model turns: navigate away mid-turn
and back (replay + live deltas in-view, fallback poll disengaged),
Esc-abort of a reattached turn (partial persisted server-side,
composer restored), two concurrent sessions (raced-settle handled
via turn-settled + record refetch; live settle adopts the final
record), and no leaked stream readers after unmount.
2026-09-25 18:12:29 +03:00
amosblomqvist c014d22c9f Wire the 409 merge-retry transport into the chat (the one-line seam)
chat.tsx now uses createChatTransport(chatId): a send racing the wake
pipeline waits for the in-flight turn, merges onto the fresh server
record, and re-POSTs instead of surfacing the 409 banner. Also fixes the
transport's import to the core barrel (shell import law).

The optional "server-ahead heal" line in the jobs poller was considered
and skipped: the racing turn IS the wake turn, whose completion flips the
job to done within a poll tick or two — the existing settle-triggered
syncFromServer already heals the post-merge gap within ~1-2s. The extra
record fetch on every poll tick would double poll traffic for a path the
settle sync already covers.
2026-09-05 21:36:04 +03:00
amosblomqvist 2f8cf71f52 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.
2026-09-05 21:33:13 +03:00