fix: false Connection lost message after settled stream disconnect — v0.50.59
* fix: 避免流结束后误插入 Connection lost 错误 * chore: bump version to v0.50.59, update CHANGELOG --------- Co-authored-by: fiver <fiver@example.com> Co-authored-by: Hermes Agent <agent@hermes>
This commit is contained in:
@@ -1,5 +1,10 @@
|
|||||||
# Hermes Web UI -- Changelog
|
# Hermes Web UI -- Changelog
|
||||||
|
|
||||||
|
## [v0.50.59] — 2026-04-16
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **False "Connection lost" message after settled stream** — the UI no longer injects a fake `**Error:** Connection lost` assistant message when an SSE connection drops after the stream already completed normally. The fix tracks terminal stream states (`done`, `stream_end`, `cancel`, `apperror`) and, on a disconnect, fetches `/api/session` to confirm the session is settled before silently restoring it instead of calling the error path. Real failures still go through the error path as before. (Fixes #561, PR #562 by @halmisen)
|
||||||
|
|
||||||
## [v0.50.58] — 2026-04-16
|
## [v0.50.58] — 2026-04-16
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -553,7 +553,7 @@
|
|||||||
<div class="settings-section-title">System</div>
|
<div class="settings-section-title">System</div>
|
||||||
<div class="settings-section-meta">Instance version and access controls.</div>
|
<div class="settings-section-meta">Instance version and access controls.</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="settings-version-badge">v0.50.58</span>
|
<span class="settings-version-badge">v0.50.59</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-field" style="border-top:1px solid var(--border);padding-top:12px;margin-top:8px">
|
<div class="settings-field" style="border-top:1px solid var(--border);padding-top:12px;margin-top:8px">
|
||||||
<label for="settingsPassword" data-i18n="settings_label_password">Access Password</label>
|
<label for="settingsPassword" data-i18n="settings_label_password">Access Password</label>
|
||||||
|
|||||||
@@ -213,6 +213,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
|||||||
|
|
||||||
// ── Shared SSE handler wiring (used for initial connection and reconnect) ──
|
// ── Shared SSE handler wiring (used for initial connection and reconnect) ──
|
||||||
let _reconnectAttempted=false;
|
let _reconnectAttempted=false;
|
||||||
|
let _terminalStateReached=false;
|
||||||
|
|
||||||
// rAF-throttled rendering: buffer tokens, render at most once per frame
|
// rAF-throttled rendering: buffer tokens, render at most once per frame
|
||||||
let _renderPending=false;
|
let _renderPending=false;
|
||||||
@@ -412,6 +413,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
|||||||
});
|
});
|
||||||
|
|
||||||
source.addEventListener('done',e=>{
|
source.addEventListener('done',e=>{
|
||||||
|
_terminalStateReached=true;
|
||||||
const d=JSON.parse(e.data);
|
const d=JSON.parse(e.data);
|
||||||
delete INFLIGHT[activeSid];
|
delete INFLIGHT[activeSid];
|
||||||
clearInflight();clearInflightState(activeSid);
|
clearInflight();clearInflightState(activeSid);
|
||||||
@@ -454,6 +456,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
|||||||
});
|
});
|
||||||
|
|
||||||
source.addEventListener('stream_end',e=>{
|
source.addEventListener('stream_end',e=>{
|
||||||
|
_terminalStateReached=true;
|
||||||
try{
|
try{
|
||||||
const d=JSON.parse(e.data||'{}');
|
const d=JSON.parse(e.data||'{}');
|
||||||
if((d.session_id||activeSid)!==activeSid) return;
|
if((d.session_id||activeSid)!==activeSid) return;
|
||||||
@@ -473,6 +476,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
|||||||
});
|
});
|
||||||
|
|
||||||
source.addEventListener('apperror',e=>{
|
source.addEventListener('apperror',e=>{
|
||||||
|
_terminalStateReached=true;
|
||||||
// Application-level error sent explicitly by the server (rate limit, crash, etc.)
|
// Application-level error sent explicitly by the server (rate limit, crash, etc.)
|
||||||
// This is distinct from the SSE network 'error' event below.
|
// This is distinct from the SSE network 'error' event below.
|
||||||
source.close();
|
source.close();
|
||||||
@@ -514,8 +518,12 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
|||||||
}catch(_){}
|
}catch(_){}
|
||||||
});
|
});
|
||||||
|
|
||||||
source.addEventListener('error',e=>{
|
source.addEventListener('error',async e=>{
|
||||||
source.close();
|
source.close();
|
||||||
|
if(_terminalStateReached){
|
||||||
|
_closeSource();
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Attempt one reconnect if the stream is still active server-side
|
// Attempt one reconnect if the stream is still active server-side
|
||||||
if(!_reconnectAttempted && streamId){
|
if(!_reconnectAttempted && streamId){
|
||||||
_reconnectAttempted=true;
|
_reconnectAttempted=true;
|
||||||
@@ -529,14 +537,17 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}catch(_){}
|
}catch(_){}
|
||||||
|
if(await _restoreSettledSession()) return;
|
||||||
_handleStreamError();
|
_handleStreamError();
|
||||||
},1500);
|
},1500);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if(await _restoreSettledSession()) return;
|
||||||
_handleStreamError();
|
_handleStreamError();
|
||||||
});
|
});
|
||||||
|
|
||||||
source.addEventListener('cancel',e=>{
|
source.addEventListener('cancel',e=>{
|
||||||
|
_terminalStateReached=true;
|
||||||
source.close();
|
source.close();
|
||||||
delete INFLIGHT[activeSid];clearInflight();clearInflightState(activeSid);stopApprovalPolling();stopClarifyPolling();
|
delete INFLIGHT[activeSid];clearInflight();clearInflightState(activeSid);stopApprovalPolling();stopClarifyPolling();
|
||||||
if(!_approvalSessionId||_approvalSessionId===activeSid) hideApprovalCard(true);
|
if(!_approvalSessionId||_approvalSessionId===activeSid) hideApprovalCard(true);
|
||||||
@@ -553,6 +564,29 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function _restoreSettledSession(){
|
||||||
|
try{
|
||||||
|
const data=await api(`/api/session?session_id=${encodeURIComponent(activeSid)}`);
|
||||||
|
const session=data&&data.session;
|
||||||
|
if(!session) return false;
|
||||||
|
if(session.active_stream_id||session.pending_user_message) return false;
|
||||||
|
delete INFLIGHT[activeSid];clearInflight();clearInflightState(activeSid);stopApprovalPolling();stopClarifyPolling();
|
||||||
|
_closeSource();
|
||||||
|
if(!_approvalSessionId||_approvalSessionId===activeSid) hideApprovalCard(true);
|
||||||
|
if(!_clarifySessionId||_clarifySessionId===activeSid) hideClarifyCard(true);
|
||||||
|
if(S.session&&S.session.session_id===activeSid){
|
||||||
|
S.activeStreamId=null;const _cbe=$('btnCancel');if(_cbe)_cbe.style.display='none';
|
||||||
|
clearLiveToolCards();if(!assistantText)removeThinking();
|
||||||
|
S.session=session;S.messages=session.messages||[];
|
||||||
|
syncTopbar();renderMessages();
|
||||||
|
}
|
||||||
|
renderSessionList();setBusy(false);setComposerStatus('');
|
||||||
|
return true;
|
||||||
|
}catch(_){
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function _handleStreamError(){
|
function _handleStreamError(){
|
||||||
delete INFLIGHT[activeSid];clearInflight();clearInflightState(activeSid);stopApprovalPolling();stopClarifyPolling();
|
delete INFLIGHT[activeSid];clearInflight();clearInflightState(activeSid);stopApprovalPolling();stopClarifyPolling();
|
||||||
_closeSource();
|
_closeSource();
|
||||||
|
|||||||
Reference in New Issue
Block a user