fix(#3900): false streaming + activity-timer reset on session switch (absorb #3899) + re-anchor regression tests

Production fixes (Tamaz-sujashvili, reviewed sound by maintainer):
- loadSession clears S.busy/S.activeStreamId as soon as metadata confirms no active_stream_id, before the async message-load gap (idle session no longer shows streaming chrome).
- Snapshots the live turn before wiping msgInner + seeds INFLIGHT, restores on the active-stream return path (timer/trace survive switch-back).

Re-anchored the 2 brittle regression tests per maintainer review: test_..snapshots.. now anchors on the unique 'Loading conversation...' marker (was matching the no-space 'Session not available' error path); test_..restores.. now asserts the LIVE Phase 2a restore (after loadInflightState) instead of the unreachable Phase-2b/1184 branch. CHANGELOG stamped v0.51.384 (MW).
This commit is contained in:
nesquena-hermes
2026-06-13 06:39:22 +00:00
3 changed files with 131 additions and 9 deletions

View File

@@ -878,6 +878,22 @@ async function loadSession(sid){
// close streams for the session the user actually landed on (#1060 guard,
// extended to cover the new pre-switch await).
if (_loadingSessionId !== sid) return;
// Snapshot the live turn before msgInner is replaced. Preserves the activity
// timer, partial response, and tool cards so switching back does not rebuild
// the stream UI from scratch.
if(
(S.busy||S.activeStreamId||(INFLIGHT&&INFLIGHT[currentSid]))&&
typeof snapshotLiveTurnHtmlForSession==='function'
){
if(!INFLIGHT[currentSid]){
INFLIGHT[currentSid]={
messages:Array.isArray(S.messages)?[...S.messages]:[],
uploaded:[],
toolCalls:Array.isArray(S.toolCalls)?[...S.toolCalls]:[],
};
}
snapshotLiveTurnHtmlForSession(currentSid);
}
}
if (currentSid !== sid || forceReload) {
// #3306: When force-reloading the currently-active session (e.g. external
@@ -1040,14 +1056,21 @@ async function loadSession(sid){
if(typeof startSessionStream==='function') startSessionStream(S.session.session_id);
const activeStreamId=S.session.active_stream_id||null;
// If the server says the session is idle, discard any browser-side inflight
// cache left behind by a crashed/restarted stream. Otherwise the UI can keep
// showing a permanent thinking/running state even though active_streams=0.
if(!activeStreamId&&INFLIGHT[sid]){
delete INFLIGHT[sid];
if(typeof clearInflightState==='function') clearInflightState(sid);
// 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){
@@ -1262,10 +1285,15 @@ async function loadSession(sid){
updateSendBtn();
setStatus('');
setComposerStatus('');
// syncTopbar();renderMessages();appendThinking();loadDir('.');
syncTopbar();renderMessages(sameSessionForceReload?{preserveScroll:true}:undefined);
if(typeof ensureLiveWorklogShell==='function') ensureLiveWorklogShell();
else appendThinking();
let restoredLiveTurn=false;
if(typeof restoreLiveTurnHtmlForSession==='function'){
restoredLiveTurn=restoreLiveTurnHtmlForSession(sid);
}
if(!restoredLiveTurn){
if(typeof ensureLiveWorklogShell==='function') ensureLiveWorklogShell();
else appendThinking();
}
loadDir('.');
updateQueueBadge(sid);
startApprovalPolling(sid);