Skip to content

[codex] Bundle Claude Code SDK CLI support file - #4

Closed
SawyerHood wants to merge 1 commit into
mainfrom
bb/investigate-claude-code-provider-bundling-thr_kdztsapzec
Closed

SawyerHood wants to merge 1 commit into
mainfrom
bb/investigate-claude-code-provider-bundling-thr_kdztsapzec

Conversation

@SawyerHood

Copy link
Copy Markdown
Collaborator

Summary

  • Copy the @anthropic-ai/claude-agent-sdk cli.js fallback support file beside the host-daemon Claude bridge bundle.
  • Extend bundle checking so the copied Claude Code CLI support file is syntax-checked and reported.
  • Add Turbo wiring for bundle:check so the package check can be run through the repo-standard Turbo path.

Root Cause

The bundled Claude Code bridge relocates SDK code into apps/host-daemon/dist, but the SDK fallback executable cli.js was not copied into that directory. When the SDK could not resolve a native platform package, its fallback lookup also failed, producing the reported native-binary error before review execution started.

Validation

  • pnpm exec prettier --check apps/host-daemon/scripts/bundle-manifest.mjs apps/host-daemon/scripts/build-bundles.mjs apps/host-daemon/scripts/check-bundles.mjs turbo.json
  • pnpm exec turbo run bundle:check --filter=@bb/host-daemon
  • pnpm exec turbo run typecheck --filter=@bb/host-daemon
  • pnpm exec turbo run test --filter=@bb/host-daemon
  • Direct bundled bridge thread/start repro returned a providerThreadId instead of the native-binary error.

@SawyerHood SawyerHood closed this May 15, 2026
SawyerHood pushed a commit that referenced this pull request Jul 30, 2026
## The problem

When you open a thread through bb connect, your browser has to talk to
your laptop through a long pipe that runs out to Cloudflare and back.
Every single question your browser asks down that pipe takes about **95
milliseconds** to get an answer, no matter how small the answer is.
That's just how long the trip takes.

So if you ask two questions one after the other, you wait ~190 ms. If
you ask them at the same time, you wait ~95 ms. The number of questions
matters way more than how big the answers are.

Opening a thread used to ask more questions than it needed to, and it
asked some of them in a line when they could have been asked all at
once.

## What this changes

**1. Stop waiting in line.** Loading a thread needs two things: the
thread itself, and its timeline. Neither one needs the other. But the
code asked for the thread, waited for the whole answer to come back, and
only then asked for the timeline. Now both go out at the same time.

**2. Stop asking twice.** Right after loading the thread, the app
immediately asked for the same thread again. Now it skips the second ask
if the first one just answered (within 5 seconds).

**3. Stop asking for things nobody looked at.** There's a "Parent"
dropdown in the thread sidebar. The app fetched the list of possible
parent threads on every thread open — even though most people never open
that dropdown. Now it only fetches when you actually click it. You'll
see a brief "Loading threads…" the first time, and a retry option if it
fails.

**4. Let answers be zipped.** The pipe had a rule that said "don't send
me compressed data." That rule made your laptop unzip everything before
pushing it through the pipe, so we shipped big uncompressed answers when
small zipped ones would do. That rule is gone.

## Did it actually work?

Yes for #1, barely for #4. I tested this against the real production
Connect — real Cloudflare, real tunnel, not a simulation.

