Skip to content

Live SSE stream does not reattach after session switch — user sees no live tokens until completion #2924

@wirtsi

Description

@wirtsi

Summary

When the user switches away from a session whose chat run is still streaming (e.g. clicks + for a new conversation, then sends a message there) and later returns to the original session, the SSE token stream does not reconnect. Live tokens never appear in the chat pane; the final response only lands once the server-side run completes and a polling refresh of the metadata swaps it in.

Repro

  1. Start chat A with a long-running prompt (long enough that the server stream takes >5s).
  2. Click + to create chat B, send a short message in B (this triggers attachLiveStream(B)closeOtherLiveStreams(B), which closes A's EventSource).
  3. Click back on chat A in the sidebar.

Expected: the live token stream resumes and the assistant bubble in A continues to fill.
Actual: the chat pane stays at whatever was rendered before the switch. The completed response only appears later, after the server-side run hits done and the next /api/session refresh lands.

Root cause

In static/messages.js, closeLiveStream() tears down the EventSource and removes the entry from LIVE_STREAMS, but it does not mark INFLIGHT[sid] for reattach. The INFLIGHT[sid].reattach = true flag is only set on the storage-load path inside loadSession() (sessions.js, the if (!INFLIGHT[sid] && activeStreamId && ...) branch). For a session that was never evicted from in-memory INFLIGHT — i.e. the user was just on it and switched away — the flag stays undefined. When the user returns, loadSession()'s reattach branch checks if (INFLIGHT[sid].reattach && activeStreamId && ...) and skips because reattach is falsy. No new EventSource is opened.

The closed-stream + in-memory-INFLIGHT combination is the exact state produced by the connection-leak fix (closeOtherLiveStreams added to loadSession to stop leaking background EventSources). Before that fix, the SSE was never closed on switch, so the closure kept rendering and "reconnect on return" wasn't needed. Once the leak was sealed, the reattach path also needed to fire in the in-memory case — which it didn't.

Fix

closeLiveStream() should set INFLIGHT[sessionId].reattach = true after deleting the LIVE_STREAMS entry, guarded by an existence check so the terminal-state teardown (_clearOwnerInflightState() runs before _closeSource()) remains a safe no-op.

A PR with the fix + regression tests is attached.

Related

  • The same investigation surfaced a separate bug in api/updates.py:_dirty_suffix() that silently dropped the -dirty suffix on any dirty checkout. The cache-busting query string (?v=<WEBUI_VERSION>) was identical between clean and dirty builds, defeating dev-build cache invalidation. Fixed in the same PR.

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingsprint-candidateStrong candidate for next sprintstreamingSSE streaming, gateway sync, real-time updates

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions