Two data-integrity blockers from round-3 review (nesquena-hermes 2026-09-19):
1. [CORE] A successful /compress returns the full transcript, but
_applyManualCompressionResult kept the bounded preflight's paging
signals (_messagesTruncated/_oldestIdx). Render now restores them
before renderMessages(), same ordering as settle/cancel paths.
2. [SILENT] _restoreSettledSession preservation used
(prefix || suffix), so repeated identical turns (colliding
_messageIdentityKey) made BOTH comparisons succeed and the prefix
branch spliced at the wrong offset - silently dropping rows and
duplicating others. The server truncation signal now decides the
strategy: truncated => suffix-exclusive, untruncated => prefix.
Tests: node behavioral driver covering /compress paging restore,
repeated-turn settle (no drop/dup, authoritative offset), and
gap-free msg_before paging after bounded recovery.
Bounded settle/cancel reloads return the latest 30 messages, not a
prefix of the visible window. The shorter-terminal preserve path only
compared prefixes, so a long session dropped the Connection-interrupted
marker. Anchor persist also ran before _oldestIdx was refreshed from
the bounded response, writing the scene against the wrong transcript
row.
- Treat a suffix match like a prefix match when preserving the terminal
recovery marker
- Refresh _messagesTruncated / _oldestIdx before attaching/persisting
the projected scene on settle, cancel, and apperror
- Document the msg_limit=30 / msg_limit=all contract
- Add runtime + source-order regression tests
Fixes the #7310 stall class: six frontend recovery paths (offline/bfcache
refreshSession, stream-end settle, cancel sync, /compress preflight, /retry,
/undo) issued bare GET /api/session reads that re-walked, re-redacted and
re-serialized the ENTIRE transcript (4-15s / 28MB on a 5k-row session,
#7625), and the same shape shipped the full session-level tool_calls list
because windowing was gated on msg_limit being present.
Changes:
- Server: add explicit msg_limit=all escape hatch; window session-level
tool_calls by actual truncation (length comparison) instead of the
presence of a limit parameter. The bare no-limit shape keeps its
full-transcript contract (pinned by
test_state_db_reconciliation_preserves_sidecar_order_when_timestamps_collide
and the run-journal snapshot test).
- Client: refreshSession / _restoreSettledSession / cancel sync /
/compress preflight / /retry / /undo now request the bounded tail
(msg_limit=30) and restore the _messages_truncated/_messages_offset paging
gate instead of hardcoding 'not truncated'; jump-to-start
(_ensureAllMessagesLoaded) and outline jump opt in to the full transcript
via msg_limit=all.
Tests: test_session_default_window.py covers helper semantics, per-call-site
bounded/all shapes, truncation-signal restoration, and a regression guard
that no bare GET /api/session fetch remains in static/. 454 related tests
pass; reverse-verified (reverting the fixes turns the tests red).