Repository navigation
Conversation
On Windows, opening a lock file another process is deleting fails with EPERM (or EBUSY) instead of EEXIST, so withLock threw and the hook crashed. Treat those as a busy lock: retry with backoff until the deadline, then give up as busy. If nothing ever held the lock, the error is a real permission problem and is still thrown. Fixes the Windows flake in "real processes writing skills.json and the ledger at once". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
🌱 graft blast radius1 area changed → 4 areas can be affected. 59 dependent symbols, depth 2. flowchart TB
A0(("Skill State Persistence<br/>36 symbols"))
A1(("Initialization Handover<br/>9 symbols"))
A2(("Trace Housekeeping<br/>7 symbols"))
A3(("Claude Integration<br/>7 symbols"))
classDef reached fill:#D9EDF3,stroke:#3AA7C9,stroke-width:1.5px,color:#0E313C;
class A0,A1,A2,A3 reached;
All 59 dependent symbols, grouped by areaSkill State Persistence — 36 symbols in 13 files
Initialization Handover — 9 symbols in 7 files
Trace Housekeeping — 7 symbols in 5 files
Claude Integration — 7 symbols in 6 files
Test signal per changed area — 1 ✓Reached = a node under a test path has a resolved edge into the changed symbol. It undercounts anything called indirectly — through a CLI, a spawned process or a dynamic import — so read a low ratio as “look here”, never as a coverage gate.
57 test suites also reference this code76 symbols, kept out of the diagram and the table so they cannot crowd out the areas a reviewer has to look at.
Open the interactive graph → — click an area to see its dependent symbols at file:line. |
What was wrong
On Windows, Trail's file lock could crash instead of waiting.
The lock works by creating a file like
state/skills.json.lock. If the file already exists, someone else holds the lock, so we wait and try again. When the holder is done, it deletes the file.On Windows, a file that is in the middle of being deleted can't be opened yet. Windows answers with
EPERM(sometimesEBUSY), not "file exists". The lock code only knew how to wait on "file exists". Anything else was thrown, so a hook that happened to try at that moment crashed.That is the Windows CI flake in "real processes writing skills.json and the ledger at once". It failed the first run of #680 and of #674 on 2026-10-23, and both passed when re-run. Real users on Windows can hit it too, whenever two sessions update skills at the same time.
The fix
In
withLock(src/util/private-fs.ts),EPERMandEBUSYon the lock's open now count as "the lock is being let go":{ ok: false }), the same as today when a lock is held too long.It doesn't try to take over a lock that is being deleted, since that lock isn't stale.
Tests
New tests in
test/skills-state.test.tsfake the Windows error on the lock's open:EPERMandEBUSYfor the first few tries: the update waits, then counts the use once and removes the lock.EPERMon every try with nothing holding the lock: the error is still thrown, after retrying.All four fail on
mainand pass with this change. I ran the full suite on macOS withHOMEandUSERPROFILEpointed at a scratch folder.Not in this PR
Two other Windows tests failed the same day: "cached 10 minutes by checkout and probe" (on #680) and "can't ask: dontAsk, bypassPermissions ... (D17)" (on a main build). Their logs show no lock error. Both are hooks running out of their 400 ms time budget on a slow runner (tests near them took about 20 s that run), so they need a separate fix.
🤖 Generated with Claude Code