Fix timeline failures for large scoped item windows - #1398
Merged
ymichael merged 1 commit intoAug 12, 2026
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Validation
pnpm exec turbo run test --filter=@bb/db --filter=@bb/server --forcepnpm exec turbo run typecheck --filter=@bb/db --filter=@bb/servergit diff --checkpassedPerformance
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.