anomalyco / anomalyco/opencode

Snapshot git transactions race across processes and a stale index.lock permanently wedges snapshots

Open
#48,848 3 comments 0 reactions 1 assignee View on GitHub

@kitlangton is already working on this.

Since Sep 13, 2026.

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

Description

What version of opencode are you using?

Latest dev at time of report (base commit 95daf90670b7c039c436c85537da5fbfe2205b41), Linux x64, git 2.x.

Describe the bug

Snapshot git operations race across separate opencode processes working in the same project worktree, and a single stale index.lock permanently and silently disables snapshots.

Root cause

packages/opencode/src/snapshot/index.ts serializes snapshot git operations with an in-process-only semaphore:

  • locks = new Map<string, Semaphore>() keyed by state.gitdir
  • state.gitdir = <global data>/snapshot/<project id>/<Hash.fast(worktree)>

Every opencode process (or any two sessions) working in the same worktree derives the same on-disk gitdir, but the semaphore only coordinates within one process. Independent processes therefore run git add, git rm --cached, and git write-tree concurrently against one shared index.

Measured reproduction (8 processes x 8 rounds, scratch repo): 56/64 git add failures and 56/64 git write-tree failures, all Unable to create '.../index.lock': File exists.

Git's own index.lock is a metadata-free zero-byte file and fails immediately when present - there is no wait/retry. If a git child is SIGKILLed (OOM, crash, terminal close) the lock is left behind forever (git intentionally never auto-removes it), and opencode has no detection or recovery: every later snapshot call fails with the same warning. In the wild this produced a 9-day silent snapshot outage - every session log filled with failed to add snapshot files ... index.lock: File exists and tracking hash="".

Related correctness issues found while fixing

  • stage()/drop() ignored failures, and write-tree stdout was not validated, so an empty or stale hash could be returned.
  • restore() failure was unobservable, and session revert persisted a revert that the filesystem never received.
  • setup() re-ran git config on every track once the gitdir existed; a concurrent config.lock (exit 255) permanently wedged new repositories.

Fix (PR to follow)

  • Keep the process-local semaphore as an outer gate and add the existing EffectFlock (packages/core/src/util/effect-flock.ts) around the whole snapshot transaction, serializing across processes.
  • Strictly handle setup/stage/drop/write-tree failures; never return empty/stale tree IDs (validate SHA-1/SHA-256 object IDs).
  • Retry only exact native index.lock contention with bounded jitter; never delete an ownership-unknown native lock; emit a deduplicated structured diagnostic (snapshot_index_lock_stuck) with lock path, age, and a manual remedy after bounded retries.
  • Reclaim an abandoned opencode-owned transaction token only when its owner is provably dead (same host + ESRCH); live/legacy/foreign-host tokens are preserved.
  • Abort session revert when snapshot restore/revert fails instead of persisting a false revert.
  • Run setup only for a new snapshot gitdir.

Verification

  • Deterministic 8-process full-transaction race test (exclusive sentinel spanning git add -> git write-tree), plus failure-path, SHA-256, stale-native-lock, token-reclaim, and revert-abort regression tests.
  • bun typecheck clean; bun test test/snapshot/snapshot.test.ts test/snapshot/snapshot-lock.test.ts -> 73 pass / 1 skip / 0 fail (780 assertions); session revert suite 8 pass / 0 fail.

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.