diff --git a/packages/mnemopi/CHANGELOG.md b/packages/mnemopi/CHANGELOG.md index 06149cdeb..5c06203ae 100644 --- a/packages/mnemopi/CHANGELOG.md +++ b/packages/mnemopi/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed `consolidateToEpisodic` (the function backing `sleep` / `sleepAllSessions`) never populating the episodic graph: the `gists` and `graph_edges` tables stayed at 0 rows across every bank even after multiple consolidation cycles, so Polyphonic Recall's `graph` voice (BFS over `findGistsByParticipant` / `findRelatedMemories`) always returned nothing. Consolidation now best-effort ingests the new episodic memory into `EpisodicGraph` so the gist row, gist→memory `ctx` edge, fact edges, and cross-memory similarity/entity/temporal edges land alongside the episodic row. Independent of the existing `MNEMOPI_PROACTIVE_LINKING` flag, which still gates the same enrichment on the `remember()` write path. ([#2435](https://github.com/can1357/oh-my-pi/issues/2435)) + ## [15.12.0] - 2026-06-12 ### Changed diff --git a/packages/mnemopi/src/core/beam/consolidate.ts b/packages/mnemopi/src/core/beam/consolidate.ts index 4d35221ae..de2b86d82 100644 --- a/packages/mnemopi/src/core/beam/consolidate.ts +++ b/packages/mnemopi/src/core/beam/consolidate.ts @@ -1,6 +1,7 @@ import type { SQLQueryBindings } from "bun:sqlite"; import { generateId, stableMemoryId } from "../../util/ids"; import { aaakEncode } from "../aaak"; +import { EpisodicGraph } from "../episodic-graph"; import { heuristicExtractFacts } from "../extraction"; import { clampVeracity } from "../veracity-consolidation"; import { scheduleEmbedding } from "./helpers"; @@ -261,6 +262,33 @@ function insertKg( }); } +/** + * Populate the episodic graph (gists, facts, edges) for a freshly consolidated + * memory. The graph backs Polyphonic Recall's `graph` voice, which relies on + * `findGistsByParticipant`, `findFactsBySubject`, and `findRelatedMemories`. + * + * Best-effort: failures (closed DB, missing optional tables) MUST NOT roll back + * the consolidation that already wrote the episodic row, so any throw here is + * swallowed. Unlike the proactive-link path in `store.ts` this is unconditional + * — consolidation is the explicit "settle and compress" step where the graph + * is supposed to gain edges. + */ +function ingestIntoEpisodicGraph(beam: BeamMemoryState, memoryId: string, summary: string): void { + try { + const graph = + beam.episodicGraph instanceof EpisodicGraph + ? beam.episodicGraph + : new EpisodicGraph({ db: beam.db, dbPath: beam.dbPath }); + graph.ingestMemory(summary, memoryId, { + sessionId: sourceSession(beam), + linkExisting: true, + extractEntities: true, + }); + } catch { + // Graph enrichment is best-effort and never blocks consolidation. + } +} + export function consolidateToEpisodic( beam: BeamMemoryState, summary: string, @@ -299,6 +327,7 @@ export function consolidateToEpisodic( ], ); extractAndStoreFacts(beam, summary, 0, memoryId); + ingestIntoEpisodicGraph(beam, memoryId, summary); scheduleEmbedding(beam, [{ memoryId, content: summary }]); emitEvent(beam, "MEMORY_CONSOLIDATED", memoryId, summary, source, importance, { summary_of: [...sourceWmIds], diff --git a/packages/mnemopi/test/beam-consolidate-unit.test.ts b/packages/mnemopi/test/beam-consolidate-unit.test.ts index d5ee6d9ba..0de3b31f6 100644 --- a/packages/mnemopi/test/beam-consolidate-unit.test.ts +++ b/packages/mnemopi/test/beam-consolidate-unit.test.ts @@ -93,6 +93,45 @@ describe("beam consolidation free functions", () => { expect(getEpisodicStats(beam).total).toBe(1); }); + it("consolidateToEpisodic populates the episodic graph (gists, edges) for the new memory (#2435)", () => { + const beam = trackedState(); + insertWorking(beam.db, "wm1", "s1", "Alice deployed the staging cluster checklist"); + + const id = consolidateToEpisodic( + beam, + "Alice deployed the staging cluster checklist", + ["wm1"], + "consolidation", + 0.7, + ); + + const gist = beam.db.query("SELECT id, memory_id FROM gists WHERE memory_id = ?").get(id) as { + id: string; + memory_id: string; + } | null; + expect(gist).not.toBeNull(); + expect(gist?.id).toBe(`gist_${id}`); + const edges = beam.db + .query("SELECT source, target, edge_type FROM graph_edges WHERE source = ? OR target = ?") + .all(id, id) as { source: string; target: string; edge_type: string }[]; + expect(edges.some(edge => edge.source === id && edge.target === `gist_${id}` && edge.edge_type === "ctx")).toBe( + true, + ); + }); + + it("sleepAllSessions adds gists and edges for every consolidated session (#2435)", () => { + const beam = trackedState("maintenance"); + insertWorking(beam.db, "wm-a1", "a", "Alpha launch checklist"); + insertWorking(beam.db, "wm-b1", "b", "Beta launch checklist"); + + const result = sleepAllSessions(beam, false); + expect(result.items_consolidated).toBe(2); + const gistCount = (beam.db.query("SELECT COUNT(*) AS count FROM gists").get() as { count: number }).count; + const edgeCount = (beam.db.query("SELECT COUNT(*) AS count FROM graph_edges").get() as { count: number }).count; + expect(gistCount).toBe(2); + expect(edgeCount).toBeGreaterThan(0); + }); + it("sleep dry-run is side-effect-free and real sleep marks originals, writes summary and log", () => { const beam = trackedState(); insertWorking(beam.db, "wm1", "s1", "task alpha", "conversation");