@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,829 @@
1
+ import type { Database, SQLQueryBindings } from "bun:sqlite";
2
+ import { transaction } from "../../db";
3
+ import { toUtcIso } from "../../util/datetime";
4
+ import { generateId } from "../../util/ids";
5
+ import { EpisodicGraph } from "../episodic-graph";
6
+ import { extractFactsSafe } from "../extraction";
7
+ import { getMnemopiRuntimeOptions, withMnemopiRuntimeOptions } from "../runtime-options";
8
+ import { storeFactStrings } from "./consolidate";
9
+ import { scheduleEmbedding, vecAvailable, vecInsert } from "./helpers";
10
+ import type {
11
+ BeamEvent,
12
+ BeamMemoryState,
13
+ BeamStats,
14
+ ImportStats,
15
+ Metadata,
16
+ RememberBatchItem,
17
+ RememberBatchOptions,
18
+ RememberOptions,
19
+ TrustTier,
20
+ Veracity,
21
+ } from "./types";
22
+
23
+ type Row = Record<string, unknown>;
24
+ type EventPayload = Omit<BeamEvent, "type" | "sessionId" | "timestamp">;
25
+
26
+ type StoreRememberOptions = RememberOptions & {
27
+ memoryId?: string;
28
+ memory_id?: string;
29
+ validUntil?: string | null;
30
+ valid_until?: string | null;
31
+ authorId?: string | null;
32
+ author_id?: string | null;
33
+ authorType?: string | null;
34
+ author_type?: string | null;
35
+ extractEntities?: boolean;
36
+ extract_entities?: boolean;
37
+ channelId?: string | null;
38
+ channel_id?: string | null;
39
+ };
40
+
41
+ type StoreRememberBatchOptions = RememberBatchOptions & {
42
+ forceVeracity?: boolean;
43
+ force_veracity?: boolean;
44
+ };
45
+
46
+ const CANONICAL_VERACITY: Record<string, true> = {
47
+ true: true,
48
+ false: true,
49
+ stated: true,
50
+ inferred: true,
51
+ tool: true,
52
+ imported: true,
53
+ unknown: true,
54
+ };
55
+ const TRUST_TIERS: Record<string, true> = {
56
+ STATED: true,
57
+ DERIVED: true,
58
+ EXTERNAL_WRITE: true,
59
+ IMPORTED: true,
60
+ };
61
+ const SCRATCHPAD_MAX_ITEMS = Number.parseInt(process.env.PROMETHEUS_MEMORY_SP_MAX ?? "1000", 10);
62
+
63
+ function metadataJson(metadata: Metadata | null | undefined): string | null {
64
+ return metadata == null ? null : JSON.stringify(metadata);
65
+ }
66
+
67
+ function jsonObject(value: unknown): Record<string, unknown> {
68
+ return value !== null && typeof value === "object" && !Array.isArray(value)
69
+ ? (value as Record<string, unknown>)
70
+ : {};
71
+ }
72
+
73
+ function isSqlBinding(value: unknown): value is SQLQueryBindings {
74
+ return (
75
+ value === null ||
76
+ typeof value === "string" ||
77
+ typeof value === "number" ||
78
+ typeof value === "bigint" ||
79
+ typeof value === "boolean" ||
80
+ value instanceof ArrayBuffer ||
81
+ (ArrayBuffer.isView(value) && !(value instanceof DataView))
82
+ );
83
+ }
84
+
85
+ function sqlBinding(value: unknown, fallback: SQLQueryBindings): SQLQueryBindings {
86
+ return isSqlBinding(value) ? value : fallback;
87
+ }
88
+
89
+ function clampVeracity(value: unknown): Veracity {
90
+ if (typeof value !== "string") return "unknown";
91
+ const normalized = value.trim().toLowerCase();
92
+ return CANONICAL_VERACITY[normalized] === true ? normalized : "unknown";
93
+ }
94
+
95
+ function sourceToTrustTier(source: string | null | undefined): TrustTier {
96
+ switch ((source ?? "").toLowerCase()) {
97
+ case "conversation":
98
+ case "user":
99
+ case "assistant":
100
+ return "STATED";
101
+ case "tool":
102
+ case "api":
103
+ case "system":
104
+ return "EXTERNAL_WRITE";
105
+ case "import":
106
+ case "imported":
107
+ case "backup":
108
+ return "IMPORTED";
109
+ default:
110
+ return "STATED";
111
+ }
112
+ }
113
+
114
+ function normalizeTrustTier(value: unknown, source: string): TrustTier {
115
+ if (value === null || value === undefined) return sourceToTrustTier(source);
116
+ if (typeof value === "string" && TRUST_TIERS[value] === true) return value;
117
+ return "STATED";
118
+ }
119
+
120
+ function emitEvent(beam: BeamMemoryState, type: string, data: EventPayload): void {
121
+ const event: BeamEvent = {
122
+ ...data,
123
+ type,
124
+ sessionId: beam.sessionId,
125
+ timestamp: toUtcIso(),
126
+ };
127
+ const candidate = beam as BeamMemoryState & {
128
+ emitEvent?: (type: string, data: EventPayload) => void;
129
+ };
130
+ if (typeof candidate.emitEvent === "function") {
131
+ candidate.emitEvent(type, data);
132
+ return;
133
+ }
134
+ beam.eventEmitter?.(event);
135
+ void beam.pluginManager?.emit?.(event);
136
+ }
137
+
138
+ function invalidateCaches(beam: BeamMemoryState): void {
139
+ const cache = beam.caches as {
140
+ queryCache?: { invalidate?: () => void };
141
+ _queryCache?: { invalidate?: () => void };
142
+ };
143
+ cache.queryCache?.invalidate?.();
144
+ cache._queryCache?.invalidate?.();
145
+ }
146
+
147
+ function findDuplicate(beam: BeamMemoryState, content: string): string | null {
148
+ const row = beam.db
149
+ .prepare("SELECT id FROM working_memory WHERE content = ? AND session_id = ? LIMIT 1")
150
+ .get(content, beam.sessionId) as { id: string } | null;
151
+ return row?.id ?? null;
152
+ }
153
+
154
+ function trimWorkingMemory(beam: BeamMemoryState): void {
155
+ const limit = beam.config.workingMemoryLimit;
156
+ if (!Number.isFinite(limit) || limit <= 0) return;
157
+ const ttlHours = beam.config.workingMemoryTtlHours;
158
+ const cutoff = toUtcIso(new Date(Date.now() - ttlHours * 3_600_000));
159
+ beam.db
160
+ .prepare(`
161
+ DELETE FROM working_memory
162
+ WHERE session_id = ?
163
+ AND consolidated_at IS NULL
164
+ AND (
165
+ timestamp < ? OR
166
+ id NOT IN (
167
+ SELECT id FROM working_memory
168
+ WHERE session_id = ? AND consolidated_at IS NULL
169
+ ORDER BY timestamp DESC
170
+ LIMIT ?
171
+ )
172
+ )
173
+ `)
174
+ .run(beam.sessionId, cutoff, beam.sessionId, limit);
175
+ }
176
+
177
+ function addTemporalAnnotations(beam: BeamMemoryState, memoryId: string, timestamp: string, source: string): void {
178
+ try {
179
+ beam.annotations?.add?.(memoryId, "occurred_on", timestamp.slice(0, 10));
180
+ if (source && source !== "conversation" && source !== "user" && source !== "assistant") {
181
+ beam.annotations?.add?.(memoryId, "has_source", source);
182
+ }
183
+ } catch {
184
+ // Annotation enrichment is best-effort, matching Python's non-blocking path.
185
+ }
186
+ }
187
+
188
+ function proactiveLinkIfEnabled(
189
+ beam: BeamMemoryState,
190
+ memoryId: string,
191
+ content: string,
192
+ extractEntities: boolean,
193
+ ): void {
194
+ if (process.env.PROMETHEUS_MEMORY_PROACTIVE_LINKING !== "1") return;
195
+ try {
196
+ const graph =
197
+ beam.episodicGraph instanceof EpisodicGraph
198
+ ? beam.episodicGraph
199
+ : new EpisodicGraph({ db: beam.db, dbPath: beam.dbPath });
200
+ graph.ingestMemory(content, memoryId, {
201
+ sessionId: beam.sessionId,
202
+ linkExisting: true,
203
+ extractEntities,
204
+ });
205
+ } catch {
206
+ // Proactive graph enrichment must never block durable memory storage.
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Run the LLM fact extractor over freshly stored content and persist the
212
+ * resulting facts. Best-effort: failures (no LLM, closed DB, malformed output)
213
+ * are swallowed so they can never disrupt the synchronous `remember` that
214
+ * scheduled them.
215
+ */
216
+ async function runFactExtraction(beam: BeamMemoryState, memoryId: string, content: string): Promise<void> {
217
+ try {
218
+ const facts = await extractFactsSafe(content);
219
+ if (facts.length === 0) return;
220
+ storeFactStrings(beam, facts, 0, memoryId);
221
+ invalidateCaches(beam);
222
+ } catch {
223
+ // Background fact extraction is best-effort and never surfaces to the caller.
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Schedule background fact extraction for a stored memory. `remember` is
229
+ * synchronous, so the async extractor is fired-and-forgotten; the promise is
230
+ * tracked on `beam.pendingExtractions` so callers can drain it via
231
+ * `flushExtractions()` (tests, graceful shutdown). The active runtime options
232
+ * (host LLM `complete`, model, prompt overrides) are captured here and
233
+ * re-entered inside the task because the AsyncLocalStorage scope set by
234
+ * `Mnemopi.#withRuntimeOptions` has already exited by the time the task runs.
235
+ */
236
+ function scheduleFactExtraction(beam: BeamMemoryState, memoryId: string, content: string): void {
237
+ if (content.trim() === "") return;
238
+ const runtimeOptions = getMnemopiRuntimeOptions();
239
+ const task = withMnemopiRuntimeOptions(runtimeOptions, () => runFactExtraction(beam, memoryId, content));
240
+ const pending = beam.pendingExtractions;
241
+ if (pending !== undefined) {
242
+ pending.add(task);
243
+ void task.finally(() => pending.delete(task));
244
+ }
245
+ }
246
+
247
+ function rowToDict(row: Row): Row {
248
+ return { ...row };
249
+ }
250
+
251
+ export function remember(beam: BeamMemoryState, content: string, options: StoreRememberOptions = {}): string {
252
+ const source = options.source ?? "conversation";
253
+ const importance = options.importance ?? 0.5;
254
+ const timestamp = options.timestamp ?? toUtcIso();
255
+ const scope = options.scope ?? "session";
256
+ const veracity = clampVeracity(options.veracity);
257
+ const trustTier = normalizeTrustTier(options.trustTier, source);
258
+ const memoryType = options.memoryType ?? "unknown";
259
+ const validUntil = options.validUntil ?? options.valid_until ?? null;
260
+ const authorId = options.authorId ?? options.author_id ?? beam.authorId;
261
+ const authorType = options.authorType ?? options.author_type ?? beam.authorType;
262
+ const channelId = options.channelId ?? options.channel_id ?? beam.channelId;
263
+ const metadata = options.metadata ?? null;
264
+
265
+ const existingId = findDuplicate(beam, content);
266
+ if (existingId !== null) {
267
+ beam.db
268
+ .prepare(`
269
+ UPDATE working_memory
270
+ SET importance = MAX(importance, ?), timestamp = ?, source = ?,
271
+ valid_until = COALESCE(?, valid_until),
272
+ scope = COALESCE(?, scope),
273
+ author_id = COALESCE(?, author_id),
274
+ author_type = COALESCE(?, author_type),
275
+ channel_id = COALESCE(?, channel_id),
276
+ memory_type = COALESCE(?, memory_type),
277
+ veracity = CASE WHEN ? != 'unknown' THEN ? ELSE veracity END,
278
+ trust_tier = COALESCE(?, trust_tier),
279
+ consolidated_at = NULL
280
+ WHERE id = ? AND session_id = ?
281
+ `)
282
+ .run(
283
+ importance,
284
+ timestamp,
285
+ source,
286
+ validUntil,
287
+ scope,
288
+ authorId,
289
+ authorType,
290
+ channelId,
291
+ memoryType,
292
+ veracity,
293
+ veracity,
294
+ trustTier,
295
+ existingId,
296
+ beam.sessionId,
297
+ );
298
+ emitEvent(beam, "MEMORY_UPDATED", {
299
+ memoryId: existingId,
300
+ content,
301
+ source,
302
+ importance,
303
+ metadata: metadata ?? undefined,
304
+ });
305
+ invalidateCaches(beam);
306
+ return existingId;
307
+ }
308
+
309
+ const memoryId = options.memoryId ?? options.memory_id ?? generateId(content, new Date(timestamp));
310
+ beam.db
311
+ .prepare(`
312
+ INSERT INTO working_memory
313
+ (id, content, source, timestamp, session_id, importance, metadata_json, valid_until, scope,
314
+ author_id, author_type, channel_id, veracity, memory_type, trust_tier)
315
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
316
+ `)
317
+ .run(
318
+ memoryId,
319
+ content,
320
+ source,
321
+ timestamp,
322
+ beam.sessionId,
323
+ importance,
324
+ metadataJson(metadata),
325
+ validUntil,
326
+ scope,
327
+ authorId,
328
+ authorType,
329
+ channelId,
330
+ veracity,
331
+ memoryType,
332
+ trustTier,
333
+ );
334
+ addTemporalAnnotations(beam, memoryId, timestamp, source);
335
+ proactiveLinkIfEnabled(beam, memoryId, content, Boolean(options.extractEntities ?? options.extract_entities));
336
+ trimWorkingMemory(beam);
337
+ emitEvent(beam, "MEMORY_ADDED", {
338
+ memoryId,
339
+ content,
340
+ source,
341
+ importance,
342
+ metadata: metadata ?? undefined,
343
+ });
344
+ scheduleEmbedding(beam, [{ memoryId, content }]);
345
+ if (options.extract === true) scheduleFactExtraction(beam, memoryId, content);
346
+ invalidateCaches(beam);
347
+ return memoryId;
348
+ }
349
+
350
+ export function rememberBatch(
351
+ beam: BeamMemoryState,
352
+ items: readonly RememberBatchItem[],
353
+ options: StoreRememberBatchOptions = {},
354
+ ): string[] {
355
+ const timestamp = toUtcIso();
356
+ const ids: string[] = [];
357
+ const forceVeracity = options.forceVeracity ?? options.force_veracity ?? false;
358
+ const defaultVeracity = clampVeracity(options.veracity);
359
+ const defaultScope = options.scope ?? "session";
360
+ const trustTier = normalizeTrustTier(options.trustTier ?? "IMPORTED", "imported");
361
+
362
+ transaction(beam.db, () => {
363
+ const statement = beam.db.prepare(`
364
+ INSERT INTO working_memory
365
+ (id, content, source, timestamp, session_id, importance, metadata_json,
366
+ author_id, author_type, channel_id, memory_type, veracity, trust_tier, scope)
367
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
368
+ `);
369
+ for (const item of items) {
370
+ const itemTimestamp = item.timestamp ?? timestamp;
371
+ const memoryId = generateId(item.content, new Date(itemTimestamp));
372
+ ids.push(memoryId);
373
+ const source = item.source ?? "conversation";
374
+ const storeItem = item as StoreRememberOptions;
375
+ const itemVeracity = forceVeracity
376
+ ? defaultVeracity
377
+ : item.veracity !== undefined
378
+ ? clampVeracity(item.veracity)
379
+ : defaultVeracity;
380
+ statement.run(
381
+ memoryId,
382
+ item.content,
383
+ source,
384
+ itemTimestamp,
385
+ beam.sessionId,
386
+ item.importance ?? 0.5,
387
+ metadataJson(item.metadata ?? null),
388
+ storeItem.authorId ?? storeItem.author_id ?? beam.authorId,
389
+ storeItem.authorType ?? storeItem.author_type ?? beam.authorType,
390
+ storeItem.channelId ?? storeItem.channel_id ?? beam.channelId,
391
+ item.memoryType ?? options.memoryType ?? "unknown",
392
+ itemVeracity,
393
+ trustTier,
394
+ item.scope ?? defaultScope,
395
+ );
396
+ addTemporalAnnotations(beam, memoryId, itemTimestamp, source);
397
+ emitEvent(beam, "MEMORY_ADDED", {
398
+ memoryId,
399
+ content: item.content,
400
+ source,
401
+ importance: item.importance ?? 0.5,
402
+ metadata: item.metadata ?? undefined,
403
+ });
404
+ }
405
+ trimWorkingMemory(beam);
406
+ });
407
+ invalidateCaches(beam);
408
+ const embeddingItems: { memoryId: string; content: string }[] = [];
409
+ items.forEach((item, index) => {
410
+ const id = ids[index];
411
+ if (id === undefined) return;
412
+ embeddingItems.push({ memoryId: id, content: item.content });
413
+ });
414
+ scheduleEmbedding(beam, embeddingItems);
415
+ items.forEach((item, index) => {
416
+ const id = ids[index];
417
+ if (id !== undefined && (item.extract === true || options.extract === true)) {
418
+ scheduleFactExtraction(beam, id, item.content);
419
+ }
420
+ });
421
+ return ids;
422
+ }
423
+
424
+ export function getContext(beam: BeamMemoryState, limit = 10): Row[] {
425
+ const now = toUtcIso();
426
+ return (
427
+ beam.db
428
+ .prepare(`
429
+ SELECT id, content, source, timestamp, importance, scope
430
+ FROM working_memory
431
+ WHERE (session_id = ? OR scope = 'global')
432
+ AND (valid_until IS NULL OR valid_until > ?)
433
+ AND superseded_by IS NULL
434
+ ORDER BY
435
+ CASE WHEN scope = 'global' THEN 0 ELSE 1 END,
436
+ importance DESC,
437
+ timestamp DESC
438
+ LIMIT ?
439
+ `)
440
+ .all(beam.sessionId, now, limit) as Row[]
441
+ ).map(rowToDict);
442
+ }
443
+
444
+ export function invalidate(beam: BeamMemoryState, memoryId: string, replacementId: string | null = null): boolean {
445
+ const now = toUtcIso();
446
+ const working = beam.db
447
+ .prepare(`
448
+ UPDATE working_memory
449
+ SET valid_until = ?, superseded_by = ?
450
+ WHERE id = ? AND (session_id = ? OR scope = 'global')
451
+ `)
452
+ .run(now, replacementId, memoryId, beam.sessionId);
453
+ if (working.changes > 0) return true;
454
+ const episodic = beam.db
455
+ .prepare(`
456
+ UPDATE episodic_memory
457
+ SET valid_until = ?, superseded_by = ?
458
+ WHERE id = ? AND (session_id = ? OR scope = 'global')
459
+ `)
460
+ .run(now, replacementId, memoryId, beam.sessionId);
461
+ return episodic.changes > 0;
462
+ }
463
+
464
+ export function getWorkingStats(
465
+ beam: BeamMemoryState,
466
+ authorId: string | null = null,
467
+ authorType: string | null = null,
468
+ channelId: string | null = null,
469
+ ): BeamStats {
470
+ const clauses: string[] = [];
471
+ const params: SQLQueryBindings[] = [];
472
+ if (authorId) {
473
+ clauses.push("author_id = ?");
474
+ params.push(authorId);
475
+ }
476
+ if (authorType) {
477
+ clauses.push("author_type = ?");
478
+ params.push(authorType);
479
+ }
480
+ if (channelId) {
481
+ clauses.push("channel_id = ?");
482
+ params.push(channelId);
483
+ }
484
+ const where = clauses.length === 0 ? "" : ` WHERE ${clauses.join(" AND ")}`;
485
+ const total = beam.db.prepare(`SELECT COUNT(*) AS total FROM working_memory${where}`).get(...params) as {
486
+ total: number;
487
+ };
488
+ const last = beam.db
489
+ .prepare(`SELECT timestamp FROM working_memory${where} ORDER BY timestamp DESC LIMIT 1`)
490
+ .get(...params) as { timestamp: string | null } | null;
491
+ return { total: total.total, count: total.total, last: last?.timestamp ?? null };
492
+ }
493
+
494
+ export function getGlobalWorkingStats(beam: BeamMemoryState): BeamStats {
495
+ return getWorkingStats(beam);
496
+ }
497
+
498
+ export function updateWorking(
499
+ beam: BeamMemoryState,
500
+ memoryId: string,
501
+ content: string | null = null,
502
+ importance: number | null = null,
503
+ ): boolean {
504
+ const assignments: string[] = [];
505
+ const params: SQLQueryBindings[] = [];
506
+ if (content !== null) {
507
+ assignments.push("content = ?");
508
+ params.push(content);
509
+ }
510
+ if (importance !== null) {
511
+ assignments.push("importance = ?");
512
+ params.push(importance);
513
+ }
514
+ if (assignments.length === 0) return false;
515
+ params.push(memoryId, beam.sessionId);
516
+ const result = beam.db
517
+ .prepare(`UPDATE working_memory SET ${assignments.join(", ")} WHERE id = ? AND session_id = ?`)
518
+ .run(...params);
519
+ if (result.changes > 0) {
520
+ invalidateCaches(beam);
521
+ if (content !== null) scheduleEmbedding(beam, [{ memoryId, content }]);
522
+ }
523
+ return result.changes > 0;
524
+ }
525
+
526
+ export function get(beam: BeamMemoryState, memoryId: string): Row | null {
527
+ const working = beam.db
528
+ .prepare(`
529
+ SELECT id, content, source, timestamp, session_id,
530
+ importance, metadata_json, veracity, created_at
531
+ FROM working_memory
532
+ WHERE id = ?
533
+ `)
534
+ .get(memoryId) as Row | null | undefined;
535
+ if (working != null) return { ...working, metadata: working.metadata_json, memory_store: "working" };
536
+
537
+ const episodic = beam.db
538
+ .prepare(`
539
+ SELECT id, content, source, timestamp, session_id,
540
+ importance, metadata_json, veracity, created_at
541
+ FROM episodic_memory
542
+ WHERE id = ? AND (session_id = ? OR scope = 'global')
543
+ `)
544
+ .get(memoryId, beam.sessionId) as Row | null | undefined;
545
+ return episodic == null ? null : { ...episodic, metadata: episodic.metadata_json, memory_store: "episodic" };
546
+ }
547
+
548
+ export function forgetWorking(beam: BeamMemoryState, memoryId: string): boolean {
549
+ let deleted = 0;
550
+ transaction(beam.db, () => {
551
+ const result = beam.db
552
+ .prepare("DELETE FROM working_memory WHERE id = ? AND session_id = ?")
553
+ .run(memoryId, beam.sessionId);
554
+ deleted = result.changes;
555
+ if (deleted > 0) {
556
+ beam.db.prepare("DELETE FROM annotations WHERE memory_id = ?").run(memoryId);
557
+ }
558
+ });
559
+ if (deleted > 0) invalidateCaches(beam);
560
+ return deleted > 0;
561
+ }
562
+
563
+ export function scratchpadWrite(beam: BeamMemoryState, content: string): string {
564
+ const padId = generateId(content);
565
+ const timestamp = toUtcIso();
566
+ beam.db
567
+ .prepare(`
568
+ INSERT INTO scratchpad (id, content, session_id, created_at, updated_at)
569
+ VALUES (?, ?, ?, ?, ?)
570
+ ON CONFLICT(id) DO UPDATE SET content = excluded.content, updated_at = excluded.updated_at
571
+ `)
572
+ .run(padId, content, beam.sessionId, timestamp, timestamp);
573
+ return padId;
574
+ }
575
+
576
+ export function scratchpadRead(beam: BeamMemoryState): Row[] {
577
+ return (
578
+ beam.db
579
+ .prepare(`
580
+ SELECT id, content, created_at, updated_at
581
+ FROM scratchpad
582
+ WHERE session_id = ?
583
+ ORDER BY updated_at DESC
584
+ LIMIT ?
585
+ `)
586
+ .all(beam.sessionId, Number.isFinite(SCRATCHPAD_MAX_ITEMS) ? SCRATCHPAD_MAX_ITEMS : 1000) as Row[]
587
+ ).map(rowToDict);
588
+ }
589
+
590
+ export function scratchpadClear(beam: BeamMemoryState): void {
591
+ beam.db.prepare("DELETE FROM scratchpad WHERE session_id = ?").run(beam.sessionId);
592
+ }
593
+
594
+ export function exportToDict(beam: BeamMemoryState): Record<string, unknown> {
595
+ const db = beam.db;
596
+ return {
597
+ prometheus_memory_export: {
598
+ version: "1.0",
599
+ export_date: toUtcIso(),
600
+ source_db: beam.dbPath ?? ":memory:",
601
+ component: "beam",
602
+ },
603
+ working_memory: db
604
+ .prepare(`
605
+ SELECT id, content, source, timestamp, session_id, importance,
606
+ metadata_json, valid_until, superseded_by, scope,
607
+ recall_count, last_recalled, created_at, veracity, consolidated_at,
608
+ memory_type, author_id, author_type, channel_id, trust_tier,
609
+ event_date, event_date_precision, temporal_tags
610
+ FROM working_memory
611
+ ORDER BY session_id, timestamp
612
+ `)
613
+ .all(),
614
+ episodic_memory: db
615
+ .prepare(`
616
+ SELECT rowid, id, content, source, timestamp, session_id, importance,
617
+ metadata_json, summary_of, valid_until, superseded_by, scope,
618
+ recall_count, last_recalled, created_at, veracity, memory_type,
619
+ author_id, author_type, channel_id, trust_tier,
620
+ event_date, event_date_precision, temporal_tags
621
+ FROM episodic_memory
622
+ ORDER BY session_id, timestamp
623
+ `)
624
+ .all(),
625
+ episodic_embeddings: [],
626
+ scratchpad: db
627
+ .prepare(`
628
+ SELECT id, content, session_id, created_at, updated_at
629
+ FROM scratchpad
630
+ ORDER BY session_id, updated_at
631
+ `)
632
+ .all(),
633
+ consolidation_log: db
634
+ .prepare(`
635
+ SELECT id, session_id, items_consolidated, summary_preview, created_at
636
+ FROM consolidation_log
637
+ ORDER BY session_id, created_at
638
+ `)
639
+ .all(),
640
+ };
641
+ }
642
+
643
+ export function importFromDict(beam: BeamMemoryState, data: Record<string, unknown>, force = false): ImportStats {
644
+ const stats = {
645
+ working_memory: { inserted: 0, skipped: 0, overwritten: 0 },
646
+ episodic_memory: { inserted: 0, skipped: 0, overwritten: 0, embeddings_inserted: 0 },
647
+ scratchpad: { inserted: 0, updated: 0 },
648
+ consolidation_log: { inserted: 0 },
649
+ } satisfies ImportStats;
650
+ const db: Database = beam.db;
651
+ const oldToNewRowid = new Map<number, number>();
652
+
653
+ transaction(db, () => {
654
+ for (const raw of Array.isArray(data.working_memory) ? data.working_memory : []) {
655
+ const item = jsonObject(raw);
656
+ const id = String(item.id ?? "");
657
+ if (id.length === 0) continue;
658
+ const exists = db.prepare("SELECT 1 FROM working_memory WHERE id = ?").get(id) !== null;
659
+ if (exists && !force) {
660
+ stats.working_memory.skipped++;
661
+ continue;
662
+ }
663
+ if (exists) {
664
+ db.prepare("DELETE FROM working_memory WHERE id = ?").run(id);
665
+ stats.working_memory.overwritten++;
666
+ } else {
667
+ stats.working_memory.inserted++;
668
+ }
669
+ db.prepare(`
670
+ INSERT INTO working_memory
671
+ (id, content, source, timestamp, session_id, importance, metadata_json,
672
+ valid_until, superseded_by, scope, recall_count, last_recalled, created_at,
673
+ veracity, consolidated_at, memory_type, author_id, author_type, channel_id,
674
+ trust_tier, event_date, event_date_precision, temporal_tags)
675
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
676
+ `).run(
677
+ id,
678
+ sqlBinding(item.content, ""),
679
+ sqlBinding(item.source, null),
680
+ sqlBinding(item.timestamp, null),
681
+ sqlBinding(item.session_id, "default"),
682
+ sqlBinding(item.importance, 0.5),
683
+ sqlBinding(item.metadata_json, "{}"),
684
+ sqlBinding(item.valid_until, null),
685
+ sqlBinding(item.superseded_by, null),
686
+ sqlBinding(item.scope, "session"),
687
+ sqlBinding(item.recall_count, 0),
688
+ sqlBinding(item.last_recalled, null),
689
+ sqlBinding(item.created_at, null),
690
+ clampVeracity(item.veracity),
691
+ sqlBinding(item.consolidated_at, null),
692
+ sqlBinding(item.memory_type, "unknown"),
693
+ sqlBinding(item.author_id, null),
694
+ sqlBinding(item.author_type, null),
695
+ sqlBinding(item.channel_id, null),
696
+ sqlBinding(item.trust_tier, "STATED"),
697
+ sqlBinding(item.event_date, null),
698
+ sqlBinding(item.event_date_precision, "unknown"),
699
+ sqlBinding(item.temporal_tags, "[]"),
700
+ );
701
+ }
702
+
703
+ for (const raw of Array.isArray(data.episodic_memory) ? data.episodic_memory : []) {
704
+ const item = jsonObject(raw);
705
+ const id = String(item.id ?? "");
706
+ if (id.length === 0) continue;
707
+ const exists = db.prepare("SELECT 1 FROM episodic_memory WHERE id = ?").get(id) !== null;
708
+ if (exists && !force) {
709
+ stats.episodic_memory.skipped++;
710
+ continue;
711
+ }
712
+ if (exists) {
713
+ const existingRow = db.prepare("SELECT rowid FROM episodic_memory WHERE id = ?").get(id) as {
714
+ rowid: number;
715
+ } | null;
716
+ if (existingRow !== null && vecAvailable(db)) {
717
+ try {
718
+ db.prepare("DELETE FROM vec_episodes WHERE rowid = ?").run(existingRow.rowid);
719
+ } catch {
720
+ // sqlite-vec cleanup is best-effort; import correctness takes precedence.
721
+ }
722
+ }
723
+ db.prepare("DELETE FROM episodic_memory WHERE id = ?").run(id);
724
+ stats.episodic_memory.overwritten++;
725
+ } else {
726
+ stats.episodic_memory.inserted++;
727
+ }
728
+ db.prepare(`
729
+ INSERT INTO episodic_memory
730
+ (id, content, source, timestamp, session_id, importance, metadata_json,
731
+ summary_of, valid_until, superseded_by, scope, recall_count, last_recalled, created_at,
732
+ veracity, memory_type, author_id, author_type, channel_id, trust_tier,
733
+ event_date, event_date_precision, temporal_tags)
734
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
735
+ `).run(
736
+ id,
737
+ sqlBinding(item.content, ""),
738
+ sqlBinding(item.source, null),
739
+ sqlBinding(item.timestamp, null),
740
+ sqlBinding(item.session_id, "default"),
741
+ sqlBinding(item.importance, 0.5),
742
+ sqlBinding(item.metadata_json, "{}"),
743
+ sqlBinding(item.summary_of, ""),
744
+ sqlBinding(item.valid_until, null),
745
+ sqlBinding(item.superseded_by, null),
746
+ sqlBinding(item.scope, "session"),
747
+ sqlBinding(item.recall_count, 0),
748
+ sqlBinding(item.last_recalled, null),
749
+ sqlBinding(item.created_at, null),
750
+ clampVeracity(item.veracity),
751
+ sqlBinding(item.memory_type, "unknown"),
752
+ sqlBinding(item.author_id, null),
753
+ sqlBinding(item.author_type, null),
754
+ sqlBinding(item.channel_id, null),
755
+ sqlBinding(item.trust_tier, "STATED"),
756
+ sqlBinding(item.event_date, null),
757
+ sqlBinding(item.event_date_precision, "unknown"),
758
+ sqlBinding(item.temporal_tags, "[]"),
759
+ );
760
+ const oldRowid = Number(item.rowid);
761
+ const newRow = db.prepare("SELECT rowid FROM episodic_memory WHERE id = ?").get(id) as {
762
+ rowid: number;
763
+ } | null;
764
+ if (Number.isFinite(oldRowid) && newRow !== null) oldToNewRowid.set(oldRowid, newRow.rowid);
765
+ }
766
+
767
+ for (const raw of Array.isArray(data.episodic_embeddings) ? data.episodic_embeddings : []) {
768
+ const item = jsonObject(raw);
769
+ const oldRowid = Number(item.rowid);
770
+ const mappedRowid = oldToNewRowid.get(oldRowid);
771
+ const embedding = Array.isArray(item.embedding) ? item.embedding.map(value => Number(value)) : null;
772
+ if (mappedRowid === undefined || embedding === null || embedding.some(v => !Number.isFinite(v))) {
773
+ continue;
774
+ }
775
+ if (!vecAvailable(db)) continue;
776
+ try {
777
+ vecInsert(db, mappedRowid, embedding);
778
+ stats.episodic_memory.embeddings_inserted++;
779
+ } catch {
780
+ // Embedding import is best-effort when sqlite-vec is unavailable or degraded.
781
+ }
782
+ }
783
+
784
+ for (const raw of Array.isArray(data.scratchpad) ? data.scratchpad : []) {
785
+ const item = jsonObject(raw);
786
+ const id = String(item.id ?? "");
787
+ if (id.length === 0) continue;
788
+ const exists = db.prepare("SELECT 1 FROM scratchpad WHERE id = ?").get(id) !== null;
789
+ if (exists) {
790
+ db.prepare(
791
+ "UPDATE scratchpad SET content = ?, session_id = ?, created_at = ?, updated_at = ? WHERE id = ?",
792
+ ).run(
793
+ sqlBinding(item.content, ""),
794
+ sqlBinding(item.session_id, "default"),
795
+ sqlBinding(item.created_at, null),
796
+ sqlBinding(item.updated_at, null),
797
+ id,
798
+ );
799
+ stats.scratchpad.updated++;
800
+ } else {
801
+ db.prepare(
802
+ "INSERT INTO scratchpad (id, content, session_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
803
+ ).run(
804
+ id,
805
+ sqlBinding(item.content, ""),
806
+ sqlBinding(item.session_id, "default"),
807
+ sqlBinding(item.created_at, null),
808
+ sqlBinding(item.updated_at, null),
809
+ );
810
+ stats.scratchpad.inserted++;
811
+ }
812
+ }
813
+
814
+ for (const raw of Array.isArray(data.consolidation_log) ? data.consolidation_log : []) {
815
+ const item = jsonObject(raw);
816
+ db.prepare(
817
+ "INSERT INTO consolidation_log (session_id, items_consolidated, summary_preview, created_at) VALUES (?, ?, ?, ?)",
818
+ ).run(
819
+ sqlBinding(item.session_id, "default"),
820
+ sqlBinding(item.items_consolidated, 0),
821
+ sqlBinding(item.summary_preview, ""),
822
+ sqlBinding(item.created_at, null),
823
+ );
824
+ stats.consolidation_log.inserted++;
825
+ }
826
+ });
827
+ invalidateCaches(beam);
828
+ return stats;
829
+ }