@prometheus-ai/memory 0.5.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 (128) hide show
  1. package/README.md +107 -0
  2. package/dist/types/cli.d.ts +35 -0
  3. package/dist/types/config.d.ts +77 -0
  4. package/dist/types/core/aaak.d.ts +55 -0
  5. package/dist/types/core/annotations.d.ts +75 -0
  6. package/dist/types/core/banks.d.ts +33 -0
  7. package/dist/types/core/beam/consolidate.d.ts +32 -0
  8. package/dist/types/core/beam/helpers.d.ts +76 -0
  9. package/dist/types/core/beam/index.d.ts +59 -0
  10. package/dist/types/core/beam/recall.d.ts +32 -0
  11. package/dist/types/core/beam/schema.d.ts +2 -0
  12. package/dist/types/core/beam/store.d.ts +35 -0
  13. package/dist/types/core/beam/types.d.ts +233 -0
  14. package/dist/types/core/binary-vectors.d.ts +54 -0
  15. package/dist/types/core/chat-normalize.d.ts +13 -0
  16. package/dist/types/core/content-sanitizer.d.ts +18 -0
  17. package/dist/types/core/cost-log.d.ts +13 -0
  18. package/dist/types/core/embeddings.d.ts +44 -0
  19. package/dist/types/core/entities.d.ts +7 -0
  20. package/dist/types/core/episodic-graph.d.ts +89 -0
  21. package/dist/types/core/extraction/client.d.ts +31 -0
  22. package/dist/types/core/extraction/diagnostics.d.ts +51 -0
  23. package/dist/types/core/extraction/prompts.d.ts +2 -0
  24. package/dist/types/core/extraction.d.ts +6 -0
  25. package/dist/types/core/index.d.ts +4 -0
  26. package/dist/types/core/llm-backends.d.ts +21 -0
  27. package/dist/types/core/local-llm.d.ts +15 -0
  28. package/dist/types/core/memory.d.ts +160 -0
  29. package/dist/types/core/migrations/e6-triplestore-split.d.ts +17 -0
  30. package/dist/types/core/migrations/index.d.ts +1 -0
  31. package/dist/types/core/mmr.d.ts +8 -0
  32. package/dist/types/core/orchestrator.d.ts +20 -0
  33. package/dist/types/core/patterns.d.ts +61 -0
  34. package/dist/types/core/plugins.d.ts +109 -0
  35. package/dist/types/core/polyphonic-recall.d.ts +66 -0
  36. package/dist/types/core/query-cache.d.ts +46 -0
  37. package/dist/types/core/query-intent.d.ts +20 -0
  38. package/dist/types/core/recall-diagnostics.d.ts +48 -0
  39. package/dist/types/core/runtime-options.d.ts +68 -0
  40. package/dist/types/core/shmr.d.ts +56 -0
  41. package/dist/types/core/streaming.d.ts +136 -0
  42. package/dist/types/core/synonyms.d.ts +46 -0
  43. package/dist/types/core/temporal-parser.d.ts +16 -0
  44. package/dist/types/core/token-counter.d.ts +8 -0
  45. package/dist/types/core/triples.d.ts +63 -0
  46. package/dist/types/core/typed-memory.d.ts +39 -0
  47. package/dist/types/core/vector-math.d.ts +1 -0
  48. package/dist/types/core/veracity-consolidation.d.ts +60 -0
  49. package/dist/types/core/weibull.d.ts +96 -0
  50. package/dist/types/db.d.ts +16 -0
  51. package/dist/types/diagnose.d.ts +24 -0
  52. package/dist/types/dr/index.d.ts +1 -0
  53. package/dist/types/dr/recovery.d.ts +68 -0
  54. package/dist/types/index.d.ts +5 -0
  55. package/dist/types/mcp-server.d.ts +40 -0
  56. package/dist/types/mcp-tools.d.ts +484 -0
  57. package/dist/types/migrations/e6-triplestore-split.d.ts +1 -0
  58. package/dist/types/migrations/index.d.ts +1 -0
  59. package/dist/types/types.d.ts +145 -0
  60. package/dist/types/util/datetime.d.ts +8 -0
  61. package/dist/types/util/env.d.ts +10 -0
  62. package/dist/types/util/ids.d.ts +3 -0
  63. package/dist/types/util/lru.d.ts +12 -0
  64. package/dist/types/util/regex.d.ts +10 -0
  65. package/package.json +85 -0
  66. package/src/cli.ts +398 -0
  67. package/src/config.ts +326 -0
  68. package/src/core/aaak.ts +142 -0
  69. package/src/core/annotations.ts +457 -0
  70. package/src/core/banks.ts +133 -0
  71. package/src/core/beam/consolidate.ts +965 -0
  72. package/src/core/beam/helpers.ts +977 -0
  73. package/src/core/beam/index.ts +353 -0
  74. package/src/core/beam/recall.ts +1100 -0
  75. package/src/core/beam/schema.ts +423 -0
  76. package/src/core/beam/store.ts +829 -0
  77. package/src/core/beam/types.ts +268 -0
  78. package/src/core/binary-vectors.ts +317 -0
  79. package/src/core/chat-normalize.ts +160 -0
  80. package/src/core/content-sanitizer.ts +136 -0
  81. package/src/core/cost-log.ts +103 -0
  82. package/src/core/embeddings.ts +423 -0
  83. package/src/core/entities.ts +259 -0
  84. package/src/core/episodic-graph.ts +708 -0
  85. package/src/core/extraction/client.ts +162 -0
  86. package/src/core/extraction/diagnostics.ts +193 -0
  87. package/src/core/extraction/prompts.ts +31 -0
  88. package/src/core/extraction.ts +335 -0
  89. package/src/core/index.ts +30 -0
  90. package/src/core/llm-backends.ts +51 -0
  91. package/src/core/local-llm.ts +436 -0
  92. package/src/core/memory.ts +630 -0
  93. package/src/core/migrations/e6-triplestore-split.ts +211 -0
  94. package/src/core/migrations/index.ts +1 -0
  95. package/src/core/mmr.ts +71 -0
  96. package/src/core/orchestrator.ts +62 -0
  97. package/src/core/patterns.ts +484 -0
  98. package/src/core/plugins.ts +375 -0
  99. package/src/core/polyphonic-recall.ts +563 -0
  100. package/src/core/query-cache.ts +354 -0
  101. package/src/core/query-intent.ts +139 -0
  102. package/src/core/recall-diagnostics.ts +157 -0
  103. package/src/core/runtime-options.ts +119 -0
  104. package/src/core/shmr.ts +460 -0
  105. package/src/core/streaming.ts +419 -0
  106. package/src/core/synonyms.ts +197 -0
  107. package/src/core/temporal-parser.ts +363 -0
  108. package/src/core/token-counter.ts +30 -0
  109. package/src/core/triples.ts +454 -0
  110. package/src/core/typed-memory.ts +407 -0
  111. package/src/core/vector-math.ts +23 -0
  112. package/src/core/veracity-consolidation.ts +477 -0
  113. package/src/core/weibull.ts +124 -0
  114. package/src/db.ts +128 -0
  115. package/src/diagnose.ts +174 -0
  116. package/src/dr/index.ts +1 -0
  117. package/src/dr/recovery.ts +405 -0
  118. package/src/index.ts +33 -0
  119. package/src/mcp-server.ts +155 -0
  120. package/src/mcp-tools.ts +970 -0
  121. package/src/migrations/e6-triplestore-split.ts +1 -0
  122. package/src/migrations/index.ts +1 -0
  123. package/src/types.ts +157 -0
  124. package/src/util/datetime.ts +69 -0
  125. package/src/util/env.ts +65 -0
  126. package/src/util/ids.ts +19 -0
  127. package/src/util/lru.ts +48 -0
  128. package/src/util/regex.ts +165 -0
