Skip to content

Use indexed lookups for task lifecycle events - #3212

Merged
ymichael merged 129 commits into
mainfrom
slopcop/issue-3210
Sep 10, 2026
Merged

Use indexed lookups for task lifecycle events#3212
ymichael merged 129 commits into
mainfrom
slopcop/issue-3210

Conversation

@bb-slop-cop

@bb-slop-cop bb-slop-cop Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Human comments

What was wrong

Every global thread lifecycle event made Tasks enumerate all tasks and query each task's worker mappings before filtering by the event's thread ID. On current main f41d564163180f87304986b61706db6d69314759, an unrelated event executes 202 SQL statements with 200 tasks and 505 with 501 tasks. The task-list method itself performs two statements per page, so method-call counts understated the database work. Original investigation: https://get-bb.github.io/reports/issues/3210.html.

What changed

Use an internal lookup returning every mapping for the event's thread ID through the existing idx_task_threads_thread index. Unrelated events now execute one lookup; legitimate multi-task fan-out, comments, notifications, recovery, and terminal protection remain intact. Startup and periodic reconciliation are unchanged; #3213 addresses the separate duplicate-startup issue.

Added SQL-level coverage using real in-memory SQLite and the Tasks schema migrations, alongside the original no-scan regression. Removed two existing non-semantic comments to satisfy repository lint. No schema/index, public SDK/CLI, configuration, or server/daemon wire changes; no host-daemon protocol bump is needed.

How you verified

  • Before production changes, eight SQL-budget regressions failed on unchanged current main; two reconciliation checks passed.
  • Compared baseline and fixed behavior with zero, one, 200, and 501 mappings, all five event types, and one thread mapped to 1, 3, or 200 tasks. Verified created-event status mapping, task status changes, deletion cascade, comments, recovery, idempotence, and notifications.
  • SQLite EXPLAIN confirms the existing thread-ID index is used. Sorting is limited to matching rows. These are structural SQL improvements; no end-user latency improvement was measured.
  • An expanded 21-case audit passed with each implementation's measured SQL budgets. The two startup/periodic audit cases remain outside the patch to avoid adding expectations that constrain Avoid duplicate Tasks startup reconciliation #3213.
  • pnpm exec turbo run test typecheck build lint --filter=bb-plugin-tasks: 388 tests passed in 36 files, plus typecheck/build. Tasks has no lint script, so explicit changed-file oxlint was also run.
  • Separately typechecked the new regression through Turbo. Changed-file oxlint, oxfmt check, and git diff --check passed.
  • Started pnpm start:worktree; server and daemon health checks passed. Installed/enabled the bundled Tasks plugin and confirmed 12 sample tasks across 3 projects through the running API/CLI. No browser-driven UI verification is claimed.

Fixes #3210

AGENT GENERATED

ymichael and others added 28 commits September 8, 2026 09:14
## Human comments

## What was wrong

When a user opened a child folder after scrolling deeply through a large
directory, the virtualized folder browser retained the prior directory's
scroll offset. A similarly large child therefore opened partway down
rather than at its first entry.

## What changed

The shared remote path browser now resets its virtualizer offset
whenever it navigates to a directory. Its virtualizer option callbacks
are also stable between unrelated renders, avoiding unnecessary
measurement churn. Added a regression for browsing between two large
directories. No wire, CLI, guide, or protocol changes.

## How you verified

- `pnpm exec turbo run test --filter=@bb/app --force -- --run
src/components/dialogs/RemotePathBrowser.createFolder.test.tsx`
- `pnpm exec turbo run typecheck --filter=@bb/app`
- `pnpm exec oxfmt --check
apps/app/src/components/dialogs/RemotePathBrowser.tsx
apps/app/src/components/dialogs/RemotePathBrowser.createFolder.test.tsx`
- `git diff --check`

Fixes #

> AGENT GENERATED
#3200)

## Human comments

## What was wrong

Markdown image URLs were only routed to host files when a caller
supplied `linkRouting.localImage`. Assistant messages supplied it, but
file previews and several other Markdown surfaces did not. Consequently,
absolute host paths were requested from the app origin and relative
paths were resolved against the SPA URL instead of the Markdown file's
directory. For example, `docs/report.md` containing
`![chart](images/chart.png)` should load `docs/images/chart.png`.

## What changed

- Share file-image routing across workspace, project, thread-host,
host-scoped, and thread-storage previews. Resolve relative images from
the Markdown directory using the existing local-path resolver, and use
existing content endpoints for each source.
- Share lease routing with local skill previews and message routing
across assistant, user, generated/system, and thread-context Plugin SDK
Markdown. Preserve intentionally suppressed images and explicit caller
routing overrides.
- Preserve existing root restrictions for scoped relative paths and
existing trusted absolute-host access. These restrictions are endpoint
constraints, not a sandbox for Markdown that can also reference absolute
host paths.
- Consolidate seven backend file-serving handlers through one
host-read/error-handling function, retaining their existing target
selection, caching, HTML sandbox headers, and size limits.

No public plugin API, CLI, endpoint contract, or server/daemon wire
behavior changes; no daemon protocol bump is needed.

## How you verified

- Added rendered-preview regression coverage for absolute and
file-relative images, all five preview adapters, root escapes, explicit
routing overrides, nested lease paths, user/system messages, Plugin SDK
Markdown, and local skill files.
- `pnpm exec turbo run typecheck lint test --filter=@bb/app`: 481 test
files passed; 3,890 tests passed and 3 skipped. App lint completed with
182 warnings and 0 errors.
- `pnpm exec turbo run typecheck lint --filter=@bb/server`: typecheck
passed (the server package has no lint task).
- `pnpm exec turbo run test --filter=@bb/server --
test/hosts/daemon-file-response.test.ts
test/files/host-file-routes.test.ts
test/public/public-projects-local-host.test.ts
test/public/public-project-workspace-routing.test.ts
test/public/public-thread-data.test.ts`: 5 files / 100 tests passed,
including byte responses, root propagation, host selection, validators,
HTML policies, and error mapping.
- Rebuilt and started the final code with `pnpm start:worktree`;
confirmed HTTP 200 and daemon connectivity. The dev QA thread has
workspace and thread-storage fixtures; the other preview types are
covered by automated tests, not completed manual demos.

No linked GitHub issue.

> AGENT GENERATED
## Human comments

## What was wrong

The loaded-empty branch picker followed a terminal fallback that omitted
the Branches section and used double vertical padding (`py-6`), while
the loading state rendered within the section with the standard compact
row spacing.

## What changed

Loaded-empty results without a current selection now remain in the
options section, preserving the Branches heading and the same spacing as
loading. The terminal fallback also uses the compact row padding. Added
an Empty states Ladle story and a regression test.

## How you verified

- `pnpm exec turbo run test --filter=@bb/app -- --run
src/components/pickers/BranchPicker.scroll.test.tsx`
- `pnpm exec turbo run typecheck --filter=@bb/app`
- `pnpm exec turbo run lint --filter=@bb/app` (passes with existing
warnings)
- Captured before/after Ladle screenshots using SawyerHood/doobie.

Fixes #

> AGENT GENERATED
…3147)

## Human comments

## What was wrong

Built-in questions and the ask-user-question plugin duplicated their
form UI, state, and shortcut handling. Their containers also diverged:
built-in prompts had an orange border and collapse controls while plugin
forms used a neutral border without collapse controls. Compact
permission prompts wrapped across multiple lines on mobile.

## What changed

- Use one `PendingInteractionShell` for built-in questions, approvals,
plan reviews, and plugin forms: neutral border, attention dot,
title/caret disclosure controls, and a single-line compact mobile
layout.
- Use one `QuestionForm` and answer-state implementation for built-in
and plugin questions, including tabs, single/multiple selection,
Other/free text, validation, navigation, and option previews. Delete the
duplicate implementations and consolidate form tests.
- Keep small transport adapters: built-in questions submit a
`user_answer` resolution and cancellation stops the thread; the plugin
submits `{answers}` and retains its interaction cancellation behavior.
- Route both forms through the app's configured question shortcuts,
limited to the focused pane. Collapsing pauses shortcut effects while
preserving draft text, selections, and the active question. Typing in an
input does not select answers.
- Share the internal question-host React context across plugin bundles
through a runtime shim. No public Plugin SDK, CLI, or server/daemon wire
changes.
- Title taps toggle the form; approval actions remain separate controls
in the expanded form. Escape collapses and restores focus. Attention-dot
policy is unchanged by the form refactor.

## How you verified

- App: 50 targeted tests passed across shared form/state, native
adapter/shell, plugin composer, shortcuts, and runtime installation. The
native adapter regression also checks the submitted resolution and
thread cancellation.
- Ask-user-question plugin: 30 tests passed, including submission,
cancellation, invalid payloads, server behavior, and translation.
- Plugin build: 11 tests passed, including a bundled-module check that
the question-host hook uses the host runtime instance.
- `pnpm exec turbo run typecheck lint --filter=@bb/app
--filter=@bb/shared-ui --filter=bb-plugin-ask-user-question
--filter=@bb/plugin-build` passed. Formatting and diff checks passed.
- Dev-browser against native and actual plugin-source Ladle fixtures:
desktop selections, multiselect/free text, mobile single-line collapse,
title-tap expansion, and retained draft/current question. Earlier
permission checks cover compact decisions and mobile expansion.
- Rebuilt and restarted using `pnpm start:worktree`; app responds HTTP
200 and the host daemon connects.
- Verification inventory has an unrelated existing failure: unmapped
`browser` CLI family.

## Screenshots

