#2974 · Embedded composer loses selected provider provenance
Verdict: REPRODUCED · Root-cause confidence: high
1. TL;DR
The embedded new-thread composer submits the provider that the user selected. However, its request omits the provider provenance field. The server then rejects that provider value and resolves the project default. Two clean tests produced the same result.
2. Claims vs findings
| Claim | Status | Evidence |
|---|---|---|
| The embedded composer can lose a selected provider. | Verified | The focused test selected a second provider. The request kept its identifier but omitted its provenance. |
| The server can replace that value with the project default. | Verified | The create policy discards a value when the request has a sources object without that field. |
| A plugin must repair the request before it forwards the request. | Verified | The exported component forwards the incomplete request through its callback. |
3. Environment
- Repository:
get-bb/bb. - Trusted base:
111af3bafb697942b55fbe60fdd47f6227234ffc. - System: macOS Darwin 25.6.0 on arm64.
- Node:
v22.22.3. Vitest:4.1.1. - The test used no server port, data directory, provider process, or account.
- Both checkouts completed the frozen install and the full Turbo build.
4. Minimal reproduction
- Check out the trusted base commit.
- Run
pnpm install --frozen-lockfile --prefer-offline. - Run
pnpm exec turbo run build. - Apply the focused test patch.
- From
apps/app, run this command:pnpm exec vitest run src/components/plugin/PluginNewThreadComposer.test.tsx -t 'marks a provider picked in an unseeded plugin composer as explicit'
Expected:
submitted[0].providerId === "claude-code" submitted[0].executionInputSources.providerId === "explicit"
Actual:
AssertionError: expected undefined to be 'explicit' - Expected: "explicit" + Received: undefined
The request contained providerId: "claude-code". Only its provenance field was absent.
Focused regression test
it("marks a provider picked in an unseeded plugin composer as explicit", async () => {
const submitted: NewThreadRequest[] = [];
render(
<MemoryRouter>
<PluginNewThreadComposer
draftKey="picked-provider"
defaultProjectId="proj_1"
initialPrompt="hello"
onSubmit={(request) => submitted.push(request)}
/>
</MemoryRouter>,
);
await waitFor(() => expect(latestPromptBoxProps().disabled).toBe(false));
await act(async () => {
latestPromptBoxProps().execution.provider.onChange("claude-code");
});
await waitFor(() => {
expect(latestPromptBoxProps().execution.provider.selectedId).toBe("claude-code");
});
await submit();
expect(submitted[0]?.providerId).toBe("claude-code");
expect(submitted[0]?.executionInputSources.providerId).toBe("explicit");
});
Verification
I repeated the test in a second clean worktree at the same trusted commit. The second worktree used a separate dependency tree. It produced the same failed assertion at the same test line. I made no report correction.
Logs: first run · second run.
5. Root cause
The exported component selects component-local state at PluginNewThreadComposer.tsx lines 54–63. This state prevents an embedded composer from changing global picker preferences.
The provenance builder computes a provider source at selection-state.ts lines 237–245. Its component-local return then omits that source at lines 278–284.
if (scope === "component-local") {
return {
...(modelSource ? { model: modelSource } : {}),
...(serviceTierSource ? { serviceTier: serviceTierSource } : {}),
...(reasoningLevelSource ? { reasoningLevel: reasoningLevelSource } : {}),
...(permissionModeSource ? { permissionMode: permissionModeSource } : {}),
};
}
The new-thread submit path forwards this incomplete object at NewThreadComposer.tsx lines 1097–1112. A seeded provider works because a separate seed overlay adds explicit provenance. A provider that the user selects after mount does not use that overlay.
The server policy discards any requested field without matching provenance at project-execution-defaults.ts lines 57–69. It then resolves the provider from the stored defaults at lines 81–100.
6. Proposed fix
Add explicit provider provenance in the exported plugin composer before it calls the plugin callback. Keep the component-local hook contract unchanged for existing-thread composers. Retain this focused test to protect the exported request contract.
7. Related issues
GitHub metadata showed no linked pull request or related issue.
8. Appendix
The investigation read issue data only as an untrusted claim. It did not run issue code or open an external issue link.
Commands used:
git fetch origin main git worktree add --detach <temporary-path> 111af3bafb697942b55fbe60fdd47f6227234ffc pnpm install --frozen-lockfile --prefer-offline pnpm exec turbo run build pnpm exec vitest run src/components/plugin/PluginNewThreadComposer.test.tsx -t 'marks a provider picked in an unseeded plugin composer as explicit' git blame -L 215,294 apps/app/src/hooks/thread-creation-options/selection-state.ts
No live process required cleanup. No port listened for either run.