diff --git a/src/node/services/historyService.ts b/src/node/services/historyService.ts index 13be7fd38e..708cd4bcdb 100644 --- a/src/node/services/historyService.ts +++ b/src/node/services/historyService.ts @@ -1,5 +1,6 @@ import * as fs from "fs/promises"; import * as path from "path"; +import writeFileAtomic from "write-file-atomic"; import type { Result } from "@/common/types/result"; import { Ok, Err } from "@/common/types/result"; import type { MuxMessage } from "@/common/types/message"; @@ -235,7 +236,8 @@ export class HistoryService { .map((msg) => JSON.stringify({ ...msg, workspaceId }) + "\n") .join(""); - await fs.writeFile(historyPath, historyEntries); + // Atomic write prevents corruption if app crashes mid-write + await writeFileAtomic(historyPath, historyEntries); return Ok(undefined); } catch (error) { const message = error instanceof Error ? error.message : String(error); @@ -272,7 +274,8 @@ export class HistoryService { .map((msg) => JSON.stringify({ ...msg, workspaceId }) + "\n") .join(""); - await fs.writeFile(historyPath, historyEntries); + // Atomic write prevents corruption if app crashes mid-write + await writeFileAtomic(historyPath, historyEntries); // Update sequence counter to continue from where we truncated if (truncatedMessages.length > 0) { @@ -399,7 +402,8 @@ export class HistoryService { .map((msg) => JSON.stringify({ ...msg, workspaceId }) + "\n") .join(""); - await fs.writeFile(historyPath, historyEntries); + // Atomic write prevents corruption if app crashes mid-write + await writeFileAtomic(historyPath, historyEntries); // Update sequence counter to continue from where we are if (remainingMessages.length > 0) { @@ -455,7 +459,8 @@ export class HistoryService { .map((msg) => JSON.stringify({ ...msg, workspaceId: newWorkspaceId }) + "\n") .join(""); - await fs.writeFile(newHistoryPath, historyEntries); + // Atomic write prevents corruption if app crashes mid-write + await writeFileAtomic(newHistoryPath, historyEntries); // Transfer sequence counter to new workspace ID const oldCounter = this.sequenceCounters.get(oldWorkspaceId) ?? 0; diff --git a/src/node/services/partialService.ts b/src/node/services/partialService.ts index bfb258149a..6bb20c3cbf 100644 --- a/src/node/services/partialService.ts +++ b/src/node/services/partialService.ts @@ -1,5 +1,6 @@ import * as fs from "fs/promises"; import * as path from "path"; +import writeFileAtomic from "write-file-atomic"; import type { Result } from "@/common/types/result"; import { Ok, Err } from "@/common/types/result"; import type { MuxMessage } from "@/common/types/message"; @@ -80,7 +81,9 @@ export class PartialService { }, }; - await fs.writeFile(partialPath, JSON.stringify(partialMessage, null, 2)); + // Atomic write: writes to temp file then renames, preventing corruption + // if app crashes mid-write (prevents "Unexpected end of JSON input" on read) + await writeFileAtomic(partialPath, JSON.stringify(partialMessage, null, 2)); return Ok(undefined); } catch (error) { const message = error instanceof Error ? error.message : String(error); diff --git a/src/node/utils/sessionFile.ts b/src/node/utils/sessionFile.ts index b253138201..e1b690127b 100644 --- a/src/node/utils/sessionFile.ts +++ b/src/node/utils/sessionFile.ts @@ -1,5 +1,6 @@ import * as fs from "fs/promises"; import * as path from "path"; +import writeFileAtomic from "write-file-atomic"; import type { Result } from "@/common/types/result"; import { Ok, Err } from "@/common/types/result"; import type { Config } from "@/node/config"; @@ -55,7 +56,8 @@ export class SessionFileManager { const sessionDir = this.config.getSessionDir(workspaceId); await fs.mkdir(sessionDir, { recursive: true }); const filePath = this.getFilePath(workspaceId); - await fs.writeFile(filePath, JSON.stringify(data, null, 2)); + // Atomic write prevents corruption if app crashes mid-write + await writeFileAtomic(filePath, JSON.stringify(data, null, 2)); return Ok(undefined); } catch (error) { const message = error instanceof Error ? error.message : String(error);