#3313 applies the ephemeral-field carry-forward at all 5 wholesale S.messages
replace sites (loadSession/_ensureMessagesLoaded/_loadOlderMessages/
_ensureAllMessagesLoaded/startGatewaySSE), renaming the replace RHS from
msgs/next to _msgsToAssign/_nextToAssign. These pre-existing tests pinned the
old literals; updated to match the new RHS (or made LHS-agnostic) while
preserving the invariants they protect (bump-before-replace, session-switch
guard, shorter-transcript guard).
_loadOlderMessages() previously fetched older messages with the legacy
index-cursor page (msg_before=_oldestIdx&msg_limit=30) and prepended
the page to S.messages. After #2716 the backend always runs the full
append-only merge for /api/session?messages=1 — the same merge as a
larger msg_limit on the same call — so we can ask for a larger
authoritative tail window directly instead of stitching pages on the
client.
Behavior
* Default request shape becomes msg_limit=currentLoaded+30. The newly
exposed head of the response is what the user sees as 'older
messages'. No new query parameters.
* msg_before remains supported by the backend and is retained in the
client as a race-fallback path: if the returned tail no longer has
the currently displayed messages as a suffix (because the session
appended new messages mid-flight, or merge filtered something), the
client issues the legacy msg_before page and prepends it instead.
This preserves correctness under concurrent appends.
* Suffix-continuity uses the existing _sameTranscriptMessage helper,
which tolerates timestamp drift and content-array reshapes.
* Existing race guards (loadingSessionId, S.session.session_id, and
the _messagesGeneration snapshot from #1937) are reapplied after
the fallback await.
Tests
Updated four static-string assertions in the existing scroll/viewport
tests to track the new mutation site (S.messages = nextMessages) and
the new msg_limit=requestedLimit shape, while still asserting that
msg_before remains in the body for the race-fallback path.
pytest -q
tests/test_older_history_viewport_preservation.py
tests/test_parallel_session_switch.py
tests/test_issue1937_endless_scroll_jumpstart_race.py
tests/test_session_tail_payload.py
-> 52 passed
node --check static/sessions.js -> ok
Notes
Originally part of PR #2835. That PR was closed because of an
architectural conflict with #2716 on a different file (api/models.py
metadata-only path). #2716 left static/sessions.js untouched — this
change applies cleanly on post-#2716 master with no rebase work.
The originally-proposed fix (gate _ensureAllMessagesLoaded on the existing
_loadingOlder flag) does not actually close the race. By the time the
prefetch reaches its post-await body, it has already cleared the entry-
gate that reads _loadingOlder, so a same-flag check inside the resolved
callback would be a no-op for an in-flight request.
The actual fix is two-pronged:
1. New module-scoped _messagesGeneration counter, bumped every time
S.messages is wholesale-replaced. _loadOlderMessages snapshots it
BEFORE its await and re-checks after — if it changed, the prepend
is aborted. This is the canonical async-invalidation pattern.
2. _ensureAllMessagesLoaded now claims the _loadingOlder mutex around
its body so a new prefetch cannot start mid-replace and concurrent
ensure-all calls (rapid double-click on Start) serialize cleanly.
It bumps the generation token before mutating S.messages, yields
until any in-flight prefetch finishes, and resets _oldestIdx so a
subsequent prefetch cannot request stale older messages.
Also adds the same-session / _loadingSessionId guards that the original
ensure-all body was missing post-await — if the user switched sessions
mid-flight, the old code would happily overwrite the new session's
messages with the previous session's full history.
12 new regression tests in tests/test_issue1937_endless_scroll_jumpstart_race.py
lock in: generation token declaration, bump-helper presence, snapshot-
before-await ordering, post-await-abort behaviour, mutex acquisition and
finally-release, yield-then-claim ordering when a prefetch is in flight,
generation bump during the wait phase, _oldestIdx reset, and the new
session-switch guard.
Closes#1937.