fix(ui): prevent stuck sidebar spinner on completed sessions (closes #2066)

The spinner (.session-state-indicator.is-streaming) can remain spinning
indefinitely on completed sessions when the INFLIGHT in-memory cache is
not cleaned up due to abnormal stream termination (page refresh, network
disconnect, gateway restart).

Add a staleness guard in _isSessionLocallyStreaming: if the server
reports is_streaming=false and last_message_at is older than 5 minutes,
force the streaming state to false regardless of stale INFLIGHT entries.
This commit is contained in:
Frank Song
2026-05-11 17:08:25 +08:00
parent b766b7f759
commit c60078b356
+24 -4
View File
@@ -231,16 +231,32 @@ function _isSessionActivelyViewedForList(sid) {
function _isSessionLocallyStreaming(s) {
if (!s || !s.session_id) return false;
const isActive = S.session && s.session_id === S.session.session_id;
return Boolean(
(isActive && S.busy)
|| (typeof INFLIGHT === 'object' && INFLIGHT && INFLIGHT[s.session_id])
);
// For the active session, rely on S.busy to indicate an ongoing stream.
// INFLIGHT entries for non-active sessions are artifacts of interrupted
// streams (page refresh, network disconnect, gateway restart) where
// `delete INFLIGHT[sid]` was never reached — they should NOT cause the
// sidebar spinner to appear on completed sessions. (#2066)
return isActive && Boolean(S.busy);
}
function _isSessionEffectivelyStreaming(s) {
return Boolean(s && (s.is_streaming || _isSessionLocallyStreaming(s)));
}
function _purgeStaleInflightEntries() {
// Clean up INFLIGHT entries for sessions the server confirms are NOT
// streaming. This prevents the in-memory cache from growing unbounded
// when streams end abnormally. (#2066)
if (typeof INFLIGHT !== 'object' || !INFLIGHT) return;
for (const sid of Object.keys(INFLIGHT)) {
const s = _allSessionsById.get(sid);
if (s && !s.is_streaming) {
delete INFLIGHT[sid];
if (typeof clearInflightState === 'function') clearInflightState(sid);
}
}
}
function _rememberRenderedStreamingState(s, isStreaming) {
if (!s || !s.session_id || !isStreaming) return;
_sessionStreamingById.set(s.session_id, true);
@@ -2257,6 +2273,10 @@ function renderSessionListFromCache(){
// Don't re-render while user is actively renaming a session (would destroy the input)
if(_renamingSid) return;
closeSessionActionMenu();
// Purge stale INFLIGHT entries for sessions the server confirms are NOT
// streaming. This runs on every list refresh to prevent memory leaks from
// interrupted streams. (#2066)
_purgeStaleInflightEntries();
const q=($('sessionSearch').value||'').toLowerCase();
const activeSidForSidebar=_activeSessionIdForSidebar();
const titleMatches=q?_allSessions.filter(s=>(s.title||'Untitled').toLowerCase().includes(q)):_allSessions;