[Before / After screenshot
gallery](https://get-bb.github.io/reports/prs/3147.html) — 19
surfaces/states × desktop/mobile × compact/expanded, compared with PR
base `6cdb4ba612`. Both columns use the same fixture data and viewport
sizes; select a surface to compare it directly. Plugin forms without an
earlier compact mode show their actual expanded Before view with an
explicit label. Each image opens at full size.

All 38 compact captures measure 38px high with no horizontal overflow.
This includes built-in and plugin questions, command/file/tool
permissions, child-thread approvals, plan review, resolving requests,
secrets, unavailable plugins, errors, Other answers, and free-text
drafts. Compact errors use a red dot without competing with the title;
expanded errors show the full message.

Compact mode now shows the short label (for example, **Approval
needed**), dot, and caret; details, source links, and decisions appear
after expansion. Expanded headings remain readable, with child-thread
links truncating first. Attention dots align with the first line when
headings wrap. All 3,935 app tests passed (3 skipped), along with
typecheck and lint.

| Desktop compact | Mobile compact |
| --- | --- |
| ![Desktop compact before and
after](https://raw.githubusercontent.com/get-bb/reports/426258efc4d2b0366185b558db9310214e66647f/prs/assets/3147-comparison-desktop-compact.png)
| ![Mobile compact before and
after](https://raw.githubusercontent.com/get-bb/reports/426258efc4d2b0366185b558db9310214e66647f/prs/assets/3147-comparison-mobile-compact.png)
|

| Desktop expanded | Mobile expanded |
| --- | --- |
| ![Desktop expanded before and
after](https://raw.githubusercontent.com/get-bb/reports/426258efc4d2b0366185b558db9310214e66647f/prs/assets/3147-comparison-desktop-expanded.png)
| ![Mobile expanded before and
after](https://raw.githubusercontent.com/get-bb/reports/426258efc4d2b0366185b558db9310214e66647f/prs/assets/3147-comparison-mobile-expanded.png)
|

Browser captures use synthetic Ladle previews at 1280px and 390px
viewport widths; mobile is Chromium emulation. No credentials entered.
Following the compact-error change, 20 shell/controller tests, app
typecheck, and lint passed.

> AGENT GENERATED
## Summary

Root cause: the Files settings navigation item used the folder icon
instead of the existing file icon used by file @-mention rows.

Change: use the shared `File` icon and assert that navigation metadata
in the settings test.

## Verification

- `pnpm exec turbo run test --filter=@bb/app -- --run
src/components/settings/settings-nav.test.tsx`
- `pnpm exec turbo run typecheck --filter=@bb/app
--filter=@bb/shared-ui`

> AGENT GENERATED
## Human comments

## What was wrong

Completed reasoning has two valid persisted channels, summary and body,
whose deltas can arrive interleaved. The final parser originally
selected only one channel; the first version of this fix retained both
but rebuilt them as all summary followed by all body, so a live `S1 → B1
→ S2` stream reopened as `S1 → S2 → B1`. This became user-visible after
[the completed-thinking history
change](6e83eb2).

## What changed

The shared reasoning projection now keeps the arrival-order buffer and
separately totals each channel. When both totals exactly match the
completed item, completion preserves the live buffer; when deltas are
missing or the provider corrects the final payload, the combined
completed summary and body remain the fallback replacement. The same
decision feeds normal and late post-interruption completion, preserving
the generic Thought row, timing, status, single-channel behavior, and
deterministic live/restored parity. There are no wire, database, CLI,
Plugin SDK, configuration, documentation, or mobile changes.

## How you verified

- Added an assembler-valid `summary S1 → body B1 → summary S2`
regression that failed before the projection change and passed after it
across active, just-completed, restored, and replayed projections.
- `pnpm exec turbo run test --filter=@bb/thread-view --force`: 23 files
and 379 tests passed.
- `pnpm exec turbo run typecheck --filter=@bb/thread-view --force`
passed.
- Repeated both Turbo checks after rebasing onto current local
`origin/main`.
- DevBrowser at the same real route and 1440×1000 viewport showed the
completed row change from `S1 → S2 → B1` to the live `S1 → B1 → S2`
order.

Fixes #1248

> AGENT GENERATED
## Human comments

## What was wrong

The shared timeline window closure treated every scoped item that
crossed a page boundary as belonging to the newer sequence window. For a
completed thinking item whose start was in an older user turn but whose
completion arrived after the next prompt began, standard turn pagination
therefore separated the lifecycle: the newest page rendered the
completion beneath the wrong prompt as “Thought for 0ms,” and loading
the older page left the originating turn without its thought. This
followed
[6e83eb2](6e83eb2)
from #3066.

A thought that never receives `item/completed` could also disappear from
both pages when its original turn moved into older history.

## What changed

- Expose the completion sequence in the existing item-span query.
- Give the shared whole-item closure explicit origin-versus-later page
ownership.
- Keep an unfinished thought on its origin page until it receives a
newer-page event; then keep the active lifecycle on the newer page
without duplication.
- Assign completed items to their originating user segment while
preserving existing later-page ownership for byte and details windows.
- Constrain span lookups to item events and distribute their thread/type
constraints into each scoped lookup, allowing SQLite to use the existing
`(thread_id, turn_id, type, item_id, sequence)` index.
- Guard that multi-turn query plan and the unfinished and late-completed
latest/older/details behavior with focused regressions.

There are no database schema or migration, host-daemon wire, CLI,
documentation, UI-component, pagination-limit, or mobile changes.

## How you verified

- The focused regression fails before the fixes: late completion appears
on the newest page, while a never-completed thought disappears from both
pages.
- `pnpm exec turbo run test --filter=@bb/server --
test/services/threads/timeline-in-turn-window.test.ts` — 28/28 passed
after rebasing onto current local `origin/main`.
- `pnpm exec turbo run test --filter=@bb/db` — 448/448 passed.
- `pnpm exec turbo run typecheck --filter=@bb/server` and `pnpm exec
turbo run typecheck --filter=@bb/db` — passed.
- The committed multi-turn `EXPLAIN QUERY PLAN` regression proves the
span lookup uses `events_thread_turn_type_item_sequence_idx`; no new
index is created.
- DevBrowser verified the real app history boundary: after loading the
older page, the original turn expands through “Worked for 5s” to the
interrupted “Thought for 2s,” its text, and “Stopped manually,” with no
thought under the next prompt.

> AGENT GENERATED
## Human comments

## What was wrong

The tooltip test compared a 250 ms real-time sleep with the production
700 ms dismissal timer. If the test worker was suspended past 700 ms,
React's async `act` could process the already-due dismissal before
returning from the shorter sleep, so the intermediate assertion observed
null even though production retained the correct 700 ms delay. A bounded
750 ms event-loop suspension reproduced the exact current-main assertion
failure.

## What changed

Theme Preview's test now switches to fake timers only after focus has
opened the tooltip, then checks the exact dismissal boundary: content
remains at 699 ms and disappears after the final 1 ms. Production code
and its 700 ms delay are unchanged.

## How you verified

- Before: focused current-main test passed unloaded and in 6 contended
runs; a bounded 750 ms event-loop suspension reproduced `expected null
not to be null` at the intermediate assertion.
- After: focused regression test passed.
- After: 10/10 focused runs passed with four bounded CPU-load workers
and two concurrent single-worker Vitest lanes; cleanup left zero load or
Vitest processes.
- `pnpm exec turbo run test --filter=bb-plugin-theme-preview
--concurrency=2 --output-logs=full --summarize=false` (74 tests passed)
- `pnpm exec turbo run typecheck --filter=bb-plugin-theme-preview
--concurrency=2 --output-logs=full --summarize=false`
- `pnpm exec turbo run build --filter=bb-app --concurrency=2
--output-logs=full --summarize=false` (12 tasks passed)
- `git diff --check`

> AGENT GENERATED: by GPT-5.6-Sol
## Human comments

## What was wrong

The Docs jsdom suite provided `matchMedia` but no Range geometry
boundary. jsdom 29 exposes Element geometry while omitting
`Range.getClientRects()` and `Range.getBoundingClientRect()`.
ProseMirror 1.41.8 calls those methods while reconciling an edited table
and scrolling its selection, so the uncaught TypeError aborted the
editor update before the existing 700 ms autosave could call `saveNote`.
The 2-second assertion in [the failing current-main
run](https://github.com/get-bb/bb/actions/runs/33923531372) reported
that upstream exception rather than a slow save.

## What changed

The Docs app test setup now provides zero-sized, browser-shaped Range
geometry for jsdom and restores the original property descriptors after
every test. No app behavior, timer, assertion, wire contract, CLI, or
documentation changed.

## How you verified

- Confirmed the cited push run failed at
`c49ba3f546d1049f0d0f4e51850908028a318d52` with `TypeError:
target.getClientRects is not a function` from ProseMirror `singleRect` /
`coordsAtPos`.
- Confirmed directly against the installed jsdom 29 environment that
both Range geometry methods are undefined while Element geometry exists.
- `pnpm exec vitest run --config plugins/docs/vitest.config.ts
plugins/docs/app.test.tsx -t "renders and autosaves editable Markdown
tables" --reporter verbose --maxWorkers=2` (1 passed; unchanged 2-second
wait).
- `pnpm exec turbo run test --filter=bb-plugin-simple-notes
--concurrency=2 -- --maxWorkers=2` (66 passed).
- `pnpm exec turbo run typecheck --filter=bb-plugin-simple-notes
--concurrency=2`.
- `pnpm exec turbo run build --filter=bb-plugin-simple-notes
--concurrency=2 --force`.
- `pnpm exec prettier --check plugins/docs/app.test.tsx`.

> AGENT GENERATED: by GPT-5.6-Sol
…oved (#3099)

## Human comments

Similar to the other PR I just opened, this almost drove me nuts
:sweat_smile: I'm now rebuilding my container with both patches and will
report back if anything goes wrong.

## What was wrong

When `update_environment_directory` moved a thread to a new environment
(e.g. the agent deleted the old worktree first), the next turn died at
the bridge: `thread/resume` rejected with `Cannot resume: the pi
session's working directory "...no longer exists."` because the pi
session file still recorded the old cwd — even though the runtime
supplied a valid, existing requested cwd. In the observed case the
thread was effectively bricked until an external stop/recovery.

Root cause: `resumedSessionMissingCwd`
(`plugins/provider-pi/src/bridge/bridge.ts:532`) rejects the whole
resume whenever the persisted cwd is gone, without considering the
requested cwd.

## What changed

- `thread/resume` now only rejects when the **requested** cwd is also
missing — i.e. there is genuinely nowhere to run. With the persisted cwd
gone but a valid requested cwd present, resume proceeds.
- `constructPiThreadSession` prefers the persisted session cwd only when
the directory still exists (new `usablePersistedSessionCwd()`), falling
back to the requested cwd for the child spawn and delta-translation
`sessionCwd`. When the persisted cwd still exists, behavior is unchanged
(session reports the header cwd).
- No wire/protocol changes; provider-bridge-only. No CLI/guide/doc
updates needed.

## How you verified

- New regression test `resumes at bb's requested cwd when the session
header's cwd was removed` (`bridge.round2.test.ts`): **red** before the
fix (reproduced the exact user error text), **green** after.
- Existing `a resumed thread reports the session header's cwd, not the
cwd bb asked for` still passes — persisted-cwd precedence preserved when
the dir exists.
- `bridge.round2` + `bridge.settings` + `bridge.checkpoint-fork`: 15/15
pass. `turbo run typecheck --filter=bb-plugin-provider-pi`: clean.

> AGENT GENERATED
> <sub><em>Generated with "Omen Alpha" (Opencode Go) via BB /
Pi</em></sub>
## Human comments


## What was wrong

Realtime workspace-change invalidation deleted every cached per-file
patch before the refreshed diff table of contents could establish
whether its contents had changed. Visible cards therefore regressed from
rendered content to loading skeletons, including for filesystem changes
that produced the same Git diff.

## What changed

Retain per-file patch cache across realtime workspace invalidations.
When the diff table of contents refreshes, refresh visible cached
patches in place while retaining their rendered content; replacement
patch content is rendered only after it arrives. Initial patch seeding
only notifies React when its patch payload actually changed. No wire or
CLI changes.

## How you verified

- `pnpm exec turbo run test --filter=@bb/app -- --run
src/hooks/realtime-cache-effects.test.ts
src/hooks/queries/use-environment-diff-patches.test.tsx`
- `pnpm exec turbo run typecheck --filter=@bb/app`
- `pnpm exec turbo run lint --filter=@bb/app`
- `git diff --check`

Fixes #

> AGENT GENERATED
## Human comments
Choosing a color palette in different editors (take Zed) allows you to
preview when scrolling through them on Command Palette - wanted to bring
similar functionality here.

If I had to choose, I would also do a transition-colors but deemed that
unnecessary.

## What was wrong

Choosing a palette in Settings → Appearance required users to choose the
selection in order to preview it. Takes a lot of time to review themes
and a bit harder to choose from there.

## What changed

- Hovering or arrow-keying over a palette item now applies that theme to
the current window as an ephemeral preview. Leaving the item, closing
the menu, or leaving Settings restores the committed theme. Selecting an
item keeps the preview in place until the saved appearance lands, so
there is no flash back to the old palette.
- `apps/app/src/lib/themes/index.ts` and `lib/code-theme.ts` gain a
preview layer over the committed palette CSS and code theme. Previews
never touch the localStorage pre-paint cache, and a committed apply
always supersedes a preview.
- New `useAppThemePreview` hook resolves themes through react-query
(`systemTheme` query key, 60s stale time) and prefetches every listed
palette when the menu opens so the first hover applies from cache.
Resolved themes are invalidated with system config, including realtime
`config-changed`.
- New route `GET /api/v1/settings/themes/:id` resolves a built-in,
custom, or plugin theme without activating it (400 for a malformed id,
404 for a missing custom theme). The server shares the validation with
`PUT /settings/appearance`. No host-daemon wire change, so
`HOST_DAEMON_PROTOCOL_VERSION` is unchanged.
- SDK: `sdk.theme.resolve({ themeId })`. CLI: `bb theme show [id]
[--css]` resolves a theme by id without activating it. Updated the CLI
guide (`bb-guide-customization.md`) and the bb-cli skill reference
(`theme-commands.md`).
- Bumped `@get-bb/plugin-sdk` to 0.4.48 with
`scripts/bump-plugin-sdk.mjs` because the bundled `ThemeArea` type
changed and 0.4.47 is already on npm.

## How you verified

Hand-tested.


https://github.com/user-attachments/assets/441a8fd8-e235-482c-958c-60a81044490d



Additionally:
- New tests: `apps/app/src/lib/themes/preview.test.ts` (preview does not
persist, commit supersedes preview),
`apps/app/src/hooks/useAppThemePreview.test.tsx` (stale resolutions
ignored, cache reuse, prefetch, unmount clears),
`apps/app/src/views/SettingsView.palette-preview.test.tsx`
(focus/blur/Escape/select flows, prefetch on open),
`packages/sdk/test/sdk.test.ts` (`theme.resolve` transport), and
`tests/integration/fake/smoke/theme-resolve.test.ts` (built-in, custom,
404, 400, active theme untouched).
- `pnpm exec turbo run typecheck lint` across the repo passed.
- `pnpm exec turbo run test --filter=@bb/app --filter=@bb/cli
--filter=@bb/sdk --filter=@bb/server-contract` passed (483 app files, 54
CLI files, 6 SDK files, 7 contract files).
- `node packages/plugin-sdk/scripts/check-npm-version-guard.mjs` passed
for 0.4.48.
- `oxfmt --check` on every changed file and `git diff --check` passed.

Fixes: no linked issue.

> AGENT GENERATED

---------

Co-authored-by: vznh <nailguninthebackpocket@gmail.com>
Co-authored-by: Sawyer Hood <sawyerjhood@gmail.com>
Co-authored-by: Michael Yong <wrong92@gmail.com>
…ssions (#3203)

## Human comments

## What was wrong

Account Pooler used the process-wide fetch dispatcher for upstream
requests. In #3201, a cached upstream HTTP/2 session was destroyed but
continued failing requests immediately with `TypeError: fetch failed`,
caused by `The session has been destroyed`
(`ERR_HTTP2_INVALID_SESSION`). This was observed for both the Anthropic
(`api.anthropic.com`) and ChatGPT/Codex (`chatgpt.com`) origins on the
same host: whichever origin's cached session was destroyed failed every
request to it, while the other origin could remain healthy. Clearing the
affected origin's cached pool restored a real pooled request. The hub
discarded the fetch cause, leaving only a generic 502 and obscuring the
transport failure. The event that originally destroyed the session
remains unknown.

## What changed

Give Account Pooler an owned `EnvHttpProxyAgent` with HTTP/2 disabled,
covering proxied inference, token refresh, usage requests, and OAuth
login. Because this transport is used for every upstream, the fix
applies uniformly to all providers (Claude and ChatGPT/Codex), for both
the plain HTTP path and the Codex realtime path, which forwards its
upstream leg through the same hub transport. It honors proxy environment
variables and avoids reuse of the process-wide HTTP/2 pool. Disposal
waits for the hub's existing request-drain window before destroying
connections. No request retries are added.

Retain the original fetch cause internally and log only a recognized
transport error code, without raw error messages or credentials.
Document the transport behavior in `PLUGIN_OVERVIEW.md`. There are no
CLI or server/daemon wire changes.

## Trade-offs

- Upstream traffic now uses HTTP/1.1 instead of HTTP/2. This is the
mechanism that avoids reusing a poisoned shared H2 session, and undici's
HTTP/1.1 pool discards dead sockets rather than caching a destroyed
session. The cost is losing H2 multiplexing: under high concurrency the
pooler opens more connections. Both providers support HTTP/1.1 including
streaming, so this is an efficiency trade, not a correctness one.
- This isolates the pooler from the shared dispatcher and avoids the
observed failure mode. It does not identify or fix the underlying
trigger that destroys a session, and it adds no retry, so a connection
that dies mid-request still fails that request.

## How you verified

- `pnpm exec turbo run test typecheck --filter=bb-plugin-account-pool`:
261 tests passed across 10 files; typecheck passed.
- `pnpm exec turbo run build --filter=@bb/server`: passed. Scoped Oxlint
and `git diff --check` passed.
- Regression coverage injects the observed destroyed-session error at
the default-fetch boundary and verifies that the owned transport
completes actual local requests. A committed TLS/ALPN test also verifies
HTTP/1.1 against a server offering both protocols; temporarily enabling
HTTP/2 makes it fail with `h2` / `2.0`, as expected. The natural
session-destruction trigger remains unknown.
- A streamed-response disposal regression failed before adding the drain
wait and passes after it. Additional tests cover cancellation, no replay
after an accepted POST loses its connection, and sanitized logging.
- All six transport tests, including TLS/ALPN negotiation, also passed
directly under Node 22.23.2 as a compatibility investigation. The full
plugin suite ran on Node 26.7.0. Local transport tests bypass ambient
proxies.
- Cross-provider transport check: unauthenticated requests through the
new transport reached both `https://api.anthropic.com` and
`https://chatgpt.com`, each returning the expected HTTP 401 rather than
the destroyed-session error, confirming the fix covers both providers.
The installed BB runtime was not replaced with this build.

Fixes #3201

> AGENT GENERATED
## Human comments

## What was wrong

Assistant messages passed incomplete Markdown straight to the renderer.
During streaming, unfinished bold and inline code showed their
delimiters, while partial links and images exposed unfinished syntax.

## What changed

Use pinned `remend@1.3.1` on the displayed live tail in
`AssistantConversationMessage`. This also covers the first paragraph,
before a settled/tail split exists. Incomplete links render as plain
text and incomplete images stay hidden. Settled content, completed
messages, and the original text used by message actions remain
unchanged.

Tails containing directives or triple code fences bypass repair. Probes
found that remend can alter partial directive syntax and tilde-fenced
code. Repair resumes after those move into the settled prefix. Unrelated
HTML, math, setext, comparison, and single-tilde transformations are
disabled.


https://github.com/user-attachments/assets/3b7de693-7fa2-4f6c-adba-c56db0200503

## How you verified

- Captured six failing repair regressions before the implementation.
- Passed 72 tests across the conversation, streaming split, directive,
and local-link suites. These include real-renderer checks for
formatting, link/image completion, directive attributes, thread
mentions, file navigation, and copying original source.
- Passed `pnpm exec turbo run typecheck lint --filter=@bb/app`. Lint
reports 184 warnings and no errors; none names the changed conversation
component.
- Passed `pnpm exec turbo run build --filter=@bb/app` and formatting
checks on the changed source files.
- Passed eight browser replay assertions using the actual conversation
component from the source-built dev app. Checked partial bold/code,
split tails, incomplete links/images, literal directives, tilde fences,
and stopped generation. Captured a short streaming comparison. This is a
component replay, not an end-to-end provider turn.

Focused test command:

```sh
pnpm exec turbo run test --filter=@bb/app -- \
  src/components/thread/timeline/ConversationMessageContent.streaming.test.tsx \
  src/components/thread/timeline/ConversationMessageContent.streaming-render.test.tsx \
  src/components/thread/timeline/ConversationMessageContent.test.tsx \
  src/components/thread/timeline/streaming-markdown-split.test.ts \
  src/components/ui/markdown-message-directives.test.tsx \
  src/components/ui/markdown-local-file-link-normalize.test.ts
```

> AGENT GENERATED

---------

Co-authored-by: Sawyer Hood <sawyerjhood@gmail.com>
## Human comments

## What was wrong

Live thinking used a separate indicator with italic details, while
completed thoughts became generic system rows with monospace details and
lost their expansion state. Persisted thoughts also broke activity
groups when interleaved with tool work. Routine provider environment
resolution also appeared in the conversation unconditionally, and
development builds ignored the existing unhandled-event visibility
setting.

## What changed

- Give reasoning its own timeline operation kind and a stable identity
across completion. Active and completed thoughts share disclosure
styling and prose details, and preserve manual expansion within the
mounted thread. Completed thought headers use the same dimming as
completed tool rows. Only completed thoughts display the AiBrain01 icon;
live Thinking has no leading icon. Completed durations use the existing
timeline treatment, which hides durations of one second or less.
- Replace `showUnhandledProviderEvents` with `showDiagnosticEvents`,
covering environment resolution and unhandled provider events. It
defaults to false in all builds; existing saved preferences carry over.
Preserve the old setting name as a deprecated API alias for existing SDK
clients, normalizing both names at the route boundary. Warnings and
model fallback remain visible, and completed warnings/deprecations no
longer receive completed-row dimming.
- Update timeline cache options, API/SDK settings, CLI coverage,
configuration docs, guide templates, and the CLI skill. The CLI setting
is `bb settings general showDiagnosticEvents <true|false>`.
- Bump the plugin SDK to 0.4.49 because the settings and timeline type
changes alter its bundled declarations.
- Carry a canonical reasoning disclosure ID separately from the
structural row ID so delegation nesting preserves expansion. Add a
reasoning lifecycle story and regression coverage. No server/host-daemon
wire payload changed, so no daemon protocol bump is needed.

- Register AiBrain01 across shared and mobile icon maps and regenerate
the plugin icon registry. Use the existing Bot icon for Tasks agent
presets in the sidebar and management panel.
- Allow 60 seconds for the existing Account Pool 4,096-session eviction
test after repeated CI timeouts at 20 seconds; retain the workload and
assertions.

- Include completed thoughts in activity groups, retaining chronological
details while deriving labels only from work. Thoughts-only sequences
stay visible, and messages, warnings, and approval boundaries still
split groups. Update shared expansion handling and CLI rendering for
mixed children.

- Exclude disabled diagnostics in database selection, event/byte
budgets, nested-child hydration, and effective timeline cache sequences.
Retain legacy Claude model-fallback events.

- Normalize canonical reasoning IDs in provider replay comparisons.
Recognize legacy fallback records by their validated event shape in both
SQL selection and projection, removing provider-specific branching from
this path.

## How you verified

- CI follow-up: all 56 provider parity tests and 238 bridge-protocol
tests pass, as do fallback extraction/visibility regressions and the
provider-literal architecture guard.

- Review regressions cover old SDK payloads and read-modify-write
updates through either setting name, canonical nested reasoning identity
and expansion, a hidden 5 MiB environment event, unchanged visible cache
sequence, and legacy model fallback. Database suite (449 tests), nested
projection suite (74), reasoning UI suite (4), and affected
typechecks/lint pass.

- Dimming and reasoning UI tests pass (14 tests); browser confirms
completed thought headers use the same 0.4 opacity as completed work.

- Grouping verification: 382 thread-view tests and 254 client-core tests
pass; all three reasoning UI tests pass. App, mobile, CLI, thread-view
and client-core typechecks and lint pass. Browser fixture confirms
grouped thoughts expand and retain expansion when a running exploration
bundle becomes a closed exploration/edit step.
- Updated reasoning lifecycle tests pass: live thinking omits the icon,
completion adds it, and expansion persists.
- Tasks plugin typecheck passes after switching preset icons to Bot.
- Plugin registry typecheck/tests and the Account Pool eviction test
pass locally.
- App lint and the plugin SDK npm version guard pass after fixing the
expansion callback dependency and bumping the SDK.
- Turbo typechecks passed for app, server, CLI, database, domain,
thread-view, and server-contract.
- Full database and thread-view suites passed; subsequent targeted
projection/title tests covered the added stable-identity regression.
- Focused app tests cover expansion through completion, independent
subsequent thoughts, icon-free live thinking and icons on completed
thoughts, warning readability, and settings cache invalidation. CLI
settings tests passed.
- Server timeline-window/cache tests and production/development
diagnostic visibility tests passed, including toggling cached results
off, on, and off while retaining warnings.
- In an isolated source app, exercised the Settings toggle, verified
persistence after reload and through the source CLI, and checked
diagnostic visibility in a synthetic thread and expanded turn details.
Browser-driven production component checks confirmed expanded Thinking
becomes expanded Thought with preserved prose styling.
- `pnpm start:worktree` built and served successfully; server and
host-daemon health checks passed. Browser checks used Chromium on macOS,
not iOS or a real provider turn.
- `git diff --check` passed and the branch merges cleanly with current
main.

> AGENT GENERATED
## Human comments

## What was wrong

A pinned request that received a short per-minute 429 waited
independently from another request for the same session. The second
request could finish its remaining hold timer while the wall clock still
reported the account as held; because routing tracked only that it had
waited once, it returned 429 without making its permitted retry. This
produced the four-attempt sequence seen in [current-main
CI](https://github.com/get-bb/bb/actions/runs/33977434733) and again in
[an unrelated PR
run](https://github.com/get-bb/bb/actions/runs/34155037225).

## What changed

Account Pool now records the active per-account pacing flight with its
exact hold deadline. Requests that arrive during that hold join the same
completion promise and then make their one permitted retry. A completed
successful flight remains observable until the wall deadline expires,
while a renewed or failed attempt releases it so repeated hold
extensions stay bounded. Hub restart also clears the in-memory flights.

The regression keeps the original 250 ms hold and pins the clock one
millisecond before the deadline, proving the concurrent request follows
the pacing completion event instead of returning a clock-bound 429. No
hold duration, retry budget, timeout, wire contract, CLI, or public API
changed. The separate 4096-session LRU timeout is not part of this
change.

## How you verified

- Red: the controlled-clock regression failed before the implementation
with `expected 429 to be 200`.
- Green focused: the short-hold regression and repeated-hold bound both
passed.
- `pnpm exec turbo run test --filter=bb-plugin-account-pool
--concurrency=1 -- --maxWorkers=1 --no-file-parallelism` — 9 files and
253 tests passed.
- `pnpm exec turbo run typecheck --filter=bb-plugin-account-pool
--concurrency=1` — 4 Turbo tasks passed.
- `pnpm exec turbo run build --filter=bb-plugin-account-pool
--concurrency=1` — the two generated prerequisites passed; this source
plugin has no package build script.
- `pnpm exec oxfmt --check plugins/account-pool/src/hub.ts
plugins/account-pool/src/server.test.ts` and `git diff --check` passed.

> AGENT GENERATED: by GPT-5.6-Sol
## Human comments

## What was wrong

The affinity-eviction integration test filled the production capacity by
sending 4,095 sequential requests through the complete fake plugin host.
Each request repeated parsing, account/quota selection, persistence,
routing, and response consumption even though the eviction itself is an
O(1) `Map` operation. On the untouched Intel baseline the single test
took 10.7–12.2 seconds without real network I/O, leaving little headroom
under its original 20-second timeout. The [current-main
failure](https://github.com/get-bb/bb/actions/runs/34256473278/job/102163451056)
ran the packages shard with four concurrent Turbo package tasks on four
vCPUs while Account Pool Vitest also parallelized its ten files; that
systemic nested CPU oversubscription made the serial test intermittently
exceed 20 seconds. The merged 60-second timeout only masked this
structural cost.

## What changed

Thread an internal `maxAffinityBindings` construction option into the
hub and apply the existing 4,096 production default once at that
boundary. The end-to-end LRU test injects a capacity of four and keeps
the same recency refresh, retention, next-oldest, and eviction
assertions while removing 4,092 redundant HTTP requests. The custom
60-second timeout is removed, restoring the normal test timeout as the
hang detector.

There are no CLI, user configuration, SDK, server/daemon wire,
migration, or protocol-version changes.

## How you verified

- Before the change, the focused untouched-main case took 12,163ms
unloaded and 10,700–10,820ms in two bounded contention samples; CI
independently recorded the exact 20-second timeout under the packages
shard.
- After the change, the focused case passed in 125–131ms through Turbo
on the rebased commit.
- `pnpm exec turbo run test typecheck --filter=bb-plugin-account-pool
--concurrency=2 --force`: 10 files and 261 tests passed; typecheck
passed.
- `pnpm exec turbo run typecheck --filter=bb-plugin-account-pool
--concurrency=2 --force`: passed after rebasing onto current main.
- `pnpm exec turbo run build --filter=@bb/server --concurrency=2
--force`: passed before and after the rebase.
- Scoped Oxlint, Oxfmt check, and `git diff --check`: passed.
- Final merge-base: `d9f2ac5a4390b8b8507da3cc32a3bae01fc3f868`.

> AGENT GENERATED: by GPT-5.6-Sol
## Human comments

## What was wrong

Main implements three workspace kinds directly in core server and
host-daemon provisioning code: the project's checkout, a managed Git
worktree, and a personal directory. Adding another environment requires
extending those switches and their cleanup paths. Plugins cannot supply
workspace creation/removal, validate inputs, report availability, or
render the associated composer controls through a common environment
contract.

## What changed

[Reviewer report — overview, API, review fixes, and before/after
screenshots](https://get-bb.github.io/reports/prs/3227.html)

Environment plugins own resource operations; core owns placement,
durable launches, cancellation, recovery, retries, retirement, and
teardown. The three existing behaviors are bundled, auto-installed
plugins. Main's fixed defaults remain: a Git project defaults to a fresh
worktree, a non-Git project uses its checkout, and a projectless root
gets a personal workspace. A project child gets a fresh worktree; a
projectless child shares its parent's environment.

The request shape already names an enrolled machine inside the provider
selection:

```ts
{
  type: "provider",
  environmentProviderId: "git-worktree",
  machine: { type: "existing", hostId },
  inputs: { branch: { kind: "default" } }
}
```

`bb.experimental_environments.register(...)` accepts `create` and
`remove` operations with `AbortSignal`, plus availability, validation,
input schemas, and lifecycle policy. The type-only
`@get-bb/plugin-sdk/environment-provider` entry exports unprefixed
types. New runtime values remain experimental. Creation returns a path
and ownership flag; removal receives the persisted provider resource and
stable path key. Core persists launch progress and settles interrupted
attempts in
`apps/server/src/services/environments/provider-orchestration.ts`.

| Bundled plugin | Responsibility | Path ownership |
| --- | --- | --- |
| `environment-project-checkout` | Reuse the project checkout; own the
Current/New branch/Checkout control and validate branch mutations |
Keeps the checkout |
| `environment-git-worktree` | Create an isolated worktree and recover
interrupted creation; core runs setup/teardown | Removes the worktree |
| `environment-personal-workspace` | Create a separate directory for a
projectless root thread | Removes the directory |

`packages/environment-provider-host` supplies shared host-side
primitives. The daemon now attaches resolved paths with
`environment.attach`/`environment.attach.cancel`; it no longer
provisions or destroys environments. Git inspection reports
`isWorktree`. `HOST_DAEMON_PROTOCOL_VERSION` increases from **183 to
193**, and the SDK patch-bump script advances **0.4.49 to 0.4.50**.

Migration **0113** is the single migration from main: it converts legacy
environment kinds and pending intents to provider records, records
owning plugin IDs, indexes active path claims, remaps lifecycle
statuses, and drops the old managed/provision-type/cleanup columns. It
creates neither canonical-path storage nor a hook-operation table. The
implementation uses stored paths without host-wide canonicalization or
backfills. Published environments expose provider fields and lifecycle;
deprecated `managed`, `workspaceProvisionType`, and display kind are
derived compatibility fields.

Provider policy exposes only retirement grace and path-key strategy.
Core uses fixed retry defaults (60 seconds for removal; three transient
creation retries 30 seconds apart) with no overall provider-create
timeout. Setup/teardown scripts retain their separate 15-minute timeout.

Hooks use IDs derived from existing launch/environment identity and are
tracked only in daemon memory. A server restart can join a known daemon
operation. If a daemon restart loses hook state, core reports an unknown
outcome and blocks automatic cleanup rather than rerunning the script or
deleting its workspace. Recovery then requires manual inspection; there
are no persisted hook files, PID reconciliation, or separate hook
ledger.

API and CLI additions include environment provider discovery,
environment list/show/delete, `bb thread spawn
--environment-provider/--environment-inputs`, and `bb thread list
--environment`. Existing path shorthand and legacy request forms
normalize at the server boundary. Hooks expose only `environment` or
`provider` environment intents, and add `thread.unarchived` and
`message.cancelled`. SDK areas, host helpers, Plugin Guide/API map,
audit documentation, built-in authoring and CLI skills, guide chapters,
and READMEs are updated together.

User-facing changes:

- Provider rows appear in fixed order: Project checkout, Worktree, then
additional environment providers. Personal workspace is
projectless-only. Availability and plugin input blockers explain
unusable choices.
- Plugins can supply environment inputs and use
`experimental_useBranches`, `experimental_useCheckoutState`, and the
seven-prop `experimental_BranchPicker`. The checkout plugin preserves
main's chip interactions.
- Reuse rows show environment identity and threads without provider
text. Follow-up and Info panels distinguish available, archived, failed,
and destroyed environments; failed creation reads **Not created ·
provisioning failed**.
- Sidebar grouping uses `isWorktree`; the plugin catalog gains
**Environments**; General uses **New branch prefix**.

This change includes no machine-provider API, machine launch table, host
lifecycle columns, installer behavior changes, machine controls,
new-machine selection, Modal plugin, or SSH plugin. Two existing test
fixtures were made portable: README-directory copy errors accept macOS
`ENOTSUP`, and the nested-ignore watcher test accounts for native event
coalescing and checks nested paths specifically. Production installer
and watcher behavior are unchanged.

## How you verified

- Migration squash: all **448 database tests** and **248 SDK tests**
pass, including legacy provider/owner and pending-intent conversion.
- Latest simplification: server orchestration **40 tests**, real hook
interruption integration **3 tests**, daemon script/dispatch **12
tests**, database **448 tests**, daemon contract **54 tests**, and
Plugin Guide **74 tests** pass.
- Typecheck/lint passes for server, daemon, database, daemon contract,
and integration packages.
- Regression coverage includes joining a still-running hook after server
restart, ordinary cancellation before cleanup, and blocking cleanup for
unknown daemon hook state.
- Earlier targeted checks cover offline stored-path filtering and
rejection of removed provider policy settings; the SDK and bundled
environment provider typechecks pass.
- `git diff --check` passes. The local worktree app is rebuilt and
restarted with the latest migrations.

> AGENT GENERATED
…ns (#3268)

## Human comments

## What was wrong

The machine authentication proxy piped upgraded WebSocket sockets
without handling their errors or closing the peer. An upstream reset
could therefore raise an uncaught `ECONNRESET` in the host daemon.
Separately, reconnect reconciliation labeled a missing active turn as a
daemon restart even when the reconnecting process had the same instance
ID.

## What changed

Handle errors and close both sides of upgraded proxy connections,
including clients that disconnect during the upgrade. When the same
daemon reconnects without a previously active turn, emit the existing
`host-connection-lost` cause so the timeline reports a lost connection;
preserve the restart message for a different daemon instance. No
server/daemon wire fields or public API changed, so no protocol bump is
needed. These fixes do not address the underlying Connect tunnel
instability.

## How you verified

- Reproduced an uncaught `read ECONNRESET` with a real upstream TCP
reset before the proxy fix; the regression passes afterward. Coverage
also exercises client-side resets and peer cleanup.
- `pnpm exec turbo run test --filter=@bb/host-daemon -- --run
src/machine-auth-proxy.test.ts` — 10 passed.
- `pnpm exec turbo run test --filter=@bb/server -- --run
test/internal/background-task-reconciliation.test.ts` — 12 passed,
covering missing/still-active turns on same-instance reconnect, actual
daemon replacement, and disconnect grace behavior.
- `pnpm exec turbo run typecheck --filter=@bb/host-daemon
--filter=@bb/server` — passed.
- `git diff --check` — passed. EAP codename scan: clean working tree,
HEAD, added lines, and commit messages in the push range.

> AGENT GENERATED
## Human comments

This is something that almost made me give up on bb but my clanker got
through and was able to diagnose the problem. TBH, not sure how this
hasn't been reported before, I got it to do some searching on the repo
issues / PRs / discussions but it did not find anything related.

Anyways, I hope this is the right way to fix the problem but if not,
happy to provide more info about my environment as a separate ticket for
discussion and investigation.

I'm about to rebuild my container with this patch + another one I got in
the works and will report back if it goes sideways before marking the PR
as ready for review.

## What was wrong

The pi provider's dynamic tools (`AskUserQuestion`,
`update_environment_directory`, plugin agent tools) hang forever under
the shipped pi. pi has been distributed as a Bun-compiled binary since
v0.55.0 ([release
assets](https://github.com/earendil-works/pi/releases/tag/v0.55.0),
built via `bun build --compile` in [earendil-works/pi
scripts/build-binaries.sh#L205](https://github.com/earendil-works/pi/blob/main/scripts/build-binaries.sh#L205)),
and pi 0.85.0's binary embeds Bun 1.3.14.

Bun's `net.Socket({ fd })` validates the fd and attaches no handle
(`_handle === null`, no poll registered — see [bun-v1.3.14
net.ts#L684](https://github.com/oven-sh/bun/blob/bun-v1.3.14/src/js/node/net.ts#L684)
and the open upstream fix PRs
[oven-sh/bun#35341](oven-sh/bun#35341),
[#29141](oven-sh/bun#29141)). The
bridge→extension channel read
([bb-pi-extension.ts#L177](https://github.com/get-bb/bb/blob/c49ba3f54/plugins/provider-pi/src/bridge/bb-pi-extension.ts#L177))
relied on a Node-only behavior, so under the compiled pi every dynamic
tool result was written but never read: the extension's tool promise
never settled, pi never emitted `tool_execution_end`, and turns hung
until manually stopped.

This is not a Bun regression (read adoption has never shipped) and
cannot be worked around by pinning Bun because pi embeds its runtime.
The existing suite missed it because the fake pi loads extensions under
Node, where the fd semantics work.

## What changed

- `plugins/provider-pi/src/bridge/bb-pi-extension.ts` — the
bridge→extension channel reader branches on the runtime: under Bun it
reads fd 4 through `Bun.file(fd).stream()` (same newline framing and
`StringDecoder` handling); under Node the existing `net.Socket` path is
unchanged, with its non-blocking comment moved onto that branch. No
message formats, wire content, or fd assignments changed — server↔daemon
payloads are untouched, so `HOST_DAEMON_PROTOCOL_VERSION` is not bumped.
- `plugins/provider-pi/src/bridge/fake-pi-rpc.mjs` — test fake: under
runtimes without `module.registerHooks` (Bun) it stages the extension
copy beside the package's gitignored `node_modules` so the extension's
bare imports resolve natively; Node keeps the resolve-hook path. Header
docs updated.
- `plugins/provider-pi/src/bridge/bridge.bun-runtime.test.ts` (new) —
regression test running the fake pi under Bun and driving a dynamic tool
round trip end to end. Known limitation, called out: it skips when Bun
is absent from `PATH` (`it.skipIf(bunBinary() === null)`), and no CI leg
installs Bun today, so this guard does not run in CI yet (follow-up
issue; without it, CI cannot detect a revert of the Bun branch).
- An adversarial review pass shaped the change: staging lives in
gitignored `node_modules` (not the source tree), the Bun branch is gated
on the runtime rather than on `registerHooks` availability, and the
single-message fd-4 coverage plus a bridge→extension liveness handshake
are tracked as follow-ups rather than shipped here.
- Verified against Bun 1.4.0 in the test and against Bun 1.3.14 (pi's
embedded runtime) via a direct fd probe.

## How you verified

- `plugins/provider-pi/src/bridge/bridge.bun-runtime.test.ts`: red on
`main` at `timed out waiting for the tool result to reach pi under Bun`
(the exact production symptom), green with this change.
- `pnpm exec vitest run --config vitest.config.ts
src/bridge/bridge.bun-runtime.test.ts src/bridge/bridge.round2.test.ts
src/bridge/bridge.framing.test.ts` — 13/13 pass (Bun on PATH).
- `pnpm exec turbo run typecheck --filter=bb-plugin-provider-pi` —
clean.
- Live-incident capture (`BB_PROVIDER_BRIDGE_RECORD_DIR`): recorded
lanes show the full request→response chain through the bridge's fd-4
write with no error anywhere, matching the failure mode; pi's own
session file shows it only ever received the abort-path tool result.

> AGENT GENERATED

---

<sub><em>Generated with "Omen Alpha" (Opencode Go) via BB /
Pi</em></sub>
## Human comments

## What was wrong

Reasoning choices used full-width menu rows, making the model picker
unnecessarily tall when a provider offered several effort levels.

## What changed

Display reasoning choices as clickable pills arranged horizontally with
wrapping. Highlight the selected level and expose its state with
`aria-pressed`, retain disabled and selection behavior, and use larger
touch targets in compact layouts.

## How you verified

- `pnpm exec turbo run test --filter=@bb/app --
ModelReasoningPicker.test.tsx`: all 29 tests pass on Node 22.23.2.
- Browser checks in the interactive component preview confirmed
selection updates, selected styling, and wrapping at a 320px viewport.
- Started the source dev app for user testing; app and server/daemon
health checks pass.
- `git diff --check` passes.
- Secret-model scan: zero hits in the working tree, tracked HEAD, added
lines, and commit messages for `origin/main..HEAD`.

> AGENT GENERATED
…actions (#3228)

## Human comments

yet another PR for making the pi provider more polished here. I’ve been
running this and my other PRs on my local install of bb and been working
fine

<img width="1170" height="2205" alt="image"
src="https://github.com/user-attachments/assets/d300ebf0-23ef-4cb0-9e0d-83832f61537a"
/>


## What was wrong

Pi extensions prompt the user through `ctx.ui.select`, `ctx.ui.confirm`,
`ctx.ui.input`, and `ctx.ui.editor`. In RPC mode these emit an
`extension_ui_request` line on stdout and block until an
`extension_ui_response` arrives on stdin (see `docs/rpc.md`, "Extension
UI Protocol"). bb's provider-pi bridge answered every such request with
`{cancelled: true}`, so any extension that raises a permission prompt
had it silently dismissed —
[pi-landstrip](https://github.com/landstrip/landstrip) sandbox-approval
prompts were unusable over bb. Investigation and reproduction trail:
#1668.

## What changed

- `plugins/provider-pi/src/bridge/rpc-child.ts` forwards
`extension_ui_request` dialogs to an optional handler; with no handler
the old auto-cancel remains, so behavior is unchanged unless the bridge
opts in.
- `plugins/provider-pi/src/extension-ui-contract.ts` validates dialog
requests at the boundary: method enum, bounded
title/options/message/placeholder/prefill, `select` requires a non-empty
options list.
- `plugins/provider-pi/src/bridge/extension-ui.ts` forwards each dialog
as a `provider-pi/extension-ui` pending interaction and maps resolutions
back (`value` for select/input/editor, `confirmed` for confirm,
`cancelled` on error, unknown value, or session close). Request ids use
a `pi-ui-N` string space so they cannot collide with numeric
item/tool/call ids. Pending dialogs are cancelled on thread stop,
session replace, and discard. Fire-and-forget methods (`notify`,
`setStatus`, `setWidget`, `setTitle`, `set_editor_text`) are dropped.
- Helper sessions such as fork helpers retain automatic dialog
cancellation when no UI handler is installed. Select responses are
checked against the originally offered options before reaching Pi.
- `plugins/provider-pi/app.tsx` renders the four dialog shapes in the
plugin pending-interaction slot. The host passes plugin components the
interaction's data object rather than the stored `{kind, title, data}`
wrapper, so the renderer parses the unwrapped payload directly.

No protocol or wire changes: no `HOST_DAEMON_PROTOCOL_VERSION` bump, no
new grammar, no new plugin API surface — this reuses the existing
`interaction/request` plugin-extension payload kind and
pending-interaction slot.

This is deliberately the minimal bridge-only subset of #2446/#2447 (both
still open): thread-scoped requests outside an active turn, withdrawal
notifications, and queued-before-announcement dialogs are left as
follow-ups — and the behavior is strictly better than the current
always-cancelled path.

## How you verified

- `pnpm exec turbo run test --filter=bb-plugin-provider-pi` (26 files,
143 tests) and `turbo run typecheck --filter=bb-plugin-provider-pi` —
clean.
- Both review regressions were reproduced before the fixes: helper
startup timed out waiting for a dialog response, and an unoffered select
string reached Pi. Both now pass; `pnpm exec turbo run test typecheck
--filter=bb-plugin-provider-pi` passed with 26 test files and 143 tests.
The working-tree and commit-range secret scan was clean.
- New bridge round-trip tests over the fake Pi harness (`/ui` directive
in `fake-pi-rpc.mjs`), rpc-child handler unit tests, and jsdom renderer
tests covering select/confirm/input/editor and the host-unwrapped
payload shape.
- Live: pi-landstrip read/write permission prompts render the full
option list over bb, and user answers propagate back and drive the
sandbox decision (e.g. "Keep blocked" denies the underlying write).

Fixes: partial groundwork for #1668


---

<sub><em>Generated with "Omen Alpha" (Opencode Go) via BB /
Pi</em></sub>

> AGENT GENERATED

---------

Co-authored-by: Sawyer Hood <sawyerjhood@gmail.com>
## Human comments

## What was wrong

Archiving a thread and its children collected only immediate children
and hidden source forks. Archiving each child then released its
unarchived children, leaving grandchildren active and detached from
their parents.

## What changed

Collect the descendant tree before archiving, then archive children
before their parents. Traverse archived hierarchy intermediaries,
include hidden source forks at each level, and deduplicate threads
reached through both relationships. Preserve parent relationships and
leave unrelated threads and visible source forks untouched.

## How you verified

Added public API regression coverage for four hierarchy levels, hidden
forks with children, duplicate relationships, and an already-archived
intermediary. The nested regression failed before the fix. All 17 tests
in the selected suites pass afterward.

- `pnpm exec turbo run test --filter=@bb/server --
test/public/public-thread-parenting.test.ts
test/threads/archive-pruned-environment.test.ts`
- `pnpm exec turbo run typecheck --filter=@bb/server`
- `git diff --check`

> AGENT GENERATED
## Human comments

## What was wrong

The Plugin marketplaces settings section used the default bordered
`SettingsSection` body while each marketplace already rendered as its
own bordered row. That produced a visually redundant card-inside-a-card
layout and unnecessarily indented the add form.

## What changed

Remove the outer body border, background, and padding for this section
while retaining every individual marketplace row card. Increase the gap
above the marketplace list so the unwrapped form and rows keep clear
visual separation. Adding, refreshing, and removing marketplaces are
unchanged.

## How you verified

- `pnpm exec turbo run test --filter=@bb/app --force --
MarketplacesSettingsSection` — 4/4 passed.
- `pnpm exec turbo run typecheck build lint --filter=@bb/app --force` —
passed; lint reported no errors.
- `pnpm exec oxfmt --check
apps/app/src/components/settings/MarketplacesSettingsSection.tsx`
- `git diff --check origin/main...HEAD`
- Reproduced the nested-card layout before the change and verified the
updated layout and existing Add/Refresh interactions in the worktree
app.

> AGENT GENERATED

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
## Human comments

## What was wrong

On a fresh iOS simulator, Maestro reports `openLink: "bb://threads"`
complete before the operating system's first custom-scheme confirmation
has been accepted. The app remains behind the native `Open in “bb”?`
dialog while `shell-deep-link` waits for `shell-webview`; that
simulator-owned dialog survives the next flow's app stop and launch, so
later flows cannot see their native screens. The original and prior
scheduled evidence show that exact sequence: [run
33637452626](https://github.com/get-bb/bb/actions/runs/33637452626) and
[run
33517924818](https://github.com/get-bb/bb/actions/runs/33517924818).

After this PR was labeled, [run
34273156200](https://github.com/get-bb/bb/actions/runs/34273156200)
checked out the PR merged with newer main and exposed a second
deterministic blocker introduced by #2881: the first-run `Get notified
when a thread needs you?` action sheet covered an otherwise rendered
shell after every direct-server pairing. That explains why
`shell-launch` now failed before any deep link. Its artifact shows the
sheet in all three failed-flow screenshots; `clear-open-confirmation`
passed after each failure and `shell-unreachable-server` passed, proving
the PR's fail-closed cleanup prevented native state from leaking. Every
scheduled main run since #2881 merged shows the same new `shell-launch`
failure, so this recurrence is current-main flow orchestration, not
infrastructure.

## What changed

- Route every Mobile E2E `bb://` action through one parameterized
Maestro subflow. It preserves the real custom-scheme invocation,
conditionally accepts the native confirmation only when visible on iOS,
and proves the dialog is absent before route assertions continue.
- Make the existing direct-server pairing subflow own the post-connect
UI boundary. It uses the unchanged 30-second readiness budget to wait
for either the notification opt-out or rendered page content,
conditionally chooses `Not now`, proves the sheet is absent, and keeps
the `shell-webview` assertion.
- Have `shell-launch`, `shell-deep-link`, and `shell-send` use that one
pairing boundary instead of duplicating pairing. The notifications
deep-link added by #2881 also uses the custom-scheme helper.
- After a failed CI flow, preserve the original final screenshot, cancel
and verify removal of the native confirmation, and stop the app before
continuing. If cleanup cannot establish isolation, the runner stops
before the next flow.
- Add regression coverage for cleanup-before-continuation, fail-closed
cleanup failure, shared pairing, and the absence of direct `bb://`
Maestro commands.
- No assertion, timeout, polling budget, or retry budget increased. No
server/host-daemon wire, CLI, SDK, or user-facing configuration changed,
so `HOST_DAEMON_PROTOCOL_VERSION` is unchanged.

## How you verified

- Mandatory initial fresh-main gate: clean worktree; `HEAD`,
`origin/main`, and merge-base all independently verified as
`ab8e0a202c70525c3c935733300198dac617e3de` before task inspection.
- Before the follow-up edit, fetched and recorded live `origin/main` as
`06047280e04e6ce57554052ec788a1c5defe7d1e`, with PR head
`5f2f960c2c289518b8493dece775f4f22103f722` and retained merge-base
`ab8e0a202c70525c3c935733300198dac617e3de`; then rebased cleanly.
Current PR merge-base is `06047280e04e6ce57554052ec788a1c5defe7d1e`.
- Native red: run 34273156200 failed `shell-launch`, `shell-deep-link`,
and `shell-send` at `shell-webview`; its screenshots show the
notification action sheet over the rendered shell. The new cleanup
passed after every failure, and `shell-unreachable-server` passed.
- Original focused runner red on pre-fix main: 2/2 tests failed because
cleanup was never invoked and the next flow always ran.
- Focused green: `pnpm exec turbo run test --filter=@bb/mobile --force
--concurrency=2 -- --run src/lib/e2e/mobile-e2e-flows.test.ts
src/lib/e2e/ci-run-flows.test.ts` — 2 files, 4 tests passed.
- `pnpm exec turbo run test --filter=@bb/mobile --force --concurrency=2`
— 46 files, 331 tests passed.
- `pnpm exec turbo run typecheck --filter=@bb/mobile --force
--concurrency=2` — passed.
- `pnpm exec turbo run lint --filter=@bb/mobile --force --concurrency=2`
— passed with two pre-existing React warnings and zero errors.
- `pnpm exec turbo run build --filter=@bb/app --force --concurrency=2` —
passed after a frozen-lockfile install refreshed dependencies for
current main; 439 files precompressed.
- `bash -n`, Ruby YAML parsing for every changed Maestro file, targeted
`oxfmt --check`, `git diff --check`, and the no-direct-`bb://` scan
passed. ShellCheck is unavailable on this host.
- [Ordinary CI run
34277221769](https://github.com/get-bb/bb/actions/runs/34277221769)
passed on commit `1594a59a2b2ccf1c17645200f2fb0db522e8861c`, including
build/typecheck/lint, package tests, all app test shards, server,
integration, and Linux/macOS package smoke.
- All local build, test, typecheck, lint, and install commands ran with
explicit process-group deadlines on Intel host `host_nwqfteeqz4`
(`x86_64`, Intel Core i5-1038NG7). A final cwd audit found one stale
earlier Xcode-discovery process group, terminated it, and then proved no
task build, test, backend, fixture, Maestro, or simulator process
remained. Temporary artifacts and logs were moved recoverably to Trash.
- This host has Command Line Tools only, with no Xcode app, iOS runtime,
or Maestro, so no after-change simulator result is claimed. The
`mobile-e2e` label was removed before the updated push; [run
34277221938](https://github.com/get-bb/bb/actions/runs/34277221938)
therefore skipped without starting a simulator.

> AGENT GENERATED: by GPT-5.6-Sol.
…cy rows (#2920)

## Human comments

## What was wrong

Large completed outputs were stored directly in `events.data`. Ordinary
timeline reads therefore loaded and parsed megabytes that the UI did not
need. Image-generation completions were also left in large provider
envelopes, so an otherwise useful generated-image event could exceed the
timeline response limit and appear as an unexplained placeholder.

Existing databases had a second problem: old inline outputs were
truncated in a bulk synchronous sweep. Large backlogs could block the
Node event loop long enough to make the server visibly unresponsive.

## What changed

### In simple terms

- New large command, tool, fetch, search, and image-generation outputs
keep a small payload preview in the event row and the exact full value
in a retained-output sidecar.
- Normal timelines read only the compact event. Explicit raw/detail
reads restore the exact full value while it is retained.
- Existing inline rows migrate incrementally. Each synchronous advance
has a deliberate bound, persists its cursor, and yields before more
work.
- Generated images render as **Generating image** / **Generated image**
rows from prompt/path metadata. The visible image preview still loads
from the saved image path; the large provider `result` payload is not
needed to render it.
- Migration and expiry invalidate affected timeline/projection caches
even though event sequence numbers do not change.
- All migration work stays on the Node main event loop. This PR adds no
worker.

### Output lifecycle and contract

The authoritative retained-output pairs are:

- `commandExecution.aggregatedOutput`
- `imageGeneration.result`
- `toolCall.result`
- `webFetch.resultText`
- `webSearch.resultText`

For a supported completed output above 32,768 JavaScript characters:

1. `events.data` receives a Unicode-safe 2 KiB head + marker + 2 KiB
tail payload preview.
2. `retained_event_outputs` stores the exact JSON-encoded string with
`event_id`, `output_path`, and `expires_at`.
3. Bounded timeline/event reads use the compact event. Raw/detail reads
hydrate from the sidecar only when the response budget allows it.
4. The retained value expires at `event.createdAt + 7 days`; the payload
preview then becomes authoritative.
5. Fork/copy preserves an unexpired sidecar value and its original
expiry without materializing the value in Node memory.

The actual generated-image preview is not truncated by this policy. The
UI loads the saved image file from `savedPath`; `imageGeneration.result`
is the large encoded provider payload. Existing
command/tool/fetch/search retention behavior is unchanged apart from
moving the exact value into the sidecar during the retention window.
Image generation is the new fifth target.

Public event pages accept at most 100 rows. A hydrated raw response
above 8 MiB returns HTTP 413 so callers can reduce their page size. The
CLI does this automatically and grows its page size again after a dense
output region.

### Existing databases

- Standard legacy completed rows use the existing
`events_completed_item_truncation_idx` and scan 25 indexed rows per
window.
- Legacy provider-envelope image events walk the existing `events.id`
primary-key index in 250-row windows. This bounds total rows examined,
not merely matching rows, and needs no new index over the existing
events table.
- At most one eligible output is rewritten per advance. Cursor/window
state and the rewrite happen transactionally, so restart resumes safely.
- Malformed, mismatched, already-previewed, expired, or over-8-MiB
events advance the cursor without blocking later rows. Expired values
become previews without creating already-expired sidecars.
- Cursor completion is persisted, so the historical database is not
rescanned on every sweep.

Drizzle migration `0113_solid_night_thrasher.sql` creates only the
sidecar table and its empty expiry index. It does not scan or rewrite
`events` during startup.

### Generated-image boundary

The Codex provider plugin translates new image-generation items into the
provider-neutral bridge delta union. The assembler persists canonical
`imageGeneration` items. Old `provider/unhandled` image completions are
recognized through one shared strict parser; migration preserves their
original envelope and sidecarizes only the nested `result`.

The provider-bridge grammar remains v2 because image generation is
additive vocabulary. The host-daemon protocol is bumped from
current-main 180 to 181 because that new delta shape can cross the
server/daemon wire; this forces already-enrolled daemons to self-update.

## Measurements

### Normal reads and new writes

BEFORE is inline baseline `eeaaa3e8db7b3aeb3c4ab46873816c84cb6ea513`;
AFTER is sidecar runtime `b78bf6038f15c081c8274f3e8141ccc6afa8f5b2`.
Same harness, Apple M4 Max, Node v22.23.1, 12 read warmups + 60
iterations and 8 write warmups + 40 iterations.

| Main-loop operation, p95 | Inline | Sidecar | Result |
| --- | ---: | ---: | ---: |
| Normal bounded event read | 42.382 ms | 0.902 ms | 47× faster |
| Normal bounded timeline read | 42.752 ms | 5.857 ms | 7.3× faster |
| Raw full-output read | 7.127 ms | 26.562 ms | explicit hydration costs
more |
| Four-row write transaction | 10.157 ms | 5.929 ms | 1.7× faster |
| Event data materialized by bounded reads | 2,327,734 B | 331,678 B |
85.8% less |

Foundation artifacts:
[comparison](https://ymichael.getbb.app/api/v1/threads/thr_pkwpa3qag4/thread-storage/files/benchmarks/comparison.md),
[inline
JSON](https://ymichael.getbb.app/api/v1/threads/thr_pkwpa3qag4/thread-storage/files/benchmarks/origin-main.json),
[sidecar
JSON](https://ymichael.getbb.app/api/v1/threads/thr_pkwpa3qag4/thread-storage/files/benchmarks/branch.json),
[harness](https://ymichael.getbb.app/api/v1/threads/thr_pkwpa3qag4/thread-storage/files/benchmarks/retained-event-output-benchmark.mjs),
[checksums](https://ymichael.getbb.app/api/v1/threads/thr_pkwpa3qag4/thread-storage/files/benchmarks/SHA256SUMS).

### Legacy completed-output responsiveness

BEFORE is foundation `259f937f7b327ea73f56b69ace6ed35645c8045e`; AFTER
is bounded migration runtime `f89baf1d0384d3f59cba5e655a1a65d083160a51`.
The later fixes do not change these four standard target advances. Same
harness, 2 warmups + 10 iterations, with one busy child in CPU runs.

| Fixture/load | Bulk synchronous sweep p95 | Bounded migration p95 |
| --- | ---: | ---: |
| 36 KiB rows, idle | 110.226 ms | 1.587 ms |
| 36 KiB rows, CPU load | 247.957 ms | 2.468 ms |
| 4 MiB rows, idle | 94.668 ms | 14.589 ms |
| 4 MiB rows, CPU load | 165.865 ms | 13.308 ms |

Every 4,000-row drain migrated 147,456,000 bytes. Persisted windows
reduced fetched scan rows from 875,500 to 4,000. Expected, hydrated, and
raw-event hashes match. A 4,000-row backlog projects to about 10.5
minutes at the production cadence, trading total drain time for
responsiveness.

Migration artifacts:
[comparison](https://ymichael.getbb.app/api/v1/threads/thr_9mfhzth64b/thread-storage/files/benchmarks/review-fixes-comparison.md),
[BEFORE
idle](https://ymichael.getbb.app/api/v1/threads/thr_9mfhzth64b/thread-storage/files/benchmarks/review-fixes-before-idle.json),
[AFTER
idle](https://ymichael.getbb.app/api/v1/threads/thr_9mfhzth64b/thread-storage/files/benchmarks/review-fixes-after-idle.json),
[BEFORE
CPU](https://ymichael.getbb.app/api/v1/threads/thr_9mfhzth64b/thread-storage/files/benchmarks/review-fixes-before-cpu.json),
[AFTER
CPU](https://ymichael.getbb.app/api/v1/threads/thr_9mfhzth64b/thread-storage/files/benchmarks/review-fixes-after-cpu.json),
[checksums](https://ymichael.getbb.app/api/v1/threads/thr_9mfhzth64b/thread-storage/files/benchmarks/review-fixes-SHA256SUMS).

### Production-shaped existing-database upgrade

Exact HEAD `e1d7c0f054012b8b4126134d58bbadddad4bc91d`, sanitized
database with all 1,490 threads and all 1,659,038 events (3.68 GiB), 2
warmups + 10 measured fresh-clone upgrades per load:

| Load | Pending-upgrade startup p50 / p95 | Already-current startup p50
/ p95 | Paired upgrade overhead p50 / max |
| --- | ---: | ---: | ---: |
| Idle | 15.83 / 29.14 ms | 17.80 / 27.36 ms | 0.53 / 7.18 ms |
| Controlled CPU | 15.98 / 23.42 ms | 13.91 / 20.65 ms | 1.43 / 5.83 ms
|

The removed index-building migration measured 8,164.99 ms p50 and
8,886.13 ms p95/max idle. The new pending-upgrade p50 is about 516×
lower. The new schema adds 12,288 bytes instead of 425,984 bytes.

The exact-HEAD primary-key image scan covered all 1,659,038 events in
6,678 yielded advances:

| Load | Sync p50 / p95 / max | Timer delay p50 / p95 / max | Direct
wall time |
| --- | ---: | ---: | ---: |
| Idle | 0.49 / 1.96 / 78.11 ms | 1.37 / 3.65 / 93.21 ms | 11.62 s |
| Controlled CPU | 0.54 / 2.50 / 225.15 ms | 1.40 / 5.20 / 226.03 ms |
15.53 s |

Both passes migrated all 40 eligible legacy image rows and 99,775,504
output bytes, scanned exactly 1,659,038 rows, and executed about 33,425
measured statements. Every retained raw event hydrated to its
byte-identical pre-migration SHA-256. A legacy-only production-scheduled
pass projects to about 17.4 minutes after other targets are exhausted.

Exact-HEAD follow-up artifacts:
[report](https://ymichael.getbb.app/api/v1/threads/thr_9mfhzth64b/thread-storage/files/stress-tests/no-index-followup-report.md),
[upgrade
JSON](https://ymichael.getbb.app/api/v1/threads/thr_9mfhzth64b/thread-storage/files/stress-tests/0113-no-index-upgrade-production-shaped-final.json),
[scan
JSON](https://ymichael.getbb.app/api/v1/threads/thr_9mfhzth64b/thread-storage/files/stress-tests/legacy-image-primary-key-scan-production-shaped.json),
[upgrade
harness](https://ymichael.getbb.app/api/v1/threads/thr_9mfhzth64b/thread-storage/files/stress-tests/measure-0113-no-index-upgrade.mjs),
[scan
harness](https://ymichael.getbb.app/api/v1/threads/thr_9mfhzth64b/thread-storage/files/stress-tests/benchmark-legacy-image-primary-key-scan.mjs),
[checksums](https://ymichael.getbb.app/api/v1/threads/thr_9mfhzth64b/thread-storage/files/stress-tests/no-index-followup-SHA256SUMS).

<details>
<summary><strong>Earlier full production-copy stress
run</strong></summary>

The earlier all-target stress pass used the same complete sanitized
1.66-million-event population. It migrated 2,989 rows and 457,525,580
source bytes with four forced reopens and identical idle/CPU totals and
hashes. The standard target behavior remains unchanged; the exact-HEAD
follow-up above replaces its legacy-image index scan and separately
measures that new pass.

| Load | Sync p50 / p95 / max | Timer delay p50 / p95 / max | Direct
drain time |
| --- | ---: | ---: | ---: |
| Idle | 4.970 / 12.681 / 388.411 ms | 4.976 / 12.691 / 388.635 ms |
82.686 s |
| Controlled CPU | 4.983 / 12.730 / 379.899 ms | 4.992 / 12.744 /
379.996 ms | 82.270 s |

Artifacts: [QA
report](https://ymichael.getbb.app/api/v1/threads/thr_9mfhzth64b/thread-storage/files/stress-tests/qa-report.md),
[idle
JSON](https://ymichael.getbb.app/api/v1/threads/thr_9mfhzth64b/thread-storage/files/stress-tests/prod-copy-migration-idle-fixed.json),
[CPU
JSON](https://ymichael.getbb.app/api/v1/threads/thr_9mfhzth64b/thread-storage/files/stress-tests/prod-copy-migration-cpu-fixed.json),
[snapshot
validation](https://ymichael.getbb.app/api/v1/threads/thr_9mfhzth64b/thread-storage/files/stress-tests/prod-thread-events-validation.json).

</details>

<details>
<summary><strong>Exact combined file list (73 files)</strong></summary>

```text
apps/app/src/components/thread/timeline/TimelineRowDetails.output-preview.test.tsx
apps/app/src/components/thread/timeline/TimelineRowDetails.tsx
apps/app/src/components/thread/timeline/useTimelineWorkRowFullOutput.ts
apps/cli/src/__tests__/command-output/thread-log.test.ts
apps/cli/src/commands/thread/show.ts
apps/server/scripts/benchmark-completed-event-output-migration.mjs
apps/server/src/routes/threads/data.ts
apps/server/src/services/system/event-loop-work.ts
apps/server/src/services/system/periodic-sweeps.ts
apps/server/src/services/threads/thread-data.ts
apps/server/src/services/threads/timeline-cache.ts
apps/server/src/services/threads/timeline-latest-rows-cache.ts
apps/server/src/services/threads/timeline-output-preview.ts
apps/server/src/services/threads/timeline.ts
apps/server/test/public/public-thread-data.test.ts
apps/server/test/public/public-thread-timeline-output-preview.test.ts
apps/server/test/services/periodic-sweeps.test.ts
apps/server/test/services/threads/timeline-cache.test.ts
apps/server/test/services/threads/timeline-in-turn-window.test.ts
apps/server/test/services/threads/timeline-latest-rows-cache.test.ts
docs/api_to_audit.md
docs/provider-bridge-protocol.md
packages/client-core/src/timeline/timeline-auto-expand.ts
packages/client-core/src/timeline/timelineRowSignatures.ts
packages/db/drizzle/0113_solid_night_thrasher.sql
packages/db/drizzle/meta/0113_snapshot.json
packages/db/drizzle/meta/_journal.json
packages/db/src/data/event-output-truncation.ts
packages/db/src/data/events.ts
packages/db/src/data/index.ts
packages/db/src/data/retained-event-outputs.ts
packages/db/src/data/sweeps.ts
packages/db/src/retained-event-output.ts
packages/db/src/schema.ts
packages/db/test/data/completed-event-output-migration.test.ts
packages/db/test/data/retained-event-outputs.test.ts
packages/db/test/data/sweeps.test.ts
packages/db/test/helpers/migrated-connection.ts
packages/db/test/migrate.test.ts
packages/db/test/query-plans.test.ts
packages/domain/package.json
packages/domain/src/index.ts
packages/domain/src/legacy-image-generation.ts
packages/domain/src/provider-event.ts
packages/domain/src/utf16.ts
packages/domain/test/legacy-image-generation.test.ts
packages/host-daemon-contract/src/protocol.ts
packages/host-daemon-contract/test/contract.test.ts
packages/provider-bridge-protocol/src/assembler/delta-assembler.ts
packages/provider-bridge-protocol/src/contract-tests/provider-bridge-grammar.v2.snapshot.json
packages/provider-bridge-protocol/src/thread-delta.ts
packages/server-contract/src/api/threads.ts
packages/server-contract/src/common.ts
packages/server-contract/src/thread-timeline.ts
packages/thread-view/src/build-event-projection.ts
packages/thread-view/src/build-thread-timeline.ts
packages/thread-view/src/event-projection-message.ts
packages/thread-view/src/event-projection-types.ts
packages/thread-view/src/format-timeline-text.ts
packages/thread-view/src/timeline-row-title.ts
packages/thread-view/src/timeline-view.ts
packages/thread-view/src/timeline-work-row-glyph.ts
packages/thread-view/src/tool-activity-cells.ts
packages/thread-view/src/tool-activity-projection.ts
packages/thread-view/src/tool-activity-web-projection.ts
packages/thread-view/src/web-activity-lifecycle.ts
packages/thread-view/test/build-thread-timeline.test.ts
plugins/provider-codex/src/delta-translation.test.ts
plugins/provider-codex/src/delta-translation.ts
plugins/provider-codex/src/presentation.ts
plugins/provider-codex/src/schemas.ts
tests/integration/helpers/api.ts
tests/integration/helpers/assertions.ts
```

</details>

### Residual tradeoffs

- Explicit raw/detail reads are slower because they load full sidecar
values; normal timeline reads avoid that work.
- Full values intentionally disappear after seven days. The compact
payload preview and generated image file/path remain.
- A raw event response above 8 MiB returns 413. An individual legacy
event above 8 MiB is preserved inline rather than migrated and can
remain too large for that endpoint. The complete sanitized production
snapshot contains no such event.
- SQLite may retain freed pages for reuse instead of immediately
shrinking the physical file. This is reusable database space, not live
duplicate data.
- Historical migration takes longer overall and uses more small
statements in exchange for bounded main-loop occupancy and a yield after
every advance.
- Rare cold/large-row advances remain visible: the exact-HEAD
legacy-image maximum was 78 ms idle and 225 ms under controlled CPU
load, while steady-state p95 stayed at or below 2.50 ms.
- An out-of-band historical row inserted behind a permanently completed
cursor would require a future cursor-version bump or explicit
maintenance action.

## How you verified

All database regressions use real in-memory SQLite without mocks.
Coverage includes byte-identical raw/detail hydration, copy and expiry,
malformed and extension events, UTF-16 boundaries, 8 MiB guards, query
plans, cursor windows, restart/completion, cache invalidation, CLI 413
pagination recovery, canonical and legacy image envelopes, and yielded
scheduler behavior.

Latest focused validation:

- `@bb/db` Turbo typecheck/build/test: 474/474 tests passed.
- `@bb/server` Turbo typecheck/build/test: 2,239/2,239 tests passed
across 230 passing files; 1 file skipped.
- `@bb/host-daemon-contract` and `@bb/host-daemon` Turbo
typecheck/build/test: 51/51 contract tests and 572/572 daemon tests
passed.
- The earlier complete combined branch validation remains green:
provider bridge 238/238, Codex plugin 260/260, thread view 370/370,
client core 253/253, domain 195/195, server contract 64/64, full app
3,867 passed with 3 skipped, and integration 77/77.
- Drizzle migration and snapshot were regenerated mechanically from the
schema; no snapshot was hand-edited.
- `origin/main...HEAD` contains the complete sidecar, image rendering,
incremental migration, and protocol-version changes with no worker or
unrelated feature work.

Fixes: no linked issue.

> AGENT GENERATED
## Human comments

## What was wrong

The timeline discarded parent links while projecting compaction
lifecycle events, so subagent compactions appeared in the main feed. The
Codex adapter also rejected `subAgentActivity` items with `kind:
"completed"`, producing duplicate “Unhandled Codex event” diagnostics
alongside the existing agent completion summary.

## What changed

Preserve explicit and inherited parent links throughout compaction
projection, including completion-only events. Recognize Codex's
completion activity notifications and consume them without duplicating
the delegation lifecycle already settled by the child turn. Root-agent
compactions remain visible in the main feed. Previously persisted
diagnostic rows are unchanged.

## How you verified

- `pnpm exec turbo run test typecheck --filter=@bb/thread-view
--filter=bb-plugin-provider-codex`: 651 tests and both typechecks
passed.
- Added regression coverage for explicit/inherited nesting,
completion-only compactions, concurrent root compaction, and duplicate
Codex completion notifications.
- Replayed the reported thread's saved events locally: all three
compactions remain nested, zero compactions appear at the root, and all
18 completion activity notifications produce zero unhandled deltas. Raw
thread data was not committed.
- Formatting and `git diff --check` passed.
- Secret-model scan: clean working tree, HEAD, added lines, and commit
messages for `origin/main..HEAD`.

> AGENT GENERATED
## Human comments

## What was wrong

The `needs-configuration` plugin banner knew whether a plugin exposed
in-app settings, but its actions did not provide a route to those
settings. Plugins configured through a frontend-contributed settings
section also still need an explicit reload after configuration, and the
banner's single-row layout left too little width for its recovery copy
once both actions were present on compact screens.

## What changed

- Adds an **Open settings** action for `needs-configuration` plugins
with either declarative settings or a frontend-contributed settings
section.
- Keeps **Reload** for plugins whose configuration is not automatically
applied, including plugins with frontend-contributed settings.
- Lets banner content wrap so the actions move below the message when
horizontal space is limited.
- Adds focused coverage for declarative settings, frontend-contributed
settings, config-less plugins, and unrelated runtime failures.
- No wire, CLI, guide, or public plugin API changes.

## How you verified

- `pnpm exec turbo run test --filter=@bb/app --force --
ToolsView.plugin-detail` (36 tests passed)
- `pnpm exec turbo run typecheck lint --filter=@bb/app --force` (passed;
lint reported 0 errors)
- `pnpm exec oxfmt --check
apps/app/src/components/tools/PluginCapabilities.tsx
apps/app/src/components/tools/plugin-detail-banner.tsx
apps/app/src/views/ToolsView.plugin-detail.test.tsx`
- `git diff --check origin/main...HEAD`
- Manually verified the link destination, retained reload action, and
compact 390px wrapping in the isolated review app.

> AGENT GENERATED

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
bb-slop-cop Bot and others added 28 commits September 10, 2026 08:25
🚨 **SLOP COP** 🚨 · `new-issue-autopilot`

## Human comments

## What was wrong

Project header controls inherited hover-only visibility without the
marker that exposes actions on compact touch screens. This made their
wrapper transparent and excluded it from pointer input. [Reproduction
and root-cause
report](https://get-bb.github.io/reports/issues/3330.html).

## What changed

Apply the existing mobile visibility marker to the header-actions
wrapper and cover closed and open states with a regression test. The
change is 34 added text lines across two sidebar files, with no
dependencies or public contract changes.

## How you verified

The new assertions failed on unchanged production code with `expected
null to be 'always'`. A browser fixture extracted from the production
JSX and CSS reproduced invisible, untappable controls in two clean
checkouts. After the fix, the same fixture is visible and receives a
real touch tap.

`pnpm exec turbo run test --filter=@bb/app -- ProjectRow
sidebarRowClasses theme.test SidebarSectionRow ProjectList.modes
--maxWorkers=1 --testTimeout=60000`: 62 tests passed across 8 files. The
initial 15-second run had three existing timeouts; all passed on the
final run. App lint passed with 0 errors (187 existing warnings). `git
diff --check` and the publication-name scan passed. Broad build and
full-app startup were stopped under host load; full-app and Safari
verification are not claimed.

Fixes #3330

> AGENT GENERATED

<!-- slopcop:rule=new-issue-autopilot run=run_69t7lbzqmtuda5d9
sha=issue-3330 kind=summary -->

Co-authored-by: Sawyer Hood <sawyerjhood@gmail.com>
## Human comments

## What was wrong

Splitting Plugins and Skills into separate workspaces removed
installed-plugin management from Settings and redirected its existing
routes into Plugins.

## What changed

Restore Settings → Installed plugins while retaining Plugins → Installed
plugins. Both locations manage the same installed plugins, and opening a
list row keeps its detail page in the originating section. Restore
Settings navigation memory and route the command palette and general
installed-plugin links to Settings. Keep individual plugin configuration
pages and legacy workspace routes working. Update the agent guide and
navigation documentation.

## How you verified

- Turbo app typecheck and lint passed (lint has existing warnings, no
errors).
- Focused Turbo app tests passed: 86 tests covering both
installed-plugin collections/details, Settings and Plugins sidebars,
canonical routes, and compatibility navigation. Earlier focused checks
also covered command palette, Settings route memory, notification links,
and update/configuration links.
- Browser verification in an isolated source dev app: clicked Installed
plugins and BB guide from both Settings and Plugins, checked destination
URLs and a single detail heading, and confirmed Settings stays selected
for its detail page.
- EAP codename scanner passed for the working tree, tracked HEAD, added
lines, and commit messages in the push range.
- SlopCop skipped at the user's request.

> AGENT GENERATED
## Human comments

## What was wrong

The compact model picker let its model list grow without a height
allocation while the entire drawer body handled scrolling. With long
catalogs such as Pi’s 437 models, the list became roughly 15,800px tall
and pushed Reasoning, fast-mode, and footer controls far below the
visible drawer. The persistent drawer also did not account for
software-keyboard overlap.

## What changed

Make the compact picker a shrinkable flex column with the model list as
its sole independently scrollable region. Provider controls and the
reasoning/footer controls remain outside that scroller, while desktop
retains its existing 256px list cap.

Teach the shared persistent drawer to measure Visual Viewport keyboard
overlap, lift above it, and reduce its maximum height by the same inset.
The existing close/reset path clears the inline keyboard styles, and the
app-root/deferred-realization behavior is unchanged.

## How you verified

- Reproduced with the real Pi catalog at 390×844: 437 models produced a
15,806px list and placed Reasoning around y=16,036
- After the fix, the list is capped at 567px on 390×844 and 314px on
320×568, with reasoning controls visible
- Verified mobile touch scrolling, reasoning selection, search, keyboard
selection, selected-item visibility, and drawer drag-to-close
- Verified desktop long/short catalogs retain the existing 397px popover
and 256px model-list cap
- Verified the layout in WebKit with iPhone 15 emulation; iOS Simulator
rendering was checked, but interactive taps were unavailable due local
accessibility permissions
- Full `@bb/app` suite passed: 502 files and 4,204 tests
- Focused picker and responsive-overlay suites passed after formatting:
61/61
- Turbo typecheck and lint passed for `@bb/app` and `@bb/shared-ui`
- Targeted formatting and `git diff --check` passed

No linked issue.

> AGENT GENERATED

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
## Human comments

## What was wrong

Radix opens a tooltip when its trigger receives focus after its
pointer-down guard has cleared. On touch, focus restoration can arrive
after pointer-up; the footer-specific suppression was then cleared when
the More drawer moved focus, so dismissing that drawer restored focus to
the footer action and left its tooltip open without hover.

## What changed

The shared tooltip trigger now composes its focus handler and allows
focus-opened tooltips only when the existing input-modality tracker
reports keyboard input. Mouse hover and keyboard focus still open
tooltips, while touch/pointer focus restoration does not. The
now-redundant footer suppression state was removed while preserving
focus restoration, and the plugin registry artifact was regenerated for
the shared dependency. Existing tooltip tests now use an explicit
keyboard-focus helper where that is the intended interaction.

## How you verified

- Focused Turbo app tests: 12/12 passed across the tooltip modality and
plugin footer suites.
- Turbo plugin-registry test and typecheck passed; the registry is fresh
and its vendor test passes.
- Turbo app and shared-UI typechecks passed.
- Formatting checks for every changed TypeScript file and git diff
checks passed.
- Browser QA reproduced the exact touch sequence before the fix and
repeated it after the fix, alongside mouse-hover and keyboard-focus
checks.

> AGENT GENERATED

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
## Human comments

## What was wrong

On coarse-pointer devices, follow-up submission happens on pointer-up.
Sending clears the composer immediately, replacing the Submit button
with the microphone button before the browser dispatches the tap
compatibility click. That trailing click is hit-tested against the
replacement microphone control and starts recording even though the
gesture began on Submit.

## What changed

Voice activation now records the exact microphone button that received
pointer-down and ignores pointer-driven clicks retargeted to a different
replacement button. Deliberate microphone taps, mouse clicks, and
keyboard or assistive activation remain supported, and touch submission
still keeps its pointer-up latency improvement.

## How you verified

- Focused Turbo PromptBoxInternal suite: 121/121 tests passed.
- Added regressions for a retargeted touch-submit click, a deliberate
microphone tap, and keyboard activation.
- Turbo app typecheck passed.
- Formatting and git diff checks passed.
- Browser QA reproduced the original compatibility-click target and
confirmed one-tap submit sends without requesting the microphone, while
a deliberate microphone tap still starts recording.

> AGENT GENERATED

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
## Human comments

## What was wrong

The new-thread composer rendered different environment-slot component
types for project and projectless scope, and the dropdown owned its open
state internally. Transient scope replay, provider loading, or
provider-count changes could therefore unmount the open picker and
recreate it closed. Loading reusable environments also temporarily
collapsed a specific reuse selection to the generic reuse value.

## What changed

Project and projectless composers now share one stable EnvironmentSlot.
It controls the dropdown open state and keeps the picker mounted while
open even if projectless options settle to the machine-only layout; the
compact machine control returns after normal dismissal. Specific reuse
selections are retained while their options load.

## How you verified

- Focused Turbo app tests: 95/95 passed across NewThreadPromptBox,
RootComposeView, and EnvironmentPicker.
- Regression tests assert the same trigger DOM node and open menu
survive scope replay and projectless option settling.
- Turbo app typecheck passed.
- Formatting and git diff checks passed.

> AGENT GENERATED
## Human comments

## What was wrong

The launcher forwarded service stdout from a data callback into
`process.stdout.write()` and inherited terminal stderr. On macOS, a
stalled terminal can block that synchronous write and stop the launcher
from draining child output. A log burst can consequently freeze the
service. Replacing the callback with `pipe()` still blocks in the
terminal write; an isolated pseudo-terminal reproduction confirmed this.

## What changed

Send both service stdout and stderr directly to private append-only
`server-stdio.log` and `host-daemon-stdio.log` files in the selected
logs directory. Apply this to full-stack launches and the standalone
server and host-daemon commands, and remove the launcher output buffer
and forwarding callbacks. Close the parent's log descriptor after
spawning each child.

Update launcher help, the package README, debugging docs, CLI guide, and
skill with the capture locations and `tail -F` usage. Existing
application logs retain their rotation and remain the desktop log
viewer's source. The new capture files append across restarts without
automatic rotation. There are no server/daemon wire changes.

## How you verified

- `pnpm exec turbo run smoke:tarball --filter=bb-app` — passed locally,
including full-stack startup and separate daemon enrollment. The smoke
harness now checks each daemon capture file for its plugin-ready message
and includes that output on failure.

- `pnpm exec turbo run test typecheck --filter=bb-app` — passed; 79
tests across four files.
- New real-child tests cover multi-megabyte stdout/stderr bursts without
parent stream readers, file permissions, retained output across restarts
and early failures, inaccessible log paths, and executable spawn
failures.
- Isolated macOS pseudo-terminal check: leave terminal output undrained,
write 8 MiB across child stdout/stderr, and verify the child health
endpoint responds and the launcher heartbeat advances. All 8,388,610
output bytes were captured.
- `git diff --check` — passed. No production processes were restarted;
desktop UI was not exercised.

> AGENT GENERATED
## Human comments

## What was wrong

After selecting High reasoning and sending a follow-up in a thread with
a saved Max override, the turn used High but reload could restore Max.
Explicit sends already repaired stale model overrides; reasoning
overrides were left unchanged. See #3257.

## What changed

Extend the existing model override recovery path to reasoning in direct
sends and queued dispatch. Picker changes remain local until submission;
queue timing and retry behavior are unchanged. This replaces the
previous save-on-selection approach. No client, wire, protocol, schema,
or CLI changes are needed.

The final diff is 6 files, 48 additions and 11 deletions. Tests extend
existing fixtures without renaming files.

## How you verified

- Fail-before regression demonstrated stale Max after explicitly
choosing High; existing regression cases now cover changed and unchanged
models.
- Focused/adjacent server tests: 128 passed across override
recovery/policy, send/queue dispatch, retries, and public thread data.
Existing direct/queued tests assert the recorded model and reasoning.
- Adjacent app tests: 97 passed in the implementation worktree.
- Turbo build/typecheck/lint for `@bb/app` and `@bb/server`, formatting,
and diff checks passed. App lint has existing warnings; server has no
lint task.
- Live isolated browser QA: an unsent selection stayed local; sent High
persisted in server defaults and after reload. The issue's exact
immediate post-send reset was not reproduced in the Codex fixture.

Fixes #3257

> AGENT GENERATED

Co-authored-by: Sawyer Hood <sawyerjhood@gmail.com>
Co-authored-by: Michael Yong <wrong92@gmail.com>
## Human comments

## What was wrong

The Account Pooler sign-in flow rendered the provider sign-in label
twice: once as the accessible dialog title and again as a visual step
heading. The footer Cancel button duplicated the dialog close action,
while the compact drawer did not expose the corresponding header close
control.

## What changed

- Keep one accessible `Sign in to …` title and retain the
provider-specific instruction copy.
- Remove Cancel from the active sign-in footer; Codex now omits the
empty footer while Claude retains Complete and the connected step
retains Add another/Done.
- Put the shared responsive `DialogClose` in the Account Pooler dialog
header so both desktop dialogs and compact drawers retain an explicit
close action.
- Add regressions for the single heading, absent Cancel action, and
pending-session cancellation/poll teardown across wide and compact
layouts.

No host-daemon wire, CLI, guide, or public plugin API contract changes
are involved.

## How you verified

- `pnpm exec turbo run test typecheck --filter=bb-plugin-account-pool
--force` — 269 tests passed; typecheck passed.
- `pnpm exec oxfmt --check plugins/account-pool/app.tsx
plugins/account-pool/app.test.tsx`
- `git diff --check`
- Browser QA at 1280×900 and 390×844 with a local fake device-auth
fixture confirmed the single title, absent Cancel button, responsive
header close, preserved device code/copy/open/countdown behavior, and
preserved Claude/final-step actions.

> AGENT GENERATED

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
## Human comments

## What was wrong

Message editing was already enabled by default, but the app and server
still gated it behind the `editMessages` experiment. Users who
previously disabled the experiment could not edit otherwise eligible
messages.

## What changed

Promote message editing to a standard feature by removing the experiment
key, Settings toggle, and app/server gates. Preserve provider rewind
support and existing thread/message eligibility checks. Update fixtures,
CLI guide templates, settings documentation, and the affected
verification recipes. Existing CLI and SDK editing surfaces now work
without the experiment. Bump the plugin SDK to `0.4.54` because removing
the experiment changes its bundled declarations. No host-daemon wire
changes.

## How you verified

- `node packages/plugin-sdk/scripts/check-npm-version-guard.mjs` —
passed after the SDK version bump.

- `pnpm exec turbo run test --filter=@bb/server --filter=@bb/db
--filter=@bb/app -- experiments thread-edit-message` — 51 tests passed.
Message-edit service tests now seed a legacy disabled experiment value
to verify it no longer blocks editing.
- `pnpm exec turbo run typecheck --filter=@bb/app --filter=@bb/server
--filter=@bb/desktop --filter=@bb/cli` — passed.
- Isolated source app: confirmed Settings → Experiments shows only the
four remaining toggles, and `/api/v1/system/config` excludes
`editMessages`. Stopped the test app and removed its marked test data
afterward.
- `git diff --check` — passed. Live provider rewind was not rerun;
service tests cover the editing flow. The broader verification inventory
remains blocked by pre-existing drift for the unmapped `browser` CLI
family.

> AGENT GENERATED
## Human comments


## What was wrong

The unshipped idle Claude process release setting exposed a dormant
provider behavior and its associated bridge lifecycle path despite
having no supported use.

## What changed

Removed the provider setting, option derivation, bridge protocol fields,
idle-release implementation, obsolete coverage, and related CLI guidance
and documentation.

## How you verified

`pnpm exec turbo run typecheck test
--filter=bb-plugin-provider-claude-code`

348 tests passed.

Fixes #

> AGENT GENERATED
## Human comments

## What was wrong

Timeline pages grouped independent event/byte windows and assigned
fragment IDs. The client merged by row ID, so boundaries could change
accepted messages to pending, fragment turns, and lose or duplicate
content. Reproduced on main before implementation: three of four large
copied-corpus walks disagreed with canonical history.

## What changed

Select conversation groups using existing anchors, load their full
turn/delegation context, and filter context-only groups out of page
ownership. Oversized groups paginate canonical children under stable
summary IDs; the client and CLI recursively merge those children. The
existing detail endpoint and SDK now support continuation cursors, and
the app walks them on expansion.

Opaque cursors bind the history tip, grouping version, status, and
display surface. Appends stay outside an existing walk. Pagination
continues on a best-effort basis across history edits and suffix
replacement: pages loaded before and after an edit may disagree until
history is reloaded. A new latest snapshot replaces loaded history
because late events can change earlier grouping. Cursors whose anchor
event was deleted return HTTP 400, preserving main's behavior. Existing
message-edit notifications invalidate request and grouping-context
caches. There is no new database table, migration, event trigger or
daemon wire change. Main's large-output sidecars remain intact; no
rejected streaming-projection code was used.

## How you verified

- Actual Hono endpoint plus actual client merge matched complete
canonical content/order on four copied threads: 63,853, 42,033, 32,384,
and 22,940 events. The oracle removes one exactly duplicated canonical
delegation row in the fourth thread; it does not discard differing
content with the same ID.
- Tiny event/512-byte targets, huge turns/details, nested/late events,
straddling items, appends between requests, continuation after in-place
edits, same-sequence suffix replacement, rejection of deleted anchors,
and a stale in-flight app page are covered.
- Turbo: 158 server timeline/public-route tests, 19 client merge tests,
142 database event/migration tests, 30 app query/controller tests, 18
CLI log tests, 104 SDK tests, and 67 server-contract tests. Affected
server/client/CLI/app/database/SDK/contract typechecks passed. Server
tests and typechecks passed again after rebasing onto `682f0d2c5d`.
- Isolated source browser: paged to all 45 synthetic messages exactly
once, expanded the oldest summary into 20 commands, and reloaded. Source
CLI `thread log --all --format verbose` returned all 45 messages exactly
once. Owned dev processes/store were cleaned up. No provider execution
or mobile Safari verification is claimed. The verification inventory has
an existing unrelated unmapped `browser` CLI family.

The initial 1.7–5.0 s walks were rejected as too slow. The optimized
paired run restores main's original timeline builder, event queries and
client merge from `682f0d2c5d` and runs main/fixed/fixed/main per copied
thread, with fresh databases and route caches. Correct walks now take
**0.72–1.76 s**, versus **0.68–2.15 s** for main: the two largest walks
are faster, and the other two are within 6%. All eight fixed walks match
canonical content/order. Cold latest requests retain roughly 3 ms and 13
ms overhead on two threads; exact numbers are in the report.

The optimization scopes payload context, selects turn IDs before reading
full event bodies, skips already-loaded request lookups, and uses
existing selective indexes for turn, child and interruption queries.
Only compact request-to-turn IDs and an ordering boundary are cached per
exact snapshot; no projected rows or payloads are cached. The profile
clears that cache to include its cold construction cost. New tests guard
against decoding unrelated history and acceptance after a later
conversation boundary. CI's missed optional-contract and old
sequence-fragment expectations are now covered and pass locally.

Ordering overlap detection uses a linear scan, with six cases for
accepted turns, overlapping alternatives and expired spans. A paired
comparison against `51e0dc78d2` preserves canonical results but does not
establish another general latency improvement. History-edit invalidation
was removed in favor of the best-effort contract above; regression tests
require these cursors to continue.

[Full benchmark
report](https://github.com/get-bb/bb/blob/bb/timeline-pagination-correctness/docs/timeline-pagination-verification.md).
[Pagination lifecycle and SDK
contract](https://github.com/get-bb/bb/blob/bb/timeline-pagination-correctness/docs/timeline-pagination.md).

> AGENT GENERATED
## Human comments

## What was wrong

Several synchronous database paths read more event history or payload
data than their results required: filtered high-water marks aggregated
complete histories, open-turn recovery did not seek directly past
interruption boundaries, request recovery repeated scans for requests
from the same thread, and search loaded text before limiting matches.
Destroyed-environment cleanup bounded rows but could rewrite megabytes
in one batch. The timeline byte-budget helper also aggregated up to
2,001 payloads before potentially scanning them again.

## What changed

Six separate fixes:

| Issue | Fix | Before → after, ms |
|---|---|---:|
| Latest sequence for seven active threads | Seek the final indexed
event per requested thread | 5.795 → 0.007 |
| Open accepted turns | Seek past each requested thread’s latest
interruption | 1.467 → 0.216 |
| Search for `the` | Rank and limit metadata before loading result text
| 87.482 → 80.570 |
| Eight request IDs in one thread | Group request IDs so each thread
history is scanned once per batch | 21.932 → 2.926 |
| Large-event environment detachment | Cap each batch at 256 KiB,
preserving progress for oversized events and existing yields between
batches | 4.038 → 0.619 longest batch |
| Timeline byte-budget helper | Stop at the byte budget without a
preceding aggregate scan | 5.075 → 0.819 in the historical 512 KiB case
|

These are controlled warm benchmark medians from a private database
snapshot or isolated fixtures, not deployed latency measurements. The
original measurements preceded the removal of an experimental delta
index; that index remains present in the benchmark snapshot. Detachment
total work was 4.038 → 3.616 ms across 1 → 17 batches.

Rebased onto the snapshot-bound conversation-group pagination change in
#3325. The byte-budget helper now serves only turn-summary detail
expansion; main timeline pages no longer call it. A fresh comparison
against main at the caller’s 4 MiB budget returned identical results in
all six fixtures. An oversized 5,000-event window improved 2.667 → 1.717
ms, while a fitting 2,000-event window regressed 0.454 → 1.114 ms due to
JavaScript iteration. Retained the early-stop behavior with this
explicit tradeoff.

The net change has no checkpoint worker, schema/migration, server/daemon
wire, CLI, or configuration changes.

## How you verified

- Database regression coverage exercises lookup batching and query
plans, byte-budget early exit, and byte-bounded environment cleanup,
including an oversized first event.
- `pnpm exec turbo run test typecheck --filter=@bb/db`: 34 files, 481
tests passed; typecheck passed.
- `pnpm exec turbo run test --filter=@bb/server --
test/services/threads/timeline
test/services/threads/thread-runtime-display.test.ts
test/services/database-maintenance-sweep.test.ts
test/system/event-pruning.test.ts test/public/public-thread-data.test.ts
test/public/public-thread-diagnostic-events.test.ts
test/public/public-thread-timeline
test/public/public-thread-search.test.ts`: 20 files, 205 tests passed,
including the new pagination and detail-expansion cases.
- `pnpm exec turbo run build typecheck --filter=@bb/server`: passed.
- `git diff --check`: passed. Rebase range comparison confirmed
unchanged patches.
- Fresh timeline helper benchmark: migrated in-memory database, five
warmups, 31 alternating measured iterations per implementation, exact
result equality. Production was not modified or restarted; no deployed
latency or full historical corpus replay is claimed.

> AGENT GENERATED
## Human comments

## What was wrong

The `desktop-latest` release has published a Linux x64 AppImage and a
`desktop-version-linux.json` feed since desktop 0.42, but getbb.app only
advertised "Download for macOS" everywhere and told Linux visitors to
use `npx`. There was no Linux download redirect on the site at all.

## What changed

- `apps/web/src/landing/site.ts`: a `DesktopPlatform` type and a
`DESKTOP_DOWNLOADS` table (labels, installer extension, version feed
URL, redirect path) replace the macOS-only constants.
`downloadHref(platform, placement)` replaces `downloadMacosHref`.
- `apps/web/src/landing/endpoints.ts`: `handleDownload(platform, ...)`
generalizes the macOS redirect. It fetches the platform's feed, picks
the first asset with that platform's installer extension, and falls back
to the release page. The PostHog event keeps its per-platform name
(`landing_download_<platform>_clicked`) and `download_target`, so the
existing macOS dashboards keep working.
- `apps/web/src/routes/download.linux.tsx`: new `/download/linux` route.
`/download/macos` now calls the shared handler.
- `apps/web/src/landing/desktop-platform.ts`: `detectDesktopPlatform`
reads client hints, `navigator.platform`, and the UA string. It returns
`linux` for X11 Linux (not Android or ChromeOS), `macos` for Macs (not
iPads reporting `MacIntel`), and `null` otherwise. `useDesktopPlatform`
applies it after hydration with macOS as the SSR default.
- Hero, nav, and footer download links follow the detected platform. The
hero shows an "Also for macOS" / "Also for Linux" link under the primary
button, and the `npx` note now reads "Windows (via WSL), Intel Macs &
remote machines". The Linux button uses a new inline `LinuxIcon`
carrying the CC0-licensed Tux glyph from Simple Icons, since hugeicons
free has no Linux icon.
- Windows and mobile visitors see the same page as before (macOS button
plus `npx`).

Design note: this is the "detect the visitor's OS, primary button
follows it, other platforms as a secondary link" pattern that VS Code,
Cursor, Zed, and Warp use. Two alternatives were mocked up and rejected:
side-by-side macOS and Linux buttons (three CTAs crowd the hero), and a
static macOS button with an "Also available: Linux AppImage" line
beneath (buries the Linux path for Linux visitors).

## How you verified

- New tests: `desktop-platform.test.ts` covers client hints, X11 Linux,
Android, ChromeOS, macOS, iPad-as-MacIntel, and Windows.
`endpoints.test.ts` adds Linux feed resolution, a guard that a Linux
request never returns a `.dmg`, and asserts the tracked event name,
`download_target`, and placement. `site-chrome.test.tsx` asserts the SSR
nav href stays `/download/macos?placement=nav`.
- `pnpm exec turbo run typecheck test lint --filter=@bb/web` passes (23
files, 124 tests).
- Manual, against `vite dev`: `curl -I /download/linux` returns a 302 to
`bb-0.42.1-x86_64.AppImage`; `/download/macos` still returns the 0.42.1
dmg. In headless Chrome on Linux the nav and hero show "Download for
Linux" with hrefs to `/download/linux`; with a macOS UA and `MacIntel`
platform emulated they show "Download for macOS". Checked the 390px
dark-mode layout as well.



🤖 Generated with [Claude Code](https://claude.com/claude-code)

> AGENT GENERATED

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
## Human comments

## What was wrong

Docs could not load any vault when the host list was nonempty:
`listNotes` returned `rpc output validation failed`. PR #3227 removed
`type` from Docs' strict host schema, while the SDK continued returning
that field. Existing Docs fixtures returned empty host lists, hiding the
regression.

## What changed

Strip additional host properties from the Docs RPC response while
continuing to validate its declared fields. Add regression coverage
using the complete SDK host fixture for both successful file listing and
the unavailable-vault fallback.

## How you verified

- Both new regression cases failed before the fix with `Unrecognized
key: "type"`; all 68 Docs tests pass afterward.
- `pnpm exec turbo run test typecheck build
--filter=bb-plugin-simple-notes` passed.
- Verified in an isolated source dev app with a connected host and
synthetic vault. Temporarily restoring the original validator reproduced
the exact error in both the browser and real RPC. Rebuilding/reloading
the fixed plugin loaded the vault and opened its Markdown note through
browser input. Captured before/after screenshots and RPC responses.
- Secret-model scan clean for the working tree, HEAD, added lines, and
commit messages in the push range.
- SlopCop skipped as requested.

> AGENT GENERATED
## Human comments

## What was wrong

- Thread-list controls changed placement and menu structure between
Pinned, projects, sections, machines, and loose threads. Worktree
creation was hidden in its menu, and independently styled buttons made
the sidebar harder to scan and maintain.
- Pinned could remain stuck above unrelated groups after its threads
scrolled away. Compact worktree actions were inaccessible to touch, and
reversing Updated at also reversed the existing active-first priority.

## What changed

- All organizing headers share New thread followed by **…** on
hover/focus, with visible controls on touch. Every header menu offers
New project, New section, and compact nested Organize and Sort by menus,
followed by existing context-specific actions.
- Organize is an exclusive choice and closes after selection. Sort
remains open while tuning; selecting the current field reverses its
arrow and direction while Updated at keeps active threads first. The
additive `sidebar.sortDirection` preference uses the existing
persistence, SDK, and CLI paths; `default` preserves prior ordering.
- Worktrees expose New thread inline, leaving Rename and Archive in
their menu. Project editing actions are grouped together and separated
from the existing Remove action. No creation destinations or destructive
consequences change.
- Shared row-control components and semantic color/state tokens keep
titles primary, grouping labels quieter, and controls
secondary—including on hovered or selected rows. Navigation **More**
uses a quieter resting color. Pinned now scrolls with its contents.
- Production Sidebar Overview stories replace the discarded prototype.
The implementation removes superseded controls and delegates menu
open/close state to the shared dropdown; the slimming pass removes 96
net lines (78 production, 18 tests) without changing approved behavior.
Thread nesting, archive placement, plugin actions, and command-palette
behavior are outside this change.

## How you verified

- [Remote CI
passed](https://github.com/get-bb/bb/actions/runs/34518997666) at exact
head `cde7aeceeeb00958b82badfecd03437b04059f2d`: build/typecheck/lint,
all app/server/package/integration tests, and Linux/macOS package smoke.
All 14 reported checks passed; native iOS and Node compatibility were
workflow-skipped. No local CI-equivalent checks were run.
- Rebased onto `main@e02f6e6`,
retaining the prior merge resolutions. Tree comparison confirms the
result is exactly the previous implementation plus upstream changes; the
PR remains 27 files. Main's touch-visibility regression exercises the
actual shared project controls through closed, open, and reclosed
states.
- Exact-head Chrome for Testing **153.0.8010.36** verification passed at
**1440×900** and **390×844**: project menu ordering, Organize closing
after selection, Sort remaining open during direction changes, and
compact Back/close/reopen reset. Project controls remain visible and
interactive with 36×36 touch targets before/during/after opening.
Restored fixture preferences persist after reload.
- Earlier bounded QA covered project/machine/custom views, active-first
sorting, light/dark control states, plugin styling, Pinned scrolling,
worktree creation/rename, and collapsed touch controls. Retained
regression tests cover action ordering, callbacks, focus behavior, menu
lifecycle, preference validation, and active-first date sorting.
- **Remaining verification limits:** phone coverage is Chrome touch
emulation, not native iOS/Safari. The existing compact retained-focus
`aria-hidden` warning also occurred during close/reopen; the app root
remained non-inert and exposed. The warning is not claimed fixed by this
rebase.
- One cumulative review was completed previously; no second review was
run. `git diff --check` passed.
- Screenshots use the same synthetic data, route `/`, project view,
Updated at sorting, light theme, and open Atlas project menu. Before:
exact merge base `e02f6e64cc7afdf490e70a662c6445b5b8dee407`. After:
exact head `cde7aeceeeb00958b82badfecd03437b04059f2d`. Desktop images
are matching 460×844 crops of a 1440×900 viewport; phone images are full
390×844 captures.

| Surface | Before | After |
| --- | --- | --- |
| Desktop project menu | ![Before — desktop project
menu](https://github.com/user-attachments/assets/522d5143-469a-4371-99b3-862977e6305f)
| ![After — shared header
menu](https://github.com/user-attachments/assets/769d343d-e613-42b3-9572-bca9d7b147be)
|
| Phone project menu | ![Before — phone project
menu](https://github.com/user-attachments/assets/36ad3004-622c-478e-8993-ff668fe869df)
| ![After — phone header
menu](https://github.com/user-attachments/assets/f138f33c-fae4-4815-82fe-ec49445fa456)
|

BB-Thread-ID: thr_ccffp4w2p2

> AGENT GENERATED
## Human comments

## What was wrong

- Custom-organized sidebar threads could be moved by drag and drop, but
their row actions offered no destination picker.

## What changed

- Adds **Move to section** with `MoveTo` to active root-thread actions
in **Custom** view: desktop dropdown/right-click submenus and a compact
long-press step with **Back**.
- Follows saved sidebar order, including **Threads**, and omits
**Pinned**. The current destination is disabled for unpinned threads.
Existing mutations preserve move persistence; moving a pinned thread
unpins it, and choosing its stored section only unpins it.
- Integrates with the shared sidebar header controls from #3402. The
focused diff remains 12 files; main's bundle limits are unchanged.

## How you verified

- Remote [CI](https://github.com/get-bb/bb/actions/runs/34524003966) for
`d334e6953963fd5891a1fdb7eb75f020cd604d0f`: **14 passed, 2 skipped**,
including checks, app/server/package/integration tests, and macOS/Ubuntu
package smoke. Optional Node compatibility and native iOS jobs were
skipped. The app fits unchanged bundle limits: **1681.7 KB raw / 410.9
KB brotli** against **1683.2 KB / 419.0 KB**.
- Menu and mutation tests cover desktop/context parity, eligibility,
compact Back/reset, destination no-op, section moves, and both pinned
transitions. Existing optimistic-update and serialized-unpin coverage
remains.
- Final-head Chrome for Testing **153.0.8010.36**: shared header **By
project → Custom** eligibility; desktop dropdown and right-click moves
to a section and **Threads**; hard-reload persistence and disabled
current destination; compact touch long-press, **Back**, close/reopen
reset, and saved moves. No captured runtime errors or compact app-root
`inert`/`aria-hidden` changes. All six synthetic threads and sidebar
settings were restored. The unchanged pinned transitions were exercised
before this rebase and retain passing remote mutation coverage.
- Native iOS is unverified: the local Xcode/CoreSimulator setup is
incompatible and cannot resolve `simctl`; remote iOS coverage was
skipped. Phone-width Chrome does not establish native iOS or Safari
behavior.

| Surface | Before — merge base `de9361268088` | After — head
`d334e6953963` |
| --- | --- | --- |
| Desktop web · 1440 × 900 | ![Before: shared sidebar headers and row
actions without Move to
section](https://github.com/user-attachments/assets/1bd7059a-e7af-454b-8cbd-a581f773027c)
| ![After: Move to section alongside shared sidebar
headers](https://github.com/user-attachments/assets/53f120a5-c178-4638-8d07-0d4f05aee7dd)
|
| Phone web · 390 × 844 | ![Before: compact long-press row actions
without Move to
section](https://github.com/user-attachments/assets/ce444e5c-34ca-4b0f-b178-6c23b5452269)
| ![After: compact Move to section step with
Back](https://github.com/user-attachments/assets/1b80ab7a-888f-4b9b-8205-ccae4cc6074e)
|

- Both revisions use `scripts/bb-dev-app current` under **Node
22.19.0**, sequentially in the same managed worktree. Captures match the
synthetic **Release notes** thread in **Follow-ups**,
`/threads/thr_move_release`, **Custom** organization, light theme, scale
1, and viewport per row. Images were inspected directly.

BB-Thread-ID: thr_ccffp4w2p2

> AGENT GENERATED
## Human comments

## What was wrong

After #3390 enabled hidden entries, every file-mention query recursively
scanned ignored build output, dependency stores, and nested worktrees.
Incremental typing started overlapping scans. A large checkout took 5.84
seconds to search roughly 304,000 entries, leaving the new-thread
composer on “Searching mentions…”.

## What changed

- Project and environment searches ask Git for ignored paths and prune
those subtrees before traversal. Tracked files and non-ignored untracked
files remain searchable, including hidden files. Empty directories,
linked worktrees, and non-Git workspaces remain supported.
- Concurrent queries share pending discovery but rank their results
independently. Completed and failed listings are discarded, so
subsequent queries see file and ignore-rule changes. Each fresh
Git-backed discovery uses two Git subprocesses; there are no per-file
subprocesses.
- General filesystem APIs and skill/thread-storage listing retain their
existing ignore behavior. The server explicitly supplies the new
`respectGitIgnore` daemon field; protocol version increases from 198 to
199.
- Keep `.pnpm-store` and root-relative `.claude/worktrees` in the
default exclusions. Exclusions now support exact root-relative paths as
well as basenames. Update CLI help and guide/SDK documentation.

## How you verified

226 relevant tests passed, including tracked files matched by ignore
patterns, new untracked files, nested ignore rules and negation, linked
worktrees, hidden files, shared discovery, failure recovery, and refresh
after edits:

```sh
pnpm exec turbo run test --filter=@bb/host-daemon -- file-list.test.ts workspace-path-list.test.ts
pnpm exec turbo run test --filter=@bb/host-daemon -- workspace-dispatch.test.ts
pnpm exec turbo run test --filter=@bb/server -- host-file-routes.test.ts public-environments.test.ts internal-environment-change.test.ts public-thread-data.test.ts
pnpm exec turbo run test --filter=@bb/host-daemon-contract
pnpm exec turbo run typecheck --filter=@bb/host-daemon --filter=@bb/server --filter=@bb/cli
```

With both new directory exclusions disabled, the same checkout improved
from **5.84 s / 304,068 entries to 119 ms / 6,681 entries**, retaining
README and hidden workflows.

Verified in an isolated source app with Worktree selected: incremental
`@README` search returned tracked and untracked files, selecting the
untracked file inserted its mention, and an arbitrary Git-ignored build
directory was omitted. The source CLI confirmed general filesystem
listing can still inspect ignored files. No deployed instance was
changed.

> AGENT GENERATED
## Summary

`.bb-env-setup.sh` runs `pnpm install` on every new environment. For a
fresh git worktree that is necessary, but copy-on-write environments
(reflink copy or Btrfs snapshot of the checkout, via the `btrfs-cow`
provider plugin) arrive with `node_modules` already installed, and the
install is a 5 to 15 s no-op.

This change hashes the install inputs (`pnpm-lock.yaml`,
`pnpm-workspace.yaml`, `package.json`, `.npmrc`) and stores the hash at
`node_modules/.bb-env-setup-install-hash` after a successful install. On
the next run, if the stamp matches and `node_modules/.pnpm` exists, the
install is skipped.

Because the stamp lives inside `node_modules`, it travels with a copied
tree and disappears with a fresh worktree, so worktree provisioning is
unchanged.

## Behavior

Measured on a reflink copy of this repo (Linux, Btrfs):

| Scenario | Result |
|---|---|
| Copied tree, no stamp yet | installs, 5.8 s, writes stamp |
| Copied tree, stamp matches | skipped, 0.01 s |
| Lockfile changed | installs, 6.3 s, rewrites stamp |
| No `node_modules` (fresh worktree) | installs, 13.7 s, writes stamp |

Other notes:

- A failed `pnpm install` no longer writes a stamp, so the next
environment retries. The hook still exits 0 in every case, as before.
- Falls back to always installing when neither `sha256sum` nor `shasum`
is available.
- Existing checkouts get the stamp the first time the hook runs after
this lands; running `sh .bb-env-setup.sh` in the project checkout once
seeds it for copies made from it.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
#3437)

## Human comments

## What was wrong

The follow-up prompt box only recognised Meta+Enter as the modifier
submit (`PromptBoxInternal.tsx` explicitly required `metaKey` and
rejected `ctrlKey`). On Windows and Linux there was no working key for
the secondary action: in Queue mode nothing steered, in Steer mode
nothing queued. Worse, Ctrl+Enter fell through to the plain-submit
branch, which only checks Shift, so it silently performed the primary
action instead. The `aria-keyshortcuts` attribute, the Settings picker
descriptions, and `docs/configuration.md` all described the shortcut as
Command+Enter only. The tasks plugin editor already accepted either Meta
or Ctrl plus Enter; the composer was the odd one out.

## What changed

- `apps/app/src/components/promptbox/modifier-submit-shortcut.ts` (new):
`isModifierSubmitKeyEvent` accepts Enter with Meta or Ctrl and no
Alt/Shift, plus platform-aware label and aria helpers built on the
existing `formatAppShortcut`/`formatAppShortcutAria` mapping ("⌘ Enter"
/ "Meta+Enter" on Mac, "Ctrl + Enter" / "Control+Enter" elsewhere).
- `PromptBoxInternal.tsx`: uses the shared key check and sets
`aria-keyshortcuts` from the platform helper instead of a hard-coded
"Meta+Enter".
- `FollowUpPromptBox.tsx`: the queue/steer submit tooltips now include
the modifier shortcut and what it does, e.g. "Queue follow-up (Enter),
Ctrl + Enter to steer", shown only when a modifier submit is available.
- `SettingsView.tsx` and `docs/configuration.md`: mention Ctrl+Enter on
Windows and Linux alongside Command+Enter.

No wire or CLI changes.

## How you verified

- New `modifier-submit-shortcut.test.ts` covers key matching
(Ctrl+Shift+Enter and Meta+Alt+Enter rejected) and the platform labels.
- New `PromptBoxInternal.test.tsx` cases: Ctrl+Enter on a Linux platform
calls `onModifierSubmit` and not `onSubmit`, and `aria-keyshortcuts`
reads "Control+Enter" on Linux and "Meta+Enter" on Mac. The Ctrl+Enter
case fails before this change (it called `onSubmit`).
- New `FollowUpPromptBox.test.tsx` cases assert the tooltip text for
both steer-on-Enter settings on Win32.
- `pnpm exec vitest run` on the three promptbox test files: 163 passed.
- `pnpm exec turbo run typecheck lint --filter=@bb/app`: passes.

> AGENT GENERATED

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
## Human comments

## What was wrong

Opening Account Pooler settings sat on "Loading…" for a long time. The
`status.get` RPC assembled `routedThreadsWithoutLocalLogin`, which calls
`system.providerStates` over host RPC for every machine that routed a
thread in the last window. A sleeping or offline machine stalls that
call up to the host RPC deadline, and the UI rendered nothing until the
whole status returned. Every action afterward (toggle, refresh usage,
reorder) re-ran the same slow status call.

## What changed

- `plugins/account-pool/src/operations.ts`, `rpc.ts`, `contracts.ts`:
`status.get` no longer waits on host RPCs. The routed-thread inspection
is its own `status.routedThreads` RPC. `statusSchema` drops
`routedThreadsWithoutLocalLogin`; a new `statusReportSchema` /
`PoolStatusReport` keeps the combined shape.
- `plugins/account-pool/src/cli.ts`: `bb pool status` calls both
operations and prints the same text and `--json` output as before.
- `plugins/account-pool/app.tsx`: the settings view caches the last
`PoolStatus` in `localStorage` (validated with `statusSchema` on read)
and renders it immediately. While the live status is in flight the hub
line shows "refreshing…", rows show "refreshing usage…", and quota
values are dimmed. A row also shows the refreshing state while its
manual "Refresh usage" RPC is pending. The empty-state card only renders
from live status so a stale cache never flashes "No accounts in the
pool".

No wire changes to the host daemon protocol; this is plugin-internal
RPC.

## How you verified

- New tests in `plugins/account-pool/app.test.tsx`: cached accounts
render with the refreshing indicator before `status.get` resolves and
the cache is rewritten afterward; a malformed cache entry falls back to
the loading state; a row shows "refreshing usage…" while
`account.refreshUsage` is pending. The first and third fail on main (no
cache, no indicator).
- `plugins/account-pool/src/server.test.ts` updated to read routed
threads from `status.routedThreads` and parse CLI JSON with
`statusReportSchema`.
- `pnpm exec turbo run test typecheck --filter=bb-plugin-account-pool`
passes (10 files, 274 tests).
- Bundled the plugin app once through `buildPluginApp` to confirm the
runtime `statusSchema` import bundles cleanly.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

> AGENT GENERATED

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…#3436)

## Human comments

## What was wrong

The prompt-box environment picker offered every environment provider on
every machine, so CoW copy showed up under Macs where its reflink probe
can never succeed. Per-machine plugin availability existed briefly
(#3227), but #3393 removed it from discovery because the app's
per-machine fan-out blocked the picker on the slowest machine's Git
inspection and plugin probe. That fix removed the information rather
than making it non-blocking: discovery became structural only,
availability is always null, and an unsupported choice now fails only at
thread creation.

## What changed

Server
(`apps/server/src/services/environments/provider-machine-availability.ts`,
new): a background per-machine availability probe. When a project's
providers are listed, each structurally eligible provider and connected
persistent machine is probed in the background (Git inspection when
`requires.gitCheckout`, then the plugin's `availability` hook), keyed by
provider, project, and machine. Listing never awaits a probe: it returns
the cached answer or null. Results are cached for ten minutes, cleared
when a plugin rechecks, and disconnected machines are never probed. When
an answer changes, the server emits a new
`environment-availability-changed` system change so clients refetch.

Contract (`packages/server-contract/src/api/system.ts`): each provider
now carries `machineAvailability`, a map from host id to `available`,
`setup-required`, `unavailable`, or null while unknown. `availability`
is filled from that map when `hostId` is given. Thread creation still
checks the selected provider and machine afresh, exactly as #3393 left
it.

App: `NewThreadComposer` lists providers for the current project and
derives each machine's rows from `machineAvailability`. The picker hides
providers a machine reports as unavailable, except the currently
selected one, which stays visible and disabled with its reason so the
selection never disappears; setup-required rows stay selectable with
their message; unknown rows show as before. The provider list is
remembered per project in localStorage through the existing last-known
cache and served as placeholder data, so the picker is already trimmed
on the first open after a reload. The realtime registry maps the new
system change to the provider query.

CLI: `bb environment providers --project <id> --machine <id>` prints
each provider's availability on that machine, and `--json` includes
`machineAvailability`. Guide template, bb-cli skill,
`docs/api_to_audit.md`, the Plugin Guide surface text, and the SDK doc
comment were updated to describe background probing.

No server/daemon wire fields changed, so `HOST_DAEMON_PROTOCOL_VERSION`
is unchanged.

## How you verified

- Server tests in
`apps/server/test/threads/environment-providers.test.ts` rewritten for
the new contract: listing answers null first, the background answer is
served on the next listing and `environment-availability-changed` fires
once; Git inspection runs in the background and a missing branch is
reported as unavailable; disconnected machines are not probed; unmet
structural requirements never probe; a plugin recheck invalidates the
cache and probes again; a throwing hook is reported as unavailable
without failing the listing; creation still checks afresh. 43 passed.
- App tests: picker hides unavailable rows, keeps the selected
unavailable row disabled with its reason, keeps setup-required
selectable with its message, hides per machine in the grouped menu;
provider query writes and serves the last-known cache and rejects a
corrupt entry; realtime registry covers the new system change.
- CLI test for the availability column with available, unavailable, and
unknown rows.
- `pnpm exec turbo run typecheck` for server, app, cli, domain,
server-contract, sdk, plugin-sdk, plugin-api-map, and templates passes.
- `pnpm exec turbo run test` for cli, server-contract, sdk, domain,
templates, plugin-api-map, and plugin-sdk passes. Full server and app
Vitest suites pass (the app's turbo test task fails before tests on this
machine because its PWA icon check needs a libvips module that isn't
installed, so the app suite was run with vitest directly).
- Manual, on the earlier client-only version of this change: a dev
server with the btrfs-cow plugin and two projects on one machine, one on
btrfs and one on tmpfs; the tmpfs picker omitted CoW copy and was
already trimmed on first open after a reload.

> AGENT GENERATED

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Integrates #3163 and #3165: inline-vis accepts source="workspace" | "thread-storage" and renders .md/.markdown files with the host Markdown renderer alongside sandboxed HTML previews.

Fixes #3158
Fixes #3159

Co-authored-by: Ilya Martynov <ilya@martynov.org>
## Human comments

## What was wrong

The Cmd-K palette consumed Enter and navigation keys during IME composition. Confirming a candidate could activate the highlighted result and close the palette; navigation keys could move the palette selection.

## What changed

Return before palette keyboard handling when the native event is composing. Cover Enter, ArrowDown, ArrowUp, Home, and End, preserving selection and leaving composing events unconsumed. Also verify ordinary navigation and exactly-once command activation after composition ends. Combines the useful regression assertions from #3414 without duplicating its identical production fix.

No wire, SDK, CLI, configuration, or host-daemon protocol changes.

## How you verified

- Independently reproduced on pristine main `7ba69eb050c4a800db9255c8fbaaa23ca4bab32d` in the running app using Chromium DOM CompositionEvent/KeyboardEvent dispatches.
- Regression-only run before the guard: 6 failed, 23 passed.
- `pnpm exec turbo run test --filter=@bb/app -- src/components/commands src/lib/command-palette`: 76 passed across 7 files.
- `pnpm exec turbo run typecheck build lint --filter=@bb/app`: passed; lint reported 190 warnings and zero errors.
- Changed-file formatting and `git diff --check`: passed.
- Real Chromium browser QA: 10 scenarios passed, including unchanged composing selection, default-prevention semantics, resumed navigation and one sidebar toggle after normal Enter.
- Composition events were synthetic (`isTrusted: false`); ordinary keys used trusted browser keyboard input. Native OS Pinyin candidate UI, Safari event ordering, and iOS were not verified.

Fixes #3413

> AGENT GENERATED
## Human comments

## What was wrong

Opening a thread with a plugin palette such as Monokai could leave diff
bodies blank. The diff worker pool resolved its initial theme before the
theme-sync component registered that palette's loader, producing `No
valid theme loader registered`.

## What changed

Register resolved custom theme files at the worker-pool acquisition
boundary before creating the singleton. Existing theme synchronization
still handles later palette changes.

## How you verified

- Added a regression test using Pierre's real theme resolver. It fails
with the original missing-loader error before the fix and passes
afterward.
- Eight focused worker-pool and theme synchronization tests pass.
- All CI test packages pass in a clean checkout with a minimal
environment. The integration suite passes all 77 tests across 26 files
with `--maxWorkers=4`.
- Clean-branch CI build, typecheck, lint, bundle budgets, version
guards, provider-literal ratchet, SDK npm guard, and macOS tarball smoke
pass. The SDK guard used npm 10 for its supported JSON output.
- Linux AppImage checks require a Linux/FUSE runner. Actions cache usage
requires the GitHub Actions token and was skipped locally.
- In the source-built dev app, cold-loaded a recovered Smart Diff using
the Monokai palette. The diff body rendered with syntax highlighting and
no browser errors.

> AGENT GENERATED
@ymichael
ymichael merged commit a3e8e10 into main Sep 10, 2026
16 checks passed
@ymichael
ymichael deleted the slopcop/issue-3210 branch September 10, 2026 23:13
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.

Tasks plugin loads every task row and issues one query per task on every bb thread status change