From 3a73400da3f69556b445ef244bce029bb85b88f7 Mon Sep 17 00:00:00 2001 From: ai-ag2026 <261867348+ai-ag2026@users.noreply.github.com> Date: Sat, 23 May 2026 11:34:18 +0200 Subject: [PATCH] fix: clear stale busy state before send --- static/messages.js | 27 ++++++++++++++++++++++++++ tests/test_inflight_send_start_race.py | 15 ++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/static/messages.js b/static/messages.js index a3e7e067..21b68b4a 100644 --- a/static/messages.js +++ b/static/messages.js @@ -190,6 +190,32 @@ let _sendInProgress = false; let _sendInProgressSid = null; // session_id of the in-flight send const _sessionTitleProvisionalBySid = new Map(); +function _clearStaleBusyStateBeforeSend({compressionRunning=false}={}){ + if(!S||!S.busy||compressionRunning) return false; + const session=S.session||{}; + const sid=session.session_id||''; + const hasRuntimeConfirmation=Boolean( + S.activeStreamId|| + session.active_stream_id|| + session.pending_user_message|| + session.pending_started_at + ); + if(hasRuntimeConfirmation) return false; + if(typeof INFLIGHT==='object'&&INFLIGHT&&sid&&INFLIGHT[sid]){ + delete INFLIGHT[sid]; + if(typeof clearInflightState==='function') clearInflightState(sid); + } + S.activeStreamId=null; + if(session) session.active_stream_id=null; + if(typeof setBusy==='function') setBusy(false); + else S.busy=false; + if(typeof setComposerStatus==='function') setComposerStatus(''); + if(typeof setStatus==='function') setStatus(''); + if(typeof updateSendBtn==='function') updateSendBtn(); + if(sid&&typeof clearOptimisticSessionStreaming==='function') clearOptimisticSessionStreaming(sid); + return true; +} + function _sessionTitleLooksDefaultOrProvisional(titleText, provisionalText){ const title=String(titleText||'').replace(/\s+/g,' ').trim(); if(!title||title==='Untitled'||title==='New Chat')return true; @@ -262,6 +288,7 @@ async function send(){ } const compressionRunning=typeof isCompressionUiRunning==='function'&&isCompressionUiRunning(); + _clearStaleBusyStateBeforeSend({compressionRunning}); // If busy or a manual compression is still running, handle based on busy_input_mode if(S.busy||compressionRunning){ if(text){ diff --git a/tests/test_inflight_send_start_race.py b/tests/test_inflight_send_start_race.py index 1908acad..fcedbe40 100644 --- a/tests/test_inflight_send_start_race.py +++ b/tests/test_inflight_send_start_race.py @@ -51,6 +51,21 @@ def test_stale_inflight_purge_preserves_current_send_before_stream_id_exists(): assert skip_idx < delete_idx, "the current-send skip must run before any purge deletion" +def test_send_clears_stale_busy_state_before_queue_branch(): + """A stale client-only busy flag must not divert a new user turn into the invisible queue.""" + body = _function_body(MESSAGES_JS, "send") + + assert "_clearStaleBusyStateBeforeSend" in body, ( + "send() should reconcile client-only stale busy state before deciding busy/queue mode" + ) + reconcile_idx = body.index("_clearStaleBusyStateBeforeSend") + busy_branch_idx = body.index("if(S.busy||compressionRunning)") + chat_start_idx = body.index("api('/api/chat/start'") + assert reconcile_idx < busy_branch_idx < chat_start_idx, ( + "stale busy reconciliation must run before the queue branch and before /api/chat/start" + ) + + def test_server_absent_optimistic_first_turn_rows_are_not_kept_forever(): """A local first-turn sidebar row must expire when /api/chat/start never persisted it.""" body = _function_body(SESSIONS_JS, "_mergeOptimisticFirstTurnSessions")