Summary
On Windows, every upload PUT fails with HTTP 500 and artifact_commit_upload then refuses the commit, so no artifact can ever be published. The cause is a directory fsync in syncDirectory(), which Windows cannot perform. The fix is a three-line platform guard.
Reproduced against 2251e42 (v0.1.0) on Windows 10 22H2, Node 24.15.0, running the local service via the MCP stdio adapter.
Symptom
artifact_create_upload succeeds and returns an upload plan.
PUTing the exact bytes to the returned uploadUrl returns HTTP 500 INTERNAL_ERROR. The service log records failure_tag: "StagingStorageFailure".
artifact_commit_upload then fails with UPLOAD_INCOMPLETE: Every declared upload file must be verified before commit.
Cause
syncDirectory() in src/storage/verified-file.ts:144 opens the directory as a file handle and calls sync() on it:
export async function syncDirectory(directory: string): Promise<void> {
const handle = await open(directory, "r");
try {
await handle.sync();
} finally {
await handle.close();
}
}
A directory fsync is a POSIX durability barrier. Windows has no equivalent and cannot open a directory as a file handle, so open()/sync() throws EPERM: operation not permitted, fsync. Minimal repro, independent of this project:
import {open} from "node:fs/promises";
const h = await open("C:/some/existing/directory", "r");
await h.sync(); // EPERM on win32
Both storage paths route through this one function, so a single guard covers both:
src/storage/local-staging-store.ts:69 — upload staging (this is what breaks the PUT)
src/storage/local-blob-store.ts:119 — commit into the blob store
Suggested fix
Early-return on win32:
export async function syncDirectory(directory: string): Promise<void> {
// Windows cannot open a directory as a file handle, so handle.sync() throws
// EPERM. A directory fsync is a POSIX durability barrier with no Windows
// equivalent, so skip it there.
if (process.platform === "win32") {
return;
}
const handle = await open(directory, "r");
try {
await handle.sync();
} finally {
await handle.close();
}
}
Durability note
This drops a crash-durability barrier, but only on the platform that cannot provide it anyway, and only for the directory entry. The file bytes are still fsynced via file.sync() before the rename, so committed artifacts remain durable. The worst case after a hard power loss is a staged upload that has to be redone.
Verification
With the guard applied and pnpm build re-run, the same sequence returns HTTP 200 with {"status":"verified"} on the PUT, and artifact_commit_upload returns a normal artifact with working review and version links. Publishing has been reliable since.
Happy to open a PR with the guard plus a process.platform test if that is useful.
Summary
On Windows, every upload
PUTfails with HTTP 500 andartifact_commit_uploadthen refuses the commit, so no artifact can ever be published. The cause is a directoryfsyncinsyncDirectory(), which Windows cannot perform. The fix is a three-line platform guard.Reproduced against
2251e42(v0.1.0) on Windows 10 22H2, Node 24.15.0, running the local service via the MCP stdio adapter.Symptom
artifact_create_uploadsucceeds and returns an upload plan.PUTing the exact bytes to the returneduploadUrlreturns HTTP 500INTERNAL_ERROR. The service log recordsfailure_tag: "StagingStorageFailure".artifact_commit_uploadthen fails withUPLOAD_INCOMPLETE: Every declared upload file must be verified before commit.Cause
syncDirectory()insrc/storage/verified-file.ts:144opens the directory as a file handle and callssync()on it:A directory
fsyncis a POSIX durability barrier. Windows has no equivalent and cannot open a directory as a file handle, soopen()/sync()throwsEPERM: operation not permitted, fsync. Minimal repro, independent of this project:Both storage paths route through this one function, so a single guard covers both:
src/storage/local-staging-store.ts:69— upload staging (this is what breaks thePUT)src/storage/local-blob-store.ts:119— commit into the blob storeSuggested fix
Early-return on win32:
Durability note
This drops a crash-durability barrier, but only on the platform that cannot provide it anyway, and only for the directory entry. The file bytes are still
fsynced viafile.sync()before the rename, so committed artifacts remain durable. The worst case after a hard power loss is a staged upload that has to be redone.Verification
With the guard applied and
pnpm buildre-run, the same sequence returnsHTTP 200with{"status":"verified"}on thePUT, andartifact_commit_uploadreturns a normal artifact with working review and version links. Publishing has been reliable since.Happy to open a PR with the guard plus a
process.platformtest if that is useful.