2 Commits
Author SHA1 Message Date
7d02596fc3 fix(agent): MUL-8307 drive OpenCode over ACP so long tools and subagents are not force-stopped (#9619)
* 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]>
2026-10-28 15:11:27 +08:00
importcpp c4c8bab56c fix(opencode): include separate reasoning in output usage (#8431) 2026-09-15 13:59:20 +08:00