From 8c162d0ba4937a8ee99eeea58906788e47d6abed Mon Sep 17 00:00:00 2001 From: jiarung Date: Sat, 14 Mar 2026 18:41:39 +0000 Subject: [PATCH] fix(usage-log): write via temp file and atomic rename to prevent corruption MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit appendRecord previously called fs.writeFile(token-usage.json, …) directly. A process crash or SIGKILL during that write can leave the file truncated; readJsonArray then throws (SyntaxError), and since attempt.ts swallows the error with .catch(), that one interrupted write silently disables all future token logging for the workspace until the file is manually repaired. Fix: write the new content to a uniquely-named sibling temp file first, then call fs.rename() to atomically replace the real file. rename(2) is atomic on POSIX when src and dst share the same directory/filesystem, so readers always see either the old complete file or the new complete file — never a partial write. The temp file is unlinked on error to avoid leaving orphans. --- src/agents/usage-log.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/agents/usage-log.ts b/src/agents/usage-log.ts index c8a1cdb254c..43bfaabeab1 100644 --- a/src/agents/usage-log.ts +++ b/src/agents/usage-log.ts @@ -145,7 +145,18 @@ async function appendRecord(file: string, entry: TokenUsageRecord): Promise { const records = await readJsonArray(file); records.push(entry); - await fs.writeFile(file, JSON.stringify(records, null, 2)); + // Write to a sibling temp file then atomically rename into place so that + // a crash or kill during the write never leaves token-usage.json truncated. + // rename(2) is atomic on POSIX when src and dst are on the same filesystem, + // which is guaranteed here because both paths share the same directory. + const tmp = `${file}.tmp.${randomBytes(4).toString("hex")}`; + try { + await fs.writeFile(tmp, JSON.stringify(records, null, 2)); + await fs.rename(tmp, file); + } catch (err) { + await fs.unlink(tmp).catch(() => {}); + throw err; + } }); }