Skip to content

desktop(win): background service SIGKILLed mid-turn when snapshot capture blocks /api/info for >6s -> ~60s restart loop, aborted turns #51216

Open

Description

@funKobii

Description

On Windows, the Desktop app kills its own background service while the service is
legitimately busy, which aborts the running turn and puts the app into a restart loop.

When a session's checkpoint work keeps the CLI's HTTP server busy for more than ~6 s,
the Desktop main process declares the background service "unresponsive", stops it
(SIGTERM -> SIGKILL) and spawns a new one. Every in-flight request is aborted
(HTTP 499), the running turn is interrupted, the app resumes it, the same work runs
again, and the cycle repeats roughly every 60 seconds.

User-visible result: the session is unusable. The activity list fills up with
"restart continuation" entries, background subagents end up "failed", and the app
never finishes a turn. In one day the CLI was started 111 times (vs. 3-6 starts/day
before the same app version), with 150 InterruptErrors.

The trigger in my case was the snapshot/checkpoint path:

timestamp=... level=INFO message=session.revert.stage sessionID=<session> messageID=<message> files=undefined http.span=862
timestamp=... cause="InterruptError: All fibers interrupted without error
    at Git.index.refresh (chunk-g2m6xm1v.js:24:2279)
    at Git.tree.capture (chunk-xm1wvpce.js:121:2381)
    at Snapshot.capture (chunk-xm1wvpce.js:121:4527)
    at SessionRevert.stage (chunk-xm1wvpce.js:137:1835)
    at Session.revert.stage (chunk-t9es9kf6.js:442:10196)"
    http.span=46646 role=server http.method=POST http.url=/api/session/<session>/revert/stage http.status=499

So POST /api/session/<id>/revert/stage ran for 46.6 s, blocked the CLI process, and
was then aborted by the app (499 = client went away, i.e. the service was killed).

While that capture was running I could verify the consequences directly:

  • netstat/raw TCP connect to the registered service URL succeeded (the port was
    listening), but GET /api/info never answered — repeated 8 s timeouts.
  • After the automatic replacement, the same endpoint answered
    200 {"version":"2.0.16","pid":<pid>,"urls":["http://127.0.0.1:<port>"],...} in ~5 ms.
  • Same request again after reducing the work: 46.6 s -> 1.22 s (HTTP 200).

The Desktop main log shows the loop (trimmed, repeated every ~59 s for hours):

[17:26:18.018] [info]  (main)    starting v2 background service
[17:26:24.103] [info]  (main)    v2 CLI background service starting { reason: 'missing' }
[17:26:24.609] [info]  (main)    v2 CLI background service ready { url: 'http://127.0.0.1:<port>', ... }

Relevant behaviour in the packaged app (resources/app.asar -> out/main/service-*.js):

{ pollInterval: 25, requestTimeout: 2e3, spawnDelay: 5e3, maxSpawnDelay: 3e4,
  promiseTimeout: 12e4, stopPollInterval: 50, stopPollAttempts: 100 }
  • the health probe is GET /api/info with a 2 s timeout;
  • after 3 consecutive timeouts the service is declared unresponsive and
    replaced (SIGTERM, then SIGKILL, service.json deleted);
  • the next ensure() then reports reason: 'missing' and spawns a new CLI;
  • /api/info returning 503 is only ever treated as state: 'waiting'.

In other words: a backend that is busy doing real work for >6 s is treated as dead,
and the kill takes the running session down with it.

Expected Behavior

Steps to reproduce

  1. On Windows, use a git project whose untracked, non-ignored tree is large
    (in my case: a repo created by another tool with a single empty commit, no
    .gitignore, 76,921 untracked/non-ignored files, ~11.8 GB, 76,458 of them
    under node_modules/).
  2. Open a session in that directory in the Desktop app and run a turn long enough
    that the app stages a checkpoint (POST /api/session/<id>/revert/stage).
  3. The request runs for tens of seconds; /api/info stops answering; the Desktop
    kills and restarts the CLI; the turn is aborted ("restart continuation"); repeat.

