Skip to content

fix: prevent file corruption when multiple IDE windows patch concurrently#12

Open
BenKalsky wants to merge 4 commits into
yechielby:masterfrom
Digitizers:fix/concurrent-write-corruption
Open

fix: prevent file corruption when multiple IDE windows patch concurrently#12
BenKalsky wants to merge 4 commits into
yechielby:masterfrom
Digitizers:fix/concurrent-write-corruption

Conversation

@BenKalsky

Copy link
Copy Markdown

Problem

Each open IDE window runs its own extension host, and they all patch the same Claude Code files on disk (webview/index.js, webview/index.css, extension.js). The injection path does an unsynchronized read-modify-write (copyFile backup → readFilewriteFile) with no lock between windows/processes.

With more than one window open, these cycles interleave and tear the file. In a real report on Antigravity IDE, webview/index.js was truncated from 4.8 MB → ~1 MB right after a Claude Code reinstall (which re-triggered patching in every window). The truncated webview bundle fails to load → the Claude panel renders completely blank and sessions appear gone. Restoring from the .bak brought it back, confirming the on-disk file was the corrupted artifact.

This is separate from #11 (which fixes which directory is searched); this fixes how the files are written.

Fix

src/injector.ts:

  • Serialize every mutating op behind a per-extension-directory lock file (withFileLock), so only one window patches at a time. Stale locks (crashed window) are auto-broken after 30s, and it falls through after a bounded wait rather than ever hanging the extension.
  • Atomic writes (atomicWrite): write to a unique temp file then rename() into place, so a reader/racing window never observes a half-written file.
  • Corruption guard: injection only adds content, so a result smaller than the pristine backup is rejected instead of written — truncation can never be persisted.
  • Public functions (addRtl, addRtlAlways, addRtlAuto, fixBidi, removeRtl) are now thin lock-serialized wrappers over lock-free *Impl functions, so fixBidi composes them without re-entrant deadlock.

Test

Adds test/concurrency.test.cjs (run with npm test): patches one extension from 8 simulated windows concurrently and asserts no truncation, no stacked injections, pristine backups, and clean removal. The pre-fix code truncates/hangs under the same race; the fixed code passes.

PASS — 8 concurrent windows, no corruption
  index.js  orig=5000044 inject=5004129 remove=5000044
  index.css orig=29 inject=4929
  JS markers after race: 1 (no stacking)

Verified: tsc --noEmit clean, npm run build clean, npm test passes. Bumped to v0.4.2.

🤖 Generated with Claude Code

BenKalsky and others added 4 commits May 31, 2026 23:51
Drop CLAUDE.md, .claude/, *.code-workspace, llms*.txt, vsix and other
dev-only files from the published extension package (16 -> 10 files).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ntly

Each open IDE window runs its own extension host, and they all patch the
same Claude Code files on disk. Their backup/read/write cycles could
interleave and truncate a file — in one report webview/index.js shrank
from 4.8 MB to ~1 MB, leaving the Claude panel blank.

Injection is now:
- Serialized via a per-extension-directory lock file (stale locks are
  auto-broken; never hangs the extension).
- Atomic — written to a temp file and renamed into place, so a reader
  never sees a half-written file.
- Guarded — a result smaller than the pristine backup is rejected
  instead of written, so truncation is never persisted.

Public entry points are thin lock-serialized wrappers over lock-free
*Impl functions, so fixBidi can compose them without deadlocking.

Adds a concurrency regression test (npm test) that patches one extension
from 8 simulated windows at once.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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