Bug Description
In the web dashboard, clicking any previous session (sidebar SESSIONS list, or Sessions -> History -> "Resume in Chat") opens an empty new session instead of resuming the transcript. Only the first session clicked after a dashboard restart resumes correctly; every later click lands back on that same PTY. No data is lost, the sessions are intact in state.db, the dashboard just never attaches to them.
This is the web/ half of the session-resume family that #55578 fixed for the desktop app (8e73481 touched apps/desktop only; the web dashboard path was missed).
Steps to Reproduce
- Start the dashboard and open the chat page once (this creates the keep-alive PTY).
- Go to Sessions -> History and click the play icon ("Resume in Chat") on any old session, or click a previous session in the sidebar.
- The chat shows a blank new session instead of the clicked session's transcript.
- Restart the dashboard process and repeat: now the FIRST clicked session resumes correctly, and every subsequent click again lands on that session's PTY regardless of which session was clicked.
Expected Behavior
I click previous session, previous session is presented...
Actual Behavior
Session resume from the web dashboard is effectively unusable (one resume per dashboard restart). Data is intact; it is purely an attach-routing bug. Since local patches to web_server.py are discarded by hermes update, the bug re-triggers after every update until fixed upstream.
Affected Component
CLI (interactive chat)
Messaging Platform (if gateway-related)
No response
Debug Report
Report https://dpaste.com/97JVMVKVC
agent.log https://dpaste.com/EBWLDSKLB
gateway.log https://dpaste.com/6WY5RYBSZ
gui.log https://dpaste.com/BZTY5EG5Y
Operating System
WSL 2 - Linux - Windows 10
Python Version
No response
Hermes Version
No response
Additional Logs / Traceback (optional)
Root Cause Analysis (optional)
Introduced by e10e4bc "feat(chat): reattach /api/pty sessions via ?attach= token" (authored 2026-06-21, shipped in the 0.18.0 -> 0.18.2 window).
The dashboard chat is a PTY-embedded TUI. A session click navigates to /chat?resume=<id>, and the PTY websocket carries both ?resume=<id> and a keep-alive ?attach=<token>. The attach token is per browser (localStorage key hermes.pty.token.chat, ptyAttachToken in ChatPage.tsx); it never varies with the resume target.
Server side, in hermes_cli/web_server.py (the /api/pty websocket endpoint, ~line 15525 at current HEAD), the handler builds the spawn env containing HERMES_TUI_RESUME=<id> and then calls:
attach_token = ws.query_params.get("attach") or None
def _spawn():
return PtyBridge.spawn(argv, cwd=cwd, env=env)
...
PTY_REGISTRY.attach_or_spawn(attach_token, spawn=_spawn)
attach_or_spawn reattaches to any living PTY registered under that token and only calls _spawn on a miss. So once a keep-alive PTY exists for the browser, every later ?resume= request reattaches to it and the freshly built HERMES_TUI_RESUME env is thrown away. The user sees whatever session that old PTY is on, typically a blank new one. Only when the registry has no living PTY under the token (dashboard restart, TTL reap) does a click spawn correctly, which is why exactly one resume works per dashboard restart.
Supporting evidence from a live instance: five rapid /api/pty websocket accepts in gui.log while only ONE TUI child process existed (reattach, not spawn); after a dashboard restart the first click spawned a child WITH the correct HERMES_TUI_RESUME, because the registry was empty.
Proposed Fix (optional)
Namespace the keep-alive identity by the explicitly requested resume target, so each resume target gets its own keep-alive PTY and reattach can only ever land on the right session. Minimal server-side version (no frontend change needed):
attach_token = ws.query_params.get("attach") or None
_explicit_resume = ws.query_params.get("resume") or None
if attach_token is not None and _explicit_resume:
attach_token = f"{attach_token}::{_explicit_resume}"
Plain refresh keep-alive (no ?resume= param) is unchanged. We have been running this exact patch locally since 2026-07-11 and it fixes the behavior end to end (verified both by probe, two loopback websockets with the same ?attach= and different ?resume= yield two distinct PTY children each with the correct HERMES_TUI_RESUME, and by real browser clicks). An alternative would be for attach_or_spawn to compare the requested spawn env against the living PTY's and respawn on mismatch, which would also cover future env-bearing parameters.
Are you willing to submit a PR for this?
Bug Description
In the web dashboard, clicking any previous session (sidebar SESSIONS list, or Sessions -> History -> "Resume in Chat") opens an empty new session instead of resuming the transcript. Only the first session clicked after a dashboard restart resumes correctly; every later click lands back on that same PTY. No data is lost, the sessions are intact in state.db, the dashboard just never attaches to them.
This is the web/ half of the session-resume family that #55578 fixed for the desktop app (8e73481 touched apps/desktop only; the web dashboard path was missed).
Steps to Reproduce
Expected Behavior
I click previous session, previous session is presented...
Actual Behavior
Session resume from the web dashboard is effectively unusable (one resume per dashboard restart). Data is intact; it is purely an attach-routing bug. Since local patches to
web_server.pyare discarded byhermes update, the bug re-triggers after every update until fixed upstream.Affected Component
CLI (interactive chat)
Messaging Platform (if gateway-related)
No response
Debug Report
Operating System
WSL 2 - Linux - Windows 10
Python Version
No response
Hermes Version
No response
Additional Logs / Traceback (optional)
Root Cause Analysis (optional)
Introduced by e10e4bc "feat(chat): reattach /api/pty sessions via ?attach= token" (authored 2026-06-21, shipped in the 0.18.0 -> 0.18.2 window).
The dashboard chat is a PTY-embedded TUI. A session click navigates to
/chat?resume=<id>, and the PTY websocket carries both?resume=<id>and a keep-alive?attach=<token>. The attach token is per browser (localStorage keyhermes.pty.token.chat,ptyAttachTokeninChatPage.tsx); it never varies with the resume target.Server side, in
hermes_cli/web_server.py(the/api/ptywebsocket endpoint, ~line 15525 at current HEAD), the handler builds the spawn env containingHERMES_TUI_RESUME=<id>and then calls:attach_or_spawnreattaches to any living PTY registered under that token and only calls_spawnon a miss. So once a keep-alive PTY exists for the browser, every later?resume=request reattaches to it and the freshly builtHERMES_TUI_RESUMEenv is thrown away. The user sees whatever session that old PTY is on, typically a blank new one. Only when the registry has no living PTY under the token (dashboard restart, TTL reap) does a click spawn correctly, which is why exactly one resume works per dashboard restart.Supporting evidence from a live instance: five rapid
/api/ptywebsocket accepts in gui.log while only ONE TUI child process existed (reattach, not spawn); after a dashboard restart the first click spawned a child WITH the correctHERMES_TUI_RESUME, because the registry was empty.Proposed Fix (optional)
Namespace the keep-alive identity by the explicitly requested resume target, so each resume target gets its own keep-alive PTY and reattach can only ever land on the right session. Minimal server-side version (no frontend change needed):
Plain refresh keep-alive (no
?resume=param) is unchanged. We have been running this exact patch locally since 2026-07-11 and it fixes the behavior end to end (verified both by probe, two loopback websockets with the same?attach=and different?resume=yield two distinct PTY children each with the correctHERMES_TUI_RESUME, and by real browser clicks). An alternative would be forattach_or_spawnto compare the requested spawn env against the living PTY's and respawn on mismatch, which would also cover future env-bearing parameters.Are you willing to submit a PR for this?