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.
- package/dist/extensions/dashboard-server/routes-memory-map.js +8 -15
- package/dist/extensions/mega-commands.js +65 -5
- package/dist/extensions/mega-config.js +6 -0
- package/dist/extensions/mega-events/context-handler.js +65 -0
- package/dist/src/config/turns.js +2 -1
- package/dist/src/memoryGraph/embedding.js +50 -0
- package/dist/src/memoryGraph/gates.js +379 -0
- package/dist/src/memoryGraph/sources.js +390 -0
- package/dist/src/memoryGraph.js +133 -259
- package/dist/src/recall.js +136 -1
- package/dist/src/store/sqlite/embedding-cache.js +71 -0
- package/dist/src/store/sqlite.js +1 -0
- package/dist/src/topics/cluster.js +10 -2
- package/extensions/dashboard-client/dist/assets/{AchievementTiles-DR9cLGDL.js → AchievementTiles-BESF1HT1.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AchievementTiles-DR9cLGDL.js.map → AchievementTiles-BESF1HT1.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{AchievementsTab-svNUgJ1e.js → AchievementsTab-8RzNnz8u.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AchievementsTab-svNUgJ1e.js.map → AchievementsTab-8RzNnz8u.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{CacheTab-ASwhOMjR.js → CacheTab-CUIiUgh3.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{CacheTab-ASwhOMjR.js.map → CacheTab-CUIiUgh3.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ConfigTab-CmmWheuW.js → ConfigTab-D85fB9HP.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ConfigTab-CmmWheuW.js.map → ConfigTab-D85fB9HP.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{EventsTab-CFE7jLsa.js → EventsTab-TCLR81G5.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{EventsTab-CFE7jLsa.js.map → EventsTab-TCLR81G5.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{GameTab-CJAX_th_.js → GameTab-BqneNRYH.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{GameTab-CJAX_th_.js.map → GameTab-BqneNRYH.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-P4SUsvMf.js → MaintenanceTab-LblMg3NB.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-P4SUsvMf.js.map → MaintenanceTab-LblMg3NB.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/MemoryMapTab-CpjMQwRr.js +2 -0
- package/extensions/dashboard-client/dist/assets/MemoryMapTab-CpjMQwRr.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{MetricsTab-CHQscixy.js → MetricsTab-LxNtNzLQ.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MetricsTab-CHQscixy.js.map → MetricsTab-LxNtNzLQ.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{OverviewTab-DBs4DJdd.js → OverviewTab-BXKUZbT6.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{OverviewTab-DBs4DJdd.js.map → OverviewTab-BXKUZbT6.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ReposTab-DeFz76L5.js → ReposTab-DTooKeAi.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ReposTab-DeFz76L5.js.map → ReposTab-DTooKeAi.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SessionsTab-BWNsj1OB.js → SessionsTab-Devulgm9.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SessionsTab-BWNsj1OB.js.map → SessionsTab-Devulgm9.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SetupTab-uS6A0xmw.js → SetupTab-By9eqUoE.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SetupTab-uS6A0xmw.js.map → SetupTab-By9eqUoE.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-CiqCOQuL.js → TimeSavedCard-z4M8TDLm.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-CiqCOQuL.js.map → TimeSavedCard-z4M8TDLm.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/TopicsTab-bCGZSfLN.js +2 -0
- package/extensions/dashboard-client/dist/assets/TopicsTab-bCGZSfLN.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{TurnsTab-CVz4f4-o.js → TurnsTab-m_XzE0H4.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TurnsTab-CVz4f4-o.js.map → TurnsTab-m_XzE0H4.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{index-DrktNOoT.js → index-PRkilPXw.js} +3 -3
- package/extensions/dashboard-client/dist/assets/{index-DrktNOoT.js.map → index-PRkilPXw.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{useSSE-Bf5LOLSi.js → useSSE-CGzgc2IK.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{useSSE-Bf5LOLSi.js.map → useSSE-CGzgc2IK.js.map} +1 -1
- package/extensions/dashboard-client/dist/index.html +1 -1
- package/extensions/dashboard-client/src/memory-map-layout.ts +161 -0
- package/extensions/dashboard-client/src/memory-map-shapes.tsx +126 -0
- package/extensions/dashboard-client/src/tabs/MemoryMapTab.tsx +201 -165
- package/extensions/dashboard-client/src/tabs/ReposTab.tsx +21 -0
- package/extensions/dashboard-client/src/tabs/TopicsTab.tsx +1 -1
- package/extensions/dashboard-server/api-contracts/index.ts +2 -1
- package/extensions/dashboard-server/api-contracts/memory-map.ts +14 -6
- package/extensions/dashboard-server/routes-memory-map.ts +21 -29
- package/extensions/mega-commands.ts +63 -6
- package/extensions/mega-config.ts +18 -0
- package/extensions/mega-events/context-handler.ts +80 -0
- package/package.json +1 -1
- package/src/config/turns.ts +5 -2
- package/src/memoryGraph/embedding.ts +63 -0
- package/src/memoryGraph/gates.ts +451 -0
- package/src/memoryGraph/sources.ts +484 -0
- package/src/memoryGraph.ts +188 -350
- package/src/recall.ts +168 -4
- package/src/store/sqlite/embedding-cache.ts +81 -0
- package/src/store/sqlite.ts +1 -0
- package/src/topics/cluster.ts +11 -1
- package/extensions/dashboard-client/dist/assets/MemoryMapTab-BBiH76eF.js +0 -2
- package/extensions/dashboard-client/dist/assets/MemoryMapTab-BBiH76eF.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/TopicsTab-BWWQxltv.js +0 -2
- package/extensions/dashboard-client/dist/assets/TopicsTab-BWWQxltv.js.map +0 -1
package/dist/src/memoryGraph.js
CHANGED
|
@@ -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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
|
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
|
|
16
|
-
import { openStore
|
|
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
|
-
//
|
|
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
|
|
25
|
+
* Build a memory graph from three sources (checkpoints / turns / memories)
|
|
26
|
+
* wired through a 9-gate validation pipeline.
|
|
30
27
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
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(
|
|
36
|
-
const {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
72
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
:
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
//
|
|
73
|
+
// Source orchestrator
|
|
134
74
|
// ---------------------------------------------------------------------------
|
|
135
|
-
function
|
|
136
|
-
const
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
if (
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
//
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
236
|
-
|
|
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
|
-
|
|
249
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
284
|
-
// ---------------------------------------------------------------------------
|
|
285
|
-
function
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
let
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
298
|
-
return denom === 0 ? 0 : dot / denom;
|
|
172
|
+
return { checkpoint, turn, turnContent, memory };
|
|
299
173
|
}
|
package/dist/src/recall.js
CHANGED
|
@@ -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
|
-
|
|
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, " ")}…)`);
|