anomalyco / anomalyco/opencode

cli: concurrent session.json writes race on Windows → EPERM, TUI loses current session

Open
#47,497 1 comment 0 reactions 1 assignee View on GitHub

@Hona is already working on this.

Since Sep 5, 2026.

2.0
Dominant language
TypeScript
Stars
209k
Forks
27.5k
PR merge metrics
PR metrics pending

Description

Summary

Concurrent opencode2 CLI processes on Windows race on the TUI state file: the atomic rename of session.json.<pid>.<uuid>.tmpsession.json fails with EPERM, surfacing as an unhandled rejection (role=cli). The write is lost and the terminal no longer shows the current session.

Environment

  • opencode version: 0.0.0-beta-19151
  • OS: Microsoft Windows [Version 10.0.26200.9168]
  • Terminal: Windows Terminal (TERM=xterm-256color, WT_SESSION set)
  • Shell: C:\Windows\system32\cmd.exe (%ComSpec%); commands run via PowerShell 5.1
  • Install/channel: beta
  • Active plugins: @prevalentware/opencode-goal-plugin@0.1.45 (via cli.json plugins); local file plugin openrouter-retry-guard (~/.config/opencode/plugin/openrouter-retry-guard.ts); ~/.config/opencode/plugins/ empty; opencode.json lists no plugins

Reproduction

  1. On Windows, open an interactive opencode2 TUI (shared background service).
  2. Concurrently run repeated opencode2 run ... invocations against the shared service (e.g., an automation loop every ~25s; with ~8 concurrent clients it reproduces fast — 9 collisions observed within a single second).
  3. Watch ~/.local/share/opencode/log/opencode.log for unhandled rejection ... EPERM ... session.json.
  4. The TUI no longer shows the current session.

Expected Behavior

State writes from concurrent CLI processes should be serialized (lock file / retry with backoff on EPERM), so no write is lost, no unhandled rejection is logged, and the current session stays visible in the terminal.

Actual Behavior

The rename fails and the error escapes as an unhandled rejection; the session.json update is dropped and the current session disappears from the terminal. Exact error (paths normalized):

timestamp=2026-09-05T14:11:42.677Z level=ERROR run=9407f77e message="unhandled rejection" cause="Error: EPERM: operation not permitted, rename '~/.local/state/opencode/session.json.17072.9354ca59-7ab6-4dd3-9014-eb81bfe24c84.tmp' -> '~/.local/state/opencode/session.json'" role=cli

Same signature in bursts on 2026-08-19 (9 distinct PIDs, same second), 2026-08-20, and 2026-09-05. After the failure session.json still held the stale 13-byte content ({"pinned":[]}).

Additional Context

  • Intermittent, load-dependent: single interactive TUI alone rarely triggers it; concurrent run clients make it near-deterministic.
  • The shared background service stayed healthy throughout (same PID, /api/health 200, model catalog 200), isolating this to the CLI state-write path rather than the service.
  • Local workarounds that helped: routing single-shot automation through opencode2 run --standalone (no shared-state contact) and a pending Windows Defender exclusion for the state dir (needs admin; Add-MpPreference fails without elevation).
  • Happy to provide fuller redacted log excerpts on request.

Suggested Resolution

  • Serialize CLI state writes across processes: guard the read-modify-write of session.json with an interprocess lock (lockfile with PID + stale-lock expiry for crash safety).
  • Retry the tmp→final rename with backoff on EPERM/EACCES (Windows AV/indexer locks are usually transient): e.g. 3–5 attempts, ~50–200 ms jittered delay, then a single WARN — never an unhandled rejection.
  • A TUI-state persist failure must degrade to in-memory state, never escape as an unhandled rejection or drop the visible current session.
  • Longer term: single-writer design where the background service owns session.json and CLI processes send update RPCs instead of writing the file directly.
  • Regression test: spawn N concurrent writers against one state dir on Windows CI and assert no EPERM escapes and the final file parses.

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.