**Asking both questions at once (change #1):**

| | how long |
|---|---:|
| Before (one after the other) | **255 ms** |
| After (both at once) | **141 ms** |

That's **114 ms faster, about 45%**. This is the change that matters.

**Zipping the answers (change #4):** barely moves the needle, ~0–10 ms.

The reason is boring: the answers are already small. Earlier work capped
how much timeline gets sent at once, so even the biggest threads on my
machine only produce 15–61 KB. Zipping shrinks that by 3–4×, which
sounds great, but the pipe moves data fast enough (~18 MB/s) that 40 KB
was never the problem. The 95 ms trip was.

Zipping still earns its place in two spots, though:
- On a slow connection (say hotel wifi at 10 Mbps), that same 40 KB *is*
worth ~35 ms per request.
- It turns the app's precompressed asset files back on. Those had been
silently disabled over Connect this whole time by the "don't send me
compressed data" rule, and they're much bigger than any API response.

**Bottom line: don't expect to feel the zipping on fast internet. Do
expect thread opens to feel snappier.**

## Things that will look different

- **A new "Parent: None" row.** If you have a project with only one
thread, the Parent row used to be hidden — the app knew there was
nothing to pick. Now it can't know that without asking, and asking is
the thing we're trying to avoid. So the row is always there for root
threads.
- **The Parent dropdown can be tabbed to** in a couple of cases where it
previously couldn't.
- **Re-opening a thread you've seen before** now asks "what's new since
X?" instead of "give me everything," and falls back to asking for
everything if the server can't answer that.

## The part to be careful about

To let zipped data through the pipe, I rewrote the piece of code that
forwards requests, swapping `fetch` for Node's lower-level HTTP client.
This touches **every** thing that goes through the tunnel — including
any dev server you've shared with `bb connect expose <port>`, not just
BB itself. It's the riskiest part of this PR.

Things that could have broken, and why they didn't:

- **Garbled responses.** Raw headers now get forwarded as-is, which is
dangerous if a "this data is chunked" header rides along with data
that's already been un-chunked. The relay already throws those headers
away (`apps/connect/src/tunnel-do.ts:44`), so it can't happen.
- **Wrong length.** A "this is 500 bytes" header on a body that isn't
500 bytes would break things. Checked both places that set it — they
always match what actually gets sent.
- **Redirects.** The old code told `fetch` not to follow them. Node's
client doesn't follow them anyway. Same behavior.
- **Reused memory.** The new code hands out views into Node's internal
buffer. Safe here, because it's copied out synchronously before Node can
reuse it.

No daemon protocol version bump needed — the wire format didn't change,
no worker code changed, and the already-deployed relay handles these
headers fine.

**One thing tests can't cover:** whether Cloudflare's runtime passes a
zipped response through untouched. I confirmed it by hand (step 2
below). Running it locally uses the same runtime but skips Cloudflare's
CDN layer, so local testing isn't a full substitute.

## How to check it yourself

Node 22, pnpm 9, clean checkout:

```bash
pnpm install --offline --frozen-lockfile
pnpm run ensure-native-modules
pnpm exec turbo run test typecheck --filter=@bb/tunnel-client --filter=bb-plugin-connect --force
pnpm exec turbo run test --filter=@bb/app --force -- src/hooks/cache-owners/cache-owner-registry.test.ts src/hooks/queries/thread-queries.test.tsx src/components/secondary-panel/ThreadMetadataContent.test.tsx src/views/thread-detail/ThreadDetailSecondaryContent.test.tsx
pnpm exec turbo run typecheck --filter=@bb/app --force
```

Then open DevTools → Network and load a thread through bb connect:

1. The thread and timeline requests start together, and the thread isn't
requested twice.
2. The timeline response has `content-encoding: gzip` and a
`server-timing: bb_connect_origin` header.
3. The Connect host log prints timing (`originTtfbMs`, `originBodyMs`,
`responseBytes`, `contentEncoding`).
4. Nothing requests parent candidates until you click **Parent**. First
click shows "Loading threads…", then the list.
5. A root thread in a project with no other threads shows the **Parent:
None** row.

## Tests

All green. Tunnel client 6/6, connect plugin 65/65, four focused app
suites 28/28, plus typechecks. The full GitHub matrix passes — app,
server, integration, packages, checks, and macOS/Linux smoke.

The new tests are the interesting ones. They check that the timeline
request really does leave before the thread request comes back, that a
warm cache asks for a delta and merges the answer correctly, that the
duplicate thread request is skipped when fresh but still happens when
stale, and that zipped bytes cross the tunnel byte-for-byte identical.
amadad pushed a commit to amadad/bb that referenced this pull request Aug 14, 2026
## The problem

When you open a thread through bb connect, your browser has to talk to
your laptop through a long pipe that runs out to Cloudflare and back.
Every single question your browser asks down that pipe takes about **95
milliseconds** to get an answer, no matter how small the answer is.
That's just how long the trip takes.

So if you ask two questions one after the other, you wait ~190 ms. If
you ask them at the same time, you wait ~95 ms. The number of questions
matters way more than how big the answers are.

Opening a thread used to ask more questions than it needed to, and it
asked some of them in a line when they could have been asked all at
once.

## What this changes

**1. Stop waiting in line.** Loading a thread needs two things: the
thread itself, and its timeline. Neither one needs the other. But the
code asked for the thread, waited for the whole answer to come back, and
only then asked for the timeline. Now both go out at the same time.

**2. Stop asking twice.** Right after loading the thread, the app
immediately asked for the same thread again. Now it skips the second ask
if the first one just answered (within 5 seconds).

**3. Stop asking for things nobody looked at.** There's a "Parent"
dropdown in the thread sidebar. The app fetched the list of possible
parent threads on every thread open — even though most people never open
that dropdown. Now it only fetches when you actually click it. You'll
see a brief "Loading threads…" the first time, and a retry option if it
fails.

**4. Let answers be zipped.** The pipe had a rule that said "don't send
me compressed data." That rule made your laptop unzip everything before
pushing it through the pipe, so we shipped big uncompressed answers when
small zipped ones would do. That rule is gone.

## Did it actually work?

Yes for get-bb#1, barely for get-bb#4. I tested this against the real production
Connect — real Cloudflare, real tunnel, not a simulation.

**Asking both questions at once (change get-bb#1):**

| | how long |
|---|---:|
| Before (one after the other) | **255 ms** |
| After (both at once) | **141 ms** |

That's **114 ms faster, about 45%**. This is the change that matters.

**Zipping the answers (change get-bb#4):** barely moves the needle, ~0–10 ms.

The reason is boring: the answers are already small. Earlier work capped
how much timeline gets sent at once, so even the biggest threads on my
machine only produce 15–61 KB. Zipping shrinks that by 3–4×, which
sounds great, but the pipe moves data fast enough (~18 MB/s) that 40 KB
was never the problem. The 95 ms trip was.

Zipping still earns its place in two spots, though:
- On a slow connection (say hotel wifi at 10 Mbps), that same 40 KB *is*
worth ~35 ms per request.
- It turns the app's precompressed asset files back on. Those had been
silently disabled over Connect this whole time by the "don't send me
compressed data" rule, and they're much bigger than any API response.

**Bottom line: don't expect to feel the zipping on fast internet. Do
expect thread opens to feel snappier.**

## Things that will look different

- **A new "Parent: None" row.** If you have a project with only one
thread, the Parent row used to be hidden — the app knew there was
nothing to pick. Now it can't know that without asking, and asking is
the thing we're trying to avoid. So the row is always there for root
threads.
- **The Parent dropdown can be tabbed to** in a couple of cases where it
previously couldn't.
- **Re-opening a thread you've seen before** now asks "what's new since
X?" instead of "give me everything," and falls back to asking for
everything if the server can't answer that.

## The part to be careful about

To let zipped data through the pipe, I rewrote the piece of code that
forwards requests, swapping `fetch` for Node's lower-level HTTP client.
This touches **every** thing that goes through the tunnel — including
any dev server you've shared with `bb connect expose <port>`, not just
BB itself. It's the riskiest part of this PR.

Things that could have broken, and why they didn't:

- **Garbled responses.** Raw headers now get forwarded as-is, which is
dangerous if a "this data is chunked" header rides along with data
that's already been un-chunked. The relay already throws those headers
away (`apps/connect/src/tunnel-do.ts:44`), so it can't happen.
- **Wrong length.** A "this is 500 bytes" header on a body that isn't
500 bytes would break things. Checked both places that set it — they
always match what actually gets sent.
- **Redirects.** The old code told `fetch` not to follow them. Node's
client doesn't follow them anyway. Same behavior.
- **Reused memory.** The new code hands out views into Node's internal
buffer. Safe here, because it's copied out synchronously before Node can
reuse it.

No daemon protocol version bump needed — the wire format didn't change,
no worker code changed, and the already-deployed relay handles these
headers fine.

**One thing tests can't cover:** whether Cloudflare's runtime passes a
zipped response through untouched. I confirmed it by hand (step 2
below). Running it locally uses the same runtime but skips Cloudflare's
CDN layer, so local testing isn't a full substitute.

## How to check it yourself

Node 22, pnpm 9, clean checkout:

```bash
pnpm install --offline --frozen-lockfile
pnpm run ensure-native-modules
pnpm exec turbo run test typecheck --filter=@bb/tunnel-client --filter=bb-plugin-connect --force
pnpm exec turbo run test --filter=@bb/app --force -- src/hooks/cache-owners/cache-owner-registry.test.ts src/hooks/queries/thread-queries.test.tsx src/components/secondary-panel/ThreadMetadataContent.test.tsx src/views/thread-detail/ThreadDetailSecondaryContent.test.tsx
pnpm exec turbo run typecheck --filter=@bb/app --force
```

Then open DevTools → Network and load a thread through bb connect:

1. The thread and timeline requests start together, and the thread isn't
requested twice.
2. The timeline response has `content-encoding: gzip` and a
`server-timing: bb_connect_origin` header.
3. The Connect host log prints timing (`originTtfbMs`, `originBodyMs`,
`responseBytes`, `contentEncoding`).
4. Nothing requests parent candidates until you click **Parent**. First
click shows "Loading threads…", then the list.
5. A root thread in a project with no other threads shows the **Parent:
None** row.

## Tests

All green. Tunnel client 6/6, connect plugin 65/65, four focused app
suites 28/28, plus typechecks. The full GitHub matrix passes — app,
server, integration, packages, checks, and macOS/Linux smoke.

The new tests are the interesting ones. They check that the timeline
request really does leave before the thread request comes back, that a
warm cache asks for a delta and merges the answer correctly, that the
duplicate thread request is skipped when fresh but still happens when
stale, and that zipped bytes cross the tunnel byte-for-byte identical.
albrand referenced this pull request in alb-hold/bb Sep 13, 2026
A thread keeps the PR of its branch forever, so a closed or merged one sat
in the banner indefinitely. On an unmanaged workspace that is worse than
untidy: 17 threads share ~/projects/meu-psi on branch dev, so every one of
them showed PR #4 - closed since - as if it were live work, and the thread
goes on to open more.

The banner now carries only an open or draft pull request. The uncommitted
section is untouched: the workspace's current state is what the banner is
for. A finished PR stays visible in the thread metadata panel, which is
where history belongs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
albrand referenced this pull request in alb-hold/bb Sep 14, 2026
A thread keeps the PR of its branch forever, so a closed or merged one sat
in the banner indefinitely. On an unmanaged workspace that is worse than
untidy: 17 threads share ~/projects/meu-psi on branch dev, so every one of
them showed PR #4 - closed since - as if it were live work, and the thread
goes on to open more.

The banner now carries only an open or draft pull request. The uncommitted
section is untouched: the workspace's current state is what the banner is
for. A finished PR stays visible in the thread metadata panel, which is
where history belongs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 478bbe1)
ymichael added a commit that referenced this pull request Oct 7, 2026
## Human comments

## What was wrong

Prompt Library searches kept the browse layout: a **Starred** section
ranked by fuzzy score, then a **Recent** section ranked by fuzzy score.
A starred prompt therefore beat a better match, prompts that start with
the query could be buried under weaker substring matches (`Deploy
later…` and `deploy the preview…` were 6th and 7th of 8 for `deploy`),
and starred prompts appeared twice (once per section). Separately, each
search inserted a "Searching prompts…" row above the list, so the list
jumped down and back on every keystroke.

## What changed

- `plugins/prompt-library/ranking.ts`: `rankByQuery` marks prefix
matches (case-insensitive, whitespace-collapsed) and sorts prefix
matches first, then fuzzy score, keeping input order (recency) for ties.
`compareRank` is exported for merging.
- `plugins/prompt-library/server.ts`: with a query, starred and history
matches are merged into one list ranked by prefix, then score, then
starred, then recency; history rows whose text is starred are dropped so
each prompt appears once. Browsing (empty query) is unchanged: starred,
then recent.
- `plugins/prompt-library/contract.ts`: **`search` output changed** from
`{ starred, recent }` to `{ prompts }`, each row with `kind: "starred" |
"recent"`. This also changes `bb prompts search --json`. Text output
follows the same order.
- `plugins/prompt-library/app.tsx`: while searching, section headers are
hidden and rows are keyed by prompt text, so the selection follows a
prompt when starring/unstarring moves it. The status row is gone:
results stay in place during a search, the search icon becomes a spinner
only for searches slower than 300 ms, and the first "Loading prompts…"
message renders inside the empty list.
- CLI help, the skill (`SKILL.md`) and `PLUGIN_OVERVIEW.md` describe the
ranking and the new output shape.

## How you verified

- `pnpm exec turbo run test typecheck lint --filter='...[origin/main]'`
(prompt library and bundled plugins), plus `turbo run test
--filter=@bb/web -- src/marketplace`.
- New tests:
- `server.test.ts`: one test checks the full order (prefix + starred,
prefix + newer, prefix + older, fuzzy + starred, fuzzy + newer, weak
fuzzy match), including a capitalised prefix and removal of the
duplicate starred row.
- `app.test.tsx`: no section headers while searching, and the selection
follows an unstarred prompt. This test fails without the text-keyed
rows.
- `app.test.tsx`: results stay in place with no status row while a
search is pending, and the spinner appears only after 300 ms. This test
fails against the old status row.
- Manual QA on a `pnpm start:worktree` instance with seeded history:
- Source CLI: `bb prompts search` checks ranking, project and thread
scopes, multi-word queries, extra whitespace, no-match queries, the flag
constraint, and star/unstar round trips. Unstarring moves a prompt from
#4 to #6 and re-starring moves it back.
- Headless Chrome: the popup shows one list without headers. With
Ctrl+S, the selection follows the moved row; Tab switches scope; Enter
inserts the prompt.
- A per-frame sampler ran over 166 frames of typing and deleting: the
list's top never moved, and the old status text never appeared.

> AGENT GENERATED

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

Co-authored-by: qa <qa@x>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
ymichael added a commit that referenced this pull request Oct 30, 2026
## Human comments

## What was wrong

Several cache owners wrote a server reply or the thread-detail bootstrap
straight into the React Query cache with `setQueryData` while a fetch of
the same key could still be in flight. React Query writes fetch results
without checking whether they're older than what's cached (query-core
`query.ts`), so the older fetch landed last and put stale data back.
Effects included: an environment or host status that stayed stale after
opening a thread, a plugin settings form resetting its draft to the old
value, a cancelled plugin install flipping back to running, new plugin
update jobs dropping out of the list, an `idle` app update phase
switching off the progress poll, and the AI service selection flickering
back. These are items #2–#10 from the direct cache write audit
(@thread:thr_fjr5x8dzaz). #1 (thread tabs) is fixed separately in #6611.

## What changed

Two fixes, depending on whether the data has a version field:

- **Environment and Host keep the newer copy by `updatedAt`** (#2, #3,
#10). The bootstrap ingest (`thread-detail-cache-owner.ts`), the rename
reply (`applyEnvironmentUpdateResult`), and the
`useEnvironment`/`useHosts` fetches all compare `updatedAt`. The
fetch-side check is what catches a fetch that started before the write
and lands after it. To make it testable, those fetches now live in
`environmentQueryOptions` and `hostsQueryOptions`. Host `status` comes
from live daemon sessions and a disconnect doesn't change `updatedAt`,
so on a tie the cached copy wins over the bootstrap. The bootstrap only
fills an empty cache or replaces strictly older data. Rename replies and
fetch results still win ties.
- **`setQueryDataOverInFlightFetch`** (`cache-effect-utils.ts`) for data
with no version (#4–#9): it writes the value, and if a fetch of the key
is in flight it cancels that fetch (the cache keeps the written value)
and refetches. With nothing in flight it's a plain write. Used by
`applyPluginSettingsView`, `applyPluginInstallJob`,
`applyPluginUpdateJob`, `hydrateAppUpdateStatus`,
`hydrateSystemVersionCache` and `writeCachedAiServices`, so every caller
gets the fix. Where the audit suggested an unconditional
cancel-write-invalidate (#7) or a cancel before the write (#8, #9), this
helper refetches only when a fetch was actually in flight. That avoids
an extra request on every write. Forced version and app-update checks
update the server's cache, so the trailing unforced refetch returns the
forced result.

No wire, CLI or plugin API changes.

Not covered here: audit items #11 (plugin safe mode) and #12 (prompt
history), both low risk. Also not covered: a lint rule restricting
direct `setQueryData` calls to the helper, which would stop new call
sites from bringing the race back.

## How you verified

- New tests use real `QueryClient`/`QueryObserver` instances: start a
fetch that resolves late with older data, do the write, let the fetch
land, then check that the newer value survives (and, for the helper,
that the trailing refetch runs).
- `thread-detail-cache-owner.test.ts` (new): late environment and
host-list fetches after a bootstrap, an older bootstrap over a newer
cache, and the equal-`updatedAt` host status case.
- `environment-workspace-cache-owner.test.ts`: a late environment fetch
after a rename, and an older rename reply over a newer cache.
- `in-flight-fetch-writes.test.ts` (new): one row per cache-owner write
for #4–#9, plus helper tests for the no-fetch case (no extra request)
and a fetch of an unobserved query.
- With each source change reverted, every race test failed on the stale
data and passed once the fix was restored.
- `pnpm exec turbo run typecheck lint test --filter=@bb/app`: passed
(647 files, 6515 tests; 0 lint errors, no new warnings). After rebasing
onto `0472409243` I re-ran the three changed test files (21 tests
passed).

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

> AGENT GENERATED

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant