Agent chat: older history keeps loading past a prompt queued with a big paste

Merged
TE
16d ago
+272-22

After acpmux and the app restarted (cosmic-gray, 2026-10-20 ~17:55 PT), Leo could not scroll up past a point in the coordinator chat. The older history was still there: acpmux keeps the whole log on disk (~/.acpmux/sessions/<id>/events/*.ndjson, 28 segments, seqs in order) and a read-only _acpmux/events walk with beforeSeq pages it back to seq 2 (92 pages). The restart only matters because a fresh pane opens on the tail and has to page everything back in. Before the restart, the pane had received the whole history live.

The pane is what stops. A prompt queued with a big paste or image (queued, 0.4 to 3.9 MB in that chat) is bigger than OLDER_PAGE_BYTES (256 KB), so acpmux sends it back as a page of its own. A page that starts no turn adds no row above the first one. The near-top loader in App.tsx (olderLoadKey, keyed on rows[0].id) asks again only when the first row changes. So once it reached one of those pages it never asked again. In the coordinator chat, the first such page is about 48 pages up (seq 47435).

Fix (direct.ts fetchOlder): one loadOlder() keeps reading older pages until it reaches a turn start older than the first drawn row (the turn opener's seq when the chat opened mid-turn), at most 8 pages (about 2 MB). The snapshot now carries historyFrom (the oldest held seq), and the transcript's olderLoadKey includes it. So a capped read that drew no row still gets the next ask. This changes nothing in acpmux or the Swift bridge.

Changelog

  • Fixed: scrolling up in a long agent chat keeps loading older history past a prompt queued with a large paste or image.

Test

There are three cases: a big queued prompt, a chat that opens mid-turn with a previous turn over 256 KB, and twelve 300 KB queued prompts in a row (at most 8 pages per ask, all turns load across asks).
cd webviews && bun test test/agent-pane-history-rowless-page.test.ts. The fake daemon pages like hub/paging.rs events_page (maxBytes, at least one record per page).

  • base (5c57ac3 / fix reverted locally): fails with expect(rows[0].id).not.toBe("user-9") after a loadOlder() whose page held only the queued record
  • 76e1034 (first fix, new cases): the mid-turn case fails with not.toBe("user-12") and the bounded case reads 13 pages (<= 8). Fleet step 7c5f7b13f9adc084455f0ad8.
  • head aa3dc84: all 3 pass. Fleet step 26fbf789f952e5e58b4e0589 (scripts/ci/run-webviews-tests.sh). agent-pane-history-paging, -reading-place, -older-rows-fade, -replay and -turn-opener pass there too.

Observed / Not run

  • Observed: the regression test fails on base and passes on head. turn-opener, transcript-newest-last, compaction-rows and subagent-stopped-after-cancel pass on head (24 tests). prepush-gate.py: no new violations. I also ran a live read-only _acpmux/events walk against cosmic-gray's daemon (no attach, no sends) and saw the single-record queued pages.
  • scripts/web-lint.sh was run in a sparse clone. Its only errors are TS2307 for fixture JSON outside the sparse set (Packages/Shared/..., cmux-tui/.../fixtures), none in the changed files. Format is clean.
  • Same 8 failures in both fleet runs, none related to this change: 7 files can't launch Playwright chromium on the worker (aws-m4pro), and stats-page has a German date-format mismatch. webviews bun run check: 0 errors.
  • Not run: dogfood in a tagged app build.

🤖 Generated with Claude Code

Agent session: cosmic-gray