Repository navigation
Description
Summary
A one-shot opencode run exits as soon as the parent turn ends, without waiting for
work that a plugin moved to the background during that turn. The result is
exit 0 with the delegated work silently absent from the output. The failure is
intermittent: a fast background job finishes before teardown and the output is
complete; a slow one is dropped.
Separately, nothing reaching a plugin's tool.hook("execute.before") distinguishes a
headless run client from an interactive one on the shared serve --service
process, so a plugin cannot avoid the problem by scoping itself.
Measured on opencode2 v0.0.0-beta-19059, Linux, 2026-09-13.
Reproduction
Isolated HOME/XDG_*, minimal config with one provider and one sub-agent.
A plugin that sets input.background = true on subagent calls in
ctx.tool.hook("execute.before", ...). Prompt: delegate once to a sub-agent whose
task takes ~60s (sleep 60; echo PROBE_TOKEN), then report the token.
| exit | wall | token in stdout | |
|---|---|---|---|
opencode run --standalone, no plugin |
0 | 74.3s | yes |
opencode run --standalone, plugin |
0 | 10.7s | no |
opencode run (shared serve --service), plugin |
0 | 9.7s | no |
The 10.7s output reads: "I launched the subagent (sessionID: …) to run the sleep
command and return the token. I'll wait for it to finish and then reply with its
exact result." The process then exits. The child's completion is delivered by
ShellTool.notifyWhenDone / Session.synthetic → wake, but by then there is no
process to wake.
With a 5s delegated task the token does appear, which is what makes this
hard to notice.
What a plugin can and cannot see
Dumped from a diagnostic plugin during a live run:
execute.before payload : { tool, sessionID, agent, messageID, id, input }
ctx.session.get(...) : { cost, id, location, projectID, time, title, tokens }
ctx.app : { name: "cli", version: "0.0.0-beta-19059", channel: "beta" }
None identifies the client. The x-opencode-client header is built from
app.name and so is cli for every client. The only discriminator found is the
server process argv: run --standalone spawns a private serve --stdio --port 0,
while plain run uses the shared serve --service. A plugin can detect the first
(and we now do) but not the second, because on the shared service a headless run
and the TUI are the same process.
Expected
Either or both of:
- A one-shot
runwaits for jobs it started (background shells, background
sub-agents) before exiting, or at minimum exits non-zero / prints a warning
when work it launched is still pending. - The plugin context or the
execute.beforepayload carries something a plugin
can use to tell a non-interactive one-shot client from an interactive one
(e.g. a session/client kind on the session record, or onctx).
Why it matters
Any plugin that backgrounds work by default (we ship two: one for shell commands,
one for sub-agent delegation, both scoped to top-level sessions) makes headless
automation lossy in exactly the situation nobody is watching. The --standalone
argv check covers the private-server case; the shared-service case has no
plugin-side fix.
Environment
opencode2 v0.0.0-beta-19059(@opencode-ai/cli)- Linux,
env -iisolated root, provider via LiteLLM proxy - Plugins involved: two small internal plugins that set
input.background = true
inctx.tool.hook("execute.before", ...)for theshellandsubagenttools
respectively, scoped to top-level sessions viactx.session.get(...).parentID.
Happy to share a minimal reproduction plugin if useful.
Activity
github-actions commentedon Sep 12, 2026
This issue might be a duplicate of existing issues. Please check:
opencode2 runandbackground: truetasks may cause premature exit #48361:opencode2 runandbackground: truetasks may cause premature exit — same core symptom: one-shotrunexits before background tasks completeopencode run(headless) exits 0 mid-pipeline after a parallel subagent fan-out, before the model's next turn #37548:opencode run(headless) exits 0 mid-pipeline after a parallel subagent fan-out — same premature exit behavior in headless modeopencodejr commentedon Oct 28, 2026
Thanks for the very detailed report and measurements! I think this is a duplicate of #48361. It reports the same problem: a one-shot
opencode runexits once the parent turn ends, while work moved to the background (there by the model withbackground: true, here by your plugin) is still running. So that work is missing from the output. On the currentdevbranch, the non-interactive run still finishes onsession.execution.succeededwithout checking for pending background shells or subagents.Possible duplicates and related issues, ordered by likelihood:
opencode2 runandbackground: truetasks may cause premature exit #48361:opencode2 runwithbackground: truesubagents exits before they finish. Same lifecycle problem.opencode2 runandbackground: truetasks may cause premature exit #48361): headless standalone run exits while background subagents are still running.run.Your second point, letting a plugin tell a non-interactive one-shot client from an interactive one on the shared
serve --service, is a separate request. You're right that nothing a plugin receives today identifies the client: not theexecute.beforepayload, notctx.session.get, notctx.app, and not thex-opencode-clientheader. If you'd like it tracked on its own, even oncerunwaits for background work, please say so here or open a feature request for it.The bot will close this issue as a duplicate in 1 day unless you reply explaining how your problem is different.
opencodejr commentedon Oct 29, 2026
I'm closing this as a duplicate of #48361. It reports the same problem: a one-shot
opencode runexits when the parent turn ends, while work moved to the background is still running, so that work is missing from the output. We haven't heard that your case is different. Please follow #48361 for updates, and #57418 for related work on waiting for background jobs.Your separate idea of giving plugins a way to tell a headless one-shot client from an interactive one isn't covered by #48361. If you'd still like it, please open a feature request for it. If your problem turns out to be different, reply here with the details and we can reopen this. Thanks again for the thorough report!