pi-mega-compact 0.11.6 → 0.11.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/dist/extensions/dashboard-server/routes-memory-map.js +8 -15
  2. package/dist/extensions/mega-commands.js +65 -5
  3. package/dist/extensions/mega-config.js +6 -0
  4. package/dist/extensions/mega-events/context-handler.js +65 -0
  5. package/dist/src/config/turns.js +2 -1
  6. package/dist/src/memoryGraph/embedding.js +50 -0
  7. package/dist/src/memoryGraph/gates.js +379 -0
  8. package/dist/src/memoryGraph/sources.js +390 -0
  9. package/dist/src/memoryGraph.js +133 -259
  10. package/dist/src/recall.js +136 -1
  11. package/dist/src/store/sqlite/embedding-cache.js +71 -0
  12. package/dist/src/store/sqlite.js +1 -0
  13. package/dist/src/topics/cluster.js +10 -2
  14. package/extensions/dashboard-client/dist/assets/{AchievementTiles-DR9cLGDL.js → AchievementTiles-BESF1HT1.js} +2 -2
  15. package/extensions/dashboard-client/dist/assets/{AchievementTiles-DR9cLGDL.js.map → AchievementTiles-BESF1HT1.js.map} +1 -1
  16. package/extensions/dashboard-client/dist/assets/{AchievementsTab-svNUgJ1e.js → AchievementsTab-8RzNnz8u.js} +2 -2
  17. package/extensions/dashboard-client/dist/assets/{AchievementsTab-svNUgJ1e.js.map → AchievementsTab-8RzNnz8u.js.map} +1 -1
  18. package/extensions/dashboard-client/dist/assets/{CacheTab-ASwhOMjR.js → CacheTab-CUIiUgh3.js} +2 -2
  19. package/extensions/dashboard-client/dist/assets/{CacheTab-ASwhOMjR.js.map → CacheTab-CUIiUgh3.js.map} +1 -1
  20. package/extensions/dashboard-client/dist/assets/{ConfigTab-CmmWheuW.js → ConfigTab-D85fB9HP.js} +2 -2
  21. package/extensions/dashboard-client/dist/assets/{ConfigTab-CmmWheuW.js.map → ConfigTab-D85fB9HP.js.map} +1 -1
  22. package/extensions/dashboard-client/dist/assets/{EventsTab-CFE7jLsa.js → EventsTab-TCLR81G5.js} +2 -2
  23. package/extensions/dashboard-client/dist/assets/{EventsTab-CFE7jLsa.js.map → EventsTab-TCLR81G5.js.map} +1 -1
  24. package/extensions/dashboard-client/dist/assets/{GameTab-CJAX_th_.js → GameTab-BqneNRYH.js} +2 -2
  25. package/extensions/dashboard-client/dist/assets/{GameTab-CJAX_th_.js.map → GameTab-BqneNRYH.js.map} +1 -1
  26. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-P4SUsvMf.js → MaintenanceTab-LblMg3NB.js} +2 -2
  27. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-P4SUsvMf.js.map → MaintenanceTab-LblMg3NB.js.map} +1 -1
  28. package/extensions/dashboard-client/dist/assets/MemoryMapTab-CpjMQwRr.js +2 -0
  29. package/extensions/dashboard-client/dist/assets/MemoryMapTab-CpjMQwRr.js.map +1 -0
  30. package/extensions/dashboard-client/dist/assets/{MetricsTab-CHQscixy.js → MetricsTab-LxNtNzLQ.js} +2 -2
  31. package/extensions/dashboard-client/dist/assets/{MetricsTab-CHQscixy.js.map → MetricsTab-LxNtNzLQ.js.map} +1 -1
  32. package/extensions/dashboard-client/dist/assets/{OverviewTab-DBs4DJdd.js → OverviewTab-BXKUZbT6.js} +2 -2
  33. package/extensions/dashboard-client/dist/assets/{OverviewTab-DBs4DJdd.js.map → OverviewTab-BXKUZbT6.js.map} +1 -1
  34. package/extensions/dashboard-client/dist/assets/{ReposTab-DeFz76L5.js → ReposTab-DTooKeAi.js} +2 -2
  35. package/extensions/dashboard-client/dist/assets/{ReposTab-DeFz76L5.js.map → ReposTab-DTooKeAi.js.map} +1 -1
  36. package/extensions/dashboard-client/dist/assets/{SessionsTab-BWNsj1OB.js → SessionsTab-Devulgm9.js} +2 -2
  37. package/extensions/dashboard-client/dist/assets/{SessionsTab-BWNsj1OB.js.map → SessionsTab-Devulgm9.js.map} +1 -1
  38. package/extensions/dashboard-client/dist/assets/{SetupTab-uS6A0xmw.js → SetupTab-By9eqUoE.js} +2 -2
  39. package/extensions/dashboard-client/dist/assets/{SetupTab-uS6A0xmw.js.map → SetupTab-By9eqUoE.js.map} +1 -1
  40. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-CiqCOQuL.js → TimeSavedCard-z4M8TDLm.js} +2 -2
  41. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-CiqCOQuL.js.map → TimeSavedCard-z4M8TDLm.js.map} +1 -1
  42. package/extensions/dashboard-client/dist/assets/TopicsTab-bCGZSfLN.js +2 -0
  43. package/extensions/dashboard-client/dist/assets/TopicsTab-bCGZSfLN.js.map +1 -0
  44. package/extensions/dashboard-client/dist/assets/{TurnsTab-CVz4f4-o.js → TurnsTab-m_XzE0H4.js} +2 -2
  45. package/extensions/dashboard-client/dist/assets/{TurnsTab-CVz4f4-o.js.map → TurnsTab-m_XzE0H4.js.map} +1 -1
  46. package/extensions/dashboard-client/dist/assets/{index-DrktNOoT.js → index-PRkilPXw.js} +3 -3
  47. package/extensions/dashboard-client/dist/assets/{index-DrktNOoT.js.map → index-PRkilPXw.js.map} +1 -1
  48. package/extensions/dashboard-client/dist/assets/{useSSE-Bf5LOLSi.js → useSSE-CGzgc2IK.js} +2 -2
  49. package/extensions/dashboard-client/dist/assets/{useSSE-Bf5LOLSi.js.map → useSSE-CGzgc2IK.js.map} +1 -1
  50. package/extensions/dashboard-client/dist/index.html +1 -1
  51. package/extensions/dashboard-client/src/memory-map-layout.ts +161 -0
  52. package/extensions/dashboard-client/src/memory-map-shapes.tsx +126 -0
  53. package/extensions/dashboard-client/src/tabs/MemoryMapTab.tsx +201 -165
  54. package/extensions/dashboard-client/src/tabs/ReposTab.tsx +21 -0
  55. package/extensions/dashboard-client/src/tabs/TopicsTab.tsx +1 -1
  56. package/extensions/dashboard-server/api-contracts/index.ts +2 -1
  57. package/extensions/dashboard-server/api-contracts/memory-map.ts +14 -6
  58. package/extensions/dashboard-server/routes-memory-map.ts +21 -29
  59. package/extensions/mega-commands.ts +63 -6
  60. package/extensions/mega-config.ts +18 -0
  61. package/extensions/mega-events/context-handler.ts +80 -0
  62. package/package.json +1 -1
  63. package/src/config/turns.ts +5 -2
  64. package/src/memoryGraph/embedding.ts +63 -0
  65. package/src/memoryGraph/gates.ts +451 -0
  66. package/src/memoryGraph/sources.ts +484 -0
  67. package/src/memoryGraph.ts +188 -350
  68. package/src/recall.ts +168 -4
  69. package/src/store/sqlite/embedding-cache.ts +81 -0
  70. package/src/store/sqlite.ts +1 -0
  71. package/src/topics/cluster.ts +11 -1
  72. package/extensions/dashboard-client/dist/assets/MemoryMapTab-BBiH76eF.js +0 -2
  73. package/extensions/dashboard-client/dist/assets/MemoryMapTab-BBiH76eF.js.map +0 -1
  74. package/extensions/dashboard-client/dist/assets/TopicsTab-BWWQxltv.js +0 -2
  75. package/extensions/dashboard-client/dist/assets/TopicsTab-BWWQxltv.js.map +0 -1
