Fix Claude resumed-input turn correlation - #2013
Merged
ymichael merged 1 commit intoAug 20, 2026
Merged
Conversation
ymichael
deleted the
bb/investigate-missing-project-thread-thr_224ncw7wrf
branch
August 20, 2026 06:38
ymichael
added a commit
that referenced
this pull request
Aug 20, 2026
## What was wrong [#2013](#2013) established the uniform rule that `input.accepted` means the provider consumed the input, never that bb queued it, because an acceptance still pending when a stale terminal arrives lets that terminal claim the input and complete an empty turn for a message the provider has not answered. Pi and ACP still emitted acceptance at dispatch. Pi's exposure is not just theoretical timing. `PiSdkSession.prompt()` resolves as soon as pi queues a prompt that arrives while a run is still unwinding, and the bridge reported that resolution as `pi/prompt/settled` — a `claimIfIdle` turn terminal. So a `turn/start` pi merely queued produced acceptance plus a terminal in the same tick, which the assembler turned into a started-and-completed empty turn while the real answer ran later under an unaccepted turn. ACP emitted acceptance in the `turn/start` handler before the turn opened, and for a steer it emitted acceptance at queue time even though the queued input is dropped whenever the turn fails or the session stops — reporting input the agent was never given as accepted into the turn. ## What changed - `PiSdkSession` tracks pending input consumption for both of pi's queues instead of steering only, and resolves it from pi's preflight hook (the input entered a run) or from the queue update that delivers a queued message. Its `prompt()` now returns that consumption signal alongside the settlement of the run it started, and reports no settlement for input pi queued into a run it did not start. - The pi bridge answers `turn/start` and emits `input.accepted` only once pi read the input. - The ACP bridge carries the waiting command with the input and emits `input.accepted` once the `session/prompt` request carrying it goes out, so the acceptance names the open turn and a dropped steer is never accepted. Every turn input still leaves with exactly one reply ([#853](#853)). - `HOST_DAEMON_PROTOCOL_VERSION` 140 to 141: older daemons emit the queue-time semantics and produce those phantom turns. Two deviations from the issue's proposed fix: 1. The issue states pi's steer path "already waits for actual SDK acceptance." It does not — `PiSdkSession.steer()` resolved once the SDK took the message into its queue, the same queue-time violation as `turn/start`. 2. Both steer paths deliberately keep answering their command at queue time, because the runtime fails a bridge request that goes unanswered for 30 seconds (`sendJsonRpcRequest`). Pi delivers steering only between assistant turns, so a steer sent during a long tool call would time out; ACP delivers a steer only when the cancelled prompt is reissued. Neither can manufacture a turn: a steer's acceptance lands in a turn the assembler already holds open, and the [#2013](#2013) failure mode needs a *pending* acceptance. Pi keeps reporting a steer its run never read through the session error path. There are no CLI, guide, configuration, or user-facing documentation changes. ## How you verified - New pi regression: a `turn/start` pi queues behind a live run emits no turn events until the queue delivers it, then the acceptance lands in the turn pi opened. Before the change it received `turn/started` + `turn/input/accepted` + `turn/completed` — the phantom turn. - New ACP regressions: acceptance is emitted immediately after the turn opens rather than before it, and a steer dropped by `thread/stop` leaves the turn with one accepted input instead of two. Both fail before, pass after. - New `PiSdkSession` coverage for queued-versus-direct dispatch, and for a queued follow-up surviving the `agent_end` that continues into it. - `pnpm exec turbo run typecheck test --filter=@bb/agent-runtime --filter=bb-plugin-provider-acp --filter=@bb/host-daemon-contract --force` — 417, 172, and 52 tests passed; typechecks passed. - `pnpm exec turbo run build typecheck --filter='...[origin/main]'` — 62 tasks passed. - `git diff --check` — passed. Fixes #2014 🤖 Generated with [Claude Code](https://claude.com/claude-code) > AGENT GENERATED: by Claude Opus 5 Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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
When Claude Code resumed a session that had orphaned background work, the SDK could drain a zero-work
resultwithorigin.kind=task-notificationimmediately before processing the newly queued human prompt. The Claude bridge emittedinput.acceptedas soon as it queued that prompt, and the translator allowed any result to claim a pending accepted input. The recovered notification therefore manufactured and completed an empty turn for the new message; the real answer continued later under a second, unaccepted turn. This is the mechanism independently reproduced in #1718 and observed again in the affected production thread.What changed
/clear.turn/startmatchturn/steer: emit acceptance and answer the command only after the SDK prompt iterator consumes the queued input.HOST_DAEMON_PROTOCOL_VERSIONfrom 138 to 139 because older daemons emit the incorrect lifecycle semantics.The uniform provider rule is:
input.acceptedmeans the provider consumed the input, never merely that bb queued it. Codex already follows that rule. Pi's prompt promise currently reports settlement rather than consumption, and ACP exposes no equivalent provider acknowledgement, so those bridges need provider-specific correlation work rather than a timing heuristic in this focused Claude fix. The Pi/ACP cross-provider follow-up is tracked in #2014. There are no CLI, guide, configuration, or user-facing documentation changes.How you verified
turn/started+turn/input/accepted+turn/completedinstead of no events, and the new turn/start ordering regression observed a response before SDK consumption.pnpm exec turbo run test --filter=bb-plugin-provider-claude-code --force— 259 tests passed, including/clearzero-work conformance.pnpm exec turbo run typecheck --filter=bb-plugin-provider-claude-code --force— passed.pnpm exec turbo run test typecheck --filter=@bb/host-daemon-contract --force— 52 tests passed; typecheck passed.pnpm exec turbo run typecheck --filter=@bb/host-daemon --force— passed.pnpm exec turbo run typecheck --filter=@bb/server --force— passed.git diff --check— passed.Fixes #1718