Skip to content

Commit e41f212

Browse files
fix(memory): atomic save + per-line parse guard against corruption (#1481)
Two defects in the knowledge-graph JSONL persistence caused the whole server to fail at startup whenever the memory file was truncated or malformed (the "JSON Parsing Error - All Tools Failing" report): 1. saveGraph wrote the file in place with fs.writeFile. A crash mid- write left a truncated last JSONL line. Now we write to a pid-suffixed .tmp file and rename into place for an atomic replace. 2. loadGraph relied on JSON.parse inside the reducer with no per- line guard, so one malformed line threw and aborted the entire load. Now each line is parsed in its own try/catch; bad lines are skipped with a stderr warning and the server keeps the valid rows. Co-Authored-By: Claude Opus 4.6 <<EMAIL>>
1 parent b03410b commit e41f212

1 file changed

Lines changed: 24 additions & 12 deletions

File tree

src/memory/index.ts

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -70,33 +70,41 @@ export class KnowledgeGraphManager {
7070
constructor(private memoryFilePath: string) {}
7171

7272
private async loadGraph(): Promise<KnowledgeGraph> {
73+
let data: string;
7374
try {
74-
const data = await fs.readFile(this.memoryFilePath, "utf-8");
75-
const lines = data.split("\n").filter(line => line.trim() !== "");
76-
return lines.reduce((graph: KnowledgeGraph, line) => {
75+
data = await fs.readFile(this.memoryFilePath, "utf-8");
76+
} catch (error) {
77+
if (error instanceof Error && 'code' in error && (error as any).code === "ENOENT") {
78+
return { entities: [], relations: [] };
79+
}
80+
throw error;
81+
}
82+
83+
const lines = data.split("\n").filter(line => line.trim() !== "");
84+
const graph: KnowledgeGraph = { entities: [], relations: [] };
85+
for (const line of lines) {
86+
try {
7787
const item = JSON.parse(line);
7888
if (item.type === "entity") {
7989
graph.entities.push({
8090
name: item.name,
8191
entityType: item.entityType,
8292
observations: item.observations
8393
});
84-
}
85-
if (item.type === "relation") {
94+
} else if (item.type === "relation") {
8695
graph.relations.push({
8796
from: item.from,
8897
to: item.to,
8998
relationType: item.relationType
9099
});
91100
}
92-
return graph;
93-
}, { entities: [], relations: [] });
94-
} catch (error) {
95-
if (error instanceof Error && 'code' in error && (error as any).code === "ENOENT") {
96-
return { entities: [], relations: [] };
101+
} catch {
102+
// Skip malformed lines (e.g. truncated by a crash mid-write) instead
103+
// of crashing the whole server. Surfaces a warning on stderr.
104+
console.error(`Skipping malformed memory line: ${line.slice(0, 120)}`);
97105
}
98-
throw error;
99106
}
107+
return graph;
100108
}
101109

102110
private async saveGraph(graph: KnowledgeGraph): Promise<void> {
@@ -114,7 +122,11 @@ export class KnowledgeGraphManager {
114122
relationType: r.relationType
115123
})),
116124
];
117-
await fs.writeFile(this.memoryFilePath, lines.join("\n"));
125+
// Atomic replace: write to a pid-suffixed temp file then rename so a
126+
// crash mid-write can never leave a truncated memory file behind.
127+
const tmpPath = `${this.memoryFilePath}.${process.pid}.tmp`;
128+
await fs.writeFile(tmpPath, lines.join("\n"));
129+
await fs.rename(tmpPath, this.memoryFilePath);
118130
}
119131

120132
async createEntities(entities: Entity[]): Promise<Entity[]> {

0 commit comments

Comments
 (0)