Skip to content

Fix timeline failures for large scoped item windows - #1398

Merged
ymichael merged 1 commit into
mainfrom
bb/investigate-timeline-loading-failure-thr_53p6dfy3c4
Aug 12, 2026
Merged

ymichael merged 1 commit into
mainfrom
bb/investigate-timeline-loading-failure-thr_53p6dfy3c4

Conversation

@ymichael

Copy link
Copy Markdown
Collaborator

Summary

  • group timeline item IDs by scope before building SQLite predicates
  • preserve cross-turn item ID isolation without one OR branch per item
  • add a regression for a byte-budget page containing 1,000 compact items

Validation

  • pnpm exec turbo run test --filter=@bb/db --filter=@bb/server --force
    • 379 database tests passed
    • 1,437 server tests passed
  • pnpm exec turbo run typecheck --filter=@bb/db --filter=@bb/server
  • focused 20-test timeline suite passed after rebasing onto current main
  • Prettier and git diff --check passed

Performance

On six real thread tails, the affected query improved from 1.7x to 8.9x at median where the old query completed. The originally failing 1,049-item thread changed from a SQLite maximum-expression-depth error to a 2.7 ms median query. Results matched exactly for every comparable thread.

AGENT GENERATED: by GPT-5

@ymichael
ymichael merged commit 923916d into main Aug 12, 2026
10 checks passed
@ymichael
ymichael deleted the bb/investigate-timeline-loading-failure-thr_53p6dfy3c4 branch August 12, 2026 17:32
SawyerHood added a commit that referenced this pull request Aug 21, 2026
)

## What was wrong

