Skip to content

fix: treat Windows vault lock EPERM as contention - #211

Merged
CuriosityOS merged 4 commits into
mainfrom
cursor/vault-windows-eperm-247c
Sep 21, 2026
Merged

CuriosityOS merged 4 commits into
mainfrom
cursor/vault-windows-eperm-247c

Conversation

@CuriosityOS

@CuriosityOS CuriosityOS commented Sep 18, 2026 •

Copy link
Copy Markdown
Collaborator

What and why

Cross-platform unit tests (windows-latest, 22) failed on main at a12e116 (2.8.7). simultaneous stale-lock recovery cannot unlink a fresh writer lock died with EPERM renaming a lock candidate over vault.lock.

NTFS reports an existing directory dest as EPERM (research/windows-fs-probe.mjs). The winner can release before the loser observes the dest, so after one retry acquireLock treated a dest-less EPERM as a hard failure.

A dest-less EPERM is now probed: if the intact candidate can rename to a unique sibling, the source and parent are writable and the original error is contention. If that sibling rename also fails, the original access error is returned instead of looping until VAULT_LOCK_TIMEOUT.

A rename/platform test seam covers that sequence on every OS: two dest-less EPERMs, a recorded sibling relocate, then a successful publish; and a sibling-rename failure that stays EACCES instead of waiting out the lock timeout.

