Repository navigation
Description
Feature hasn't been suggested before.
- I have verified this feature I'm about to request hasn't been suggested before.
Describe the enhancement you want to request
Orchestrating sessions can spawn subagents but have no agent-facing tools to observe or recover child sessions: no status, no message/context read, no salvage, no cancel. The server HTTP API already exposes everything needed; the request is to surface a scoped subset as agent tools.
The gap, from a real incident (opencode 2.1.0, 2026-10-26):
- A background subagent completed a 40-item legal/linguistic analysis and was killed by a provider stream cut during its final artifact write (see companion bug report). The child session ended in a "completed"-looking state with the full analysis in its context and 0 bytes of artifact on disk.
- The orchestrating session tried to rescue it and had no tool for any of it: it could not check whether the child was running/idle/stuck, could not read the child's messages or context, could not cancel the stuck session, and the
sessionIDcontinuation hung (companion bug). - The entire diagnosis had to be driven through raw
opencode apicalls from the shell and log grepping; cancellation was only possible via the UI close button.
Expected behavior: parent agents should have a minimal, read-mostly toolset for children they spawned, mirroring existing server capabilities.
Current state: the agent-facing toolset exposes only session_move/session_rename/worktree tools — nothing for child-session observability or recovery. Meanwhile, the V2 HTTP API already provides: session.get, session.inbox.list/cancel/update, session.message.list/get, session.context (active model-context projection), session.fork (copies full message history; forks adopt instruction state), session.interrupt, session.remove, and experimental session.wait/session.export/session.import, plus write-ahead execution claims with startup recovery.
Proposed tools (scoped to children spawned by the calling session):
session.status(childID)→{ state: idle|running|claimed|unknown, pendingInbox, lastActivity }— distinguishes a working child from a zombie without log grepping.session.messages(childID, limit)/session.context(childID)— read-only harvest of a child's message history / model-context projection (the rescue path for finished-but-unwritten work).subagent.fork(sessionID, prompt)— fork with full history + prompt in one call: salvage a hung child's completed analysis without redoing the work.subagent.cancel(sessionID)— programmatic parity with the UI close (map tosession.interrupt/inbox.cancel/session.remove).
Concrete use case (real): a child session holds a completed 40-item audit analysis; the final write dies on a stream cut. With the proposed tools the parent would do: session.status → confirm stuck → session.messages harvest → subagent.fork(child, "write the artifact now, in ≤10 KB segments; if your context lacks the complete results, say so instead of fabricating") → artifact salvaged. Today the only options are losing the work (fresh relaunch) or manual opencode api/UI surgery from outside the agent loop.
These tools also serve as the missing supervision layer for the companion bug: status + pendingInbox makes a silently-dead child visible, and cancel clears it without desktop UI access.
Related issues:
- v2: subagent: no cancellation support for background subagents #36423 (open) — requests cancellation for background subagents only;
subagent.cancelhere is one of four tools in a broader observability/recovery toolset. - [FEATURE]: Sanitize interrupted running/pending tool parts on subagent resume #50214 (open) — sanitize interrupted running/pending tool parts on subagent resume; complementary recovery hygiene for the same stuck-session class.
- subagent: sessionID continuation forks running lanes — zero-context sidecar spawns, no parentage in payload #56249 (closed) — documents state-dependent
sessionIDcontinuation (silent fork of running lanes); an explicitsubagent.fork(sessionID, prompt)would make the salvage path deterministic and observable instead of state-dependent. - subagent: background subagent's resumed turn completes but the parent session never receives the completion notification #50751 (open) — the current workaround is reading the child's last assistant message straight from the local SQLite store;
session.messages(childID)/session.context(childID)would replace manual DB surgery.
Activity
opencode-agent commentedon Oct 27, 2026
Thanks for the detailed write-up, @ThanatoS-cdr! You're asking for a small set of agent tools that let an orchestrating session inspect and recover the child sessions it spawned: status, reading messages/context, fork + prompt, and cancel. These would expose routes the server already has. Maintainers will consider it.
Could this be a plugin today? Only partly. The v2 plugin session API already has
get,context,interrupt,removeandwait, so status, context read and cancel could be added as plugin tools. It doesn't have message listing,forkor the inbox, so the message-harvest and fork + prompt parts can't be built as a plugin yet.Related issues:
ThanatoS-cdr commentedon Oct 27, 2026
Thanks for the triage and the plugin-path analysis — that split matches our use case exactly. In the incident behind the companion bug (#59565), the pieces that actually recovered the work were the message harvest + fork + prompt, so those are the ones that can't stay plugin-less long-term. The supervision subset (status / context read / cancel) looks like a plugin-sized first step — happy to prototype it and report back how far the existing plugin session API gets.
ThanatoS-cdr commentedon Oct 27, 2026
Follow-up as promised — prototype done and tested headless on 2.1.0.
What was built: a small plugin ("subagent-supervisor" — plain ESM JS, no dependencies) registering three direct agent tools on the current plugin session API:
session_status(sessionID),context_read(sessionID),subagent_cancel(sessionID).The scoping answer is yes: a plugin running in the orchestrating session can address child sessions by ID. The plugin session domain resolves sessions through the global session service with no ownership check (
packages/core/src/plugin/host.tsL534–575), and thesubagenttool returns the child's sessionID directly, so the parent can pass it straight to supervision tools. One wrinkle: the plugin domain has nolist/active, so the "running" state has to be folded fromsession.execution.*events via the plugin event bus — which works fine on 2.1.0.End-to-end test (isolated config,
opencode run --standalone, background child running a 60-second shell task):session_status(child)→state:"running"mid-taskcontext_read(child)→ the child's prompt plus its liveshelltool part, visible from the parentsubagent_cancel(child)→{"interrupted":true}; ~0.4 s latersession_status→state:"idle", outcome:"interrupted"So the supervision subset (status / context read / cancel) is confirmed deliverable as a plugin today, exactly as you sketched. Still blocked on core, per your analysis: message listing, fork, and the inbox — the harvest + fork + prompt path that actually recovers finished-but-unwritten work in #59565. One related note for scoping: without
list/activein the plugin domain, a parent that loses a child's sessionID cannot rediscover it.Worth noting the flip side of that open resolution: as written, the tools act on any session the server knows, not just the caller's own children — a production version would scope them (the tool execution context carries the calling session's ID, and
SessionInfo.parentIDis available for the check). Two more core-side notes from reviewing this prototype:session.removecascades to all of the target's child sessions, and the plugin API has no per-call permission ask — so a safe delete path would need core support as well.A finding for future plugin authors, hit during the first full run: a tool result that includes an
outputfield must come from a tool that declared anoutputJSON Schema ineditor.add, otherwise the result is rejected withTool result declared output without an output schema(packages/core/src/tool/runtime.tsL46–47). Theexecute()side runs fine — only the result is dropped, which makes it a confusing failure mode to debug.Happy to share the plugin source if that's useful.