Skip to content

subagent: parent gets cancelled when an interrupt with resume=true continues a background subagent #54658

Open

Description

@heliowap

Description

If you interrupt a background subagent with resume=true while a steer prompt waits in its inbox, the subagent continues. It runs the steer prompt, replies, and its session ends with idle succeeded.

The parent session never sees that reply. At interrupt time, the parent receives <subagent state="cancelled">Subagent cancelled</subagent>. No later notification arrives.

The parent should receive state="completed" with the final reply. If no steer prompt waits, the subagent stops, and cancelled is correct.

#50751 also loses a subagent reply, but the trigger is different. In #50751, a new prompt reaches the subagent after it finished. Here, an interrupt stops the subagent while it runs, and the subagent resumes.

Plugins

None. The bug reproduces without plugins.

OpenCode version

2.0.26

Steps to reproduce

  1. In a parent session, call the subagent tool with background: true and a prompt that runs sleep 600.
  2. While sleep runs, send the subagent a steer prompt:
    opencode api post /api/session/<child>/prompt -d '{"text":"Write STEERED to a file, then reply DONE.","delivery":"steer"}'
  3. Interrupt the subagent with resume:
    opencode api post "/api/session/<child>/interrupt?resume=true"
    The server returns {"interrupted":true}.
  4. Read the parent messages. The last subagent notification has state="cancelled".
  5. Read the subagent messages with opencode api session.message.list --param sessionID=<child>. The file exists, the last assistant message says DONE, and the session ends with idle succeeded.

Operating System

macOS 26.5.2

Activity

  1. opencode-agent commented on Oct 12, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report. I was able to reproduce this on 2.0.26, and it still happens on the latest dev (707de58).

    Steps, using a simulated model:

    1. A parent session starts a background subagent whose shell call is held running.
    2. A delivery: "steer" prompt is sent to the child.
    3. interrupt?resume=true is called on the child and returns {"interrupted":true}.

    Afterwards, the parent's only subagent notification is state="cancelled". Meanwhile the child handles the steer prompt, replies DONE and ends idle succeeded. No completed notification reaches the parent. The script below reproduces it.

    The trigger is different from #50751, as you noted. The likely cause, not yet confirmed, is that the subagent's background job is settled as cancelled when the interrupt ends the current run, and the run that resumes for the steer prompt isn't linked back to the parent's notification.

    Reproduction script (drive.ts)
    // Repro for anomalyco/opencode#54658: interrupt?resume=true on a background subagent with a
    // pending steer prompt -> parent gets state="cancelled" although the child resumes and completes.
    import { Effect, Stream } from "effect"
    import { Llm, OpenCodeDriver } from "opencode-drive"
    
    const has = (req: unknown, s: string) => JSON.stringify(req).includes(s)
    
    export default OpenCodeDriver.use(
      {
        ...(process.env.OC_DEV ? { opencode: { dev: process.env.OC_DEV } } : {}),
        tui: { recording: true },
        tools: ["shell"],
      },
      ({ ui, llm, tools, opencode }) =>
        Effect.gen(function* () {
          const shells = yield* tools.control("shell")
          yield* llm.serve((req) => {
            if (has(req, "Write STEERED")) return Stream.make(Llm.text("DONE"))
            if (has(req, "<subagent")) return Stream.make(Llm.text("Noted."))
            if (has(req, "Launch a background subagent") && has(req, "working in the background"))
              return Stream.make(Llm.text("Launched."))
            if (has(req, "Launch a background subagent"))
              return Stream.make(
                Llm.toolCall({
                  index: 0,
                  id: "call_sub",
                  name: "subagent",
                  input: { agent: "general", description: "sleep task", prompt: "Run sleep 600", background: true },
                }),
                Llm.finish("tool-calls"),
              )
            if (has(req, "Run sleep 600"))
              return Stream.make(
                Llm.toolCall({ index: 0, id: "call_sleep", name: "shell", input: { command: "sleep 600", description: "sleep" } }),
                Llm.finish("tool-calls"),
              )
            return Stream.make(Llm.text("ok"))
          })
    
          yield* ui.submit("Launch a background subagent")
          yield* ui.waitFor("Launched.")
          const sleep = yield* shells.take("call_sleep")
          yield* sleep.progress("sleeping...\n")
    
          const all = yield* opencode.session.list()
          const items: any[] = (all as any).data ?? (all as any).items ?? (all as any)
          const child = items.find((s: any) => s.parentID)
          const parent = items.find((s: any) => !s.parentID)
          yield* Effect.log(`parent=${parent.id} child=${child.id}`)
    
          yield* opencode.session.prompt({ sessionID: child.id, text: "Write STEERED to a file, then reply DONE.", delivery: "steer" } as any)
          const r = yield* opencode.session.interrupt({ sessionID: child.id, resume: true })
          yield* Effect.log(`interrupt -> ${JSON.stringify(r)}`)
    
          yield* Effect.sleep(8000)
          const pm = yield* opencode.message.list({ sessionID: parent.id } as any)
          const cm = yield* opencode.message.list({ sessionID: child.id } as any)
          const pj = JSON.stringify(pm)
          const cj = JSON.stringify(cm)
          const states = [...pj.matchAll(/<subagent[^>]*state=\\?"(\w+)\\?"/g)].map((m) => m[1])
          yield* Effect.log(`PARENT subagent notifications: ${JSON.stringify(states)}`)
          yield* Effect.log(`CHILD replied DONE: ${cj.includes('"DONE"') || cj.includes("DONE\"")}`)
          yield* Effect.log(`CHILD messages: ${cj.slice(0, 700)}`)
          yield* ui.screenshot("parent-final")
        }),
    )

    Run it with opencode-drive: opencode-drive run ./drive.ts. It drives opencode from your PATH; set OC_DEV to the path of an OpenCode checkout to run that from source instead.

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions