Skip to content

**Web dashboard: clicking a previous session opens a blank new session (/api/pty ?attach= reattach discards the ?resume= spawn env)** #63701

Description

@Axmr1

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

  1. Start the dashboard and open the chat page once (this creates the keep-alive PTY).
  2. Go to Sessions -> History and click the play icon ("Resume in Chat") on any old session, or click a previous session in the sidebar.
  3. The chat shows a blank new session instead of the clicked session's transcript.
  4. 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?

  • I'd like to fix this myself and submit a PR

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Medium — degraded but workaround existsarea/sessionsSession lifecycle, resume, persistence, historybugcomp/dashboardWeb dashboard / control panel UI (dashboard/, landing)sweeper:risk-message-deliverySweeper risk: may drop, duplicate, misroute, or suppress messagessweeper:risk-session-stateSweeper risk: may lose/corrupt/mis-associate session or context statetype/bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions