← reports

#3151 · ACP steering cancels the active prompt

Bug Medium Effort: Medium threads provider-acp open on GitHub 2026-09-05 · base 39636049b3d6

Verdict: REPRODUCED · Root-cause confidence: high

1. TL;DR

Sending a steer to an active ACP-backed thread cancels the provider’s current session/prompt request. BB then sends only the steer text in a second session/prompt, so an ACP agent can abandon unfinished work from the original request and respond only to the new message. A focused fake-agent test records both prompt requests and proves that session/cancel appears between them on trusted main. The same test failed identically in a second clean checkout.

2. Claims vs findings

Paraphrased claimStatusEvidence
An active steer interrupts the previous ACP request.VerifiedThe ACP request log contains session/cancel after the first prompt and before the steered prompt.
The agent may handle only the steered message.VerifiedThe existing fake-agent path leaves the first prompt incomplete when cancelled, while the second prompt receives only the steer input. Existing bridge coverage explicitly expects no completed response for a cancelled hanging prompt.
The behavior occurs on every attempt.Unverified frequencyBoth controlled runs reproduced deterministically, but no statistical or real-account frequency study was performed.
Desktop and mobile exhibit the same behavior.Verified by shared pathBoth clients submit through the same server/runtime ACP bridge; cancellation is performed inside the provider bridge, below either UI.

3. Environment

4. Minimal reproduction

  1. Check out the trusted base commit and prepare it:
    git checkout 39636049b3d6eefb3dd1601035675afa52f22b72
    pnpm install --frozen-lockfile --prefer-offline
    pnpm exec turbo run build
  2. Apply the test block below to packages/provider-bridge-acp/src/bridge/bridge.test.ts immediately before the stacked-steers test, or use the saved complete test file.
  3. Run only the regression test:
    pnpm exec turbo run test --filter=@bb/provider-bridge-acp -- src/bridge/bridge.test.ts -t 'preserves the active ACP prompt when steering'

Regression test

it("preserves the active ACP prompt when steering", async () => {
  const requestLog = join(workspaceDir, "steer-request-log.jsonl");
  const promptLog = join(workspaceDir, "steer-prompt-log.jsonl");
  const { providerThreadId } = await startThread({
    envVars: {
      FAKE_ACP_REQUEST_LOG: requestLog,
      FAKE_ACP_PROMPT_LOG: promptLog,
    },
  });
  const turnId = sendTurnRequest("turn/start", providerThreadId, {
    input: [{ type: "text", text: "slow first", mentions: [] }],
  });
  await waitForResponse(turnId);

  const steerId = sendTurnRequest("turn/steer", providerThreadId, {
    expectedTurnId: "turn-1",
    input: [{ type: "text", text: "steered", mentions: [] }],
  });
  await waitForResponse(steerId);
  await waitForTurnCompleted();

  expect(loggedPrompts(promptLog)).toEqual(["slow first", "steered"]);
  expect(
    loggedAcpRequests(requestLog).map((request) => request.method),
  ).not.toContain("session/cancel");
});

Expected vs actual

Expected: the ACP request sequence does not contain session/cancel.
Actual:   AssertionError: expected [ 'initialize', 'session/new', …(3) ]
          to not include 'session/cancel'
Result:   1 failed | 95 skipped

This is a behavioral wire test rather than a visual test, so no screenshot is required.

5. Verification

The reproduction was repeated by the same investigator in a fresh clone checked out detached at the same full SHA. After a separate frozen install, the same focused Turbo/Vitest command failed on the same session/cancel assertion with 1 failed | 95 skipped. No report claim was weakened or corrected after the second run.

6. Root cause

The turn/steer handler first appends the new input to queuedInputs, then immediately calls requestSteerCancel. That helper marks the live prompt cancelled and sends an ACP session/cancel notification. See the steer handler and the cancellation helper.

session.queuedInputs.push({ input: params.input, ... });
requestSteerCancel(session);

session.connection.notify("session/cancel", {
  sessionId: session.providerThreadId,
});

After the cancelled request resolves, the turn loop shifts the queued input and starts a new session/prompt carrying only that input. It does not combine the unfinished original input with the steer. See the prompt loop. The bridge also advertises steerMode: "queue" in its capabilities, but implements an interrupting cancel-then-prompt sequence. This behavior was deliberately introduced by merged PR #1477 because ACP v1 has no native mid-prompt injection.

7. Proposed fix (first principles)

Choose and document one ACP-specific contract. To preserve the active request, leave the steer in queuedInputs, wait for the current session/prompt to finish, and then submit the queued input as the next prompt without calling session/cancel. That matches the bridge’s advertised queue mode, but it means a hung ACP prompt cannot be interrupted by steering. Alternatively, retain hard-steer cancellation and expose that limitation clearly in the product. Selecting between responsiveness and preservation is a product/architecture decision, so no production patch should be made automatically.

8. Related issues and changes

9. Appendix

Commands run:

git fetch https://github.com/get-bb/bb.git refs/heads/main:refs/remotes/origin/main
pnpm install --frozen-lockfile --prefer-offline
pnpm exec turbo run build
pnpm exec turbo run test --filter=@bb/provider-bridge-acp -- src/bridge/bridge.test.ts -t 'preserves the active ACP prompt when steering'
git clone https://github.com/get-bb/bb.git <fresh-temporary-checkout>
git checkout --detach 39636049b3d6eefb3dd1601035675afa52f22b72
pnpm install --frozen-lockfile --prefer-offline
pnpm exec turbo run test --filter=@bb/provider-bridge-acp -- src/bridge/bridge.test.ts -t 'preserves the active ACP prompt when steering'

Untrusted-data note: issue text was used only as a claim to test. The external attachment was not opened, no issue-provided command or code was run, and no linked pull request branch was checked out.