#3207 · Project reads fail before provider registration settles
Verdict: REPRODUCED · Root-cause confidence: high
1. TL;DR
During provider-plugin startup, the project list and sidebar bootstrap routes answer immediately even though their response builder needs the provider registry to choose a default provider for projects without stored execution defaults. A deferred empty registry therefore makes both reads return HTTP 409. Once the same registry is populated and marked settled, both routes return HTTP 200. The server deliberately starts listening before plugin startup finishes, so the route-level omission creates a real cold-start race.
2. Claims vs findings
| Claim | Status | Evidence |
|---|---|---|
| Sidebar bootstrap can fail while provider registration is pending. | Verified | The focused test held registration open and received 409 from /api/v1/sidebar-bootstrap. |
| The project list with threads follows the same failing branch. | Verified | The parallel request to /api/v1/projects?include=threads&includePersonal=true also received 409. |
| The failure is tied to projects without stored execution defaults. | Verified | The in-memory database starts with no stored defaults; the response builder passes null into the creation-default resolver, which then requires a provider from the empty registry. |
| The endpoints recover after registration settles. | Verified | After registering the first-party providers and marking the registry settled, fresh requests to both endpoints returned 200. |
| The read routes wait on the existing registration barrier. | Refuted | Both handlers are synchronous and call their response builders without invoking whenRegistrationsSettled(). |
3. Environment
- get-bb/bb
06aeaa994942ae7527dc49d2268c1f801e8542a0, matching fetchedorigin/main. - macOS 26.6.1, Darwin 25.6.0 arm64, Node 22.22.3, pnpm 9.15.0.
- Repository Vitest server harness with an in-memory SQLite database and a deferred provider registry.
- No network listener, provider process, account, real BB server, persistent data directory, or user data was used.
4. Minimal reproduction
- Check out
06aeaa994942ae7527dc49d2268c1f801e8542a0, runpnpm install --frozen-lockfile --prefer-offline, thenpnpm exec turbo run build. - Add the focused test below to
apps/server/test/public/public-threads.defaults.test.ts, using the imports in the linked artifact. - Run
pnpm exec turbo run test --filter=@bb/server -- --run test/public/public-threads.defaults.test.ts.
Expected: both initial reads remain pending until provider registration settles, then return 200.
Actual: both initial reads return 409; fresh requests return 200 after settlement.
- Expected
+ Received
{
"afterSettled": [200, 200],
- "beforeSettled": "pending",
+ "beforeSettled": [409, 409],
"firstStatuses": [
- 200,
- 200,
+ 409,
+ 409,
],
}
Test Files 1 failed (1)
Tests 1 failed | 13 passed (14)
Focused regression test source
const registry = createProviderRegistryService({
deferRegistrationsSettled: true,
});
harness.deps.providerRegistry = registry;
const responsesPromise = Promise.all([
harness.app.request("/api/v1/sidebar-bootstrap"),
harness.app.request(
"/api/v1/projects?include=threads&includePersonal=true",
),
]);
const earlyResult = await Promise.race([
responsesPromise.then((responses) =>
responses.map((response) => response.status),
),
new Promise<"pending">((resolve) =>
setTimeout(() => resolve("pending"), 20),
),
]);
await registerFirstPartyProviders(registry);
registry.markRegistrationsSettled();
const firstStatuses =
earlyResult === "pending"
? (await responsesPromise).map((response) => response.status)
: earlyResult;
const settledResponses = await Promise.all([
harness.app.request("/api/v1/sidebar-bootstrap"),
harness.app.request(
"/api/v1/projects?include=threads&includePersonal=true",
),
]);
expect({
beforeSettled: earlyResult,
firstStatuses,
afterSettled: settledResponses.map((response) => response.status),
}).toEqual({
beforeSettled: "pending",
firstStatuses: [200, 200],
afterSettled: [200, 200],
});Repro file: project-provider-readiness.test.ts.
5. Verification
The same agent created a second clean detached checkout at 06aeaa994942ae7527dc49d2268c1f801e8542a0, repeated the frozen install and full Turbo build, added only the authored test, and ran the same focused command. It produced the same before-settlement [409, 409] and after-settlement [200, 200] result. No report claim required correction.
6. Root cause
start-server.ts lines 222–249 starts the HTTP listener before plugin startup, then marks provider registration settled only when plugin startup finishes. Requests can therefore validly arrive while the registry is deferred and empty.
projects.ts lines 239–253 loads stored defaults and resolves creation defaults for every project. When a project has no row, thread-default-policy.ts lines 26–45 requires an available provider and throws HTTP 409 if the registry is empty.
projects.ts lines 341–359 registers synchronous handlers for the thread-inclusive project list and sidebar bootstrap, so neither waits before entering that resolver. The barrier already exists at provider-registry.ts lines 375–382. The missing awaits, rather than registration itself, explain why the same requests switch from 409 to 200 immediately after settlement.
7. Proposed fix (first principles)
Make the sidebar-bootstrap handler asynchronous and await deps.providerRegistry.whenRegistrationsSettled() before building its response. Do the same in the project-list handler only for the thread-inclusive branch, because the lean project response does not consult provider defaults. Keep the existing response contract and add the focused test to lock in pending-before-settlement and 200-after-registration behavior.
8. Related issues
Issue #2533 tracks the user-visible terminal sidebar state and is still open. Pull request #2767 is linked to that client-side issue and remains open; no open pull request links to #3207, and no other issue containing the trusted error code was found.
9. Appendix
Commands run
git fetch origin main git rev-parse origin/main pnpm install --frozen-lockfile --prefer-offline pnpm exec turbo run build pnpm exec turbo run test --filter=@bb/server -- --run test/public/public-threads.defaults.test.ts git blame -L 330,370 06aeaa994942ae7527dc49d2268c1f801e8542a0 -- apps/server/src/routes/projects.ts git blame -L 360,385 06aeaa994942ae7527dc49d2268c1f801e8542a0 -- apps/server/src/services/providers/provider-registry.ts git blame -L 220,252 06aeaa994942ae7527dc49d2268c1f801e8542a0 -- apps/server/src/start-server.ts
Trust handling
The issue title, body, comments, links, code blocks, and suggestions were treated as untrusted claims. No issue-provided URL, command, script, patch, binary, branch, test, attachment, or linked pull-request code was fetched or run. All executed application code came from the trusted main SHA or from the focused test authored from repository evidence.
Limits
The reproduction exercises the production Hono routes and real in-memory database migration through the repository harness rather than launching the packaged desktop shell. This directly verifies the server race and status transition; the separate client rendering symptom was not re-tested.