fix: clear stale active session spinner
This commit is contained in:
1 parent
603183a301
commit
bd9a4924bf
3 files changed
+95
-3
No files matched your search
@@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Keep the sidebar spinner in sync with server session metadata when the currently open session has finished but the browser still has stale local busy state (#2454).
|
||||
|
||||
## [v0.51.82] — 2026-05-17 — Release BF (stage-375 — 2-PR batch — table renderer pipe protection + Catppuccin appearance skin)
|
||||
|
||||
### Added
|
||||
|
||||
+29
-3
@@ -256,6 +256,31 @@ function _isSessionEffectivelyStreaming(s) {
|
||||
return Boolean(s && (s.is_streaming || _isSessionLocallyStreaming(s)));
|
||||
}
|
||||
|
||||
function _reconcileActiveSessionIdleStateFromList(serverRows) {
|
||||
if (!S || !S.session || !S.session.session_id) return false;
|
||||
if (typeof _sendInProgress !== 'undefined' && _sendInProgress) return false;
|
||||
if (!Array.isArray(serverRows)) return false;
|
||||
const sid=S.session.session_id;
|
||||
const serverRow=serverRows.find(s=>s&&s.session_id===sid);
|
||||
if (!serverRow) return false;
|
||||
const serverRowIsIdle=!serverRow.is_streaming&&!serverRow.active_stream_id&&!serverRow.pending_user_message;
|
||||
if (!serverRowIsIdle) return false;
|
||||
let changed=false;
|
||||
if (S.busy) { S.busy=false; changed=true; }
|
||||
if (S.activeStreamId) { S.activeStreamId=null; changed=true; }
|
||||
if (INFLIGHT&&INFLIGHT[sid]) {
|
||||
delete INFLIGHT[sid];
|
||||
if (typeof clearInflightState==='function') clearInflightState(sid);
|
||||
changed=true;
|
||||
}
|
||||
if (S.session) {
|
||||
S.session.active_stream_id=null;
|
||||
S.session.pending_user_message=null;
|
||||
}
|
||||
if (changed&&typeof updateSendBtn==='function') updateSendBtn();
|
||||
return changed;
|
||||
}
|
||||
|
||||
function _purgeStaleInflightEntries() {
|
||||
// Clean up INFLIGHT entries for sessions the server confirms are NOT
|
||||
// streaming. This prevents the in-memory cache from growing unbounded
|
||||
@@ -1878,9 +1903,6 @@ function _applySessionListPayload(sessData, projData){
|
||||
// active profile so the "Show N from other profiles" toggle can render
|
||||
// without a second round-trip. Stashed on the module for renderSessionListFromCache.
|
||||
_otherProfileCount = sessData.other_profile_count || 0;
|
||||
_allSessions = _mergeOptimisticFirstTurnSessions(sessData.sessions||[]);
|
||||
_clearLineageReportCache();
|
||||
_allProjects = projData.projects||[];
|
||||
// Capture server clock for clock-skew compensation (issue #1144).
|
||||
// server_time is epoch seconds from the server's time.time().
|
||||
// _serverTimeDelta = client - server, so (Date.now() - _serverTimeDelta)
|
||||
@@ -1891,6 +1913,10 @@ function _applySessionListPayload(sessData, projData){
|
||||
if (typeof sessData.server_tz === 'string') {
|
||||
_serverTz = sessData.server_tz;
|
||||
}
|
||||
_reconcileActiveSessionIdleStateFromList(sessData.sessions||[]);
|
||||
_allSessions = _mergeOptimisticFirstTurnSessions(sessData.sessions||[]);
|
||||
_clearLineageReportCache();
|
||||
_allProjects = projData.projects||[];
|
||||
_markPollingCompletionUnreadTransitions(_allSessions);
|
||||
const isStreaming = _allSessions.some(s => Boolean(s && s.is_streaming));
|
||||
if (isStreaming) {
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
"""Regression coverage for #2454 active-session stale sidebar spinner.
|
||||
|
||||
The backend can already reconcile stale stream state and return `/api/sessions`
|
||||
rows with `is_streaming: false`, `active_stream_id: null`, and
|
||||
`pending_user_message: null`. The remaining bug is frontend-local: the current
|
||||
open session can keep `S.busy = true`, so `_isSessionLocallyStreaming()` still
|
||||
makes the sidebar row render as streaming even after the server says idle.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
SESSIONS_SRC = (REPO / "static" / "sessions.js").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _function_body(src: str, signature: str) -> str:
|
||||
start = src.find(signature)
|
||||
assert start != -1, f"missing {signature}"
|
||||
brace = src.find("{", start)
|
||||
assert brace != -1, f"missing opening brace for {signature}"
|
||||
depth = 0
|
||||
for i in range(brace, len(src)):
|
||||
ch = src[i]
|
||||
if ch == "{":
|
||||
depth += 1
|
||||
elif ch == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return src[brace + 1 : i]
|
||||
raise AssertionError(f"could not extract function body for {signature}")
|
||||
|
||||
|
||||
def test_active_session_idle_reconcile_clears_stale_busy_and_inflight_state():
|
||||
body = _function_body(SESSIONS_SRC, "function _reconcileActiveSessionIdleStateFromList(")
|
||||
|
||||
assert "serverRows" in body, "reconcile must inspect raw /api/sessions rows before optimistic merging"
|
||||
assert "S.session.session_id" in body, "reconcile must target the currently active session"
|
||||
assert "_sendInProgress" in body, "cleanup must not interrupt a send that has not received stream_id yet"
|
||||
assert "!serverRow.is_streaming" in body, "server idle metadata must gate the cleanup"
|
||||
assert "!serverRow.active_stream_id" in body, "active stream id must be absent before cleanup"
|
||||
assert "!serverRow.pending_user_message" in body, "pending user text must be absent before cleanup"
|
||||
assert "S.busy=false" in body, "stale local busy state must be cleared"
|
||||
assert "S.activeStreamId=null" in body, "stale active stream id must be cleared"
|
||||
assert "delete INFLIGHT[sid]" in body, "stale active-session inflight cache must be purged"
|
||||
assert "clearInflightState(sid)" in body, "persisted inflight cache must be cleared too"
|
||||
assert "updateSendBtn()" in body, "composer controls must reflect the idle state after cleanup"
|
||||
|
||||
|
||||
def test_session_list_payload_reconciles_active_idle_state_before_optimistic_merge_and_render():
|
||||
body = _function_body(SESSIONS_SRC, "function _applySessionListPayload(")
|
||||
|
||||
reconcile_pos = body.find("_reconcileActiveSessionIdleStateFromList(sessData.sessions||[])")
|
||||
merge_pos = body.find("_allSessions = _mergeOptimisticFirstTurnSessions")
|
||||
render_pos = body.find("renderSessionListFromCache()")
|
||||
|
||||
assert reconcile_pos != -1, "active-session idle reconciliation must run for refreshed rows"
|
||||
assert merge_pos != -1, "session rows must still be applied from /api/sessions"
|
||||
assert render_pos != -1, "payload application must still render from cache"
|
||||
assert reconcile_pos < merge_pos < render_pos, (
|
||||
"local S.busy/INFLIGHT state must be reconciled against raw server rows "
|
||||
"before optimistic merging can re-label a stale active session as streaming"
|
||||
)
|
||||
Reference in new issue
Block a user