memo-grafter 0.1.1 → 0.2.0

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 (130) hide show
  1. package/.env.example +1 -0
  2. package/README.md +187 -95
  3. package/USER_GUIDE.md +226 -9
  4. package/dist/MemoGrafter.d.ts +1 -1
  5. package/dist/MemoGrafter.d.ts.map +1 -1
  6. package/dist/MemoGrafter.js +16 -5
  7. package/dist/MemoGrafter.js.map +1 -1
  8. package/dist/MemoGrafterAgent.d.ts +7 -1
  9. package/dist/MemoGrafterAgent.d.ts.map +1 -1
  10. package/dist/MemoGrafterAgent.js +45 -8
  11. package/dist/MemoGrafterAgent.js.map +1 -1
  12. package/dist/adapters/AnthropicAdapter.d.ts.map +1 -1
  13. package/dist/adapters/AnthropicAdapter.js +8 -2
  14. package/dist/adapters/AnthropicAdapter.js.map +1 -1
  15. package/dist/adapters/GeminiAdapter.d.ts +15 -0
  16. package/dist/adapters/GeminiAdapter.d.ts.map +1 -0
  17. package/dist/adapters/GeminiAdapter.js +48 -0
  18. package/dist/adapters/GeminiAdapter.js.map +1 -0
  19. package/dist/fleet/FleetStore.d.ts +1 -1
  20. package/dist/fleet/FleetStore.d.ts.map +1 -1
  21. package/dist/index.d.ts +4 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +2 -0
  24. package/dist/index.js.map +1 -1
  25. package/dist/pipeline/GrafterPipeline.d.ts +1 -2
  26. package/dist/pipeline/GrafterPipeline.d.ts.map +1 -1
  27. package/dist/pipeline/GrafterPipeline.js +3 -5
  28. package/dist/pipeline/GrafterPipeline.js.map +1 -1
  29. package/dist/pipeline/IngestPipeline.d.ts +7 -3
  30. package/dist/pipeline/IngestPipeline.d.ts.map +1 -1
  31. package/dist/pipeline/IngestPipeline.js +31 -5
  32. package/dist/pipeline/IngestPipeline.js.map +1 -1
  33. package/dist/pipeline/RetrieverPipeline.d.ts +11 -0
  34. package/dist/pipeline/RetrieverPipeline.d.ts.map +1 -0
  35. package/dist/pipeline/RetrieverPipeline.js +73 -0
  36. package/dist/pipeline/RetrieverPipeline.js.map +1 -0
  37. package/dist/pipeline/SegmentProcessor.d.ts +3 -2
  38. package/dist/pipeline/SegmentProcessor.d.ts.map +1 -1
  39. package/dist/pipeline/SegmentProcessor.js +50 -18
  40. package/dist/pipeline/SegmentProcessor.js.map +1 -1
  41. package/dist/pipeline/TopicDriftDetector.d.ts +21 -3
  42. package/dist/pipeline/TopicDriftDetector.d.ts.map +1 -1
  43. package/dist/pipeline/TopicDriftDetector.js +143 -17
  44. package/dist/pipeline/TopicDriftDetector.js.map +1 -1
  45. package/dist/prompts/factRetrievalPrompt.d.ts +4 -0
  46. package/dist/prompts/factRetrievalPrompt.d.ts.map +1 -0
  47. package/dist/prompts/factRetrievalPrompt.js +25 -0
  48. package/dist/prompts/factRetrievalPrompt.js.map +1 -0
  49. package/dist/prompts/historyCompressionPrompt.d.ts +3 -0
  50. package/dist/prompts/historyCompressionPrompt.d.ts.map +1 -0
  51. package/dist/prompts/historyCompressionPrompt.js +4 -0
  52. package/dist/prompts/historyCompressionPrompt.js.map +1 -0
  53. package/dist/prompts/intentShiftPrompt.d.ts +3 -0
  54. package/dist/prompts/intentShiftPrompt.d.ts.map +1 -0
  55. package/dist/prompts/intentShiftPrompt.js +11 -0
  56. package/dist/prompts/intentShiftPrompt.js.map +1 -0
  57. package/dist/prompts/segmentExtractionPrompt.d.ts.map +1 -1
  58. package/dist/prompts/segmentExtractionPrompt.js +68 -12
  59. package/dist/prompts/segmentExtractionPrompt.js.map +1 -1
  60. package/dist/store/GraphStore.d.ts +16 -19
  61. package/dist/store/GraphStore.d.ts.map +1 -1
  62. package/dist/store/GraphStore.js +1 -576
  63. package/dist/store/GraphStore.js.map +1 -1
  64. package/dist/store/index.d.ts +3 -0
  65. package/dist/store/index.d.ts.map +1 -0
  66. package/dist/store/index.js +2 -0
  67. package/dist/store/index.js.map +1 -0
  68. package/dist/store/postgres-pgvector/GraphStore.d.ts +75 -0
  69. package/dist/store/postgres-pgvector/GraphStore.d.ts.map +1 -0
  70. package/dist/store/postgres-pgvector/GraphStore.js +782 -0
  71. package/dist/store/postgres-pgvector/GraphStore.js.map +1 -0
  72. package/dist/types.d.ts +69 -1
  73. package/dist/types.d.ts.map +1 -1
  74. package/dist/utils/drift/driftMarkers.d.ts +4 -0
  75. package/dist/utils/drift/driftMarkers.d.ts.map +1 -0
  76. package/dist/utils/drift/driftMarkers.js +31 -0
  77. package/dist/utils/drift/driftMarkers.js.map +1 -0
  78. package/dist/utils/drift/driftScore.d.ts +3 -0
  79. package/dist/utils/drift/driftScore.d.ts.map +1 -0
  80. package/dist/utils/drift/driftScore.js +11 -0
  81. package/dist/utils/drift/driftScore.js.map +1 -0
  82. package/dist/utils/drift/driftThreshold.d.ts +7 -0
  83. package/dist/utils/drift/driftThreshold.d.ts.map +1 -0
  84. package/dist/utils/drift/driftThreshold.js +21 -0
  85. package/dist/utils/drift/driftThreshold.js.map +1 -0
  86. package/dist/utils/drift/reentryMatch.d.ts +3 -0
  87. package/dist/utils/drift/reentryMatch.d.ts.map +1 -0
  88. package/dist/utils/drift/reentryMatch.js +10 -0
  89. package/dist/utils/drift/reentryMatch.js.map +1 -0
  90. package/dist/utils/extraction/segmentExtraction.d.ts +5 -0
  91. package/dist/utils/extraction/segmentExtraction.d.ts.map +1 -0
  92. package/dist/utils/extraction/segmentExtraction.js +82 -0
  93. package/dist/utils/extraction/segmentExtraction.js.map +1 -0
  94. package/dist/utils/reentry/reentryCues.d.ts +3 -0
  95. package/dist/utils/reentry/reentryCues.d.ts.map +1 -0
  96. package/dist/utils/reentry/reentryCues.js +14 -0
  97. package/dist/utils/reentry/reentryCues.js.map +1 -0
  98. package/dist/utils/reentry/reentryEdges.d.ts +16 -0
  99. package/dist/utils/reentry/reentryEdges.d.ts.map +1 -0
  100. package/dist/utils/reentry/reentryEdges.js +80 -0
  101. package/dist/utils/reentry/reentryEdges.js.map +1 -0
  102. package/dist/utils/reentry/reentrySimilarity.d.ts +5 -0
  103. package/dist/utils/reentry/reentrySimilarity.d.ts.map +1 -0
  104. package/dist/utils/reentry/reentrySimilarity.js +26 -0
  105. package/dist/utils/reentry/reentrySimilarity.js.map +1 -0
  106. package/dist/utils/reentry/reentryText.d.ts +6 -0
  107. package/dist/utils/reentry/reentryText.d.ts.map +1 -0
  108. package/dist/utils/reentry/reentryText.js +29 -0
  109. package/dist/utils/reentry/reentryText.js.map +1 -0
  110. package/dist/utils/reentry/types.d.ts +6 -0
  111. package/dist/utils/reentry/types.d.ts.map +1 -0
  112. package/dist/utils/reentry/types.js +2 -0
  113. package/dist/utils/reentry/types.js.map +1 -0
  114. package/dist/utils/text/normalizeText.d.ts +2 -0
  115. package/dist/utils/text/normalizeText.d.ts.map +1 -0
  116. package/dist/utils/text/normalizeText.js +5 -0
  117. package/dist/utils/text/normalizeText.js.map +1 -0
  118. package/dist/utils/text/terms.d.ts +3 -0
  119. package/dist/utils/text/terms.d.ts.map +1 -0
  120. package/dist/utils/text/terms.js +51 -0
  121. package/dist/utils/text/terms.js.map +1 -0
  122. package/dist/utils/text/tokenCount.d.ts +2 -0
  123. package/dist/utils/text/tokenCount.d.ts.map +1 -0
  124. package/dist/utils/text/tokenCount.js +4 -0
  125. package/dist/utils/text/tokenCount.js.map +1 -0
  126. package/dist/utils/vector/vectorLiteral.d.ts +3 -0
  127. package/dist/utils/vector/vectorLiteral.d.ts.map +1 -0
  128. package/dist/utils/vector/vectorLiteral.js +19 -0
  129. package/dist/utils/vector/vectorLiteral.js.map +1 -0
  130. package/package.json +11 -3
