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
- Start chat A with a long-running prompt (long enough that the server stream takes >5s).
- Click + to create chat B, send a short message in B (this triggers
attachLiveStream(B) → closeOtherLiveStreams(B), which closes A's EventSource).
- 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
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
attachLiveStream(B)→closeOtherLiveStreams(B), which closes A'sEventSource).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
doneand the next/api/sessionrefresh lands.Root cause
In
static/messages.js,closeLiveStream()tears down theEventSourceand removes the entry fromLIVE_STREAMS, but it does not markINFLIGHT[sid]for reattach. TheINFLIGHT[sid].reattach = trueflag is only set on the storage-load path insideloadSession()(sessions.js, theif (!INFLIGHT[sid] && activeStreamId && ...)branch). For a session that was never evicted from in-memoryINFLIGHT— i.e. the user was just on it and switched away — the flag staysundefined. When the user returns,loadSession()'s reattach branch checksif (INFLIGHT[sid].reattach && activeStreamId && ...)and skips becausereattachis falsy. No newEventSourceis opened.The closed-stream + in-memory-
INFLIGHTcombination is the exact state produced by the connection-leak fix (closeOtherLiveStreamsadded toloadSessionto 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 setINFLIGHT[sessionId].reattach = trueafter deleting theLIVE_STREAMSentry, 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
api/updates.py:_dirty_suffix()that silently dropped the-dirtysuffix 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