* fix(agent): drive OpenCode over ACP so long tools and subagents are not force-stopped
`opencode run --format json` reports a tool only when it finishes, with the
call and its result in one event. The daemon never saw a tool in flight, so a
long silent tool or a delegated subagent was judged by the 10-minute OpenCode
idle window instead of the tool budget and force-stopped (GH #9614).
Built-in OpenCode 1.15+ and 2.x runtimes now run `opencode acp`, which on both
majors announces a tool when it starts executing. MessageToolUse is emitted
from that in_progress frame, so the daemon applies AgentToolWatchdog for the
whole tool. One code path covers both majors:
- model via session/set_config_option (2.x has no session/set_model); effort
via the advertised `effort` option, splitting 2.x `model#variant`
- resume via session/load; a missing session is reported as ResumeRejected
- cancel via session/cancel, which aborts the running tool on both majors
- MCP: 1.x keeps OPENCODE_CONFIG_CONTENT; 2.x gets servers in session/new,
which lifts the 2.x MCP refusal on this transport
- 2.x subagent text (`_meta["opencode/child-session"]`) stays out of the
parent's output; its tool calls are kept
- usage summed from the session export over this turn's assistant messages,
since the prompt result's usage is not per-turn on either major
The run transport stays as the fallback for custom profiles, older or
unknown versions, agents with custom_args, and MULTICA_OPENCODE_TRANSPORT=run.
Also fix the run transport on 2.x: tool_use parts carry the call id as `id`
(1.x: `callID`), so every 2.x tool message had an empty CallID.
Co-authored-by: multica-agent <[email protected]>
* fix(agent): bound OpenCode ACP teardown when the runtime stops reading
A runtime that stops reading stdin mid-prompt left the prompt write blocked
holding the client's write lock. The cancel path then waited on that lock to
send session/cancel, and the cancel grace and the process-group kill only ran
after the write, so the process could outlive the task indefinitely.
- session/cancel is sent only once the prompt is fully delivered, from its own
goroutine, so the grace starts immediately and a stalled pipe cannot hold it
- a turn OpenCode never answered (or a cancelled/timed-out setup) is torn
down forcibly: stdin closed first to release a blocked write, then SIGTERM to
the group and SIGKILL after the grace; an answered turn still exits on EOF
Regression tests cover a stalled prompt write under a 1s timeout and a runtime
that ignores session/cancel.
Co-authored-by: multica-agent <[email protected]>
* fix(agent): bound OpenCode ACP teardown when a setup write stalls
Setup requests (initialize, session/new, session/load, set_config_option)
are synchronous stdin writes that check their context only after the write
returns. A runtime that stops reading after initialize, with a large MCP
payload in session/new, left the run goroutine blocked in that write, so
the deferred teardown never ran and the process outlived the timeout.
A watcher now force-stops the process as soon as the run ends before the
prompt is fully delivered; closing stdin releases the blocked write. Once
the prompt is delivered the existing session/cancel path owns shutdown.
Regression test: 256 KiB MCP env, runtime stalls after initialize, 1s
timeout; returns in ~1s (60s before the fix).
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: Multica Eve <[email protected]>
Co-authored-by: multica-agent <[email protected]>