diff --git a/src/memory/__tests__/knowledge-graph.test.ts b/src/memory/__tests__/knowledge-graph.test.ts index 236242413a..bbfe6190c6 100644 --- a/src/memory/__tests__/knowledge-graph.test.ts +++ b/src/memory/__tests__/knowledge-graph.test.ts @@ -515,4 +515,51 @@ describe('KnowledgeGraphManager', () => { expect(result.relations[0]).not.toHaveProperty('type'); }); }); + + describe('loadGraph validation', () => { + it('skips corrupt entities instead of crashing search', async () => { + const lines = [ + JSON.stringify({ type: 'entity', name: 'Alice', entityType: 'person', observations: ['works at Acme Corp'] }), + JSON.stringify({ type: 'entity', name: 'Broken', observations: ['missing entityType'] }), + JSON.stringify({ type: 'entity', name: 'BadObs', entityType: 'person', observations: ['ok', null] }), + ]; + await fs.writeFile(testFilePath, lines.join('\n') + '\n'); + + const graph = await manager.readGraph(); + expect(graph.entities).toHaveLength(1); + expect(graph.entities[0].name).toBe('Alice'); + + // searchNodes must not throw even though the file contains corrupt entries + const result = await manager.searchNodes('Acme'); + expect(result.entities).toHaveLength(1); + expect(result.entities[0].name).toBe('Alice'); + }); + + it('skips corrupt relations', async () => { + const lines = [ + JSON.stringify({ type: 'entity', name: 'Alice', entityType: 'person', observations: [] }), + JSON.stringify({ type: 'relation', from: 'Alice', to: 'Bob' }), // missing relationType + JSON.stringify({ type: 'relation', from: 'Alice', to: 'Bob', relationType: 'knows' }), + ]; + await fs.writeFile(testFilePath, lines.join('\n') + '\n'); + + const graph = await manager.readGraph(); + expect(graph.entities).toHaveLength(1); + expect(graph.relations).toHaveLength(1); + expect(graph.relations[0].relationType).toBe('knows'); + }); + + it('skips malformed JSON lines', async () => { + const lines = [ + JSON.stringify({ type: 'entity', name: 'Alice', entityType: 'person', observations: [] }), + '{this is not valid json', + JSON.stringify({ type: 'entity', name: 'Bob', entityType: 'person', observations: [] }), + ]; + await fs.writeFile(testFilePath, lines.join('\n') + '\n'); + + const graph = await manager.readGraph(); + expect(graph.entities).toHaveLength(2); + expect(graph.entities.map(e => e.name)).toEqual(['Alice', 'Bob']); + }); + }); }); diff --git a/src/memory/index.ts b/src/memory/index.ts index b8704a9515..c15b5693fc 100644 --- a/src/memory/index.ts +++ b/src/memory/index.ts @@ -74,24 +74,47 @@ export class KnowledgeGraphManager { 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) => { - const item = JSON.parse(line); - if (item.type === "entity") { - graph.entities.push({ - name: item.name, - entityType: item.entityType, - observations: item.observations - }); + const graph: KnowledgeGraph = { entities: [], relations: [] }; + + for (const line of lines) { + let item: unknown; + try { + item = JSON.parse(line); + } catch { + console.error("Skipping malformed line in memory file"); + continue; } - if (item.type === "relation") { - graph.relations.push({ - from: item.from, - to: item.to, - relationType: item.relationType - }); + + if (typeof item !== "object" || item === null) { + console.error("Skipping non-object line in memory file"); + continue; + } + + const record = item as Record; + if (record.type === "entity") { + const parsed = EntitySchema.safeParse(item); + if (parsed.success) { + graph.entities.push(parsed.data); + } else { + console.error( + "Skipping invalid entity in memory file:", + parsed.error.issues.map(issue => `${issue.path.join(".")}: ${issue.message}`).join(", ") + ); + } + } else if (record.type === "relation") { + const parsed = RelationSchema.safeParse(item); + if (parsed.success) { + graph.relations.push(parsed.data); + } else { + console.error( + "Skipping invalid relation in memory file:", + parsed.error.issues.map(issue => `${issue.path.join(".")}: ${issue.message}`).join(", ") + ); + } } - return graph; - }, { entities: [], relations: [] }); + } + + return graph; } catch (error) { if (error instanceof Error && 'code' in error && (error as any).code === "ENOENT") { return { entities: [], relations: [] };