Skip to content

fix(gateway): stop a resuming viewer stealing a running session's stream - #83524

Open
jaxmatrix wants to merge 1 commit into
NousResearch:mainfrom
jaxmatrix:jaishukla7768/resume-transport-steal
Open

fix(gateway): stop a resuming viewer stealing a running session's stream#83524
jaxmatrix wants to merge 1 commit into
NousResearch:mainfrom
jaxmatrix:jaishukla7768/resume-transport-steal

Conversation

@jaxmatrix

Copy link
Copy Markdown

The bug

session.resume is not only "reattach me" — it is also how a second viewer peeks at a session from another window, tile, or client.

_live_session_payload rebinds the session's transport unconditionally:

if transport is not None:
    session["transport"] = transport

So the peek takes over the live stream. A turn already running for viewer A suddenly emits to viewer B, and A watches its own answer stop mid-sentence — no error, no indication, and no way to get the stream back short of another resume (which then steals it from B).

The window is exactly "a turn is in flight and someone else opens the same session", which is the normal case as soon as a client supports more than one view of a conversation.

The fix

A running session keeps the transport it is streaming to. Three cases still rebind, because none of them can interrupt anyone:

  • an idle session — nothing is in flight, and the next prompt would rebind anyway;
  • a session parked on _detached_ws_transport or _stdio_transport — a disconnect points detached sessions at the former (_reap_or_detach_sessions_for_transport), so there is no live reader and the resuming client is strictly better than nothing;
  • the viewer that already owns the stream, re-asserting it (also covers current is None).

The predicate is a pure function on (session, transport), so it is directly testable without standing up a gateway.

Scope

tui_gateway/server.py only — one new helper plus one condition on an existing assignment. No API, wire-format, or config change; a client that only ever has one viewer sees no difference.

Tests

Four regression tests in tests/test_tui_gateway_ws.py, one per branch above.

  • With the patch: 10 passed.
  • Reverting server.py and keeping the tests: 4 failed, 6 passed.

Provenance

This has been running in a downstream fork for some time, where the multi-viewer case (extra windows, tiles, a phone client on the same session) is common enough that the stolen-stream symptom showed up immediately. Sending it upstream because the root cause is in the shared gateway, not in anything fork-specific — the guard is worth having wherever more than one viewer can resume a session.

Confirmed still present on main at 37e46c774c.

`session.resume` is not only "reattach me" — it is also how a second viewer
PEEKS at a session from another window, tile, or client. `_live_session_payload`
rebound `session["transport"]` unconditionally, so that peek took over the live
stream: a turn already running for viewer A suddenly emitted to viewer B, and A
watched its own answer stop mid-sentence with no error and no way to get it
back.

A running session now keeps the transport it is streaming to. Three cases still
rebind, because none of them can interrupt anyone:

- an IDLE session — nothing is in flight, and the next prompt would rebind
  anyway;
- a session parked on `_detached_ws_transport` or `_stdio_transport` — a
  disconnect points detached sessions there, so there is no live reader and the
  resuming client is strictly better than nothing;
- the viewer that already owns the stream re-asserting it.

Four regression tests cover exactly those branches. They fail on the unpatched
tree with `AttributeError` on the missing helper, and the behavioural one fails
on the semantics rather than the symbol once it exists.
@alt-glitch alt-glitch added type/bug Something isn't working comp/tui Terminal UI (ui-tui/ + tui_gateway/) P2 Medium — degraded but workaround exists sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Aug 11, 2026
@Enough1122

Copy link
Copy Markdown
Contributor

AI code review — automated review for reference, author can ignore or act on any point.

fix(gateway): stop a resuming viewer stealing a running session's stream

  1. tui_gateway/server.py_resume_may_rebind_transport reads session["running"] and session["transport"] under history_lock in _live_session_payload, but if running transitions (turn start/end) are mutated outside that lock elsewhere, a resume landing exactly at turn completion can still rebind mid-stream (TOCTOU). Verify the running/transport mutations share the same lock, or re-check both under the lock at decision time.

  2. UX gap: a resuming viewer that is denied the rebind receives the same payload shape as a successful resume, with no indication the stream belongs to someone else — client B renders as if live while receiving nothing. Consider returning the current transport owner or a "peek/read-only" flag so viewers can render that state instead of appearing frozen.

  3. _resume_may_rebind_transport treats _stdio_transport as always rebindable even while running — correct for the headless serve backend (no live reader), but if a stdio transport is ever used by an attached TUI client mid-turn, the same mid-sentence steal occurs. Worth a comment (or assertion) that stdio is exclusively the headless-backend channel.

  4. Minor: the identity checks (current is _detached_ws_transport) depend on disconnects always routing to the sentinel rather than None — _reap_or_detach_sessions_for_transport is covered; confirming that is the only detach path would close the last rebind hole.

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

Labels

comp/tui Terminal UI (ui-tui/ + tui_gateway/) P2 Medium — degraded but workaround exists sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants