Repository navigation
Description
Summary
A V2 subagent with any pending background work 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.
This happens with every kind of background work a subagent can start in V2:
- a background shell:
shellwithbackground: true - a background subagent (nested):
subagentwithbackground: true
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 subagent tool's "You will be notified automatically when it finishes."
Still present on opencode 2.2.6 (2026-11-07)
Re-verified on opencode 2.2.6 (latest release) with opencode-drive and a simulated model, once per kind of background work. Both fail the same way.
Background shell. Parent → child (subagent, background: true) → child runs shell with background: true (sleep 10):
+2.1s parent launches the child in the background
+2.2s child starts the background shell, ends its turn with EARLY_REPLY
+2.2s parent woken with state="completed" + EARLY_REPLY (shell still running)
+12.2s shell completes; child woken, produces REAL_FINAL -> never delivered
+22.8s follow-up prompt to the parent: REAL_FINAL is not in its history
Background subagent (nested). Parent → child (subagent, background: true) → child launches a leaf subagent with background: true (the leaf runs sleep 10), experimental.subagent_depth: 5:
+2.2s parent launches the child in the background
+2.3s child launches the background leaf, ends its turn with EARLY_REPLY
+2.3s parent woken with state="completed" + EARLY_REPLY (leaf still running)
+12.4s leaf finishes; child woken by <subagent state="completed">, produces REAL_FINAL -> never delivered
+22.8s follow-up prompt to the parent: REAL_FINAL is not in its history
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 withEARLY_REPLYat +1s. - Shell completed at +21s; the child was woken by the
<shell state="completed">synthetic and producedREAL_FINALat +22s. - The parent received exactly one subagent notification —
state="completed"carryingEARLY_REPLY— and never receivedREAL_FINAL. The result remains stranded in the child session.
Second run, parent transcript (ses_f09a61904ffeKwQIrO799zLEVB):
07:25:00 child starts background shell, ends turn
07:25:01 child: EARLY_REPLY (idle)
07:25:21 child woken: <shell state="completed">; marker written
07:25:22 child: REAL_FINAL (idle) — stranded
07:25:46 parent receives: <subagent state="completed">EARLY_REPLY</subagent>
07:26:06 parent finishes; no REAL_FINAL notification ever arrives
Cause
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. Nothing checks whether the child still owns running background work, whether a shell or a subagent. When that 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 was proposed in #49305 (closed unmerged); #57418 is the current open fix.
Environment
- opencode 2.2.6 (current) — re-verified 2026-11-07; previously re-verified on 2.0.21, 2.0.11 and 2.0.5, originally observed on 2.0.8
experimental.subagent_depth: 5- Background subagents are core since 2.0.21; the
OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTSflag no longer exists
Reproduction
- Subagent A starts background work —
shellwithbackground: true, orsubagentwithbackground: true— then ends its turn. - Parent is notified
completedwith 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.2.6: both background
shelland backgroundsubagentreproduce; see above. - 2.0.21: see above — same shape, two runs.
- 2.0.11: parent notified
completedat +6.5s with the premature reply; shell finished at +25.2s; subagent woken and producedREAL_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.
Expected
Parent notified only once A is quiescent (no pending background shell or subagent left), or woken again with the real result.
Actual
Parent notified early with a premature reply; A's real result is never delivered.
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.cevr commentedon Nov 6, 2026
I hit this too and have a fix on my fork, in case it helps the API design: cevr@80da76e (+86 src lines, with a live and a restart-recovery test).
Jobtracks each backgrounded job's completion notice until its owner session admits it (undelivered(sessionID)/awaitDelivered). A removed session releases its notices.SessionBackground.awaitQuiet).So the parent gets one notification, carrying the child's real final answer. Same rule covers a child waiting on its own background subagent. Still reproduces on dev @ 885df74 without it. Happy to open a PR if this shape fits what you had in mind.
Lolle2000la commentedon Nov 6, 2026
Nested subagent variant on 2.2.5
The same failure reproduces when the child's pending background work is another subagent, not only a background shell/task. This is the shape a depth-2 orchestrator uses:
leadcallssubagent({ agent: "orchestrator", prompt: ... })— foreground orbackground: true.subagent({ agent: "explore", background: true, prompt: ... })and ends its turn. Its tool result isThe subagent is working in the background (sessionID: ...).explorefinishes; the orchestrator is woken by the<subagent state="completed">synthetic and produces its real final report.leadonly ever receives the orchestrator's first-turn "working in the background" text. The real report is stranded in the orchestrator session.Why it follows the same path
SubagentToolstarts a job whose run isSession.resume(childSessionID), then returns the last completed assistant message.Session.resumecompletes when the session's drain loop has no pending input. It does not wait for descendant jobs.Job.blocktherefore returnsfinishedat the child's first idle, andSubagentJob.notify(the synthetic completion back to the parent) waits on that same job, so it fires at the same point.Version
Reproduced consistently on 2.2.5 (Linux,
experimental.subagent_depth: 2). The planned "do not complete until all background tasks are done" behavior is not in this build: #49305 is closed unmerged.Workaround for the nested variant
Force the nested agent's own
subagentcalls to the foreground with anexecute.beforetool hook that rewritesevent.input.background = false. Its turn then stays open until its children return, and the final report flows back through the normal tool result. Parallelism is preserved because the tool calls of a single assistant step execute concurrently.One related API gap surfaced while building a steering workaround: the V2 plugin client (
ctx.session) does not exposesession.background, even though the server endpoint exists and the TUI uses it. A plugin that needs to unblock a session stuck in a foreground tool call has to read the local service registration and callPOST /api/session/{id}/backgrounddirectly.[-][BUG] V2: subagent with pending background work (task/shell, background: true) marked completed early — results never collected[/-][+][BUG] V2: subagent with pending background work (shell commands or subagents, background: true) marked completed early — results never collected[/+]nkoynov commentedon Nov 8, 2026
Same on 2.2.6, and the code is unchanged on dev ff4a652.
SubagentJob(packages/core/src/session/subagent-job.ts) is only started by the subagent tool, command subagents and restart recovery, and its run issessions.resume(child). When the child's own shell or nested subagent finishes, the notice wakes the child throughsessions.synthetic→execution.wakewith no job attached, so that turn's result has nowhere to go.SessionBackground.outstandingeven notes that the child's shell wakes it again.We hit it on a real install. A background child ended its turn while its own shells and subagents were running, got woken six times over about an hour, and its final report never reached the parent, which then sat idle for ten hours.
We've been running a fix on 2.2.6, checked in a VM with a repro like the ones above: the child starts
sleep 45; exit 5in the background, replies CHILD-STARTED and ends its turn; the parent gets that, and 45 s later it also gets CHILD-EXIT=5.SubagentJobwatchessession.execution.started, and when a subagent child's turn has no running job and was started only by synthetic notices, it starts a job that just joins that execution (awaitIdle). It never starts a turn itself, and the result goes through the normal report path. A call that steers the child in the meantime joins the same job, so there's still one report. One pitfall:Bus.subscribeonly delivers events for the subscriber'sLocation, so the global node has to drop the location it was built in, or it misses other projects.This is a different direction from cevr's: it keeps the early reply and adds a report for each self-woken turn, instead of holding the first report back. Happy to open a PR against this issue if that works for you.