Skip to content

Keep loaded history while omitted timeline rows keep changing - #4397

Open
hemaaanth wants to merge 1 commit into
get-bb:mainfrom
hemaaanth:fix/timeline-omitted-row-rebuild-loop
Open

hemaaanth wants to merge 1 commit into
get-bb:mainfrom
hemaaanth:fix/timeline-omitted-row-rebuild-loop

Conversation

@hemaaanth

Copy link
Copy Markdown
Contributor

Human comments

What was wrong

A latest timeline page reports timelinePage.olderRowsSourceSeqEnd, the highest sourceSeqEnd of the rows it omits. mergeAdvancedSnapshotTimelineRows rebuilds from the latest page when that value is past the client's loaded tip. #3686 documented this as a known limitation.

A long-running background subagent (a delegation row) under a later message trips this on every event. The delegation's turn sits on an older page but keeps getting child rows. Each new event pushes olderRowsSourceSeqEnd past the tip. The client drops its loaded history, useAutoLoadOlderRows loads the older pages again, and the next event repeats this. The transcript flips several times a second between full history and "Loading older messages…". See #4388 for the live trace.

Also, the latest page did not include the thread-scoped item/delegation/progress and item/delegation/completed events for a delegation whose turn it pulled in. That delegation projected as pending with no output.

What changed

  • paginateTimelineRows: on a latest page, omitted rows whose sourceSeqEnd reaches the window start go to a new timelinePage.olderRowUpdates array, not into olderRowsSourceSeqEnd.
    • Each update keeps only the children / childRows suffix that reaches the window start, applied recursively.
    • The client only merges when the window start is at most its tip + 1 (timelineWindowsAreContiguous), so this suffix holds every child that changed after the tip.
    • Updates larger than the page byte budget are not sent. Those rows count toward olderRowsSourceSeqEnd as before. Leaves cut by a content page still always count, so that limitation remains.
  • mergeAdvancedSnapshotTimelineRows joins each update into the loaded row with the same id, using prependOlderTimelineRows, the same join used for partial latest rows. It ignores updates for rows it has not loaded.
  • Output truncation and output preview share one helper that also processes the update rows.
  • listLatestBackgroundTaskStateRowsByItemIds also returns the latest delegation progress/completed row, and the timeline window backfill applies it to delegation items. Without this, an update replaces a completed delegation with a pending one.
  • Contract: optional olderRowUpdates on timelinePageMetadataSchema, with allowlist entries. docs/timeline-pagination.md describes the updates.
  • Four existing tests expected the old rebuild signal for rows that changed inside the latest window. They now expect updates and a merge.
  • No request parameter, cache key, HOST_DAEMON_PROTOCOL_VERSION, CLI or SDK changes.

How you verified

  • New timeline-event-budget.test.ts case: a running subagent under a later message streams child messages. The test reads each latest page, walks the older pages, and merges like the client. On main at 2726ab1ee, the merge takes the rebuild path after the first event. With the fix, loaded history stays equal to a fresh full walk. It also fails if the delegation state backfill is removed.
  • New unit tests: paginateTimelineRows update cutting and the byte-budget fallback; the client merge joining updates; the delegation state backfill query.
  • pnpm exec turbo run test --filter=@bb/db --filter=@bb/server-contract --filter=@bb/client-core --filter=@bb/server --continue --force: @bb/db, @bb/server-contract and @bb/client-core passed. @bb/server: 3241 passed, 1 skipped, and 3 timed out in install-machine-script.test.ts, which this change does not touch. That file passes on its own, together with timeline-event-budget.test.ts (74/74).
  • pnpm exec turbo run typecheck lint for those packages passed. oxfmt --check passes on the changed files.

Fixes #4388

AGENT GENERATED

🤖 Generated with Claude Code

A latest timeline page reports olderRowsSourceSeqEnd for rows it omits.
When a row on an older page keeps receiving events (a running background
subagent under a later message), that value passes the client's loaded
tip on every event. The client then drops its loaded history, reloads
the older pages, and repeats, so the transcript flickers.

The latest page now sends omitted rows that reach its window start as
timelinePage.olderRowUpdates, cut to the children that reach it. The
client joins them into its loaded rows by id, and those rows no longer
count toward olderRowsSourceSeqEnd. The latest build also backfills
delegation progress and completion state, so a pulled-in delegation
keeps its status and output.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hemaaanth
hemaaanth force-pushed the fix/timeline-omitted-row-rebuild-loop branch from be6695e to f53a197 Compare September 26, 2026 05:59

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Transcript flickers between full history and "Loading older messages…" while background subagents stream

1 participant