Additional Context

  • Interrupted endpoints in my logs: GET /api/event (156x, the session event
    stream), POST /api/rpc/experimental.browser/attach (9x), GET /api/vcs/diff
    (7x), POST /api/session/<id>/revert/stage (4x), POST /api/worktree/refresh (2x).
  • Workaround that stopped the loop immediately: add ignore rules for build
    artifacts (node_modules/, target/, .venv/, dist/, build/, caches) to the
    repo (both .gitignore and the snapshot view — note the snapshot repo uses its own
    git dir). Capture time dropped from 46.6 s to ~1.2 s and the service has now been
    running for hours without being replaced.
  • Two secondary problems found in the same snapshot code, both worth hardening:
    • a file named nul in the work tree makes every capture fail with
      error: invalid path 'nul' / error: unable to add 'nul' to index /
      fatal: adding files failed (reserved device name on Windows; created by a
      Linux-side tool) — 10+ failures per minute while a turn was running;
    • a stale index.lock left in the snapshot repo caused permanent
      failed to capture snapshot failures (331 occurrences over two days) with no
      self-healing (see snapshot: corrupt index causes repeated capture failures and unresponsive service #42237).

OpenCode version

OpenCode Desktop 2.0.16 (Windows installer), bundled/managed CLI 2.0.16,
channel=latest

Operating System

Windows 11 22H2 (10.0.22621) x64

Terminal

Desktop app (Electron) — not a terminal session

Related issues

Activity

  1. 4Liberty commented on Sep 27, 2026

    @4Liberty

    Independent confirmation from a heavily loaded Windows machine (32 logical cores, 61 GB RAM,
    many open projects, several agent sessions in parallel):

    The ~6-second window is the whole problem. Polling the service health endpoint from a second
    process every 500 ms for 7 minutes measured: 19 samples over 1 s, 9 over 2 s, worst 4.45 s,
    and four requests that did not answer within 4 s - all while the service was alive and its
    own CPU stayed under one core. The 2 s liveness timeout sits inside the service's normal tail
    latency under load, so any ~6-second burst becomes a kill + full respawn.

    Snapshot capture is one trigger, not the only one: after disabling snapshots here, the same
    loop persists with other long spans - POST /api/worktree/refresh and GET /api/vcs/diff running
    30-62 s and then aborting (HTTP 499) when the app gives up, plus bursts of parallel tool-call
    process spawns. Every replacement re-initializes everything: ~150 MCP server processes, ~32
    other MCP instances, and tens of thousands of file-watcher subscriptions - a full respawn
    storm exactly when the host is already saturated (measured 1-2 GB free RAM, 3.5-7.4k pages/s
    paging), making the next burst more likely.

    Suggestion along the lines already raised: derive liveness from something request work cannot
    block (a dedicated endpoint/thread, or "process alive + descriptor valid"), and/or back off
    replacement frequency instead of re-running full initialization on every strike.

  2. vheins commented on Sep 28, 2026

    @vheins

    Independent confirmation from Linux (this report and the comment above are Windows; the other linked reports are macOS). Same mechanism, and it is not limited to snapshot capture.

    Environment

    • opencode v2.0.18, compiled binary (~/.opencode/bin/opencode), Bun 1.4.0
    • Linux 6.12.38, 8 cores, 16 GB RAM, load average ~10–13
    • Managed service launched by the TUI

    Observed loop (~10 h of log)

    Signal Count
    serve --service starts 66
    /api/info → HTTP 503 55
    Background service is unresponsive; recovery cannot preserve persistent terminals 17
    event stream disconnected 64
    uncaught exception: write EPIPE 44

    Consistent ~5 s pairing:

    08:37:48  Background service is unresponsive
    08:37:53  cli starting  args=[serve --service]
    08:39:09  Background service is unresponsive
    08:39:14  cli starting  args=[serve --service]
    

    Confirms the ~6 s window

    Inspecting the shipped v2.0.18 binary, the ensure path uses:

    requestTimeout : 2000
    retry          : count >= 3 before declaring the service missing
    spawnDelay     : 5000
    

    so the effective window is ~6 s (3 × 2 s), matching 4Liberty's measurement above. In my case /api/info normally answers in 36–413 ms, but during load bursts it exceeded that window while the process was alive and later recovered.

    Snapshots disabled, loop persisted

    I set "snapshots": false; the git capture spawns went to zero but the restart loop did not stop on its own — consistent with the comment above that snapshot capture is one trigger, not the only one.

    Same-second contention still present

    Observed bursts with no effective serialization: 5 starts within one second (05:28:30), 4 at 04:05:22, 3 at 03:25:30.

    Workaround that helped here

    OPENCODE_DISABLE_FILEWATCHER=true. Watcher churn is a major load source on this host: 3,556 watcher started vs 1,467 watcher stopped, and 8,027 subscribes to a symlinked skills directory.

    Happy to attach sanitized log excerpts.

  3. piknockyou commented on Sep 30, 2026

    @piknockyou

    Your analysis of the snapshot step matches what I hit, so I'm adding the second trigger in case it helps others landing here with the same symptom (no reply and/or the server restarted while you were working loop, event stream disconnected / stalled).

    In my case the huge tree was not a real project: a stray empty .git in a parent folder (my home folder) made opencode treat everything underneath it as one giant project. Same result you describe - the per-message scan takes ~26s for me (541k files), the watchdog gives up, the service gets restarted, and it loops.

    For anyone with this symptom: run git rev-parse --show-toplevel in the folder where you start opencode.

    • If it prints your home folder (or a big parent folder) instead of your actual project, a leftover .git is the culprit - delete it (usually empty, nothing lost; rename if unsure) and start opencode inside a real project folder. That was the fix for me.
    • If it prints your real project, then it is exactly this issue's case: get the heavy folders (node_modules/, target/, build/, caches) into .gitignore, or set "snapshot": false.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions