You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
opencode2 run and background: true tasks may cause premature exit #48361
I have a script that uses opencode2 run in which I am trying to achieve parallelization. The model decided to use background: true for the subagents/tasks.
When that happened, it correctly dispatched all the subagents, but opencode2 run terminated prematurely.
Plugins
None
OpenCode version
v0.0.0-beta-19425
Steps to reproduce
Use opencode2 run with instructions to execute a lot of work in parallel and ask it to use background: true
If your issue is distinct (e.g., specific to the background: true flag in task config rather than the general headless lifecycle), please add details clarifying the difference.
I used a mock model whose first reply starts two subagent calls with background: true. Each subagent takes about 15 seconds to answer. Then I ran opencode2 run --standalone --auto "..." on v0.0.0-beta-19425. What happened:
Both subagents were shown as ✓ immediately.
The parent printed its "waiting for them to finish" text.
run exited with code 0 after about 1–2 seconds.
The subagents went on to finish about 15 seconds later, but the parent was never resumed with their results, so the final answer never appeared. It still happens on the latest dev (1e7ba46f8b99). The script below reproduces it with a built-in mock provider.
Until this is addressed, you could ask the model in your script not to use background: true. That gives up the parallelism you were after, but foreground subagents block until they finish, so run gets their results.
Reproduction script (repro.sh)
#!/usr/bin/env bash# Repro for #48361: `opencode run` exits before background subagents finish.# Needs: bun and `opencode` on PATH (override with OC="..."). Starts an embedded OpenAI-compatible mock on :4999.# The mock makes the parent call `subagent` twice with background:true; each child takes 15s to answer.set -u
OC=${OC:-opencode}# OC_DEV=/path/to/checkout runs the CLI from source insteadif [ -n"${OC_DEV:-}" ];then
OC="bun --conditions=browser --preload=$OC_DEV/packages/tui/node_modules/@opentui/solid/scripts/preload.js $OC_DEV/packages/cli/src/index.ts"fi
DIR=$(mktemp -d); mkdir -p "$DIR/home""$DIR/proj"export HOME=$DIR/home XDG_CONFIG_HOME=$DIR/home/.config XDG_DATA_HOME=$DIR/home/.local/share XDG_STATE_HOME=$DIR/home/.local/state XDG_CACHE_HOME=$DIR/home/.cache
cat >"$DIR/proj/opencode.json"<<'JSON'{ "model": "mock/mock", "permission": { "*": "allow" }, "provider": { "mock": { "npm": "@ai-sdk/openai-compatible", "name": "Mock", "options": { "baseURL": "http://127.0.0.1:4999/v1", "apiKey": "x" }, "models": { "mock": { "name": "Mock", "tool_call": true, "limit": { "context": 100000, "output": 4000 } } } } } }JSON
cat >"$DIR/mock.ts"<<'MOCK'// Minimal OpenAI-compatible streaming mock.const log = (...a: unknown[]) => console.error(new Date().toISOString(), ...a)const text = (m: any) => typeof m.content === "string" ? m.content : (m.content ?? []).map((p: any) => p.text ?? "").join("")function sse(chunks: any[]) { const enc = new TextEncoder() return new Response(new ReadableStream({ start(c) { for (const ch of chunks) c.enqueue(enc.encode(`data: ${JSON.stringify(ch)}\n\n`)) c.enqueue(enc.encode("data: [DONE]\n\n")); c.close() } }), { headers: { "content-type": "text/event-stream" } })}const base = { id: "x", object: "chat.completion.chunk", created: 0, model: "mock" }const say = (s: string) => sse([{ ...base, choices: [{ index: 0, delta: { role: "assistant", content: s } }] }, { ...base, choices: [{ index: 0, delta: {}, finish_reason: "stop" }] }])const calls = (cs: any[]) => sse([{ ...base, choices: [{ index: 0, delta: { role: "assistant", tool_calls: cs.map((c, i) => ({ index: i, id: c.id, type: "function", function: { name: c.name, arguments: JSON.stringify(c.args) } })) } }] }, { ...base, choices: [{ index: 0, delta: {}, finish_reason: "tool_calls" }] }])Bun.serve({ port: Number(process.env.PORT ?? 4999), idleTimeout: 120, async fetch(req) { const url = new URL(req.url) if (!url.pathname.endsWith("/chat/completions")) return new Response("{}") const body: any = await req.json() const msgs = body.messages as any[] const all = msgs.map(text).join("\n") const users = msgs.filter((m) => m.role === "user").map(text) const tools = (body.tools ?? []).map((t: any) => t.function.name) if ((users[0] ?? "").includes("CHILD_TASK")) { const tok = users[0].match(/CHILD_TASK_(\w+)/)![1] log("child", tok, "-> sleeping 15s"); await Bun.sleep(15000); log("child", tok, "done") return say(`TOKEN_${tok}`) } if (!tools.length) { log("title/other"); return say("Title") } if (all.includes("PARENT")) { const toolMsgs = msgs.filter((m) => m.role === "tool").length if (/TOKEN_A/.test(all) && /TOKEN_B/.test(all)) { log("parent final"); return say("FINAL: got TOKEN_A and TOKEN_B") } if (toolMsgs === 0) { log("parent -> launching 2 background subagents; tools:", tools.join(",")); return calls([ { id: "call_a", name: "subagent", args: { agent: "general", description: "child a", prompt: "CHILD_TASK_A", background: true } }, { id: "call_b", name: "subagent", args: { agent: "general", description: "child b", prompt: "CHILD_TASK_B", background: true } }, ]) } log("parent -> waiting text"); return say("Launched both subagents; waiting for them to finish.") } log("unknown"); return say("ok")} })log("mock listening")MOCK
PORT=4999 bun "$DIR/mock.ts"2>"$DIR/mock.log"& MOCK_PID=$!; sleep 1
# the --standalone child server is spawned without the preload flag; give it a bunfig.toml
[ -n"${OC_DEV:-}" ] &&echo"preload = [\"$OC_DEV/packages/tui/node_modules/@opentui/solid/scripts/preload.js\"]">"$DIR/proj/bunfig.toml"cd"$DIR/proj"
start=$(date +%s)
timeout 120 $OC run --standalone --auto "PARENT: launch two background subagents and report their tokens"echo"exit=$? wall=$(( $(date +%s)-start ))s (expected: waits ~15s and prints FINAL: got TOKEN_A and TOKEN_B)"
sleep 20;echo"--- mock log (children keep running after run exited; parent never resumed) ---"; cat "$DIR/mock.log";kill$MOCK_PID
Activity
github-actions commentedon Sep 10, 2026
This issue might be a duplicate of existing issues. Please check:
opencode run(headless) exits 0 mid-pipeline after a parallel subagent fan-out, before the model's next turn #37548:opencode runexits 0 mid-pipeline after a parallel subagent fan-out — same symptom of premature exit after background tasks are dispatchedopencode runexits before background tasks (explore/librarian) complete #16380:opencode runexits before background tasks complete — same root cause where headless mode treats a model response as session completion without waiting for pending background workIf your issue is distinct (e.g., specific to the
background: trueflag in task config rather than the general headless lifecycle), please add details clarifying the difference.opencodejr commentedon Oct 28, 2026
Thanks for the report. I could reproduce this.
I used a mock model whose first reply starts two
subagentcalls withbackground: true. Each subagent takes about 15 seconds to answer. Then I ranopencode2 run --standalone --auto "..."onv0.0.0-beta-19425. What happened:✓immediately.runexited with code 0 after about 1–2 seconds.The subagents went on to finish about 15 seconds later, but the parent was never resumed with their results, so the final answer never appeared. It still happens on the latest
dev(1e7ba46f8b99). The script below reproduces it with a built-in mock provider.Closely related:
runexits before plugin-backgrounded work completes; plugins cannot detect a headless client on the shared service #48683: the same early exit, when work is moved to the background by a pluginUntil this is addressed, you could ask the model in your script not to use
background: true. That gives up the parallelism you were after, but foreground subagents block until they finish, sorungets their results.Reproduction script (repro.sh)
Run it:
bash repro.sh