Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion src/task-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,41 @@ export class FileTaskStore implements TaskStore {
const payload = `${JSON.stringify(nextTask, null, 2)}\n`;

await writeFile(tmpPath, payload, "utf8");
await rename(tmpPath, targetPath);

// Windows: atomic rename can intermittently fail with EPERM/EACCES when the
// destination file is being scanned/read. This breaks task polling.
// Prefer rename (atomic), but fall back to direct write with cleanup.
try {
await rename(tmpPath, targetPath);
return;
} catch (error: unknown) {
const code = (error as { code?: string } | undefined)?.code;
if (code !== "EPERM" && code !== "EACCES") {
throw error;
}

// Retry a few times with small backoff; then fall back to overwrite.
for (let attempt = 0; attempt < 5; attempt++) {
try {
await new Promise((r) => setTimeout(r, 25 * (attempt + 1)));
await rename(tmpPath, targetPath);
return;
} catch (retryError: unknown) {
const retryCode = (retryError as { code?: string } | undefined)?.code;
if (retryCode !== "EPERM" && retryCode !== "EACCES") {
throw retryError;
}
}
}

// Non-atomic fallback (best-effort).
await writeFile(targetPath, payload, "utf8");
try {
await unlink(tmpPath);
} catch {
// ignore
}
}
}

/** List all stored task IDs. */
Expand Down
Loading