@@ -0,0 +1,708 @@
1
+ import type { Database } from "bun:sqlite";
2
+ import { closeQuietly, type DatabasePath, openDatabase } from "../db";
3
+
4
+ export interface Gist {
5
+ readonly id: string;
6
+ readonly text: string;
7
+ readonly timestamp: string;
8
+ readonly participants: readonly string[];
9
+ readonly location: string | null;
10
+ readonly emotion: string | null;
11
+ readonly timeScope: string | null;
12
+ }
13
+
14
+ export interface Fact {
15
+ readonly id: string;
16
+ readonly subject: string;
17
+ readonly predicate: string;
18
+ readonly object: string;
19
+ readonly timestamp: string;
20
+ readonly confidence: number;
21
+ readonly temporalQualifier?: string | null;
22
+ }
23
+
24
+ export interface GraphEdge {
25
+ readonly source: string;
26
+ readonly target: string;
27
+ readonly edgeType: string;
28
+ readonly weight: number;
29
+ readonly timestamp: string;
30
+ }
31
+
32
+ export interface RelatedMemory {
33
+ readonly memoryId: string;
34
+ readonly edgeType: string;
35
+ readonly weight: number;
36
+ readonly depth: number;
37
+ }
38
+
39
+ export interface GraphStats {
40
+ readonly gists: number;
41
+ readonly facts: number;
42
+ readonly edges: number;
43
+ readonly totalNodes: number;
44
+ }
45
+
46
+ export interface IngestOptions {
47
+ readonly sessionId?: string;
48
+ readonly linkExisting?: boolean;
49
+ readonly minLinkScore?: number;
50
+ readonly extractEntities?: boolean;
51
+ }
52
+
53
+ export interface IngestResult {
54
+ readonly memoryId: string;
55
+ readonly gist: Gist;
56
+ readonly facts: readonly Fact[];
57
+ readonly edges: readonly GraphEdge[];
58
+ }
59
+
60
+ export interface EpisodicGraphOptions {
61
+ readonly db?: Database;
62
+ readonly dbPath?: DatabasePath;
63
+ }
64
+
65
+ interface CountRow {
66
+ readonly count: number;
67
+ }
68
+
69
+ interface GistRow {
70
+ readonly id: string;
71
+ readonly text: string;
72
+ readonly timestamp: string | null;
73
+ readonly participants_json: string | null;
74
+ readonly location: string | null;
75
+ readonly emotion: string | null;
76
+ readonly time_scope: string | null;
77
+ readonly memory_id: string | null;
78
+ }
79
+
80
+ interface FactRow {
81
+ readonly fact_id: string;
82
+ readonly session_id: string | null;
83
+ readonly subject: string;
84
+ readonly predicate: string;
85
+ readonly object: string;
86
+ readonly timestamp: string | null;
87
+ readonly source_msg_id: string | null;
88
+ readonly confidence: number | null;
89
+ }
90
+
91
+ interface EdgeRow {
92
+ readonly source: string;
93
+ readonly target: string;
94
+ readonly edge_type: string;
95
+ readonly weight: number;
96
+ readonly timestamp: string | null;
97
+ }
98
+
99
+ const EXTRACT_FACTS_MAX_CONTENT_LEN = 4096;
100
+ const MAX_FACTS_PER_MEMORY = 5;
101
+ const DEFAULT_LINK_THRESHOLD = 0.35;
102
+
103
+ function nowIso(): string {
104
+ return new Date().toISOString();
105
+ }
106
+
107
+ function unique(values: Iterable<string>, limit = Number.MAX_SAFE_INTEGER): string[] {
108
+ const seen = new Set<string>();
109
+ const out: string[] = [];
110
+ for (const raw of values) {
111
+ const value = raw.trim();
112
+ if (value.length === 0) continue;
113
+ const key = value.toLocaleLowerCase();
114
+ if (seen.has(key)) continue;
115
+ seen.add(key);
116
+ out.push(value);
117
+ if (out.length >= limit) break;
118
+ }
119
+ return out;
120
+ }
121
+
122
+ function parseJsonStringArray(value: string | null): string[] {
123
+ if (value === null || value === "") return [];
124
+ try {
125
+ const parsed: unknown = JSON.parse(value);
126
+ if (!Array.isArray(parsed)) return [];
127
+ const strings: string[] = [];
128
+ for (const item of parsed) {
129
+ if (typeof item === "string") strings.push(item);
130
+ }
131
+ return strings;
132
+ } catch {
133
+ return [];
134
+ }
135
+ }
136
+
137
+ function rowToGist(row: GistRow): Gist {
138
+ return {
139
+ id: row.id,
140
+ text: row.text,
141
+ timestamp: row.timestamp ?? "",
142
+ participants: parseJsonStringArray(row.participants_json),
143
+ location: row.location,
144
+ emotion: row.emotion,
145
+ timeScope: row.time_scope,
146
+ };
147
+ }
148
+
149
+ function rowToFact(row: FactRow): Fact {
150
+ return {
151
+ id: row.fact_id,
152
+ subject: row.subject,
153
+ predicate: row.predicate,
154
+ object: row.object,
155
+ timestamp: row.timestamp ?? "",
156
+ confidence: row.confidence ?? 0.5,
157
+ temporalQualifier: null,
158
+ };
159
+ }
160
+
161
+ function edgeFromRow(row: EdgeRow): GraphEdge {
162
+ return {
163
+ source: row.source,
164
+ target: row.target,
165
+ edgeType: row.edge_type,
166
+ weight: row.weight,
167
+ timestamp: row.timestamp ?? "",
168
+ };
169
+ }
170
+
171
+ function clampWeight(weight: number): number {
172
+ if (!Number.isFinite(weight)) return 1;
173
+ if (weight < 0) return 0;
174
+ if (weight > 1) return 1;
175
+ return weight;
176
+ }
177
+
178
+ function lowerSet(values: readonly (string | null)[]): Set<string> {
179
+ const out = new Set<string>();
180
+ for (const value of values) {
181
+ if (value === null) continue;
182
+ const normalized = value.trim().toLocaleLowerCase();
183
+ if (normalized.length > 0) out.add(normalized);
184
+ }
185
+ return out;
186
+ }
187
+
188
+ const CONTENT_STOPWORDS = new Set([
189
+ "the",
190
+ "and",
191
+ "for",
192
+ "with",
193
+ "that",
194
+ "this",
195
+ "from",
196
+ "into",
197
+ "onto",
198
+ "about",
199
+ "was",
200
+ "were",
201
+ "are",
202
+ "is",
203
+ "has",
204
+ "have",
205
+ "had",
206
+ "she",
207
+ "he",
208
+ "they",
209
+ "them",
210
+ "their",
211
+ "our",
212
+ "new",
213
+ ]);
214
+
215
+ function contentTokenSet(text: string): Set<string> {
216
+ const out = new Set<string>();
217
+ for (const match of text.toLocaleLowerCase().matchAll(/[\p{L}\p{N}_-]+/gu)) {
218
+ const token = match[0] ?? "";
219
+ if (token.length < 3 || CONTENT_STOPWORDS.has(token)) continue;
220
+ out.add(token);
221
+ }
222
+ return out;
223
+ }
224
+
225
+ function jaccard(left: Set<string>, right: Set<string>): number {
226
+ if (left.size === 0 || right.size === 0) return 0;
227
+ let intersection = 0;
228
+ for (const item of left) {
229
+ if (right.has(item)) intersection++;
230
+ }
231
+ return intersection / (left.size + right.size - intersection);
232
+ }
233
+
234
+ function overlapScore(left: Set<string>, right: Set<string>): number {
235
+ if (left.size === 0 || right.size === 0) return 0;
236
+ let hits = 0;
237
+ for (const item of left) {
238
+ if (right.has(item)) hits++;
239
+ }
240
+ return hits / Math.max(left.size, right.size);
241
+ }
242
+
243
+ export class EpisodicGraph {
244
+ readonly db: Database;
245
+ readonly dbPath: DatabasePath;
246
+ readonly ownsConnection: boolean;
247
+
248
+ constructor(options: EpisodicGraphOptions = {}) {
249
+ this.dbPath = options.dbPath ?? ":memory:";
250
+ this.db = options.db ?? openDatabase(this.dbPath);
251
+ this.ownsConnection = options.db === undefined;
252
+ this.initTables();
253
+ }
254
+
255
+ private initTables(): void {
256
+ this.db.run(`
257
+ CREATE TABLE IF NOT EXISTS gists (
258
+ id TEXT PRIMARY KEY,
259
+ text TEXT NOT NULL,
260
+ timestamp TEXT,
261
+ participants_json TEXT,
262
+ location TEXT,
263
+ emotion TEXT,
264
+ time_scope TEXT,
265
+ memory_id TEXT,
266
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
267
+ )
268
+ `);
269
+ this.db.run(`
270
+ CREATE TABLE IF NOT EXISTS facts (
271
+ fact_id TEXT PRIMARY KEY,
272
+ session_id TEXT DEFAULT 'default',
273
+ subject TEXT NOT NULL,
274
+ predicate TEXT NOT NULL,
275
+ object TEXT NOT NULL,
276
+ timestamp TEXT,
277
+ source_msg_id TEXT,
278
+ confidence REAL DEFAULT 0.5,
279
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
280
+ )
281
+ `);
282
+ this.db.run("CREATE INDEX IF NOT EXISTS idx_facts_subject ON facts(subject)");
283
+ this.db.run("CREATE INDEX IF NOT EXISTS idx_facts_predicate ON facts(predicate)");
284
+ this.db.run("CREATE INDEX IF NOT EXISTS idx_facts_object ON facts(object)");
285
+ this.db.run("CREATE INDEX IF NOT EXISTS idx_facts_source_msg ON facts(source_msg_id)");
286
+ this.db.run(`
287
+ CREATE TABLE IF NOT EXISTS graph_edges (
288
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
289
+ source TEXT NOT NULL,
290
+ target TEXT NOT NULL,
291
+ edge_type TEXT NOT NULL,
292
+ weight REAL DEFAULT 1.0,
293
+ timestamp TEXT,
294
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
295
+ UNIQUE(source, target, edge_type)
296
+ )
297
+ `);
298
+ this.db.run("CREATE INDEX IF NOT EXISTS idx_edges_source ON graph_edges(source)");
299
+ this.db.run("CREATE INDEX IF NOT EXISTS idx_edges_target ON graph_edges(target)");
300
+ this.db.run("CREATE INDEX IF NOT EXISTS idx_edges_type ON graph_edges(edge_type)");
301
+ }
302
+
303
+ extractGist(content: string, memoryId: string): Gist {
304
+ return {
305
+ id: `gist_${memoryId}`,
306
+ text: this.createSummary(content),
307
+ timestamp: nowIso(),
308
+ participants: this.extractParticipants(content),
309
+ location: this.extractLocation(content),
310
+ emotion: this.extractEmotion(content),
311
+ timeScope: this.extractTemporalScope(content),
312
+ };
313
+ }
314
+ extractFacts(content: string, memoryId: string): Fact[] {
315
+ const bounded =
316
+ content.length > EXTRACT_FACTS_MAX_CONTENT_LEN ? content.slice(0, EXTRACT_FACTS_MAX_CONTENT_LEN) : content;
317
+ const facts: Fact[] = [];
318
+ const pushFact = (subject: string, predicate: string, object: string, confidence: number): void => {
319
+ const cleanSubject = subject.trim();
320
+ const cleanObject = object.trim();
321
+ if (cleanSubject.length <= 2 || cleanObject.length <= 2 || facts.length >= MAX_FACTS_PER_MEMORY) return;
322
+ facts.push({
323
+ id: `fact_${memoryId}_${facts.length}`,
324
+ subject: cleanSubject,
325
+ predicate,
326
+ object: cleanObject,
327
+ timestamp: nowIso(),
328
+ confidence,
329
+ temporalQualifier: null,
330
+ });
331
+ };
332
+
333
+ for (const match of bounded.matchAll(/\b([A-Z][a-zA-Z\s]+?)\s+is\s+(?:a|an|the)?\s*([a-zA-Z\s]+?)\b/g)) {
334
+ pushFact(match[1] ?? "", "is", match[2] ?? "", 0.7);
335
+ }
336
+ for (const match of bounded.matchAll(/\b([A-Z][a-zA-Z\s]+?)\s+has\s+(?:a|an|the)?\s*([a-zA-Z\d\s]+?)\b/g)) {
337
+ pushFact(match[1] ?? "", "has", match[2] ?? "", 0.6);
338
+ }
339
+ for (const match of bounded.matchAll(
340
+ /\b([A-Z][a-zA-Z\s]+?)\s+(uses?|using|used)\s+(?:a|an|the)?\s*([a-zA-Z\s]+?)\b/g,
341
+ )) {
342
+ pushFact(match[1] ?? "", "uses", match[3] ?? "", 0.6);
343
+ }
344
+ for (const match of bounded.matchAll(
345
+ /\b([A-Z][a-zA-Z\s]+?)\s+works?\s+(?:at|for|with)\s+([A-Z][a-zA-Z\s]+?)\b/g,
346
+ )) {
347
+ pushFact(match[1] ?? "", "works_at", match[2] ?? "", 0.7);
348
+ }
349
+ return facts;
350
+ }
351
+ storeGist(gist: Gist, memoryId: string): void {
352
+ this.db.run(
353
+ `INSERT OR REPLACE INTO gists
354
+ (id, text, timestamp, participants_json, location, emotion, time_scope, memory_id)
355
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
356
+ [
357
+ gist.id,
358
+ gist.text,
359
+ gist.timestamp,
360
+ JSON.stringify(gist.participants),
361
+ gist.location,
362
+ gist.emotion,
363
+ gist.timeScope,
364
+ memoryId,
365
+ ],
366
+ );
367
+ }
368
+ getGist(id: string): Gist | null {
369
+ const row = this.db.query("SELECT * FROM gists WHERE id = ?").get(id) as GistRow | null;
370
+ return row === null ? null : rowToGist(row);
371
+ }
372
+ storeFact(fact: Fact, memoryId: string, sessionId = "default"): void {
373
+ this.db.run(
374
+ `INSERT OR REPLACE INTO facts
375
+ (fact_id, session_id, subject, predicate, object, timestamp, source_msg_id, confidence)
376
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
377
+ [fact.id, sessionId, fact.subject, fact.predicate, fact.object, fact.timestamp, memoryId, fact.confidence],
378
+ );
379
+ }
380
+ getFact(id: string): Fact | null {
381
+ const row = this.db.query("SELECT * FROM facts WHERE fact_id = ?").get(id) as FactRow | null;
382
+ return row === null ? null : rowToFact(row);
383
+ }
384
+ addEdge(edge: GraphEdge): void {
385
+ this.db.run(
386
+ `INSERT INTO graph_edges (source, target, edge_type, weight, timestamp)
387
+ VALUES (?, ?, ?, ?, ?)
388
+ ON CONFLICT(source, target, edge_type) DO UPDATE SET
389
+ weight = excluded.weight,
390
+ timestamp = excluded.timestamp`,
391
+ [edge.source, edge.target, edge.edgeType, clampWeight(edge.weight), edge.timestamp],
392
+ );
393
+ }
394
+ getEdges(source: string | null = null): GraphEdge[] {
395
+ const rows =
396
+ source === null
397
+ ? (this.db
398
+ .query("SELECT source, target, edge_type, weight, timestamp FROM graph_edges ORDER BY id")
399
+ .all() as EdgeRow[])
400
+ : (this.db
401
+ .query(
402
+ "SELECT source, target, edge_type, weight, timestamp FROM graph_edges WHERE source = ? OR target = ? ORDER BY id",
403
+ )
404
+ .all(source, source) as EdgeRow[]);
405
+ return rows.map(edgeFromRow);
406
+ }
407
+ findRelatedMemories(memoryId: string, depth = 2, edgeType = "", minWeight = 0): RelatedMemory[] {
408
+ const results: RelatedMemory[] = [];
409
+ let currentLevel = new Set([memoryId]);
410
+ const seen = new Set([memoryId]);
411
+ const maxDepth = Math.max(0, Math.trunc(depth));
412
+ const threshold = clampWeight(minWeight);
413
+
414
+ for (let hop = 1; hop <= maxDepth; hop++) {
415
+ const nextLevel = new Set<string>();
416
+ for (const mem of currentLevel) {
417
+ const rows =
418
+ edgeType.length > 0
419
+ ? (this.db
420
+ .query(
421
+ `SELECT source, target, edge_type, weight FROM graph_edges
422
+ WHERE (source = ? OR target = ?) AND edge_type = ? AND weight >= ?
423
+ ORDER BY weight DESC, id`,
424
+ )
425
+ .all(mem, mem, edgeType, threshold) as EdgeRow[])
426
+ : (this.db
427
+ .query(
428
+ `SELECT source, target, edge_type, weight FROM graph_edges
429
+ WHERE (source = ? OR target = ?) AND weight >= ?
430
+ ORDER BY weight DESC, id`,
431
+ )
432
+ .all(mem, mem, threshold) as EdgeRow[]);
433
+ for (const row of rows) {
434
+ const neighbor = row.source === mem ? row.target : row.source;
435
+ if (seen.has(neighbor)) continue;
436
+ seen.add(neighbor);
437
+ nextLevel.add(neighbor);
438
+ results.push({
439
+ memoryId: neighbor,
440
+ edgeType: row.edge_type,
441
+ weight: row.weight,
442
+ depth: hop,
443
+ });
444
+ }
445
+ }
446
+ currentLevel = nextLevel;
447
+ }
448
+ return results;
449
+ }
450
+ findFactsBySubject(subject: string): Fact[] {
451
+ const rows = this.db
452
+ .query("SELECT * FROM facts WHERE subject = ? ORDER BY confidence DESC, timestamp DESC")
453
+ .all(subject) as FactRow[];
454
+ return rows.map(rowToFact);
455
+ }
456
+ findGistsByParticipant(participant: string): Gist[] {
457
+ const rows = this.db
458
+ .query("SELECT * FROM gists WHERE participants_json LIKE ? ORDER BY timestamp DESC")
459
+ .all(`%"${participant}"%`) as GistRow[];
460
+ return rows.map(rowToGist);
461
+ }
462
+ scoreMemoryLink(sourceMemoryId: string, targetMemoryId: string): number {
463
+ const left = this.memoryFeatures(sourceMemoryId);
464
+ const right = this.memoryFeatures(targetMemoryId);
465
+ return this.scoreFeatures(left, right);
466
+ }
467
+ ingestMemory(content: string, memoryId: string, options: IngestOptions = {}): IngestResult {
468
+ const sessionId = options.sessionId ?? "default";
469
+ const linkExisting = options.linkExisting ?? true;
470
+ const minLinkScore = options.minLinkScore ?? DEFAULT_LINK_THRESHOLD;
471
+ const extractEntities = options.extractEntities ?? true;
472
+ const gist = this.extractGist(content, memoryId);
473
+ const facts = extractEntities ? this.extractFacts(content, memoryId) : [];
474
+ const edges: GraphEdge[] = [];
475
+ const timestamp = nowIso();
476
+
477
+ const previousMemoryIds = linkExisting ? this.knownMemoryIds(memoryId) : [];
478
+ this.storeGist(gist, memoryId);
479
+ const gistEdge = { source: memoryId, target: gist.id, edgeType: "ctx", weight: 1, timestamp };
480
+ this.addEdge(gistEdge);
481
+ edges.push(gistEdge);
482
+
483
+ for (const fact of facts) {
484
+ this.storeFact(fact, memoryId, sessionId);
485
+ const edge = {
486
+ source: gist.id,
487
+ target: fact.id,
488
+ edgeType: "rel",
489
+ weight: fact.confidence,
490
+ timestamp,
491
+ };
492
+ this.addEdge(edge);
493
+ edges.push(edge);
494
+ }
495
+
496
+ if (linkExisting) {
497
+ const sourceTokens = contentTokenSet(content);
498
+ for (const otherId of previousMemoryIds) {
499
+ const otherContent = this.memoryContent(otherId);
500
+ const lexicalScore = Math.round(jaccard(sourceTokens, contentTokenSet(otherContent)) * 1000) / 1000;
501
+ let wroteCtxEdge = false;
502
+ if (lexicalScore >= minLinkScore) {
503
+ const edge = {
504
+ source: memoryId,
505
+ target: otherId,
506
+ edgeType: "related_to",
507
+ weight: lexicalScore,
508
+ timestamp,
509
+ };
510
+ this.addEdge(edge);
511
+ edges.push(edge);
512
+ const ctxEdge = {
513
+ source: memoryId,
514
+ target: otherId,
515
+ edgeType: "ctx",
516
+ weight: lexicalScore,
517
+ timestamp,
518
+ };
519
+ this.addEdge(ctxEdge);
520
+ edges.push(ctxEdge);
521
+ wroteCtxEdge = true;
522
+ }
523
+ const entityScore = this.entityOverlapScore(memoryId, otherId);
524
+ if (entityScore > 0) {
525
+ const edge = {
526
+ source: memoryId,
527
+ target: otherId,
528
+ edgeType: "references",
529
+ weight: entityScore,
530
+ timestamp,
531
+ };
532
+ this.addEdge(edge);
533
+ edges.push(edge);
534
+ }
535
+ const contextualScore = Math.max(lexicalScore, entityScore, this.temporalContextScore(memoryId, otherId));
536
+ if (!wroteCtxEdge && contextualScore >= minLinkScore) {
537
+ const ctxEdge = {
538
+ source: memoryId,
539
+ target: otherId,
540
+ edgeType: "ctx",
541
+ weight: contextualScore,
542
+ timestamp,
543
+ };
544
+ this.addEdge(ctxEdge);
545
+ edges.push(ctxEdge);
546
+ }
547
+ }
548
+ }
549
+
550
+ return { memoryId, gist, facts, edges };
551
+ }
552
+ getStats(): GraphStats {
553
+ const gists = this.count("gists");
554
+ const facts = this.count("facts");
555
+ const edges = this.count("graph_edges");
556
+ return { gists, facts, edges, totalNodes: gists + facts };
557
+ }
558
+ close(): void {
559
+ if (this.ownsConnection) closeQuietly(this.db);
560
+ }
561
+
562
+ private count(table: "gists" | "facts" | "graph_edges"): number {
563
+ const row = this.db.query(`SELECT COUNT(*) AS count FROM ${table}`).get() as CountRow;
564
+ return row.count;
565
+ }
566
+
567
+ private extractParticipants(content: string): string[] {
568
+ const names = Array.from(content.matchAll(/\b([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)\b/g), match => match[1] ?? "");
569
+ const pronouns = Array.from(
570
+ content.matchAll(/\b(I|you|we|they|he|she|it|me|us|them|him|her)\b/gi),
571
+ match => match[1] ?? "",
572
+ );
573
+ return unique([...names, ...pronouns], 5);
574
+ }
575
+
576
+ private extractTemporalScope(content: string): string | null {
577
+ const patterns: readonly [RegExp, string][] = [
578
+ [/\b(yesterday|today|tomorrow|now|soon|later|earlier)\b/i, "point_in_time"],
579
+ [/\b(last\s+week|last\s+month|last\s+year|next\s+week)\b/i, "point_in_time"],
580
+ [/\b(since|from|starting)\b.*\b(until|to|through|end)\b/i, "duration"],
581
+ [/\b(between|from)\b.*\b(and|to)\b/i, "range"],
582
+ [/\b\d{1,2}:\d{2}\s*(AM|PM|am|pm)?\b/, "point_in_time"],
583
+ [/\b\d{4}-\d{2}-\d{2}\b/, "point_in_time"],
584
+ ];
585
+ for (const [pattern, scope] of patterns) {
586
+ if (pattern.test(content)) return scope;
587
+ }
588
+ return null;
589
+ }
590
+
591
+ private extractLocation(content: string): string | null {
592
+ const properPlace =
593
+ /\b(?:at|in|from)\s+([A-Z][a-zA-Z\s]+?)(?:\s+(?:yesterday|today|tomorrow|now|last|next|on|at)\b|$)/i.exec(
594
+ content,
595
+ );
596
+ if (properPlace?.[1] !== undefined) return properPlace[1].trim();
597
+ const genericPlace = /\b(office|home|work|school|hospital|store|restaurant|building|room)\b/i.exec(content);
598
+ return genericPlace?.[1] ?? null;
599
+ }
600
+
601
+ private extractEmotion(content: string): string | null {
602
+ const lower = content.toLocaleLowerCase();
603
+ if (
604
+ ["happy", "excited", "great", "awesome", "love", "enjoy", "glad", "pleased"].some(word => lower.includes(word))
605
+ )
606
+ return "positive";
607
+ if (["sad", "angry", "frustrated", "upset", "hate", "disappointed", "worried"].some(word => lower.includes(word)))
608
+ return "negative";
609
+ if (["fine", "okay", "alright", "normal", "standard"].some(word => lower.includes(word))) return "neutral";
610
+ return null;
611
+ }
612
+
613
+ private createSummary(content: string): string {
614
+ const firstSentence = content.split(/[.!?]+/, 1)[0]?.trim() ?? "";
615
+ if (firstSentence.length > 10) return firstSentence.slice(0, 100);
616
+ return content.slice(0, 100).trim();
617
+ }
618
+
619
+ private knownMemoryIds(exclude: string): string[] {
620
+ const ids = new Set<string>();
621
+ const gistRows = this.db
622
+ .query("SELECT DISTINCT memory_id FROM gists WHERE memory_id IS NOT NULL AND memory_id != ?")
623
+ .all(exclude) as { memory_id: string }[];
624
+ for (const row of gistRows) ids.add(row.memory_id);
625
+ try {
626
+ const workingRows = this.db.query("SELECT id FROM working_memory WHERE id != ?").all(exclude) as {
627
+ id: string;
628
+ }[];
629
+ for (const row of workingRows) ids.add(row.id);
630
+ } catch {
631
+ // Standalone graph stores do not have Beam memory tables.
632
+ }
633
+ try {
634
+ const episodicRows = this.db.query("SELECT id FROM episodic_memory WHERE id != ?").all(exclude) as {
635
+ id: string;
636
+ }[];
637
+ for (const row of episodicRows) ids.add(row.id);
638
+ } catch {
639
+ // Standalone graph stores do not have Beam memory tables.
640
+ }
641
+ return [...ids];
642
+ }
643
+
644
+ private memoryContent(memoryId: string): string {
645
+ try {
646
+ const working = this.db.query("SELECT content FROM working_memory WHERE id = ?").get(memoryId) as {
647
+ content: string;
648
+ } | null;
649
+ if (working !== null) return working.content;
650
+ } catch {
651
+ // Standalone EpisodicGraph users may not have Beam tables.
652
+ }
653
+ try {
654
+ const episodic = this.db.query("SELECT content FROM episodic_memory WHERE id = ?").get(memoryId) as {
655
+ content: string;
656
+ } | null;
657
+ if (episodic !== null) return episodic.content;
658
+ } catch {
659
+ // Fall through to graph-local gist text.
660
+ }
661
+ const gist = this.db.query("SELECT text FROM gists WHERE memory_id = ?").get(memoryId) as {
662
+ text: string;
663
+ } | null;
664
+ return gist?.text ?? "";
665
+ }
666
+
667
+ private entityOverlapScore(sourceMemoryId: string, targetMemoryId: string): number {
668
+ const leftRows = this.db
669
+ .query("SELECT subject, object FROM facts WHERE source_msg_id = ?")
670
+ .all(sourceMemoryId) as FactRow[];
671
+ const rightRows = this.db
672
+ .query("SELECT subject, object FROM facts WHERE source_msg_id = ?")
673
+ .all(targetMemoryId) as FactRow[];
674
+ const left = lowerSet(leftRows.flatMap(row => [row.subject, row.object]));
675
+ const right = lowerSet(rightRows.flatMap(row => [row.subject, row.object]));
676
+ return Math.round(overlapScore(left, right) * 1000) / 1000;
677
+ }
678
+
679
+ private temporalContextScore(sourceMemoryId: string, targetMemoryId: string): number {
680
+ const left = this.db.query("SELECT time_scope FROM gists WHERE memory_id = ?").get(sourceMemoryId) as {
681
+ time_scope: string | null;
682
+ } | null;
683
+ if (left?.time_scope === null || left?.time_scope === undefined) return 0;
684
+ const right = this.db.query("SELECT time_scope FROM gists WHERE memory_id = ?").get(targetMemoryId) as {
685
+ time_scope: string | null;
686
+ } | null;
687
+ if (right?.time_scope === null || right?.time_scope === undefined) return 0;
688
+ return left.time_scope === right.time_scope ? DEFAULT_LINK_THRESHOLD : 0;
689
+ }
690
+
691
+ private memoryFeatures(memoryId: string): Set<string> {
692
+ const gistRows = this.db.query("SELECT * FROM gists WHERE memory_id = ?").all(memoryId) as GistRow[];
693
+ const factRows = this.db.query("SELECT * FROM facts WHERE source_msg_id = ?").all(memoryId) as FactRow[];
694
+ const features: (string | null)[] = [];
695
+ for (const row of gistRows) {
696
+ const gist = rowToGist(row);
697
+ features.push(...gist.participants, gist.location, gist.emotion, gist.timeScope);
698
+ }
699
+ for (const row of factRows) {
700
+ features.push(row.subject, row.predicate, row.object);
701
+ }
702
+ return lowerSet(features);
703
+ }
704
+
705
+ private scoreFeatures(left: Set<string>, right: Set<string>): number {
706
+ return Math.round(overlapScore(left, right) * 1000) / 1000;
707
+ }
708
+ }