feat(acp): steer claude-code and codex agents via _session/steering - #3007
Draft
wpfleger96 wants to merge 1 commit into
Draft
feat(acp): steer claude-code and codex agents via _session/steering#3007wpfleger96 wants to merge 1 commit into
wpfleger96 wants to merge 1 commit into
Conversation
Mid-turn steering was reachable only through goose's `_goose/unstable/session/steer`, which requires an `expectedRunId` sourced from `_meta.goose.activeRunId`. claude-agent-acp and codex-acp never emit a run id, so every mid-turn mention to those harnesses bailed before writing a byte and degraded to cancel + merge, destroying in-flight tool calls.
Both adapters ship `_session/steering` (params `{sessionId, prompt}`, result `{outcome}`) and advertise it as `_meta.steering.supported` at initialize. Select the transport at write time: a run id picks the goose method, since `expectedRunId` is strictly more precise about which run is steered; otherwise the advertised capability picks the extension; neither leaves the existing cancel+merge fallback untouched.
The capability flag is the only gate — never error-code probing. codex-acp answers unrecognized extension methods with a bare `{}`, a JSON-RPC success rather than -32601, and Buzz maps a steer success to `queue.remove_event`. Probing would silently delete the user's message. As second-layer defense, an `outcome` must be positively recognized: anything other than `injected` or `startedNewTurn`, including absent, is `SteerError::OutcomeRejected` and falls back.
`startedNewTurn` acks Success (the message was delivered) but does not renew the read loop's hard deadline — the turn being awaited had already settled, so renewing would extend the wrong clock.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
wpfleger96
marked this pull request as draft
July 26, 2026 17:45
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.
Mid-turn steering was reachable only through goose's
_goose/unstable/session/steer, which requires anexpectedRunIdsourced from_meta.goose.activeRunId. claude-agent-acp and codex-acp never emit a run id, so every mid-turn mention to those harnesses bailed at the run-id guard before writing a byte and degraded to cancel + merge, destroying in-flight tool calls.Both adapters ship
_session/steering(params{sessionId, prompt}, result{outcome}) and advertise it as_meta.steering.supportedon theinitializeresponse. This adds it as a second steer transport selected at write time, reusing the existing withhold/release, ack routing, and cancel+merge fallback machinery unchanged.Transport selection
active_run_idsteering_supportedSome(run_id)_goose/unstable/session/steer+expectedRunId(unchanged)Nonetrue_session/steeringwith{sessionId, prompt}NonefalseExpectedRunIdMissing, write nothing (unchanged)goose keeps priority when both are present —
expectedRunIdis strictly more precise about which run is being steered.Two load-bearing safety properties
The advertised capability is the only gate — never error-code probing. codex-acp's
extMethodanswers unrecognized extension methods with a bare{}, which is a JSON-RPC success rather than-32601. Buzz maps a steer success toqueue.remove_event, so probing an unknown method would silently delete the user's message with no error, no fallback, and no log line.An
outcomemust be positively recognized. OnlyinjectedandstartedNewTurncount as delivery. Anything else — codex'sfailed, an unknown value, or a missingoutcomeentirely — isSteerError::OutcomeRejected, which releases the withheld event and fires the cancel+merge fallback. This makes the silent-loss path above unreachable even if an adapter mis-advertises.startedNewTurnacksSuccess, because the message really was delivered and must not be redelivered, but deliberately does not renew the read loop's hard deadline: the turn Buzz was awaiting had already settled, and renewing would extend the clock on a finished turn.Notes for reviewers
SteerError::OutcomeRejectedneeds no new arm in thePoolEvent::SteerAckmatch — the existing catch-allOk(SteerAck::Err(_)) => (true, false, true)already gives release + fallback, and the twoAgentErrorarms above it match that variant specifically, so they do not shadow it.-32601behavior are corrected; that assumption was never valid for codex-acp.buzz-acptests. The full package suite was run locally: 617 passing, 0 failing.