Skip to content

Integrate upstream 0.41.0 and the dispatch-queue rework; re-tail the fork migration - #6

Merged
NeelM0906 merged 79 commits into
mainfrom
integration/upstream-2026-09-02
Sep 2, 2026
Merged

Integrate upstream 0.41.0 and the dispatch-queue rework; re-tail the fork migration#6
NeelM0906 merged 79 commits into
mainfrom
integration/upstream-2026-09-02

Conversation

@NeelM0906

@NeelM0906 NeelM0906 commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Human comments

What was wrong

The fork was 75 commits behind get-bb/bb after PR #5, missing the 0.41.0 release the user's desktop app already runs, the dispatch-queue rework (get-bb#2779: every send is a dispatch attempt, typed waitingOn queue rows, the message.dispatch plugin checkpoint and the concurrency-limit plugin), persisted conversation outlines (get-bb#2668), the notification center, and the steer-on-Enter default. Two of those upstream changes collided with fork-owned code: the rework replaced the queued-message-auto-send follow-up the fork used to release a thread's work admission on turn completion, and upstream shipped migrations 01100112, so the fork's admission/lease migration, also numbered 0110, would have been silently skipped by Drizzle on any database that had already applied upstream's tail (the user's live ~/.bb/bb.db is one), leaving the admission tables missing at runtime.

What changed

  • Stacked on Integrate upstream post-0.40 stability batch and fix fork CI reds #5 (integration/upstream-2026-08-31) so the diff shows only this batch; retarget to main once Integrate upstream post-0.40 stability batch and fix fork CI reds #5 lands.
  • Merged upstream/main (75 commits, through 88ee0d012) preserving the fork features: workspace admission/leases and protectUnmanagedWorkspace, provider-process ownership and diagnostics, the DB read worker, timeline fork history, explicit GitHub repo tracking, GitHub-hosted CI runners.
  • Wire change: HOST_DAEMON_PROTOCOL_VERSION bumped to 177, ahead of upstream's 176, because the fork's admission RPCs change the wire; enrolled fork daemons must update.
  • Migration re-tailed: the fork's schema addition is regenerated with drizzle-kit generate as 0113_workspace_safety after upstream's 0112, with IF NOT EXISTS on its CREATE statements so databases that already ran the old 0110 replay cleanly; migrate.ts's protect-column staging hack and the migrate tests point at the new tag. Upstream's 01100112 and their snapshots are taken verbatim.
  • Admission release on turn completion now lives in the queued-message-dispatch follow-up (thread-ready wake) in apps/server/src/internal/events.ts; daemon connect runs the fork's admission reconcile and upstream's host-connected dispatch wake.
  • Thread creation, the conversation-outline route, and the thread count/running routes take upstream's implementation; the fork's read worker keeps serving thread-list and timeline snapshots.
  • Dropped the fork's now-dead deferred_thread_messages data exports and the removed provider-session-reaping experiment flag (upstream deleted both).
  • Fork-required fields (admission, protectUnmanagedWorkspace) added to upstream's new test fixtures in the app, CLI, and concurrency-limit plugin; the GitHub plugin's new extraRepos schema validation uses the fork's repo parser.
  • Plugin SDK version follows upstream (0.4.35); SDK inventory hash regenerated.
  • Fork CI: Linux AppImage smoke. After the merge the lifecycle smoke failed deterministically on GitHub-hosted ubuntu-latest with "The owned runtime exited before bb became ready", while upstream is green on Blacksmith. Two diagnostics landed to find it and stay: the smoke script now dumps BB_DATA_DIR/logs tails on failure, and the desktop logs the captured child output when bb-app exits before the server is ready. That log showed the nested runtime dying with exit 9, bad option: --no-sandbox: Ubuntu 24.04 restricts unprivileged user namespaces, the AppImage toolset's AppRun (from upstream Fix AppImage pre-runtime SIGTRAP flake get-bb/bb#2885's electron-builder upgrade) responds by adding --no-sandbox to the payload args, and the desktop's nested runtime relaunch runs that same AppImage as Node, which rejects the flag. CI now lifts kernel.apparmor_restrict_unprivileged_userns before the smoke, matching upstream's runner environment. Real Ubuntu 24.04 users of an AppImage build would hit the same failure; that is an upstream bug worth reporting separately.

How you verified

