Skip to content

opencode2 run and background: true tasks may cause premature exit #48361

Description

@ucirello

Description

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

  1. Use opencode2 run with instructions to execute a lot of work in parallel and ask it to use background: true
  2. Wait for the jobs to be parallelized
  3. Observe opencode2 run ending prematurely.

Screenshot and/or share link

No response

Operating System

MacOS 26.5.2

Terminal

iTerm + tmux

Activity

  1. github-actions commented on Sep 10, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

    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.

  2. opencodejr commented on Oct 28, 2026

    @opencodejr

    Thanks for the report. I could reproduce this.

    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.

    Closely related:

    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 instead
    if [ -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

    Run it: bash repro.sh

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions