Skip to content

feat(core,cli): default subagent tool to background: true - #60618

Open
kitlangton wants to merge 1 commit into
devfrom
t902-subagent-bg
Open

kitlangton wants to merge 1 commit into
devfrom
t902-subagent-bg

Conversation

@kitlangton

@kitlangton kitlangton commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

subagent now runs in the background by default (background: true) while keeping background: false for synchronous waits, and opencode run stays alive until spawned background subagents finish and the parent completes its resumed reply (#48361).

🔍 Origin: reported in #48361 and #48826

💡 Why: Subagent turns typically run for minutes, so foreground-by-default stalled parent turns whenever an agent omitted background: true — while non-interactive opencode run exited on the parent's first idle boundary before background subagents delivered their results (#48361). Across Claude Code SDK (v2.1.198+) and Codex (spawn_agent), background subagent launch is now the default, and in a 30-run multi-turn eval across Haiku 5.5, DeepSeek V4.1 Flash, MiniMax M3, Gemini 3.5 Flash Lite, and Sonnet 5.5 (sequential dependency + 3-subagent parallel fan-out), defaulting subagent to background: true while preserving background: false scored 30/30 (100%) with 0 polling loops and 0 premature writes.

🆕 Now you can: Spawn subagents asynchronously by default in both interactive sessions and opencode run, or pass background: false explicitly when the caller must block synchronously in the same turn.

- const background = input.background === true
+ const background = input.background !== false
+ // In noninteractive.ts: keep consuming events & awaiting child sessions until background subagents + wake turns settle

📊 Proof

Case Before After
subagent called without background Blocks parent step until child finishes Launches in background (status: "running") and wakes parent on completion
opencode run with background subagent (#48361) ❌ Exited on Turn 1 idle before child finished ✅ Waits for child + streams resumed parent reply before exiting
subagent called with background: false — Blocks synchronously until child completes (status: "completed")
5-model eval (sequential + 3-way parallel, n = 30) 28/30 30/30 (0 polls, 0 premature writes)

packages/core/test/tool-subagent.test.ts and packages/cli/test/run/noninteractive.test.ts pass.

👀 Read: packages/cli/src/run/noninteractive.ts around backgroundSubagents and waitUntilSettled — keeps consume() open across the Turn 1 idle boundary until every spawned background subagent delivers its <subagent> inbox notice and the resumed parent turn settles.

Checks run
  • bun run script/test.ts test/tool-subagent.test.ts (packages/core): pass
  • bun test test/run/noninteractive.test.ts (packages/cli): pass
  • bun run typecheck && bun run typecheck:tsc (packages/core, packages/cli): 0 errors

Added by the opencode bot (AI-generated).

Fixes #48361

Related: #48826, #50751

Public API: changes the built-in subagent tool's background default from false to true (behaviour change: callers needing an inline result must pass background: false)

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new default works in the TUI, but it makes opencode run (and any client that waits only for the parent session to go idle) exit before subagents finish whenever the model leaves out background.

How I checked it, with the simulated model and a child that takes about 2.5s:

  • TUI: on the base the call blocks and the parent reads the child's result in the same turn. With this PR it returns running immediately, the parent continues, and it is woken with the result later, as described.
  • opencode run "delegate it": on the base it prints the reply that uses the child's result. With this PR it prints the parent's first reply and exits with code 0 before the child finishes. The reply after the notification is never shown.
  • Tests and type checks: test/tool-subagent.test.ts passes (12/12) and typecheck:tsc in packages/core is clean.

Public API: no SDK, HTTP or config changes, but the built-in subagent tool's contract changes: background now defaults to true instead of false. Anything that relied on getting the subagent's result inline without passing the flag must now pass background: false. The main example is scripted or CI opencode run; plugin hooks that inspect subagent input are also affected.

Blocking: runNonInteractivePrompt only awaits experimental.session.wait on the parent, which returns as soon as the parent's turn ends. Nothing waits for running background jobs or for the turn the completion notification triggers. This is the known gap in #48361, which so far only showed up when a model opted in to background: true. With this change every delegation hits it. Either make run wait for the session's background jobs and the resumed turn before exiting, or keep run on synchronous subagents, before flipping the default.

Related: #48361, #48826, #55760

Before

After

))

const background = input.background === true
const background = input.background !== false

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this default, opencode run no longer waits for subagents. The non-interactive runner only waits for the parent session to go idle (experimental.session.wait), so once the parent ends its turn after launching a subagent, the process exits. The child's result and the parent's reply after the notification are lost. I reproduced this with the simulated model: the base prints the reply that uses the child's result, while this branch exits 0 after the first reply. See #48361. This needs the runner (and other clients that wait for idle) to also wait for pending background jobs and the resumed turn, or a synchronous default for non-interactive runs, before the default flips.

@kitlangton kitlangton changed the title feat(core): default subagent tool to background: true feat(core,cli): default subagent tool to background: true Oct 30, 2026
@kitlangton
kitlangton force-pushed the t902-subagent-bg branch 2 times, most recently from 72026d9 to dcd4f80 Compare October 30, 2026 14:25

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The earlier blocker is fixed: opencode run now waits for background subagents. I re-ran the same simulated-model repro against a server running this PR: run prints the parent's first reply, waits for the child, then prints the parent's reply after the completion notice and exits with code 0. At f3d91bf it exited after the first reply. noninteractive.test.ts (13/13) and tool-subagent.test.ts (12/12) pass, and typecheck:tsc is clean in packages/cli.

Public API: unchanged apart from the built-in subagent tool's background default, false → true. Callers that need the result inline must pass background: false. The run change adds no flags or output formats.

One remaining point, inline: the new wait loop can poll forever if a completion notice never arrives.

Comment thread packages/cli/src/run/noninteractive.ts Outdated
)
}
if (backgroundSubagents.size > 0 || pendingWakeIds.size > 0) {
await Promise.race([completed, new Promise((resolve) => setTimeout(resolve, 25))])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once the parent and the child are both idle, this loop only exits when the child's completion notice shows up as session.inbox.enqueued and is then delivered. If that notice never arrives (for example the lost-notification bug in #50751, or a notice enqueued while the event stream is reconnecting), both wait calls return immediately and opencode run polls every 25ms forever instead of exiting. A time limit on this loop, or treating an idle child with no pending job as settled, would make a missed notice fail safely instead of hanging CI.

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both earlier points are addressed: opencode run now waits for background subagents, and a child whose completion notice never arrives no longer leaves run polling forever.

I ran the reproduction script from #48361 (two background subagents, each taking 15s, run --standalone --auto) from source. On the base, run exits with code 0 after about 4s and the final answer never prints. With this PR it waits about 19s, prints FINAL: got TOKEN_A and TOKEN_B and exits with code 0. In packages/cli, noninteractive.test.ts passes (14/14) and typecheck:tsc is clean.

Public API: unchanged apart from the built-in subagent tool's background default, false → true. Callers that need the result inline must pass background: false.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

opencode2 run and background: true tasks may cause premature exit

1 participant