A light-weight and powerful meta-prompting, context engineering and spec-driven development system for Claude Code by TÂCHES.
fix(3670): break migration lock self-deadlock on Windows (#3765)
* test(installer): add regression tests for #3670 migration lock self-deadlock
- T1: same-process PID re-entry reclamation (primary regression)
- T2: dead-PID stale lock reclamation
- T3: unlinkSync EPERM surfaces (not silently swallowed via force:true)
- T4: counter-test — normal round-trip still works
- T5: counter-test — genuinely-held live lock still errors clearly
- Update existing 'reports lock release failures' test to mock
fs.unlinkSync (not fs.rmSync) matching the fixed release path
Windows wall-clock deadlock repro depends on Docker matrix Windows runners.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(installer): break migration lock self-deadlock on Windows (#3670)
Root cause: `acquireInstallMigrationLock` release closure called
`fs.rmSync(lockPath, { force: true })`. On Windows NTFS, a file
recently closed via `closeSync(fd)` may still return EPERM from
`unlink` until the OS fully releases the handle. The `force: true`
flag silently swallows EPERM, leaving the lock file on disk. The
subsequent `runInstallerMigrations` call in the same install()
invocation hits EEXIST, spins for 30 s, then throws
"installer migration lock is held".
Fix:
1. Release closure uses `fs.unlinkSync` (not rmSync+force) so
EPERM propagates via releaseError instead of being swallowed.
2. `acquireInstallMigrationLock` closes the fd before writing the
payload (path-based write), eliminating the open handle that
caused the deferred EPERM on Windows.
3. Stale-lock reclamation: on EEXIST, parse the on-disk PID and
reclaim immediately if it matches process.pid (same-process
re-entry, the primary #3670 failure mode) or if the PID is
dead (ESRCH). Live alien PIDs still trigger the 30 s timeout.
4. Error message on a genuinely-held lock now includes the holder
PID and acquiredAt timestamp for operator diagnostics.
No public API change. All callers of runInstallerMigrations are
inside installer-migrations.cjs and bin/install.js.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(3670): update changeset to reference PR #3765
* fix(3670): timeout on reclaim-unlink failure to prevent spin-loop regression
When unlinkSync throws (e.g. Windows EPERM on an open handle) in the
same-PID / dead-PID reclamation path, the original code continued
unconditionally — bypassing the timeout check and reintroducing the
exact deadlock the PR is supposed to fix.
Guard the continue behind a `reclaimed` flag: only loop back to
openSync if unlink SUCCEEDED. On failure, fall through to the existing
bounded sleep + timeout, which surfaces "installer migration lock is
held" within lockTimeoutMs instead of spinning indefinitely.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(3670): tighten T5 — assert bounded failure when reclaim unlink fails on live lock
The old T5 accepted BOTH success and throw, which allowed over-reclamation
of a genuinely un-reclaimable lock to pass undetected.
Rewrite T5 to force deterministically unreclaimable conditions:
- Pre-seed lock with process.pid (triggers isSameProcess path)
- Mock fs.unlinkSync via mock.method() to throw EPERM for the lock file
With the production fix: reclaimed=false → falls through to timeout →
throws "installer migration lock is held" within ~200ms.
Without the production fix: unlink throws but continue runs anyway →
process spins and eventually OOMs (confirmed RED: 136s runtime, V8 heap
exhaustion from infinite readLockFile + new Error() allocations).
assert.throws() now makes success a hard failure, closing the
over-reclamation gap.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(3670): clean up orphan lock file when writeFileSync fails after closeSync
If closeSync(fd) succeeds (fd=null) but the subsequent writeFileSync
throws, the empty lock file was left on disk. readLockFile returns null
for an empty/invalid-JSON file, so the stale-lock reclamation path
skips it, causing the next acquire attempt to spin to timeout.
Track ownership with lockCreatedByUs flag; add a second cleanup branch
in the catch block for the fd-already-closed case.
Also fix changeset body to use the bold-prefix format required by all
other fragments in .changeset/.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> T
Tom Boucher committed
172e6920eb7f37197221b053edd27a2d71f73135
Parent: 49dcabf
Committed by GitHub <noreply@github.com>
on 5/21/2026, 3:12:08 AM