fix: gateway sync race condition + hybrid session data loss — v0.50.93 (PR #714)

Fixes and extends PR #676 (yunyunyunyun-yun). Race guard in sessions.js SSE handler; prefix-equality check in routes.py _handle_session_import_cli. Closes #676.

Co-authored-by: yunyunyunyun-yun <yunyunyunyun-yun@users.noreply.github.com>
This commit is contained in:
nesquena-hermes
2026-04-18 23:18:28 -07:00
committed by GitHub
parent 877a32f49c
commit 66fbfbaa2b
3 changed files with 38 additions and 1 deletions

View File

@@ -1,5 +1,10 @@
# Hermes Web UI -- Changelog
## [v0.50.93] — 2026-04-19
### Fixed
- **Gateway message sync no longer corrupts the active session on slow networks** — the `sessions_changed` SSE handler now captures the active session ID before the async `import_cli` fetch and validates it in `.then()`, preventing session-switch races from overwriting the wrong conversation. Added `is_cli_session` guard so the handler only fires for CLI-originated sessions. The backend import path now also verifies that existing messages are a strict prefix of the fresh CLI messages before overwriting, preventing silent data loss on hybrid WebUI+CLI sessions. (PR #676 by @yunyunyunyun-yun)
## [v0.50.91] — 2026-04-19
### Added

View File

@@ -2869,9 +2869,17 @@ def _handle_session_import_cli(handler, body):
sid = str(body["session_id"])
# Check if already imported — idempotent
# Check if already imported — refresh messages from CLI store if new ones arrived
existing = Session.load(sid)
if existing:
fresh_msgs = get_cli_session_messages(sid)
if fresh_msgs and len(fresh_msgs) > len(existing.messages):
# Prefix-equality guard: only extend if existing messages are a prefix of
# the fresh CLI messages. Prevents silently dropping WebUI-added messages
# on hybrid sessions (user sent messages via WebUI while CLI continued).
if existing.messages == fresh_msgs[:len(existing.messages)]:
existing.messages = fresh_msgs
existing.save(touch_updated_at=False)
return j(
handler,
{

View File

@@ -331,6 +331,30 @@ function startGatewaySSE(){
const data = JSON.parse(ev.data);
if(data.sessions){
renderSessionList(); // re-fetch and re-render
// If the active session received new gateway messages, refresh the conversation view.
// S.busy check prevents stomping on an in-progress WebUI response.
// is_cli_session check ensures we only poll import_cli for CLI-originated sessions.
if(S.session && !S.busy && S.session.is_cli_session){
const changedIds = new Set((data.sessions||[]).map(s=>s.session_id));
if(changedIds.has(S.session.session_id)){
// Capture active session ID before async fetch — race guard.
// If the user switches sessions while the fetch is in-flight, discard the result.
const activeSid = S.session.session_id;
api('/api/session/import_cli',{method:'POST',body:JSON.stringify({session_id:activeSid})})
.then(res=>{
if(!S.session || S.session.session_id !== activeSid) return;
if(res && res.session && Array.isArray(res.session.messages)){
const prev = S.messages.length;
S.messages = res.session.messages.filter(m=>m&&m.role);
if(S.messages.length !== prev){
renderMessages();
if(typeof highlightCode==='function') highlightCode();
}
}
})
.catch(()=>{ /* ignore — next poll will retry */ });
}
}
}
}catch(e){ /* ignore parse errors */ }
});