Closed as duplicate
Closed as duplicate
Copy link
Labels
Description
Summary
Calling subagent with a sessionID — intending to resume or steer a RUNNING background lane — behaves state-dependently. Against a lane mid-active-work (between tool calls) it silently FORKS: returns a NEW sessionID and spawns a fresh session carrying only the prompt, while the target keeps running. Against a lane held inside a long tool call it interleaves in place. Against a lane that has reported/completed it resumes in place reliably.
Environment
- opencode version: 2.0.30 (latest channel)
- OS: Linux 7.1-76070105-generic (linux x64)
- Terminal: TERM=xterm-256color, COLORMA=truecolor
- Shell: /bin/bash
- Install/channel: latest
- Active plugins: autocompact, elementbridge, lanetools, opencode-memory (local)
Reproduction
All steps below use neutral scratch tasks; no project content required.
- Dispatch a background
subagentWorker lane whose task keeps it inside one long tool call (e.g. "runsleep 150via your shell tool, then reply DONE"). - While it is tool-held, call
subagentagain passing that lane'ssessionIDplus a short directive. Observed: the SAME sessionID is returned; the lane answers the directive in-session and then completes its original task — in-place interleave (1/1 controlled test). - Repeat the same call shape against a lane that is mid-active-work (iterating read/grep/edit loops between tool calls). Observed: a NEW sessionID is returned and a fresh session spawns carrying only the prompt; the target lane keeps running — fork, 4/4 in a live session lineage.
- Contrast: the same call shape against a lane that has finished and reported resumes in place with the same sessionID, reliably across many uses.
Expected Behavior
A subagent call carrying an existing sessionID should resume/steer that session in place — or fail loudly if the platform cannot — and should never silently spawn a new session under a new ID.
Actual Behavior
- Mid-active-work targets fork silently (4/4); tool-held targets interleave (1/1); completed targets resume (reliable). The fork condition appears to correlate with the target's execution phase.
- The spawned sidecar receives ONLY the prompt — none of the target's conversation or task context. One such sidecar consumed 212,725 input tokens reconstructing context it was never given, then acted on it.
- The spawn payload carries no parent session ID, no worktree, no branch — a duplicated session cannot self-diagnose as a duplicate. Delivered steer text also showed literal
\nescape artifacts mid-prose. - Real impact path observed: a zero-context sidecar interpreted a status/freeze directive as work-to-do and wrote files into the wrong checkout (untracked files; cleaned up). It kept running ~40 minutes until stopped via
POST /api/session/{sessionID}/interrupt, which worked cleanly. - Possibly related: one live fork briefly returned unknown-session on a direct lookup while still running and writing; the same sessionID resolved normally on later lookup (may point at a spawn-registration gap rather than a lookup bug).
Additional Context
- Frequency: deterministic within phase — fork 4/4 (mid-active-work), interleave 1/1 (tool-held), in-place resume consistent (completed).
- Workaround in force: treat
sessionIDcontinuation as the handle for lanes that have REPORTED only; use synthetic-message channels for mid-run contact; never assume a fork inherits anything. - Hardening suggestion (echoed from the affected teams): stamp spawned sessions with parent sessionID + worktree + branch so duplication is self-evident to the spawn itself; and/or warn-refuse
sessionID-carrying calls against busy targets instead of silently forking. - No session logs or transcripts attached by design; everything above is reproducible with synthetic tasks as written.
Activity
opencode-agent commentedon Oct 16, 2026
Thanks for the detailed report! Before we try to reproduce this, we need a few things.
In 2.0.30, when the
subagenttool gets asessionID, it either fails (Subagent session not found/not a child of the current session) or prompts that same session (it starts the session if idle, or steers it if running) and returns the same ID. We couldn't find a code path that creates a new session when a validsessionIDis passed. A new session that gets only the prompt is what happens when the call has nosessionID, so we suspect those calls didn't actually include one, or a plugin changed them.Could you please share:
subagentinput (theagent,prompt,sessionIDandbackgroundfields) and the output it returned. A screenshot of the expanded tool call or the JSON of that message part is fine. We need to see thatsessionIDwas really sent.lanetools,autocompact,elementbridge,opencode-memory).lanetoolsin particular sounds like it might touch subagent calls.You can reply here with the details or edit the issue. If you edit it, please also leave a comment saying you've updated it, so we know you're done.
Related issues, ordered from most to least likely to be relevant:
sessionIDfield on thesubagenttoolcancelledwhen an interrupt withresume=truecontinues a background subagent #54658: problems continuing a background subagent after an interrupt withresume=trueEventideMiles commentedon Oct 16, 2026
Thanks for the sharp triage — we took it to the bench with controlled probes. Short version: you're right, and we can no longer reproduce the fork when the field is verifiably present. Consider this a correction with data.
What we ran (synthetic tasks only)
Three background Worker lanes running tight tool loops (
sleep 3-4× 15-18 iterations, one shell call per iteration — targets alternate between tool-held and between-calls phases). From the orchestrator seat, we re-invokedsubagentwith each lane'ssessionIDat three different phases: early loop, mid loop, deep loop.Result — 3/3 in-place, zero forks
usermessage mid-run (message 6 of 20), and the loop continued to completion with its iteration count unrestarted — genuine in-place interleave, matching the tool-held behavior we'd measured earlier with a single longsleep 150.What this means for the original report
The "state-dependent fork" framing was likely wrong. We could not reproduce a fork from a caller that verifiably composed the field, across phases. The original four "fork" calls came from a different orchestrator session on the same build and plugin set — per your #55504 link, the plausible mechanism is those calls being emitted without the
sessionID(model-side optional-field drop), which the platform then correctly treated as fresh spawns. The downstream impact data stands as reported: a spawn-that-should-have-been-a-steer burns enormous context reconstructing what it was never handed (212k input tokens in our incident) and can act on the wrong checkout. Our caller-side doctrine now treats "returned a different ID than the one I sent" as the live failure signal regardless of mechanism.Your specific asks
subagenttool —lanetoolsregisters its own namespaced tools only; the others hook compaction, a chat bridge, and memory. Happy to run the literal disabled-config rerun at our next scheduled restart if the source-level attestation isn't sufficient.subagentpassing the child's session ID ("steer"). Measured behavior when the field is present: routed in-place, message delivered into the child's transcript, same ID returned. The failure we originally reported — a fresh unrelated session receiving only the prompt (a "sidecar") — now looks like the session ID field being absent from those calls, not a routing fork.Secondary finding (possibly its own ticket)
/api/experimental/session/{id}/exportdrops tool-call inputs — assistant messages render with no text, so only user prompts survive. That makes post-hoc verification of "was the parameter actually sent" impossible from server data alone, which is precisely the question triage needs answered in #55504-class cases. Including tool-call inputs in exports would make optional-field drops self-diagnosable.We'll follow up with the redacted call example. Given the above, retagging/redirecting this toward #55504 as a duplicate-with-impact-data is reasonable if that fits your triage.
opencode-agent commentedon Oct 16, 2026
Thanks for testing this so carefully and writing up the results. With 3/3 in-place steers and no new sessions when
sessionIDwas really sent, this matches the code. In 2.0.30, asubagentcall with asessionIDeither reaches that session (starting it if idle, steering it if running) or fails with an error. A new session is only created when the field is missing. So your four original "forks" were most likely GLM-5.3 leavingsessionIDout of those calls.That makes this the same root cause as #55504: models getting the optional
sessionIDfield on thesubagenttool wrong. That report covers models filling it with made-up values, and yours is the other direction, dropping it. Your figures on the cost (212k input tokens and writes to the wrong checkout) are useful there, so feel free to add them as a comment on #55504. As you suggested, the bot will close this issue as a duplicate of #55504 in about a day unless you reply explaining how it's different. No need to rerun with plugins disabled.Your other two points are worth their own issues so they don't get lost here:
/api/experimental/session/{id}/export). A separate bug report with an example would be the best way to track it.If you do attach the redacted tool call, posting it on #55504 or in a new issue is more useful than posting it here.
opencode-agent commentedon Oct 17, 2026
Closing this as a duplicate of #55504, as discussed above. Your tests showed that a
subagentcall that really includessessionIDsteers that session in place. The new sessions came from calls where the model (GLM-5.3) left the field out, which is the same model-side problem with the optionalsessionIDfield that #55504 tracks.Thanks again for the careful follow-up. Your figures on the impact (212k input tokens, writes to the wrong checkout) and the redacted tool call would be useful as a comment on #55504. The export gap (tool-call inputs missing from
/api/experimental/session/{id}/export) and marking spawned sessions with their parent are still worth separate issues. If you think this is a different problem after all, reply here and we'll take another look.