anomalyco / anomalyco/opencode
run --format json drops every subagent part: the loop tracks child sessions, honours them for permissions, and filters parts against the root session
@rekram1-node is already working on this.
Since Sep 16, 2026.
- Dominant language
- TypeScript
- Stars
- 209k
- Forks
- 27.5k
- PR merge metrics
- PR metrics pending
Description
Summary
In packages/opencode/src/cli/cmd/run.ts (v1.18.30), the event loop deliberately maintains a set of child sessions, uses that set when handling permissions, and then ignores it when handling parts — so --format json emits nothing a subagent does.
const sessions = new Set([sessionID]) // :699
if (event.type === "session.created" && event.properties.info.parentID) {
if (sessions.has(event.properties.info.parentID)) sessions.add(event.properties.info.id) // :704
}
if (event.type === "message.part.updated") {
const part = event.properties.part
if (part.sessionID !== sessionID) continue // :722 <-- root only
...
}
if (event.type === "permission.asked") {
const permission = event.properties
if (!sessions.has(permission.sessionID)) continue // :803 <-- set honoured
}
:704 exists only to populate sessions, and :803 is its only consumer. If subagent parts were meant to be filtered out, :704 would not need to run at all — the two lines read as though :722 was written before the set existed and never updated.
Effect
A task subagent's reasoning, tool calls and tool results never reach stdout in JSON mode. The parent's own task tool part is still emitted when it completes, so a consumer sees that a subagent ran and its final task_result, but nothing of how it got there. Interactive mode is unaffected in the same way (the same filter applies, it just matters less when a human is watching).
The data is not lost — it is in the session store with the correct parent_id — so this is purely the JSON stream dropping something the process already has and already tracked.
Why it matters beyond cosmetics
--format json is the integration surface. Anything driving opencode programmatically (we front it through an OpenAI-compatible bridge) can persist only what crosses this stream. In our case an agent delegated the independent verification of a research result to a task subagent; the subagent's 152-step working record exists only in opencode.db, and every downstream record of that run shows a half-hour gap where its most important work happened.
Suggested fix
- if (part.sessionID !== sessionID) continue
+ if (!sessions.has(part.sessionID)) continue
and stamp the originating session on emitted events so a consumer can attribute them, since emit currently writes the root sessionID unconditionally:
function emit(type: string, data: Record<string, unknown>) {
if (args.format === "json") {
process.stdout.write(JSON.stringify({ type, timestamp: Date.now(), sessionID, ...data }) + EOL)
Passing the part's own sessionID (with the root still available as, say, rootSessionID) would let a consumer rebuild the tree that parentID already describes.
Two things worth a maintainer's judgement, which is why this is an issue rather than a PR: whether subagent parts should be on by default or behind a flag (they are verbose, and some consumers will assume one session per stream), and whether --thinking should gate subagent reasoning the way it gates the root's. I have not sent a patch because I cannot exercise the interactive path or your test suite here, and a behaviour change on the default output of run deserves your call on the shape rather than mine.
Happy to send a PR in whatever shape you prefer.
Environment
opencode 1.18.30, macOS 15 (Darwin 25.1.0), opencode run --format json driven non-interactively over stdin.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.