When multiple tool calls (e.g. concurrent gsd_save_decision) target the same markdown file, the deterministic .tmp suffix caused ENOENT on rename() because one caller consumed the temp file before another could rename it. Replace the static `.tmp` suffix with a per-call random suffix so each concurrent writer gets its own temp file. Also clean up orphaned temp files on rename failure.
This commit is contained in:
parent
b44896e187
commit
ef706726c8
1 changed files with 13 additions and 2 deletions
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
import { promises as fs } from 'node:fs';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { resolveMilestoneFile, relMilestoneFile, resolveGsdRootFile } from './paths.js';
|
||||
import { milestoneIdSort, findMilestoneIds } from './guided-flow.js';
|
||||
|
||||
|
|
@ -705,9 +706,19 @@ export async function saveFile(path: string, content: string): Promise<void> {
|
|||
const dir = dirname(path);
|
||||
await fs.mkdir(dir, { recursive: true });
|
||||
|
||||
const tmpPath = path + '.tmp';
|
||||
// Use a unique temp path per call to avoid collisions when parallel
|
||||
// tool calls target the same file (e.g. concurrent gsd_save_decision).
|
||||
// rename() is atomic on POSIX, so last-writer-wins is correct for
|
||||
// regenerate-from-DB writes.
|
||||
const tmpPath = path + `.tmp.${randomBytes(4).toString("hex")}`;
|
||||
await fs.writeFile(tmpPath, content, 'utf-8');
|
||||
await fs.rename(tmpPath, path);
|
||||
try {
|
||||
await fs.rename(tmpPath, path);
|
||||
} catch (err) {
|
||||
// Clean up orphaned temp file on rename failure
|
||||
await fs.unlink(tmpPath).catch(() => {});
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export function parseRequirementCounts(content: string | null): RequirementCounts {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue