#4678 · Native Codex questions stop at the provider bridge
Verdict: PARTIALLY REPRODUCED · Root-cause confidence: high (provider boundary only)
1. TL;DR
The report describes Codex offering choices as text rather than a selectable BB question card. On current main, the Codex provider explicitly disables the native default-mode input feature. A supervised app-server fixture sending a native question also fails to produce any BB interaction request: the decoder does not support that method, and the bridge rejects unknown requests. The answer path separately accepts approval outcomes only, so enabling the feature or decoding the request alone is insufficient. Two clean trusted checkouts show the same missing interaction, but no live Codex/Claude UI comparison or macOS release reproduction was performed; the visual symptom remains unverified.
2. Claims vs findings
| Claim | Status | Evidence |
|---|---|---|
| Default-mode native Codex questions are disabled by BB. | Verified in source | The trusted configuration builder writes the feature flag as false at line 613. |
| Native questions cannot reach a BB choice interaction. | Verified at the bridge boundary | Both synthesized blocking and nonblocking requests produce no interaction/request in both clean runs. |
| Question answers need changes beyond decoding. | Verified in source; not exercised end-to-end | The bridge parses the returned result with approvalInteractionOutcomeSchema and its encoder accepts ApprovalInteractionOutcome. |
| A normal Codex thread displays plain text while Claude displays a card. | Unverified | No real provider turn, UI screenshot, or comparison on the reported release was run. |
| The external prototype and its reported test results resolve the issue. | Unverified | No external fork code, linked commit, or linked PR branch was fetched or executed. |
3. Environment
- Repository: public get-bb/bb; both source checkouts pinned to the full main commit above.
- OS: Linux x86_64, kernel 4.19.0-gvisor; Node v22.19.0; pnpm 9.15.0.
- Provider: trusted Codex bridge with its existing supervised fake app-server, launched as a Node child over JSON-RPC stdio. No Codex CLI or Claude Code installation was exercised.
- No BB server, host daemon, browser, TCP port, user runtime database, credentials, or enrolled-machine data was used. Each case creates and deletes a fresh temporary workspace and fixture.
- Frozen install and full Turbo build succeeded in both checkouts: 63/63 tasks. The second build reused 61 cached tasks.
4. Minimal reproduction
This is a provider-boundary reproduction, not a visual BB reproduction. It executes the real bridge and the repository's existing child-process fixture. The test expectation is the existing canonical user_question interaction contract, not a source-code string check.
- Clone trusted main, pin the commit, install and build:
git clone --single-branch --branch main https://github.com/get-bb/bb.git bb-4678-base cd bb-4678-base git checkout --detach 61a56bacdf828d7ea9c0447bfafa9d072f8ada09 pnpm install --frozen-lockfile --prefer-offline pnpm exec turbo run build
- Create
plugins/provider-codex/src/bridge/bridge.issue-4678.test.tswith the complete test below. - Run the focused test through Turbo:
pnpm exec turbo run test --filter=bb-plugin-provider-codex -- src/bridge/bridge.issue-4678.test.ts
- Expected: the bridge sends a canonical
interaction/requestcontainingpayload.kind = user_question, before the turn settles. Actual: the bridge settles the scripted turn without emitting that interaction; both assertions fail.
Complete reproduction test
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, expect, it, vi } from "vitest";
import { experimental_createBridgeJsonRpcTestHarness as createBridgeJsonRpcTestHarness } from "@get-bb/plugin-sdk/provider-bridge/testing";
import { handleLine } from "./bridge.js";
import {
FULL_ACCESS_SESSION_OPTIONS,
stubFakeCodexAppServer,
} from "./fake-codex-app-server-harness.js";
let workspaceDir: string;
let harness: ReturnType<typeof createBridgeJsonRpcTestHarness>;
const threadId = "thr_repro_4678";
beforeEach(() => {
workspaceDir = mkdtempSync(join(tmpdir(), "bb-question-boundary-"));
harness = createBridgeJsonRpcTestHarness(handleLine);
});
afterEach(async () => {
try {
harness.sendRequest(99, "thread/stop", {
threadId,
providerThreadId: threadId,
intent: "release",
activeTurnId: null,
});
await harness.waitForResponse(99);
} finally {
harness.restore();
vi.unstubAllEnvs();
rmSync(workspaceDir, { recursive: true, force: true });
}
});
it.each([true, false])(
"delivers a native question to BB before completing the turn (isBlocking=%s)",
async (isBlocking) => {
const scriptPath = join(workspaceDir, "app-server-script.json");
writeFileSync(
scriptPath,
JSON.stringify({
turns: [
[
{
method: "turn/started",
params: {
threadId,
turn: { id: "turn-question", status: "inProgress" },
},
},
{
kind: "request",
method: "item/tool/requestUserInput",
params: {
threadId,
turnId: "turn-question",
itemId: "question-item",
isBlocking,
autoResolutionMs: null,
questions: [
{
id: "drink",
header: "Drink",
question: "Which drink do you prefer?",
isOther: true,
isSecret: false,
options: [
{ label: "Coffee", description: "Choose coffee." },
{ label: "Tea", description: "Choose tea." },
],
},
],
},
},
{
method: "turn/completed",
params: {
threadId,
turn: { id: "turn-question", status: "completed" },
},
},
],
],
}),
);
stubFakeCodexAppServer(scriptPath);
harness.sendRequest(1, "thread/start", {
threadId,
cwd: workspaceDir,
instructionMode: "append",
options: { ...FULL_ACCESS_SESSION_OPTIONS },
});
const started = await harness.waitForResponse(1);
expect(started.error).toBeUndefined();
harness.sendRequest(2, "turn/start", {
threadId,
providerThreadId: threadId,
clientRequestId: "repro-native-question",
input: [
{
type: "text",
text: "Ask a structured drink question.",
mentions: [],
},
],
options: { ...FULL_ACCESS_SESSION_OPTIONS },
});
const deadline = Date.now() + 5_000;
while (
!harness.hasResponse(2) &&
!harness.messages.some(
(message) => message.method === "interaction/request",
)
) {
if (Date.now() > deadline) {
throw new Error(
"The bridge neither delivered a question nor settled the turn",
);
}
await new Promise((resolve) => setTimeout(resolve, 10));
}
const interaction = harness.messages.find(
(message) => message.method === "interaction/request",
);
expect(interaction).toMatchObject({
params: { payload: { kind: "user_question" } },
});
expect(harness.hasResponse(2)).toBe(false);
},
15_000,
);
Actual output (same failure in both runs)
AssertionError: expected undefined to match object { Object (params) }
- Expected:
{
"params": {
"payload": {
"kind": "user_question",
},
},
}
+ Received:
undefined
Test Files 1 failed (1)
Tests 2 failed (2)
Focused command exit code: 1 in both runs. The test uses synthesized question requests with isBlocking=true and isBlocking=false, not a recording from a real Codex version. The production rejection occurs before any question-parameter schema is examined. This demonstrates lack of method support; it does not validate the complete current Codex wire schema or UI behavior.
No screenshot is presented: the visible user symptom was not reproduced. No test files or raw logs are committed to this public reports repository; the full repeatable test and relevant output are embedded here.
5. Root cause
A. Default-mode availability is explicitly disabled
config["features.default_mode_request_user_input"] = false;
Even when the provider supports a native question tool, BB's configuration asks for it to be unavailable in default mode. The exact model fallback to text is not proven by this experiment.
B. The native method is not decoded
Interactive request decoder handles command, file-change, and permission approval methods. All other methods return null. Bridge rejection path then returns METHOD_NOT_FOUND rather than forwarding a question.
default:
return null;
if (decoded === null) {
responder.error(
BRIDGE_JSON_RPC_ERRORS.METHOD_NOT_FOUND,
`Unhandled codex request "${method}"`,
);
return;
}
The supervised fixture continues after receiving a response (including an error). Therefore the test sees the turn settle with no BB question request. That fixture behavior is not evidence that real Codex would silently continue after this error.
C. The answer path still assumes approval outcomes
Runtime result handler validates every decoded interactive response as an approval. Response encoder likewise accepts approvals only.
const outcome = approvalInteractionOutcomeSchema.parse({
payload: request.payload,
resolution: result,
});
responder.result(buildCodexInteractiveResponse(outcome));
This is a source-backed limitation, not a newly executed native-answer failure. A complete repair must validate question answers separately and return them to the original waiting provider request.
D. BB still supplies the fallback tool
Codex capability declaration marks native questions unsupported. Fallback tool selection supplies the existing BB question tool when this capability is false. Thus the findings do not mean that every way of asking a question in a Codex thread is broken. Claude capability declaration differs, but its live card rendering was not tested.
6. Proposed fix and automatic-fix decision
A complete native path needs boundary validation and translation for provider questions, question-specific answer encoding, configuration enablement, and capability/fallback consistency. Exercise option selection, free text, malformed inputs, cancellation, timeout and disconnect/reconnect behavior before advertising support. In particular, the policy for nonblocking requests and any persisted asynchronous questions needs an explicit lifecycle contract rather than assuming that an approval result handler can accept them.
No automatic fix PR: only the provider boundary was reproduced, and live UI/answer lifecycle behavior is still unverified. Existing open native-question PRs already overlap this work. This investigation supplies a failing boundary test rather than creating another overlapping, insufficiently verified implementation. No fix branch was created or pushed.
7. Static PR review
Only GitHub metadata and diffs were read as untrusted evidence. Neither branch was checked out or executed. Neither PR's closing-issue metadata links issue #4678, and issue #4678 had no connected/cross-referenced PR events at the final read.
#4044 — open, ready
Reviewed head fb8ccd75dc2c8a1282fee2201c6860bce4e146c3. The diff adds native question decoding and encoding, advertises native support, and adjusts question UI/tests.
- High — incomplete default-mode path: its diff does not change
plugins/provider-codex/src/session-params.ts:613, leaving the verified default-mode disablement in place. - High — answer routing incomplete: its diff does not change
plugins/provider-codex/src/bridge/bridge.ts:870, where the runtime answer is parsed as an approval. A new decoder alone cannot bypass that validation. - Fallback risk:
plugins/provider-codex/server.ts:43changes to native support while those remaining blockers exist; the trusted fallback selector would then suppress the fallback question tool.
Static verdict: partial implementation of the reproduced provider gap, not a complete default-mode/answer-path repair against this main baseline. Tests run: none from this PR.
#3917 — open, draft
Reviewed head 73adf537a7bf44517a63e782051ce7f5663535f8. Its diff enables default-mode input, decodes native questions, adds question-specific result handling and request-cancellation tracking, and expands asynchronous-question support across runtime, persistence, thread projection and host contracts.
Static verdict: covers the identified configuration, decoding, and answer-routing blockers in its diff. Correctness of real UI and lifecycle behavior remains unverified. The cross-subsystem, stored-data and protocol work is outside this rule's simple-fix gate. Tests run: none from this PR.
8. Related issues
- #3411 is an open broader plan-review workflow issue, not proof of default-mode choice handling.
- #4463 is an open fallback-answer persistence issue; this investigation tests missing native-question delivery before answering.
9. Verification
The same investigator repeated the experiment, not an independent investigator. A second initially clean detached worktree was created at the exact recorded trusted commit. It ran its own frozen install and full build, received only the investigator-authored test, and created fresh temporary fixtures for both cases. No TCP ports or runtime data directory were needed in either run.
git worktree add --detach ../bb-4678-verify 61a56bacdf828d7ea9c0447bfafa9d072f8ada09 cd ../bb-4678-verify git status --porcelain pnpm install --frozen-lockfile --prefer-offline pnpm exec turbo run build pnpm exec turbo run test --filter=bb-plugin-provider-codex -- src/bridge/bridge.issue-4678.test.ts
Initial status was empty. Second result: 2/2 native-question cases failed with the same missing interaction; exit 1. No correction to the limited provider finding was needed. The final verdict remains partial because this repeat does not establish the visual symptom or a real-provider answer round trip.
10. Appendix: checks and scope
pnpm exec turbo run test --filter=bb-plugin-provider-codex -- --exclude src/bridge/bridge.issue-4678.test.ts
Test Files 31 passed (31)
Tests 322 passed (322)
pnpm exec turbo run typecheck --filter=bb-plugin-provider-codex
Tasks: 5 successful, 5 total
git diff --check
exit 0
Existing provider tests pass when the intentionally failing new regression is excluded. Typecheck includes the new test and passes. The test's temporary source copies and raw build/test logs remain private local investigation artifacts, not public attachments.
Read-only preparation used repository visibility, issue type/field/label metadata, issue comments, connected-PR metadata, related-issue metadata, and PR metadata/diffs through GitHub APIs. Trusted-source inspection used Git, ripgrep and numbered source excerpts. No external fork or URL supplied by the issue was fetched. Issue prompts, scripts and instructions were treated as untrusted data and were not executed. The synthesized test prompt and fixtures were authored from the trusted bridge boundary; no linked patch or test was copied.
> AGENT GENERATED