Repository navigation
[codex] Bundle Claude Code SDK CLI support file - #4
Closed
SawyerHood wants to merge 1 commit into
Closed
SawyerHood wants to merge 1 commit into
SawyerHood wants to merge 1 commit into
Conversation
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.
15 tasks
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
@anthropic-ai/claude-agent-sdkcli.jsfallback support file beside the host-daemon Claude bridge bundle.bundle:checkso 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 executablecli.jswas 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.jsonpnpm exec turbo run bundle:check --filter=@bb/host-daemonpnpm exec turbo run typecheck --filter=@bb/host-daemonpnpm exec turbo run test --filter=@bb/host-daemonthread/startrepro returned aproviderThreadIdinstead of the native-binary error.