@@ -1,299 +1,173 @@
1
1
  /**
2
- * memoryGraph.ts — S46 visual memory map: build a graph of graphs.
2
+ * memoryGraph.ts — S46/D3 visual memory map: build a graph of graphs.
3
3
  *
4
- * Nodes = checkpoints (with optional RAPTOR level annotations).
5
- * Edges = temporal adjacency, semantic similarity (cosine above threshold), and
6
- * RAPTOR parent–child links.
4
+ * Delegated-shell pattern: implementation lives in memoryGraph/gates.ts,
5
+ * memoryGraph/sources.ts, and memoryGraph/embedding.ts. This file owns
6
+ * the public types, the entry point (buildMemoryGraph), and exports.
7
7
  *
8
8
  * pi-agnostic (no pi runtime types). Zero network calls (PREVENT-PI-004).
9
- * Synchronous read-only interface non-fatal on store errors.
9
+ * Synchronous read-only interface -- non-fatal on store errors.
10
10
  *
11
11
  * Re-exports the public types as both MemoryGraph* and shorter aliases
12
12
  * (GraphNode, GraphEdge, GraphMetadata) so dashboard api-contracts can import
13
13
  * from the canonical source.
14
14
  */
15
- import { getStateDir, normalizeSessionId } from "./store.js";
16
- import { openStore, decodeEmbedding } from "./store/sqlite/utils.js";
17
- import { listRaptorNodes } from "./store/sqlite/raptor.js";
15
+ import { getStateDir } from "./store.js";
16
+ import { openStore } from "./store/sqlite/utils.js";
18
17
  import { Logger } from "./log.js";
