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
After the restart, the old instance runs in the background #52638
The agent loop is not in the client. In 2.0 the turn runs in the detached background server the client spawns (spawn(process.execPath, [..., "serve", "--register"], { detached: true, stdio: "ignore" }).unref(), packages/cli/src/services/daemon.ts:122), so whatever restarted the TUI in window 1 could not have stopped it. The thing that is supposed to stop it is Daemon.stopProcess (daemon.ts:91), and on a version change it can skip the kill: both escalation gates are /health calls with a 2s timeout (healthy(), daemon.ts:66-73), not pid checks. SIGTERM goes out at :95, the wait is 100x50ms, then :101-102 re-runs healthy() and returns early when it fails, so the SIGKILL at :104 never reaches a server that has already stopped answering - which is the state a wedged graceful shutdown is in (#37521: signal delivered, consumed, nothing happens). start() spawns the replacement regardless, because stopProcess is Effect.ignored at :115.
I transcribed that ladder (health 2s -> SIGTERM -> 100x50ms -> re-health -> SIGKILL -> spawn + register) into a standalone Node script, since I can't build the 2.0 binary here, and drove two real processes with it - the old one wedged in graceful shutdown on an in-flight turn:
stopProcess: SIGTERM sent to 2827368
[daemon 2.0.20 pid=2827368] SIGTERM received: closing listener, awaiting in-flight agent turn
stopProcess: pre-SIGKILL health gate failed -> return, no SIGKILL
start: spawned replacement daemon pid= 2830340
old daemon pid 2827368 alive: true
SIGTERM sent: true | SIGKILL sent: false
-> live agent-capable servers: 2
The control run, with an old server that exits on SIGTERM, ends at live agent-capable servers: 1, so the gate is what makes the difference and not the harness. Two follow-ons from the same run: the orphan's own watchdog (daemon.ts:173-178) is SIGTERM-only with no escalation, so it cannot clear itself either (it self-signals at the 10s mark and stays up); and the replacement's register() (:166-172) overwrites server.json, so opencode service stop|status only ever sees the new pid and the orphan is unreachable from the CLI.
Until this is fixed: run opencode service stop before upgrading, and clear an existing orphan with pgrep -af "serve --register" plus kill -9 on the pid that is not the one in ~/.local/state/opencode/server.json.
For a fix, keying the escalation on process.kill(pid, 0) liveness instead of /health would cover it - the wedged server is by definition the one that stops answering - and letting start() surface the failure instead of Effect.ignore would stop a second daemon from being started silently.
Thanks for reporting this! In 2.0.23, service replacement was fixed to wait for the old process to exit and forcibly stop it if necessary, rather than leaving it running alongside its replacement. Update with opencode upgrade or npm i -g @opencode/cli@latest (desktop: update the app). Please comment or reopen if you still see competing instances on that version or newer.
Activity
github-actions commentedon Oct 2, 2026
This issue might be a duplicate of existing issues. Please check:
serveprocess continuing after an auto-update and affecting the newly started instance.aron-intframe commentedon Oct 2, 2026
The agent loop is not in the client. In 2.0 the turn runs in the detached background server the client spawns (
spawn(process.execPath, [..., "serve", "--register"], { detached: true, stdio: "ignore" }).unref(),packages/cli/src/services/daemon.ts:122), so whatever restarted the TUI in window 1 could not have stopped it. The thing that is supposed to stop it isDaemon.stopProcess(daemon.ts:91), and on a version change it can skip the kill: both escalation gates are/healthcalls with a 2s timeout (healthy(), daemon.ts:66-73), not pid checks. SIGTERM goes out at :95, the wait is 100x50ms, then :101-102 re-runshealthy()and returns early when it fails, so the SIGKILL at :104 never reaches a server that has already stopped answering - which is the state a wedged graceful shutdown is in (#37521: signal delivered, consumed, nothing happens).start()spawns the replacement regardless, becausestopProcessisEffect.ignored at :115.I transcribed that ladder (health 2s -> SIGTERM -> 100x50ms -> re-health -> SIGKILL -> spawn + register) into a standalone Node script, since I can't build the 2.0 binary here, and drove two real processes with it - the old one wedged in graceful shutdown on an in-flight turn:
The control run, with an old server that exits on SIGTERM, ends at
live agent-capable servers: 1, so the gate is what makes the difference and not the harness. Two follow-ons from the same run: the orphan's own watchdog (daemon.ts:173-178) is SIGTERM-only with no escalation, so it cannot clear itself either (it self-signals at the 10s mark and stays up); and the replacement'sregister()(:166-172) overwritesserver.json, soopencode service stop|statusonly ever sees the new pid and the orphan is unreachable from the CLI.Until this is fixed: run
opencode service stopbefore upgrading, and clear an existing orphan withpgrep -af "serve --register"pluskill -9on the pid that is not the one in~/.local/state/opencode/server.json.For a fix, keying the escalation on
process.kill(pid, 0)liveness instead of/healthwould cover it - the wedged server is by definition the one that stops answering - and lettingstart()surface the failure instead ofEffect.ignorewould stop a second daemon from being started silently.A cleanup step whose failure is swallowed so a duplicate starts quietly is a class we hit a lot; the checks we use for it are in our repo (ours): https://github.com/aron-intframe/agent-guards
neriousy commentedon Oct 20, 2026
Thanks for reporting this! In 2.0.23, service replacement was fixed to wait for the old process to exit and forcibly stop it if necessary, rather than leaving it running alongside its replacement. Update with
opencode upgradeornpm i -g @opencode/cli@latest(desktop: update the app). Please comment or reopen if you still see competing instances on that version or newer.