Run under fnm exec --using 22.22.1 unless noted, one turbo run at a time.

  • pnpm exec turbo run typecheck: green across the repo (initial run surfaced 13 errors from the merge, all fixed).
  • pnpm exec turbo run lint: green.
  • pnpm exec turbo run test for @bb/db (465 tests), @bb/host-daemon-contract, @bb/agent-runtime, @bb/host-daemon, @bb/cli, @bb/demo-server, @bb/plugin-api-map, bb-plugin-github, bb-plugin-concurrency-limit: green. The migrate tests that pin the fork migration's timestamp failed before the re-tail and pass after.
  • @bb/server (Node 22.23.2): full suite 2188/2196, then the 8 remaining failures fixed and their three files re-run green: two fork read-worker tests and the fork-adapted outline-reuse test asserted the outline route runs in the worker, which now takes upstream's persisted-outline path (tests follow upstream); three of upstream's new turn-starting queue wait tests asserted a turn.submit command synchronously, but the fork's admission reserve is an async host RPC, so they now wait with vi.waitFor like the fork's other dispatch tests; the other two were 5s load timeouts that pass alone.
  • @bb/app: 3724/3733 in the full run, the 5 timeouts (4 files) pass when re-run alone.
  • @bb/integration-tests (Node 22.23.2): 77/77. Note: on Node 22.19–22.22 the fork's tsx read worker cannot map a relative .js import to .ts inside a worker thread and every harness times out; Node 22.23.2 (CI's 22.x) and 24 resolve it. This is pre-existing and local-only (PR Integrate upstream post-0.40 stability batch and fix fork CI reds #5 noted the same symptom); it is not touched by this merge.
  • Conflict resolutions were reviewed against git log <merge-base>..upstream/main -- <file> provenance for each of the 22 conflicted files.

AGENT GENERATED

SawyerHood and others added 30 commits August 31, 2026 12:24
## Human comments

## What was wrong

Idle session release remained behind the `providerSessionReaping`
experiment after the provider restorability contract became available.
Users had to enable the experiment before BB released restorable
non-Codex sessions.

## What changed

BB now releases every restorable provider session after 30 idle minutes.
The change removes the experiment key, Settings toggle, server policy
route, and host-daemon policy request. It also removes the legacy
Codex-only fallback. The host-daemon protocol version increases to 175
because the internal server contract changed. The Plugin SDK version
increases to 0.4.30, and the Plugin Guide inventory includes the changed
declaration hash. The configuration guide, built-in CLI skill reference,
generated guide source, fixtures, and tests now describe the standard
behavior.

## How you verified

- `pnpm exec turbo run typecheck --filter=@bb/agent-runtime
--filter=@bb/host-daemon --filter=@bb/server --filter=@bb/app
--filter=@bb/db --filter=@bb/host-daemon-contract`
- `pnpm exec turbo run test --filter=@bb/agent-runtime --force` passed
318 tests.
- `pnpm exec turbo run test --filter=@bb/host-daemon --filter=@bb/server
--filter=@bb/app --filter=@bb/db --filter=@bb/host-daemon-contract
--force` passed all affected suites.
- `pnpm exec turbo run test typecheck --filter=@get-bb/plugin-sdk
--filter=@bb/plugin-api-map --filter=@bb/app
--filter=bb-plugin-plugin-api-docs`
- `node packages/plugin-sdk/scripts/check-npm-version-guard.mjs`
- `bb plugin build plugins/plugin-api-docs`
- `node scripts/check-provider-literal-ratchet.mjs`
- `git diff --check`

Fixes get-bb#1604

> AGENT GENERATED
## Human comments

## What was wrong

The Tasks lifecycle correctly lets a failed thread return to working,
but its shared comment formatter still described every failure as
terminal with “final message posted.” A recovered, actively working
thread could therefore retain contradictory terminal history.

## What changed

Failures now post a factual non-terminal failure comment, while
completed threads retain “completed — final message posted.” Focused
lifecycle coverage drives failure and recovery together and verifies
that recovery changes status without adding another comment. Existing
recovery, active views, completed semantics, and task status updates
remain unchanged. This is plugin-local behavior with no SDK, wire, CLI,
or documentation changes.

## How you verified

- `pnpm exec turbo run test typecheck build --filter=bb-plugin-tasks
--force` (364 tests)
- `pnpm exec oxfmt --check plugins/tasks/lifecycle/index.ts
plugins/tasks/lifecycle/lifecycle.test.ts`
- `git diff --check origin/main...HEAD`

Follow-up to get-bb#2694.

> AGENT GENERATED
## Human comments

## What was wrong

The parent-thread selector rendered every eligible thread in an
unsearchable dropdown. As projects accumulated threads, locating a
parent by title or ID became slow and error-prone.

## What changed

Extract the selector into a dedicated `ParentThreadPicker` and add
title/ID search, keyboard-friendly command navigation, empty results,
loading and retry states, and query reset on close. The metadata row now
only composes the picker and translates its selected value. Existing
lazy loading, candidate ordering, eligibility filtering, selection, and
responsive drawer behavior remain intact. This is app-only behavior with
no wire, CLI, or documentation changes.

## How you verified

- `pnpm exec turbo run test typecheck lint --filter=@bb/app --force`
(449 files; 3,541 passed, 4 skipped)
- `pnpm exec oxfmt --check` on all four changed files
- `git diff --check`
- Manual QA against parent candidates by title, ID, empty results,
selection, and clearing

Fixes: no linked issue.

> AGENT GENERATED
## Human comments

Fix for thsi:

<img width="400"
src="https://github.com/user-attachments/assets/caddb865-26b2-428d-83fb-8ca0bc5f713a"
/>


## What was wrong

Pi's provider bridge rejected a turn whenever its extracted text was
empty, even when the request contained a valid local image attachment.
As a result, sending a screenshot without accompanying text returned
`Missing input text`.

## What changed

The Pi bridge now accepts a prompt or steer request when it contains
either text or images. Image-only requests are forwarded to Pi with an
empty text payload, while completely empty requests remain invalid.
Added lifecycle coverage for a local image-only prompt.

No wire protocol, CLI, or documentation changes were required.

## How you verified

- `cd plugins/provider-pi && pnpm exec vitest run --config
vitest.config.ts src/bridge/bridge.lifecycle.test.ts --reporter=dot`
- 1 test file passed, 9 tests passed.
- The new test failed initially because its test request ID did not
match the repository's allowed format; after correcting the fixture, it
passed.

> AGENT GENERATED
## Human comments

## What was wrong

The new-thread branch hook kept its explicit base branch only in
component-local React state. Route navigation unmounted the composer and
discarded that state. The verified report is
https://get-bb.github.io/reports/issues/2743.html.

## What changed

The new-thread selection scope now retains the branch and its
project-environment scope key in memory. Component-local plugin
composers still reset on unmount. The change has no wire, CLI, guide,
dependency, schema, or stored-data effect.

## How you verified

- The new remount regression test failed before the production change:
one failed test received `undefined` instead of `origin/release`.
- The focused test passed after the change: 2 tests passed.
- The relevant app tests passed: 91 tests in 4 files.
- `pnpm exec turbo run typecheck --filter=@bb/app` passed.
- `pnpm exec turbo run lint --filter=@bb/app` passed with 0 errors.
- `git diff --check origin/main..HEAD` passed.
- The EAP model-name range scan was clean.
- The change contains 119 text lines across 3 files.

Fixes get-bb#2743

> AGENT GENERATED

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

## What was wrong

The host terminal environment did not advertise OSC 8 support, so
programs using hyperlink capability detection could flatten named
hyperlinks into display text and discard the target. The shared terminal
also did not install an OSC 8 activation handler. Its context menu did
not snapshot xterm's logical selection before xterm handled a macOS
secondary click, so xterm could replace a soft-wrapped selection with
the clicked word before Copy read it. See get-bb#2469 and the [reproduction
and root-cause
report](https://get-bb.github.io/reports/issues/2469.html).

## What changed

- Advertise hyperlink support to spawned terminal processes with
`FORCE_HYPERLINK=1`.
- Route shared-app OSC 8 activation through bb's URL navigation after
disclosing and confirming the exact target in bb's responsive dialog;
visibly detected URLs retain direct activation.
- Add terminal context-menu actions for opening/copying links and
copying the exact xterm selection across soft-wrapped rows.
- Snapshot the logical selection during context-menu capture, before
xterm's nested macOS handler can replace it.
- Add regression coverage for the host environment, shared-app handlers,
dialog behavior, and the real xterm context-menu event sequence.
- Keep the implementation in the host daemon and shared app. There are
no native mobile terminal changes because mobile now uses the shared app
terminal UI.
- No `HOST_DAEMON_PROTOCOL_VERSION` bump is needed because this changes
only the environment of newly spawned PTYs; no server/daemon wire field
or message changed.
- `FORCE_HYPERLINK` is the available opt-in for programs that do not
recognize bb as a terminal. Programs that treat it as an unconditional
override may also emit OSC 8 when their output is redirected; callers
requiring plain redirected output can set `FORCE_HYPERLINK=0` for that
command.

## How you verified

- `pnpm --filter @bb/app exec vitest run --config vitest.config.ts
src/components/thread/terminal` — 42 tests passed across 7 files.
- `pnpm --filter @bb/host-daemon exec vitest run --config
vitest.config.ts src/terminals/terminal-manager.test.ts` — 29 tests
passed.
- `pnpm exec turbo run typecheck lint --filter=@bb/app
--filter=@bb/host-daemon` — all tasks passed; lint reported 0 errors.
- `pnpm run build` — all 12 production build tasks passed.
- Oxfmt and `git diff --check` passed for the changed source files.
- Exercised the flow with Computer Use against an isolated worktree
instance: a concealed OSC 8 label disclosed its exact target in bb's
confirmation dialog, Open launched that target in a new browser tab, and
a multi-row soft-wrapped selection copied and pasted back as one
continuous logical value.

Fixes get-bb#2469

> AGENT GENERATED
… leaks (get-bb#2675)

## Human comments

## What was wrong

node-pty 1.1.0 leaks three file descriptors per spawned terminal on
macOS:

- The cleanup loop in `pty_posix_spawn` (`for (; count > 0; count--)`)
never closes `low_fds[0]`. One `/dev/ptmx` master leaks per spawn.
- The parent's copy of the slave fd is never closed.
- The kqueue opened by `SetupExitCallback` is never closed.

The host daemon spawns a pty per terminal session, so a long-lived
daemon drains the system-wide pool of 511 pty devices. On the machine
that hit this, the daemon held 505 leaked masters after 22 hours. No app
on the host, Ghostty included, could open a new terminal.

Upstream fixed all three leaks in microsoft/node-pty#882 and
microsoft/node-pty#931.

## What changed

- `node-pty` 1.1.0 → 1.2.0-beta.15 in `apps/host-daemon` and
`packages/bb-app`, plus the lockfile.
- New darwin-only regression test:
`apps/host-daemon/src/terminals/node-pty-fd-leak.test.ts` spawns five
ptys and asserts the `/dev/ptmx` fd count returns to baseline. CI's
macOS 15 runners execute it.

Why a beta: npm `latest` is still 1.1.0, and the fixes exist only on the
1.2.0 line. VS Code ships this exact version in production
(`^1.2.0-beta.15` in `package.json` and `remote/package.json`).

The only typings change since 1.1.0 is an optional `pixelSize` argument
on `resize`. Nothing on the wire changes, so no
`HOST_DAEMON_PROTOCOL_VERSION` bump.

## How you verified

- The new test fails on 1.1.0 (`expected 5 to be less than or equal to
0`) and passes on 1.2.0-beta.15. The failing test is committed before
the fix.
- A standalone 25-spawn repro leaks 25 masters on 1.1.0 and zero on the
beta.
- `pnpm exec turbo run test --filter=@bb/host-daemon`: 561 passed. One
pre-existing flake in `host-branches-dispatch.test.ts`, passes
standalone, unrelated.
- `pnpm exec turbo run typecheck --filter=@bb/host-daemon
--filter=bb-app`: passes.

> AGENT GENERATED
## Human comments

## What was wrong

Cursor skill discovery covered conventional skill directories, but not
plugins installed under the local plugin directory or completed
marketplace cache. The composer therefore omitted skills and commands
supplied by installed Cursor plugins. This is a follow-up to get-bb#1292.

## What changed

- Added a Cursor native-roots resolver for local and marketplace
plugins.
- Parsed Cursor plugin manifests and forwarded declared skills and
commands.
- Selected the newest completed marketplace revision and ignored
incomplete cache entries.
- Deduplicated local links that resolve to the same cached plugin.
- No host-daemon protocol change.

## How you verified

- pnpm exec turbo run test typecheck --filter=bb-plugin-provider-acp
--force
- 81 provider ACP tests passed across 8 files.
- Provider ACP typecheck passed.

> AGENT GENERATED
## Human comments

## What was wrong

Project branch listing exposed the app cache policy as a public
`refresh` choice. Plain `bb project branches` and
`sdk.projects.branches()` therefore defaulted to cached refs and could
omit a newly pushed remote branch unless callers knew to request a
blocking refresh.

## What changed

Make the public CLI/SDK branch operation always wait for the existing
bounded best-effort remote fetch, remove `--refresh` and its request
field, and add a fixed cache-first branch-options route for app
initialization. The app still initializes responsively and switches to
the public blocking operation when the branch picker requests fresh
remotes. The generated CLI Guide was updated; the bb-cli skill already
documents only the now-canonical plain command. The checked-in Plugin
SDK declaration inventory was regenerated for the intentional nested
`BbSdk` type change; its existing generic SDK Guide card remains
accurate. No host-daemon wire shape changed, so
`HOST_DAEMON_PROTOCOL_VERSION` is unchanged.

## How you verified

- `pnpm exec turbo run test typecheck --filter=@bb/app --filter=@bb/cli
--filter=@bb/server --filter=@bb/server-contract --filter=@bb/sdk
--force` (734 test files; 6,284 passed, 3 skipped; all typechecks
passed)
- `pnpm exec turbo run build --filter=@bb/app --filter=@bb/cli
--filter=@bb/server --filter=@bb/server-contract --filter=@bb/sdk
--force` (7 tasks passed)
- `pnpm exec turbo run lint --filter=@bb/app --force` (0 errors)
- `pnpm exec turbo run test --filter=@bb/plugin-api-map --force` (73
passed; reproduces the inventory failure before regeneration and passes
after)
- Plugin Guide required test/typecheck workflow (11 tasks passed; Plugin
SDK 220, Plugin API Map 73, app 3,551 passed / 3 skipped)
- `bb plugin build plugins/plugin-api-docs`
- `pnpm exec oxfmt --check` on all changed supported files
- `git diff --check origin/main...HEAD`

Fixes: no linked issue.

> AGENT GENERATED
## Human comments

## What was wrong

Cold app loads had multiple owners for the same system-config and
plugin-list resources, while pending-interaction query options
accidentally overwrote the shared QueryClient freshness default with
`undefined`. The plugin frontend loader also interpreted a failed shared
query as an authoritative empty installation set, and overlapping
initial-connection atom refreshes repeated local daemon discovery.

## What changed

The app now has one shared QueryClient for React and non-React owners,
with stable shared query options for system config and the
installed-plugin list. Plugin frontend reconciliation reuses that query
and preserves active frontends when inventory loading fails. Pending
interactions inherit the app freshness default unless callers provide an
override, while stale remounts still revalidate. The redundant
local-daemon connection refresh was removed, cache writers use the raw
installed-plugin array shape, and narrow regressions cover each
ownership and failure boundary.

There are no wire, daemon protocol, CLI, guide, or public Plugin SDK
changes.

Review requested from @slopcop.

## How you verified

The regressions failed before the fixes and pass afterward. Browser
verification used the copied production repro database and confirmed one
cold request each for system config, plugins, thread include,
interactions, daemon status, and workspace targets. Hosts and sidebar
retain their intentional initial-connect race-closing revalidation.

- `pnpm exec turbo run test --filter=@bb/app --force`: 443 files passed;
3,510 tests passed and 3 skipped
- `pnpm exec turbo run typecheck --filter=@bb/app --force`: passed
- `pnpm exec turbo run build --filter=@bb/app --force`: passed
- `pnpm exec turbo run lint --filter=@bb/app --force`: 0 errors
- Touched-file `oxfmt --check` and `git diff --check`: passed

Fixes: none

> AGENT GENERATED

---------

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

## What was wrong

Narrow coarse-pointer layouts did not have surface-specific icon sizing.
The File Preview copy action remained a 20px target with a 12px glyph
beside existing 36px/20px header actions, while attachment remove
controls coupled their visible treatment and layout footprint to their
hit target. A global icon-sizing rule would overcorrect dense surfaces
and require unrelated core and plugin controls to opt into or out of
that policy.

## What changed

- Size the File Preview copy and open-in-editor actions to the existing
36px target and 20px glyph treatment on narrow coarse pointers.
- Enlarge the tab close target only for the three core file-preview tab
kinds; browser, terminal, and plugin tabs retain their existing
behavior.
- Give image and file attachment remove controls a compact 28px
coarse-pointer hit target while preserving the 16px visible affordance
and file-chip layout footprint.
- Keep the change app-core only. There are no plugin, Plugin SDK, shared
global sizing, host protocol, CLI, guide, or documentation changes.

## How you verified

- Added focused regression coverage that fails without the
surface-specific classes and file-preview opt-in.
- `pnpm exec turbo run test --filter=@bb/app -- --run
src/components/promptbox/AttachmentPreview.test.tsx
src/components/secondary-panel/FilePreview.test.tsx
src/components/secondary-panel/SecondaryPanelTabStrip.test.ts
src/components/ui/tab-pill.test.tsx` (34 tests passed).
- `pnpm exec turbo run typecheck --filter=@bb/app`.
- `pnpm exec turbo run lint --filter=@bb/app` (0 errors; 173
pre-existing warnings).
- `git diff --check origin/main...HEAD`.
- Used the dev browser at a narrow coarse-pointer viewport and captured
honest before/after screenshots for both affected surfaces. File Preview
moved from a 20px/12px copy control to 36px/20px, matching its
neighboring actions; attachment removal moved from a 16px target to a
28px target while keeping a 16px visible control and unchanged chip
layout.

Fixes: no linked issue.

> AGENT GENERATED
## Human comments

## What was wrong

Conversation outlines still materialized large tool-call and
background-task payloads that cannot affect the outline, then decoded
and projected them synchronously on the server event loop. The bounded
route LRU made repeated reads fast only until the server restarted;
reopening a stable 42,033-event thread rebuilt 9,666 rows and 18.70 MiB
to produce a 698-item, 211,114-byte response.

## What changed

- Select only structurally necessary outline data: successful structural
completions with an earlier start are omitted, completion-only and
failed events remain, and unused tool/background-task fields are removed
in SQL. The query stays inline and uses the existing lifecycle indexes;
no new event index was required.
- Persist one versioned outline per stable (`idle` or `error`) thread.
Exact reuse requires the outline-event revision plus provider id/display
name, thread status, title, and fallback title to match. Active threads
keep the existing bounded in-process LRU and do not write intermediate
revisions.
- Add a generated Drizzle migration for the cascade-owned outline table
and a reproducible route/query/event-loop benchmark.
- There is no worker, worker contract, lifecycle, build entry, or worker
test. There are no retained-output, hydration, pruning, sweep, watcher,
environment, daemon-wire, CLI, SDK, or public API contract changes.
`HOST_DAEMON_PROTOCOL_VERSION` is unchanged.

## How you verified

### Reproduction

I used one SQLite backup on the same Apple Silicon Mac for both
revisions. The copied database contains 1,006 threads, 1,115,607 events,
and 1,724,880,138 event-payload bytes. The measured stable error thread
(`thr_gcuc46ug4j`) contains 42,033 events / 64,886,072 payload bytes;
its outline contains 698 items / 211,114 response bytes.

```bash
benchmark_dir=$(mktemp -d)
sqlite3 "$SOURCE_DB" ".backup '$benchmark_dir/bb.db'"
sqlite3 "$benchmark_dir/bb.db" "SELECT COUNT(*) FROM threads; SELECT COUNT(*), SUM(length(data)) FROM events;"

outline_branch=bb/pr-faster-conversation-outlines-no-worker-thr_3cmx4izjsx
outline_ref=origin/$outline_branch
git fetch origin main "$outline_branch"
git worktree add "$benchmark_dir/base" origin/main
git worktree add "$benchmark_dir/after" "$outline_ref"
cp "$benchmark_dir/after/apps/server/scripts/benchmark-conversation-outline.ts" \
  "$benchmark_dir/base/apps/server/scripts/benchmark-conversation-outline.ts"

(cd "$benchmark_dir/base" && pnpm install --frozen-lockfile && \
  node --conditions=source --import tsx \
    apps/server/scripts/benchmark-conversation-outline.ts \
    "$benchmark_dir/bb.db" thr_gcuc46ug4j 30 100 30 > "$benchmark_dir/before.json")

(cd "$benchmark_dir/after" && pnpm install --frozen-lockfile && \
  node --input-type=module --conditions=source --import tsx -e \
    'import { createConnection, migrate } from "./packages/db/src/index.ts"; const db = createConnection(process.argv[1]); migrate(db); db.$client.close();' \
    "$benchmark_dir/bb.db" && \
  node --conditions=source --import tsx \
    apps/server/scripts/benchmark-conversation-outline.ts \
    "$benchmark_dir/bb.db" thr_gcuc46ug4j 30 100 30 > "$benchmark_dir/after.json")
```

`cold` means a fresh route LRU with no persisted outline but warm
SQLite/OS page caches. `warm` means the same route instance after one
priming read. `reopened` means a fresh route LRU after one stable read.
Route cold/reopened and raw query/build use 30 iterations; warm uses
100.

| Outline route | Main p50 / p95 / max | Branch p50 / p95 / max |
|---|---:|---:|
| Cold miss | 115.598 / 138.960 / 148.725 ms | 74.120 / 83.996 / 90.342
ms |
| Warm in-process hit | 0.603 / 0.849 / 2.334 ms | 0.556 / 0.702 / 1.127
ms |
| Reopened stable thread | 112.349 / 129.917 / 130.892 ms | 1.124 /
1.350 / 1.437 ms |

| Query/build work | Main | Branch |
|---|---:|---:|
| Selected rows | 9,666 | 6,691 (-30.8%) |
| Selected payload | 19,608,409 bytes (18.70 MiB) | 3,967,029 bytes
(3.78 MiB, -79.8%) |
| Raw query p50 / p95 / max | 30.111 / 38.511 / 39.057 ms | 25.998 /
28.289 / 29.888 ms |
| Raw build p50 / p95 / max | 113.850 / 140.609 / 152.361 ms | 74.527 /
90.017 / 92.361 ms |

`EXPLAIN QUERY PLAN` on main used `events_thread_type_sequence_idx`, two
`events_thread_type_item_kind_sequence_idx` arms, and the covering
background-task index, with no table scan. The branch retains those
plans and adds the completion fallback lookup through
`events_item_lifecycle_thread_item_sequence_idx`; it also has no table
scan. The query-plan test protects all of these indexes.

The zero-delay timer around a cold in-process route read measured
116.928 / 139.723 / 149.481 ms p50/p95/max on main and 75.060 / 84.571 /
91.002 ms on the branch. A reopened persisted hit measured 1.758 / 2.549
/ 2.724 ms on the branch. Because there is deliberately no worker, the
first uncached build remains synchronous and can still block the event
loop for roughly 75 ms p50 on this workload. Synchronous handlers
serialize requests, so promise-based request coalescing would add no
benefit; stable durable reuse and the existing LRU avoid the repeat work
instead.

Correctness checks rebuilt every outline in the production copy before
and after: all 1,006 thread hashes were identical (42,031 total items;
aggregate result SHA-256
`4c229e658d255d0337b9a20e2ddc646bf2ef9902a3a33bd376185b78f4dabf6d`).
Main has no separate raw/uncapped conversation-outline mode. Tests cover
exact persisted reuse, metadata invalidation, downward sequence rewinds,
active-thread non-persistence, relevant-event invalidation,
timeline-only reuse, structural completion boundaries,
completion-only/failure preservation, attachments, and the index plan.
The structural-row regression fixture returns `[1, 2, 6, 7]` on main and
`[1, 2, 6]` with this change.

- `pnpm exec turbo run test --filter=@bb/db --force` — 410 passed.
- `pnpm exec turbo run test --filter=@bb/server --force` — 2,075 passed.
- `pnpm exec turbo run typecheck --filter=@bb/db --filter=@bb/server` —
passed.
- `pnpm exec turbo run build --filter=@bb/db --filter=@bb/server` —
passed.

Fixes: N/A — performance follow-up.

> AGENT GENERATED
## Human comments

## What was wrong

Sticky section headers and parent thread rows explicitly used
`bg-sidebar`, while transparent leaf rows inherited the sidebar content
scroll surface. When that surface was rendered over the canvas, the two
paths resolved to visibly different backgrounds.

## What changed

Added `bg-sidebar` to `SidebarContent` so transparent rows and sticky
rows share the same theme-derived surface. Added a focused component
regression test for that styling contract. This is UI-only and
introduces no host daemon protocol or wire changes.

## How you verified

- `pnpm exec turbo run test --filter=@bb/app -- --run
src/components/ui/sidebar.test.tsx` — 22 tests passed.
- `pnpm exec turbo run typecheck lint --filter=@bb/app` — typecheck
passed; lint completed with 0 errors and 173 existing warnings.
- Dev-browser visual and computed-style checks confirmed parent, child,
and standalone rows resolve to the same sidebar surface.

No linked issue.

> AGENT GENERATED
## Human comments

## What was wrong

`apps/desktop/test/bb-process.test.ts` waited for real child-process
readiness by polling a buffered log every 10 ms behind a fixed
one-second wall-clock deadline. The AppImage lifecycle case serially
starts a supervisor, bridge, and grandchild, so package-shard CPU
oversubscription can delay `grandchild=` beyond that deadline even when
the process tree is correct. The flake was therefore caused by
synchronizing on elapsed wall time instead of the observable child
output.

## What changed

The test helper now synchronizes on stdout/stderr `data` events from the
supervised process. It first accepts already-buffered output, closes the
check/subscribe race with a post-subscription buffer check, rejects if
the supervised process exits before the marker, and removes its stdout,
stderr, and exit listeners on every settlement path. Existing test
teardown remains the containment boundary if a test body times out.

No timeout, polling interval, or retry budget was increased. The
lifecycle assertions are preserved: the supervisor owns the process
group, remains alive while its descendant runs, the descendant shares
the supervisor PGID, and stop removes it.

This is test-only. There is no host-daemon wire/protocol change, so
`HOST_DAEMON_PROTOCOL_VERSION` is unchanged. There is no CLI, SDK,
guide, or user-facing configuration change.

## How you verified

- Takeover gate on Intel host `host_nwqfteeqz4`: clean initial HEAD and
remote PR ref `c952224c1c31691bb7d7708e1f5cba9617d42314`; initial
`origin/main` `2c9f82548471c7fb160ab343c7dd8ce74bbecbc1`; initial
merge-base `a2d47f5a6e7779c1d30f3b297582dc342878faec`.
- Read the complete PR discussion, review threads, and checks: no
comments or reviews were present; every original required check passed.
Searched open work and current-main history. PR get-bb#2583 changes the later
AppImage smoke readiness boundary and does not overlap this helper;
current main only removed a now-forbidden comment from this file.
- Independently inspected the original Linux package-shard failure ([job
98638300425](https://github.com/get-bb/bb/actions/runs/33106688728/job/98638300425)):
`bb-process.test.ts` failed only the supervisor case after 1.429 s with
`Timed out waiting for log line: grandchild=`, while the package job was
running 68 Turbo tasks. The same run/head rerun ([job
98639685530](https://github.com/get-bb/bb/actions/runs/33106688728/job/98639685530))
passed the file 8/8 and the target case in 575 ms.
- Fresh Linux red/green on a temporary Ubuntu 26.04 x86_64 VM running on
the same Intel i5 host, limited to two vCPUs. Under 12 concurrent
supervisor → bridge → grandchild topologies, the unchanged one-second
polling wait timed out 24/24 times although every marker arrived (p50
3649.9 ms, p95 3904.3 ms, max 3946.8 ms). Under identical load, the
event-driven wait completed 24/24 (p50 3147.4 ms, p95 3252.0 ms, max
3264.6 ms). Every run also verified supervisor PGID ownership/survival,
descendant PGID, stop/removal, and post-run process cleanup.
- Linux helper cases passed for already-buffered output, early exit
rejection, simulated timed-body teardown containment, and listener
cleanup. The VM and every load process were hard-bounded and removed
after the run.
- Merged current `origin/main` normally, including its final advance to
`fb05e93ff5ad4f58831e804b973f2de790557afa`; final branch merge-base is
that current-main commit and the PR delta remains only
`apps/desktop/test/bb-process.test.ts`.
- `pnpm exec turbo run test --filter=@bb/desktop -- --run
test/bb-process.test.ts` (7 passed, 1 Linux-only skip on macOS; the real
Linux case is covered above).
- `pnpm exec turbo run test --filter=@bb/desktop --force`: the target
file and 37/38 desktop files passed (246 passed, 1 skipped). The only
local failure was current-main's unrelated Electron GUI preload smoke,
which cannot reach its page in this remote macOS session; it reproduces
in isolation and does not touch this PR's file.
- `pnpm exec turbo run typecheck --filter=@bb/desktop --force` (3 Turbo
tasks passed).
- `pnpm exec turbo run build --filter=@bb/desktop --force` (12 Turbo
tasks passed), followed by a latest-main incremental build (12 tasks
passed; app, bb-app, and desktop rebuilt).
- `pnpm exec oxlint apps/desktop/test/bb-process.test.ts`.
- `pnpm exec oxfmt apps/desktop/test/bb-process.test.ts --check`.
- `git diff --check`.

- Final PR CI at `49ff22f3239874dbececaaaf2ade286c30456b42`: 13 checks
passed, 0 failed or cancelled, and 2 expected jobs skipped; the Linux
package test shard and both package-smoke jobs passed.

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

## What was wrong

The Linux AppImage lifecycle smoke treated the server listener's first
successful `/health` response as proof that the owned bb stack was
ready. The listener opens before `pluginService.start()` settles, and
the desktop returns from its initial server probe before `bb-app` has
finished starting and connecting the host daemon. The smoke could
therefore tear down the GUI while server startup work was still in
flight, then misreport a later timed-out health request as `bb became
unhealthy after the GUI mount teardown`.

Fresh Intel x86_64 validation against current main
(`2c9f82548471c7fb160ab343c7dd8ce74bbecbc1`) reproduced that exact
signature on the first real AppImage run. It also showed that the
original PR's daemon-connected condition alone was insufficient on
current main: that version reproduced the exact failure in 3/5 real
AppImage runs without synthetic load. A daemon can connect before plugin
startup settles. The separate pre-runtime `SIGTRAP` fixed by
[get-bb#2657](get-bb#2657) occurs before
`owned-runtime.json` exists and is not this failure.

## What changed

- Merged current main normally, retaining the atomic owned-runtime
publication from [get-bb#2561](get-bb#2561), the
Linux smoke sandbox/stdio fixes from
[get-bb#2657](get-bb#2657), and packaged-smoke exit
settlement from [get-bb#2663](get-bb#2663).
- Before GUI teardown, the lifecycle smoke now requires the isolated
host daemon's `/status` response to report `connected: true` for the
expected server URL.
- It then calls the existing `/api/v1/system/providers` endpoint, whose
handler waits on `providerRegistry.whenRegistrationsSettled()`. This is
the direct signal that asynchronous plugin startup has settled; plugin
startup failure also settles accurately rather than hanging readiness.
- If the GUI exits during this readiness phase, the smoke reuses the
existing bounded stdio-flush diagnostic before reporting its exit. GUI
and owned-runtime death still fail immediately.

The post-teardown contract is unchanged: the owned runtime PID must
remain live, its independent AppImage mount must remain active, and all
three health requests must succeed. No timeout, polling interval, retry
budget, protocol, product runtime, or packaged behavior changed, so no
host-daemon protocol bump is needed.

## How you verified

All reproduction, build, test, typecheck, smoke, and load work ran on
Intel host `host_nwqfteeqz4` in a temporary local x86_64 Ubuntu 24.04 VM
with working FUSE; the VM was deleted after a zero-leak
process/mount/temp-root audit.

- Red, current main: the first real x64 AppImage run reproduced `bb
became unhealthy after the GUI mount teardown`.
- Red, inherited daemon-only candidate: the exact failure reproduced in
3/5 real AppImage runs without synthetic load.
- Green, final direct-settlement candidate: 5/5 consecutive real
AppImage runs passed, then the official Turbo smoke passed after the
exact-final AppImage rebuild.
- Bounded load: 3/3 additional real AppImage runs passed while two of
four guest CPUs ran at 75%; every load process was stopped after its run
and every cleanup audit was empty.
- `pnpm exec turbo run desktop:build:linux --filter=@bb/desktop
--cache-dir=.turbo/cache --output-logs=new-only`: 13 tasks passed on the
final commit.
- `xvfb-run -a pnpm exec turbo run smoke:appimage-lifecycle
--filter=@bb/desktop --force --cache-dir=.turbo/cache
--output-logs=new-only`: passed on the final AppImage.
- `pnpm exec turbo run test --filter=@bb/desktop --force`: 38 files, 248
tests passed on Linux.
- `pnpm exec turbo run typecheck --filter=@bb/desktop`: 3 tasks passed.
- `pnpm exec turbo run build --filter=@bb/desktop`: 12 tasks passed.
- `node --check
apps/desktop/scripts/smoke-linux-appimage-lifecycle.mjs`, focused `oxfmt
--check`, and `git diff --check`: passed.

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

## What was wrong

`buildPluginApp` always requested a complete esbuild metafile and
canonicalized every bundled input with `realpath()`, even when no direct
dependency declared `bb.pluginTailwindContent`. The fresh CLI scaffold
in [main CI run
33101856455](https://github.com/get-bb/bb/actions/runs/33101856455/job/98621342155)
bundles 5,154 inputs but none of its four linked runtime dependencies
opts into dependency Tailwind scanning. Under package-shard contention,
that unused manifest and canonicalization pass pushed the end-to-end
scaffold test to 5,424 ms and beyond Vitest's unchanged 5,000 ms
default.

## What changed

Resolve dependency Tailwind sources before the frontend bundle, request
esbuild's metafile only when at least one direct dependency opts into
scanning, and canonicalize bundled inputs only on that path. The
scaffold regression test delegates to real esbuild through a guard that
rejects an unnecessary metafile while retaining real scaffold
generation, dependency links, esbuild and Tailwind transforms, artifact
assertions, and dynamic bundle import. The existing symlinked
dependency-content test continues to exercise metafile-backed opt-in
scanning.

The branch includes current `origin/main` at
`fb05e93ff5ad4f58831e804b973f2de790557afa`. The only fix-specific
current-main adaptation was removing the new explanatory TypeScript
comments required by the repository's no-comments policy. There are no
timeout, retry, wire, CLI behavior, guide, or documentation changes.

## How you verified

- Fresh Intel-only takeover on `host_nwqfteeqz4` (`x86_64`, Intel Core
i5).
- Original failure log: the exact scaffold case took 5,424 ms and timed
out at the unchanged 5,000 ms default while 495 sibling CLI tests
passed.
- Final-current-main red boundary: restoring `metafile: true` made the
exact scaffold test fail in 2,747 ms with `unexpected esbuild metafile:
5154 inputs`.
- Final-current-main green boundary: the conditional implementation
passed the same exact test in 2,316 ms without requesting a metafile.
- Opt-in preservation: the symlinked workspace-dependency Tailwind scan
passed in 212 ms, retaining the bundled `.tracking-widest` utility while
excluding unbundled and negated content.
- Earlier bounded contention evidence: 16/16 timed out before at
5.418–6.159 seconds; 16/16 passed after at 2.007–2.839 seconds. The
takeover used the deterministic current-main red/green instead of
repeating that stress fan-out.
- `pnpm exec vitest run src/__tests__/plugin-build.test.ts
--reporter=verbose` from `apps/cli`: 10/10 passed.
- `pnpm exec vitest run src/build-plugin-app.test.ts --reporter=verbose`
from `packages/plugin-build`: 11/11 passed.
- Final-main `TMPDIR=/private/tmp pnpm exec turbo run test
--filter=@bb/cli --filter=@bb/plugin-build --force --env-mode=loose
--concurrency=2`: 8/8 tasks; CLI 503/503; plugin-build 134 passed, 1
skipped.
- Final-main `pnpm exec turbo run typecheck --filter=@bb/cli
--filter=@bb/plugin-build --concurrency=2 --force`: 5/5 tasks, zero
cache hits.
- Final-main `pnpm exec turbo run build --filter=@bb/cli
--filter=@bb/plugin-build --concurrency=2 --force`: 4/4 tasks, zero
cache hits.
- `pnpm exec oxfmt apps/cli/src/__tests__/plugin-build.test.ts
packages/plugin-build/src/build-plugin-app.ts --check`.
- `pnpm exec oxlint apps/cli/src/__tests__/plugin-build.test.ts
packages/plugin-build/src/build-plugin-app.ts`.
- `git diff --check`.

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

## What was wrong

PR get-bb#2773 correctly retires a provider bridge after its final thread
ends, but the shared process manager did not record that retirement
while it was in progress. A same-key `ensureProvider` could therefore
reuse the bridge being terminated; the new thread would reject
immediately or briefly start and then lose its provider when the old
bridge exited.

Review exposed two full-shutdown ordering cases: a retirement-blocked
start could spawn after the shutdown sweep, and waiting for that
replacement to finish initialization could exceed the daemon's 15-second
exit grace because provider RPC initialization waits up to 30 seconds.

## What changed

The provider process manager now memoizes same-key retirement promises.
New starts wait for retirement before selecting or spawning a bridge,
and concurrent shutdown callers share the active retirement. Full
runtime shutdown becomes terminal before any lifecycle wait:
retirement-blocked or later ensures fulfill without spawning, while
already-initializing starts treat shutdown as superseding their internal
RPC wait. Shutdown then terminates every process already visible to its
sweep.

Final-thread retirement, bridge reuse while another thread remains, and
later cold resume remain unchanged. The change is internal to
`@bb/agent-runtime`; no daemon wire contract, protocol version, public
API, CLI, or configuration changed.

## How you verified

- Added a regression that overlaps same-key provider shutdown and
ensure. It failed before the production change with `Provider "fake" is
not running` and passes afterward with a distinct surviving replacement
process.
- Added a regression that overlaps retirement, a same-key ensure with a
three-second delayed initialization response, and full shutdown. It was
red because shutdown took the full initialization delay; it is green
within the one-second bound, all calls fulfill, and no provider remains.
A post-shutdown ensure also fulfills without recreating a provider.
- `pnpm exec turbo run test typecheck --filter=@bb/agent-runtime
--force` — 22 files / 318 tests passed; typecheck passed.
- GitHub CI run `33459198901` — all non-skipped jobs passed, including
Linux AppImage lifecycle smoke.
- `git diff --check origin/main...HEAD` — passed.
- Rebased and reran validation on `origin/main` at
`fb05e93ff5ad4f58831e804b973f2de790557afa`.

Completes the lifecycle coordination missed by get-bb#2773.

Tagging @slopcop for re-review of head
`0ab3801d24d4d8c04e9cc1bff808357cb0f07d70`.

> AGENT GENERATED
## Human comments

## What was wrong

The compact New Thread page placed a capped three-row Recent list below
the composer in one page scroller. The composer moved away from the
thumb, the list looked complete even when more work existed, and rows
omitted provider, hierarchy, workspace, branch, and activity context.

## What changed

- Anchor the production composer at the bottom while realistic recents
scroll behind it.
- Leave the recents viewport on native overflow behavior; the
imperceptible product-managed scrollbar timer and state were removed.
- Keep the `Recent` label sticky and leave a partial row visible to
signal more content.
- Render every active thread with production provider marks, hierarchy,
status, workspace, branch, and attention-time metadata.
- Vertically center each provider tile against its thread title and
metadata block.
- Preserve highlighted descendants by expanding collapsed ancestors.
- Render the real production compact-home, recents, prompt box, and
overflow-fade components in Ladle with populated fixtures.

No compact drawer, panel, route-feedback, or server-cache behavior
changes in this layer.

## How you verified

- Original geometry and alignment evidence used Chrome for Testing
151.0.7922.71 at 393×852 CSS px, DPR 2, against exact base
`f4bbc2fe81a9b7639ff9a7396e172bddd89109e4` and feature head
`df613809d7080cbc145913355958478de5733245`. The current exact head is
`0eb3bef25c3589f2b3f40cdcb80b20cff131512a`.
- Measured the production compact-home subtree itself: 393×852;
production composer subtree: y=672, h=180; scroll viewport: y=318,
h=534; 23 production recent rows rendered.
- Measured provider-tile center alignment on representative parent,
child, and ordinary rows: the prior head placed every tile 12px above
its title/metadata center; the final head measures 0px center delta
before and after client navigation.
- Hard reload and thread navigation back to the compact home preserved
the alignment; no console errors.
- Scrolled that viewport to 360px and measured the `Recent` heading at
y=318, exactly pinned to the viewport's top edge.
- On the exact current head, the viewport has no product scroll
listener, idle timeout, `data-scrollbar-scrolling` state, or
`transient-scrollbar` class; the browser owns scrollbar presentation.
- Verified collapsed parents whose only activity came from a child: the
glyph and accessible label report the same aggregated state. Child-only
pending, plan, runtime, and unsubmitted-draft states have regression
coverage.
- On the exact current head, a realistic hidden child draft produced
`Thread has unsubmitted draft` on both the parent link and production
status glyph; the child stayed collapsed and the recents subtree
contained zero nested interactive pairs.
- The hierarchy toggle is now a sibling of the thread link; the
production recents subtree contained zero nested `a button` or `button
a` pairs.
- Compact-home geometry now uses `useLayoutEffect`, not `useEffect`.
Across 143 requestAnimationFrame samples from first real content, the
scroll viewport stayed at y=310 with no post-paint geometry jump.
- Remote CI passed all required tests, checks, package smokes, and
version gates on the exact final head. No local CI-equivalent command
was run.

<table>
  <tr>
    <th>Before — <code>main</code></th>
    <th>After — compact production home</th>
  </tr>
  <tr>
<td><img
src="https://gist.githubusercontent.com/brsbl/d8363c420a5e5c2167498079a27162f0/raw/5904f796578a5112b80ec007db7176bfd8fb83d4/layer1-before.svg"
width="393" alt="Before: main compact home" /></td>
<td><img
src="https://gist.githubusercontent.com/brsbl/d8363c420a5e5c2167498079a27162f0/raw/234f625398536ff1b4dc4af74c1027698d99cfa0/pr2758-after-scrollbar-removal.svg"
width="393" alt="Exact final head: compact production home after
removing imperceptible scrollbar bookkeeping" /></td>
  </tr>
  <tr>
    <th colspan="2">After — sticky header while scrolled</th>
  </tr>
  <tr>
<td colspan="2" align="center"><img
src="https://gist.githubusercontent.com/brsbl/d8363c420a5e5c2167498079a27162f0/raw/f3574205a70ba781f588d9ac17fc9fd703a34610/pr2758-sticky.svg"
width="393" alt="After: Recent header remains sticky after scrolling"
/></td>
  </tr>
</table>

### Child-only draft aggregation

| Feature head `62c4c6d40` — collapsed parent reflects its hidden child
draft |
| --- |
| <img
src="https://gist.githubusercontent.com/brsbl/d8363c420a5e5c2167498079a27162f0/raw/9add1f52c7d662561b0f93eba3f8733d0574835a/pr2758-child-draft-final-head.svg"
width="393" alt="Feature head: production compact recents show a draft
glyph on a collapsed parent whose hidden child owns the draft" /> |

BB-Thread-ID: thr_wftu7bh9ez

> AGENT GENERATED
## Human comments

## What was wrong

Deferred route navigation could appear inert, and a cold thread route
could begin without metadata even though the persisted sidebar bootstrap
already contained a usable thread placeholder.

## What changed

- Show route-progress feedback only after a 120ms delay, then keep it
visible for at least 320ms so quick routes do not flash and slow routes
do not feel ignored.
- Seed cold thread queries from the in-memory or persisted sidebar
bootstrap before the full response arrives.
- Keep indicator colors derived from the active theme anchors.

No mobile-home layout, compact shelf, provider-discovery, or right-panel
presentation changes in this layer.

## How you verified

- The original behavior capture used Chrome for Testing 151.0.7922.71 at
393×852 CSS px, DPR 2, against exact feature revisions
`e1b749660fb19ecc402799cfbbaa1cd887ac0188` and
`ac97a215cb1db1640f11a2de60e9bf2b97d3a70a`. The direct layer remains
unchanged at current exact base
`0eb3bef25c3589f2b3f40cdcb80b20cff131512a` and exact head
`1b8c66d38227299d70e3033bac8a2f2af35c294d`.
- Held the real thread/environment requests pending, clicked the same
production recent-thread anchor, and captured at 220ms. The base route
had only an anonymous skeleton; the head had already reconstructed the
real `Wire up automations CLI` header and production follow-up composer
from persisted sidebar data.
- Assertions and captures targeted the routed header/composer subtrees,
not `document.body.innerText`.
- Remote CI passed the timing tests, typecheck, lint, package smokes,
and version gates on the exact final head. No local CI-equivalent
command was run.

| Before — no cold-route identity | After — immediate production
placeholder |
| --- | --- |
| <img
src="https://gist.githubusercontent.com/brsbl/d8363c420a5e5c2167498079a27162f0/raw/d2ad95502b764fe32cef1e834a34838db73036f6/pr2-before-nav.svg"
width="393" alt="Before: anonymous cold route skeleton" /> | <img
src="https://gist.githubusercontent.com/brsbl/d8363c420a5e5c2167498079a27162f0/raw/90c46b9e1fad5c7546b259c0033a1f971fd5e6d3/pr2-after-nav.svg"
width="393" alt="After: real title and production composer while the
route request remains pending" /> |

BB-Thread-ID: thr_wftu7bh9ez

> AGENT GENERATED
## Human comments

## What was wrong

Listing providers repeatedly spawned provider-bridge health probes that
can take seconds per host, and provider logos used mutable URLs even
though the bytes were already hashed. The app also discarded a persisted
provider list that could avoid generic-icon churn during startup.

## What changed

- Cache provider installed-state per host and provider for five minutes,
invalidated by provider registration changes or failed probes.
- Reuse the persisted provider list while the live query settles.
- Add the icon content hash to provider registrations and immutable logo
URLs.
- Update plugin-provider registration and server tests for the explicit
hash contract.

No route-feedback, compact-home layout, shelf, or full-page panel
behavior changes in this layer.

## How you verified

- The original rendering capture used Chrome for Testing 151.0.7922.71
at 393×852 CSS px, DPR 2, against exact feature revisions
`ac97a215cb1db1640f11a2de60e9bf2b97d3a70a` and
`f65de938732b45cf993ed9fc721264229e0d96b3`. The scoped cache-ownership
fix was verified at exact base
`f906c20416a6ca5e508901c916597bfeaa114fdb` and then-head
`4b8a22b0aaada44f118eac54af996db0844d267c`; the final slimming-only
layer is exact base `1b8c66d38227299d70e3033bac8a2f2af35c294d` and exact
head `38c122fc1b74ae4a0afe28596f86e6da9a73aa48`.
- Primed the same persisted provider cache, held only the live
provider-list request pending, let immutable logo requests proceed, then
captured after the production recents subtree had rendered. The base
left visible provider cells blank; the head reconstructed the real
provider marks immediately from persisted data.
- Captures waited for the production recents subtree and its rows; no
page-body text assertion was used.
- Targeted server coverage holds one failing 502/504 health probe across
concurrent callers and verifies that both callers take the same fallback
path.
- Provider health ownership now uses explicit `{ hostId, providerId }`
keys. A failed probe invalidates only that host-provider entry, provider
registration can invalidate one provider across hosts, and settings
changes clear the complete cache. Regression coverage keeps a healthy
host cached after a different host fails.
- Targeted app coverage verifies capability-filtered placeholders omit
persisted providers that cannot satisfy maintenance.
- Remote CI passed server/app tests, typecheck, lint, package smokes,
and version gates on the exact final head. No local CI-equivalent
command was run.

| Before — live list pending | After — persisted production marks |
| --- | --- |
| <img
src="https://gist.githubusercontent.com/brsbl/d8363c420a5e5c2167498079a27162f0/raw/47dbd0714d5a290c33d608248379592cfdd9510f/pr3-before-provider.svg"
width="393" alt="Before: provider cells remain blank while provider
discovery is pending" /> | <img
src="https://gist.githubusercontent.com/brsbl/d8363c420a5e5c2167498079a27162f0/raw/7bd06c492f30d1e67a9fd7d9b78f950c12b1cfa7/pr3-after-provider.svg"
width="393" alt="After: real provider marks render from the persisted
provider list" /> |

BB-Thread-ID: thr_wftu7bh9ez

> AGENT GENERATED
## Human comments

## What was wrong

On compact viewports, the left navigation covered and dimmed the page,
while the right panel rose as a dimmed bottom drawer. Both overlays hid
the surface users were navigating from and made the product feel like
disconnected modal sheets.

## What changed

- Keep the left navigation mounted beneath the page and translate the
live page right to reveal a 76vw shelf without a scrim.
- Replace the right-panel bottom drawer with a 76vw right shelf that
translates the live page left.
- Keep the translated center pane square and let each shelf own one
stable divider.
- Preserve swipe, dismissal, deferred realization, desktop behavior, and
compact panel state.
- Use the right-panel glyph consistently now that compact presentation
is no longer a bottom drawer.
- Suppress the translated page-header actions and the fixed left-sidebar
trigger while the right shelf is open, leaving the shelf-owned collapse
control as the only visible sidebar control.
- Add real many-tab shelf coverage in the production
`ThreadSecondaryPanel` story.

No full-page tool-tab behavior is introduced in this layer; every
right-panel tab still uses the shelf.

## How you verified

- Original shelf-geometry evidence used Chrome for Testing 151.0.7922.71
at 393×852 CSS px against exact base
`f65de938732b45cf993ed9fc721264229e0d96b3` and then-head
`991070eab2c60f00fdb242403b38890dffdc852d`.
- Current compact-shelf evidence uses the same populated production Info
route, realistic environment-backed fixture, open-shelf interaction
state, and 393×852 viewport at capture base
`4b8a22b0aaada44f118eac54af996db0844d267c` and capture head
`108402ee09e4a70b2df61542e7054ce73fae924a`. The final square-pane layer
is exact base `38c122fc1b74ae4a0afe28596f86e6da9a73aa48` and exact head
`8bd201435a25ce4feb3579a5fe3b9fa84d22e4b6`.
- Left: base drawer measured x=0, w=320 with an 80%-opaque backdrop and
an unshifted app; head shelf measured w=298.67 with the live app shifted
to x=298.68 and a transparent backdrop.
- Right: base bottom drawer measured x=0, y=68.17, w=393, h=783.83; head
shelf measured x=94.33, y=0, w=298.67, h=852 with the live app shifted
to x=-298.68.
- At the exact final head, both translated center panes measured 0px on
all four corners, no shadow, and visible overflow. The left shelf owns a
1px right divider; the right Info shelf owns a 1px left divider.
- The left capture found 23 real thread rows in the production sidebar
subtree; the right capture found the production `Start terminal` action.
Both captured subtrees had zero loading skeletons.
- Exercised first paint through settled content, real client navigation,
a hard reload, and a complete shelf transition with no browser errors.
- Focus entered the dialog when opened; Tab and Shift+Tab stayed
contained; a nested production tooltip consumed the first Escape; the
second Escape closed the shelf and restored the exact right-panel
trigger.
- The exact-head Info shelf measured 296.4×844 at x=93.6 in the 390×844
evidence viewport. Translated header actions and the fixed left trigger
were absent while the panel-owned collapse control remained visible.
- At the exact current head, the open Info shelf exposed one on-screen
sidebar control: `Hide right panel` in
`thread-secondary-panel-top-chrome`. The fixed left trigger was absent
from the DOM and focus order; closing the panel restored it, and opening
the left shelf kept the right shelf closed.
- Remote CI passed tests, typecheck, lint, package smokes, and version
gates on the exact final head. No local CI-equivalent command was run.

### Left navigation: modal drawer → persistent shelf

| Before | After |
| --- | --- |
| <img
src="https://gist.githubusercontent.com/brsbl/d8363c420a5e5c2167498079a27162f0/raw/a20e0040a9e60ea8b22349c40335ca1a9adf5c6b/pr4-before-left.svg"
width="393" alt="Before: left modal drawer covers and dims the app" /> |
<img
src="https://gist.githubusercontent.com/brsbl/d8363c420a5e5c2167498079a27162f0/raw/ce6a6c5de38d20aa33013c514085f1ca13bb20bc/pr4-after-left.svg"
width="393" alt="After: left persistent shelf reveals a rounded, clipped
center pane without a scrim" /> |

### Right panel: bottom drawer → persistent right shelf

| Before | After |
| --- | --- |
| <img
src="https://gist.githubusercontent.com/brsbl/d8363c420a5e5c2167498079a27162f0/raw/942305656055bd9b3ae10a76cd5237767ca4e7dd/pr4-before-right.svg"
width="393" alt="Before: right panel is a full-width bottom drawer" /> |
<img
src="https://gist.githubusercontent.com/brsbl/d8363c420a5e5c2167498079a27162f0/raw/6d9cb9a739fbef47d6d9167d4db8af2b09403969/pr4-after-right.svg"
width="393" alt="After: right shelf reveals a rounded, clipped center
pane" /> |

### Square shelf edges before the toggle-ownership fix

The earlier before/after captures document the drawer-to-shelf change at
their labeled capture heads. The square-pane captures below come from
the preceding head; the final toggle-only update leaves this geometry
unchanged.

| Left shelf — square pane | Right Info shelf — square pane |
| --- | --- |
| <img
src="https://gist.githubusercontent.com/brsbl/d8363c420a5e5c2167498079a27162f0/raw/45ddcf245aaf3dd2f441c3b9a21d38cb96f5dc6b/left-square-pane.svg"
width="195" alt=\"Preceding PR head: production left shelf with a square
center pane and one stable divider\" /> | <img
src="https://gist.githubusercontent.com/brsbl/d8363c420a5e5c2167498079a27162f0/raw/520f7ffeb3c06c2a6c41cbb3e9dcadf7327253b6/right-square-pane.svg"
width="195" alt=\"Preceding PR head: production Info shelf with a square
center pane and one stable divider\" /> |

### One compact sidebar control at a time

The preceding get-bb#2761 head correctly suppressed translated workspace
actions but still showed the fixed left trigger beside the right shelf
owned control. At the exact final head, the fixed trigger leaves the DOM
and focus order while the right shelf is active. Closing the right shelf
restores it; opening the left shelf keeps the right shelf closed and
leaves only the left trigger on screen.

| Before — two competing controls | After — right shelf owns one control
|
| --- | --- |
| <img
src="https://gist.githubusercontent.com/brsbl/d8363c420a5e5c2167498079a27162f0/raw/520f7ffeb3c06c2a6c41cbb3e9dcadf7327253b6/right-square-pane.svg"
width="195" alt="Preceding PR head: the fixed left trigger competes with
the right shelf collapse control" /> | <img
src="https://gist.githubusercontent.com/brsbl/d8363c420a5e5c2167498079a27162f0/raw/37ffb2fde812f68e2ec1b90d1f514b7c06460660/right-panel-single-toggle.svg"
width="195" alt="Exact final PR head: the production Info shelf exposes
only its own collapse control" /> |

BB-Thread-ID: thr_wftu7bh9ez

> AGENT GENERATED
## Human comments

## What was wrong

After the right panel became a shelf, every tab remained constrained to
298.67px. Selecting New Tab could update the tab strip while leaving
Info content visible, so tool surfaces did not receive a usable
phone-width canvas. A newly created terminal could also lose its
persisted active selection while the terminal list caught up, causing
the settled tab to fall back to the shelf.

## What changed

- Let the active tab determine the compact presentation; the control
that opened the panel does not independently choose shelf versus
full-page.
- Keep Info in the compact right shelf.
- Expand every non-Info tab, including New Tab and tabs opened from it,
from the right to a full-width surface above fixed app chrome.
- Render the available production tab and preserve its full-page
presentation while persisted active selection catches up.
- Select and render New Tab's production content after its loading state
settles.
- Close the right panel when the left navigation opens so compact
surfaces cannot compete for the page.
- Replace substitute story markup and wrappers with the production
shelf, panel, file preview, compact home, sidebar provider, and sidebar
inset.

### Compact presentation rules

The opener only selects or restores a tab. The active tab kind
determines the panel geometry, and switching tabs changes that geometry
immediately.

| Trigger or active destination | Compact result |
| --- | --- |
| Open or select Info (`thread-info`) | Right shelf at 76vw. An
unresolved or empty initial tab state also uses this safe fallback. |
| Open or select New Tab | Full-page at 100vw. |
| Open or select Terminal, Diff, Browser, file preview, plugin page, or
any other non-Info tab | Full-page at 100vw. |
| Tap the right-panel toggle on an existing thread | Reopen the current
tab: Info returns as a shelf; a non-Info tab returns full-page. If no
tab has resolved, Info is the default shelf destination. |
| Tap the right-panel toggle on Home / new-thread compose | Create and
select New Tab, so the panel opens full-page immediately. |
| Open the left navigation or close the right panel | Hide the right
surface; this does not create a competing shelf. |

No mobile-home, navigation-feedback, provider-cache, or base shelf
behavior changes in this layer.

## How you verified

- Chrome for Testing 151.0.7922.71 at 393×852 CSS px, DPR 2, against
capture base `108402ee09e4a70b2df61542e7054ce73fae924a` and capture head
`fb9902ab803b381574252195d4980b937d9405c4`. The final layer is exact
base `8bd201435a25ce4feb3579a5fe3b9fa84d22e4b6` and exact head
`0f50516ccab03168a1730e62d05c65b512405571`; it inherits the square-pane
rollback without changing this layer’s full-page tab policy.
- From the same production New Tab surface, chose `Start terminal` and
waited for the real `zsh` tab and xterm subtree to settle with zero
panel skeletons.
- Base: terminal remained in the 298.67px shelf at x=94.33 and the app
stopped at x=-298.68. Head: the same terminal became a 393px full-page
surface at x=0 and shifted the app completely to x=-393.
- Reproduced the active-selection race after terminal creation, then
verified the final head keeps the settled production terminal full-page
with no browser errors.
- Rechecked the capture head in the branch web app on
`/projects/proj_m65y3vujaj/threads/thr_done`, using one realistic
environment-backed fixture, the same persisted `AppPageHeader.tsx` tab,
the same populated Recent item, and the same viewport for both
current-head captures. Only the active tab changed.
- Waited for production content before capture: Info rendered
environment, branch, merge base, PR, commits, and changed files; New Tab
rendered file search, production actions, and a Recent file opened
through the production panel flow.
- Measured each production right-panel dialog subtree in the populated
capture: Info settled at x=94.33, 298.67×852; New Tab settled at x=0,
393×852. The exact final-head control capture measured Info at 296.4×844
and New Tab at 390×844.
- In the Info shelf, translated center-pane actions and the fixed left
trigger were absent while the panel-owned `Hide right panel` control
remained visible.
- At the exact final head, Info exposed one on-screen sidebar control,
`Hide right panel`, with the fixed left trigger absent from the DOM and
focus order. New Tab preserved the same one-control ownership while
full-page. Closing either right surface restored the left trigger;
opening the left shelf left only that trigger on screen and kept the
right surface closed.
- Focus entered the panel, Tab and Shift+Tab stayed contained, the first
Escape dismissed the nested tooltip without closing the tool page, and
the second restored the exact right-panel trigger. The exact final-head
run found zero focusable fixed-left triggers while either right surface
was active.
- The exact final-head Chrome pass had no runtime exceptions. Its only
failed response was the deterministic no-environment fixture returning
409 for `thread-storage/files`; the rendered production empty states and
panel interactions remained valid. Remote CI passed tests, typecheck,
lint, both package smokes, and version gates on the exact final head. No
local CI-equivalent command was run.

### Populated feature evidence at the preceding head

These captures retain the realistic populated fixture from the preceding
feature head. They prove the Info shelf exception and the New Tab
full-page policy with production content.

| Info — 76vw shelf | New Tab — full page |
| --- | --- |
| <img
src="https://gist.githubusercontent.com/brsbl/d8363c420a5e5c2167498079a27162f0/raw/b0fd7ce7a815d56e686651b6b7b4d4da2d2cef97/pr2762-info-current.svg"
width="260" alt="Preceding feature head: populated production Info tab
renders in the compact right shelf" /> | <img
src="https://gist.githubusercontent.com/brsbl/d8363c420a5e5c2167498079a27162f0/raw/b014aec0871e11a932a061dbd65a10014cc2d631/pr2762-new-tab-current.svg"
width="260" alt="Preceding feature head: production New Tab renders file
search, actions, and realistic Recent files full page" /> |

### Exact final head: one owning control

Chrome for Testing 151.0.7922.71 captured both states in one session on
`/projects/proj_m65y3vujaj/threads/thr_done` at 390×844 CSS px. The
route, fixture, viewport, and open right-panel interaction stayed fixed;
only the active Info versus New Tab destination changed. Both are
production panel surfaces, and neither capture contains a loading
skeleton.

| Info — shelf-owned collapse | New Tab — full-page collapse |
| --- | --- |
| <img
src="https://gist.githubusercontent.com/brsbl/d8363c420a5e5c2167498079a27162f0/raw/37ffb2fde812f68e2ec1b90d1f514b7c06460660/pr2762-info-single-toggle.svg"
width="195" alt="Exact final PR head: production Info shelf shows only
its own collapse control" /> | <img
src="https://gist.githubusercontent.com/brsbl/d8363c420a5e5c2167498079a27162f0/raw/4745fc94cdd9099a3a204446764818a5284dceab/pr2762-new-tab-single-toggle.svg"
width="195" alt="Exact final PR head: production New Tab full page shows
only its own collapse control" /> |

### Original base/head width regression

This historical pair remains the original feature regression; the
current-head pair above is the final-branch evidence.

| Before — tool tab constrained to shelf | After — same tool tab full
page |
| --- | --- |
| <img
src="https://gist.githubusercontent.com/brsbl/d8363c420a5e5c2167498079a27162f0/raw/e0665b43f4153ad50636b954ab02a8ff69137ef4/pr5-before-newtab.svg"
width="393" alt="Before: production zsh tab remains constrained to the
rounded right shelf" /> | <img
src="https://gist.githubusercontent.com/brsbl/d8363c420a5e5c2167498079a27162f0/raw/ad6730e8e37be548bc2f94558169d57e32724a23/pr5-after-newtab.svg"
width="393" alt="After: production zsh tab remains selected and expands
to a full-page compact surface" /> |

BB-Thread-ID: thr_wftu7bh9ez

> AGENT GENERATED
## Human comments

## What was wrong

File-preview tabs enlarge their close target to 36px on narrow
coarse-pointer screens, but the selectable tab trigger retained its 6px
desktop left inset. Centering the close glyph in that wider target moved
it over the filename: the glyph ended at x=122 while the label began at
x=120, producing a 2px overlap.

## What changed

- Add a coarse-pointer left inset to non-icon tab triggers only when
they have a close action with the enlarged touch target.
- Preserve the 36×36px close target while restoring 6px of visible space
before the filename.
- Add focused regression coverage for the conditional file-tab spacing.
- No wire, CLI, guide, or documentation behavior changed.

## How you verified

- Dev Browser at a 480×700 viewport with coarse-pointer emulation: the
close-glyph-to-label gap changed from -2px to 6px while the close target
remained 36×36px.
- The focused regression test failed before the fix and passed
afterward.
- Post-rebase: `pnpm exec turbo run test --filter=@bb/app --force --
--run src/components/ui/tab-pill.test.tsx` (5 tests passed).
- Post-rebase: `pnpm exec turbo run typecheck --filter=@bb/app --force`.
- Full app verification before the no-op rebase: 456 test files passed,
3,643 tests passed, 4 skipped.

BB-Thread-ID: thr_q2ws2irggv

> AGENT GENERATED
…n checkpoint (get-bb#2779)

## Human comments

## What was wrong

bb had no way to put anything between "a user asked for work" and "bb
runs it": no scheduled sends, no concurrency control, and four parallel
single-purpose parking mechanisms (`dispatch_holds` never existed on
main, but `deferred_thread_messages`, the reprovision-parked turn, and
the drain-only queue each solved one blocked-message case with its own
machinery). Rate-limit retries were faked by `plugins/provider-retry`
re-reading the whole event log (~450 lines) and injecting a synthetic
"Please continue." message, so the provider never saw the same
conversation twice. There was no plugin surface that could defer,
refuse, or reschedule a dispatch.

## What changed

**The model: a send is always a dispatch attempt.** If nothing blocks
it, it dispatches directly — the happy path is byte-for-byte unchanged
and allocates nothing (test-guarded: a stock install registers zero
hooks). If something blocks it, the message queues as a row carrying a
typed `waitingOn` (`time` | `thread-busy` | `provisioning` |
`interaction` | `host-offline` | `plugin`), and core re-attempts when
conditions change (due sweep, thread-idle drain, workspace-ready,
interaction-settled, plugin `recheck`, Send-now, orphan sweep).
`deferred_thread_messages` is deleted; its cases are queue rows. Threads
gain a canonical `pending` status (created, first message never
dispatched) — scheduled spawns do zero provisioning until due.

**Plugin API** (all `experimental_`):
`bb.experimental_hooks.on("message.dispatch", h)` — one checkpoint, run
identically for fresh sends, drained rows, and steers, returning
`proceed | wait(reason, sendAt?) | reject(message)`; fail-closed (10s
box, plugin named). `bb.experimental_hooks.recheck(hook)` asks core to
re-pose the question. New `bb.events`: `message.queued`,
`message.dispatched`, `turn.failed` (ids + failure facts). Public API:
`sendAt` on send/create, `threads.retry({turnRequestId?, sendAt?})`
(re-dispatch decided by the provider's acceptance record: an input the
provider never accepted is re-sent verbatim, an accepted turn is
continued with an agent-only nudge; the original execution tuple is
replayed and no user message is duplicated), `threads.listRunning()`
(exact for cold admissions inside a hook — a `pending → starting` flip
commits before the evaluation lock releases; a warm follow-up's `idle →
active` flip lands in the send transaction just after it, so a burst of
follow-ups to distinct idle threads can briefly under-report),
`threads.count()`.

**Plugins**: new `scheduled-send` ("Send later…" via the composer's own
submit pipeline + dialog; **off by default**) and `concurrency-limit`
(global + per-host caps as one pure hook over `listRunning`; **off by
default**); `provider-retry` rewritten to a `turn.failed` listener
calling `threads.retry` — `recovery.ts` and the synthetic continue are
deleted (plugin: 1,427 → ~300 lines).

**UI/CLI**: queued rows render their wait (kind icon, reason, live
countdown, retry attempt, failure reason) with Send-now/Edit/Cancel;
sidebar shows a clock for threads with waiting work and the error glyph
when a queued dispatch failed; `bb thread queue` gains wait columns; new
`--send-at` on spawn/tell, `bb thread count`, `bb thread retry`; guide +
skills updated alongside.

**Wire**: `HOST_DAEMON_PROTOCOL_VERSION` → 176 (thread-event schema
changes ride the daemon event batch). One drizzle migration (`0110`) on
main's chain: queue wait columns + partial indexes,
`threads.pending_start_context`, and the `deferred_thread_messages`
drop. Plugin SDK 0.4.31 with `docs/api_to_audit.md` entries for every
new `experimental_` member.

## How you verified

Repo-wide `turbo run typecheck` (82/82) and `turbo run lint
--filter=@bb/app` (0 errors) at every layer; final battery across 15
packages (~7,900 tests: server 2094, app 453 files, cli, db incl.
migration-replay + `EXPLAIN QUERY PLAN` index pins, domain, contracts,
plugin-sdk, thread-view, client-core, three plugins, templates,
plugin-api-map api-sync). Exit-criteria tests: `--send-at` survives
restarts; exactly-once dispatch under racing drains; orphaned plugin
waits clear; retries re-issue the original turn with no duplicated
message; hook exactness (attempt N+1's `listRunning` sees attempt N's
admission — load-bearing test fails if the commit moves outside the
lock); zero-hook installs byte-identical. Integration suite: failures on
the loaded build machine were verified as environment flakes — the same
suite on a clean origin/main worktree failed 4× more files under
identical load; all failing files pass in isolation.


> AGENT GENERATED

---------

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

## What was wrong

Top-level sidebar sections rendered collapsed child activity as an
absolutely positioned sibling of the section actions. The indicator only
switched into normal flow when both the compact breakpoint and a coarse
pointer matched, so a mobile-width drawer with a fine or remote pointer
kept the desktop overlay and allowed the activity glyph to collide with
the trailing controls.

## What changed

The collapsed activity indicator now lives inside a stable, relatively
positioned trailing-controls container. At compact widths it becomes an
in-flow status cell regardless of pointer type; at desktop widths it
remains pinned to the trailing edge and keeps the existing hover fade
behavior. A regression test covers a collapsed working section with
three visible actions. This is UI-only: there are no wire, CLI, guide,
or documentation changes.

## How you verified

- `pnpm exec turbo run test --filter=@bb/app --
src/components/sidebar/ProjectListSectionHeader.test.tsx` (8 tests pass;
the new containment assertion fails before the fix)
- `pnpm exec turbo run typecheck --filter=@bb/app`
- `pnpm exec turbo run lint --filter=@bb/app` (0 errors)
- `dev-browser` before/after capture at a 376px compact viewport with a
fine pointer
- `git diff --check origin/main...HEAD`

> AGENT GENERATED
## Human comments

## What was wrong

Compact right-panel visibility is owned separately from persisted tab
state. Closing the final tab emptied and closed the tab model, but the
compact drawer stayed visible and rendered the unavailable-view
fallback.

## What changed

The shared file-tab and terminal-tab close paths now notify their panel
host when a close removes the final tab. Root compose, thread detail,
and plugin panel hosts use that notification to close compact drawer
visibility immediately. A focused compact-viewport regression test
verifies that closing the remaining tab restores the panel trigger
without showing the unavailable fallback. There are no wire, CLI, guide,
or public API changes.

## How you verified

- Confirmed the regression test failed twice before the fix with This
panel view is unavailable.
- pnpm exec turbo run test --filter=@bb/app --
src/components/plugin/PluginPanelRightPanelHost.test.tsx
src/components/secondary-panel/useThreadFileTabs.test.ts
src/lib/fixed-panel-tabs.plugin-terminal.test.ts (50 tests passed)
- pnpm exec turbo run typecheck lint --filter=@bb/app (successful; lint
reported 0 errors)
- Rebased cleanly onto origin/main at 46f23c4 before final
verification.

> AGENT GENERATED
## Human comments

## What was wrong

Provider discovery correctly cached installed state for five minutes,
but the successful provider-installation route did not invalidate an
earlier negative answer. After installing an installed-only plugin
provider, the provider could therefore remain hidden until the cache
expired. This was the remaining correctness gap from [the provider
discovery caching change](get-bb#2760).

## What changed

- Invalidate only the installed-state entry for the target host and
provider when installation events contain a successful completion.
- Add a public-route regression that caches the provider as absent,
installs it successfully, then verifies the next provider listing probes
again and includes it.
- Preserve discovery caching, retries, persisted provider roster
behavior, and immutable logo URLs. There are no server/daemon wire
changes, CLI changes, or documentation changes.

## How you verified

- Confirmed the regression failed before the fix: installation returned
HTTP 200 with `success:true`, but the installed-only provider remained
absent and was not probed again.
- `pnpm exec turbo run test --filter=@bb/server -- --run
test/public/public-provider-installations.test.ts
test/system/execution-options.test.ts
test/providers/provider-registry.test.ts` — 3 files, 61 tests passed.
- `pnpm exec turbo run typecheck --filter=@bb/server` — passed.
- `git diff --check origin/main...HEAD` — passed after rebasing onto
current `origin/main`.

> AGENT GENERATED
## Human comments

## What was wrong

PR get-bb#2665 turned the follow-up shortcut preference into a Queue/Steer
picker, but Queue stayed the default. Steer gives a better first
experience.

A simple default change would also affect existing stores.
`getAppSettings` starts from `defaultAppSettings` and overlays only keys
with an `app_settings_values` row. A user who never opened Settings has
no preference row.

## What changed

- `packages/db/drizzle/0112_steer_on_enter_default.sql` writes `false`
for an existing store without an explicit preference. A non-personal
project, a thread, or a saved setting shows prior use. Migration 0006
seeds `proj_personal` into every store, so that project alone does not
show prior use.
- `packages/domain/src/app-settings.ts` now defaults
`steerActiveThreadOnEnter` to `true`. A new install receives Steer
because the migration leaves its empty store unchanged.
- The configuration guide, the BB CLI skill reference, and the guide
template explain the split default.

The migration cannot identify an earlier install that has only the
seeded personal project. That store receives Steer because it has no
durable prior-use marker.

This change does not alter a schema, a route, or the host-daemon wire
contract. `HOST_DAEMON_PROTOCOL_VERSION` does not change.

## How you verified

- `pnpm exec turbo run test typecheck --filter=@bb/db
--filter=@bb/domain --filter=@bb/templates --filter=@bb/server --force`
passed after the rebase. Turbo completed 14 tasks. The suites passed
2,778 tests and skipped one expected server file.
- Five database cases cover an existing store, a personal thread, a
seeded-only store, a full migration run, and an explicit preference.
- The EAP codename scan found no match in the working tree, `HEAD`,
added lines, or commit messages.

Fixes #

> AGENT GENERATED

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

## What was wrong

The compact left sidebar tracked closing gestures only from inside its
panel, while the backdrop covering the exposed main-content strip
handled clicks only. The compact right shelf had no swipe-dismiss
controller after its shelf conversion, so neither its panel nor its
exposed-content dismiss layer could track a closing swipe. A dismiss
controller without an edge reservation would also compete with browser
history navigation gestures.

## What changed

Added one shared horizontal-dismiss hook for pointer/touch tracking,
intent and velocity arbitration, nested horizontal-scroll protection,
settling, and cleanup. The left and right shelf adapters now attach it
to both the panel and their exposed-content dismiss layer, configure
their direction and visual transforms, and reserve the corresponding
24px browser-navigation edge. No wire, CLI, guide, or protocol changes.

## How you verified

- Added red-to-green regressions for closing both shelves from the
remaining exposed-content strip.
- Added red-to-green regressions proving both corresponding 24px browser
edges are ignored.
- pnpm exec turbo run test --filter=@bb/app -- --run
src/components/ui/sidebar.test.tsx
src/components/secondary-panel/CompactSecondaryPanelShelf.test.tsx
src/components/secondary-panel/SecondaryPanelLayout.test.tsx (56 passed)
- pnpm exec turbo run typecheck --filter=@bb/app
- pnpm exec turbo run lint --filter=@bb/app (0 errors)
- Manually verified the left exposed-strip gesture at a 390x844 Chromium
viewport before shutting down the requested dev share.

Reported directly; no linked GitHub issue.

> AGENT GENERATED
SawyerHood and others added 16 commits September 1, 2026 16:06
## Human comments

## What was wrong

Every branch bb creates for a managed worktree or a new checkout branch was hardcoded to `bb/<slug>-<threadId>` in `buildManagedBranchName`. Nothing consumed `bb/` as a marker; it was simply a literal with no way to change it. Users who share a repository with teammates, or who want their agent branches under their own namespace, were stuck with it.

## What changed

**Setting.** `packages/domain/src/app-settings.ts` adds `managedBranchPrefix`, default `bb/`. `managedBranchPrefixSchema` validates a candidate by synthesizing a branch name from it and running the existing `isValidGitBranchName`, which is now exported from `git-checkout.ts`. Max length 64. `app_settings_values` is a key/value table, so there is no migration; the key falls back to `defaultAppSettings`.

**Server.** `buildManagedBranchName` takes an explicit `branchPrefix` instead of the literal. The three call sites (`thread-provisioning-environment.ts` ×2, `environment-provisioning-internal.ts`) resolve it once from `getAppSettings(deps.db)` at the boundary and pass the explicit value down through the provisioning plan args. The prefix does not need a trailing slash: `sawyer/wt-` gives `sawyer/wt-<slug>-<threadId>`, and an empty prefix gives `<slug>-<threadId>`. A new prefix applies to branches bb creates after the change; it renames nothing.

**UI.** A text field in Settings → General that commits on Enter or blur, reverts on Escape, previews the branch name your prefix produces, and marks an invalid prefix with a destructive border plus a `role="alert"` message. Empty shows a `No prefix` placeholder.

**Agent surfaces.** `bb settings general managedBranchPrefix <prefix>` and `sdk.system.updateGeneralSettings` both work through the existing generic settings surface with no new code. Documented in `docs/configuration.md`, the bb-cli skill reference `app-settings.md`, and the customization guide chapter.

No `HOST_DAEMON_PROTOCOL_VERSION` bump: `branchName` in `environment.provision` already carried an arbitrary branch string, and its type, requiredness, and meaning are unchanged. `AppSettings` is a published SDK type, so the new field changes `bundled-types/bb-plugin-sdk.d.ts` and `packages/plugin-api-map/sdk-public-api.json` is regenerated to match; main's own bump to `@get-bb/plugin-sdk` 0.4.35 is unpublished, so it carries this change too and no further bump is needed.

## How you verified

**Tests added** (each fails against the hardcoded literal, passes now):

- `packages/domain/test/app-settings.test.ts` — accepted and rejected prefixes, default round trip.
- `apps/app/src/views/SettingsView.branch-prefix.test.tsx` — commit on Enter, empty prefix, invalid revert on blur, Escape.
- `apps/server/test/threads/thread-create-helpers.test.ts` — configured prefix on both branch-name shapes, empty prefix.
- `apps/server/test/threads/generated-branch-names.test.ts` — sets the setting, then asserts the queued `environment.provision` command carries `sawyer/wt-custom-prefix-branch-<threadId>`.

**Commands.** `pnpm exec turbo run typecheck` 83/83. `pnpm exec turbo run test` 80/80. `node packages/plugin-sdk/scripts/check-npm-version-guard.mjs` passes at 0.4.35.

**Live end-to-end** against the dev app, with a scratch git repo as the project:

| Prefix set | Branch git actually created |
| --- | --- |
| `sawyer/wt-` | `sawyer/wt-add-a-greeting-helper-thr_ffm22h8kku` |
| `""` (empty) | `no-prefix-at-all-thr_tstep2ffyh` |

`git worktree list` confirmed a real checkout on the prefixed branch. Also verified the value round-trips through the UI and survives a reload, `/api/v1/system/config` reports it, `bb settings general managedBranchPrefix ""` works, and the CLI rejects `bad prefix/` with a named error.

The live run caught two UI defects the unit tests did not, both fixed here: the invalid state had no visual signal (the shared `Input` has no `aria-invalid` styling), and the empty-field placeholder showed `bb/`, which reads as though the field holds `bb/` when empty means no prefix.

> AGENT GENERATED
## Human comments

## What was wrong

The agent-tool boundary detected a plugin-owned Zod schema through
`safeParse`. It then used the host Zod converter. That converter read
private data from another package instance. See the [reproduction
report](https://get-bb.github.io/reports/issues/2837.html).

## What changed

The shared plugin policy now uses the schema-owned JSON Schema converter
when available. It keeps the host converter as a compatibility fallback.
The production server and the fake plugin host use the same helper. This
change has no wire or protocol change.

## How you verified

The focused regression failed before the fix and passed after the fix.
The final focused run passed 20 tests. The full server suite passed
2,121 tests. The Plugin SDK suite passed 221 tests. Turbo typechecks
passed for `@bb/server` and `@get-bb/plugin-sdk`. The change has 67
added and deleted text lines across four files.

Fixes get-bb#2837

> AGENT GENERATED

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

## What was wrong

Queued-message readiness was decided independently by startup events,
idle sweeps, plugin/timer drains, failed-row handling, and manual Stop
handling. Each fix could pass alone while their predicates disagreed
when composed: a settled `turn-starting` row could be discovered but
rejected, a failed grouped sibling could ride with a healthy row, and a
stale runtime event or in-flight hook could cross a manual Stop. Startup
admission also trusted a stale active thread after it changed to error,
which could strand a row or change `steer` semantics.

## What changed

- Made startup admission atomic: it either queues against the
still-active startup or retries once from the current thread state.
- Centralized automatic whole-group readiness across plugin, time,
`thread-busy`, and `turn-starting` waits.
- Replaced the positional `sendNow`/optional-callback claim API with an
explicit `automatic` versus `explicit-send` policy.
- Enforced failed-row exclusion and manual-Stop pause inside the group
claim transaction. Explicit Send now remains the override; independent
scheduled, plugin-held, and system-notice rows remain drainable.
- Required a post-Stop client request before a later root turn can clear
the pause, and rechecked the pause at final dispatch consumption.
- Used one SQL `IN` predicate for idle-drainable wait kinds.

This supersedes get-bb#2866 and get-bb#2867 and includes the failed-group and
Stop-boundary follow-ups. There is no host-daemon wire, schema, CLI,
SDK, migration, or documentation contract change.

## How you verified

- Added deterministic regressions for the settled-startup lost wake,
mixed-wait groups, failed grouped siblings, stale post-Stop starts,
hook-held Stop races, and active-to-error startup admission for both
`steer` modes.
- Focused Turbo server matrix: 70/70 passed.
- Focused Turbo DB/query-plan matrix: 68/68 passed.
- Standalone production-path Stop race harness: both races passed.
- Full DB suite: 444/444 passed.
- Full server suite: 2,131 passed; 10 unrelated `install-machine-script`
tests failed because their fake host daemon did not connect.
- `pnpm exec turbo run typecheck --filter=@bb/server --filter=@bb/db`
passed.
- `pnpm exec turbo run build --filter=@bb/server` passed.
- `git diff --check` passed.

> AGENT GENERATED
## Human comments

## What was wrong

The app-level toaster forwarded the desktop bottom-right position at
every viewport size. On mobile, archive feedback therefore occupied the
same lower-screen region as the home composer and obscured its controls.

## What changed

- Override toast placement to top-center at the shared compact
breakpoint.
- Preserve the configured toast position on desktop.
- Add focused assertions for compact top-center and desktop bottom-right
behavior.
- No wire, persistence, SDK, CLI, or daemon protocol behavior changed.

## Screenshots

The comparison uses the same disposable thread, archive interaction,
open home composer, light theme, and 390×844 viewport. The only
meaningful difference is the code revision: the current merge base
(`b4628ade6`) for before and the exact PR head (`1eca51a39`) for after.

| Before — merge base b4628ad | After — PR head 1eca51a |
| --- | --- |
| ![Before: archive toast obscures the lower controls of the open mobile
composer](https://img3.pixhost.cc/images/5396/765151634_before-current-base.png)
| ![After: archive toast is top-center above the open mobile
composer](https://img3.pixhost.cc/images/5396/765151641_after-final-head.png)
|

## How you verified

- Chrome for Testing 151.0.7922.71 against the exact branch web app at
390×844.
- Hard reloaded the seeded thread, archived it through the rendered
action menu, followed the client navigation home, opened the composer,
and observed the toast through its settled state.
- On the merge base, confirmed the toast occupies y=760–828 over the
composer's lower-screen controls.
- On the exact PR head, confirmed the toast occupies y=16–84 while the
composer begins at y=665, with no overlap or console errors.
- Focused compact and desktop regression tests cover the responsive
position behavior.
- Remote CI passed on final head `1eca51a39`: 13 checks successful and 2
not-applicable checks skipped.
- `git diff --check origin/main...HEAD`

BB-Thread-ID: thr_mq2niw3uxf

> AGENT GENERATED
## Human comments

## What was wrong

The parent PR centers compact-viewport toasts at the top edge, where
they cover the app's 48 px top navigation and its controls.

The first implementation also encoded that 48 px chrome height
separately in the toaster offset and shared chrome-row class, allowing
those two surfaces to drift if the navigation height changed later.

## What changed

- Inset compact toasts by the device top safe area, the shared app
chrome row height, and the standard 16 px toast gap.
- Define `--bb-app-chrome-row-height: 3rem` as the single length owner
used by both chrome rows and the toast calculation.
- Apply the inset to both Sonner's phone and wider compact layout modes
while preserving configured offsets on the unaffected edges.
- Assert the shared token declaration and both consumers in focused
regression tests.
- No wire, persistence, SDK, CLI, or daemon protocol behavior changed.

## Screenshots

The comparison uses the same disposable thread, archive interaction,
open home composer, light theme, and 390×844 viewport. The before image
is the parent PR behavior; the after image is exact head `168db15df`.

| Before — toast overlaps navigation | After — toast below navigation |
| --- | --- |
| ![Before: archive toast covers the app top
navigation](https://img3.pixhost.cc/images/5396/765151641_after-final-head.png)
| ![After: archive toast sits below the app top
navigation](https://img3.pixhost.cc/images/5397/765166017_after.png) |

## How you verified

- Remote CI passed on exact head `168db15df`; all applicable checks
succeeded and the two not-applicable checks were skipped.
- Chrome for Testing 151.0.7922.71 at 390×844 hard reloaded the seeded
thread, archived it, navigated home, opened the composer, and completed
the toast animation.
- The exact-head archive toast rendered centered at y=64–132, below the
shared 48 px chrome row plus 16 px gap, with the composer clear, no
horizontal overflow, and no console or page errors.

BB-Thread-ID: thr_mq2niw3uxf

> AGENT GENERATED
## What was wrong

The manually organized sidebar called its root thread group
`Unorganized`, while the same content group is called `Threads` in the
other sidebar views. The view-dependent name made one stable destination
sound like an error state and created unnecessary sidebar terminology.

## What changed

- Use `Threads` for the loose-thread group in every organization mode.
- Update the remove-section confirmation to say that released threads
move back to `Threads`.
- Keep grouping, ordering, drag-and-drop, status, and thread behavior
unchanged.

No public or wire contracts changed.

## Screenshots

Both screenshots use the same dev database, route, chronological
organization mode, light theme, and 1440×900 viewport.

### Before — main (`b4628ade6`)

![Before — loose thread group labeled
Unorganized](https://github.com/user-attachments/assets/4f81a6f6-8509-49a7-beab-6c43d9b1dfb5)

### After — this PR (`551ccd018`)

![After — loose thread group labeled
Threads](https://github.com/user-attachments/assets/143777f2-5736-464c-91ef-fce936363bee)

## How you verified

- `git diff --check`
- Chrome for Testing 151 at 1440×900 against the exact main and PR-head
branch web apps.
- Confirmed the same chronological sidebar fixture renders `Unorganized`
on main and `Threads` on this PR without changing row order or content.
- Remote CI passed the required checks, app/server/package/integration
tests, and Linux/macOS package smoke on the current head; iOS simulator
and Node Compatibility Smoke were intentionally skipped by workflow
configuration.
- Per repository policy, no CI-equivalent checks were run locally.

Fixes #

BB-Thread-ID: thr_ccffp4w2p2

> AGENT GENERATED
## Human comments

## What was wrong

The side-chat footer chose between the pending-interaction banner and
the entire `FollowUpPromptBox`, so showing a question or approval also
removed the queue stack that lived inside the prompt box. After
rendering both surfaces, the queue card's composer-attached `-mb-5`
treatment caused a second bug: it overlapped and clipped the
pending-interaction card because no composer sat beneath it. This is the
root cause of get-bb#2869.

Mobile side-chat sizing had a separate portal boundary bug. The
visual-viewport hook published `--bb-shell-height` on the nested sidebar
provider, but `CompactSecondaryPanelShelf` portals the compact side-chat
drawer to `document.body`. The drawer therefore kept the full
layout-viewport height while the keyboard reduced the visible viewport,
leaving its prompt box below the screen.

Side chats also bypassed the main follow-up submission guard.
`buildSideChatSubmitMode` hardcoded both pending-interaction inputs to
`false`, while `EmbeddedThreadChat` treated an unresolved or failed
interactions query as an empty list. A composer could therefore appear
usable before bb knew whether the child thread was waiting for input; a
submission made during that window was held by the server even though
the pending interaction and queue were not visible.

## What changed

- Always render the side-chat `FollowUpPromptBox`, pass the pending
interaction into its dedicated slot, and keep its queued-message stack
mounted while the composer is hidden.
- Treat queued messages as disabled while the interaction is pending.
- Add an explicit `attachedToComposer` contract to `QueuedMessagesList`;
side-chat queues use standalone card spacing during an interaction,
while normal side-chat and main-thread queues retain the attached
composer cap.
- Pass real pending-interaction and initial-loading state through
`buildSideChatSubmitMode`, matching the main follow-up submission guard.
- Hide the side-chat composer behind a visible checking state until the
first interaction request resolves. If it fails, keep the composer
unavailable and show an inline Retry action.
- Keep queues attached to the visible composer for plugin-owned
interactions, which deliberately do not replace that composer.
- Publish the mobile shell height and keyboard safe-area override on
`document.body`, the common style ancestor for both the app shell and
body-portaled compact drawers.
- Add regression coverage for the combined question-and-queue state,
standalone queue-card styling, unknown and failed interaction state,
plugin-owned interactions with queues, and the real body-portaled
side-chat shelf inheriting the corrected viewport height.
- This is UI-only: there are no host-daemon wire, CLI, guide, or
documentation changes, so `HOST_DAEMON_PROTOCOL_VERSION` is unchanged.

## How you verified

- Added focused submission-policy tests that failed before the fix
because active child side chats still returned queue mode while
pending-interaction state was loading or present, then passed once side
chats reused the real guard.
- Added component regressions that failed before the fix because an
unresolved or failed interactions request exposed the composer; they now
pass with a checking state and retryable error state.
- Added the plugin-interaction-plus-queue regression requested in
review; it failed before the fix because the queue used standalone
styling above a visible composer, then passed after deriving placement
from the composer-blocking condition.
- `pnpm exec turbo run test --filter=@bb/client-core -- --run
test/threadDetailPromptSubmission.test.ts` — 17 tests passed.
- `pnpm exec turbo run test --filter=@bb/app -- --run
src/components/thread/embedded-chat/EmbeddedThreadChat.test.tsx` — 15
tests passed.
- `pnpm exec turbo run test --force --filter=@bb/app -- --run
src/components/promptbox/banner/QueuedMessagesList.test.tsx
src/components/thread/embedded-chat/EmbeddedThreadChat.test.tsx` — 56
tests passed before the latest focused additions.
- `pnpm exec turbo run test --force --filter=@bb/app -- --run
src/components/layout/useMobileVisualViewportHeight.test.tsx
src/components/secondary-panel/CompactSecondaryPanelShelf.test.tsx
src/components/layout/AppLayout.root-compose-project.test.tsx
src/components/layout/AppLayout.sidebar-resize.test.tsx` — 32 tests
passed.
- `pnpm exec turbo run typecheck --filter=@bb/client-core
--filter=@bb/app` — passed.
- `pnpm exec turbo run lint --filter=@bb/client-core --filter=@bb/app` —
passed with 0 errors and the existing warnings.
- `pnpm exec turbo run build --filter=@bb/app` — passed with existing
Vite/chunk warnings.
- `git diff --check` and Prettier checks — passed.
- Reproduced the interaction states with a real Claude Code provider
question and a CLI-confirmed queued message, then checked desktop (1440
x 1000) and mobile (390 x 844) views. Live geometry changed from a 12 px
overlap to the intended 8 px gap, and the normal composer states
remained unchanged.
- Confirmed the real QA sidechat still has the pending `user_question`
at the server boundary while testing the fixed client policy.
- Reproduced the Android IME behavior in a target-SDK-36 System WebView
harness. WebViews that report a reduced visual viewport now pass that
height through to the body-portaled shelf. A host that consumes IME
insets exposes no web-side keyboard geometry and must correct its native
inset propagation.

Fixes get-bb#2869

> AGENT GENERATED
## Human comments

## What was wrong

When saving a new managed worktree branch prefix failed, the Settings
input continued displaying the rejected draft even though bb retained
the previously saved value. Users could therefore see one prefix while
new branches still used another.

## What changed

- Await the existing settings mutation from the branch-prefix field.
- Restore the authoritative saved prefix when that mutation rejects.
- Preserve successful Enter and blur saves, validation, dirty drafts,
the default `bb/`, and branch creation behavior.
- Add a focused regression for the failed-save path.

No daemon wire contract, server API, SDK API, CLI, documentation, or
protocol version changed.

## How you verified

- `pnpm exec turbo run test --filter=@bb/app -- --run
src/views/SettingsView.branch-prefix.test.tsx` — 5 tests passed.
- `pnpm exec turbo run typecheck --filter=@bb/app` — passed.
- `pnpm exec oxfmt --check apps/app/src/views/SettingsView.tsx
apps/app/src/views/SettingsView.branch-prefix.test.tsx` — passed.
- `git diff --check origin/main...HEAD` — passed.
- The original source thread also completed full app tests, app build,
and matched desktop failure/success QA.

> AGENT GENERATED
## Human comments

## What was wrong

Linux AppImages were still built with electron-builder's legacy
`appimage-12.0.1` toolset and its 2019 AppImage runtime (`effcebc`).
Crash capture on the recurring CI signature showed that the zygote first
received `SIGBUS/BUS_ADRERR` while fetching an executable instruction
from the AppImage FUSE mount. The fault address equaled RIP at the first
instruction of PDFium's `copy8`; the browser crash handler then
terminated with `int3`/`SIGTRAP`, which is the outer signal reported by
the lifecycle smoke. This is why AppImage stdout/stderr was normally
empty: the failure was below app JavaScript and before owned-runtime
publication.

The captured system had 14 GiB available, untouched swap, and no OOM
evidence. The same signature occurred on Intel and AMD CI runners. The
core and matched Electron 41.7.0 symbols ruled out the Chromium sandbox
CHECK addressed by get-bb#2657, the post-marker port race addressed by get-bb#2822,
an application fatal path, and CPU-vendor-specific behavior.

## What changed

- Upgrade electron-builder to 26.15.7 and explicitly pair its exact
Squirrel Windows peer.
- Select AppImage toolset 1.0.3, which packages the maintained 20251108
static runtime instead of the legacy external-libfuse runtime.
- Extend the existing packaging-config regression test to require
toolset 1.0.3.

The resulting x86_64 AppImage is a static PIE and reports runtime commit
`dd6cebe`. There is no server/host-daemon wire change, so
`HOST_DAEMON_PROTOCOL_VERSION` is unchanged.

## How you verified

Red, before the fix:

- [Diagnostic CI run
33574359370](https://github.com/get-bb/bb/actions/runs/33574359370)
reproduced the exact pre-runtime `SIGTRAP` on iteration 3 after two
healthy launches.
- Its zygote core recorded `SIGBUS`, `si_code=BUS_ADRERR`, and `si_addr
== RIP`; the matching Electron build ID resolved the address to the
first instruction fetched from the FUSE-backed executable.
- Memory, swap, kernel logs, browser and zygote cores, the exact
AppImage, and matched symbols were inspected. All diagnostic load was
sequential and bounded, and failure stopped the loop.

Green, with the fix:

- On the same bounded six-launch workflow and runner class, the
candidate completed five target launches without a pre-runtime signal;
iteration 6 then exposed the distinct post-marker owned-runtime flake
and produced no crash core.
- The exact same candidate SHA passed all six AppImage mount/lifecycle
launches on [attempt 2 of run
33575866378](https://github.com/get-bb/bb/actions/runs/33575866378).
- [PR CI run
33576754644](https://github.com/get-bb/bb/actions/runs/33576754644)
passed the full matrix, including normal Linux AppImage packaging and
lifecycle smoke.
- `pnpm exec turbo run build typecheck test --filter=@bb/desktop
--concurrency=2`: 15/15 tasks passed; 38/38 desktop test files passed,
247 tests passed, one skipped.
- `pnpm install --frozen-lockfile` and `git diff --check` passed.

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

## What was wrong

After a text setting blurred and began a slow autosave, typing a newer
draft before the request finished could be undone by the older response.
The reconciliation effect replaced the newer unsaved text with the
earlier stored value.

## What changed

- Track whether a string setting has a draft newer than its last blur.
- Reconcile stored values only when no newer local draft exists.
- Keep the shared behavior for regular, multiline, and secret string
settings without changing validation or autosave semantics.
- Add a focused regression that completes the older request, preserves
the newer draft, and then saves it normally.

No daemon wire contract, server API, public Plugin SDK API, CLI,
documentation, or protocol version changed.

## How you verified

- `pnpm exec turbo run test --filter=@bb/app -- --run
src/components/plugin/PluginSettings.test.tsx` — 12 tests passed.
- `pnpm exec turbo run typecheck --filter=@bb/app` — passed.
- `pnpm exec oxfmt --check
apps/app/src/components/plugin/PluginSettings.tsx
apps/app/src/components/plugin/PluginSettings.test.tsx` — passed.
- `git diff --check origin/main...HEAD` — passed.
- The original source thread also completed app build and matched
slow-save browser QA.

> AGENT GENERATED
## Human comments

## What was wrong

The project selector menu had no viewport-aware height limit or vertical
scroll behavior. A long project list extended beyond short screens.

## What changed

The project selector menu now uses the available Radix height and the
dynamic viewport height. It scrolls vertically and contains overscroll.

## How you verified

- `pnpm exec turbo run test --filter=@bb/app -- --run
src/components/pickers/ProjectSelector.test.tsx`
- `pnpm exec turbo run typecheck --filter=@bb/app`
- Browser test at 900 by 320 pixels with 20 projects. The 615-pixel list
scrolled within a 190-pixel menu.

> AGENT GENERATED
## Human comments

## What was wrong

get-bb#2884 centralized atomic queue admission, whole-group eligibility,
failed-row exclusion, and manual-Stop policy, but automatic dispatch
orchestration was still split across lifecycle events, timer and plugin
walkers, wait-release handlers, and recovery. Those callers
independently chose candidate rows, pacing, coalescing, and failure
behavior. In particular, the `turn-started` path captured queued row IDs
and only logged terminal failures, while other drains persisted them,
and plugin recheck coalescing used process-global state rather than
server-instance state.

The manual-Stop pause also classified ordinary queued rows as only
unwaited or `thread-busy`. A user message parked while a turn was
starting uses `turn-starting`, so an automatic drain could claim that
older row after Stop and unexpectedly restart the thread.

The initial coordinator migration also grouped idle, scheduled, and
orphan recovery under one periodic job guard and expanded plugin removal
into one deferred task per affected thread. A slow idle pass could
therefore suppress due-schedule recovery on overlapping ticks, while
removing a plugin could create an unbounded burst of deferred queue
work.

## What changed

- Replaced the old `queue-drains` module and automatic-send entry points
with one queued-message dispatch coordinator whose interface accepts
typed wake-up facts.
- Migrated thread lifecycle, turn events, workspace readiness,
provisioning settlement, interaction settlement, host reconnect, plugin
recheck/removal, message creation, and periodic recovery to that
interface.
- Moved candidate discovery, wait release, per-thread dispatch
coalescing, requeue pacing, automatic eligibility, claims, timeout
treatment, persistent failure handling, and orphaned-plugin recovery
behind the coordinator seam.
- Kept lifecycle wake-ups immediate even after a recent requeue while
timer and plugin sweeps retain their anti-spin cooldown.
- Kept periodic idle, due-scheduled, and orphaned-plugin recovery as
independently guarded typed wakes, so an overlapping tick can deliver
due work while an earlier idle pass is still running.
- Kept plugin-unregister release and dispatch as one sequential
coordinator batch instead of one deferred task per affected thread.
- Scoped plugin-recheck request coalescing to each server instance.
- Kept explicit “Send now” separate behind an explicit-send-only
interface.
- Classified `turn-starting` user rows as ordinary for manual-Stop pause
policy, while keeping explicit Send now as the deliberate override.

There is no host-daemon wire, database schema, CLI, SDK, migration, or
documentation contract change.

## How you verified

- Added regressions for terminal failure persistence on `turn-started`,
lifecycle wake-ups during the requeue cooldown, per-server request
coalescing, and orphaned plugin waits.
- Added a public HTTP/session-event regression covering steer during
startup, manual Stop, a late `turn/started`, a later queued message,
automatic `thread-ready` dispatch, and explicit Send now. It failed
before the predicate fix and passed after it.
- Added an overlapping-periodic-tick regression that blocks idle
recovery and proves due scheduled work gets the next dispatch
opportunity. It failed before independent guards and passed after.
- Added a plugin-unregister regression covering multiple affected
threads. It failed with two deferred tasks before the batch fix and
passed with one afterward while draining both rows.
- Final focused Turbo queue/lifecycle matrix: 69/69 server tests passed.
- `@bb/server` Turbo typecheck and build passed.
- Full server suite before the review follow-up: 2,134 passed. The ten
unrelated `install-machine-script` cases failed both in the full run and
alone because this host has `/tmp/package.json` with `"type": "module"`,
which makes their extensionless CommonJS fixture fail with
`ReferenceError: require is not defined`.
- Changed-file formatting and `git diff --check` passed.

Follow-up to get-bb#2884.

> AGENT GENERATED
## Human comments

## What was wrong

The dispatch checkpoint handled scheduled sends, busy threads,
provisioning, and pending interactions, but it did not treat a
disconnected host as a core wait. A follow-up to an existing idle thread
therefore continued into live host dispatch and could fail with a 502
instead of becoming durable queued work.

## What changed

The dispatch checkpoint now resolves the attached environment and its
live host status before dispatching. When a usable thread environment
belongs to a disconnected host, the message is persisted with the
existing `host-offline` wait reason.

The public-route regression test covers the normal `steer-if-active`
follow-up path on an existing idle thread. It then reconnects that same
host through the real daemon socket-open handler and the centralized
queued-message dispatch path from get-bb#2886, verifies the wait is released
and the row is consumed, and verifies a `turn.submit` carrying the
original input is issued on the original thread.

This changes no server/daemon wire contract, CLI command, or user-facing
setting, so no protocol version or documentation update is needed.

## How you verified

- `pnpm exec turbo run test --filter=@bb/server --
test/public/public-thread-offline-followup.test.ts
test/threads/queue-drain-failure.test.ts
test/threads/requested-queue-drain.test.ts
test/threads/thread-send-dispatch.test.ts`
- All 43 focused queue, reconnect, and dispatch tests passed.
- `pnpm exec turbo run typecheck --filter=@bb/server`
- Prettier and `git diff --check` passed.
- Live dev CLI: created an existing idle thread, held its host offline,
and ran default `bb thread tell`. The command returned `delivery:
queued` with `waitingOn.kind: host-offline`; `bb thread queue list`
showed the same row. After reconnect, the queue emptied and `bb thread
output` returned the queued prompt response on the original thread.

Fixes: N/A (no linked issue)

> AGENT GENERATED
## Human comments

## What was wrong

Sonner's built-in toast stylesheet targets every descendant with a
`data-icon` attribute and adds asymmetric icon margins. bb's shared
`Icon` component uses the same attribute inside custom toast content, so
the close glyph sat 3.5px left of its hover background's center; the
leading status glyph inherited the same displacement.

## What changed

Neutralized Sonner's inherited margins on the custom toast status and
close icons. Added focused regression coverage for both glyphs. This has
no wire, protocol, CLI, guide, or documentation impact.

## How you verified

- `pnpm exec turbo run test --filter=@bb/app --force --
src/components/ui/app-toast.test.tsx` (4 tests passed)
- `pnpm exec turbo run typecheck --filter=@bb/app --force`
- `pnpm exec oxfmt apps/app/src/components/ui/app-toast.tsx
apps/app/src/components/ui/app-toast.test.tsx --check`
- `git diff --check origin/main...HEAD`
- Doobie hover-state capture confirmed that the close glyph and its
hover background now share the same horizontal center.

> AGENT GENERATED
## Human comments

This adds a notification center where you can see the full contents of
all the notifications you got in 1 place. Currently it's needed cuz the
notifications are actually clamped so you can't even see the full
contents of a single notification without copy-pasting.



https://github.com/user-attachments/assets/34c32b6e-66ec-43ac-9e2a-6640d6244080

Notification center can be accessed via command palette or just pressing
"show more" on one of the longer notifications.

## What was wrong

Long toast text was unreadable and unrecoverable, for two independent
reasons. First, sonner 1.7.4 forces a collapsed non-front toast to the
front toast's height — `[data-expanded="false"][data-front="false"] {
height: var(--front-toast-height) }` — and `apps/app/src/app.css:594`
adds `overflow: hidden` on that same node so our custom card is clipped
rather than overflowing. The moment a second, shorter toast arrived, a
long one was cut off; hovering sets `data-expanded="true"` and restores
it, which is why the text was present but unreachable. Second, nothing
recorded toasts, so a message that auto-dismissed after its four-second
duration was gone for good. Together these made a long plugin install
failure impossible to read — the reported symptom was a `Could not
resolve "@radix-ui/react-tabs"` esbuild error that could only be
recovered by copy-pasting it elsewhere.

## What changed

- `apps/app/src/lib/notifications/notification-store.ts` (new):
in-memory store recording every toast for the session, capped at 200.
Reusing a toast `id` updates the entry in place, so a loading toast
replaced by its result stays one row; transient `loading` toasts are not
recorded.
- `apps/app/src/components/ui/app-toast.tsx`: descriptions clamp to four
lines and measure real overflow (`scrollHeight > clientHeight`). When
clipped, the toast grows a **Show more** action that dismisses it and
opens the center scrolled to that entry. Clamping also keeps toasts
short, so the stacking bug cannot reproduce.
- `apps/app/src/components/notifications/NotificationCenter.tsx` (new):
bottom-right tray built on shared-ui's `Popover`, which already falls
back to `ResponsiveDrawerShell` on compact viewports per the AGENTS.md
drawer rule. Untruncated text, per-row copy and dismiss, "Clear all".
Opening it dismisses the toast stack so the two do not overlap.
- `apps/app/src/components/sidebar/SidebarNotificationsButton.tsx` (new)
plus `AppSidebar`: sidebar bell badging the count raised since the
center was last opened.
- `packages/domain/src/app-keybindings.ts`,
`apps/server/src/services/system/app-keybindings.ts`,
`apps/app/src/lib/app-command-metadata.ts`: new `notifications.open`
command, surfaced in the quick palette as "Show all notifications". It
ships **unbound** rather than claiming a chord; Settings → Keyboard can
assign one.
- `apps/app/src/lib/notifications/plugin-toast-recording.ts` (new) and
`plugin-frontend.ts`: plugins receive the raw `sonner` namespace from
`installPluginRuntime`, bypassing `appToast`, so its `toast` export is
wrapped to record plugin toasts too.
- `apps/app/src/components/sidebar/PluginThreadList.tsx`: the only place
in the app calling `sonner` directly; moved to `appToast`.
- `packages/shared-ui`: new `Bell` icon (`Notification03Icon`).
- `docs/configuration.md`: new Notifications section.
`apps/app/src/components/ui/sonner.stories.tsx`: long-build-error case
so the clamp is reviewable in the Ladle catalog.

No wire changes: this is app-layer state plus one `AppCommandId` enum
addition, and nothing crosses the server/daemon boundary, so
`HOST_DAEMON_PROTOCOL_VERSION` is untouched.

## How you verified

26 new tests across the store, the plugin-toast wrapper, the toast
clamp, and the tray. The clamp and Show-more tests fail before this
change because neither the clamp class nor the action exists.

- `pnpm exec turbo run test --filter=@bb/app` — 448 files, 3532 passed,
3 skipped
- `pnpm exec turbo run test --filter=@bb/server` — 2072 passed
- `pnpm exec turbo run test --filter=@bb/domain --filter=@bb/shared-ui`
— passing
- `pnpm exec turbo run typecheck --filter=@bb/app --filter=@bb/domain
--filter=@bb/server --filter=@bb/shared-ui` — clean

Not yet verified visually: the clamp's visual balance and the popover's
placement against the toast stack have not been eyeballed in a running
app or in iOS Simulator Safari. Worth a look before merge.

> AGENT GENERATED

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Merge upstream/main (75 commits through 88ee0d0) into the fork.

Wire: HOST_DAEMON_PROTOCOL_VERSION 177 (upstream 176); the fork's
admission RPCs change the wire so enrolled daemons must update.

DB: the fork's admission/lease schema is regenerated as
0113_workspace_safety after upstream's 0110-0112 (drizzle-kit generate,
IF NOT EXISTS on CREATE statements so databases that ran the old fork
0110 replay cleanly). Drizzle only applies journal entries newer than the
latest applied one, so leaving the fork migration at 0110 would have
silently skipped it on any database already at upstream's tail.

Dispatch rework (get-bb#2779): the admission release on turn completion moves
from the removed queued-message-auto-send follow-up to the
queued-message-dispatch follow-up's thread-ready wake; daemon connect
keeps the fork's admission reconcile and adds upstream's host-connected
dispatch wake. Thread creation, the conversation-outline route and the
thread count/running routes take upstream's implementation. The dead
deferred_thread_messages exports and the removed provider-session-reaping
flag are dropped.

Fork-required fields (admission, protectUnmanagedWorkspace) are added to
upstream's new test fixtures; the GitHub plugin's extraRepos schema uses
the fork's repo parser; plugin SDK follows upstream 0.4.35 and the SDK
inventory hash is regenerated.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 2, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-02T06:39:32.904896Z eba92ab PR opened
🔒 Security Review Completed 2026-09-02T06:52:30.814407Z eba92ab PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: eba92ab504

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +109 to +112
requestQueuedMessageDispatch(deps, {
hostId: args.hostId,
kind: "host-connected",
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Serialize dispatch behind admission reconciliation

When a socket drops after a turn becomes idle but before its admission release completes, the database can retain a running admission for that idle thread. Starting the host-connected drain concurrently with reconciliation lets a queued send reuse that stale reservation while reconciliation releases it; depending on ordering, the daemon then rejects the command or allocates a replacement reservation that is not persisted, leaking a host slot. Chain this wake after reconciliation and durable-admission recovery finish.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, and fixed in #7. onDaemonSocketOpen now requests the host-connected wake in a .finally after reconcileHostWorkAdmissions and recoverDurableWorkAdmissions settle, so a drained send can no longer race a stale running admission that reconciliation is about to release. On a failed reconcile the wake still fires after the logged warning rather than stranding host-offline rows until the next reconnect; every dispatch still passes the daemon's fail-closed reserve, so that path degrades to a rejected command, not a leaked slot.

Covered by a new test that defers the reconcile reply and asserts no turn.submit dispatches until it is answered (public-thread-offline-followup.test.ts). No wire change, so HOST_DAEMON_PROTOCOL_VERSION stays at 177.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One adjustment after CI on #7: the wake now runs only when reconciliation succeeds, gated on the opening session still being the host's registered daemon session, instead of in a .finally. The .finally form fired the wake after a dropped daemon (and, in tests, after the harness had closed its database whenever a fake daemon never answered reconcile: six unhandled "database connection is not open" errors on the server job). A failed reconcile means the daemon is unhealthy or gone; its next reconnect reconciles and wakes the parked rows. Covered by a second test that answers reconcile with an error and asserts the host-offline row stays parked.

Comment on lines +105 to +108
// A dispatch that arrived while this machine was away parked its row on a
// `host-offline` wait with no schedule, so no sweep can see it — the
// machine coming back is that wait's release signal, and this socket
// opening is where core hears it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Remove the forbidden reconnect comment

This newly added prose comment is neither a semantic tool directive nor a Plugin SDK declaration comment, so it violates the repository's explicit prohibition on code comments and should be removed while keeping the behavior self-explanatory through naming.

AGENTS.md reference: AGENTS.md:L7-L9

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in #7. The comment came in verbatim from upstream's side of the merge conflict; the wake's kind: "host-connected" carries the meaning on its own. For the record, the bb/no-comments rule only runs in the two packages that define a lint script (apps/app, apps/mobile), which is why this passed CI; the AGENTS.md policy still applies, so it is gone.

The Linux AppImage lifecycle smoke fails on the fork's GitHub-hosted
runners with "The owned runtime exited before bb became ready" while
passing upstream on Blacksmith. The runtime's stdout/stderr live only in
the GUI's in-memory child log, so the failure prints updater noise and
nothing else. Dump the tails of the bb-app launcher's log files under
BB_DATA_DIR/logs into the error so the run carries the runtime's own
exit reason.
…eady

When the Electron-owned bb-app process exits before the server answers,
its stdout/stderr only reach the in-window error page. Also write the
exit result and the captured child log through the desktop logger so a
headless launch (the Linux AppImage lifecycle smoke on CI) carries the
runtime's own failure reason on stderr.
The AppImage lifecycle smoke fails on GitHub-hosted ubuntu-latest with
"The owned runtime exited before bb became ready". The captured child
log shows the nested runtime dying with exit 9 and "bad option:
--no-sandbox": Ubuntu 24.04 restricts unprivileged user namespaces, the
AppImage toolset's AppRun reacts by adding --no-sandbox to the payload
args, and the desktop's nested runtime relaunch runs the same AppImage
as Node, which rejects the flag. Upstream's Blacksmith runners do not
restrict user namespaces, so lift the restriction on the runner before
the smoke to match them, printing the prior value for the record.
@NeelM0906
NeelM0906 deleted the branch main September 2, 2026 13:16
@NeelM0906 NeelM0906 closed this Sep 2, 2026
@NeelM0906 NeelM0906 reopened this Sep 2, 2026
@NeelM0906
NeelM0906 changed the base branch from integration/upstream-2026-08-31 to main September 2, 2026 13:17
@NeelM0906
NeelM0906 merged commit 2ec230f into main Sep 2, 2026
28 checks passed
@NeelM0906
NeelM0906 deleted the integration/upstream-2026-09-02 branch September 2, 2026 13:25
NeelM0906 added a commit that referenced this pull request Sep 2, 2026
…iliation

On daemon connect the fork's admission reconciliation ran concurrently
with upstream's host-connected dispatch wake. A socket that dropped
after a turn went idle but before its admission release could leave a
running admission row behind; a queued send drained on reconnect could
reuse that stale reservation while reconciliation released it, so the
daemon rejected the command or allocated a replacement reservation the
server never persisted, leaking a host slot. Request the wake in a
finally after reconcile and durable recovery settle; a failed reconcile
still wakes host-offline rows after the logged warning, and every
dispatch still passes the daemon's fail-closed reserve.

Also drop the two prose comments Codex flagged on #5 and #6 and move
the protocol-offset rationale into AGENTS.md. No wire change.
NeelM0906 added a commit that referenced this pull request Sep 2, 2026
…r-reconcile

Wake host-connected dispatch only after admission reconciliation (Codex review of #5/#6)
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.

8 participants