18
+ import { runValidationPipeline } from "./memoryGraph/gates.js";
19
+ import { buildCheckpointNodes, buildTurnNodes, buildTurnContentNodes, buildMemoryNodes, addRaptorAnnotations, deduplicateEdges, areTurnsEnabled, isTurnContentEnabled, isTurnContentFlaggedOn, areMemoriesEnabled, } from "./memoryGraph/sources.js";
19
20
  const log = new Logger();
20
21
  // ---------------------------------------------------------------------------
21
- // Defaults
22
- // ---------------------------------------------------------------------------
23
- const DEFAULT_SIMILARITY_THRESHOLD = 0.7;
24
- const DEFAULT_MAX_EDGES_PER_NODE = 4;
25
- // ---------------------------------------------------------------------------
26
- // Public API
22
+ // Public entry point
27
23
  // ---------------------------------------------------------------------------
28
24
  /**
29
- * Build a memory graph from the store.
25
+ * Build a memory graph from three sources (checkpoints / turns / memories)
26
+ * wired through a 9-gate validation pipeline.
30
27
  *
31
- * Opens the SQLite store, reads all (or session-filtered) checkpoints,
32
- * computes cosine similarity edges, temporal edges, and RAPTOR parent edges.
33
- * Returns a MemoryGraph or an empty graph on failure (non-fatal).
28
+ * Returns an empty graph (byte-identical pre-change for checkpoint-only) when
29
+ * no sources are available.
34
30
  */
