mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-26 11:40:26 +00:00
Stage 397: PR #2689 — fix(chat): preserve inflight send state during start race
Co-authored-by: ai-ag2026 <ai-ag2026@users.noreply.github.com>
This commit is contained in:
+7
-2
@@ -415,7 +415,8 @@ async function send(){
|
||||
if(typeof upsertActiveSessionForLocalTurn==='function'){
|
||||
upsertActiveSessionForLocalTurn({title:displayText.slice(0,64),messageCount:S.messages.length,timestampMs:Date.now()});
|
||||
}
|
||||
INFLIGHT[activeSid]={messages:[...S.messages],uploaded:uploadedNames,toolCalls:[]};
|
||||
const optimisticMessages=[...S.messages];
|
||||
INFLIGHT[activeSid]={messages:optimisticMessages,uploaded:uploadedNames,toolCalls:[]};
|
||||
if(typeof saveInflightState==='function'){
|
||||
saveInflightState(activeSid,{streamId:null,messages:INFLIGHT[activeSid].messages,uploaded:uploadedNames,toolCalls:[]});
|
||||
}
|
||||
@@ -486,9 +487,13 @@ async function send(){
|
||||
// against real active-stream metadata before the background refresh lands.
|
||||
upsertActiveSessionForLocalTurn({title:S.session&&S.session.title||displayText.slice(0,64),messageCount:S.messages.length,timestampMs:Date.now()});
|
||||
}
|
||||
if(!INFLIGHT[activeSid]){
|
||||
INFLIGHT[activeSid]={messages:optimisticMessages,uploaded:uploadedNames,toolCalls:[]};
|
||||
}
|
||||
const currentInflight=INFLIGHT[activeSid];
|
||||
markInflight(activeSid, streamId);
|
||||
if(typeof saveInflightState==='function'){
|
||||
saveInflightState(activeSid,{streamId,messages:INFLIGHT[activeSid].messages,uploaded:uploadedNames,toolCalls:INFLIGHT[activeSid].toolCalls||[]});
|
||||
saveInflightState(activeSid,{streamId,messages:currentInflight.messages||optimisticMessages,uploaded:uploadedNames,toolCalls:currentInflight.toolCalls||[]});
|
||||
}
|
||||
// Refresh session list so background streaming indicators appear immediately for the
|
||||
// session that was just started and any others that may already be running.
|
||||
|
||||
@@ -310,6 +310,9 @@ function _purgeStaleInflightEntries() {
|
||||
}
|
||||
}
|
||||
for (const sid of Object.keys(INFLIGHT)) {
|
||||
if (typeof _sendInProgress !== 'undefined' && _sendInProgress && sid === _sendInProgressSid) {
|
||||
continue;
|
||||
}
|
||||
if (!sessionsById.has(sid)) {
|
||||
// Session is absent from _allSessions — it was deleted / archived /
|
||||
// filtered and can never stream again, so drop the entry.
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
"""Regression coverage for send/start optimistic INFLIGHT races."""
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
MESSAGES_JS = (REPO_ROOT / "static" / "messages.js").read_text(encoding="utf-8")
|
||||
SESSIONS_JS = (REPO_ROOT / "static" / "sessions.js").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _function_body(src: str, name: str) -> str:
|
||||
marker = f"function {name}"
|
||||
start = src.index(marker)
|
||||
brace = src.index("{", start)
|
||||
depth = 1
|
||||
i = brace + 1
|
||||
while depth and i < len(src):
|
||||
if src[i] == "{":
|
||||
depth += 1
|
||||
elif src[i] == "}":
|
||||
depth -= 1
|
||||
i += 1
|
||||
return src[brace + 1 : i - 1]
|
||||
|
||||
|
||||
def test_send_preserves_optimistic_messages_across_chat_start_await():
|
||||
"""send() must not dereference INFLIGHT[activeSid] after await without a fallback."""
|
||||
body = _function_body(MESSAGES_JS, "send")
|
||||
setup_idx = body.index("const optimisticMessages=[...S.messages];")
|
||||
inflight_idx = body.index("INFLIGHT[activeSid]={messages:optimisticMessages")
|
||||
await_idx = body.index("const startData=await api('/api/chat/start'")
|
||||
save_idx = body.index("saveInflightState(activeSid,{streamId", await_idx)
|
||||
|
||||
assert setup_idx < inflight_idx < await_idx < save_idx
|
||||
post_await = body[await_idx:save_idx]
|
||||
assert "if(!INFLIGHT[activeSid])" in post_await, (
|
||||
"send() should recreate the INFLIGHT entry if a session-list refresh pruned it"
|
||||
)
|
||||
assert "messages:INFLIGHT[activeSid].messages" not in body[save_idx : save_idx + 220], (
|
||||
"saveInflightState() should use a guarded local/current inflight object, not a blind nested read"
|
||||
)
|
||||
|
||||
|
||||
def test_stale_inflight_purge_preserves_current_send_before_stream_id_exists():
|
||||
"""Sidebar cleanup must not delete the active send before /api/chat/start responds."""
|
||||
body = _function_body(SESSIONS_JS, "_purgeStaleInflightEntries")
|
||||
|
||||
assert "_sendInProgress" in body and "_sendInProgressSid" in body, (
|
||||
"_purgeStaleInflightEntries() should skip the current send while start is in progress"
|
||||
)
|
||||
skip_idx = body.index("_sendInProgress")
|
||||
delete_idx = body.index("delete INFLIGHT[sid];")
|
||||
assert skip_idx < delete_idx, "the current-send skip must run before any purge deletion"
|
||||
Reference in New Issue
Block a user