A tool call can outlive the turn that spawned it. Its `item/completed`
then arrives scoped to the next turn (the claude-code translator emits
it as a generic `toolCall` once the turn boundary cleared its call map).
The timeline projection merges that lifecycle by bare call id into the
spawning turn's row (`upsertRunningExecCall`, #447), so the turn row
spans the late completion and its inline children show the call
completed with its output. The turn-summary details route re-projects
the row's sequence window but `filterExactEventRowsForRequestedTurn`
drops every turn-scoped row whose turn id differs from the requested
turn (#164's overlapping-turn fix). The completion is gone,
`ensureSequenceWindowWholeItemRows` cannot restore it (it keys items by
scoped identity and only backfills below the window), and the call
renders `pending` forever (or `interrupted` / "Tool execution
interrupted" on an idle thread). The response is a 200, so nothing logs.

Issue: #1714. Report: https://get-bb.github.io/reports/issues/1714.html

## What changed

`apps/server/src/services/threads/timeline.ts`,
`filterExactEventRowsForRequestedTurn`: the filter tracks tool calls the
requested turn `item/started` (kinds the projection keys by bare call
id: `commandExecution`, `toolCall`, `webSearch`, `webFetch`,
`imageView`) that have not completed yet, and keeps another turn's
`item/*` rows for those ids. The id leaves the set at its
`item/completed`, so a later turn that reuses the id for a new item (a
resumed ACP session restarting its counter, #1224/#1398) stays out of
the spawning turn. File edits (scope-partitioned in the projection),
buffered text (keyed per turn), and background tasks (own thread-scoped
state rows) do not establish cross-turn ownership, matching the
projection.

The re-admitted completion is the last surviving row, so
`resolveTurnSummaryDetailsSourceRange` still yields the requested range
and the exact-bounds match holds; the issue's "missing-match 500" trap
does not bite.

Server-only. No wire change, no `HOST_DAEMON_PROTOCOL_VERSION` bump, no
CLI or doc surface.

Known sibling, out of scope: when the *later* turn has its own work and
therefore its own summary row, that turn's details window contains the
orphan completion and re-projects it as an extra completed "unknown"
tool row that its inline children do not have. That is pre-existing and
unchanged here; fixing it needs a bare-id lifecycle lookup before the
window.

## How you verified

Added to
`apps/server/test/services/threads/timeline-in-turn-window.test.ts`
(`describe("turn details for an item that finishes in a later turn")`):

- "shows the spawning turn's item completed with its late output"
asserts the turn-1 details rows deep-equal the same row's inline
`children`. Fails on origin/main:
  ```
  AssertionError: expected [ { …(19) } ] to deeply equal [ { …(19) } ]
  -     "completedAt": 1787299091836,
  +     "completedAt": null,
  -     "output": "dev server exited with code 0",
  +     "output": "",
  -     "sourceSeqEnd": 6,
  +     "sourceSeqEnd": 2,
  -     "status": "completed",
  +     "status": "pending",
  ```
  Passes with the fix.
- "keeps a later turn's reuse of the call id out of the spawning turn"
guards the ownership release: turn 1 completes `call-1`, turn 2 starts
and completes its own `call-1`; both turns' details equal their inline
children (passes before and after).

Commands (from the committed tree, `git status --porcelain` empty):

- `pnpm exec turbo run typecheck --filter=@bb/server` — `Tasks: 4
successful, 4 total`
- `pnpm exec turbo run test --filter=@bb/server` — `Tests 1 failed |
1824 passed (1825)`; the one failure is
`test/internal/internal-skill-trees.test.ts` expecting file mode 0644
and getting 0664 on this machine's umask 0002. It fails identically on
clean main here and passes in CI; unrelated to this change.

Manual: seeded the report's eight-event shape into my own dev instance,
set the thread idle, and hit both routes. `GET
/timeline?includeNestedRows=true` and `GET
/timeline/turn-summary-details?turnId=turn-1&sourceSeqStart=1&sourceSeqEnd=6`
now return the same row: `status: "completed"`, `output: "dev server
exited with code 0"`, `sourceSeqEnd: 6`. In the app, expanding "Worked
for" shows "Ran npm run dev" with the real output instead of
"interrupted" / "Tool execution interrupted".

Fixes #1714

> AGENT GENERATED: by Claude Opus 5


## Independent verification

Checked out `726d071fc` (this branch) on top of `origin/main` (`git
merge-base --is-ancestor origin/main HEAD` holds; `mergeable:
MERGEABLE`). Diff: `apps/server/src/services/threads/timeline.ts`
(+44/-2) and
`apps/server/test/services/threads/timeline-in-turn-window.test.ts`
(+171). Server-only; no wire, CLI, plugin-API, or UI surface touched, so
no `HOST_DAEMON_PROTOCOL_VERSION` bump is needed.

Root cause confirmed independently against `origin/main` before reading
the PR description: `filterExactEventRowsForRequestedTurn` drops every
other-turn turn-scoped row, while `upsertRunningExecCall` / `onExecEnd`
in `packages/thread-view` merge exec lifecycles by bare call id into the
spawning turn, and `ensureSequenceWindowWholeItemRows` keys by scoped
identity and only backfills below the window. The PR changes the filter,
which is the right layer. I also confirmed the projection does not clear
`runningCallsById` at `turn/completed` (only at `item/completed` or
final interruption), so the PR's "release at `item/completed`" rule
matches the projection more closely than the report's prototype (which
released on a later turn's `item/started`).

Fail-before / pass-after:

- `git checkout origin/main --
apps/server/src/services/threads/timeline.ts`, then `pnpm exec vitest
run --root apps/server
test/services/threads/timeline-in-turn-window.test.ts -t "finishes in a
later turn"` -> `1 failed | 1 passed`:
  ```
  AssertionError: expected [ { …(19) } ] to deeply equal [ { …(19) } ]
  -     "completedAt": 1787299535570,
  +     "completedAt": null,
  -     "output": "dev server exited with code 0",
  +     "output": "",
  -     "sourceSeqEnd": 6,
  +     "sourceSeqEnd": 2,
  -     "status": "completed",
  +     "status": "pending",
  ```
- `git checkout HEAD -- apps/server/src/services/threads/timeline.ts`,
then the whole file -> `Tests 24 passed (24)`.

Extra edge cases (ad-hoc test over in-memory SQLite, not committed): (1)
turn 1 starts `call-1` and never completes it, turn 2 starts and
completes its own `call-1`; (2) turn 2 carries an
`item/commandExecution/outputDelta` plus the completion for turn 1's
call; (3) turn 1 starts a `webFetch`, turn 2 carries the degraded
`toolCall` completion for its id. All three: details rows deep-equal the
inline children on this branch; (1) and (2) diverge on `origin/main`.

Package checks from the committed tree (`git status --porcelain` empty):
`pnpm exec turbo run typecheck --filter=@bb/server` -> `Tasks: 4
successful, 4 total`. `pnpm exec turbo run test --filter=@bb/server
--force` -> `Tests 1 failed | 1824 passed (1825)`; the one failure is
`test/internal/internal-skill-trees.test.ts` (mode 420 vs 436), the
known umask-0002 local-only failure that also fails on clean main here
and passes in CI.

Repro on the fixed branch: started my own dev instance
(`scripts/bb-dev-app current`, server :21529), seeded the report's
eight-event shape into its `bb.db` via `@bb/db`, set the thread `idle`,
then compared `GET /api/v1/threads/<id>/timeline?includeNestedRows=true`
(turn-1 row `[1,6]`, child `completed`, output `dev server exited with
code 0`, `sourceSeqEnd: 6`) with `GET
/api/v1/threads/<id>/timeline/turn-summary-details?turnId=turn-1&sourceSeqStart=1&sourceSeqEnd=6`
-> HTTP 200, same row id, `status: "completed"`, `output: "dev server
exited with code 0"`, `completedAt` set, `sourceSeqEnd: 6`. The report's
wrong answer (`interrupted` / `Tool execution interrupted`,
`sourceSeqEnd: 2`) no longer reproduces.

CI: all required checks green (Checks, Tests
server/packages/integration/app-1..3, Package Smoke ubuntu+macos,
version check).

Residual risks: the `CROSS_TURN_TOOL_ITEM_KINDS` doc comment says the
projection tracks web activity and image views by bare id across turns;
in fact `mergeWebActivityMessage` throws on a cross-scope merge
(pre-existing, untouched here), so those kinds only matter when the late
completion arrives as a degraded `toolCall`, which the allowlist still
handles correctly. The later turn's own details (when it has its own
summary row) still re-project the orphan completion as an extra
"unknown" tool row, as the PR body notes; pre-existing and unchanged.

> AGENT GENERATED: by Claude Opus 5

Co-authored-by: Claude <noreply@anthropic.com>
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.

1 participant