Skip to content

Commit f9846c0

Browse files
committed
🤖 fix: use atomic writes for session files to prevent corruption
Non-atomic fs.writeFile can leave truncated/malformed JSON if the app crashes mid-write. This causes 'Unexpected end of JSON input' errors when reading partial.json on the next stream start, leading to amnesia (messages being lost). Use write-file-atomic which writes to a temp file then renames, ensuring readers always see either the old complete file or the new complete file, never a partial write. Fixed in: - partialService.ts (partial.json) - historyService.ts (chat.jsonl rewrites) - sessionFile.ts (generic session file utility) Fixes #803
1 parent 284dbc7 commit f9846c0

File tree

3 files changed

+16
-6
lines changed

3 files changed

+16
-6
lines changed

src/node/services/historyService.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as fs from "fs/promises";
22
import * as path from "path";
3+
import writeFileAtomic from "write-file-atomic";
34
import type { Result } from "@/common/types/result";
45
import { Ok, Err } from "@/common/types/result";
56
import type { MuxMessage } from "@/common/types/message";
@@ -235,7 +236,8 @@ export class HistoryService {
235236
.map((msg) => JSON.stringify({ ...msg, workspaceId }) + "\n")
236237
.join("");
237238

238-
await fs.writeFile(historyPath, historyEntries);
239+
// Atomic write prevents corruption if app crashes mid-write
240+
await writeFileAtomic(historyPath, historyEntries);
239241
return Ok(undefined);
240242
} catch (error) {
241243
const message = error instanceof Error ? error.message : String(error);
@@ -272,7 +274,8 @@ export class HistoryService {
272274
.map((msg) => JSON.stringify({ ...msg, workspaceId }) + "\n")
273275
.join("");
274276

275-
await fs.writeFile(historyPath, historyEntries);
277+
// Atomic write prevents corruption if app crashes mid-write
278+
await writeFileAtomic(historyPath, historyEntries);
276279

277280
// Update sequence counter to continue from where we truncated
278281
if (truncatedMessages.length > 0) {
@@ -399,7 +402,8 @@ export class HistoryService {
399402
.map((msg) => JSON.stringify({ ...msg, workspaceId }) + "\n")
400403
.join("");
401404

402-
await fs.writeFile(historyPath, historyEntries);
405+
// Atomic write prevents corruption if app crashes mid-write
406+
await writeFileAtomic(historyPath, historyEntries);
403407

404408
// Update sequence counter to continue from where we are
405409
if (remainingMessages.length > 0) {
@@ -455,7 +459,8 @@ export class HistoryService {
455459
.map((msg) => JSON.stringify({ ...msg, workspaceId: newWorkspaceId }) + "\n")
456460
.join("");
457461

458-
await fs.writeFile(newHistoryPath, historyEntries);
462+
// Atomic write prevents corruption if app crashes mid-write
463+
await writeFileAtomic(newHistoryPath, historyEntries);
459464

460465
// Transfer sequence counter to new workspace ID
461466
const oldCounter = this.sequenceCounters.get(oldWorkspaceId) ?? 0;

src/node/services/partialService.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as fs from "fs/promises";
22
import * as path from "path";
3+
import writeFileAtomic from "write-file-atomic";
34
import type { Result } from "@/common/types/result";
45
import { Ok, Err } from "@/common/types/result";
56
import type { MuxMessage } from "@/common/types/message";
@@ -80,7 +81,9 @@ export class PartialService {
8081
},
8182
};
8283

83-
await fs.writeFile(partialPath, JSON.stringify(partialMessage, null, 2));
84+
// Atomic write: writes to temp file then renames, preventing corruption
85+
// if app crashes mid-write (prevents "Unexpected end of JSON input" on read)
86+
await writeFileAtomic(partialPath, JSON.stringify(partialMessage, null, 2));
8487
return Ok(undefined);
8588
} catch (error) {
8689
const message = error instanceof Error ? error.message : String(error);

src/node/utils/sessionFile.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as fs from "fs/promises";
22
import * as path from "path";
3+
import writeFileAtomic from "write-file-atomic";
34
import type { Result } from "@/common/types/result";
45
import { Ok, Err } from "@/common/types/result";
56
import type { Config } from "@/node/config";
@@ -55,7 +56,8 @@ export class SessionFileManager<T> {
5556
const sessionDir = this.config.getSessionDir(workspaceId);
5657
await fs.mkdir(sessionDir, { recursive: true });
5758
const filePath = this.getFilePath(workspaceId);
58-
await fs.writeFile(filePath, JSON.stringify(data, null, 2));
59+
// Atomic write prevents corruption if app crashes mid-write
60+
await writeFileAtomic(filePath, JSON.stringify(data, null, 2));
5961
return Ok(undefined);
6062
} catch (error) {
6163
const message = error instanceof Error ? error.message : String(error);

0 commit comments

Comments
 (0)