Repository navigation
Description
Problem
In chat (acp) mode, messages sent while an agent is working are added to the running turn ("steered") for claude, codex and pi, the same as their TUIs do. opencode still holds the message until the turn ends. The pane shows it as "queued until the agent finishes", and it can be removed before it runs (see "Sending mid-turn" in docs/agent-modes.md).
opencode's own TUI steers by default: Enter submits with delivery: "steer", and <leader>return queues. So chat mode does not behave like the TUI for opencode.
Why yaac can't fix it on its side
opencode acp (checked on the pinned 2.0.12 and the latest 2.0.21) has no way to accept a second message mid-turn:
- A second
session/promptwhile one is running fails withSession already has an active ACP prompt. - It does not implement the
_session/steeringextension that claude-agent-acp and codex-acp do, and its initialize reply does not advertise_meta.steering. - Its own prompts already go to opencode's server with
delivery: "steer"; there is just no entry point for a second one.
We fixed pi-acp the same way with an install-time patch (dockerfiles/agent-patches/pi-acp.js). That isn't reasonable for opencode:
- The ACP adapter is compiled into opencode's Bun binary. Patching it means rewriting the binary's module table so Bun runs edited source instead of the bytecode.
- The darwin binary would need re-signing (
codesign --force -s -). - The minified names change every release.
A side channel to the private opencode serve child that opencode acp spawns (scraping its port and OPENCODE_PASSWORD from /proc) was also rejected as too fragile.
Upstream status
- [FEATURE]: ACP v2 draft spec support anomalyco/opencode#44877 asks for ACP v2, under which a second
session/promptwould steer. - feat(acp): add ACP v2 draft support/features anomalyco/opencode#44524 implemented that behind
OPENCODE_EXPERIMENTAL_ACP_V2, but was closed unmerged on 2026-09-23. - Standardization in ACP itself is still open: docs(rfd): steer and queue prompts sent while the Agent is working agentclientprotocol/agent-client-protocol#1261 (
session/inject) and #2255 (implicit steering via a capability).
How to fix it upstream
Proposed change to opencode's ACP adapter (packages/opencode/src/acp/service.ts on the dev branch), matching the contract claude-agent-acp and codex-acp already implement.
-
Advertise it. Add
_meta: { steering: { supported: true } }to theinitializeresult, merged with the existingopencode/child-session-updateskey. -
Add a
_session/steeringrequest handler. Register it besidesession/prompt; the adapter's ACP SDK accepts custom methods viaonRequest(method, parser, handler). The params are{ sessionId, prompt: ContentBlock[], _meta?: { steering?: { idleBehavior?: "promptRequired" } } }.-
A turn is running (the adapter holds an active ACP prompt for the session): convert the blocks the way
session/promptdoes, then submit through the same SDK client:session.prompt({ sessionID, text, files, delivery: "steer", resume: false })Reply
{ outcome: "injected" }. The running turn's event forwarding already picks up the steered work. The originalsession/promptreply arrives once, after the steered output; we verified this through the HTTP side channel. -
Idle, and the client sent
idleBehavior: "promptRequired": consume nothing and reply{ outcome: "promptRequired", reason: "noRunningTurn" }. The client then sends a normalsession/prompt. -
Idle, no opt-in: start a turn and reply
{ outcome: "startedNewTurn" }, as the other adapters do. Better still, treat it like the opt-in. codex-acp ignoring the opt-in is the reason yaac has to follow codex's thread status at all.
-
-
Close the turn-end race. A steer accepted just as the turn finishes must not be stranded. With
resume: false, an inbox item posted after execution ends stays pending and nothing runs it.- Keep the active-prompt entry until after execution has fully ended.
- Before replying to the original
session/prompt, checksession.inboxfor items this turn accepted. If any are still pending, either resume execution and keep forwarding, or delete them (DELETE /api/session/{id}/inbox/{inboxID}) so the steer'sinjectedreply was not a lie. - On
session/cancel, delete this turn's pending steers.
-
Echoes. No change is needed for yaac: it shows the steered message from the recorded request and reply. Still, emitting a
user_message_chunkfor it onsession/loadreplay would keep history complete.
The adapter's behaviour can be checked without model credentials by running it against a scripted model server and driving opencode acp over stdio. That is how the side-channel findings above were verified.
yaac side once a release has it
- Bump the opencode pin (
AGENT_CLIS.opencode,dockerfiles/Dockerfile.tools). - Set
steers: truefor opencode inpackages/server/src/runtime/agents/acp-adapters.ts. - Update the "opencode queues" paragraph in
docs/agent-modes.md. - The queue path stays for anything that hands a message back (
promptRequired). - Verify against the new binary: a mid-turn message is steered, the original prompt sends exactly one reply, and an idle steer is handed back.
Activity
Let chat messages sent mid-turn steer the running turn
Let chat messages sent mid-turn steer the running turn