From 8cf10b152bfa3a110362334042f7420aeba65a4d Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Wed, 15 Apr 2026 20:25:31 -0700 Subject: [PATCH] =?UTF-8?q?fix:=20false=20Connection=20lost=20message=20af?= =?UTF-8?q?ter=20settled=20stream=20disconnect=20=E2=80=94=20v0.50.59?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 避免流结束后误插入 Connection lost 错误 * chore: bump version to v0.50.59, update CHANGELOG --------- Co-authored-by: fiver Co-authored-by: Hermes Agent --- CHANGELOG.md | 5 +++++ static/index.html | 2 +- static/messages.js | 36 +++++++++++++++++++++++++++++++++++- 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dac725c..27e33cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 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 ### Fixed diff --git a/static/index.html b/static/index.html index 4236e08..9a1f7aa 100644 --- a/static/index.html +++ b/static/index.html @@ -553,7 +553,7 @@
System
- v0.50.58 + v0.50.59
diff --git a/static/messages.js b/static/messages.js index 4696119..fcdf71f 100644 --- a/static/messages.js +++ b/static/messages.js @@ -213,6 +213,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){ // ── Shared SSE handler wiring (used for initial connection and reconnect) ── let _reconnectAttempted=false; + let _terminalStateReached=false; // rAF-throttled rendering: buffer tokens, render at most once per frame let _renderPending=false; @@ -412,6 +413,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){ }); source.addEventListener('done',e=>{ + _terminalStateReached=true; const d=JSON.parse(e.data); delete INFLIGHT[activeSid]; clearInflight();clearInflightState(activeSid); @@ -454,6 +456,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){ }); source.addEventListener('stream_end',e=>{ + _terminalStateReached=true; try{ const d=JSON.parse(e.data||'{}'); if((d.session_id||activeSid)!==activeSid) return; @@ -473,6 +476,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){ }); source.addEventListener('apperror',e=>{ + _terminalStateReached=true; // Application-level error sent explicitly by the server (rate limit, crash, etc.) // This is distinct from the SSE network 'error' event below. source.close(); @@ -514,8 +518,12 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){ }catch(_){} }); - source.addEventListener('error',e=>{ + source.addEventListener('error',async e=>{ source.close(); + if(_terminalStateReached){ + _closeSource(); + return; + } // Attempt one reconnect if the stream is still active server-side if(!_reconnectAttempted && streamId){ _reconnectAttempted=true; @@ -529,14 +537,17 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){ return; } }catch(_){} + if(await _restoreSettledSession()) return; _handleStreamError(); },1500); return; } + if(await _restoreSettledSession()) return; _handleStreamError(); }); source.addEventListener('cancel',e=>{ + _terminalStateReached=true; source.close(); delete INFLIGHT[activeSid];clearInflight();clearInflightState(activeSid);stopApprovalPolling();stopClarifyPolling(); 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(){ delete INFLIGHT[activeSid];clearInflight();clearInflightState(activeSid);stopApprovalPolling();stopClarifyPolling(); _closeSource();