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
[BUG] V2: subagent with pending background work (task/shell, background: true) marked completed early — results never collected #48826
A V2 subagent with pending background work (task/shell with background: true) ends its turn and is reported to its caller as completed while the work is still running — the parent is notified early with the subagent's premature reply.
When the work finishes, the subagent is woken and produces its real final answer, but that answer is never delivered to the parent. The result is stranded in the subagent session.
Nested variant of #45480 / #35066; contradicts the task tool's "You will be notified automatically when it finishes."
Still present on opencode 2.0.21 (2026-10-01)
Re-verified live on opencode 2.0.21 with two independent runs (--standalone, isolated config, transcripts read back from the database). No experimental flag is needed anymore — OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS no longer exists in 2.0.21 and background subagents are core, yet the delivery failure reproduces unchanged:
Child ran a background shell (sleep 20; ...), ended its turn with EARLY_REPLY at +1s.
Shell completed at +21s; the child was woken by the <shell state="completed"> synthetic and produced REAL_FINAL at +22s.
The parent received exactly one subagent notification — state="completed" carrying EARLY_REPLY — and never received REAL_FINAL. The result remains stranded in the child session.
Second run, parent transcript (ses_f09a61904ffeKwQIrO799zLEVB):
The subagent's completion notification is produced when the child's job first settles (its first idle), so the parent is told completed with the premature reply. When the pending background work later wakes the child and it produces its real final answer, that notification pathway has already been consumed and nothing delivers the real result. A fix exists in #49305 (still unmerged as of 2026-10-01).
Environment
opencode 2.0.21 (current) — re-verified live 2026-10-01; originally observed on 2.0.8, re-verified on 2.0.11 and 2.0.5
experimental.subagent_depth: 5
Background subagents are core in 2.0.21; the OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS flag no longer exists
Reproduction
Subagent A starts background work (task or shell with background: true), then ends its turn.
Parent is notified completed with A's early reply, while the work is still running.
Work finishes; A is woken, collects it and produces its real final answer.
Parent never receives that answer.
Live re-verification history
2.0.11: parent notified completed at +6.5s with the premature reply; shell finished at +25.2s; subagent woken and produced REAL_FINAL; parent never received it.
2.0.5: parent notified at +16s, 6s before the shell finished; child woken at +22s; real result never delivered.
2.0.21: see above — same shape, two runs.
Expected
Parent notified only once A is quiescent (no pending background work left), or woken again with the real result.
Actual
Parent notified early with a premature reply; A's real result is never delivered.
changed the title [-][BUG] V2: background subagent result never collected — parent marked completed while child still running[/-][+][BUG] V2: subagent with pending background work (task/shell, background: true) marked completed early — results never collected[/+]on Sep 13, 2026
Still broken on v2.0.5 — reproduced live (Linux, background subagents enabled). Fix #49305 is unmerged (BLOCKED) and the binary lacks its symbols.
Timeline (t=0 = spawn; child starts a sleep 20 background shell, then ends its turn early):
+3s — child idle succeeded, shell still running
+16s — parent gets the only notification: completed + early reply, 6s before the shell finished
+22s — shell completes; child is woken and produces its real result
parent never receives it — nothing pending, result stranded in the child session
Nuance: the wake did reach the child here; the parent-side delivery failure is what reproduces. An isolated opencode run --standalone trial also showed the early notification, and there the pending shell never completed (likely killed at run exit) — possible extra severity variant.
@Ploppy3 thanks for explaining this, we are thinking through how to approach it. definitely aware of it. need to solve this for other use cases too where something needs to know if there's anything running in a session
we will fix this, will take a little time to figure out the right APIs/states but we'll fix it!
Corroborating with a fully-traced instance of the exact shape described.
Worker with pending background shell processes: parent notified state="completed" at 14:57. The background shell finished at 15:22:07 — the child's transcript shows the wake source explicitly: a <shell id=… state="completed"> synthetic at 15:22:07, followed by a final verification turn and the real final report, idle at 15:26:23. Never delivered; the answer was stranded exactly as described. The parent then held its turn ~2.5 h (~200 no-op turns) waiting for a result that had already happened.
Recovery today is only out-of-band:GET /api/session/{id}/message readback. Parent agents cannot self-detect this — the only ground truth is the HTTP API, and agent-side webfetch/Code-Mode fetch cannot authenticate to it (401 without CLI credentials), so there is no agent-facing way to ask "what is my child's actual state?"
Population. Same local scan as the sibling reports: 44 instances of "terminal >60 s after last notification" over 09-10 → 09-25 (v2.0.16 back to beta-19425); losses concentrate on children with background shells or resumes; fresh-spawn first completions always delivered. Error-drain controls still notify, so it is not the drain-failure path.
Can share the same sanitized form for this instance — event types, timestamps, opaque IDs, and the synthetic injection points only; no transcripts, paths, or project names.
@Ploppy3 just an update, we plan on changing it so that a subagent isn't "completed" and send a message back until all background tasks are complete. Still finalizing the api changes for it, it should land next week
This affects external orchestrators too, not just task subagents. We drive OpenCode 2.0.32 sessions from another app as delegated workers. When a worker starts a long background shell (a test gate), it often ends its turn with "waiting for the completion notification". The orchestrator sees that first idle and treats the worker as finished, even though OpenCode later wakes the session on <shell state="completed"> and the work continues.
Of 23 suspicious turn ends we classified across two days, about 14 were this pattern. A session-level "background work pending" state, exposed to API clients alongside idle as discussed above, would let clients tell "done" from "waiting".
Same pattern hits top-level sessions and plugins, not only subagents. On 2.1.0 I run OpenCode inside Orca. The agent started a long background shell (whisper on a ~1 h audio) and ended its turn. Orca's status integration marked the session done and sent a "finished" notification while the process kept running for several more minutes. The built-in opencode.notifications TUI plugin does the same: it says "Session done" on session.execution.succeeded, with no way to know background work is pending.
+1 to the session-level "background work pending" state @thymikee asked for. Once you settle the shape, I'm happy to send a PR, e.g. expose running background jobs per session (from Job.runningBackground) so clients and the notifications plugin can wait before reporting done.
Still reproducible on 2.1.2 (2026-10-21) — live, isolated --standalone instance, task subagent starting a background shell, transcripts read back from the database.
12:45:23.253 child: EARLY_REPLY, ends turn
12:45:24.660 child idle -> parent notified state="completed" with EARLY_REPLY
(shell still running; 13s before it exited)
12:45:38.275 <shell state="completed"> synthetic wakes the child
12:45:39.587 child: FINAL_REAL_RESULT_42 -> never delivered
The parent's inbox had zero pending rows afterwards, and a second run (20s shell) showed the same shape. Source at tag v2.1.2 still has SubagentJob.start = sessions.resume(child) + read latest assistant message, with no background wait.
#57418 looks like the fix. Same prompt, same 15s background shell, A/B on source:
build
what the parent got
dev (base)
state="completed" + EARLY_REPLY at +3s, shell still running
nothing at first idle; notification only after the shell's completion notice is drained; tool returns FINAL_REAL_RESULT_42 at +21s
Its new tool-subagent.test.ts case passes (12 pass / 0 fail in that file).
One caveat for CI: the unit (linux) job on #57418 has a single failure (createStorage degrades gracefully when fs.watch throws), which looks unrelated to this change.
Thanks for the detailed report. I reproduced this on 2.0.21, and it still happens on the latest dev (b94a305).
Steps, using a simulated model:
The parent starts a subagent with background: true.
The subagent starts shell with background: true (sleep 10; …) and ends its turn with EARLY_REPLY.
The parent is woken straight away by the "finished" notification, which carries EARLY_REPLY.
About 10s later the shell finishes. The subagent is woken and replies REAL_FINAL.
The parent never gets another notification. A follow-up prompt to the parent shows REAL_FINAL never reached its history.
The recording and screenshot below show the run on dev, and the script below reproduces it.
Related: #50751 (later turns of a resumed background subagent are never reported to the parent) and #45480 (umbrella issue on task lifecycle).
recording.mp4
Reproduction script (drive.ts)
// Repro for anomalyco/opencode#48826: a background subagent that ends its turn while its own// background shell is still running is reported to the parent as completed with its early// reply; its real final answer (produced after the shell finishes) never reaches the parent.import{Effect,Stream}from"effect"import{Llm,OpenCodeDriver}from"opencode-drive"constparentRequests: string[]=[]constlog: string[]=[]constt0=Date.now()constnote=(s: string)=>log.push(`+${((Date.now()-t0)/1000).toFixed(1)}s ${s}`)exportdefaultOpenCodeDriver.use({
...(process.env.OC_DEV ? {opencode: {dev: process.env.OC_DEV}} : {}),tui: {recording: true},project: {git: true},},({ ui, llm })=>Effect.gen(function*(){yield*llm.serve((request)=>{constbody=JSON.stringify(request.body)if(body.includes("PARENT_TASK")){parentRequests.push(body)if(body.includes("REAL_FINAL")){note("parent request contains REAL_FINAL")returnStream.make(Llm.text("PARENT_SAW_REAL_FINAL"))}if(body.includes("FOLLOWUP_CHECK")){note("parent follow-up request WITHOUT REAL_FINAL")returnStream.make(Llm.text("PARENT_NEVER_GOT_REAL_FINAL"))}if(body.includes("EARLY_REPLY")){note("parent woken with EARLY_REPLY notification")returnStream.make(Llm.text("PARENT_SAW_EARLY_REPLY"))}if(body.includes("working in the background")){note("parent: subagent launched in background")returnStream.make(Llm.text("PARENT_WAITING"))}note("parent: launching background subagent")returnStream.make(Llm.toolCall({index: 0,id: "call_subagent",name: "subagent",input: {agent: "general",description: "child job",prompt: "CHILD_TASK",background: true},}),Llm.finish("tool-calls"),)}// child sessionif(body.includes("CHILD_1DONE")){note("child woken after shell completed -> REAL_FINAL")returnStream.make(Llm.text("REAL_FINAL"))}if(body.includes("call_shell")){note("child ends turn with EARLY_REPLY while shell runs")returnStream.make(Llm.text("EARLY_REPLY"))}note("child: starting background shell")returnStream.make(Llm.toolCall({index: 0,id: "call_shell",name: "shell",input: {command: "sleep 10; echo CHILD_$((1))DONE",description: "slow work",background: true},}),Llm.finish("tool-calls"),)})yield*ui.submit("PARENT_TASK: delegate to a background subagent")yield*ui.waitFor("PARENT_SAW_EARLY_REPLY",{timeout: 60_000}).pipe(Effect.catch(()=>ui.screenshot("timeout")))// Give the child's shell time to finish and the child to write REAL_FINAL.yield*Effect.sleep(20_000)yield*ui.screenshot("after-wait")// Ask the parent again: its request shows whether REAL_FINAL ever reached it.yield*ui.submit("FOLLOWUP_CHECK: did the subagent send anything else?")yield*ui.waitFor((frame: any)=>JSON.stringify(frame).includes("PARENT_NEVER_GOT_REAL_FINAL")||JSON.stringify(frame).includes("PARENT_SAW_REAL_FINAL"),{timeout: 30_000},).pipe(Effect.catch(()=>Effect.void))yield*Effect.sleep(1500)yield*ui.screenshot("final")constreal=parentRequests.some((b)=>b.includes("REAL_FINAL"))note(`RESULT: parent ever received REAL_FINAL = ${real}`)yield*Effect.log(log.join("\n"))}),)
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.
Activity
[-][BUG] V2: background subagent result never collected — parent marked completed while child still running[/-][+][BUG] V2: subagent with pending background work (task/shell, background: true) marked completed early — results never collected[/+]Ploppy3 commentedon Sep 16, 2026
Fix opened in #49305 (targets
v2).Ploppy3 commentedon Sep 17, 2026
Still broken on v2.0.5 — reproduced live (Linux, background subagents enabled). Fix #49305 is unmerged (
BLOCKED) and the binary lacks its symbols.Timeline (t=0 = spawn; child starts a
sleep 20background shell, then ends its turn early):succeeded, shell still runningcompleted+ early reply, 6s before the shell finishedNuance: the wake did reach the child here; the parent-side delivery failure is what reproduces. An isolated
opencode run --standalonetrial also showed the early notification, and there the pending shell never completed (likely killed at run exit) — possible extra severity variant.jlongster commentedon Sep 23, 2026
@Ploppy3 thanks for explaining this, we are thinking through how to approach it. definitely aware of it. need to solve this for other use cases too where something needs to know if there's anything running in a session
we will fix this, will take a little time to figure out the right APIs/states but we'll fix it!
0x4nur4g commentedon Sep 25, 2026
Corroborating with a fully-traced instance of the exact shape described.
Worker with pending background shell processes: parent notified
state="completed"at 14:57. The background shell finished at 15:22:07 — the child's transcript shows the wake source explicitly: a<shell id=… state="completed">synthetic at 15:22:07, followed by a final verification turn and the real final report, idle at 15:26:23. Never delivered; the answer was stranded exactly as described. The parent then held its turn ~2.5 h (~200 no-op turns) waiting for a result that had already happened.Recovery today is only out-of-band:
GET /api/session/{id}/messagereadback. Parent agents cannot self-detect this — the only ground truth is the HTTP API, and agent-sidewebfetch/Code-Modefetchcannot authenticate to it (401 without CLI credentials), so there is no agent-facing way to ask "what is my child's actual state?"Population. Same local scan as the sibling reports: 44 instances of "terminal >60 s after last notification" over 09-10 → 09-25 (v2.0.16 back to beta-19425); losses concentrate on children with background shells or resumes; fresh-spawn first completions always delivered. Error-drain controls still notify, so it is not the drain-failure path.
Can share the same sanitized form for this instance — event types, timestamps, opaque IDs, and the synthetic injection points only; no transcripts, paths, or project names.
jlongster commentedon Oct 1, 2026
@Ploppy3 just an update, we plan on changing it so that a subagent isn't "completed" and send a message back until all background tasks are complete. Still finalizing the api changes for it, it should land next week
Ploppy3 commentedon Oct 3, 2026
@jlongster perfect, thank you very much for you hard work.
Guard the credentials in a plugin, and make the install gap visible
thymikee commentedon Oct 17, 2026
This affects external orchestrators too, not just
tasksubagents. We drive OpenCode 2.0.32 sessions from another app as delegated workers. When a worker starts a long backgroundshell(a test gate), it often ends its turn with "waiting for the completion notification". The orchestrator sees that first idle and treats the worker as finished, even though OpenCode later wakes the session on<shell state="completed">and the work continues.Of 23 suspicious turn ends we classified across two days, about 14 were this pattern. A session-level "background work pending" state, exposed to API clients alongside idle as discussed above, would let clients tell "done" from "waiting".
Ploppy3 commentedon Oct 19, 2026
@jlongster still not fixed, what is the current state of this?
menyo-factorit commentedon Oct 20, 2026
Same pattern hits top-level sessions and plugins, not only subagents. On 2.1.0 I run OpenCode inside Orca. The agent started a long background
shell(whisper on a ~1 h audio) and ended its turn. Orca's status integration marked the session done and sent a "finished" notification while the process kept running for several more minutes. The built-inopencode.notificationsTUI plugin does the same: it says "Session done" onsession.execution.succeeded, with no way to know background work is pending.+1 to the session-level "background work pending" state @thymikee asked for. Once you settle the shape, I'm happy to send a PR, e.g. expose running background jobs per session (from
Job.runningBackground) so clients and the notifications plugin can wait before reporting done.Ploppy3 commentedon Oct 21, 2026
Still reproducible on 2.1.2 (2026-10-21) — live, isolated
--standaloneinstance,tasksubagent starting a backgroundshell, transcripts read back from the database.The parent's inbox had zero pending rows afterwards, and a second run (20s shell) showed the same shape. Source at tag
v2.1.2still hasSubagentJob.start=sessions.resume(child)+ read latest assistant message, with no background wait.#57418 looks like the fix. Same prompt, same 15s background shell, A/B on source:
dev(base)state="completed"+EARLY_REPLYat +3s, shell still runningFINAL_REAL_RESULT_42at +21sIts new
tool-subagent.test.tscase passes (12 pass / 0 fail in that file).One caveat for CI: the
unit (linux)job on #57418 has a single failure (createStorage degrades gracefully when fs.watch throws), which looks unrelated to this change.opencodejr commentedon Oct 28, 2026
Thanks for the detailed report. I reproduced this on 2.0.21, and it still happens on the latest
dev(b94a305).Steps, using a simulated model:
subagentwithbackground: true.shellwithbackground: true(sleep 10; …) and ends its turn withEARLY_REPLY.EARLY_REPLY.REAL_FINAL.REAL_FINALnever reached its history.The recording and screenshot below show the run on
dev, and the script below reproduces it.Related: #50751 (later turns of a resumed background subagent are never reported to the parent) and #45480 (umbrella issue on task lifecycle).
recording.mp4
Reproduction script (drive.ts)
Run it with opencode-drive:
opencode-drive run ./drive.ts. It drivesopencodefrom yourPATH; setOC_DEVto the path of an OpenCode checkout to run that from source instead.