anomalyco / anomalyco/opencode

One-shot `run` exits before plugin-backgrounded work completes; plugins cannot detect a headless client on the shared service

Open
#48,683 1 comment 0 reactions 1 assignee View on GitHub

@jlongster is already working on this.

Since Sep 12, 2026.

2.0
Dominant language
TypeScript
Stars
209k
Forks
27.5k
Avg merge
7h 2m
Merged PRs (30d)
384

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.syntheticwake, 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:

  1. A one-shot run waits 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.
  2. The plugin context or the execute.before payload 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 on ctx).

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 -i isolated root, provider via LiteLLM proxy
  • Plugins involved: two small internal plugins that set input.background = true
    in ctx.tool.hook("execute.before", ...) for the shell and subagent tools
    respectively, scoped to top-level sessions via ctx.session.get(...).parentID.
    Happy to share a minimal reproduction plugin if useful.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.