Skip to content

fix: serialize snapshot Git indexes across processes and reject failed captures - #4

Draft
thelabcorner wants to merge 12 commits into
mainfrom
fix/snapshot-index-lock-fail-closed-20261010
Draft

thelabcorner wants to merge 12 commits into
mainfrom
fix/snapshot-index-lock-fail-closed-20261010

Conversation

@thelabcorner

@thelabcorner thelabcorner commented Oct 10, 2026 •

Copy link
Copy Markdown
Owner

Why this was necessary

OpenFork logged fatal: Unable to create .../snapshot/.../index.lock: File exists (Git exit 128), then logged tracking hash="". The legacy snapshot stage operation logged and swallowed errors, allowing subsequent Git operations to proceed on an untrusted shadow index. The V1 service had only an in-process semaphore; the core/V2 snapshot service uses the same shadow Git directory with a separate lock. Two services or independent server processes could therefore interleave writes.

Implemented

  • Canonical cross-process lock: packages/core/src/snapshot-index-lock.ts defines one filesystem-lease protocol with a single key, root and timeout contract. Both V1 and core/V2 acquire it over the entire index mutation transaction, on top of existing in-process locks. Reuses the project Flock implementation (atomic mkdir, heartbeat, owner-token verification, stale lock reclamation).
  • Fail-closed staging and capture: V1 git add retries bounded transient legacy index.lock collisions, then rejects exhaustion. Errors from Git ignore detection, file enumeration, indexed removals, staging and index rebuilding no longer allow write-tree to run. Both V1 and core Git reject malformed/empty tree object IDs. Core Git ignore checking now correctly treats exit 1 as no ignored paths but rejects real check failures; it no longer silently includes ignored files in a capture. V1 invalidates unsuccessful capture memoization and returns undefined rather than publishing stale/empty trees.
  • Restore/revert safety: errors from read-tree/checkout-index now fail the operation. Revert prevalidates tree IDs and worktree containment; a failed ls-tree cannot imply absence and therefore cannot trigger file deletion.
  • V2 parity: V2 core capture, preview, restore and checkout use the identical shared lease, including repository setup and index healing.

Targeted regression tests added

  • Existing index.lock during git add → failed capture, lock untouched, no empty/stale hash, successful subsequent recovery once unlocked.
  • Independent filesystem lease holder → capture waits rather than interleaving Git mutations.
  • Invalid revert tree → existing worktree file remains intact.
  • Out-of-worktree revert path → external file remains intact.
  • Existing capture/restore, corrupt-index healing, concurrent same-project callers, worktree isolation and core snapshot tests should be rerun.

Verification / outstanding gates

  • The code is committed in an isolated branch. No user's Windows checkout, database or shadow-index directory was modified.
  • The repo's self-hosted GitHub Actions installation fails before lint/typecheck/tests when tree-sitter-powershell invokes an older g++ that does not recognize -std=gnu++20; this is an unrelated runner toolchain blocker, not a passing validation result. See job 114323169269.
  • Next: update the homelab runner's GCC/G++ toolchain, rerun target V1/core snapshot suites and typecheck, then run actual Windows multi-process contention and crash/restart stress tests. Keep this PR draft until those gates pass.

Recovery / rollout

  • An old index.lock is not blindly deleted: because Git does not encode lock ownership, removing a live writer's lock can corrupt the index. For the already-affected machine, stop OpenFork servers and ensure Git is not writing that shadow repository, then remove only the verified stale index.lock.
  • The new protocol protects processes running the patched version; legacy processes remain subject to bounded Git lock retries and fail-closed capture.
  • Merge/deploy only after the required tests pass and concurrent local changes are reconciled.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant