Files
turnstone/tests
Patrick Buckley cdc1dbcc1d feat(sse): event-id cursor resume for fresh-connect in-flight tool batches
A fresh browser connect during a parallel tool batch (e.g. several
web_fetch) left completed siblings' tool blocks empty until a manual
refresh: each tool_result SSE event fires the instant a sibling
finishes, but the result messages persist only after the whole batch
returns, so a fresh connect replayed neither the already-fired event
(a fresh connect doesn't replay the ring buffer) nor a /history row.

Route the fresh connect through the same delta replay a reconnect
already uses. Persist the per-ws SSE ring-buffer high-water mark
(_event_id) onto each saved conversation row. /history returns the
committed snapshot up to a resolved-turn-boundary cursor and omits the
trailing executing in-flight turn; the client opens its initial SSE
with that cursor (Last-Event-ID) so the existing replay_ok path
fast-forwards the in-flight turn whole -- tool blocks, results, and
approve/plan prompts all rebuild from the ring buffer.

The cut sits at the last resolved-turn boundary (not max(saved
event_id)), so out-of-order result saves in the post-batch loop can't
move it or strand a sibling. Gated on buffer-liveness (can_replay_from):
reloaded / evicted / awaiting-approval cases keep the in-flight turn in
/history and return a null cursor, falling back to the synthetic
snapshot floor -- preserving the existing in-flight render and never
leaving a turn unrenderable.

- Migration 059: nullable event_id BIGINT on conversations + a
  (ws_id, event_id) index (keeps the cold-open high-water reseed a seek).
- save_message(event_id=) across the storage wrapper / protocol /
  sqlite / postgres backends; get_max_event_id; reconstruct_messages
  surfaces the _event_id side-channel.
- SessionUIBase: reseed _event_id from storage on construction (so the
  id space stays monotonic across restarts); can_replay_from() gate.
- make_history_handler: _resume_cursor_and_trim() + cursor in the
  response (WorkstreamHistoryResponse.cursor). The shared projection,
  export, and coord-rebuild paths are untouched.
- app.js: seed the resume cursor on the initial-connect path only, and
  gate the last_event_id param on != null so a cursor of 0 (a brand-new
  workstream's first-turn boundary) is not dropped.

Tests: helper, storage round-trip, and seed unit tests; two
make_history_handler integration tests (cursor + orphan-trim when
replayable, null cursor + orphan kept when not); app.js static guards.
Migration applies up and down on SQLite.
2026-05-30 04:23:11 -07:00
..