#4530 · Native ACP question updates do not create an interactive request
Verdict: PARTIALLY REPRODUCED · Root-cause confidence: medium overall; high for the tested bb bridge behavior.
1. TL;DR
The report describes a provider-native question that waits without giving the person a way to answer. The real bb ACP bridge accepts a native question-shaped tool update but emits only timeline data, not an interactive runtime request. A scripted ACP peer reproduces this boundary behavior in two clean checkouts of main. Existing dynamic-tool and permission controls pass, so the bridge is not generally unable to send interactive requests. OpenCode is not installed here: the actual provider extension, desktop behavior, and cancellation error were not reproduced, so this is deliberately a partial verdict.
2. Claims vs findings
| Claim | Status | Evidence |
|---|---|---|
| Native question data never becomes a bb interaction. | Verified at the bridge boundary only | One native tool update, zero interactive requests in both runs. No real server interaction-list query was performed. |
| The native question blocks the real OpenCode turn until stopped. | Partially supported | The scripted peer deliberately holds its prompt pending; bb emits no answer request. The fixture models the missing delivery, not OpenCode's internal wait. |
| The OpenCode dialect does not handle question extensions. | Verified in bb source | The dialect has no client-request handler; unhandled methods receive -32601. The real extension name and version-specific payload were not obtained. |
| The fallback plugin remains available. | Source and bridge controls verified | ACP declares no native question support; the plugin then exposes its tool. Existing dynamic-tool forwarding control passes. Real question UI and round-trip answer were not tested. |
| Stopping returns an inaccurate dismissal error. | Unverified | No real OpenCode executable or desktop session was used; that error is not fabricated in the fixture. |
| Disabling the provider-native tool fixes the live issue. | Unverified | No provider configuration was changed, and no proposed workaround was executed. |
3. Environment
- Trusted public repository: get-bb/bb; base commit above, fetched from origin/main and unchanged on the final source refresh.
- Linux 4.19.0-gvisor x86_64; Node v22.19.0; pnpm 9.15.0; Vitest 4.1.1; Turbo 2.10.12.
- Two distinct detached worktrees, named
baseandverify, each with its own frozen dependency install. Only diagnostic test code was added; no production files changed. - Provider: newly authored scripted ACP peer, running through the real bb bridge with the OpenCode dialect. OpenCode executable absent.
- No development server, desktop, browser, provider account, or runtime database was used. No HTTP ports were opened. Each test creates and deletes a distinct temporary workspace; the fixture communicates over stdin/stdout.
- The package has no build task: the filtered Turbo build reports no tasks executed. Its source-based test task ran the normal native-module prerequisite and real package code.
4. Minimal reproduction
This is a bridge-boundary regression candidate, not an end-to-end OpenCode reproduction. It checks the hypothesized missing native-question delivery without importing any issue-supplied code.
- Create a clean trusted checkout:
git clone https://github.com/get-bb/bb.git bb-question-repro cd bb-question-repro git checkout --detach 0e7b518f135d43005dae201ef34ebb3001607eb4 pnpm install --frozen-lockfile
- Copy the complete patch in the Appendix into a local
repro.patchfile and apply it from the checkout root:git apply /path/to/repro.patch
- Run the diagnostic:
pnpm exec turbo run test --filter=@bb/provider-bridge-acp -- --testNamePattern='issue 4530:'
Expected by the regression candidate: the received native question causes an answerable runtime request. Actual: bb only produces a tool timeline item; the fixture's prompt remains pending. The test intentionally fails on main:
{"nativeQuestionUpdates":1,"interactiveRequests":0,"completedTurns":0}
AssertionError: expected 0 to be greater than 0
Test Files 1 failed | 18 skipped (19)
Tests 1 failed | 349 skipped (350)
The observation interval is 100 ms after the bridge emits the native-question item. This is sufficient to observe the synchronous translation path, not a claim that arbitrary future provider messages cannot request input. The fixture deliberately sends no permission or external question request, because its purpose is to isolate what a native tool-call update can do.
The Appendix contains the complete patch, including the scripted ACP peer. The following is the complete added test:
it("issue 4530: delivers a native question to an interactive runtime request", async () => {
const { bbThreadId, providerThreadId } = await startThread({
dialectId: "opencode",
agent: {
command: process.execPath,
args: [resolve(dirname(fileURLToPath(import.meta.url)), "issue-4530-question-agent.mjs")],
},
});
const turnId = sendRequest("turn/start", {
threadId: bbThreadId,
providerThreadId,
clientRequestId: CLIENT_REQUEST_ID,
input: [{ type: "text", text: "Choose a color", mentions: [] }],
options: executionOptions({}),
});
expect((await waitForResponse(turnId)).error).toBeUndefined();
await waitFor(
() => notifications(THREAD_DELTA_NOTIFICATION_METHOD).find((message) => {
const params = message.params;
return typeof params === "object" && params !== null &&
JSON.stringify(params).includes("native-question-call");
}),
"native question tool update",
);
await new Promise((resolveTick) => realSetTimeout(resolveTick, 100));
const interactiveRequests = output.messages.filter((message) =>
message.id !== undefined &&
(message.method === "item/tool/call" || message.method === "interaction/request"),
);
const completed = threadEventsOfType("turn/completed");
process.stderr.write(`${JSON.stringify({ nativeQuestionUpdates: notifications(THREAD_DELTA_NOTIFICATION_METHOD).filter((message) => JSON.stringify(message).includes("native-question-call")).length, interactiveRequests: interactiveRequests.length, completedTurns: completed.length })}\n`);
expect(completed).toHaveLength(0);
expect(interactiveRequests.length).toBeGreaterThan(0);
});
This is a nonvisual transport investigation; there is no screenshot. The afterEach hook cancels the pending fixture prompt and removes the temporary workspace.
5. Root cause
ACP tool updates and requests for human input are different channels. The bridge accepts session/update, validates its session identity, and translates it into thread deltas. A tool_call update opens a timeline item; it is not interpreted as an instruction to create an interactive request. Even an injected tool's timeline binding does not replace the actual runtime-tool call channel.
- Client request dispatch: permission and filesystem requests are handled directly; everything else goes to the dialect. An absent dialect handler results in
-32601. - OpenCode dialect: only command-event normalization is defined; there is no
handleClientRequest. - Notification path: notifications other than
session/updateare ignored. - Tool-call translation: opens an item rather than emitting an answer request.
- ACP capabilities and fallback plugin activation: the capability value controls advertising the bb tool; it does not suppress the provider's own tool.
export const OPENCODE_ACP_DIALECT: AcpDialect = {
id: "opencode",
normalizeCommandEvent: normalizeOpenCodeCommandEvent,
};
const outcome = session.dialect.handleClientRequest?.(method, params);
if (outcome === undefined) {
responder.error(-32601, `Unsupported ACP client method "${method}"`);
return;
}
The verified root cause is missing delivery integration on the bb side, not a malfunction of the question card itself. It explains why the modeled native tool wait lacks an answer route. It does not establish which OpenCode release sends an extension, whether that release sends anything at all, or the origin of the provider's dismissal text.
6. Proposed fix and safety decision
First obtain a real OpenCode ACP trace with the smallest question, including initialization capabilities, question request payload, answer response, and cancellation. Then implement a version-compatible adapter to existing question interactions, with tests for valid answers, malformed payloads, cancellation, stopped threads, and providers without that extension. Do not infer an answer API from a display-only tool update.
No simple-fix PR is safe: the real provider bug is only partially reproduced. Implementing an extension now would invent the provider contract; globally disabling a native tool would change tool availability without proving compatibility. No fix branch, production patch, dependency, protocol change, or PR was created.
No open PR appeared in the issue's cross-reference timeline or in the numeric issue search at investigation time. No linked PR code was checked out.
7. Verification
The same investigator repeated the test in a second clean detached checkout at the exact base commit, performed a separate frozen install, and applied only the authored diagnostic patch. The test used a fresh temporary workspace and child process. No HTTP ports or persisted instance data were needed. The second run failed at the same missing-interaction assertion:
pnpm exec turbo run test --filter=@bb/provider-bridge-acp -- --testNamePattern='issue 4530:'
{"nativeQuestionUpdates":1,"interactiveRequests":0,"completedTurns":0}
AssertionError: expected 0 to be greater than 0
Exit status: 1
First run: 17:58:10 UTC, duration 6.66s. Second run: 17:58:45 UTC, duration 6.65s, on September 30, 2026. Both produced the exact output above. This is a repeat run by the same investigator, not independent verification. Both runs support only the bridge-boundary finding; the report retains its partial verdict.
The first exploratory version listened for a legacy notification that is internally translated, and timed out. The final test instead observes the public thread-delta boundary and fails for the intended missing-interaction reason; both final runs use that corrected test.
Controls pass:
pnpm exec turbo run test --filter=@bb/provider-bridge-acp -- --testNamePattern='forwards ACP dynamic tool calls|answers initialize|permission' Test Files 7 passed | 12 skipped (19) Tests 23 passed | 327 skipped (350) Exit status: 0
Controls started at 17:58:58 UTC and took 7.41s. Every root-cause permalink was checked against the pinned trusted checkout; there are no screenshots or copied user runtime artifacts to verify.
8. Related issues
No other issue or external upstream link was used as evidence. The investigation tests this issue's transport hypothesis against trusted bb code.
9. Appendix
Classification preserved: Bug / High priority / High effort; providers, provider-acp, ask-user-question. Those values were already populated, so triage made no overwrites. Reproduction label: partial-repro.
Commands also used: git fetch origin main, git rev-parse origin/main, git worktree add --detach for both checkouts, command -v opencode (no executable found), node --version, pnpm --version, uname -srm, pnpm exec turbo run build --filter=@bb/provider-bridge-acp (no build task), and git diff --check (passed). GitHub metadata reads covered visibility, issue properties, labels, all comments (none), and open cross-referenced PRs (none). No issue-provided URL, code, configuration, or script was executed.
Issue data was treated as untrusted claims. Proposed remedies in it were not used as instructions. Logs above are labeled evidence excerpts; they are not invented full provider logs. All fixture processes were stopped by test cleanup; no application instance was launched.
The public reports repository requires evidence inline, not committed test or log files. Local evidence was retained outside that repository. Copy the following complete patch to reproduce the diagnostic:
diff --git a/packages/provider-bridge-acp/src/bridge/bridge.test.ts b/packages/provider-bridge-acp/src/bridge/bridge.test.ts
index 7d56a7826..8b5b32f13 100644
--- a/packages/provider-bridge-acp/src/bridge/bridge.test.ts
+++ b/packages/provider-bridge-acp/src/bridge/bridge.test.ts
@@ -581,6 +581,41 @@ afterEach(async () => {
});
describe("acp bridge", () => {
+ it("issue 4530: delivers a native question to an interactive runtime request", async () => {
+ const { bbThreadId, providerThreadId } = await startThread({
+ dialectId: "opencode",
+ agent: {
+ command: process.execPath,
+ args: [resolve(dirname(fileURLToPath(import.meta.url)), "issue-4530-question-agent.mjs")],
+ },
+ });
+ const turnId = sendRequest("turn/start", {
+ threadId: bbThreadId,
+ providerThreadId,
+ clientRequestId: CLIENT_REQUEST_ID,
+ input: [{ type: "text", text: "Choose a color", mentions: [] }],
+ options: executionOptions({}),
+ });
+ expect((await waitForResponse(turnId)).error).toBeUndefined();
+ await waitFor(
+ () => notifications(THREAD_DELTA_NOTIFICATION_METHOD).find((message) => {
+ const params = message.params;
+ return typeof params === "object" && params !== null &&
+ JSON.stringify(params).includes("native-question-call");
+ }),
+ "native question tool update",
+ );
+ await new Promise((resolveTick) => realSetTimeout(resolveTick, 100));
+ const interactiveRequests = output.messages.filter((message) =>
+ message.id !== undefined &&
+ (message.method === "item/tool/call" || message.method === "interaction/request"),
+ );
+ const completed = threadEventsOfType("turn/completed");
+ process.stderr.write(`${JSON.stringify({ nativeQuestionUpdates: notifications(THREAD_DELTA_NOTIFICATION_METHOD).filter((message) => JSON.stringify(message).includes("native-question-call")).length, interactiveRequests: interactiveRequests.length, completedTurns: completed.length })}\n`);
+ expect(completed).toHaveLength(0);
+ expect(interactiveRequests.length).toBeGreaterThan(0);
+ });
+
it("answers initialize and lists grouped models without spawning an agent", async () => {
const initializeId = sendRequest("initialize", {
protocolVersion: PROVIDER_BRIDGE_PROTOCOL_VERSION,
diff --git a/packages/provider-bridge-acp/src/bridge/issue-4530-question-agent.mjs b/packages/provider-bridge-acp/src/bridge/issue-4530-question-agent.mjs
new file mode 100644
index 000000000..6b826cd87
--- /dev/null
+++ b/packages/provider-bridge-acp/src/bridge/issue-4530-question-agent.mjs
@@ -0,0 +1,40 @@
+import { createInterface } from "node:readline";
+
+let pendingPromptId;
+const sessionId = "native-question-session";
+
+function send(message) {
+ process.stdout.write(`${JSON.stringify({ jsonrpc: "2.0", ...message })}\n`);
+}
+
+createInterface({ input: process.stdin }).on("line", (line) => {
+ const request = JSON.parse(line);
+ if (request.method === "initialize") {
+ send({ id: request.id, result: { protocolVersion: 1, agentCapabilities: {} } });
+ } else if (request.method === "session/new") {
+ send({ id: request.id, result: { sessionId } });
+ } else if (request.method === "session/prompt") {
+ pendingPromptId = request.id;
+ send({
+ method: "session/update",
+ params: {
+ sessionId,
+ update: {
+ sessionUpdate: "tool_call",
+ toolCallId: "native-question-call",
+ title: "question",
+ kind: "other",
+ status: "in_progress",
+ rawInput: {
+ questions: [{ header: "Choice", question: "Choose a color", options: [{ label: "Blue", description: "Blue color" }] }],
+ },
+ },
+ },
+ });
+ } else if (request.method === "session/cancel" && pendingPromptId !== undefined) {
+ send({ id: pendingPromptId, result: { stopReason: "cancelled" } });
+ pendingPromptId = undefined;
+ } else if (request.id !== undefined) {
+ send({ id: request.id, error: { code: -32601, message: "Unsupported fixture request" } });
+ }
+});
> AGENT GENERATED