mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-26 03:30:36 +00:00
fix(queue): drain correct session queue after cross-session stream completion (#964)
When a session finishes streaming while the user has switched to a different session, setBusy(false) was draining S.session.session_id (the currently *viewed* session) instead of the session that actually finished. Queued follow-up messages were silently dropped. Root cause: setBusy() has no context about which session triggered it. The activeSid closure variable inside attachLiveStream() knew the right session but was not propagated. Fix: add _queueDrainSid module global (null by default). Stream done and error handlers set it to activeSid immediately before calling setBusy(false). setBusy(false) reads and clears _queueDrainSid, falling back to S.session if it is unset (the common case where the user hasn't switched away). Handlers patched: done event, start-call error handler, stream_end/stream_stop reconnection fallback, and max-retry error exit. Co-authored with Claude Sonnet 4.6 / Anthropic.
This commit is contained in:
+4
-4
@@ -153,7 +153,7 @@ async function send(){
|
||||
if(!_approvalSessionId || _approvalSessionId===activeSid) hideApprovalCard(true);removeThinking();
|
||||
if(!_clarifySessionId || _clarifySessionId===activeSid) hideClarifyCard(true);
|
||||
S.messages.push({role:'assistant',content:`**Error:** ${errMsg}`});
|
||||
renderMessages();setBusy(false);setComposerStatus(`Error: ${errMsg}`);
|
||||
_queueDrainSid=activeSid;renderMessages();setBusy(false);setComposerStatus(`Error: ${errMsg}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -726,7 +726,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
||||
_markSessionViewed(activeSid, d.session.message_count ?? S.messages.length);
|
||||
syncTopbar();renderMessages();loadDir('.');
|
||||
}
|
||||
renderSessionList();setBusy(false);setStatus('');
|
||||
_queueDrainSid=activeSid;renderSessionList();setBusy(false);setStatus('');
|
||||
setComposerStatus('');
|
||||
playNotificationSound();
|
||||
sendBrowserNotification('Response complete',assistantText?assistantText.slice(0,100):'Task finished');
|
||||
@@ -900,7 +900,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
||||
_markSessionViewed(activeSid, session.message_count ?? S.messages.length);
|
||||
syncTopbar();renderMessages();
|
||||
}
|
||||
renderSessionList();setBusy(false);setComposerStatus('');
|
||||
_queueDrainSid=activeSid;renderSessionList();setBusy(false);setComposerStatus('');
|
||||
return true;
|
||||
}catch(_){
|
||||
return false;
|
||||
@@ -951,7 +951,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
||||
const _cbe=$('btnCancel');if(_cbe)_cbe.style.display='none';
|
||||
clearLiveToolCards();
|
||||
removeThinking();
|
||||
setBusy(false);
|
||||
_queueDrainSid=activeSid;setBusy(false);
|
||||
setComposerStatus('');
|
||||
renderMessages();
|
||||
renderSessionList();
|
||||
|
||||
+12
-3
@@ -1,6 +1,13 @@
|
||||
const S={session:null,messages:[],entries:[],busy:false,pendingFiles:[],toolCalls:[],activeStreamId:null,currentDir:'.',activeProfile:'default'};
|
||||
const INFLIGHT={}; // keyed by session_id while request in-flight
|
||||
const SESSION_QUEUES={}; // keyed by session_id for queued follow-up turns
|
||||
// Tracks which session's queue to drain in setBusy(false).
|
||||
// Set to activeSid just before setBusy(false) in done/error handlers so the
|
||||
// queue drains the session that *finished*, not the one currently viewed.
|
||||
// Single-shot: setBusy() reads and clears this on every call. Concurrent
|
||||
// back-to-back stream completions would overwrite it, but HTTPServer is
|
||||
// single-threaded so only one done event fires at a time in practice.
|
||||
let _queueDrainSid=null;
|
||||
const $=id=>document.getElementById(id);
|
||||
function _getSessionQueue(sid, create=false){
|
||||
if(!sid) return [];
|
||||
@@ -946,10 +953,12 @@ function setBusy(v){
|
||||
setComposerStatus('');
|
||||
// Always hide Cancel button when not busy
|
||||
const _cb=$('btnCancel');if(_cb)_cb.style.display='none';
|
||||
const sid=S.session&&S.session.session_id;
|
||||
const sid=_queueDrainSid||(S.session&&S.session.session_id);
|
||||
_queueDrainSid=null;
|
||||
updateQueueBadge(sid);
|
||||
// Drain one queued message for the currently viewed session after UI settles
|
||||
const next=sid?shiftQueuedSessionMessage(sid):null;
|
||||
// Drain one queued message for the finished session after UI settles
|
||||
const _isViewedSid=!S.session||sid===S.session.session_id;
|
||||
const next=sid&&_isViewedSid?shiftQueuedSessionMessage(sid):null;
|
||||
if(next){
|
||||
updateQueueBadge(sid);
|
||||
setTimeout(()=>{
|
||||
|
||||
@@ -501,8 +501,9 @@ def test_session_scoped_message_queue_frontend_wiring(cleanup_test_sessions):
|
||||
assert "const SESSION_QUEUES" in ui_src
|
||||
assert "function queueSessionMessage" in ui_src
|
||||
assert "function shiftQueuedSessionMessage" in ui_src
|
||||
assert "const sid=S.session&&S.session.session_id;" in ui_src
|
||||
assert "const next=sid?shiftQueuedSessionMessage(sid):null;" in ui_src
|
||||
# _queueDrainSid tracks which session's queue to drain even after session switches
|
||||
assert "_queueDrainSid" in ui_src
|
||||
assert "shiftQueuedSessionMessage(sid)" in ui_src
|
||||
assert "queueSessionMessage(S.session.session_id" in messages_src
|
||||
assert "updateQueueBadge(S.session.session_id);" in messages_src
|
||||
assert "updateQueueBadge(sid);" in sessions_src
|
||||
|
||||
Reference in New Issue
Block a user