Repository navigation
cli(win): 45s event-stream idle watchdog restarts the managed service, aborting all sessions/subagents #52049
Description
Description
On Windows the v2 managed background service (opencode serve --service) is repeatedly killed and respawned during normal use, aborting every in-flight session and background subagent ("failed"). It is not a crash — the client is killing it.
Every restart is preceded by the client log line event stream disconnected ... error="Event stream stalled", then ~6s later a new cli starting ... args=["serve","--service"].
The direct trigger is the client's event-stream idle watchdog (values read from the shipped 2.0.19 binary):
- server sends
: heartbeaton/api/eventevery 15s; - client resets a 45s idle timer on any received byte (
idleTimeout ?? 45000); - after 45s with no bytes the client aborts with
Event stream stalled, then treats the service as unresponsive and replaces it (Background service is unresponsive; recovery cannot preserve persistent terminals,Failed to shut down persistent terminals before service replacement) instead of reconnecting.
Measured correlation: server's last log line 08:47:12.749, stall logged 08:48:40.688 → 46.1s, i.e. the 45s watchdog, not the ~6s health probe.
This is the same class as #50594 (Windows: file-watcher re-registration storm on ~/.claude/skills → serve --service disappears with no error/dump) and #50591 (Linux: Event stream stalled + InterruptError on /api/event). The new information here is that the service is not dying on its own: the client kills it after a 45s heartbeat gap, so the "silent death" in #50594 is a client-side replacement, and the fix must also cover the watchdog margin / the replace-instead-of-reconnect policy.
Ruled out on this machine:
- Not a crash — no
opencode.exeAPPCRASH in the Windows Application log; no crash dumps under%LOCALAPPDATA%\CrashDumpsor WER (noRADAR_PRE_LEAK_64either). - Not OOM — 23.7 GB RAM, 6.7 GB free, page file usage ~205 MB.
- Not graceful shutdown — 2
shutting downlines in 12 days; the old process logs nothing before disappearing. - Not the subagents — they are victims of the replacement.
Frequency (opencode.log, one machine): Event stream stalled = 3× on 2026-09-28, 6× on 2026-09-29; serve --service starts = 7 and 6 on those days. It stops completely when idle (12+ min with zero restarts, liveness probe 0–2ms).
Suspected trigger for the ≥45s heartbeat gap (evidence below, hypothesis): concurrent cold-boot of many locations plus watcher re-registration churn, forming a feedback loop — replace service → all recently-active locations cold-boot → watcher/config/MCP churn blocks the loop → heartbeat gap ≥45s → watchdog replaces the service again.
Plugins
- local
~/.config/opencode/plugins/orca-opencode-status.js - local
~/.config/opencode/plugins/win-notify/ - no
pluginarray inopencode.json
(~/.claude/skills and ~/.agents/skills are both 44-entry directories and are re-subscribed ~950× each — see Additional context.)
OpenCode version
2.0.19 (channel=latest, npm global @opencode/cli)
Steps to reproduce
Intermittent, load-dependent; not reproducible on demand.
-
On Windows with opencode 2.0.19 CLI/TUI, let the shared managed service run (
~/.local/state/opencode/service.json→http://127.0.0.1:49372). -
Have several locations active (home directory plus several projects on other drives) and run subagent-heavy turns.
-
Observe repeatedly in
~/.local/share/opencode/log/opencode.log:08:48:40 client event stream disconnected ... error="Event stream stalled" 08:48:46 cli cli starting version=2.0.19 args=["serve","--service"] 08:48:47 client event stream connected -
In-flight work is aborted:
InterruptError: All fibers interrupted without error ... http.url=/api/event http.status=200. -
Restarts recur every ~2.5–16 min while busy; zero restarts while idle.
Screenshot and/or share link
N/A — log excerpts, code snippets and counts are inline below.
Operating System
Windows 11 Home Single Language, build 26200
Terminal
Windows Terminal (WT_SESSION set, TERM=xterm-256color), shell C:\WINDOWS\system32\cmd.exe
Additional context
Relevant client code (from the shipped 2.0.19 binary):
// event stream idle watchdog
var yY = 2000, Pv = 1000, gY = 50, bY = 45000, EY = 20000;
function wv(t, n) {
let u = n.idleTimeout ?? bY; // 45000 ms
...
let Ke = () => { x = Date.now(); ...; qe = setTimeout(() => te.abort(Error("Event stream stalled")), u) };
// onActivity resets the timer on ANY received byte
}// server SSE handler
let s = qK("15 seconds").pipe(wa(() => `: heartbeat\n\n`));// SSE reader: any byte counts as activity, including the heartbeat comment
if (!y.done) n?.onActivity?.();Watcher churn / location cold-boot evidence (hypothesis for the 45s gap):
stall 08:18:06 : watcher_subscribe=116 location_boot=8 spawns=40
stall 08:24:19 : watcher_subscribe=123 location_boot=9 spawns=12
stall 08:26:55 : watcher_subscribe=129 location_boot=10 spawns=6
| path | subscribes |
|---|---|
C:\Users\<user>\.claude\skills |
944 |
C:\Users\<user>\.agents\skills |
923 |
C:\Users\<user>\.config\opencode\skills |
250 |
C:\Users\<user> |
162 |
C:\, C:\Users, D:\ (type=entries) |
77 / 77 / 80 |
location services booted: C:\Users\<user> 73×, plus ~20 other project locations. This matches #50594 (.claude/skills storm) and #51852 (intermittent main-thread spin correlated with watcher churn).
Next step being tested: OPENCODE_DISABLE_FILEWATCHER=true (per #51852). Will report whether restarts stop under the same load.
Related issues: #50594, #50591, #51852, #51216, #37795, #48588, #36285.
Activity
github-actions commentedon Sep 29, 2026
This issue doesn’t fully meet our contributing guidelines.
What needs to be fixed:
Please edit this issue to address the above within 2 hours, or it will be automatically closed.
This issue might be a duplicate of existing issues. Please check:
Event stream stalledsymptom followed by a managed-service restart, though on Linux.serve --serviceand aborting active sessions.If you believe this was flagged incorrectly, please let a maintainer know.
github-actions commentedon Sep 29, 2026
Thanks for updating the issue with the Bug Report details and system information!
piknockyou commentedon Sep 30, 2026
Had the same failure: the session title gets created, but no reply ever comes - only the
server restarted while you were workingnotice, withevent stream disconnected / stalledin the log.Root cause for me: a stray empty
.gitfolder in my home folder. That hidden folder makes everything around it look like one big "git project" - so opencode treated my entire home folder as the project and scanned all of it for changes before each reply (541k files, ~26 seconds per scan, twice per message). That is longer than opencode's internal watchdog waits, so it assumes the service died, restarts it, and you get the loop.Quick check - run this in the folder where you start opencode:
git rev-parse --show-toplevel.gitis the problem. Delete that.gitfolder (accidents like this usually contain no commits, so nothing is lost - if unsure, just rename it to.git.bak). Then start opencode inside a normal project folder, and never rungit initin your home folder. This removed the failure completely for me.git ls-files --others --exclude-standard | wc -lIf that number is huge (100k+), opencode is choking on files that should have been skipped. List your big folders that don't belong to the project (
node_modules/,target/,build/, caches, datasets) in a.gitignorefile - one per line. As a last resort you can set"snapshot": falsein your opencode config, which switches the scan off (but you also lose the undo feature).Deleting the stray
~/.git/%USERPROFILE%\.gitwas enough for me.ptmr3 commentedon Oct 3, 2026
Reproducing this on macOS (Apple Silicon) with an additional deterministic trigger: the OpenCode desktop app running concurrently.
Versions: CLI v2.0.21 and v2.0.22 (identical behavior on both; the updater installed 2.0.22 mid-incident); desktop app v1.18.34.
Evidence (from
~/.local/share/opencode/log/opencode.log):Event stream stalledevents on 2026-10-02; zero on each of the four prior days — the signature appeared for the first time with this setupserve --servicespawns in ~9 minutes, one per ~57s; the cadence is machine-regular: connect → exactly ~46s →Event stream disconnected … error="Event stream stalled"→ ~11.5s → reconnect to a freshly spawned service (9 consecutive cycles)recovery cannot preserve persistent terminalsline at each killglobal event disconnected) and stays down for the entire churn; it reconnects only after the CLI processes are killed. The loop did not occur in a CLI-only setup (desktop app quit)./api/session/<id>/permission|form, zero sessions created — so the CLI is effectively unusable alongside the desktop app.Workaround: don't run the CLI TUI while the desktop app is running.
Happy to provide the full log window or any additional diagnostics from the macOS side.
ptmr3 commentedon Oct 3, 2026
Follow-up from our macOS reproduction with protocol-level evidence — this isolates the root cause precisely:
The service emits no SSE heartbeats at all. A direct probe (
curl -N --noproxy '*' http://127.0.0.1:<port>/api/event— no proxy, no interceptors, direct socket) received zero bytes for 42+ seconds — not even HTTP response headers — on an idle stream. The only emissions observed are boot-burst events (provider/model/agent/command.updated) and activity events; there is no keepalive.The watchdog is exactly 60.0s. Measured across multiple cycles: connect → exactly 60.00s of silence →
Event stream stalled→ the client kills the server child (PIDs observed cycling) → respawn (~11.5s) → reconnect → repeat indefinitely. Every kill logsInterruptError: All fibers interruptedatServerProcess.startserver-side.Environment fully excluded: no proxy env vars at any layer, no SSE-intercepting software installed, and the TUI stalls identically under a scrubbed
env -ienvironment as under the full user shell config.Cross-client confirmation: a second, independent client (a long-lived session through a different service instance) stalls the same way in every >60s idle window — 30 stalls across 7 runs. The failure is not specific to one TUI; any idle consumer trips it.
Nuance worth noting: the CLI works while ACTIVE — non-interactive
opencode runcompletes fine, sessions create, activity events flow. It is specifically idleness that kills it: for an interactive session, 60s of reading/thinking = service killed + respawn loop.Session orphaning: the replace-not-reconnect policy also orphans sessions — we observed sessions created against one instance 404ing ~0.9s later because the replacement instance answered. User-typed prompts can vanish this way.
Suggested fix scope: (1) the service should emit SSE keepalives (e.g.,
: pingevery 15-30s) on /api/event; (2) the client's idle watchdog should not treat a silent stream as fatal; and critically (3) the replace-not-reconnect policy should not kill the server child on reconnect.Reproduced on v2.0.22 (latest; brew anomalyco/tap/opencode-v2), macOS Apple Silicon.
Bearmancer commentedon Oct 5, 2026
Second Windows box (Win10 19045, v2.0.23): 8-day census — stall → service replacement occurs here too, but it is rare (3 stalls / 8 days) and the other branch (stall → plain reconnect) is also observable on the same machine.
Different machine from the report above (Windows 10 Pro build 19045 vs your Windows 11 build 26200; 2.0.23 vs 2.0.19), so this is an independent datapoint, not a reproduction of your counts.
Evidence (verbatim,
C:\Users\Lance\.local\share\opencode\log\opencode.log, UTC)Every
Event stream stalledthis machine logged on or after 2026-09-28 (the log starts 2026-09-27T22:39:12.657Z) — 3 total, alllevel=INFO:What followed each one:
serve --servicespawn06:41:25.562Z run=b0a22f0407:37:08.777Z run=d4ebd344Your client-side warning string does occur on this machine (2×, both
level=WARN, bothrole=cli):Preceding context of the 10-01 warning — competing spawns, then a port collision:
Replacement also happens without a stall. In the 2.0.22→2.0.23 update window on 2026-10-05 the client failed on connect, not on idle, and a new service was still spawned 6 s later:
Run
60edadc9's log trail ends at08:38:37.377Zwith no shutdown line and no such process in the live process table;92b823b7is the survivor (service.jsonpid 13344). Cause of the first spawn's death: not determinable from the log.Frequency (my log, my counting method)
Service spawns = lines matching
message="cli starting" … args=["serve","--service"], counted per UTC day:Total 69 service spawns in 8 days, versus only 3
Event stream stalledevents. So on this machine the 45 s watchdog stall is not the dominant spawn driver — most spawns come from version-mismatch restarts (5 auto-updates in that window: 2.0.18→2.0.19→2.0.20→2.0.21→2.0.22→2.0.23) and from the competing-spawn/port-collision pattern shown above. Zero stalls on 2026-10-04 and 2026-10-05 despite 12 and 4 spawns respectively.Full disconnect census (log starts 2026-09-27T22:39:12.657Z; all counted events are 2026-09-28 or later) (
message="event stream disconnected", 33 lines, alllevel=INFO):Transport: The socket connection was closed unexpectedly. For more information, pass `verbose: true` in the second argument to fetch()Timed out connecting to serverEvent stream stalled31 of the 33 reconnected for the same
run=within 0.1–9.3 s; 1 re-disconnected and then reconnected; 1 is the last line of its run (process exit). Crash checks: no disconnect has alevel=ERRORline as its next line, or within the next 5 lines (0 of 33); the first ERROR after the 10-05 update window is the 3× EPERM block 2 minutes after its disconnect (cited in counter-row 5);tui bootstrap failednever appears as an application log line (the only whole-log matches are our own grep commands echoed back);UnknownErrornever appears except inside my own grep commands echoed back by the server; the Windows Application log holds 78Application Error(providerApplication Error, ID 1000) events in the last 8 days, 0 of them mentionopencode.Expect vs Actual
serve --service6.3 s and 7.0 s later, and theBackground service is unresponsive; recovery cannot preserve persistent terminalswarning fired twice — but on this machine it is rare (3 stalls / 8 days), one stall recovered by plain reconnect in 1.3 s, and the far more common spawn source is version-mismatch restarts plus competing spawns that collide on the managed port.Steps to reproduce (observed, not reproducible on demand)
http://127.0.0.1:49374./api/event(heartbeat interval 15 s per your analysis) — happens ~3 times per 8 days here.~/.local/share/opencode/log/opencode.logforerror="Event stream stalled"and, 6-7 s later,args=["serve","--service"]./api/eventhandler dies withInterruptError: All fibers interrupted without error … http.method=GET http.url=/api/event http.status=200(example from the 2.0.23 update:timestamp=2026-10-05T08:38:28.433Z level=INFO run=38b70a8a cause="InterruptError: All fibers interrupted without error […] http.method=GET http.url=/api/event http.status=200).Counter-scenarios tried
event stream disconnectedlines with the samerun='s nextevent stream connectedlevel=INFO; 31 reconnected in 0.1–9.3 s; 1 re-disconnected then reconnected; 1 = final line of its run (process exit)Get-CimInstance Win32_Process -Filter "Name='opencode.exe'"(PID/PPID/CreationDate/CommandLine)serve --service(08:38:48Z)~/.local/state/opencode/service.json{"id":"45b432f6-…","version":"2.0.23","url":"http://127.0.0.1:49374","pid":13344,"password":"<redacted>"}Get-ChildItem ~\.local\state\opencode -Filter *.tmp -Recurse.tmpfiles from interrupted atomic writes are wedging the registration"level=ERRORin 08:38:25Z–08:40:46ZEPERM … rename 'session.json….tmp'at 08:40:47.458-459Z, a separate mechanism, not transport)Environment
Scope note
This does not contradict your measurement — different machine, different load, newer version. What it adds: (a) the stall→replace branch reproduces on a second Windows box with your exact warning string; (b) the same box also shows the benign branch (stall → 1.3 s reconnect, no replacement) and a non-stall replacement path (connect timeout → new spawn); (c) stall counts alone under-count replacements here, because version-mismatch restarts and competing spawns on the managed port dominate the 69 spawns/8 days.
Brams-s commentedon Oct 8, 2026
Linux datapoint: same stall → replace path. Here the trigger is host CPU saturation, and the replacement makes things worse.
Environment: Linux 6.8 (Ubuntu), x86_64, 24 cores / 64 GB.
@opencode/cli2.0.24 daily, with one managedserve --serviceshared by about 10–19 TUI clients (one per Herdr pane) plus subagents. Plugin: oh-my-opencode-slim 3.0.3. The file watcher is disabled (OPENCODE_DISABLE_FILEWATCHER=1), so the watcher storms from #50594 / #53416 are ruled out here.What happens
event stream disconnected ... error="Event stream stalled"(some alsoTimed out connecting to server).Background service is unresponsive; recovery cannot preserve persistent terminals. The old server is SIGKILLed: a systemd user unit supervising it recordedMain process exited, code=killed, status=9/KILL, with no kernel OOM-killer entry. A newserve --serviceis started, and every in-flight turn and subagent in every session is aborted.Event stream stalledin two replacements I measured, consistent with the 45 s idle watchdog in this issue. These are freezes, not crashes: Linux PSI showedmemory full avg300≈9%and very high CPU run queues at the time.Frequency, over two days on one machine
27 stall bursts, each with 1–34 clients logging the stall within the same minute:
Why replacing makes it worse here
serve --servicewithin ~2 s. New servers answered/api/infowith 503 while booting. On Oct 7 at 17:05, two servers came up concurrently and one shut down after losing the port race.Suggestions (in addition to the watchdog margin and heartbeat points raised above)
/proc/<pid>/statstate is R/S/D), back off and reconnect instead of SIGKILLing. Better still, keep the heartbeat on a timer that a busy event loop can't starve.Happy to provide log excerpts or run a build with extra logging.
fix(client): reconnect transient stalls instead of replacing the serv…
luhfilho commentedon Oct 11, 2026
Confirming on macOS and adding the multi-client angle, which makes the restart loop much worse.
Environment: opencode
2.0.26, macOS (Darwin 27.0.0, arm64), iTerm.app, zsh. No plugins.~/.opencode/bin/opencode, channellatest.Same client-side replacement. Three TUIs were attached to the one shared
serve --service. When the daemon's event loop ran long (see aggravators below), all three declared the stream stalled in the same millisecond, then all three spawnedserve --servicewithin the same second — a restart stampede on a single port:Stalls/restarts repeated every 1–3 minutes (19:33, 19:43, 19:49, 20:05, 20:09, 20:17, 20:28, 20:31, 20:34, 20:36, 20:39 …), each stall followed ~11 s later by a fresh
serve --service. Same class as #50591/#50594, and overlapping #54181 ("restart stampede races the service slot").Also on macOS:
service set|unsetrecycle the live daemon. Besidesserve --service,opencode service restart|set|unsetare forwarded to the real binary and go through~/.config/work-managed port49374, so a config write (service set env …) also kills the shared daemon and drops every session. Only special-casingserve --servicewhile ignoringservice set|unsetis not enough.Aggravators on this machine (not the root cause, but what made the loop constant):
~/.local/share/opencode/opencode.dbwas 4.5 GB (part1.6 GB +session_message1.6 GB), WAL checkpoint 2.1 GB / 246 s — the daemon pegged one core for minutes. After the DB was migrated/compacted to 1.7 GB the pressure dropped.peekaboo) whoseinitialize/tools/listtook ~30 s and timed out (mcp connect failed server=peekaboo … Request timed out,http.span=30899), re-attempted on every restart.Workaround used: a shim named
opencodethat forserve --serviceadopts an existing listener on the port (holds the client open) instead of starting a second daemon, and that refusesservice restart|stop|start|set|unsetwhile a daemon is alive. With it the daemon survived 20+ minutes instead of being replaced every 1–3 minutes. This only masks the client behaviour — the real fix is on the client side.Suggestion: on
Event stream stalled, retry/reconnect the SSE stream (or fall back to polling/refetch) and only start a service when nothing is listening on the port yet. Ideally also treatservice set|unsetas non-destructive config writes that never recycle the running service.