From cb94cecc1addcc5aeb0cedf7151a1e738dfcef3a Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Sat, 11 Jul 2026 05:52:56 +0000 Subject: [PATCH] fix(sessions): reset busy/stream flags before the visit-ack repaint (gate) Gate finding: the idle-session S.busy/S.activeStreamId reset ran AFTER _acknowledgeSessionVisit(), so the ack's sidebar repaint could inherit the PREVIOUS session's busy state and manufacture a phantom unread/streaming indicator on the freshly-loaded idle session. Moved the activeStreamId calc + idle reset BEFORE the acknowledge call. Co-authored-by: neaucode-bot. --- static/sessions.js | 44 ++++++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/static/sessions.js b/static/sessions.js index c1a1cc70..4efd1b52 100644 --- a/static/sessions.js +++ b/static/sessions.js @@ -1779,6 +1779,30 @@ async function loadSession(sid){ if(typeof syncTopbar==='function') syncTopbar(); // Acknowledge the visit as soon as the session metadata is accepted for the // in-flight load: clears the viewed count + any stale completion-unread marker + // `let` (not const): re-read below, after the awaited _ensureMessagesLoaded, + // so a server_turn_started that attaches a live stream MID-RELOAD is honored + // by the attach/idle decision instead of being clobbered by the stale snapshot. + let activeStreamId=S.session.active_stream_id||null; + // If the server says the session is idle, reset browser-side streaming flags + // NOW — BEFORE _acknowledgeSessionVisit() below (whose sidebar repaint would + // otherwise inherit the PREVIOUS session's busy/stream state) and before the + // async _ensureMessagesLoaded gap. Without this, S.busy can remain true from a + // still-running stream in the PREVIOUS session while S.session.session_id has + // already advanced to the new one. _isSessionLocallyStreaming() checks + // (isActive && S.busy), so the new session would appear locally-streaming + // (sidebar spinner, Stop button, thinking state on an idle chat) and the visit + // repaint would manufacture a phantom unread. Also clears stale INFLIGHT + // entries left behind by a crashed/restarted stream. (#5917 gate: reset must + // precede the acknowledge repaint.) + if(!activeStreamId){ + S.activeStreamId=null; + S.busy=false; + if(INFLIGHT[sid]){ + delete INFLIGHT[sid]; + if(typeof clearInflightState==='function') clearInflightState(sid); + } + } + // and syncs the polling snapshot so a deferred /api/sessions poll landing // during the async message-load gap below cannot re-flag a stale unread dot. _acknowledgeSessionVisit( @@ -1790,26 +1814,6 @@ async function loadSession(sid){ _setActiveSessionUrl(S.session.session_id); if(typeof startSessionStream==='function') startSessionStream(S.session.session_id); - // `let` (not const): re-read below, after the awaited _ensureMessagesLoaded, - // so a server_turn_started that attaches a live stream MID-RELOAD is honored - // by the attach/idle decision instead of being clobbered by the stale snapshot. - let activeStreamId=S.session.active_stream_id||null; - // If the server says the session is idle, reset browser-side streaming flags - // NOW — before the async _ensureMessagesLoaded gap below. Without this, - // S.busy can remain true from a still-running stream in the PREVIOUS session - // while S.session.session_id has already advanced to the new one. - // _isSessionLocallyStreaming() checks (isActive && S.busy), so during the - // async window the new session would appear locally-streaming (sidebar spinner, - // Stop button, thinking state on an idle chat). Also clears stale INFLIGHT - // entries left behind by a crashed/restarted stream. - if(!activeStreamId){ - S.activeStreamId=null; - S.busy=false; - if(INFLIGHT[sid]){ - delete INFLIGHT[sid]; - if(typeof clearInflightState==='function') clearInflightState(sid); - } - } function _mergePendingSessionMessage(session,messages){ if(!Array.isArray(messages)) return false;