@@ -0,0 +1,782 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import postgres, {} from "postgres";
3
+ import { cosineSimilarity } from "../../utils/drift/cosineSimilarity.js";
4
+ import { parseVector, toVectorLiteral } from "../../utils/vector/vectorLiteral.js";
5
+ export class PostgresGraphStore {
6
+ sql;
7
+ constructor(connectionString) {
8
+ this.sql = postgres(connectionString, {
9
+ max: 10,
10
+ idle_timeout: 30,
11
+ connect_timeout: 10,
12
+ });
13
+ }
14
+ async initialize() {
15
+ await this.sql `CREATE EXTENSION IF NOT EXISTS vector`;
16
+ await this.sql `CREATE EXTENSION IF NOT EXISTS pgcrypto`;
17
+ await this.sql `
18
+ CREATE TABLE IF NOT EXISTS mg_message_buffer (
19
+ session_id TEXT NOT NULL,
20
+ message_index INT NOT NULL,
21
+ role TEXT NOT NULL,
22
+ content TEXT NOT NULL,
23
+ PRIMARY KEY (session_id, message_index)
24
+ )
25
+ `;
26
+ await this.sql `
27
+ CREATE TABLE IF NOT EXISTS mg_segments (
28
+ id TEXT PRIMARY KEY,
29
+ session_id TEXT NOT NULL,
30
+ start_index INT NOT NULL,
31
+ end_index INT NOT NULL,
32
+ topic_order INT NOT NULL,
33
+ drift_score FLOAT NOT NULL,
34
+ created_at TIMESTAMPTZ DEFAULT NOW(),
35
+ UNIQUE (session_id, start_index, end_index)
36
+ )
37
+ `;
38
+ await this.sql `
39
+ CREATE TABLE IF NOT EXISTS mg_topic_nodes (
40
+ id TEXT PRIMARY KEY,
41
+ session_id TEXT NOT NULL,
42
+ segment_id TEXT NOT NULL REFERENCES mg_segments(id) ON DELETE CASCADE,
43
+ label TEXT,
44
+ summary TEXT,
45
+ embedding vector(1536),
46
+ message_range INT[],
47
+ topic_order INT NOT NULL DEFAULT 0,
48
+ drift_score FLOAT NOT NULL DEFAULT 0,
49
+ agent_color TEXT,
50
+ fleet_id TEXT,
51
+ agent_id TEXT,
52
+ created_at TIMESTAMPTZ DEFAULT NOW(),
53
+ UNIQUE (segment_id)
54
+ )
55
+ `;
56
+ await this.sql `
57
+ CREATE TABLE IF NOT EXISTS mg_topic_edges (
58
+ src_id TEXT NOT NULL,
59
+ dst_id TEXT NOT NULL,
60
+ weight FLOAT NOT NULL,
61
+ type TEXT NOT NULL,
62
+ PRIMARY KEY (src_id, dst_id)
63
+ )
64
+ `;
65
+ await this.sql `
66
+ CREATE TABLE IF NOT EXISTS mg_memory_nodes (
67
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
68
+ segment_id TEXT NOT NULL REFERENCES mg_segments(id) ON DELETE CASCADE,
69
+ topic_node_id TEXT NOT NULL REFERENCES mg_topic_nodes(id) ON DELETE CASCADE,
70
+ agent_id TEXT,
71
+ session_id TEXT NOT NULL,
72
+ memory_type TEXT NOT NULL CHECK (memory_type IN ('fact','insight','question','task','reference')),
73
+ source_type TEXT NOT NULL DEFAULT 'conversation' CHECK (source_type IN ('conversation','note','document','code')),
74
+ subject TEXT NOT NULL,
75
+ predicate TEXT NOT NULL,
76
+ value TEXT NOT NULL,
77
+ confidence FLOAT NOT NULL DEFAULT 1.0,
78
+ embedding vector(1536),
79
+ source_url TEXT,
80
+ source_title TEXT,
81
+ superseded_by UUID REFERENCES mg_memory_nodes(id),
82
+ decayed BOOLEAN NOT NULL DEFAULT FALSE,
83
+ agent_color TEXT,
84
+ fleet_id TEXT,
85
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
86
+ )
87
+ `;
88
+ await this.sql `
89
+ CREATE TABLE IF NOT EXISTS mg_memory_edges (
90
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
91
+ source_id UUID NOT NULL REFERENCES mg_memory_nodes(id) ON DELETE CASCADE,
92
+ target_id UUID NOT NULL REFERENCES mg_memory_nodes(id) ON DELETE CASCADE,
93
+ edge_type TEXT NOT NULL CHECK (edge_type IN ('semantic','conflicts','updates','related')),
94
+ weight FLOAT,
95
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
96
+ )
97
+ `;
98
+ await this.sql `
99
+ CREATE TABLE IF NOT EXISTS mg_fleets (
100
+ id TEXT PRIMARY KEY,
101
+ name TEXT,
102
+ created_at TIMESTAMPTZ DEFAULT NOW()
103
+ )
104
+ `;
105
+ await this.sql `
106
+ CREATE TABLE IF NOT EXISTS mg_fleet_agents (
107
+ id TEXT PRIMARY KEY,
108
+ fleet_id TEXT NOT NULL REFERENCES mg_fleets(id) ON DELETE CASCADE,
109
+ session_id TEXT NOT NULL,
110
+ agent_color TEXT NOT NULL,
111
+ created_at TIMESTAMPTZ DEFAULT NOW(),
112
+ UNIQUE (fleet_id, agent_color)
113
+ )
114
+ `;
115
+ await this.migrateExistingNodeTable();
116
+ await this.createIndexes();
117
+ }
118
+ async saveMessages(sessionId, messages) {
119
+ await this.saveMessagesAt(sessionId, 0, messages);
120
+ }
121
+ async saveMessagesAt(sessionId, startIndex, messages) {
122
+ for (const [index, message] of messages.entries()) {
123
+ await this.sql `
124
+ INSERT INTO mg_message_buffer (session_id, message_index, role, content)
125
+ VALUES (${sessionId}, ${startIndex + index}, ${message.role}, ${message.content})
126
+ ON CONFLICT (session_id, message_index)
127
+ DO UPDATE SET role = EXCLUDED.role, content = EXCLUDED.content
128
+ `;
129
+ }
130
+ }
131
+ async saveSegment(segment) {
132
+ const rows = await this.sql `
133
+ INSERT INTO mg_segments (id, session_id, start_index, end_index, topic_order, drift_score, created_at)
134
+ VALUES (
135
+ ${segment.id},
136
+ ${segment.sessionId},
137
+ ${segment.startIndex},
138
+ ${segment.endIndex},
139
+ ${segment.topicOrder},
140
+ ${segment.driftScore},
141
+ ${segment.createdAt}
142
+ )
143
+ ON CONFLICT (session_id, start_index, end_index)
144
+ DO UPDATE SET
145
+ topic_order = EXCLUDED.topic_order,
146
+ drift_score = EXCLUDED.drift_score
147
+ RETURNING *
148
+ `;
149
+ return this.rowToSegment(rows[0]);
150
+ }
151
+ async saveNode(node) {
152
+ await this.sql `
153
+ INSERT INTO mg_topic_nodes (
154
+ id,
155
+ session_id,
156
+ segment_id,
157
+ label,
158
+ summary,
159
+ embedding,
160
+ message_range,
161
+ topic_order,
162
+ drift_score,
163
+ agent_color,
164
+ fleet_id,
165
+ agent_id,
166
+ created_at
167
+ )
168
+ VALUES (
169
+ ${node.id},
170
+ ${node.sessionId},
171
+ ${node.segmentId},
172
+ ${node.label},
173
+ ${node.summary},
174
+ ${toVectorLiteral(node.embedding)}::vector,
175
+ ${node.messageRange},
176
+ ${node.topicOrder},
177
+ ${node.driftScore},
178
+ ${node.agentColor},
179
+ ${node.fleetId},
180
+ ${node.agentId},
181
+ ${node.createdAt}
182
+ )
183
+ ON CONFLICT (segment_id)
184
+ DO UPDATE SET
185
+ id = EXCLUDED.id,
186
+ session_id = EXCLUDED.session_id,
187
+ label = EXCLUDED.label,
188
+ summary = EXCLUDED.summary,
189
+ embedding = EXCLUDED.embedding,
190
+ message_range = EXCLUDED.message_range,
191
+ topic_order = EXCLUDED.topic_order,
192
+ drift_score = EXCLUDED.drift_score,
193
+ agent_color = EXCLUDED.agent_color,
194
+ fleet_id = EXCLUDED.fleet_id,
195
+ agent_id = EXCLUDED.agent_id,
196
+ created_at = EXCLUDED.created_at
197
+ `;
198
+ }
199
+ async saveEdge(edge) {
200
+ await this.sql `
201
+ INSERT INTO mg_topic_edges (src_id, dst_id, weight, type)
202
+ VALUES (${edge.srcId}, ${edge.dstId}, ${edge.weight}, ${edge.type})
203
+ ON CONFLICT (src_id, dst_id)
204
+ DO UPDATE SET weight = EXCLUDED.weight, type = EXCLUDED.type
205
+ `;
206
+ }
207
+ async getEdgesByType(sessionId, type) {
208
+ const rows = await this.sql `
209
+ SELECT e.*
210
+ FROM mg_topic_edges e
211
+ JOIN mg_topic_nodes n ON n.id = e.src_id
212
+ WHERE n.session_id = ${sessionId}
213
+ AND e.type = ${type}
214
+ `;
215
+ return rows.map((row) => ({
216
+ srcId: row.src_id,
217
+ dstId: row.dst_id,
218
+ weight: row.weight,
219
+ type: row.type,
220
+ }));
221
+ }
222
+ async clearSession(sessionId) {
223
+ const nodeRows = await this.sql `
224
+ SELECT id FROM mg_topic_nodes
225
+ WHERE session_id = ${sessionId}
226
+ `;
227
+ const nodeIds = nodeRows.map((row) => row.id);
228
+ if (nodeIds.length > 0) {
229
+ await this.sql `
230
+ DELETE FROM mg_topic_edges
231
+ WHERE src_id = ANY(${this.sql.array(nodeIds)})
232
+ OR dst_id = ANY(${this.sql.array(nodeIds)})
233
+ `;
234
+ }
235
+ await this.sql `
236
+ DELETE FROM mg_topic_nodes
237
+ WHERE session_id = ${sessionId}
238
+ `;
239
+ await this.sql `
240
+ DELETE FROM mg_segments
241
+ WHERE session_id = ${sessionId}
242
+ `;
243
+ }
244
+ async clearSessionGraph(sessionId) {
245
+ await this.clearSession(sessionId);
246
+ }
247
+ async getTopicNode(topicNodeId, sessionId) {
248
+ const rows = await this.sql `
249
+ SELECT * FROM mg_topic_nodes
250
+ WHERE id = ${topicNodeId}
251
+ ${sessionId ? this.sql `AND session_id = ${sessionId}` : this.sql ``}
252
+ LIMIT 1
253
+ `;
254
+ return rows[0] ? this.rowToNode(rows[0]) : null;
255
+ }
256
+ async getNodeBySegment(segmentId) {
257
+ const rows = await this.sql `
258
+ SELECT * FROM mg_topic_nodes
259
+ WHERE segment_id = ${segmentId}
260
+ LIMIT 1
261
+ `;
262
+ return rows[0] ? this.rowToNode(rows[0]) : null;
263
+ }
264
+ async getNodesBySession(sessionId) {
265
+ const rows = await this.sql `
266
+ SELECT * FROM mg_topic_nodes
267
+ WHERE session_id = ${sessionId}
268
+ ORDER BY topic_order ASC, created_at ASC
269
+ `;
270
+ return rows.map((row) => this.rowToNode(row));
271
+ }
272
+ async getSegmentsBySession(sessionId) {
273
+ const rows = await this.sql `
274
+ SELECT * FROM mg_segments
275
+ WHERE session_id = ${sessionId}
276
+ ORDER BY topic_order ASC, created_at ASC
277
+ `;
278
+ return rows.map((row) => this.rowToSegment(row));
279
+ }
280
+ async insertMemories(nodes) {
281
+ if (nodes.length === 0)
282
+ return;
283
+ const rows = nodes.map((node) => ({
284
+ id: node.id,
285
+ segment_id: node.segmentId,
286
+ topic_node_id: node.topicNodeId,
287
+ agent_id: node.agentId,
288
+ session_id: node.sessionId,
289
+ memory_type: node.memoryType,
290
+ source_type: node.sourceType,
291
+ subject: node.subject,
292
+ predicate: node.predicate,
293
+ value: node.value,
294
+ confidence: node.confidence,
295
+ embedding: toVectorLiteral(node.embedding),
296
+ source_url: node.sourceUrl,
297
+ source_title: node.sourceTitle,
298
+ superseded_by: node.supersededBy,
299
+ decayed: node.decayed,
300
+ agent_color: node.agentColor,
301
+ fleet_id: node.fleetId,
302
+ }));
303
+ await this.sql `
304
+ INSERT INTO mg_memory_nodes ${this.sql(rows, "id", "segment_id", "topic_node_id", "agent_id", "session_id", "memory_type", "source_type", "subject", "predicate", "value", "confidence", "embedding", "source_url", "source_title", "superseded_by", "decayed", "agent_color", "fleet_id")}
305
+ `;
306
+ }
307
+ async getMemoriesBySegment(segmentId) {
308
+ const rows = await this.sql `
309
+ SELECT * FROM mg_memory_nodes
310
+ WHERE segment_id = ${segmentId}
311
+ ORDER BY created_at ASC
312
+ `;
313
+ return rows.map((row) => this.rowToMemoryNode(row));
314
+ }
315
+ async getMemoriesByTopic(topicNodeId) {
316
+ const rows = await this.sql `
317
+ SELECT * FROM mg_memory_nodes
318
+ WHERE topic_node_id = ${topicNodeId}
319
+ AND decayed = false
320
+ AND superseded_by IS NULL
321
+ ORDER BY created_at ASC
322
+ `;
323
+ return rows.map((row) => this.rowToMemoryNode(row));
324
+ }
325
+ async searchMemories(embedding, sessionId, limit, minSimilarity) {
326
+ const rows = await this.sql `
327
+ SELECT *, 1 - (embedding <=> ${toVectorLiteral(embedding)}::vector) AS similarity
328
+ FROM mg_memory_nodes
329
+ WHERE session_id = ${sessionId}
330
+ AND decayed = false
331
+ AND superseded_by IS NULL
332
+ AND 1 - (embedding <=> ${toVectorLiteral(embedding)}::vector) >= ${minSimilarity}
333
+ ORDER BY similarity DESC
334
+ LIMIT ${limit}
335
+ `;
336
+ return rows.map((row) => ({
337
+ ...this.rowToMemoryNode(row),
338
+ similarity: row.similarity,
339
+ }));
340
+ }
341
+ async buildMemoryEdges(topicNodeId, sessionId, threshold) {
342
+ try {
343
+ const memories = (await this.getMemoriesByTopic(topicNodeId))
344
+ .filter((memory) => memory.sessionId === sessionId);
345
+ if (memories.length < 2)
346
+ return;
347
+ for (let sourceIndex = 0; sourceIndex < memories.length; sourceIndex += 1) {
348
+ const source = memories[sourceIndex];
349
+ if (!source)
350
+ continue;
351
+ for (let targetIndex = sourceIndex + 1; targetIndex < memories.length; targetIndex += 1) {
352
+ const target = memories[targetIndex];
353
+ if (!target || source.id === target.id)
354
+ continue;
355
+ const similarity = cosineSimilarity(source.embedding, target.embedding);
356
+ if (!Number.isFinite(similarity) || similarity < threshold)
357
+ continue;
358
+ const existing = await this.sql `
359
+ SELECT id FROM mg_memory_edges
360
+ WHERE (source_id = ${source.id}::uuid AND target_id = ${target.id}::uuid)
361
+ OR (source_id = ${target.id}::uuid AND target_id = ${source.id}::uuid)
362
+ LIMIT 1
363
+ `;
364
+ if (existing.length > 0)
365
+ continue;
366
+ await this.sql `
367
+ INSERT INTO mg_memory_edges (source_id, target_id, edge_type, weight)
368
+ VALUES (${source.id}::uuid, ${target.id}::uuid, 'semantic', ${similarity})
369
+ `;
370
+ }
371
+ }
372
+ }
373
+ catch (error) {
374
+ console.warn("GraphStore memory edge build warning:", error);
375
+ }
376
+ }
377
+ async getTopKSimilar(nodeId, embedding, sessionId, k) {
378
+ return this.getSimilarNodes(embedding, sessionId, { k, excludeNodeId: nodeId });
379
+ }
380
+ async getSimilarNodes(embedding, sessionId, options = {}) {
381
+ const rows = await this.sql `
382
+ SELECT *, 1 - (embedding <=> ${toVectorLiteral(embedding)}::vector) AS similarity
383
+ FROM mg_topic_nodes
384
+ WHERE session_id = ${sessionId}
385
+ ${options.excludeNodeId ? this.sql `AND id != ${options.excludeNodeId}` : this.sql ``}
386
+ ${options.minSimilarity === undefined ? this.sql `` : this.sql `AND 1 - (embedding <=> ${toVectorLiteral(embedding)}::vector) >= ${options.minSimilarity}`}
387
+ ORDER BY embedding <=> ${toVectorLiteral(embedding)}::vector ASC
388
+ LIMIT ${options.k ?? 5}
389
+ `;
390
+ return rows.map((row) => this.rowToNode(row));
391
+ }
392
+ async getSimilarNodesAcrossFleet(fleetId, embedding, options = {}) {
393
+ const rows = await this.sql `
394
+ SELECT *, 1 - (embedding <=> ${toVectorLiteral(embedding)}::vector) AS similarity
395
+ FROM mg_topic_nodes
396
+ WHERE fleet_id = ${fleetId}
397
+ ${options.agentColor ? this.sql `AND agent_color = ${options.agentColor}` : this.sql ``}
398
+ ${options.excludeNodeId ? this.sql `AND id != ${options.excludeNodeId}` : this.sql ``}
399
+ ${options.minSimilarity === undefined ? this.sql `` : this.sql `AND 1 - (embedding <=> ${toVectorLiteral(embedding)}::vector) >= ${options.minSimilarity}`}
400
+ ORDER BY embedding <=> ${toVectorLiteral(embedding)}::vector ASC
401
+ LIMIT ${options.k ?? 5}
402
+ `;
403
+ return rows.map((row) => this.rowToNode(row));
404
+ }
405
+ async getNodesByColor(fleetId, agentColor) {
406
+ const rows = await this.sql `
407
+ SELECT * FROM mg_topic_nodes
408
+ WHERE fleet_id = ${fleetId}
409
+ AND agent_color = ${agentColor}
410
+ ORDER BY topic_order ASC, created_at ASC
411
+ `;
412
+ return rows.map((row) => this.rowToNode(row));
413
+ }
414
+ async saveFleet(fleetId, name) {
415
+ await this.sql `
416
+ INSERT INTO mg_fleets (id, name)
417
+ VALUES (${fleetId}, ${name ?? null})
418
+ ON CONFLICT (id)
419
+ DO UPDATE SET name = COALESCE(EXCLUDED.name, mg_fleets.name)
420
+ `;
421
+ }
422
+ async saveFleetAgent(agent) {
423
+ await this.sql `
424
+ INSERT INTO mg_fleet_agents (id, fleet_id, session_id, agent_color)
425
+ VALUES (${agent.id}, ${agent.fleetId}, ${agent.sessionId}, ${agent.agentColor})
426
+ ON CONFLICT (fleet_id, agent_color)
427
+ DO UPDATE SET
428
+ id = EXCLUDED.id,
429
+ session_id = EXCLUDED.session_id
430
+ `;
431
+ }
432
+ async getFleetAgents(fleetId) {
433
+ const rows = await this.sql `
434
+ SELECT * FROM mg_fleet_agents
435
+ WHERE fleet_id = ${fleetId}
436
+ ORDER BY created_at ASC
437
+ `;
438
+ return rows.map((row) => ({
439
+ id: row.id,
440
+ fleetId: row.fleet_id,
441
+ sessionId: row.session_id,
442
+ agentColor: row.agent_color,
443
+ createdAt: row.created_at,
444
+ }));
445
+ }
446
+ async tagSessionNodes(sessionId, metadata) {
447
+ await this.sql `
448
+ UPDATE mg_topic_nodes
449
+ SET
450
+ fleet_id = ${metadata.fleetId},
451
+ agent_id = ${metadata.agentId},
452
+ agent_color = ${metadata.agentColor}
453
+ WHERE session_id = ${sessionId}
454
+ `;
455
+ }
456
+ async getPreviousNode(sessionId, topicOrder) {
457
+ const rows = await this.sql `
458
+ SELECT *
459
+ FROM mg_topic_nodes
460
+ WHERE session_id = ${sessionId}
461
+ AND topic_order = ${topicOrder - 1}
462
+ LIMIT 1
463
+ `;
464
+ return rows[0] ? this.rowToNode(rows[0]) : null;
465
+ }
466
+ async nodeSimilarity(nodeAId, nodeBId) {
467
+ const rows = await this.sql `
468
+ SELECT 1 - (n1.embedding <=> n2.embedding) AS similarity
469
+ FROM mg_topic_nodes n1, mg_topic_nodes n2
470
+ WHERE n1.id = ${nodeAId}
471
+ AND n2.id = ${nodeBId}
472
+ `;
473
+ return rows[0]?.similarity ?? 0;
474
+ }
475
+ async getNeighbours(nodeIds, hopDepth, sessionId) {
476
+ const visited = new Set(nodeIds);
477
+ let frontier = [...nodeIds];
478
+ for (let hop = 0; hop < hopDepth; hop += 1) {
479
+ if (frontier.length === 0)
480
+ break;
481
+ const edges = await this.sql `
482
+ SELECT * FROM mg_topic_edges
483
+ WHERE src_id = ANY(${this.sql.array(frontier)})
484
+ OR dst_id = ANY(${this.sql.array(frontier)})
485
+ `;
486
+ const nextFrontier = [];
487
+ for (const edge of edges) {
488
+ const candidates = [edge.src_id, edge.dst_id];
489
+ for (const candidate of candidates) {
490
+ if (!visited.has(candidate)) {
491
+ visited.add(candidate);
492
+ nextFrontier.push(candidate);
493
+ }
494
+ }
495
+ }
496
+ frontier = nextFrontier;
497
+ }
498
+ const ids = Array.from(visited);
499
+ if (ids.length === 0)
500
+ return [];
501
+ const rows = await this.sql `
502
+ SELECT * FROM mg_topic_nodes
503
+ WHERE id = ANY(${this.sql.array(ids)})
504
+ ${sessionId ? this.sql `AND session_id = ${sessionId}` : this.sql ``}
505
+ ORDER BY topic_order ASC, created_at ASC
506
+ `;
507
+ return rows.map((row) => this.rowToNode(row));
508
+ }
509
+ async getBufferMessages(sessionId, start, end, maxChars = 600) {
510
+ const rows = await this.sql `
511
+ SELECT * FROM mg_message_buffer
512
+ WHERE session_id = ${sessionId}
513
+ AND message_index >= ${start}
514
+ AND message_index <= ${end}
515
+ ORDER BY message_index ASC
516
+ `;
517
+ return rows.map((row) => ({
518
+ role: row.role,
519
+ content: row.role === "assistant" ? this.cleanAssistantMessage(row.content, maxChars) : row.content,
520
+ }));
521
+ }
522
+ async absorbNodes(nodes, targetSessionId, options = {}) {
523
+ const copiedNodes = [];
524
+ const nextMessageIndex = await this.getNextMessageIndex(targetSessionId);
525
+ const nextTopicOrder = await this.getNextTopicOrder(targetSessionId);
526
+ for (const [index, node] of nodes.entries()) {
527
+ const messageIndex = nextMessageIndex + index;
528
+ await this.saveMessagesAt(targetSessionId, messageIndex, [{
529
+ role: "assistant",
530
+ content: this.formatGraftedMemoryMessage(node),
531
+ }]);
532
+ const segment = await this.saveSegment({
533
+ id: randomUUID(),
534
+ sessionId: targetSessionId,
535
+ startIndex: messageIndex,
536
+ endIndex: messageIndex,
537
+ topicOrder: nextTopicOrder + index,
538
+ driftScore: node.driftScore,
539
+ createdAt: new Date(),
540
+ });
541
+ const copy = {
542
+ ...node,
543
+ id: randomUUID(),
544
+ sessionId: targetSessionId,
545
+ segmentId: segment.id,
546
+ messageRange: [segment.startIndex, segment.endIndex],
547
+ topicOrder: segment.topicOrder,
548
+ driftScore: segment.driftScore,
549
+ agentColor: options.agentColor ?? node.agentColor,
550
+ fleetId: options.fleetId ?? node.fleetId,
551
+ agentId: options.agentId ?? node.agentId,
552
+ createdAt: new Date(),
553
+ };
554
+ await this.saveNode(copy);
555
+ await this.saveEdge({
556
+ srcId: copy.id,
557
+ dstId: node.id,
558
+ weight: 1,
559
+ type: "grafted",
560
+ });
561
+ copiedNodes.push(copy);
562
+ }
563
+ return copiedNodes;
564
+ }
565
+ async rebuildEdgesForSession(sessionId, semanticTopK = 5, semanticThreshold = 0.6) {
566
+ const nodes = await this.getNodesBySession(sessionId);
567
+ const nodeIds = nodes.map((node) => node.id);
568
+ if (nodeIds.length > 0) {
569
+ await this.sql `
570
+ DELETE FROM mg_topic_edges
571
+ WHERE type != 'grafted'
572
+ AND (
573
+ src_id = ANY(${this.sql.array(nodeIds)})
574
+ OR dst_id = ANY(${this.sql.array(nodeIds)})
575
+ )
576
+ `;
577
+ }
578
+ for (let index = 1; index < nodes.length; index += 1) {
579
+ const current = nodes[index];
580
+ const previous = nodes[index - 1];
581
+ if (!current || !previous)
582
+ continue;
583
+ await this.saveEdge({
584
+ srcId: current.id,
585
+ dstId: previous.id,
586
+ weight: await this.nodeSimilarity(current.id, previous.id),
587
+ type: "temporal",
588
+ });
589
+ }
590
+ for (const node of nodes) {
591
+ const similarNodes = await this.getSimilarNodes(node.embedding, sessionId, {
592
+ k: semanticTopK,
593
+ excludeNodeId: node.id,
594
+ minSimilarity: semanticThreshold,
595
+ });
596
+ for (const similarNode of similarNodes) {
597
+ await this.saveEdge({
598
+ srcId: node.id,
599
+ dstId: similarNode.id,
600
+ weight: await this.nodeSimilarity(node.id, similarNode.id),
601
+ type: "semantic",
602
+ });
603
+ }
604
+ }
605
+ }
606
+ async close() {
607
+ await this.sql.end();
608
+ }
609
+ async migrateExistingNodeTable() {
610
+ await this.sql `ALTER TABLE mg_segments DROP COLUMN IF EXISTS node_id`;
611
+ await this.sql `ALTER TABLE mg_topic_nodes ADD COLUMN IF NOT EXISTS segment_id TEXT`;
612
+ await this.sql `ALTER TABLE mg_topic_nodes ADD COLUMN IF NOT EXISTS topic_order INT NOT NULL DEFAULT 0`;
613
+ await this.sql `ALTER TABLE mg_topic_nodes ADD COLUMN IF NOT EXISTS drift_score FLOAT NOT NULL DEFAULT 0`;
614
+ await this.sql `ALTER TABLE mg_topic_nodes ADD COLUMN IF NOT EXISTS agent_color TEXT`;
615
+ await this.sql `ALTER TABLE mg_topic_nodes ADD COLUMN IF NOT EXISTS fleet_id TEXT`;
616
+ await this.sql `ALTER TABLE mg_topic_nodes ADD COLUMN IF NOT EXISTS agent_id TEXT`;
617
+ await this.sql `
618
+ UPDATE mg_topic_nodes n
619
+ SET
620
+ topic_order = s.topic_order,
621
+ drift_score = s.drift_score
622
+ FROM mg_segments s
623
+ WHERE s.id = n.segment_id
624
+ `;
625
+ await this.sql `
626
+ DO $$
627
+ BEGIN
628
+ IF NOT EXISTS (
629
+ SELECT 1
630
+ FROM pg_constraint
631
+ WHERE conname = 'mg_topic_nodes_segment_id_key'
632
+ ) THEN
633
+ ALTER TABLE mg_topic_nodes
634
+ ADD CONSTRAINT mg_topic_nodes_segment_id_key UNIQUE (segment_id);
635
+ END IF;
636
+ END $$;
637
+ `;
638
+ }
639
+ async createIndexes() {
640
+ await this.sql `
641
+ CREATE INDEX IF NOT EXISTS mg_message_buffer_session_idx
642
+ ON mg_message_buffer(session_id, message_index)
643
+ `;
644
+ await this.sql `
645
+ CREATE INDEX IF NOT EXISTS mg_segments_session_idx
646
+ ON mg_segments(session_id, topic_order)
647
+ `;
648
+ await this.sql `
649
+ CREATE INDEX IF NOT EXISTS mg_nodes_session_idx
650
+ ON mg_topic_nodes(session_id, topic_order)
651
+ `;
652
+ await this.sql `
653
+ CREATE INDEX IF NOT EXISTS mg_nodes_fleet_idx
654
+ ON mg_topic_nodes(fleet_id, agent_color)
655
+ `;
656
+ await this.sql `
657
+ CREATE INDEX IF NOT EXISTS mg_fleet_agents_fleet_idx
658
+ ON mg_fleet_agents(fleet_id, agent_color)
659
+ `;
660
+ await this.sql `
661
+ CREATE INDEX IF NOT EXISTS mg_nodes_embedding_idx
662
+ ON mg_topic_nodes
663
+ USING ivfflat (embedding vector_cosine_ops)
664
+ WITH (lists = 100)
665
+ `;
666
+ await this.sql `
667
+ CREATE INDEX IF NOT EXISTS idx_memory_nodes_topic
668
+ ON mg_memory_nodes(topic_node_id)
669
+ `;
670
+ await this.sql `
671
+ CREATE INDEX IF NOT EXISTS idx_memory_nodes_segment
672
+ ON mg_memory_nodes(segment_id)
673
+ `;
674
+ await this.sql `
675
+ CREATE INDEX IF NOT EXISTS idx_memory_nodes_session
676
+ ON mg_memory_nodes(session_id)
677
+ `;
678
+ await this.sql `
679
+ CREATE INDEX IF NOT EXISTS idx_memory_nodes_embedding
680
+ ON mg_memory_nodes
681
+ USING ivfflat (embedding vector_cosine_ops)
682
+ WITH (lists = 100)
683
+ `;
684
+ await this.sql `
685
+ CREATE INDEX IF NOT EXISTS idx_memory_edges_source
686
+ ON mg_memory_edges(source_id)
687
+ `;
688
+ await this.sql `
689
+ CREATE INDEX IF NOT EXISTS idx_memory_edges_target
690
+ ON mg_memory_edges(target_id)
691
+ `;
692
+ }
693
+ cleanAssistantMessage(content, maxChars) {
694
+ const withoutCodeBlocks = content.replace(/```[\s\S]*?```/g, "[code omitted]");
695
+ if (withoutCodeBlocks.length <= maxChars)
696
+ return withoutCodeBlocks;
697
+ const truncated = withoutCodeBlocks.slice(0, maxChars);
698
+ const sentenceEnd = Math.max(truncated.lastIndexOf("."), truncated.lastIndexOf("!"), truncated.lastIndexOf("?"));
699
+ if (sentenceEnd >= Math.floor(maxChars * 0.5)) {
700
+ return `${truncated.slice(0, sentenceEnd + 1)} [truncated]`;
701
+ }
702
+ return `${truncated.trimEnd()} [truncated]`;
703
+ }
704
+ async getNextMessageIndex(sessionId) {
705
+ const rows = await this.sql `
706
+ SELECT COALESCE(MAX(message_index) + 1, 0) AS next_index
707
+ FROM mg_message_buffer
708
+ WHERE session_id = ${sessionId}
709
+ `;
710
+ return rows[0]?.next_index ?? 0;
711
+ }
712
+ async getNextTopicOrder(sessionId) {
713
+ const rows = await this.sql `
714
+ SELECT COALESCE(MAX(topic_order) + 1, 1) AS next_order
715
+ FROM mg_topic_nodes
716
+ WHERE session_id = ${sessionId}
717
+ `;
718
+ return rows[0]?.next_order ?? 1;
719
+ }
720
+ formatGraftedMemoryMessage(node) {
721
+ return [
722
+ `Grafted memory: ${node.label}`,
723
+ node.summary,
724
+ ].filter(Boolean).join("\n");
725
+ }
726
+ rowToNode(row) {
727
+ const [start = 0, end = start] = row.message_range ?? [];
728
+ return {
729
+ id: row.id,
730
+ sessionId: row.session_id,
731
+ segmentId: row.segment_id,
732
+ label: row.label ?? "Untitled topic",
733
+ summary: row.summary ?? "",
734
+ embedding: parseVector(row.embedding),
735
+ messageRange: [start, end],
736
+ topicOrder: row.topic_order ?? 0,
737
+ driftScore: row.drift_score ?? 0,
738
+ agentColor: row.agent_color,
739
+ fleetId: row.fleet_id,
740
+ agentId: row.agent_id,
741
+ createdAt: row.created_at,
742
+ };
743
+ }
744
+ rowToMemoryNode(row) {
745
+ return {
746
+ id: row.id,
747
+ segmentId: row.segment_id,
748
+ topicNodeId: row.topic_node_id,
749
+ agentId: row.agent_id,
750
+ sessionId: row.session_id,
751
+ memoryType: row.memory_type,
752
+ sourceType: row.source_type,
753
+ subject: row.subject,
754
+ predicate: row.predicate,
755
+ value: row.value,
756
+ confidence: row.confidence,
757
+ embedding: parseVector(row.embedding),
758
+ sourceUrl: row.source_url,
759
+ sourceTitle: row.source_title,
760
+ supersededBy: row.superseded_by,
761
+ decayed: row.decayed,
762
+ agentColor: row.agent_color,
763
+ fleetId: row.fleet_id,
764
+ createdAt: row.created_at,
765
+ };
766
+ }
767
+ rowToSegment(row) {
768
+ if (!row) {
769
+ throw new Error("Expected segment row.");
770
+ }
771
+ return {
772
+ id: row.id,
773
+ sessionId: row.session_id,
774
+ startIndex: row.start_index,
775
+ endIndex: row.end_index,
776
+ topicOrder: row.topic_order,
777
+ driftScore: row.drift_score,
778
+ createdAt: row.created_at,
779
+ };
780
+ }
781
+ }
782
+ //# sourceMappingURL=GraphStore.js.map