Checklist

  • Focused bun test tests/node/vault.test.ts after bun run build is the verification for this lock-path change. Full release:check will run on CI (Windows included).
  • Locally launched browsers and Electron attachments stay on the guard proxy. No launch, transport, or fetch change.
  • No secret value reaches the model sandbox. No new output channel. Vault lock errors still do not include secret values.
  • Dependency pins are still mirrored everywhere. No pin change.
  • Public API changes update types/*.d.ts in this same commit. No public API change. _renameForTest and _lockPlatformForTest are test-only constructor hooks, same as _lockAcquiredForTest.
  • The two branch-protected CI job names, "Worker copies in sync" and "Node tests", are unchanged, and no new action was added.
  • No unit test imports src/worker.ts or dist/src/worker.js directly.
  • User-visible behaviour is in CHANGELOG.md under Unreleased.
  • Nothing private is being committed.

How it was verified

  • Failed job: CI run 35297753539 on a12e116, Cross-platform unit tests (windows-latest, 22).
  • Error: EPERM: operation not permitted, rename '…\vault.lock.candidate.…' -> '…\vault.lock' from acquireLock, then vault child exited 1.
  • Devin Review: a lasting ACL denial must still surface as the access error. The sibling-rename probe does that.
  • Greptile: the 24-child stress test is not deterministic. The new seam tests force the dest-less sequence on Linux.
  • Local: bun run build && bun test tests/node/vault.test.ts — 34 pass, 1 skip.
Open in Web Open in Cursor 

@CuriosityOS
CuriosityOS marked this pull request as ready for review September 18, 2026 02:22

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

Devin Review

Comment thread src/vault.ts Outdated
(isWindowsRenameDestinationError(error) &&
(observedWindowsDestination ||
(await readLockDirectory(paths.lock)) !== null));
isRenameCollision(error) || isWindowsRenameDestinationError(error);

@devin-ai-integration devin-ai-integration Bot Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Later publish failures become contention

After relocation sets windowsPublishContention, any later publish error becomes contention. Disk failures retry until VAULT_LOCK_TIMEOUT instead of reaching the caller.

Learn more

The candidate relocation proves only that the preceding EPERM or EACCES represented contention. The code then retries the publish with the relocated candidate. If that new rename fails for another reason, the persistent boolean still makes the outer catch suppress that new error.

Example: The relocation succeeds after a vanished lock caused EPERM. The next publish returns ENOSPC. The operation discards that error, creates more candidates, and eventually reports VAULT_LOCK_TIMEOUT instead of the disk-space failure.

Recommended fix: Combine windowsPublishContention with isWindowsRenameDestinationError(error) when classifying the caught error. Continue recognizing EEXIST and ENOTEMPTY through isRenameCollision.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

@greptile-apps

greptile-apps Bot commented Sep 18, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5

Safe to merge: the remaining concern is non-blocking test coverage rather than a reproduced product failure.

Findings

  1. P2 Make Windows Regression Deterministic ▶

Summary

The Windows vault-lock handling change is covered by a multi-process stress test, but the test does not deterministically exercise the specific publish-rename failure sequence it is intended to protect. The change is safe to merge, though adding focused coverage would prevent this Windows-only behavior from regressing unnoticed.

Reviews (1) · Last reviewed commit: "fix: treat Windows vault lock EPERM as c..."

Comment thread tests/node/vault.test.ts Outdated
Comment on lines +1299 to +1303
test("simultaneous stale-lock recovery cannot unlink a fresh writer lock", async () => {
// 24 children race one stale lock. On Windows the publish rename reports
// EPERM for an existing dest (research/windows-fs-probe.mjs); the winner
// can release before a loser observes it. That must be contention, not a
// fatal EPERM — Cross-platform unit tests (windows-latest, 22) on 2.8.7.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Make Windows Regression Deterministic

This stress test starts 24 writers behind a shared barrier, but it does not force the publish rename to fail with EPERM or EACCES while the lock destination disappears before it is read. All writers can therefore succeed without executing the changed Windows contention path, so a later regression in that path would not be detected. Add a focused filesystem seam or unit test that injects this error-and-missing-destination sequence, verifies a retry succeeds, and records that the branch ran. This is non-blocking, but without it the Windows-specific fix has only probabilistic coverage.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Artifacts

Evidence from the check

  • Authored and executed script that inspects the test wiring and runs the unchanged vault suite three times; it shows the test has no executable error injection and Linux cannot enter the Windows-only predicate.

Command output from the check

  • Executed parent-revision build and complete vault test suite, including the 24-child stale-lock test; all 32 runnable tests passed, showing the pre-change test also passes without deterministic error forcing.

Command output from the check

  • Executed current-revision validator and three full vault-suite runs; each passed on Linux while the validator recorded the absent injector and Windows-only branch gate, confirming the coverage gap.

View artifacts

T-Rex Ran code and verified through T-Rex

cursoragent and others added 4 commits September 21, 2026 18:18
Cross-platform unit tests (windows-latest, 22) failed on 2.8.7:
simultaneous stale-lock recovery threw EPERM when renaming a lock
candidate over vault.lock. NTFS reports an existing dest as EPERM;
the winner can release before the loser observes it. After one retry
that still saw no dest, acquireLock treated the error as fatal.

Unpublished EPERM/EACCES on the publish rename is now always
contention: back off and try again, same as a visible colliding lock.

Co-authored-by: Nav <CuriosityOS@users.noreply.github.com>
A dest-less publish EPERM can be a vanished colliding lock or a lasting
ACL denial. Rename the intact candidate to a unique sibling: if that
works, the source and parent are writable and the original error is
contention. If it fails, surface the access error instead of looping
until VAULT_LOCK_TIMEOUT.

Co-authored-by: Nav <CuriosityOS@users.noreply.github.com>
The 24-child stress test can pass without taking the dest-less publish
path. Add a rename/platform seam and two focused cases: relocate after
two dest-less EPERMs, and a sibling-rename failure that stays EACCES
instead of waiting out the lock timeout.

Co-authored-by: Nav <CuriosityOS@users.noreply.github.com>
…ified as such

After the candidate relocated, contention was flagged before the retry,
so an unrelated failure on that publish (ENOSPC, EIO) looped until
VAULT_LOCK_TIMEOUT instead of surfacing. Flag it only on the branches
that actually classify an EPERM/EACCES as contention, and cover the
unrelated-error path with a seam test.
@CuriosityOS
CuriosityOS force-pushed the cursor/vault-windows-eperm-247c branch from d200818 to 10ce39d Compare September 21, 2026 10:19
@devin-ai-integration

Copy link
Copy Markdown
Contributor

Starting E2E review of BetterWright/betterwright PR #211 (head 10ce39d); will check PR state/dedupe first.

@CuriosityOS
CuriosityOS merged commit e594b0d into main Sep 21, 2026
9 checks passed
@CuriosityOS
CuriosityOS deleted the cursor/vault-windows-eperm-247c branch September 21, 2026 10:33
@CuriosityOS CuriosityOS mentioned this pull request Sep 21, 2026
9 tasks done
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.

2 participants