feat(chat): keep-alive + reattach for dashboard terminal sessions (#50084 salvage) - #60515
Merged
Conversation
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Keep-alive path when ?attach=<token> is present: PTY outlives the socket via PTY_REGISTRY, reattaches on reconnect. No token = unchanged legacy pump (_legacy_pump). detach (not close) on disconnect. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ChatPage sends ?attach=<localStorage token> so /chat reattaches to its live PTY across refresh. onclose: 4410=process-exit (session ended), 4409=superseded (quiet), else transient -> auto-reconnect. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This was referenced Jul 7, 2026
This was referenced Jul 8, 2026
20 tasks
2 tasks
This was referenced Jul 10, 2026
76 tasks
This was referenced Aug 3, 2026
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Dashboard chat sessions now survive browser refreshes, sleep/wake, and network drops: the
/api/ptyPTY can outlive its WebSocket and be reattached with full output replay, instead of the running agent being killed on every disconnect.Salvages PR #50084 by @TinkerOfThings (rebase-merge, authorship preserved) onto current main, resolving the cluster behind #22027 / #50074 / #15915 / #53839.
Infographic
Root cause
pty_wsties the PTY 1:1 to the WebSocket — spawn on connect,bridge.close()on disconnect. Any transport drop kills the running agent mid-reply; reconnect spawns a fresh PTY (the session-id breadcrumb resumes the conversation, but the in-flight turn and scrollback are lost).Changes
hermes_cli/pty_session.py(new):RingBuffer(1MB),PtySession(single drain task, attach/detach, EOF → WS close4410),PtySessionRegistry(token-keyed reattach, 16-session cap, 30-min idle reap),run_reaper.hermes_cli/web_server.py: keep-alive path on/api/ptybehind?attach=<token>; no-token path factored into_legacy_pumpbyte-identical to today's behavior; reaper wired into the app lifespan.web/src/pages/ChatPage.tsx: persistent per-browser attach token (localStorage), sent on every connect; close-code contract4410= process exited (restart affordance),4409= superseded tab (quiet), anything else = transient → existing backoff reconnect now reattaches to the living PTY.tests/test_pty_session.py(10 unit),tests/test_pty_keepalive_ws.py(WS integration).Follow-up fixes on top of the salvage (integrating with main's post-#50084 hardening):
bridge.close()go throughasyncio.to_thread(event-loop stall protection, fix(dashboard): offload PTY spawn/close off the event loop #53227).ws.close(), half-open-socket FD-leak protection) plus theRuntimeErrorguard onws.receive().PtySession.detachignores a superseded socket's late detach so a session with a live viewer can't be marked idle and reaped.?fresh=1) rotate the attach token so "Start new session" never reattaches the old PTY.Validation
scripts/run_tests.sh tests/test_pty_session.py tests/test_pty_keepalive_ws.pyscripts/run_tests.sh tests/hermes_cli/test_pty_bridge.py tests/hermes_cli/test_web_server.pyweb:tsc -b --force+npm run buildLive E2E specifics: hard transport abort mid-session → PTY survives; reconnect with same token → ring buffer replays and the SAME shell process is reattached (env var set before the drop still present); child exit → close
4410; no-token legacy path → PTY killed on disconnect, unchanged.Non-goals (as designed in #50084)
No cross-device handoff (token is per-browser localStorage), no multi-viewer (last-attach-wins via
4409), no persistence across a dashboard restart (in-process registry). The token never bypasses auth — all existing per-connect auth/host/origin gates run before any reattach.Credit: @TinkerOfThings (design doc + implementation, PR #50084). Related earlier attempts: #34039 by @cpmidnite, #37682, #47297.
Fixes #50074. Closes #22027. Closes #15915. Addresses the web-dashboard half of #53839.