Read ACP command exit codes from rawOutput instead of the status - #2131
Merged
Conversation
SawyerHood
marked this pull request as ready for review
August 21, 2026 03:32
SawyerHood
force-pushed
the
bb/fix-1529-shell-session-deleted-cwd
branch
from
August 21, 2026 14:45
e042d5a to
c331136
Compare
SawyerHood
marked this pull request as draft
August 21, 2026 14:54
The ACP translator turned a tool call's terminal status into an exit code: completed became 0, failed became 1. ACP has no exit-code field, and Cursor reports both a command that exited 1 and a command that never spawned (its persistent shell's cwd was deleted) as `status: "completed"`, so bb labelled both as exit code 0 and hid the failure from the timeline and the CLI. Read the agent's reported `rawOutput.exitCode` when it is an integer. Without one, a failed call still maps to 1 and a completed call carries no exit code rather than a fabricated 0. Re-record the acp-cursor `bridge→runtime.current.ndjson` lanes (`pnpm --filter @bb/provider-parity rerecord --provider acp-cursor`): the only delta-level change is `exitCode` on item closes. Command closes without a reported code drop the fabricated 0 (approval-deny, steer, web-search); the web-search `node -e` close now carries the reported 1; tool and fileChange closes no longer carry an exitCode the assembler ignored (subagent, turn-tools, user-question, web-search). Row-count pins are unchanged. Refs #1529 Co-Authored-By: Claude <noreply@anthropic.com>
SawyerHood
force-pushed
the
bb/fix-1529-shell-session-deleted-cwd
branch
from
August 21, 2026 16:41
c331136 to
06c27aa
Compare
SawyerHood
marked this pull request as ready for review
August 21, 2026 16:54
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What was wrong
Issue #1529 (report: https://get-bb.github.io/reports/issues/1529.html). The wedge itself lives in Cursor's CLI: its Shell tool re-spawns every command with the cwd persisted from the last run, Node's
spawnfails with ENOENT once that directory is deleted (git worktree remove), and Cursor's ACP adapter reports the call asstatus: "completed"with nocontentand norawOutput. bb cannot fix that, but bb made it invisible.plugins/provider-acp/src/delta-translation.tsderived the exit code from the ACP status alone (completed-> 0,failed-> 1). ACP has no exit-code field, and Cursor also reports a command that really exited non-zero asstatus: "completed"with the real code inrawOutput.exitCode. So the bb timeline andbb thread logshowed "exit code 0" both for commands that never ran and for commands that exited 1.What changed
plugins/provider-acp/src/wire.ts:acpToolCallRawOutputExitCodeSchemaparses{ exitCode: integer }out of the agent-definedrawOutputpayload at the boundary.packages/provider-bridge-protocol/recordings/acp-cursor/*/bridge→runtime.current.ndjson: re-recorded acp-cursor lanes (six cells); onlyexitCodeon item closes changed. Details under Rebase below.plugins/provider-acp/src/delta-translation.ts:toolCallClosenow gets its exit code fromextractAcpExitCode. A reported integerrawOutput.exitCodewins. Without one, afailedcall still maps to 1 (non-zero is all we know) and acompletedcall carries noexitCodeinstead of a fabricated 0.exitCodeis already optional on thecommandExecutionitem, the delta close, the thread-view exec lifecycle, the CLI formatter, and the app'sTerminalOutputBlock, so consumers render "unknown" by omitting the exit-code line.Behavior change for every ACP provider: a completed command without a reported exit code no longer shows
exit code 0; a completed command withrawOutput.exitCode: 1now renders as an error withexit 1. Translation only, nothing crosses the server/daemon wire, so noHOST_DAEMON_PROTOCOL_VERSIONbump. I did not take the report's optional step of mapping a result-less completed call tofailed: that guesses, and omitting the exit code is the honest signal.The wedge remains upstream (Cursor CLI,
cursor-agent2026.08.11). With this change the agent's own "returned no exit status" text is no longer contradicted by a bb row claiming success.How you verified
New tests in
plugins/provider-acp/src/delta-translation.test.ts(command exit codes), using the exacttool_call/tool_call_updateshapes recorded fromcursor-agent acp. Against the origin/main sources they fail:With the fix:
pnpm exec turbo run typecheck test --filter=bb-plugin-provider-acppasses (14 files, 186 tests). The existingtranslates execute tool calls into command executionsexpectation dropped itsexitCode: 0(no exit code was reported in that fixture).Manual check on my dev instance with a real
acp-cursorthread and the report's prompt (scratch repo + worktree,git worktree remove --force, thenecho hi; git status, thenpwdwith a working-directory override, thenfalse).bb thread log --format verboseafter the fix:The persisted items: the two wedged
echo hi; git statuscalls arestatus: "completed"with noexitCode;falseisexitCode: 1.Rebase
Rebased onto current main (
cf00cfe06) after #2136 (WS1a generic assembler) and #2179 (WS1b-acp: ACP bridge to grammar v3 with presentation) rewroteplugins/provider-acp/src/delta-translation.ts. What moved:extractAcpToolCallOutputText,buildAcpFileChanges, andclassifyAcpToolCallnow live inplugins/provider-acp/src/tool-classification.tsand return{ item, presentation }; the PR's conflict hunks that carried copies of those helpers were dropped, not re-added.toolCallClosestill derived the exit code from the ACP status on main (terminal ? { exitCode: status === "failed" ? 1 : 0 } : {}), so the bug was still present. The fix maps one-to-one:extractAcpExitCode(args.event, args.status)replaces theterminalflag, and the close spreadsexitCodeonly when one is known. The newclassified.item/classified.presentationfields and the injected-tool binding cleanup from [stacked on #2178] WS1b-acp: ACP bridge to grammar v3 with presentation #2179 are kept as-is.wire.ts(acpToolCallRawOutputExitCodeSchema) applied without conflict.translates execute tool calls into command executionskeeps main's newpresentationblock and dropsexitCode: 0, as before.Re-verified on the new base. With
git checkout origin/main -- delta-translation.ts wire.ts,vitest run src/delta-translation.test.ts: 5 failed / 32 passed (expected +0 to be 1,expected +0 to be undefined,expected 1 to be 127,expected +0 to be undefined, plus the fixture diff). With the fix restored, from the committed tree:pnpm exec turbo run typecheck test --filter=bb-plugin-provider-acp --force→Tasks: 7 successful, 7 total;Test Files 15 passed (15),Tests 201 passed (201)(includes the recorded ACP conformance cells).Fixes #1529
Rebase onto
75d6fc4d4and acp-cursor re-recordRebased onto current main (
75d6fc4d4, five commits ahead of the previous basecf00cfe06: #2202, #2201, #2147, #2150, #2120). None touchplugins/provider-acp/src/{delta-translation,wire}.ts; the rebase applied clean and the fix is unchanged.The verifier found that the fix changes what the ACP bridge emits for committed acp-cursor recordings, so
pnpm exec turbo run test --filter=@bb/provider-parity --forcefailed onacp-cursor/{approval-deny,steer,web-search}(3 failed / 40 passed on the rebased tree before this change; CI had only been green through a turbo cache hit). Ranpnpm --filter @bb/provider-parity rerecord --provider acp-cursor(planning with this checkout's assembler; no--plan-withneeded) and committed the resultingbridge→runtime.current.ndjsonlanes. Checked every changed line at the delta level (parsed old vs new, removedexitCode, deep-equal): the ONLY change isexitCodeonitem.closedeltas. Line counts are unchanged, row-count pins are unchanged,parity-allowlist.jsonis untouched.approval-deny:touch ~/bb-recording-outside.txtclose,exitCode: 0-> absent (Cursor reported norawOutput).steer: the fourthsleep 2close,exitCode: 0-> absent (the steer interrupted it before acompletedupdate; the other four keep their reported0).web-search: thenode -e ...close,exitCode: 0->1(Cursor reportedrawOutput.exitCode: 1understatus: "completed"; this is the Persistent shell session wedges silently when its stored cwd is deleted (git worktree remove); all subsequent spawns return no exit status #1529 shape).subagent,turn-tools,user-question,web-search:tool/fileChangecloses drop theexitCode: 0the old code attached to every terminal close. The assembler only readsexitCodeon commands, which is why those cells passed before; the re-recorded lanes now match the wire.forkre-recorded identically except for the recording machine's checkout path inside the "does not advertise session/fork support" error string; left as committed.Re-verified from the committed tree (
git status --porcelainempty). Fail-before: withgit checkout origin/main -- delta-translation.ts wire.ts,vitest run src/delta-translation.test.ts-> 5 failed / 32 passed (expected +0 to be 1,expected +0 to be undefined,expected 1 to be 127,expected +0 to be undefined, plus the fixture diff). Pass-after: 37 passed.pnpm exec turbo run typecheck test --filter=bb-plugin-provider-acp --filter=@bb/provider-bridge-protocol --filter=@bb/provider-parity --force->Tasks: 11 successful, 11 total(Cached: 0 cached);bb-plugin-provider-acp15 files / 201 tests,@bb/provider-bridge-protocol15 files / 217 tests,@bb/provider-parity1 file / 43 tests (all 43 cells reproduce their recordings).Independent verification
Checked out
bb/fix-1529-shell-session-deleted-cwd(e042d5a, one commit on top of 2ff8598) in a separate worktree;git merge-tree --write-tree origin/main HEADmerges clean against current main (703213a). PR diff is limited toplugins/provider-acp/src/{delta-translation.ts,wire.ts,delta-translation.test.ts}; no wire change between server and daemon, so noHOST_DAEMON_PROTOCOL_VERSIONbump needed. Consumers ofexitCode(delta-assemblerclose fields,thread-view/exec-lifecycle.ts,format-timeline-text.ts,TerminalOutputBlock) all treat it as optional/nullable; a missing code falls back to the item status, a non-zero code maps toerror.Fail-before / pass-after (
pnpm exec vitest run src/delta-translation.test.tsinplugins/provider-acp):git checkout origin/main -- delta-translation.ts wire.ts: 5 failed / 20 passed.expected +0 to be 1,expected +0 to be undefined,expected 1 to be 127,expected +0 to be undefined, plustranslates execute tool calls into command executions(fixture no longer expects the fabricatedexitCode: 0).pnpm exec turbo run typecheck test --filter=bb-plugin-provider-acp --force: 7 tasks successful, 14 test files / 186 tests passed.gh pr checks 2131: all checks pass (Checks, Package Smoke x2, Tests app-1/2/3, integration, packages, server, version check).Repro on the fixed branch (own dev instance, scratch repo + worktree, real
acp-cursorthread thr_eenfcb5cv5 with the report's prompt plus a finalfalse):bb thread log --format verboseshowsRan echo hi; git statuswith no output and no exit-code line, andRan false (error) ... exit 1. Persisted items: seq 42{"command":"echo hi; git status","status":"completed"}(noexitCode), seq 54{"command":"false","status":"completed","exitCode":1}. On main the same log printedexit code 0for both. The upstream wedge itself still reproduces (Cursor returns "The shell command returned no exit status" for step 3 and heals after the working-directory override), which is outside bb's control.Residual risks: completed ACP commands from agents that do not report
rawOutput.exitCode(or use a different key) no longer showexit code 0; a Cursor call that never ran is still rendered as a completed row with no output rather than as failed. Merging closes #1529 although the wedge needs an upstream Cursor CLI fix; the body above says so.Independent verification (post-rebase)
Checked out
bb/fix-1529-shell-session-deleted-cwdat 06c27aa (one commit on top of 75d6fc4) in a fresh worktree. Since the rebase main gained only 85eec4d (#2210, iOS TestFlight CI; no overlap) and GitHub reports the PRMERGEABLE. The fix still targets the live path after today's grammar-v3 bridge stack:createAcpDeltaTranslatoris wired once inplugins/provider-acp/src/bridge/bridge.ts, andtoolCallCloseis the only place the ACP bridge emits a commandexitCode; no legacyevent-translation.tsremains. The delta/assembler contract is unchanged (exitCodealready optional inthread-delta.tsand applied only tocommandExecutionindelta-assembler.ts), so noHOST_DAEMON_PROTOCOL_VERSIONbump is needed.thread-viewprojects a missing code tonull(no exit line) and a non-zero one to anerrorrow.Recordings: parsed every changed line of the six re-recorded
acp-cursor/*/bridge→runtime.current.ndjsonlanes against 75d6fc4 withexitCodestripped; all 9 changed deltas are deep-equal otherwise, line counts unchanged, other acp-cursor cells byte-identical. Theweb-searchnode -eclose moves 0 -> 1 because Cursor reportedrawOutput.exitCode: 1understatus: "completed"(the #1529 shape in a real recording).Fail-before / pass-after (
pnpm exec vitest run src/delta-translation.test.tsinplugins/provider-acp): withgit checkout origin/main -- plugins/provider-acp/src/delta-translation.ts plugins/provider-acp/src/wire.ts-> 5 failed / 32 passed:expected +0 to be 1,expected +0 to be undefined,expected 1 to be 127,expected +0 to be undefined, plus thetranslates execute tool calls into command executionsfixture. Sources restored -> 37 passed (37).From the committed tree (
git status --porcelainempty), afterpnpm install --frozen-lockfile --prefer-offlineandpnpm exec turbo run build(18/18):pnpm exec turbo run typecheck test --filter=bb-plugin-provider-acp --filter=@bb/provider-bridge-protocol --filter=@bb/provider-parity --force->Tasks: 11 successful, 11 total,Cached: 0 cached; bb-plugin-provider-acp 15 files / 201 tests, @bb/provider-bridge-protocol 15 files / 217 tests, @bb/provider-parity 1 file / 43 tests.gh pr checks 2131: all required checks pass on 06c27aa.Repro on the fixed branch (own dev instance, scratch repo +
git worktree add, realacp-cursorthread thr_5mn7xbb4j2 with the report's verbatim prompt): the upstream wedge still reproduces (Cursor returns "The shell command returned no exit status" for steps 3 and 4 and heals after the working-directory override), and bb now persists those two items as{"command":"echo hi; git status","status":"completed"}/{"command":"echo alive","status":"completed"}with noexitCodeand no output, where main wroteexitCode: 0. A second thread (thr_sxg224nzrj) in which Cursor reportedrawOutput.exitCode: 127and126understatus: "completed"rendered asexit 127/exit 126(error)rows inbb thread log.Residual risks: unchanged from the previous section. The wedge itself is a Cursor CLI bug; merging closes #1529 on the bb side only (misreported exit codes), which the body states.