Skip to content

AA04: Reattach ring buffer and chat.resume - #856

Merged
LinuxDevil merged 1 commit into
mainfrom
fb-aa04-reattach
Nov 3, 2026
Merged

LinuxDevil merged 1 commit into
mainfrom
fb-aa04-reattach

Conversation

@LinuxDevil

Copy link
Copy Markdown
Owner

What changed

A client that loses its socket (or reloads) mid-turn reconnects, sends chat.resume, gets the missed events of a running turn, and otherwise reconciles from the stored transcript. Settled turns are never replayed.

  • shared/authorWire.ts: chat.resume gains an optional afterSeq cursor; new AuthorSessionRecord extends AuthorSession { runningRequestId } is what GET /assistant/sessions/:id now returns.
  • server/author/replayBuffer.ts (new): per-session ring of the last turn's AuthorServerEvents. turn.started resets the entry; overflow past REPLAY_CAPACITY (4000) evicts the oldest text.deltas first so every tool.* survives; a settled entry answers only its terminal event and is evicted lazily after REPLAY_SETTLED_TTL_MS (120 s).
  • server/author/authorStream.ts: one ReplayBuffer per server records every runner event before broadcast; chat.resume is answered on the asking socket only — replay sends the buffered events with seq > afterSeq (live events follow on the same socket), settled sends only the terminal event, unknown sends an UNKNOWN_REQUEST turn.error (seq 0). Malformed afterSeq is a BAD_MESSAGE.
  • server/author/routes.ts: GET /assistant/sessions/:id returns AuthorSessionRecord with runningRequestId from runner.running(id); runtimeClient.getAuthorSession retyped accordingly.
  • src/author/reattach.ts (new): the pure helpers the chat hook will drive — streamEndRecovery (stoppedByUser → none, BUSY → busy, drop mid-stream → drop), acceptEvent seq dedupe (seq 0 always applies; new requestId always applies; same requestId only past lastSeq), pending-request localStorage helpers with try/catch around every access, and the reattach timeout/retry/gave-up constants.
  • Removed four stale fallow-ignore-next-line unused-export suppressions in src/author/authorStream.ts that fallow reports as stale (the exports are used by the existing test entry point).

How it was tested

  • server/author/__tests__/replayBuffer.test.ts (new, 7 tests): replay-after-cursor, unknown requestId/session/replaced turn, settled → terminal only (both turn.finished and turn.error), settled TTL expiry at exactly 120 s + 1 (fake clock via the now seam), running turns don't expire, and a 5 101-event overflow keeps all 2 500 tool.* events while dropping the oldest 1 101 text.deltas.
  • server/__tests__/authorStream.test.ts (extended, 8 tests): a gated provider parks a turn mid-stream; the client drops after seq 3, reconnects, and chat.resume { afterSeq: 3 } delivers seqs 4–5 from the buffer then 6–7 live with no duplicates; a resume after settle delivers only the terminal event; an unknown requestId gets UNKNOWN_REQUEST. Also asserts GET /assistant/sessions/:id reports runningRequestId while the turn runs and null after.
  • src/author/__tests__/reattach.test.ts (new, 12 tests): the streamEndRecovery table, acceptEvent dedupe cases, storage round-trip/malformed data, and a throwing localStorage.
  • server/__tests__/authorSessions.test.ts: updated the GET /:id body assertion for the new runningRequestId field.

Command results:

  • Studio npm run typecheck ✓ · npm run typecheck:server ✓ · npm test -- --run ✓ 968 passed · npm run test:server ✓ 1012 passed · npm run lint ✓ (0 errors)
  • Root npx tsc --noEmit ✓ · npm run lint ✓ · npm run test:coverage ✓ 7163 passed · npm run fallow ✓ · npm run docs:ratchet ✓ · npm run api:check ✓ · npm run docs:llms:check ✓ · npm run size-check ✓

Acceptance criteria

  • Reconnect mid-turn + chat.resume { afterSeq: 3 } delivers events 4…n then the live tail, with no duplicates.
  • Resume after the turn settled sends only the terminal event (no text.delta, no tool.*).
  • Resume with an unknown requestId → UNKNOWN_REQUEST.
  • A settled entry older than 120 s answers unknown.
  • Capacity overflow drops text.delta first and keeps every tool.* event.

Generated with Devin

A client that loses its socket mid-turn reconnects, sends chat.resume,
and gets the missed events of a running turn; a settled turn is never
replayed (it could resurrect a card saved later).

- shared/authorWire: chat.resume gains optional afterSeq; GET
  /assistant/sessions/:id now returns AuthorSessionRecord
  (AuthorSession + runningRequestId)
- server/author/replayBuffer.ts: per-session ring of the last turn's
  events (REPLAY_CAPACITY 4000, evicting text.delta first); settled
  entries answer only the terminal event and expire after 120 s
- server/author/authorStream.ts: records every runner event and answers
  chat.resume (replay tail / terminal event / UNKNOWN_REQUEST)
- server/author/routes.ts: GET /:id returns the record; runtimeClient
  getAuthorSession typed accordingly
- src/author/reattach.ts: streamEndRecovery, acceptEvent seq dedupe,
  pending-request localStorage helpers, reattach constants

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@LinuxDevil
LinuxDevil merged commit e587420 into main Nov 3, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant