Skip to content

fix(studio-server): a history claim on Windows waits out a lock file being deleted - #5923

Queued
miguel-heygen wants to merge 3 commits into
mainfrom
fix/history-lock-windows-delete-pending
Queued

miguel-heygen wants to merge 3 commits into
mainfrom
fix/history-lock-windows-delete-pending

Conversation

@miguel-heygen

Copy link
Copy Markdown
Collaborator

What changes for users

On Windows, two hyperframes history begin calls on one project at the same moment no longer crash one of them with EPERM: operation not permitted, open '...\owner.pid'. The second waits for the history lock and gets the normal refusal ("'s turn is open; it has to end before yours begins"), as on macOS and Linux.

Why it failed

The history owner lock reads and hard-links owner.pid. When the owner releases the lock it deletes that file, and on Windows a file that is being deleted cannot be opened or linked for a moment: the call fails with EPERM (sometimes EACCES or EBUSY) instead of ENOENT or EEXIST. The lock treated that as a fatal error, so the waiting process exited with code 1.

Seen on a hosted Windows runner looping the existing test serializes concurrent begin --exclusive claims under the history owner lock 30 times with each process's stderr printed: one run in 30 had the waiting process exit 1 with EPERM: operation not permitted, open '...\owner.pid'; the other 29 refused with code 2 as expected.

The fix

On Windows, an EPERM, EACCES or EBUSY while claiming or reading the lock means the lock is busy: the claimer waits and tries again until its existing deadline, then gives up with the same error. Elsewhere these errors still fail at once. Only the claim is retried, not the work after this process holds the lock. One claim attempt moved into its own function to keep the loop readable. The codes are the ones the shared atomic-file writes already treat as Windows-busy.

Proof

ownerLock.test.ts makes the lock file unreadable with mode 000 (a real EACCES on Linux and macOS) and deletes it after 200 ms, as a finishing delete would:

  • reported as Windows, the claim waits and then takes the lock; on main it throws EACCES at once
  • on Linux it still throws at once
  • an unreadable lock that never goes away still fails at the deadline
  • a live owner still ends in the busy error at the deadline

20 runs in a row pass; the history suites (203 tests) and the package typecheck pass.

No visible change

Nothing changes outside Windows, and nothing changes on Windows unless two processes race for one project's history.

@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 271 (base branch 271), smooth 178 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (0)

Unstable (1)

  • seqplainkeys-none-px-r0-root-z100: tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo false, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true

@vanceingalls vanceingalls left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed at ceecd011. Approving.

The retry is narrow.

  • Only a win32 EPERM/EACCES/EBUSY raised by one claim attempt is retried (ownerLock.ts:15-21, :128-133), and only until the existing deadline. Past the deadline the original error is rethrown, so a lock that stays unreadable still fails, as before.
  • Off Windows, every error path is unchanged.
  • Every step inside attemptClaim is safe to repeat:
    • claim removes its draft in finally, so a linkSync that hits a deleting owner.pid leaves nothing behind.
    • ownerOf only reads.
    • evictDeadOwner releases its evict lock in finally and re-reads the owner under it, so a retry can't take a live owner's lock.
  • A caught busy error falls to "wait", so the loop sleeps 50 ms rather than spinning.

The work after the claim is unchanged. advertise and the release closure moved out of the loop verbatim, so only the claim is retried, as the body says.

The tests exercise the real path. A mode-000 owner.pid gives a real EACCES on Linux and macOS, and process.platform is stubbed to win32 and restored in afterEach. The first test fails on main, because the old loop has no catch. Skipping on root and on real Windows is right, since mode 000 can't produce the error there.

Nit

  • This is pre-existing, not new here. If advertise's renameSync hits a Windows busy code (for example a waiter reading owner.url at its deadline), it throws after this process already holds owner.pid. The lock then stays held until the process exits, and no release is returned. If the history CLI ever hits that, wrapping advertise in the same busy retry, or releasing the lock on failure, would close it.

Verdict: APPROVE
Reasoning: The retry is limited to Windows busy codes within the existing deadline, every retried step is idempotent, and the test drives a real EACCES through the Windows branch. Required CI is green at this head.

— Via

@miguel-heygen
miguel-heygen added this pull request to the merge queue Oct 20, 2026
Any commits made after this event will not be merged.

This branch has not been deployed

No deployments
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