Conversation
## What was wrong #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). - `HOST_DAEMON_PROTOCOL_VERSION` 140 to 141: older daemons emit the queue-time semantics and produce those phantom turns. Two paths deliberately keep answering their command at queue time, because the runtime fails a bridge request that goes unanswered for 30 seconds. 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. 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 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ymichael
deleted the
bb/2014-pi-and-acp-emit-input-accepted-at-queue-tim-thr_s88amww8ie
branch
August 20, 2026 07:51
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
#2013 established the uniform rule that
input.acceptedmeans 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 aspi/prompt/settled— aclaimIfIdleturn terminal. So aturn/startpi 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/starthandler 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
PiSdkSessiontracks 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. Itsprompt()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.turn/startand emitsinput.acceptedonly once pi read the input.input.acceptedonce thesession/promptrequest 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).HOST_DAEMON_PROTOCOL_VERSION140 to 141: older daemons emit the queue-time semantics and produce those phantom turns.Two deviations from the issue's proposed fix:
PiSdkSession.steer()resolved once the SDK took the message into its queue, the same queue-time violation asturn/start.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 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
turn/startpi 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 receivedturn/started+turn/input/accepted+turn/completed— the phantom turn.thread/stopleaves the turn with one accepted input instead of two. Both fail before, pass after.PiSdkSessioncoverage for queued-versus-direct dispatch, and for a queued follow-up surviving theagent_endthat 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