Repository navigation
fix(casting): make durable casting registry work on Windows - #2114
Conversation
🟡 Impact Analysis — PR #2114Risk tier: 🟡 MEDIUM 📊 Summary
🎯 Risk Factors
📦 Modules Affectedroot (1 file)
squad-sdk (1 file)
tests (2 files)
This report is generated automatically for every PR. See #733 for details. |
🛫 PR Readiness Check
PR Scope: 📦🔧 Mixed (product + infrastructure)
|
| Status | Check | Details |
|---|---|---|
| ❌ | Single commit | 4 commits — consider squashing before review |
| ✅ | Not in draft | Ready for review |
| ✅ | Branch up to date | Up to date with dev |
| ❌ | Copilot review | No Copilot review yet — it may still be processing |
| ✅ | Changeset present | Changeset file found |
| ✅ | Scope clean | No .squad/ or docs/proposals/ files |
| ✅ | No merge conflicts | No merge conflicts |
| ✅ | Copilot threads resolved | 1 active Copilot thread(s) resolved (1 outdated skipped) |
| ❌ | CI passing | 6 check(s) still running |
Files Changed (4 files, +383 −10)
| File | +/− |
|---|---|
.changeset/2106-windows-durable-casting.md |
+5 −0 |
packages/squad-sdk/src/casting/durable-registry.ts |
+105 −8 |
test/casting-durable-registry-windows.test.ts |
+270 −0 |
test/casting-durable-registry.test.ts |
+3 −2 |
Total: +383 −10
This check runs automatically on every push. Fix any ❌ items and push again.
See CONTRIBUTING.md and PR Requirements for details.
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Abandoned quarantine directories can indefinitely pollute tracked casting state, and two supported retry error codes remain untested.
Review effort: Balanced
Findings: 1
Open (2)
What changed in this PR
Fixes Windows filesystem incompatibilities in the durable casting registry while preserving POSIX durability behavior.
Changes:
- Skips unsupported directory fsync operations on Windows.
- Handles Windows lock contention and transient rename/delete failures.
- Adds Windows filesystem regression tests and portable child-process imports.
| File | Description |
|---|---|
packages/squad-sdk/src/casting/durable-registry.ts |
Adds Windows-specific durability and lock handling. |
test/casting-durable-registry-windows.test.ts |
Tests simulated Windows filesystem behavior. |
test/casting-durable-registry.test.ts |
Uses file URLs for Windows-compatible child imports. |
.changeset/2106-windows-durable-casting.md |
Records the SDK patch release. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
tamirdresher
left a comment
There was a problem hiding this comment.
Looks good to me. The Windows lock handling and recovery changes look solid, and the targeted durability tests passed. Thanks for fixing this!
squad init and team casting failed on Windows with "EPERM: operation not permitted, fsync" (regression from github#2066). - flushDirectory() fsynced directory handles after each rename, which Windows rejects. Skip it on win32; the file-level fsync already ran. - The lock publishes by renaming a candidate dir onto registry.lock. Windows reports a rename onto an existing directory as EPERM, not ENOTEMPTY/EEXIST, so a contended lock crashed instead of waiting. Treat win32 EPERM as contention in the sync and async acquirers, and name the last publish error in the timeout so a real permission problem isn't blamed on a phantom writer. - Lock release retries the quarantine rename on transient win32 EPERM/EACCES/EBUSY (antivirus holding a fresh owner.json) for up to 1s, since a leftover registry.lock.recovery blocks every later writer. Deleting the already-quarantined copy is best-effort on win32. - A blocked quarantine delete could leave registry.lock.released-* folders in .squad/casting/ with no later cleanup. Release now sweeps abandoned lock quarantines while holding the recovery guard; guard quarantines are swept only once older than the stale-lock age. Tests: an emulated-Windows suite runs on any host, including the ubuntu-only CI; each fix turns it red when reverted. The retry and delete tests run for EPERM, EACCES and EBUSY. Child-process tests import the module via a file:// URL so the existing durability suite also runs on Windows. Closes github#2106 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
264b9ab to
54af3e3
Compare


What
Makes the durable casting registry work on Windows. On current
dev,squad initand team casting fail there withEPERM: operation not permitted, fsyncbefore any team is written. This PR removes that failure, fixes two more Windows lock failures that show up once it's gone, and adds tests that reproduce Windows filesystem behavior on any host, including the ubuntu-only CI.Why
Closes #2106
flushDirectory()inpackages/squad-sdk/src/casting/durable-registry.ts(added in #2066) fsyncs directory handles, which Windows rejects withEPERM. Every durable casting write goes through it. This is a regression that hasn't shipped yet: npm0.13.1is not affected.How
All changes are in
durable-registry.tsand apply towin32only. POSIX behavior is unchanged, and tests check that.flushDirectory()returns early onwin32. Directory fsync is a POSIX step to make a rename durable. Windows has no equivalent, and the file-level fsync has already run before the rename.registry.lock. When another process holds the lock, Windows reports that rename asEPERM, notENOTEMPTY/EEXIST. The sync and async acquirers crashed instead of waiting.isLockPublishContention()now treatsEPERMonwin32as "lock is held".EPERMcould also be a real permission problem, so the timeout message now names the last error, e.g.(last publish error: EPERM). A persistentEPERMis then no longer blamed on a writer that doesn't exist.registry.lockandregistry.lock.recoveryto uniquely named quarantine folders. On Windows that rename can briefly fail withEPERM/EACCES/EBUSYwhen antivirus or the indexer is holding the freshly writtenowner.json.registry.lock.recoveryleft behind is never cleaned up, so every later writer would time out. Release therefore retries these codes for up to 1 s.win32: ownership has already moved, and nothing reads the leftover. POSIX still reports delete failures.Tests
test/casting-durable-registry-windows.test.tsmocksnode:fsto behave like Windows (directory fsync, rename onto an existing directory, temporary quarantine rename/delete failures) and setsprocess.platform. That lets it run on the Linux CI. I reverted each fix separately to confirm its test fails, and each failure message names the input that caused it.test/casting-durable-registry.test.ts: child-process tests now import the module through afile://URL (pathToFileURL). Before this, the existing durability suite couldn't run on Windows at all (ERR_UNSUPPORTED_ESM_URL_SCHEME).Trade-off: on
win32, a rename is only as durable as the file-level fsync before it. Windows offers no directory flush, so this is the best available there.Out of scope: the issue also suggests adding a
windows-latestCI job for the casting durability tests. Per the repo's PR scope rules, a CI change belongs in its ownrepo-healthPR.npx changeset add/.changeset/*.md, directCHANGELOG.mdentry for maintainers, orskip-changeloglabel for no user-facing changes)PR Readiness Checklist
Branch & Commit
dev(notmain)dev(git fetch upstream && git rebase upstream/dev)git diff --cached --stat)Build & Test
npm run buildpassesnpm testpasses (all tests green)npm run lintpasses (type check clean)npm run lint:eslintpassesValidation, all run locally on Windows 11 / Node v24.13.0:
SKIP_BUILD_BUMP=1 npm run buildnpm run lintnpm run lint:eslintdev)npx vitest run test/casting-durable-registry.test.ts test/casting-durable-registry-windows.test.tsdev: 71 of 83 failed withEPERM ... fsync)npx vitest run test/cli-packaging-smoke.test.ts test/cli/doctor.test.tsnpx vitest run(full suite)squad init --no-workflows --no-vscode-defaultin a freshgit initrepo.squad/casting/About the full-suite failures: these are problems that already exist on Windows. I re-ran the 26 failing files with
dev'sdurable-registry.ts(and a rebuiltdist):devpass here, including init inacceptance, thecli/export-importsuite and thecli/upgradesuite.cli-packaging-smokeand 3cli/doctortests. All of them pass when those files are run on their own (the 70/70 above).dev(97 tests), for examplegh-aw-*andstate-backend.CI runs on ubuntu, where none of the Windows-only failures apply.
Changeset
npx changeset add(ifpackages/squad-sdk/src/orpackages/squad-cli/src/changed)CHANGELOG.mdentry (maintainers only — write-protected for external contributors)skip-changeloglabel applied (if no user-facing changes).changeset/2106-windows-durable-casting.md:@bradygaster/squad-sdkpatch.Docs
Exports
Breaking Changes
None. Everything new runs only on
win32, and POSIX behavior is unchanged. The only visible change on every platform is that the casting lock timeout message can now end in(last publish error: <code>).Waivers
None requested.
npm testis unchecked only because some tests already fail on Windows before this change (see the comparison above). CI on ubuntu is the gate.