Skip to content

fix(casting): make durable casting registry work on Windows - #2114

Merged
bradygaster merged 4 commits into
github:devfrom
cherie-q:users/cherieq/squad-2106-windows-casting
Oct 2, 2026
Merged

bradygaster merged 4 commits into
github:devfrom
cherie-q:users/cherieq/squad-2106-windows-casting

Conversation

@cherie-q

Copy link
Copy Markdown
Contributor

What

Makes the durable casting registry work on Windows. On current dev, squad init and team casting fail there with EPERM: operation not permitted, fsync before 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() in packages/squad-sdk/src/casting/durable-registry.ts (added in #2066) fsyncs directory handles, which Windows rejects with EPERM. Every durable casting write goes through it. This is a regression that hasn't shipped yet: npm 0.13.1 is not affected.

How

All changes are in durable-registry.ts and apply to win32 only. POSIX behavior is unchanged, and tests check that.

  1. Directory fsync: flushDirectory() returns early on win32. 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.
  2. Waiting on a held lock: the writer lock publishes by renaming a candidate folder onto registry.lock. When another process holds the lock, Windows reports that rename as EPERM, not ENOTEMPTY/EEXIST. The sync and async acquirers crashed instead of waiting. isLockPublishContention() now treats EPERM on win32 as "lock is held".
    • EPERM could also be a real permission problem, so the timeout message now names the last error, e.g. (last publish error: EPERM). A persistent EPERM is then no longer blamed on a writer that doesn't exist.
  3. Lock release: release moves registry.lock and registry.lock.recovery to uniquely named quarantine folders. On Windows that rename can briefly fail with EPERM/EACCES/EBUSY when antivirus or the indexer is holding the freshly written owner.json.
    • A registry.lock.recovery left behind is never cleaned up, so every later writer would time out. Release therefore retries these codes for up to 1 s.
    • Deleting the quarantined copy afterwards is best-effort on win32: ownership has already moved, and nothing reads the leftover. POSIX still reports delete failures.

Tests

  • New test/casting-durable-registry-windows.test.ts mocks node:fs to behave like Windows (directory fsync, rename onto an existing directory, temporary quarantine rename/delete failures) and sets process.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 a file:// 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-latest CI job for the casting durability tests. Per the repo's PR scope rules, a CI change belongs in its own repo-health PR.


⚠️ Quick Check

  • If SDK/CLI source files changed: completed the applicable Changeset step below (npx changeset add / .changeset/*.md, direct CHANGELOG.md entry for maintainers, or skip-changelog label for no user-facing changes)

PR Readiness Checklist

The PR readiness bot will validate these automatically after push.
Check each item before requesting review. See CONTRIBUTING.md for full details.

Branch & Commit

  • Branch created from dev (not main)
  • Branch is up to date with dev (git fetch upstream && git rebase upstream/dev)
  • Verified diff contains only intended changes (git diff --cached --stat)
  • PR is not in draft mode (mark ready when checks pass)
  • Commit history is clean (squash fixups before review)

Build & Test

  • npm run build passes
  • npm test passes (all tests green)
  • npm run lint passes (type check clean)
  • npm run lint:eslint passes
  • For migration PRs (>20 files): include test output summary in PR description

Validation, all run locally on Windows 11 / Node v24.13.0:

Command Result
SKIP_BUILD_BUMP=1 npm run build ✅ passed
npm run lint ✅ passed
npm run lint:eslint ✅ passed (0 errors; warnings already present on dev)
npx vitest run test/casting-durable-registry.test.ts test/casting-durable-registry-windows.test.ts ✅ 91/91 passed (on dev: 71 of 83 failed with EPERM ... fsync)
npx vitest run test/cli-packaging-smoke.test.ts test/cli/doctor.test.ts ✅ 70/70 passed
npx vitest run (full suite) ❌ 100 failed / 9699 passed (26 files)
Issue repro: squad init --no-workflows --no-vscode-default in a fresh git init repo ✅ exits 0 and writes .squad/casting/

About the full-suite failures: these are problems that already exist on Windows. I re-ran the 26 failing files with dev's durable-registry.ts (and a rebuilt dist):

  • Fixed by this PR: 52 tests that fail on dev pass here, including init in acceptance, the cli/export-import suite and the cli/upgrade suite.
  • Failed only on this branch: cli-packaging-smoke and 3 cli/doctor tests. All of them pass when those files are run on their own (the 70/70 above).
  • Everything else fails the same way on dev (97 tests), for example gh-aw-* and state-backend.

CI runs on ubuntu, where none of the Windows-only failures apply.

Changeset

  • Changeset added via npx changeset add (if packages/squad-sdk/src/ or packages/squad-cli/src/ changed)
  • Or direct CHANGELOG.md entry (maintainers only — write-protected for external contributors)
  • Or skip-changelog label applied (if no user-facing changes)

.changeset/2106-windows-durable-casting.md: @bradygaster/squad-sdk patch.

Docs

  • README section updated (if new feature/module): N/A, bug fix
  • Docs feature page (if new user-facing capability): N/A, bug fix

Exports

  • package.json subpath exports updated (if new module): N/A, no new modules

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 test is unchecked only because some tests already fail on Windows before this change (see the comparison above). CI on ubuntu is the gate.

Copilot AI balanced review requested due to automatic review settings September 29, 2026 22:30
@github-actions

Copy link
Copy Markdown
Contributor

🟡 Impact Analysis — PR #2114

Risk tier: 🟡 MEDIUM

📊 Summary

Metric Count
Files changed 4
Files added 2
Files modified 2
Files deleted 0
Modules touched 3

🎯 Risk Factors

  • 4 files changed (≤5 → LOW)
  • 3 modules touched (2-4 → MEDIUM)

📦 Modules Affected

root (1 file)
  • .changeset/2106-windows-durable-casting.md
squad-sdk (1 file)
  • packages/squad-sdk/src/casting/durable-registry.ts
tests (2 files)
  • test/casting-durable-registry-windows.test.ts
  • test/casting-durable-registry.test.ts

This report is generated automatically for every PR. See #733 for details.

@github-actions

github-actions Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

🛫 PR Readiness Check

ℹ️ This comment updates on each push. Last checked: commit 08fb878

PR Scope: 📦🔧 Mixed (product + infrastructure)

⚠️ 3 item(s) to address before review

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.

Copilot AI 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.

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 Medium severity · 1 Low severity

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 tamirdresher 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.

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>
@cherie-q
cherie-q force-pushed the users/cherieq/squad-2106-windows-casting branch from 264b9ab to 54af3e3 Compare October 1, 2026 12:25
@bradygaster
bradygaster merged commit b846a1d into github:dev Oct 2, 2026
17 checks passed
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.

squad init and casting fail on Windows with EPERM: operation not permitted, fsync (dev, since #2066)

4 participants