Skip to content

Commit d974388

Browse files
nesquenaclaude
andcommitted
fix(sessions): close stale-response race in _loadOlderMessages
The cancellation guard at the top of _loadOlderMessages checks _loadingSessionId !== null && _loadingSessionId !== sid. That catches the case where a NEW session load is in progress when the older-messages fetch returns. It misses the case where: 1. user is on session A, _loadOlderMessages in flight 2. user switches to session B; loadSession(B) completes; sets _loadingSessionId = null 3. older-messages response for A returns -> _loadingSessionId === null -> guard evaluates to (null !== null) && ... -> false -> NO bail -> S.messages = [...olderMsgs, ...S.messages] prepends A's old messages onto B's S.messages Tighten the guard by also comparing the captured sid against the currently-active S.session.session_id. Together they cover both windows: mid-switch (new sid loading) and post-switch (no load in progress). Added a regression test (test_load_older_compares_against_active_session_id) that asserts the new check is present and runs before any S.messages mutation. Existing 5 cancellation tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
1 parent 43ab1df commit d974388

2 files changed

Lines changed: 34 additions & 1 deletion

File tree

static/sessions.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,15 @@ async function _loadOlderMessages() {
370370
_loadingOlder = true;
371371
try {
372372
const data = await api(`/api/session?session_id=${encodeURIComponent(sid)}&messages=1&resolve_model=0&msg_before=${_oldestIdx}&msg_limit=${_INITIAL_MSG_LIMIT}`);
373-
if (!data || !data.session || (_loadingSessionId !== null && _loadingSessionId !== sid)) return;
373+
// Cancellation guards:
374+
// - response shape sane
375+
// - the active session is still the one we issued the request for.
376+
// Compare against S.session.session_id, NOT _loadingSessionId — the
377+
// latter is null between session loads, leaving a window where a
378+
// stale response could prepend onto the new session's S.messages.
379+
if (!data || !data.session) return;
380+
if (!S.session || S.session.session_id !== sid) return;
381+
if (_loadingSessionId !== null && _loadingSessionId !== sid) return;
374382
const olderMsgs = (data.session.messages || []).filter(m => m && m.role);
375383
if (!olderMsgs.length) { _messagesTruncated = false; return; }
376384
// Prepend older messages

tests/test_parallel_session_switch.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,3 +532,28 @@ def test_oldest_idx_reset_prevents_wrong_cursor(self):
532532
"_loadOlderMessages should bail out if _oldestIdx <= 0, "
533533
"which is the reset value after session switch."
534534
)
535+
536+
def test_load_older_compares_against_active_session_id(self):
537+
"""_loadOlderMessages must verify S.session.session_id === sid after await.
538+
539+
_loadingSessionId alone is insufficient: it is null between session
540+
loads, so a stale older-messages response that lands AFTER a
541+
completed session switch would otherwise pass the guard and prepend
542+
onto the new session's S.messages. The S.session.session_id check
543+
closes that window.
544+
"""
545+
fn_start = SESSIONS_JS.find("async function _loadOlderMessages")
546+
fn_end = SESSIONS_JS.find("\n}", fn_start) + 2
547+
fn_body = SESSIONS_JS[fn_start:fn_end]
548+
549+
assert "S.session.session_id !== sid" in fn_body, (
550+
"_loadOlderMessages must compare S.session.session_id against "
551+
"the captured sid after await — _loadingSessionId is null "
552+
"between sessions and would let a stale response through."
553+
)
554+
# The S.session check must appear BEFORE the S.messages mutation.
555+
active_check_idx = fn_body.find("S.session.session_id !== sid")
556+
mutation_idx = fn_body.find("S.messages = [...olderMsgs")
557+
assert active_check_idx >= 0 and mutation_idx >= 0 and active_check_idx < mutation_idx, (
558+
"Active-session guard must run before S.messages mutation."
559+
)

0 commit comments

Comments
 (0)