Skip to content

feat(chat): keep-alive + reattach for dashboard terminal sessions - #50084

Closed
TinkerOfThings wants to merge 6 commits into
NousResearch:mainfrom
TinkerOfThings:feat/pty-keepalive-reattach
Closed

feat(chat): keep-alive + reattach for dashboard terminal sessions#50084
TinkerOfThings wants to merge 6 commits into
NousResearch:mainfrom
TinkerOfThings:feat/pty-keepalive-reattach

Conversation

@TinkerOfThings

Copy link
Copy Markdown
Contributor

What does this PR do?

Dashboard terminals tie the PTY process 1:1 to the WebSocket: pty_ws spawns on connect and calls bridge.close() on disconnect. So a browser refresh, tab close, or mobile signal-drop kills the running agent, and reconnecting spawns a brand-new session — losing an in-flight reply and the conversation.

This makes a terminal session survive disconnect and reattach on reconnect, opt-in via a ?attach=<token> query param (no token ⇒ existing spawn-and-kill behavior, unchanged). A shared PtySessionRegistry owns each PTY: a single drain task always reads the PTY into a bounded ring buffer and forwards to the attached socket when present. On reconnect with the same token the buffer is replayed (repainting the terminal) and live streaming resumes. Detached sessions are reaped after 30 min idle, or immediately when the agent process exits.

Intentional non-goals: no cross-device handoff (token is per-browser localStorage); no simultaneous multi-viewer (last-attach-wins); sessions don't persist across a dashboard restart (registry is in-process). The token is opaque and never bypasses auth — all existing per-connect auth/host/origin gates run unchanged before any reattach.

Related Issue

Fixes #50074

Type of Change

  • ✨ New feature (non-breaking change that adds functionality)

Changes Made

  • hermes_cli/pty_session.py (new)RingBuffer, PtySession (drain/attach/detach, EOF → WS close 4410), PtySessionRegistry (reattach/reap/capacity), run_reaper.
  • hermes_cli/web_server.py/api/pty keep-alive path behind ?attach=; the original pump factored into _legacy_pump (no-token path, unchanged); module-level PTY_REGISTRY; reaper started/cancelled in the app lifespan.
  • web/src/pages/ChatPage.tsx — persist a per-browser token in localStorage, send it as ?attach=, reconnect on a transient close, and distinguish process-exit (4410) / superseded (4409) from a transient drop.
  • tests/test_pty_session.py, tests/test_pty_keepalive_ws.py (new) — unit + integration coverage.

Close-code contract: 4410 = process exited, 4409 = superseded by a newer tab on the same token, anything else = transient → client reconnects.

How to Test

  1. Start the dashboard, open /chat, send a prompt.
  2. While the reply is streaming, refresh the page (or drop/restore the network) → the tab reconnects and the in-progress reply continues in place; no new session.
  3. /exit the agent → the panel shows "session ended" (close 4410), no reconnect loop.
  4. pytest tests/test_pty_session.py tests/test_pty_keepalive_ws.py -q → pass.

Checklist

  • Read the Contributing Guide
  • Commit messages follow Conventional Commits (feat(scope):)
  • Searched existing PRs for duplicates
  • PR contains only keep-alive changes (a separate mobile touch-copy fix is its own PR)
  • Ran tests — new keep-alive tests pass; the full suite collects cleanly (33,203 tests, no import errors introduced)
  • Added tests for the change
  • Tested on my platform: Ubuntu (Linux 6.17), Chrome on Android over the LAN
  • Docs: docstrings + design notes; N/A cli-config.yaml.example (no config keys)
  • Cross-platform: keep-alive is POSIX-PTY-only, same constraint as the existing pty_ws (already gated by _PTY_BRIDGE_AVAILABLE); no Windows-specific paths

TinkerOfThings and others added 6 commits June 21, 2026 08:48
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>
@alt-glitch alt-glitch added type/feature New feature or request comp/cli CLI entry point, hermes_cli/, setup wizard comp/tui Terminal UI (ui-tui/ + tui_gateway/) P3 Low — cosmetic, nice to have labels Jun 21, 2026
@alt-glitch alt-glitch added comp/dashboard Web dashboard / control panel UI (dashboard/, landing) and removed comp/tui Terminal UI (ui-tui/ + tui_gateway/) labels Jun 26, 2026
@teknium1

teknium1 commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Merged in #60515 via rebase-merge — your commits landed on main with authorship preserved (e10e4bc..79f4f78). The branch had drifted ~2,400 commits behind, so we cherry-picked your 6 commits onto current main and added small integration fixes on top (to_thread offloads for the post-#53227 event-loop rules, a detach guard for superseded sockets, and attach-token rotation on forced-fresh starts). Thanks for the clean design — the registry + ring-buffer + close-code contract shipped essentially as you built it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard comp/dashboard Web dashboard / control panel UI (dashboard/, landing) P3 Low — cosmetic, nice to have type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dashboard terminals don't survive a browser refresh (chat session is lost)

3 participants