From c4c8ffad6ef9eec7330f484d320e49c6fd835174 Mon Sep 17 00:00:00 2001 From: ZhangJing-gugugaga <2240215294@qq.com> Date: Fri, 10 Jul 2026 20:33:03 +0800 Subject: [PATCH] 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 <> --- src/memory/index.ts | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/src/memory/index.ts b/src/memory/index.ts index 9865c5318e..930c6cddc6 100644 --- a/src/memory/index.ts +++ b/src/memory/index.ts @@ -70,10 +70,20 @@ export class KnowledgeGraphManager { constructor(private memoryFilePath: string) {} private async loadGraph(): Promise { + let data: string; try { - const data = await fs.readFile(this.memoryFilePath, "utf-8"); - const lines = data.split("\n").filter(line => line.trim() !== ""); - return lines.reduce((graph: KnowledgeGraph, line) => { + data = await fs.readFile(this.memoryFilePath, "utf-8"); + } catch (error) { + if (error instanceof Error && 'code' in error && (error as any).code === "ENOENT") { + return { entities: [], relations: [] }; + } + throw error; + } + + const lines = data.split("\n").filter(line => line.trim() !== ""); + const graph: KnowledgeGraph = { entities: [], relations: [] }; + for (const line of lines) { + try { const item = JSON.parse(line); if (item.type === "entity") { graph.entities.push({ @@ -81,22 +91,20 @@ export class KnowledgeGraphManager { entityType: item.entityType, observations: item.observations }); - } - if (item.type === "relation") { + } else if (item.type === "relation") { graph.relations.push({ from: item.from, to: item.to, relationType: item.relationType }); } - return graph; - }, { entities: [], relations: [] }); - } catch (error) { - if (error instanceof Error && 'code' in error && (error as any).code === "ENOENT") { - return { entities: [], relations: [] }; + } catch { + // Skip malformed lines (e.g. truncated by a crash mid-write) instead + // of crashing the whole server. Surfaces a warning on stderr. + console.error(`Skipping malformed memory line: ${line.slice(0, 120)}`); } - throw error; } + return graph; } private async saveGraph(graph: KnowledgeGraph): Promise { @@ -114,7 +122,11 @@ export class KnowledgeGraphManager { relationType: r.relationType })), ]; - await fs.writeFile(this.memoryFilePath, lines.join("\n")); + // Atomic replace: write to a pid-suffixed temp file then rename so a + // crash mid-write can never leave a truncated memory file behind. + const tmpPath = `${this.memoryFilePath}.${process.pid}.tmp`; + await fs.writeFile(tmpPath, lines.join("\n")); + await fs.rename(tmpPath, this.memoryFilePath); } async createEntities(entities: Entity[]): Promise {