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:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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 */ }
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user