35
- export function buildMemoryGraph(opts = {}) {
36
- const { similarityThreshold = DEFAULT_SIMILARITY_THRESHOLD, maxEdgesPerNode = DEFAULT_MAX_EDGES_PER_NODE, sessionId, stateDir = getStateDir(), includeRaptorEdges = true, includeTemporalEdges = true, } = opts;
37
- const builtAt = Date.now();
38
- try {
39
- const nodes = loadNodes(sessionId, stateDir);
40
- const nodeIndex = buildNodeIndex(nodes);
41
- const edges = [];
42
- // Temporal edges: consecutive checkpoints within the same session
43
- if (includeTemporalEdges) {
44
- edges.push(...buildTemporalEdges(nodes));
45
- }
46
- // Semantic edges: cosine similarity above threshold
47
- edges.push(...buildSemanticEdges(nodes, nodeIndex, similarityThreshold, maxEdgesPerNode));
48
- // RAPTOR parent–child edges
49
- if (includeRaptorEdges) {
50
- edges.push(...buildRaptorEdges(nodes, sessionId, stateDir, nodeIndex));
51
- }
52
- // Deduplicate edges (keep highest weight for each source-target pair)
53
- const dedupedEdges = deduplicateEdges(edges);
54
- // Count sessions
55
- const sessionSet = new Set();
56
- for (const n of nodes) {
57
- sessionSet.add(n.sessionId);
58
- }
59
- const meta = {
60
- sessionCount: sessionSet.size,
61
- totalCheckpoints: nodes.length,
62
- totalEdges: dedupedEdges.length,
63
- semanticEdgeCount: dedupedEdges.filter((e) => e.type === "semantic").length,
64
- temporalEdgeCount: dedupedEdges.filter((e) => e.type === "temporal").length,
65
- raptorEdgeCount: dedupedEdges.filter((e) => e.type === "raptor_parent").length,
66
- similarityThresholdUsed: similarityThreshold,
67
- builtAt,
68
- };
69
- return { nodes, edges: dedupedEdges, metadata: meta };
31
+ export function buildMemoryGraph(sessionId, stateDir = getStateDir()) {
32
+ const { nodes, edges } = buildUnvalidatedGraph(sessionId, stateDir);
33
+ // Gate pipeline -- filters, deduplicates, and validates
34
+ const { ws, stats } = runValidationPipeline({ nodes, edges });
35
+ const nodeTypeBreakdown = {};
36
+ const edgeTypeBreakdown = {};
37
+ for (const n of ws.nodes) {
38
+ nodeTypeBreakdown[n.nodeType] = (nodeTypeBreakdown[n.nodeType] ?? 0) + 1;
70
39
  }
71
- catch (err) {
72
- log.warn("buildMemoryGraph failed", { error: String(err) });
73
- return {
74
- nodes: [],
75
- edges: [],
76
- metadata: {
77
- sessionCount: 0,
78
- totalCheckpoints: 0,
79
- totalEdges: 0,
80
- semanticEdgeCount: 0,
81
- temporalEdgeCount: 0,
82
- raptorEdgeCount: 0,
83
- similarityThresholdUsed: similarityThreshold,
84
- builtAt,
85
- },
86
- };
40
+ for (const e of ws.edges) {
41
+ edgeTypeBreakdown[e.type] = (edgeTypeBreakdown[e.type] ?? 0) + 1;
87
42
  }
88
- }
89
- // ---------------------------------------------------------------------------
90
- // Node loading
91
- // ---------------------------------------------------------------------------
92
- function loadNodes(sessionId, stateDir) {
93
- const db = openStore(stateDir);
94
- const sql = sessionId
95
- ? "SELECT * FROM context_chunks WHERE session_id = ? ORDER BY id ASC"
96
- : "SELECT * FROM context_chunks ORDER BY session_id, id ASC";
97
- const rows = sessionId
98
- ? db.prepare(sql).all(normalizeSessionId(sessionId))
99
- : db.prepare(sql).all();
100
- return rows.map(rowToGraphNode);
101
- }
102
- function rowToGraphNode(row) {
103
- const summary = row.summary ?? "";
43
+ const avgWeight = ws.edges.length > 0
44
+ ? ws.edges.reduce((s, e) => s + e.weight, 0) / ws.edges.length
45
+ : 0;
46
+ const sources = countSourcesByType(ws.nodes);
47
+ const validation = {
48
+ gatesRun: stats.gatesRun.length,
49
+ gatesPassed: stats.gatesPassed.length,
50
+ dropped: stats.dropped,
51
+ warnings: stats.warnings.map((w) => ({
52
+ gate: w.gate,
53
+ code: w.code,
54
+ count: w.count,
55
+ })),
56
+ sources,
57
+ builtAt: Date.now(),
58
+ };
104
59
  return {
105
- id: row.id,
106
- sessionId: row.session_id,
107
- label: row.id,
108
- summaryTruncated: summary.length > 200 ? summary.slice(0, 197) + "..." : summary,
109
- tokenEstimate: row.token_estimate ?? 0,
110
- timestamp: row.created_at ?? Date.now(),
111
- dedupStatus: row.dedup_status ?? undefined,
112
- raptorLevel: 0, // enriched below if RAPTOR data exists
113
- topicSummary: row.topic_summary ?? undefined,
114
- decisionCount: row.key_decisions
115
- ? safeJsonCount(row.key_decisions)
116
- : 0,
117
- textSnippet: summary.length > 200 ? summary.slice(0, 197) + "..." : summary,
60
+ nodes: ws.nodes,
61
+ edges: ws.edges,
62
+ metadata: {
63
+ totalNodes: ws.nodes.length,
64
+ totalEdges: ws.edges.length,
65
+ avgWeight,
66
+ nodeTypeBreakdown,
67
+ edgeTypeBreakdown,
68
+ },
69
+ validation,
118
70
  };
119
71
  }
120
- /** Parse a JSON array stored in a text column and return its length. */
121
- function safeJsonCount(raw) {
122
- if (!raw)
123
- return 0;
124
- try {
125
- const arr = JSON.parse(raw);
126
- return Array.isArray(arr) ? arr.length : 0;
127
- }
128
- catch {
129
- return 0;
130
- }
131
- }
132
72
  // ---------------------------------------------------------------------------
133
- // Node index building
73
+ // Source orchestrator
134
74
  // ---------------------------------------------------------------------------
135
- function buildNodeIndex(nodes) {
136
- const idx = new Map();
137
- nodes.forEach((n, i) => idx.set(n.id, i));
138
- return idx;
139
- }
140
- // ---------------------------------------------------------------------------
141
- // Edges: temporal
142
- // ---------------------------------------------------------------------------
143
- function buildTemporalEdges(nodes) {
75
+ function buildUnvalidatedGraph(sessionId, stateDir) {
76
+ const nodes = [];
144
77
  const edges = [];
145
- if (nodes.length < 2)
146
- return edges;
147
- let prev = nodes[0];
148
- for (let i = 1; i < nodes.length; i++) {
149
- const cur = nodes[i];
150
- if (cur.sessionId === prev.sessionId) {
151
- edges.push({
152
- source: prev.id,
153
- target: cur.id,
154
- weight: 1.0,
155
- type: "temporal",
156
- });
157
- }
158
- prev = cur;
78
+ let db;
79
+ try {
80
+ db = openStore(stateDir);
159
81
  }
160
- return edges;
161
- }
162
- // ---------------------------------------------------------------------------
163
- // Edges: semantic (cosine similarity)
164
- // ---------------------------------------------------------------------------
165
- function buildSemanticEdges(nodes, _nodeIndex, threshold, maxPerNode) {
166
- if (nodes.length < 2)
167
- return [];
168
- const db = openStore();
169
- // Retrieve embeddings from DB for each node
170
- const embeddings = new Map();
171
- const placeholders = nodes.map(() => "?").join(",");
172
- const ids = nodes.map((n) => n.id);
173
- const rows = db
174
- .prepare(`SELECT id, embedding_blob FROM context_chunks WHERE id IN (${placeholders})`)
175
- .all(...ids);
176
- for (const row of rows) {
177
- const emb = decodeEmbedding(row.embedding_blob);
178
- if (emb.length > 0) {
179
- embeddings.set(row.id, emb);
180
- }
82
+ catch {
83
+ log.warn("graph_source_unavailable", { detail: "Cannot open store" });
84
+ return { nodes: [], edges: [] };
181
85
  }
182
- const edges = [];
183
- for (const node of nodes) {
184
- const aId = node.id;
185
- const aEmb = embeddings.get(aId);
186
- if (!aEmb)
187
- continue;
188
- const candidates = [];
189
- for (const other of nodes) {
190
- if (other.id === aId)
191
- continue;
192
- // Skip if already connected via temporal or raptor edge
193
- const bEmb = embeddings.get(other.id);
194
- if (!bEmb)
195
- continue;
196
- const sim = cosineSimilarity(aEmb, bEmb);
197
- if (sim >= threshold) {
198
- candidates.push({ target: other.id, weight: sim });
86
+ try {
87
+ // Source: checkpoints (always on)
88
+ buildCheckpointNodes(db, sessionId, nodes, edges);
89
+ // Source A: turns (structural, metadata-only)
90
+ if (areTurnsEnabled()) {
91
+ try {
92
+ buildTurnNodes(db, sessionId, nodes, edges);
93
+ }
94
+ catch (err) {
95
+ log.warn("graph_source_unavailable", {
96
+ source: "turns",
97
+ detail: String(err),
98
+ });
199
99
  }
200
100
  }
201
- // Keep top-K highest-weight semantic neighbors
202
- candidates.sort((a, b) => b.weight - a.weight);
203
- const top = candidates.slice(0, maxPerNode);
204
- for (const c of top) {
205
- edges.push({
206
- source: aId,
207
- target: c.target,
208
- weight: c.weight,
209
- type: "semantic",
210
- });
101
+ // Source B: turn content (raw_transcript join, embedding-based)
102
+ if (isTurnContentEnabled()) {
103
+ try {
104
+ buildTurnContentNodes(db, sessionId, nodes, edges);
105
+ }
106
+ catch (err) {
107
+ log.warn("graph_source_unavailable", {
108
+ source: "turn-content",
109
+ detail: String(err),
110
+ });
111
+ }
211
112
  }
212
- }
213
- return edges;
214
- }
215
- // ---------------------------------------------------------------------------
216
- // Edges: RAPTOR parent–child
217
- // ---------------------------------------------------------------------------
218
- function buildRaptorEdges(nodes, sessionId, stateDir, nodeIndex) {
219
- const edges = [];
220
- try {
221
- if (sessionId) {
222
- enrichRaptorForSession(sessionId, stateDir, nodeIndex, nodes, edges);
113
+ else if (isTurnContentFlaggedOn() && !isTurnContentEnabled()) {
114
+ log.warn("graph_source_unavailable", {
115
+ source: "turn-content",
116
+ detail: "dbMirror is OFF -- no raw_transcript available",
117
+ });
223
118
  }
224
- else {
225
- // Load all sessions from DB
226
- const db = openStore(stateDir);
227
- const sessionRows = db
228
- .prepare("SELECT DISTINCT session_id FROM raptor_nodes ORDER BY session_id")
229
- .all();
230
- for (const row of sessionRows) {
231
- enrichRaptorForSession(row.session_id, stateDir, nodeIndex, nodes, edges);
119
+ // Source C: memories
120
+ if (areMemoriesEnabled()) {
121
+ try {
122
+ buildMemoryNodes(db, sessionId, nodes, edges);
123
+ }
124
+ catch (err) {
125
+ log.warn("graph_source_unavailable", {
126
+ source: "memories",
127
+ detail: String(err),
128
+ });
232
129
  }
233
130
  }
234
- }
235
- catch (err) {
236
- log.warn("RAPTOR edge build failed (non-fatal)", { error: String(err) });
237
- }
238
- return edges;
239
- }
240
- function enrichRaptorForSession(sessId, stateDir, nodeIndex, nodes, edges) {
241
- const raptorNodes = listRaptorNodes(sessId, stateDir);
242
- for (const rn of raptorNodes) {
243
- const idx = nodeIndex.get(rn.id);
244
- if (idx !== undefined) {
245
- // Annotate the graph node with the RAPTOR level
246
- nodes[idx].raptorLevel = rn.level;
131
+ // RAPTOR annotations on checkpoint nodes
132
+ try {
133
+ addRaptorAnnotations(sessionId, stateDir, nodes, edges);
247
134
  }
248
- if (!rn.parentId)
249
- continue;
250
- // Only create edge if both nodes are in the graph
251
- if (nodeIndex.has(rn.id) && nodeIndex.has(rn.parentId)) {
252
- edges.push({
253
- source: rn.id,
254
- target: rn.parentId,
255
- weight: Math.max(0.1, 1.0 - rn.level * 0.1), // higher levels = weaker link
256
- type: "raptor_parent",
257
- });
135
+ catch {
136
+ // non-fatal
258
137
  }
259
138
  }
260
- }
261
- // ---------------------------------------------------------------------------
262
- // Edge deduplication
263
- // ---------------------------------------------------------------------------
264
- function deduplicateEdges(edges) {
265
- const seen = new Map();
266
- for (const e of edges) {
267
- const keyA = `${e.source}|${e.target}`;
268
- const keyB = `${e.target}|${e.source}`;
269
- const existing = seen.get(keyA) ?? seen.get(keyB);
270
- if (!existing) {
271
- seen.set(keyA, e);
272
- }
273
- else if (e.weight > existing.weight) {
274
- // Replace with higher-weight edge
275
- seen.delete(keyA);
276
- seen.delete(keyB);
277
- seen.set(keyA, e);
278
- }
139
+ finally {
140
+ // DatabaseSync has no close in Node 22 -- leave GC to clean up
279
141
  }
280
- return Array.from(seen.values());
142
+ // Pre-gate edge deduplication
143
+ const deduped = deduplicateEdges(edges);
144
+ edges.length = 0;
145
+ edges.push(...deduped);
146
+ return { nodes, edges };
281
147
  }
282
148
  // ---------------------------------------------------------------------------
283
- // Cosine similarity
284
- // ---------------------------------------------------------------------------
285
- function cosineSimilarity(a, b) {
286
- const len = Math.min(a.length, b.length);
287
- if (len === 0)
288
- return 0;
289
- let dot = 0;
290
- let normA = 0;
291
- let normB = 0;
292
- for (let i = 0; i < len; i++) {
293
- dot += a[i] * b[i];
294
- normA += a[i] * a[i];
295
- normB += b[i] * b[i];
149
+ // Helpers
150
+ // ---------------------------------------------------------------------------
151
+ function countSourcesByType(nodes) {
152
+ let checkpoint = 0;
153
+ let turn = 0;
154
+ let turnContent = 0;
155
+ let memory = 0;
156
+ for (const n of nodes) {
157
+ switch (n.nodeType) {
158
+ case "checkpoint":
159
+ checkpoint++;
160
+ break;
161
+ case "turn":
162
+ turn++;
163
+ break;
164
+ case "turn-content":
165
+ turnContent++;
166
+ break;
167
+ case "memory":
168
+ memory++;
169
+ break;
170
+ }
296
171
  }
297
- const denom = Math.sqrt(normA) * Math.sqrt(normB);
298
- return denom === 0 ? 0 : dot / denom;
172
+ return { checkpoint, turn, turnContent, memory };
299
173
  }
@@ -20,6 +20,19 @@ import { defaultEmbedder, cosineSimilarity } from "./embedder.js";
20
20
  import { rehydrateRaptorTree, isShadowMode } from "./dedup/raptor/index.js";
21
21
  import { maxCheckpointTimestamp } from "./store/sqlite.js";
22
22
  import { normalizeSessionId } from "./store.js";
23
+ // ---------------------------------------------------------------------------
24
+ // S57 RAG suite feature flags + module imports. Each flag defaults OFF — the
25
+ // flag-OFF path below is byte-identical to the pre-S57 recall path (recallQuery
26
+ // === opts.query, the single searchRecall call), so the helpers are only reached
27
+ // when their flag is ON. Wiring is additive; the proven v0.11.5 behavior is the
28
+ // default.
29
+ // ---------------------------------------------------------------------------
30
+ import { RAG_QUERY_REFORMULATION, RAG_TIERED_ROUTER, RAG_RECALL_METRICS } from "./config.js";
31
+ import { reformulateQuery, isVagueQuery } from "./queryReformulation.js";
32
+ import { VAGUE_MIN_WORDS, VAGUE_VERY_SHORT_WORDS } from "./queryReformulation/cache.js";
33
+ import { getTieredRouter } from "./tieredRouter.js";
34
+ import { computeRecallMetrics } from "./recallMetrics.js";
35
+ import { Logger } from "./log.js";
23
36
  /** Wrap a recall block so the model reads it as restored compacted context. */
24
37
  export function formatRecallBlock(hits) {
25
38
  if (hits.length === 0)
@@ -72,6 +85,107 @@ export function formatRaptorBlock(nodes) {
72
85
  "session so far. Use it as a map of what has been covered:\n\n" +
73
86
  parts.join("\n"));
74
87
  }
88
+ // ---------------------------------------------------------------------------
89
+ // S57 RAG suite helpers (B1/B2/B3). Each is only called when its feature flag
90
+ // is ON; flag-OFF never reaches them. All degrade non-fatally to the standard
91
+ // path on any error.
92
+ // ---------------------------------------------------------------------------
93
+ /**
94
+ * B1: If the query is vague, reformulate it via embedding-neighbor TF-IDF
95
+ * expansion. Returns the original query when reformulation is not supported
96
+ * or the query is already specific. Non-fatal: any error → original query.
97
+ */
98
+ function reformulateRecallQuery(query, store, sessionId) {
99
+ try {
100
+ if (!isVagueQuery(query, {
101
+ vagueMinWords: VAGUE_MIN_WORDS,
102
+ vagueVeryShortWords: VAGUE_VERY_SHORT_WORDS,
103
+ }))
104
+ return query;
105
+ // Pre-compute neighbor candidates once. reformulateQuery embeds the query
106
+ // and calls `scan` internally; we return these pre-computed neighbors (same
107
+ // query) so the store needs no by-embedding lookup.
108
+ const { hits } = searchRecall({ sessionId, query, limit: 10, skipInjected: false }, store);
109
+ if (hits.length < 2)
110
+ return query; // too few neighbors for TF-IDF
111
+ const neighbors = hits.map((h) => ({
112
+ id: h.checkpoint.checkpointId,
113
+ score: h.score,
114
+ }));
115
+ const textById = new Map();
116
+ for (const h of hits)
117
+ textById.set(h.checkpoint.checkpointId, h.checkpoint.summary);
118
+ const scan = () => neighbors;
119
+ const corpus = {
120
+ totalDocs: neighbors.length,
121
+ docFreq: (term) => {
122
+ let df = 0;
123
+ for (const text of textById.values()) {
124
+ if (text.toLowerCase().includes(term.toLowerCase()))
125
+ df++;
126
+ }
127
+ return Math.max(df, 1);
128
+ },
129
+ };
130
+ const neighborTexts = (ids) => ids.map((id) => ({ id, text: textById.get(id) ?? "" }));
131
+ const { result } = reformulateQuery(query, defaultEmbedder(), scan, corpus, neighborTexts);
132
+ return result.expanded.length > query.length ? result.expanded : query;
133
+ }
134
+ catch {
135
+ return query; // non-fatal: fall back to original query
136
+ }
137
+ }
138
+ /**
139
+ * B2: Synchronous tiered recall. The TieredRouter.route() is async, so the
140
+ * sync path can only peek the L0 in-memory cache; on miss it falls through to
141
+ * the standard sync vector scan. The router's L1/L2 value is realized on the
142
+ * async path (recallAndInlineAsync). Non-fatal.
143
+ */
144
+ function runTieredRecall(query, sessionId, limit, skip, store) {
145
+ try {
146
+ const router = getTieredRouter();
147
+ if (!router) {
148
+ const result = searchRecall({ sessionId, query, limit, skipInjected: skip }, store);
149
+ return { newHits: result.newHits, tier: "off" };
150
+ }
151
+ // Peek the L0 in-memory cache synchronously; fall through on miss.
152
+ const cached = router.peekCache(sessionId, query, limit);
153
+ if (cached && cached.length > 0)
154
+ return { newHits: cached, tier: "L0-cache" };
155
+ const result = searchRecall({ sessionId, query, limit, skipInjected: skip }, store);
156
+ return { newHits: result.newHits, tier: "sync-fallback" };
157
+ }
158
+ catch {
159
+ const result = searchRecall({ sessionId, query, limit, skipInjected: skip }, store);
160
+ return { newHits: result.newHits, tier: "fallback-error" };
161
+ }
162
+ }
163
+ /**
164
+ * B3: Compute recall-quality metrics on the injected hits and log them.
165
+ * Only called when RAG_RECALL_METRICS() is ON. Non-fatal.
166
+ */
167
+ function scoreAndLogRecallMetrics(query, toInject) {
168
+ try {
169
+ const logger = new Logger();
170
+ const metrics = computeRecallMetrics(query, toInject);
171
+ logger.info("recall_metrics", {
172
+ hitCount: toInject.length,
173
+ score: metrics.score,
174
+ pass: metrics.pass,
175
+ });
176
+ if (!metrics.pass && toInject.length > 0) {
177
+ logger.info("recall_metrics_low_quality", {
178
+ score: metrics.score,
179
+ relevance: metrics.breakdown.relevance,
180
+ coverage: metrics.breakdown.coverage,
181
+ diversity: metrics.breakdown.diversity,
182
+ });
183
+ }
184
+ }
185
+ catch {
186
+ /* non-fatal: metrics never break recall */
187
+ }
188
+ }
75
189
  /**
76
190
  * Run the unified recall+dudupe+prepare-inject pipeline. Does NOT touch pi;
77
191
  * it records injections via `markInjected` so the next call dedupes. The
@@ -123,7 +237,22 @@ export function recallAndInline(opts, store) {
123
237
  // F4: thread skipInjected through to searchRecall instead of hardcoding false
124
238
  // and re-implementing the filter here. newHits is already deduped when skip
125
239
  // is true (default); equals hits when skip is false (openclaw command path).
126
- const { newHits } = searchRecall({ sessionId: opts.sessionId, query: opts.query, limit, skipInjected: skip }, store);
240
+ //
241
+ // S57 B1: optionally expand a vague query via TF-IDF neighbor terms.
242
+ // S57 B2: optionally route via the TieredRouter's sync L0 cache peek.
243
+ // Both flags default OFF — recallQuery === opts.query and the single
244
+ // searchRecall call reproduce the pre-S57 byte-identical path.
245
+ const recallQuery = RAG_QUERY_REFORMULATION()
246
+ ? reformulateRecallQuery(opts.query, store, opts.sessionId)
247
+ : opts.query;
248
+ let newHits;
249
+ if (RAG_TIERED_ROUTER()) {
250
+ newHits = runTieredRecall(recallQuery, opts.sessionId, limit, skip, store).newHits;
251
+ }
252
+ else {
253
+ const result = searchRecall({ sessionId: opts.sessionId, query: recallQuery, limit, skipInjected: skip }, store);
254
+ newHits = result.newHits;
255
+ }
127
256
  // F1: hoist one embedder instance for inline dedupe (matches the async path).
128
257
  // defaultEmbedder() is deterministic but creating it per hit wastes allocations.
129
258
  const embedder = defaultEmbedder();
@@ -154,6 +283,12 @@ export function recallAndInline(opts, store) {
154
283
  blockTokens += partTokens;
155
284
  vectorMarkInjected(store, opts.sessionId, h.checkpoint.checkpointId);
156
285
  }
286
+ // S57 B3: optionally score + log recall quality metrics (flag-OFF: skipped,
287
+ // byte-identical). Scores the injected hits against the ORIGINAL query so the
288
+ // metric measures whether the (possibly expanded) search results stay
289
+ // relevant to what the user actually asked.
290
+ if (RAG_RECALL_METRICS())
291
+ scoreAndLogRecallMetrics(opts.query, toInject);
157
292
  // F3: format once — one preamble, correct [1..n] numbering.
158
293
  const recallBlock = toInject.length > 0 ? formatRecallBlock(toInject) : "";
159
294
  const report = toInject.map((h) => ` • ${h.checkpoint.checkpointId} (${h.checkpoint.summary.slice(0, 60).replace(/\n/g, " ")}…)`);