From 7241aef563d2ec37c2210e57f7552e8179724fc2 Mon Sep 17 00:00:00 2001 From: Luo0oo Date: Sat, 4 Jul 2026 05:29:38 +1000 Subject: [PATCH] fix(MemoryGraph): first-wins on duplicate KNOWLEDGE slugs (drop UsageGraphError crash) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit buildGraph iterates KNOWLEDGE domains (People/Companies/Ideas/Research) and derives each node's id from the bare filename via entry.replace(/\.md$/, ''). No domain prefix, so any two files sharing a basename across domains produce identical ids and the second addNode throws: UsageGraphError: Graph.addNode: the "synthesis" node already exist in the graph. The crash blocks the whole build; the graph never renders and PATTERNS.md never regenerates, silently breaking Pulse's /api/memory/graph route. Guards graph.addNode with hasNode, matching the existing first-wins semantics of the bareToId population two lines below. Emits console.warn listing kept-path and skipped-path so users notice the data loss (this is a report-generator tool, not a scriptable pipe — silent drop would hide files they wrote). Reproducer: mkdir -p ~/.claude/LIFEOS/MEMORY/KNOWLEDGE/{Companies,Research} printf '%s\n' '---' 'title: A' '---' 'body' > ~/.claude/LIFEOS/MEMORY/KNOWLEDGE/Companies/foo.md printf '%s\n' '---' 'title: B' '---' 'body' > ~/.claude/LIFEOS/MEMORY/KNOWLEDGE/Research/foo.md bun ~/.claude/LIFEOS/TOOLS/MemoryGraph.ts build # crashes without this fix Orthogonal issues noticed but out of scope (worth separate PRs): - KNOWLEDGE-first ordering in ingest() means WORK ISAs lose bare-slug wikilink resolution when a KNOWLEDGE file shares the basename - 'patterns' command rebuilds only on 60min cache age, never checks source mtimes — user can edit a note and get stale patterns silently - addEdge widens weight on collision but never upgrades kind (a later 'related' edge over an earlier 'tag' keeps kind 'tag' — provenance loss) --- LifeOS/install/LifeOS/TOOLS/MemoryGraph.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/LifeOS/install/LifeOS/TOOLS/MemoryGraph.ts b/LifeOS/install/LifeOS/TOOLS/MemoryGraph.ts index 4a173dfba..a2c441f36 100644 --- a/LifeOS/install/LifeOS/TOOLS/MemoryGraph.ts +++ b/LifeOS/install/LifeOS/TOOLS/MemoryGraph.ts @@ -284,8 +284,19 @@ function buildGraph(raws: Raw[], layer: EdgeLayer = "declared"): { graph: Graph; // bare-slug -> id resolver (knowledge slug is bare; work id is "work:slug") const bareToId = new Map(); + // Track winner's source path so we can warn on collision (report-generator + // tool, not a scriptable pipe — silent drop hides data loss from users). + const idToPath = new Map(); for (const { node } of raws) { + if (graph.hasNode(node.id)) { + // First-wins on duplicate ids (e.g. Companies/synthesis.md and + // Research/synthesis.md both derive slug "synthesis"). Matches the + // bareToId first-wins semantics below. + console.warn(`[MemoryGraph] slug collision: kept ${idToPath.get(node.id)}, skipped ${node.path}`); + continue; + } graph.addNode(node.id, { ...node }); + idToPath.set(node.id, node.path); const bare = node.id.startsWith("work:") ? node.id.slice(5) : node.id; if (!bareToId.has(bare)) bareToId.set(bare, node.id); }