You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
While one thread streamed, the bb server logged 11 Event loop stalled warnings in 24 minutes, with max stalls of 1.2–1.3 s. The slowest work was GET /api/v1/threads/:id/conversation-outline for the thread with the most event data (24 MB at the time). The outline route rebuilds the full-history outline from scratch on every appended event because its cache key is ${threadId}:${maxSeq}, and the rebuild reads every toolCallitem/completed row with its full inline output, synchronously on the event loop. I expected the outline to cost a few milliseconds per event batch, not hundreds of milliseconds of blocking SQLite time that freezes every other client.
Versions and environment
bb 0.38.0 (bb --version), packaged app served from source checkout ~/projects/bb at a108fa7ef383a99cbc3c56388bb4fdf345db1492 (server started 2026-08-18 14:49, PID 3590098, Node v24.18.0)
Viewed remotely through bb Connect (bee.getbb.app); streaming thread used the claude-code provider in a worktree environment
Steps to reproduce
Observed in production (the 24 MB thread has since been truncated to 2.5 MB by something outside this report, so the 1.3 s figure is no longer live; the mechanism still reproduces):
Pick the thread with the most outline-relevant bytes:
sqlite3 ~/.bb/bb.db "select thread_id, count(*), sum(length(data)) b from events where (type in ('client/turn/requested','turn/input/accepted','turn/started','turn/completed','system/manager/user_message','system/thread/interrupted','system/error','provider/error','item/agentMessage/delta','item/plan/delta')) or (type='item/completed' and item_kind in ('agentMessage','plan')) or (type in ('item/started','item/completed','item/backgroundTask/progress','item/backgroundTask/completed') and item_kind in ('backgroundTask','toolCall')) group by thread_id order by b desc limit 3;"
On my DB today: thr_62bp724ett|3662|5670143 (5.7 MB).
Time a cold then warm outline request (the cache key is threadId:maxSeq, so the first call after any new event is cold):
Open that thread in the wide layout (so the TOC is visible) and start a turn that runs many tool calls with large outputs (for example cat a few big files). Watch the log:
Every events-appended batch invalidates the outline query on the client, the client refetches, the server misses the cache, and a full rebuild runs.
To reproduce at the original scale without waiting, pnpm seed:perf -- --reset --events 400000 and pick the largest thread the same way; I did not run this (unverified).
Did not reproduce with: a small thread (thr_qvau3b2d5b, 1.1 KB outline → 0.6–33 ms), or a warm cache (second call 1–3 ms). The timeline route for the same 24 MB thread stayed at median 69 ms / max 368 ms in the same window, so this is specific to the outline path.
Expected vs actual
Actual — ~/.bb/logs/server.66.log, 2026-08-19 22:50–23:14 UTC-7 (trimmed; slowestWork names the request that held the loop):
Expected: an outline refresh during streaming should not block the server for more than a few milliseconds, and the server should not re-read megabytes of tool output to produce a 3 KB table of contents.
Stall counts per 10 MB log file: server.61 0, .62 3, .63 6, .64 0, .65 0, .66 11 (24 min) — the spike coincides with the streaming turn on thr_n3xqyz69nk.
Why the rebuild is expensive: the outline query selects full data for every toolCallitem/started and item/completed row. On thr_62bp724ett, toolCall item/completed rows are 3.42 MB of the 5.67 MB read (60%); the outline only needs their lifecycle/grouping, not the tool output.
Measurement method: stall and slow-query lines come from the server's own attribution logger (Attribute event-loop stalls to the work that blocked them #1437); request timings from the [plugin:connect] bb connect thread load lines and curl -w; byte counts from sqlite3 against the live DB.
Any one of these removes most of the cost; the first two together are the smallest change:
In listStoredConversationOutlineEventRows, select storedEventRowFieldsWithInlineOutputLimit(<small cap>) instead of storedEventRowFields so SQLite truncates $.item.aggregatedOutput / $.item.result / $.item.resultText before the rows cross into JS. The outline never renders tool output.
Make the outline incremental: cache by threadId with the maxSeq it was built at, and on a request with a higher maxSeq read only rows with sequence > cachedMaxSeq and fold them into the cached outline (the timeline route already does a delta for afterSequence).
On the client, debounce outline refetches during streaming (for example once per N seconds while events-appended batches arrive) instead of on every batch; the TOC does not need sub-second freshness.
If the rebuild stays synchronous, add the outline query to packages/db/test/query-plans.test.ts with a bytes-read assertion so a future field addition does not regrow it.
Suggested priority and effort
Medium-High — hits any user who keeps a long, tool-heavy thread open in the wide layout while it streams; every other client on that server stalls for up to ~1.3 s per event batch; no workaround short of closing the thread. Effort Low for (1)+(3), Medium for (2).
Summary
While one thread streamed, the bb server logged 11
Event loop stalledwarnings in 24 minutes, with max stalls of 1.2–1.3 s. The slowest work wasGET /api/v1/threads/:id/conversation-outlinefor the thread with the most event data (24 MB at the time). The outline route rebuilds the full-history outline from scratch on every appended event because its cache key is${threadId}:${maxSeq}, and the rebuild reads everytoolCallitem/completedrow with its full inline output, synchronously on the event loop. I expected the outline to cost a few milliseconds per event batch, not hundreds of milliseconds of blocking SQLite time that freezes every other client.Versions and environment
bb --version), packaged app served from source checkout~/projects/bbata108fa7ef383a99cbc3c56388bb4fdf345db1492(server started 2026-08-18 14:49, PID 3590098, Node v24.18.0)main=fc6e11fe771d61dd854127e3333a0f4d46b7519f(same code paths; unchanged since Full-history conversation outline for the thread TOC minimap #399/Improve open in editor remote support #403)~/.bb/bb.dbis 3.0 GB, 980,873 event rows, 2,656 threads, WAL 58 MBbee.getbb.app); streaming thread used the claude-code provider in a worktree environmentSteps to reproduce
Observed in production (the 24 MB thread has since been truncated to 2.5 MB by something outside this report, so the 1.3 s figure is no longer live; the mechanism still reproduces):
thr_62bp724ett|3662|5670143(5.7 MB).threadId:maxSeq, so the first call after any new event is cold):cata few big files). Watch the log:events-appendedbatch invalidates the outline query on the client, the client refetches, the server misses the cache, and a full rebuild runs.To reproduce at the original scale without waiting,
pnpm seed:perf -- --reset --events 400000and pick the largest thread the same way; I did not run this (unverified).Did not reproduce with: a small thread (
thr_qvau3b2d5b, 1.1 KB outline → 0.6–33 ms), or a warm cache (second call 1–3 ms). Thetimelineroute for the same 24 MB thread stayed at median 69 ms / max 368 ms in the same window, so this is specific to the outline path.Expected vs actual
Actual —
~/.bb/logs/server.66.log, 2026-08-19 22:50–23:14 UTC-7 (trimmed;slowestWorknames the request that held the loop):The same window logged the outline's
UNION ALLquery as slow 18 times (100–266 ms each, 2,654 ms total):Connect's per-request log for the same thread: 166 outline requests in 24 minutes (up to 29/min), avg TTFB 212 ms, max 1,512 ms, response 3 KB gzip.
Today (thread now 5.7 MB of outline-relevant rows, step 2 above):
Expected: an outline refresh during streaming should not block the server for more than a few milliseconds, and the server should not re-read megabytes of tool output to produce a 3 KB table of contents.
Evidence
thr_n3xqyz69nk(projectproj_atixi2qwed); live measurement threadthr_62bp724ett; investigation threadthr_272jbuyzju.server.610,.623,.636,.640,.650,.6611 (24 min) — the spike coincides with the streaming turn onthr_n3xqyz69nk.datafor everytoolCallitem/startedanditem/completedrow. Onthr_62bp724ett,toolCall item/completedrows are 3.42 MB of the 5.67 MB read (60%); the outline only needs their lifecycle/grouping, not the tool output.fc6e11fe7):bb/packages/db/src/data/events.ts
Lines 2519 to 2590 in fc6e11f
storedEventRowFieldsWithInlineOutputLimitto cap inline output inside SQLite; the outline query does not use it:bb/packages/db/src/data/events.ts
Line 966 in fc6e11f
threadId:maxSeq, so every appended event is a miss:bb/apps/server/src/routes/threads/data.ts
Lines 422 to 433 in fc6e11f
runEventLoopWorkSync:bb/apps/server/src/services/threads/timeline.ts
Lines 1899 to 1908 in fc6e11f
events-appendedbatch:bb/apps/app/src/hooks/cache-owners/cache-invalidation-groups.ts
Lines 131 to 145 in fc6e11f
bb/apps/app/src/hooks/cache-owners/realtime-cache-registry.ts
Lines 898 to 905 in fc6e11f
[plugin:connect] bb connect thread loadlines andcurl -w; byte counts fromsqlite3against the live DB.What you ruled out
timelineroute: same thread, same window, median 69 ms.mainatfc6e11fe7(code unchanged since Full-history conversation outline for the thread TOC minimap #399/Improve open in editor remote support #403).Suggested fix
Any one of these removes most of the cost; the first two together are the smallest change:
listStoredConversationOutlineEventRows, selectstoredEventRowFieldsWithInlineOutputLimit(<small cap>)instead ofstoredEventRowFieldsso SQLite truncates$.item.aggregatedOutput/$.item.result/$.item.resultTextbefore the rows cross into JS. The outline never renders tool output.threadIdwith themaxSeqit was built at, and on a request with a highermaxSeqread only rows withsequence > cachedMaxSeqand fold them into the cached outline (the timeline route already does a delta forafterSequence).events-appendedbatches arrive) instead of on every batch; the TOC does not need sub-second freshness.packages/db/test/query-plans.test.tswith a bytes-read assertion so a future field addition does not regrow it.Suggested priority and effort
Medium-High — hits any user who keeps a long, tool-heavy thread open in the wide layout while it streams; every other client on that server stalls for up to ~1.3 s per event batch; no workaround short of closing the thread. Effort Low for (1)+(3), Medium for (2).
Investigation thread:
thr_272jbuyzju.