Skip to content

Keep agent chat turns alive across websocket disconnects, replay on reconnect - #4711

Merged
georgi merged 1 commit into
mainfrom
claude/agent-websocket-resilience-4aj056
Aug 4, 2026
Merged

Keep agent chat turns alive across websocket disconnects, replay on reconnect#4711
georgi merged 1 commit into
mainfrom
claude/agent-websocket-resilience-4aj056

Conversation

@georgi

@georgi georgi commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Problem

A dropped websocket aborted the in-flight chat/agent turn (disconnect()cancelChatTurn()), so a laptop lid-close or network blip killed the agent mid-work. The client had no way to see what happened while it was away: chat frames carried no sequence numbers and the server kept no event buffer.

What changed

Server — detachable chat-turn sessions (packages/websocket/src/chat-turn-registry.ts)

  • Each chat_message turn now runs inside a ChatTurnSession, keyed by user+thread in a process-wide registry. Every outbound frame is stamped with a monotonic chat_seq and kept in a bounded replay buffer (NODETOOL_CHAT_REPLAY_BUFFER_EVENTS, default 2000); delivery goes to whichever connection is attached.
  • On disconnect the session detaches instead of aborting: the agent keeps executing and buffering. A grace timer (NODETOOL_CHAT_DETACH_GRACE_MS, default 10 min) aborts a turn nobody reclaims. Sessionless inference turns and job behavior are unchanged.
  • New resume_chat {thread_id, last_seq} command: the server replies with a chat_resumed header (status running / finished / unknown, replay_incomplete when the buffer evicted the requested tail) and replays the missed frames, then live frames follow in seq order. Finished turns stay replayable for a retention window (NODETOOL_CHAT_REPLAY_RETENTION_MS, default 5 min); after that the client reconciles from persisted REST history.
  • tool_result / approval / stop frames arriving on the new connection are routed to the runner executing the adopted turn, and a detached turn's pending client tool calls survive the disconnect (replay re-delivers the tool_call frames so the reconnected client can still answer them).
  • Seq numbering continues across turns on a thread, so a stale last_seq can never skip newer frames. A new turn supersedes and aborts a detached one, matching existing single-turn semantics.

Web client

  • GlobalChatStore tracks the per-thread chat_seq high-water mark and, on every socket reopen, sends resume_chat for threads with a generation in flight (registered after the tools manifest so replayed tool_call frames find the manifest in place).
  • chat_resumed with status unknown or an incomplete replay resets the thread runtime to idle and reloads the thread's messages over REST.

Protocol

  • resume_chat added to UnifiedCommandType / command schemas; chat_resumed added to the outbound frame schemas. All schemas passthrough, so older clients ignore the new chat_seq field.

Tests

  • packages/websocket/tests/chat-turn-registry.test.ts — seq stamping, detach/attach replay ordering, stale-target guard, buffer eviction → replay_incomplete, supersede-abort, cross-turn seq continuity, detach-grace and retention timers.
  • packages/websocket/tests/unified-websocket-runner-chat-resume.test.ts — end-to-end: turn survives disconnect(), finishes unattended, replays on a fresh connection with no duplicates; resume_chat for an unknown thread; stop from a later connection aborts a detached turn.
  • web/src/core/chat/__tests__/chatResume.test.ts — cursor tracking and chat_resumed handling.
  • Full websocket suite (183 files / 2094 tests), protocol suite, and web chat/websocket suites pass; npm run lint and web+electron typecheck clean (mobile typecheck fails only on uninstalled Expo deps in this sandbox, unrelated).

Notes / scope

  • Workflow jobs keep their existing lifecycle (cancelled on disconnect, reconnect_job unchanged); this PR covers chat/agent turns. A chat turn that routes into a workflow run still loses that job on disconnect.
  • Resume is in-memory per page session: after a full reload the client rebuilds from REST history instead of replaying.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MWpARdXPyxfxgJnLR7SV6C


Generated by Claude Code

…econnect

A dropped socket used to abort the in-flight chat/agent turn
(disconnect() -> cancelChatTurn()): closing the laptop lid killed the
agent mid-work with nothing to reattach to. Now the turn detaches from
the socket and keeps executing, and a reconnecting client replays what
it missed.

Server: a new ChatTurnRegistry keys detachable sessions by user+thread.
Every frame a turn emits is stamped with a monotonic chat_seq and kept
in a bounded replay buffer; delivery goes to whichever connection is
attached. On disconnect the session detaches (a grace timer aborts a
turn nobody reclaims, default 10 min); on `resume_chat {thread_id,
last_seq}` the server replies `chat_resumed` and replays the missed
tail, then streams live. Finished sessions stay replayable for a
retention window (default 5 min). tool_result / approvals / stop frames
arriving on the new connection are routed to the runner executing the
adopted turn, and a detached turn's pending client tool calls survive
the disconnect so a reconnected client can still answer them.

Web: GlobalChatStore tracks the per-thread chat_seq high-water mark and,
on every socket reopen, sends resume_chat for threads with a generation
in flight. chat_resumed with status "unknown" (or an incomplete replay)
resets the thread runtime and reconciles from persisted REST history.

Protocol: resume_chat command + chat_resumed outbound frame schemas.

Knobs: NODETOOL_CHAT_REPLAY_BUFFER_EVENTS,
NODETOOL_CHAT_DETACH_GRACE_MS, NODETOOL_CHAT_REPLAY_RETENTION_MS.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MWpARdXPyxfxgJnLR7SV6C
@georgi
georgi merged commit 2085ee8 into main Aug 4, 2026
26 checks passed
@georgi
georgi deleted the claude/agent-websocket-resilience-4aj056 branch August 4, 2026 21:19
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.

2 participants