mnemocyte 0.1.5 → 0.3.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.
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { n as isMnemocyteError, t as MnemocyteError } from "./errors-BG3-mNur.mjs";
1
+ import { n as isMnemocyteError, t as MnemocyteError } from "./errors-B_7ZdjEO.mjs";
2
2
  import { drizzle } from "drizzle-orm/postgres-js";
3
3
  import postgres from "postgres";
4
4
  import { and, count, countDistinct, desc, eq, gt, inArray, isNotNull, isNull, lt, or, sql } from "drizzle-orm";
@@ -15,26 +15,29 @@ var __exportAll = (all, no_symbols) => {
15
15
  return target;
16
16
  };
17
17
  //#endregion
18
+ //#region src/db/vector.ts
19
+ function formatVectorComponent(value) {
20
+ if (!Number.isFinite(value)) throw new Error("Vector values must be finite numbers.");
21
+ return Object.is(value, -0) ? "0" : value.toFixed(17);
22
+ }
23
+ //#endregion
18
24
  //#region src/db/schema.ts
19
25
  var schema_exports = /* @__PURE__ */ __exportAll({
20
26
  eventsTable: () => eventsTable,
21
- memoriesTable: () => memoriesTable
27
+ memoriesTable: () => memoriesTable,
28
+ metaTable: () => metaTable
22
29
  });
23
30
  const vector = customType({
24
31
  dataType(config) {
25
- return `vector(${typeof config === "number" ? config : 1536})`;
32
+ return `vector(${config?.dimensions ?? 1536})`;
26
33
  },
27
34
  toDriver(value) {
28
- return `[${value.map(formatVectorComponent$1).join(",")}]`;
35
+ return `[${value.map(formatVectorComponent).join(",")}]`;
29
36
  },
30
37
  fromDriver(value) {
31
38
  return value.slice(1, -1).split(",").filter(Boolean).map(Number);
32
39
  }
33
40
  });
34
- function formatVectorComponent$1(value) {
35
- if (!Number.isFinite(value)) throw new Error("Vector values must be finite numbers.");
36
- return Object.is(value, -0) ? "0" : value.toFixed(17);
37
- }
38
41
  const memoriesTable = pgTable("mnemocyte_memories", {
39
42
  id: text("id").primaryKey(),
40
43
  entityId: text("entity_id").notNull(),
@@ -43,7 +46,7 @@ const memoriesTable = pgTable("mnemocyte_memories", {
43
46
  importance: text("importance").notNull().default("normal"),
44
47
  tags: text("tags").array().notNull().default(sql`'{}'::text[]`),
45
48
  source: text("source"),
46
- metadata: jsonb("metadata").notNull().default({}),
49
+ metadata: jsonb("metadata").$type().notNull().default({}),
47
50
  confidence: real("confidence").notNull().default(1),
48
51
  embedding: vector("embedding", { dimensions: 1536 }),
49
52
  embeddingModel: text("embedding_model").notNull(),
@@ -64,9 +67,13 @@ const eventsTable = pgTable("mnemocyte_events", {
64
67
  id: text("id").primaryKey(),
65
68
  entityId: text("entity_id").notNull(),
66
69
  description: text("description").notNull(),
67
- metadata: jsonb("metadata").notNull().default({}),
70
+ metadata: jsonb("metadata").$type().notNull().default({}),
68
71
  timestamp: timestamp("timestamp", { withTimezone: true }).notNull().defaultNow()
69
72
  }, (table) => [index("mnemocyte_events_entity_time_idx").on(table.entityId, table.timestamp)]);
73
+ const metaTable = pgTable("mnemocyte_meta", {
74
+ key: text("key").primaryKey(),
75
+ embeddingDimensions: integer("embedding_dimensions").notNull()
76
+ });
70
77
  //#endregion
71
78
  //#region src/db/index.ts
72
79
  function parseSslFromUrl(url) {
@@ -76,7 +83,12 @@ function parseSslFromUrl(url) {
76
83
  if (mode === "verify-full") return "verify-full";
77
84
  }
78
85
  function createDatabase(databaseUrl) {
79
- const url = new URL(databaseUrl);
86
+ let url;
87
+ try {
88
+ url = new URL(databaseUrl);
89
+ } catch (error) {
90
+ throw new MnemocyteError("databaseUrl must be a valid Postgres connection URL.", "CONFIG", error);
91
+ }
80
92
  const ssl = parseSslFromUrl(url);
81
93
  const isPooler = url.port === "6543" || url.searchParams.get("pgbouncer") === "true";
82
94
  const client = postgres(databaseUrl, {
@@ -94,6 +106,203 @@ function createDatabase(databaseUrl) {
94
106
  };
95
107
  }
96
108
  //#endregion
109
+ //#region src/memory/json.ts
110
+ function invalidJsonValue(path, cause) {
111
+ return new MnemocyteError(`${path} must contain only JSON-compatible value data.`, "VALIDATION", cause);
112
+ }
113
+ function cloneJsonValue(value, path, ancestors) {
114
+ if (value === null || typeof value === "string" || typeof value === "boolean") return value;
115
+ if (typeof value === "number") {
116
+ if (!Number.isFinite(value)) throw invalidJsonValue(path);
117
+ return Object.is(value, -0) ? 0 : value;
118
+ }
119
+ if (typeof value !== "object") throw invalidJsonValue(path);
120
+ if (ancestors.has(value)) throw invalidJsonValue(path);
121
+ ancestors.add(value);
122
+ try {
123
+ if (Array.isArray(value)) {
124
+ if (Object.getOwnPropertySymbols(value).length > 0 || Object.keys(value).some((key) => !/^(0|[1-9]\d*)$/.test(key))) throw invalidJsonValue(path);
125
+ return Array.from({ length: value.length }, (_, index) => {
126
+ if (!Object.hasOwn(value, index)) throw invalidJsonValue(`${path}[${index}]`);
127
+ return cloneJsonValue(value[index], `${path}[${index}]`, ancestors);
128
+ });
129
+ }
130
+ const prototype = Object.getPrototypeOf(value);
131
+ if (prototype !== Object.prototype && prototype !== null) throw invalidJsonValue(path);
132
+ const descriptors = Object.getOwnPropertyDescriptors(value);
133
+ const entries = [];
134
+ for (const key of Reflect.ownKeys(descriptors)) {
135
+ if (typeof key !== "string") throw invalidJsonValue(path);
136
+ const descriptor = descriptors[key];
137
+ if (descriptor === void 0 || descriptor.enumerable !== true || !("value" in descriptor)) throw invalidJsonValue(`${path}.${key}`);
138
+ entries.push([key, cloneJsonValue(descriptor.value, `${path}.${key}`, ancestors)]);
139
+ }
140
+ return Object.fromEntries(entries);
141
+ } finally {
142
+ ancestors.delete(value);
143
+ }
144
+ }
145
+ /** Validate and deep-clone a JSON-compatible object. */
146
+ function cloneJsonObject(value, path = "metadata") {
147
+ try {
148
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw invalidJsonValue(path);
149
+ return cloneJsonValue(value, path, /* @__PURE__ */ new Set());
150
+ } catch (error) {
151
+ if (error instanceof MnemocyteError) throw error;
152
+ throw invalidJsonValue(path, error);
153
+ }
154
+ }
155
+ /** Assert that a runtime value is a JSON-compatible object. */
156
+ function validateJsonObject(value, path = "metadata") {
157
+ cloneJsonObject(value, path);
158
+ }
159
+ //#endregion
160
+ //#region src/memory/records.ts
161
+ function normalizeTags(tags) {
162
+ if (!tags) return [];
163
+ return [...new Set(tags.map((tag) => tag.trim()).filter(Boolean))];
164
+ }
165
+ function createId() {
166
+ return `mem_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
167
+ }
168
+ /** Generate a unique audit-event id (`evt_*`). */
169
+ function createEventId() {
170
+ return `evt_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
171
+ }
172
+ function cloneMemory(memory) {
173
+ return {
174
+ id: memory.id,
175
+ entityId: memory.entityId,
176
+ content: memory.content,
177
+ type: memory.type,
178
+ importance: memory.importance,
179
+ tags: [...memory.tags],
180
+ source: memory.source,
181
+ metadata: cloneJsonObject(memory.metadata),
182
+ confidence: memory.confidence,
183
+ embeddingModel: memory.embeddingModel,
184
+ embeddingDimensions: memory.embeddingDimensions,
185
+ supersededBy: memory.supersededBy,
186
+ supersededAt: memory.supersededAt ? new Date(memory.supersededAt) : null,
187
+ expiresAt: memory.expiresAt ? new Date(memory.expiresAt) : null,
188
+ lastAccessedAt: memory.lastAccessedAt ? new Date(memory.lastAccessedAt) : null,
189
+ accessCount: memory.accessCount,
190
+ createdAt: new Date(memory.createdAt),
191
+ updatedAt: new Date(memory.updatedAt)
192
+ };
193
+ }
194
+ //#endregion
195
+ //#region src/retrieval/scorer.ts
196
+ const IMPORTANCE_SCORES = {
197
+ low: 0,
198
+ normal: .5,
199
+ high: .8,
200
+ critical: 1
201
+ };
202
+ const DEFAULT_RETRIEVAL_WEIGHTS = {
203
+ vector: .55,
204
+ lexical: .2,
205
+ recency: .1,
206
+ confidence: .05,
207
+ access: .05,
208
+ importance: .05
209
+ };
210
+ const DEFAULT_RECENCY_HALF_LIFE_DAYS = 90;
211
+ const DEFAULT_ACCESS_SATURATION = 10;
212
+ function cosineSimilarity(a, b) {
213
+ let dot = 0;
214
+ let aMagnitude = 0;
215
+ let bMagnitude = 0;
216
+ for (let i = 0; i < a.length; i += 1) {
217
+ const av = a[i] ?? 0;
218
+ const bv = b[i] ?? 0;
219
+ dot += av * bv;
220
+ aMagnitude += av * av;
221
+ bMagnitude += bv * bv;
222
+ }
223
+ if (aMagnitude === 0 || bMagnitude === 0) return 0;
224
+ return dot / (Math.sqrt(aMagnitude) * Math.sqrt(bMagnitude));
225
+ }
226
+ function tokenizeQuery(query) {
227
+ return query.toLowerCase().split(/\s+/).filter(Boolean);
228
+ }
229
+ function lexicalScoreWithTerms(content, terms) {
230
+ if (terms.length === 0) return 0;
231
+ const lowerContent = content.toLowerCase();
232
+ return terms.filter((term) => lowerContent.includes(term)).length / terms.length;
233
+ }
234
+ function createLexicalScorer(query) {
235
+ const terms = tokenizeQuery(query);
236
+ return (content) => lexicalScoreWithTerms(content, terms);
237
+ }
238
+ function clampScore(value) {
239
+ if (!Number.isFinite(value)) return 0;
240
+ return Math.max(0, Math.min(1, value));
241
+ }
242
+ function normalizeWeights(weights) {
243
+ const merged = {
244
+ ...DEFAULT_RETRIEVAL_WEIGHTS,
245
+ ...weights
246
+ };
247
+ const total = merged.vector + merged.lexical + merged.recency + merged.confidence + merged.access + merged.importance;
248
+ if (total <= 0) return { ...DEFAULT_RETRIEVAL_WEIGHTS };
249
+ return {
250
+ vector: merged.vector / total,
251
+ lexical: merged.lexical / total,
252
+ recency: merged.recency / total,
253
+ confidence: merged.confidence / total,
254
+ access: merged.access / total,
255
+ importance: merged.importance / total
256
+ };
257
+ }
258
+ function createScoringConfig(config) {
259
+ return {
260
+ weights: normalizeWeights(config?.weights),
261
+ recencyHalfLifeDays: config?.recencyHalfLifeDays ?? DEFAULT_RECENCY_HALF_LIFE_DAYS,
262
+ accessSaturation: config?.accessSaturation ?? DEFAULT_ACCESS_SATURATION
263
+ };
264
+ }
265
+ function recencyScore(createdAt, halfLifeDays) {
266
+ const ageMs = Date.now() - createdAt.getTime();
267
+ const ageDays = Math.max(0, ageMs / 864e5);
268
+ return Math.exp(-Math.LN2 * ageDays / halfLifeDays);
269
+ }
270
+ function accessScore(accessCount, saturation) {
271
+ if (accessCount <= 0) return 0;
272
+ return Math.min(1, Math.log1p(accessCount) / Math.log1p(saturation));
273
+ }
274
+ function toScoredMemoryWithConfig(memory, vector, lexical, input, config) {
275
+ const { weights } = config;
276
+ const recency = recencyScore(memory.createdAt, config.recencyHalfLifeDays);
277
+ const confidence = clampScore(memory.confidence);
278
+ const access = accessScore(memory.accessCount, config.accessSaturation);
279
+ const importance = IMPORTANCE_SCORES[memory.importance];
280
+ const score = Math.max(0, Math.min(1, clampScore(vector) * weights.vector + clampScore(lexical) * weights.lexical + recency * weights.recency + confidence * weights.confidence + access * weights.access + importance * weights.importance));
281
+ return {
282
+ ...cloneMemory(memory),
283
+ score,
284
+ scores: {
285
+ vector: clampScore(vector),
286
+ lexical: clampScore(lexical),
287
+ recency,
288
+ confidence,
289
+ access,
290
+ importance
291
+ },
292
+ explanation: input.explain ? {
293
+ vectorScore: clampScore(vector),
294
+ lexicalScore: clampScore(lexical),
295
+ recencyScore: recency,
296
+ confidenceScore: confidence,
297
+ accessScore: access,
298
+ importanceScore: importance,
299
+ importanceBoost: importance * weights.importance,
300
+ weights,
301
+ finalScore: score
302
+ } : null
303
+ };
304
+ }
305
+ //#endregion
97
306
  //#region src/context/formatter.ts
98
307
  function escapeXml(value) {
99
308
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&apos;");
@@ -245,6 +454,17 @@ async function observe(config, backend, operation, metadata, action, successMeta
245
454
  throw error;
246
455
  }
247
456
  }
457
+ /**
458
+ * Ordering of {@link ImportanceLevel} from least to most important. Used
459
+ * by `prune({ maxImportance })` to compare a memory's importance against
460
+ * the caller-supplied ceiling.
461
+ */
462
+ const IMPORTANCE_RANK = {
463
+ low: 0,
464
+ normal: 1,
465
+ high: 2,
466
+ critical: 3
467
+ };
248
468
  //#endregion
249
469
  //#region src/resilience.ts
250
470
  /**
@@ -331,37 +551,44 @@ function delay(ms, signal) {
331
551
  }
332
552
  });
333
553
  }
554
+ function timeoutError(timeoutMs) {
555
+ return new MnemocyteError(`Provider call timed out after ${timeoutMs}ms.`, "TIMEOUT");
556
+ }
334
557
  /**
335
- * Race `promise` against the configured `timeoutMs` and the caller's
336
- * `signal`. Rejects with `"TIMEOUT"` or `"ABORTED"` respectively. Resolves
337
- * with the original promise value if it settles first.
558
+ * Run one provider attempt with a signal linked to the caller's signal and
559
+ * Mnemocyte's configured timeout. Timeout aborts are actively propagated to
560
+ * providers that honor `AbortSignal`.
338
561
  */
339
- function withTimeout(promise, timeoutMs, signal) {
340
- if (timeoutMs <= 0 && !signal) return promise;
562
+ function runAttempt(action, timeoutMs, signal) {
563
+ if (timeoutMs <= 0 && !signal) return action(void 0);
341
564
  return new Promise((resolve, reject) => {
342
565
  let settled = false;
566
+ const controller = new AbortController();
343
567
  const timer = timeoutMs > 0 ? setTimeout(() => {
344
568
  if (settled) return;
345
569
  settled = true;
346
570
  signal?.removeEventListener("abort", onAbort);
347
- reject(new MnemocyteError(`Provider call timed out after ${timeoutMs}ms.`, "TIMEOUT"));
571
+ controller.abort(timeoutError(timeoutMs));
572
+ reject(timeoutError(timeoutMs));
348
573
  }, timeoutMs) : void 0;
349
574
  const onAbort = () => {
350
575
  if (settled) return;
351
576
  settled = true;
352
577
  if (timer) clearTimeout(timer);
578
+ controller.abort(signal?.reason);
353
579
  reject(new MnemocyteError("Operation was aborted.", "ABORTED", signal?.reason));
354
580
  };
355
581
  if (signal) {
356
582
  if (signal.aborted) {
357
583
  if (timer) clearTimeout(timer);
358
584
  settled = true;
585
+ controller.abort(signal.reason);
359
586
  reject(new MnemocyteError("Operation was aborted.", "ABORTED", signal.reason));
360
587
  return;
361
588
  }
362
589
  signal.addEventListener("abort", onAbort, { once: true });
363
590
  }
364
- promise.then((value) => {
591
+ action(controller.signal).then((value) => {
365
592
  if (settled) return;
366
593
  settled = true;
367
594
  if (timer) clearTimeout(timer);
@@ -400,7 +627,7 @@ async function withResilience(action, options = {}) {
400
627
  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
401
628
  throwIfAborted(externalSignal);
402
629
  try {
403
- return await withTimeout(action(externalSignal), timeoutMs, externalSignal);
630
+ return await runAttempt(action, timeoutMs, externalSignal);
404
631
  } catch (error) {
405
632
  lastError = error;
406
633
  if (error instanceof MnemocyteError && error.code === "ABORTED") throw error;
@@ -411,84 +638,11 @@ async function withResilience(action, options = {}) {
411
638
  }
412
639
  throw lastError;
413
640
  }
414
- /**
415
- * Ordering of {@link ImportanceLevel} from least to most important. Used
416
- * by `prune({ maxImportance })` to compare a memory's importance against
417
- * the caller-supplied ceiling.
418
- */
419
- const IMPORTANCE_RANK = {
420
- low: 0,
421
- normal: 1,
422
- high: 2,
423
- critical: 3
424
- };
425
- function assertNonEmptyString(value, field) {
426
- if (value.trim().length === 0) throw new MnemocyteError(`${field} must be a non-empty string.`, "VALIDATION");
427
- }
428
- function assertLimit(value) {
429
- if (!Number.isInteger(value) || value < 1) throw new MnemocyteError("limit must be a positive integer.", "VALIDATION");
430
- }
431
- function assertMinScore(value) {
432
- if (!Number.isFinite(value) || value < 0 || value > 1) throw new MnemocyteError("minScore must be a number between 0 and 1.", "VALIDATION");
433
- }
434
- function assertEmbedder(embedder) {
435
- if (!embedder || typeof embedder.embed !== "function") throw new MnemocyteError("embedder with an embed function is required.", "CONFIG");
436
- assertNonEmptyString(embedder.model, "embedder.model");
437
- if (!Number.isInteger(embedder.dimensions) || embedder.dimensions < 1) throw new MnemocyteError("embedder.dimensions must be a positive integer.", "CONFIG");
438
- }
439
- function normalizeTags(tags) {
440
- if (!tags) return [];
441
- return [...new Set(tags.map((tag) => tag.trim()).filter(Boolean))];
442
- }
443
- function createId() {
444
- return `mem_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
445
- }
446
- /** Generate a unique audit-event id (`evt_*`). */
447
- function createEventId() {
448
- return `evt_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
449
- }
450
- function isExpired(memory, now) {
451
- return memory.expiresAt !== null && memory.expiresAt.getTime() <= now.getTime();
452
- }
453
- function cloneMemory(memory) {
454
- return {
455
- ...memory,
456
- tags: [...memory.tags],
457
- metadata: { ...memory.metadata },
458
- supersededAt: memory.supersededAt ? new Date(memory.supersededAt) : null,
459
- expiresAt: memory.expiresAt ? new Date(memory.expiresAt) : null,
460
- lastAccessedAt: memory.lastAccessedAt ? new Date(memory.lastAccessedAt) : null,
461
- createdAt: new Date(memory.createdAt),
462
- updatedAt: new Date(memory.updatedAt)
463
- };
464
- }
465
- function toDate(value) {
466
- return value instanceof Date ? value : new Date(value);
467
- }
468
- function toNullableDate(value) {
469
- return value === null ? null : toDate(value);
470
- }
471
- function rowToMemory(row) {
472
- return {
473
- id: row.id,
474
- entityId: row.entityId,
475
- content: row.content,
476
- type: row.type,
477
- importance: row.importance,
478
- tags: row.tags,
479
- source: row.source,
480
- metadata: row.metadata,
481
- confidence: row.confidence,
482
- embeddingModel: row.embeddingModel,
483
- embeddingDimensions: row.embeddingDimensions,
484
- supersededBy: row.supersededBy,
485
- supersededAt: toNullableDate(row.supersededAt),
486
- expiresAt: toNullableDate(row.expiresAt),
487
- lastAccessedAt: toNullableDate(row.lastAccessedAt),
488
- accessCount: row.accessCount,
489
- createdAt: toDate(row.createdAt),
490
- updatedAt: toDate(row.updatedAt)
491
- };
641
+ //#endregion
642
+ //#region src/memory/embeddings.ts
643
+ function validateEmbedding(embedding, dimensions) {
644
+ if (embedding.length !== dimensions) throw new MnemocyteError("Embedder returned an embedding with unexpected dimensions.", "EMBEDDING");
645
+ if (embedding.some((component) => !Number.isFinite(component))) throw new MnemocyteError("Embedder returned an embedding with non-finite values.", "EMBEDDING");
492
646
  }
493
647
  async function embedOne(embedder, text, options = {}) {
494
648
  let embeddings;
@@ -503,7 +657,7 @@ async function embedOne(embedder, text, options = {}) {
503
657
  }
504
658
  const embedding = embeddings[0];
505
659
  if (!embedding) throw new MnemocyteError("Embedder returned no embedding.", "EMBEDDING");
506
- if (embedding.length !== embedder.dimensions) throw new MnemocyteError("Embedder returned an embedding with unexpected dimensions.", "EMBEDDING");
660
+ validateEmbedding(embedding, embedder.dimensions);
507
661
  return embedding;
508
662
  }
509
663
  async function embedMany(embedder, texts, options = {}) {
@@ -519,23 +673,67 @@ async function embedMany(embedder, texts, options = {}) {
519
673
  throw new MnemocyteError("Failed to embed texts.", "EMBEDDING", error);
520
674
  }
521
675
  if (embeddings.length !== texts.length) throw new MnemocyteError(`Embedder returned ${embeddings.length} embeddings for ${texts.length} texts.`, "EMBEDDING");
522
- for (const embedding of embeddings) if (embedding.length !== embedder.dimensions) throw new MnemocyteError("Embedder returned an embedding with unexpected dimensions.", "EMBEDDING");
676
+ for (const embedding of embeddings) validateEmbedding(embedding, embedder.dimensions);
523
677
  return embeddings;
524
678
  }
525
- function validateRememberInput(input) {
526
- assertNonEmptyString(input.entityId, "entityId");
527
- assertNonEmptyString(input.content, "content");
528
- if (input.confidence !== void 0 && (!Number.isFinite(input.confidence) || input.confidence < 0 || input.confidence > 1)) throw new MnemocyteError("confidence must be a number between 0 and 1.", "VALIDATION");
679
+ //#endregion
680
+ //#region src/memory/validation.ts
681
+ function assertNonEmptyString(value, field, code = "VALIDATION") {
682
+ if (typeof value !== "string" || value.trim().length === 0) throw new MnemocyteError(`${field} must be a non-empty string.`, code);
529
683
  }
530
- function validateRecallInput(input) {
531
- assertNonEmptyString(input.entityId, "entityId");
532
- assertNonEmptyString(input.query, "query");
533
- if (input.limit !== void 0) assertLimit(input.limit);
534
- if (input.minScore !== void 0) assertMinScore(input.minScore);
684
+ function assertLimit(value) {
685
+ if (!Number.isInteger(value) || value < 1) throw new MnemocyteError("limit must be a positive integer.", "VALIDATION");
535
686
  }
536
- function contextInputToRecallInput(input) {
537
- return {
538
- entityId: input.entityId,
687
+ function assertMinScore(value) {
688
+ if (!Number.isFinite(value) || value < 0 || value > 1) throw new MnemocyteError("minScore must be a number between 0 and 1.", "VALIDATION");
689
+ }
690
+ function assertEmbedder(embedder) {
691
+ if (!embedder || typeof embedder.embed !== "function") throw new MnemocyteError("embedder with an embed function is required.", "CONFIG");
692
+ assertNonEmptyString(embedder.model, "embedder.model", "CONFIG");
693
+ if (!Number.isInteger(embedder.dimensions) || embedder.dimensions < 1) throw new MnemocyteError("embedder.dimensions must be a positive integer.", "CONFIG");
694
+ }
695
+ const RETRIEVAL_WEIGHT_KEYS = [
696
+ "vector",
697
+ "lexical",
698
+ "recency",
699
+ "confidence",
700
+ "access",
701
+ "importance"
702
+ ];
703
+ function assertPositiveFiniteConfigValue(value, field) {
704
+ if (!Number.isFinite(value) || value <= 0) throw new MnemocyteError(`${field} must be a positive finite number.`, "CONFIG");
705
+ }
706
+ function validateRetrievalConfig(config) {
707
+ if (config === void 0) return;
708
+ let weightTotal = 0;
709
+ for (const key of RETRIEVAL_WEIGHT_KEYS) {
710
+ const weight = config.weights?.[key] ?? DEFAULT_RETRIEVAL_WEIGHTS[key];
711
+ if (!Number.isFinite(weight) || weight < 0) throw new MnemocyteError(`retrieval.weights.${key} must be a non-negative finite number.`, "CONFIG");
712
+ weightTotal += weight;
713
+ }
714
+ if (weightTotal === 0) throw new MnemocyteError("retrieval.weights must have an effective total greater than zero.", "CONFIG");
715
+ if (config.recencyHalfLifeDays !== void 0) assertPositiveFiniteConfigValue(config.recencyHalfLifeDays, "retrieval.recencyHalfLifeDays");
716
+ if (config.accessSaturation !== void 0) assertPositiveFiniteConfigValue(config.accessSaturation, "retrieval.accessSaturation");
717
+ if (config.candidateMultiplier !== void 0 && (!Number.isInteger(config.candidateMultiplier) || config.candidateMultiplier < 1)) throw new MnemocyteError("retrieval.candidateMultiplier must be an integer greater than or equal to 1.", "CONFIG");
718
+ }
719
+ function validateBuildContextInput(input) {
720
+ if (input.maxTokens !== void 0 && (!Number.isInteger(input.maxTokens) || input.maxTokens < 1)) throw new MnemocyteError("maxTokens must be a positive integer when supplied.", "VALIDATION");
721
+ }
722
+ function validateRememberInput(input) {
723
+ assertNonEmptyString(input.entityId, "entityId");
724
+ assertNonEmptyString(input.content, "content");
725
+ if (input.metadata !== void 0) validateJsonObject(input.metadata);
726
+ if (input.confidence !== void 0 && (!Number.isFinite(input.confidence) || input.confidence < 0 || input.confidence > 1)) throw new MnemocyteError("confidence must be a number between 0 and 1.", "VALIDATION");
727
+ }
728
+ function validateRecallInput(input) {
729
+ assertNonEmptyString(input.entityId, "entityId");
730
+ assertNonEmptyString(input.query, "query");
731
+ if (input.limit !== void 0) assertLimit(input.limit);
732
+ if (input.minScore !== void 0) assertMinScore(input.minScore);
733
+ }
734
+ function contextInputToRecallInput(input) {
735
+ return {
736
+ entityId: input.entityId,
539
737
  query: input.query,
540
738
  ...input.limit === void 0 ? {} : { limit: input.limit },
541
739
  ...input.minScore === void 0 ? {} : { minScore: input.minScore },
@@ -555,27 +753,6 @@ function validatePruneInput(input) {
555
753
  if (!(input.entityId !== void 0 || input.expired === true || input.superseded === true || input.createdBefore !== void 0 || input.notAccessedSince !== void 0 || input.types !== void 0 && input.types.length > 0 || input.tags !== void 0 && input.tags.length > 0 || input.maxImportance !== void 0)) throw new MnemocyteError("prune requires at least one selector (entityId, expired, superseded, createdBefore, notAccessedSince, types, tags, or maxImportance).", "VALIDATION");
556
754
  }
557
755
  /**
558
- * Test whether `memory` matches a {@link PruneInput}. All specified
559
- * selectors are AND-combined; unspecified selectors do not restrict.
560
- *
561
- * `now` is supplied by the caller so callers can share a consistent
562
- * timestamp across a batch.
563
- */
564
- function matchesPruneFilter(memory, input, now) {
565
- if (input.entityId !== void 0 && memory.entityId !== input.entityId) return false;
566
- if (input.expired === true && !isExpired(memory, now)) return false;
567
- if (input.superseded === true && memory.supersededBy === null) return false;
568
- if (input.createdBefore !== void 0 && memory.createdAt.getTime() >= input.createdBefore.getTime()) return false;
569
- if (input.notAccessedSince !== void 0) {
570
- const last = memory.lastAccessedAt;
571
- if (last !== null && last.getTime() >= input.notAccessedSince.getTime()) return false;
572
- }
573
- if (input.types !== void 0 && !input.types.includes(memory.type)) return false;
574
- if (input.tags !== void 0 && input.tags.length > 0 && !input.tags.every((tag) => memory.tags.includes(tag))) return false;
575
- if (input.maxImportance !== void 0 && IMPORTANCE_RANK[memory.importance] > IMPORTANCE_RANK[input.maxImportance]) return false;
576
- return true;
577
- }
578
- /**
579
756
  * Validate a {@link FindDuplicatesInput} and throw a `"VALIDATION"`
580
757
  * {@link MnemocyteError} for malformed `threshold` / `limit` values.
581
758
  */
@@ -587,18 +764,6 @@ function validateFindDuplicatesInput(input) {
587
764
  if (input.limit !== void 0) assertLimit(input.limit);
588
765
  }
589
766
  /**
590
- * Return `true` when a memory should participate in a duplicate scan,
591
- * given the filters on `input` and a shared `now` timestamp.
592
- */
593
- function matchesDuplicateFilter(memory, input, now) {
594
- if (memory.entityId !== input.entityId) return false;
595
- if (input.includeSuperseded !== true && memory.supersededBy !== null) return false;
596
- if (input.includeExpired !== true && isExpired(memory, now)) return false;
597
- if (input.types !== void 0 && !input.types.includes(memory.type)) return false;
598
- if (input.tags !== void 0 && input.tags.length > 0 && !input.tags.every((tag) => memory.tags.includes(tag))) return false;
599
- return true;
600
- }
601
- /**
602
767
  * Validate a {@link ConsolidateInput} and throw a `"VALIDATION"`
603
768
  * {@link MnemocyteError} for malformed inputs (empty `supersededIds`
604
769
  * or a `survivorId` appearing in `supersededIds`).
@@ -618,274 +783,178 @@ function validateListAuditLogInput(input) {
618
783
  assertNonEmptyString(input.entityId, "entityId");
619
784
  if (input.limit !== void 0) assertLimit(input.limit);
620
785
  }
621
- function matchesRecallFilter(memory, input, now) {
622
- if (memory.entityId !== input.entityId) return false;
623
- if (!input.includeSuperseded && memory.supersededBy !== null) return false;
624
- if (!input.includeExpired && isExpired(memory, now)) return false;
625
- if (input.types && !input.types.includes(memory.type)) return false;
626
- if (input.tags && !input.tags.every((tag) => memory.tags.includes(tag))) return false;
627
- if (input.before && memory.createdAt >= input.before) return false;
628
- if (input.after && memory.createdAt <= input.after) return false;
629
- return true;
630
- }
631
786
  //#endregion
632
- //#region src/retrieval/scorer.ts
633
- const IMPORTANCE_SCORES = {
634
- low: 0,
635
- normal: .5,
636
- high: .8,
637
- critical: 1
638
- };
639
- const DEFAULT_WEIGHTS = {
640
- vector: .55,
641
- lexical: .2,
642
- recency: .1,
643
- confidence: .05,
644
- access: .05,
645
- importance: .05
646
- };
647
- const DEFAULT_RECENCY_HALF_LIFE_DAYS = 90;
648
- const DEFAULT_ACCESS_SATURATION = 10;
649
- function cosineSimilarity(a, b) {
650
- let dot = 0;
651
- let aMagnitude = 0;
652
- let bMagnitude = 0;
653
- for (let i = 0; i < a.length; i += 1) {
654
- const av = a[i] ?? 0;
655
- const bv = b[i] ?? 0;
656
- dot += av * bv;
657
- aMagnitude += av * av;
658
- bMagnitude += bv * bv;
659
- }
660
- if (aMagnitude === 0 || bMagnitude === 0) return 0;
661
- return dot / (Math.sqrt(aMagnitude) * Math.sqrt(bMagnitude));
662
- }
663
- function tokenizeQuery(query) {
664
- return query.toLowerCase().split(/\s+/).filter(Boolean);
787
+ //#region src/memory/client-core.ts
788
+ function isPositionalRememberManyInput(input) {
789
+ return Array.isArray(input);
665
790
  }
666
- function lexicalScoreWithTerms(content, terms) {
667
- if (terms.length === 0) return 0;
668
- const lowerContent = content.toLowerCase();
669
- return terms.filter((term) => lowerContent.includes(term)).length / terms.length;
670
- }
671
- function createLexicalScorer(query) {
672
- const terms = tokenizeQuery(query);
673
- return (content) => lexicalScoreWithTerms(content, terms);
674
- }
675
- function clampScore(value) {
676
- if (!Number.isFinite(value)) return 0;
677
- return Math.max(0, Math.min(1, value));
678
- }
679
- function normalizeWeights(weights) {
680
- const merged = {
681
- ...DEFAULT_WEIGHTS,
682
- ...weights
683
- };
684
- const total = merged.vector + merged.lexical + merged.recency + merged.confidence + merged.access + merged.importance;
685
- if (total <= 0) return DEFAULT_WEIGHTS;
791
+ function providerOptions(config, signal) {
686
792
  return {
687
- vector: merged.vector / total,
688
- lexical: merged.lexical / total,
689
- recency: merged.recency / total,
690
- confidence: merged.confidence / total,
691
- access: merged.access / total,
692
- importance: merged.importance / total
793
+ ...signal === void 0 ? {} : { signal },
794
+ ...config.provider === void 0 ? {} : { resilience: config.provider }
693
795
  };
694
796
  }
695
- function createScoringConfig(config) {
797
+ function createStoredMemory(config, input, embedding, now) {
696
798
  return {
697
- weights: normalizeWeights(config?.weights),
698
- recencyHalfLifeDays: config?.recencyHalfLifeDays ?? DEFAULT_RECENCY_HALF_LIFE_DAYS,
699
- accessSaturation: config?.accessSaturation ?? DEFAULT_ACCESS_SATURATION
799
+ id: createId(),
800
+ entityId: input.entityId,
801
+ content: input.content,
802
+ type: input.type ?? "fact",
803
+ importance: input.importance ?? "normal",
804
+ tags: normalizeTags(input.tags),
805
+ source: input.source ?? null,
806
+ metadata: cloneJsonObject(input.metadata ?? {}),
807
+ confidence: input.confidence ?? 1,
808
+ embeddingModel: config.embedder.model,
809
+ embeddingDimensions: config.embedder.dimensions,
810
+ supersededBy: null,
811
+ supersededAt: null,
812
+ expiresAt: input.expiresAt ?? null,
813
+ lastAccessedAt: null,
814
+ accessCount: 0,
815
+ createdAt: now,
816
+ updatedAt: now,
817
+ embedding
700
818
  };
701
819
  }
702
- function recencyScore(createdAt, halfLifeDays) {
703
- const ageMs = Date.now() - createdAt.getTime();
704
- const ageDays = Math.max(0, ageMs / 864e5);
705
- return Math.exp(-Math.LN2 * ageDays / halfLifeDays);
706
- }
707
- function accessScore(accessCount, saturation) {
708
- if (accessCount <= 0) return 0;
709
- return Math.min(1, Math.log1p(accessCount) / Math.log1p(saturation));
820
+ function auditEvent(entityId, description, metadata, timestamp = /* @__PURE__ */ new Date()) {
821
+ return {
822
+ id: createEventId(),
823
+ entityId,
824
+ description,
825
+ metadata: cloneJsonObject(metadata),
826
+ timestamp
827
+ };
710
828
  }
711
- function toScoredMemoryWithConfig(memory, vector, lexical, input, config) {
712
- const { weights } = config;
713
- const recency = recencyScore(memory.createdAt, config.recencyHalfLifeDays);
714
- const confidence = clampScore(memory.confidence);
715
- const access = accessScore(memory.accessCount, config.accessSaturation);
716
- const importance = IMPORTANCE_SCORES[memory.importance];
717
- const score = Math.max(0, Math.min(1, clampScore(vector) * weights.vector + clampScore(lexical) * weights.lexical + recency * weights.recency + confidence * weights.confidence + access * weights.access + importance * weights.importance));
829
+ function toDuplicatePair(pair) {
718
830
  return {
719
- ...cloneMemory(memory),
720
- score,
721
- scores: {
722
- vector: clampScore(vector),
723
- lexical: clampScore(lexical),
724
- recency,
725
- confidence,
726
- access,
727
- importance
728
- },
729
- explanation: input.explain ? {
730
- vectorScore: clampScore(vector),
731
- lexicalScore: clampScore(lexical),
732
- recencyScore: recency,
733
- confidenceScore: confidence,
734
- accessScore: access,
735
- importanceScore: importance,
736
- importanceBoost: importance * weights.importance,
737
- weights,
738
- finalScore: score
739
- } : null
831
+ a: cloneMemory(pair.a),
832
+ b: cloneMemory(pair.b),
833
+ similarity: Math.max(0, Math.min(1, pair.similarity))
740
834
  };
741
835
  }
742
- //#endregion
743
- //#region src/memory/in-memory.ts
744
- function createInMemoryClient(config) {
745
- const memories = /* @__PURE__ */ new Map();
746
- const auditLog = [];
836
+ function createMemoryClient(config, store) {
747
837
  let closed = false;
748
838
  function assertOpen() {
749
839
  if (closed) throw new MnemocyteError("Mnemocyte client is closed.", "DB");
750
840
  }
751
- function recordAudit(entityId, description, metadata) {
752
- if (config.audit?.enabled !== true) return;
753
- auditLog.push({
754
- id: createEventId(),
755
- entityId,
756
- description,
757
- metadata,
758
- timestamp: /* @__PURE__ */ new Date()
759
- });
841
+ async function ensureSchema() {
842
+ await store.ensureSchema();
760
843
  }
761
- function cloneAuditEvent(event) {
762
- return {
763
- id: event.id,
764
- entityId: event.entityId,
765
- description: event.description,
766
- metadata: { ...event.metadata },
767
- timestamp: new Date(event.timestamp)
768
- };
844
+ async function ensureEmbeddingCompatibility() {
845
+ await store.ensureEmbeddingCompatibility(config.embedder);
846
+ }
847
+ async function recordAudit(events) {
848
+ if (events.length === 0 || config.audit?.enabled !== true) return;
849
+ try {
850
+ await store.addAuditEvents(events);
851
+ } catch {}
769
852
  }
770
853
  async function remember(input) {
771
- return observe(config, "in-memory", "remember", { entityId: input.entityId }, async () => {
854
+ const preparedInput = {
855
+ ...input,
856
+ metadata: cloneJsonObject(input.metadata ?? {})
857
+ };
858
+ return observe(config, store.backend, "remember", { entityId: preparedInput.entityId }, async () => {
772
859
  assertOpen();
773
- validateRememberInput(input);
774
- const embedding = await embedOne(config.embedder, input.content, {
775
- ...input.signal === void 0 ? {} : { signal: input.signal },
776
- ...config.provider === void 0 ? {} : { resilience: config.provider }
777
- });
778
- const now = /* @__PURE__ */ new Date();
779
- const memory = {
780
- id: createId(),
781
- entityId: input.entityId,
782
- content: input.content,
783
- type: input.type ?? "fact",
784
- importance: input.importance ?? "normal",
785
- tags: normalizeTags(input.tags),
786
- source: input.source ?? null,
787
- metadata: input.metadata ?? {},
788
- confidence: input.confidence ?? 1,
789
- embeddingModel: config.embedder.model,
790
- embeddingDimensions: config.embedder.dimensions,
791
- supersededBy: null,
792
- supersededAt: null,
793
- expiresAt: input.expiresAt ?? null,
794
- lastAccessedAt: null,
795
- accessCount: 0,
796
- createdAt: now,
797
- updatedAt: now,
798
- embedding
799
- };
800
- memories.set(memory.id, memory);
801
- recordAudit(memory.entityId, "memory.created", {
860
+ validateRememberInput(preparedInput);
861
+ await ensureEmbeddingCompatibility();
862
+ const embedding = await embedOne(config.embedder, preparedInput.content, providerOptions(config, preparedInput.signal));
863
+ const [memory] = await store.insertMemories([createStoredMemory(config, preparedInput, embedding, /* @__PURE__ */ new Date())]);
864
+ if (!memory) throw new MnemocyteError("Memory insert returned no rows.", "DB");
865
+ await recordAudit([auditEvent(memory.entityId, "memory.created", {
802
866
  memoryId: memory.id,
803
867
  type: memory.type,
804
868
  importance: memory.importance
805
- });
869
+ })]);
806
870
  return cloneMemory(memory);
807
871
  }, (memory) => ({
808
872
  memoryId: memory.id,
809
873
  count: 1
810
874
  }));
811
875
  }
876
+ async function rememberMany(input) {
877
+ const positional = isPositionalRememberManyInput(input);
878
+ const inputs = positional ? input : input.inputs;
879
+ const signal = positional ? input[0]?.signal : input.signal;
880
+ const preparedInputs = inputs.map((item) => ({
881
+ ...item,
882
+ metadata: cloneJsonObject(item.metadata ?? {})
883
+ }));
884
+ return observe(config, store.backend, "rememberMany", { count: preparedInputs.length }, async () => {
885
+ assertOpen();
886
+ if (preparedInputs.length === 0) return [];
887
+ for (const item of preparedInputs) validateRememberInput(item);
888
+ await ensureEmbeddingCompatibility();
889
+ const embeddings = await embedMany(config.embedder, preparedInputs.map((item) => item.content), providerOptions(config, signal));
890
+ const now = /* @__PURE__ */ new Date();
891
+ const stored = preparedInputs.map((item, idx) => createStoredMemory(config, item, embeddings[idx], now));
892
+ const memories = await store.insertMemories(stored);
893
+ await recordAudit(memories.map((memory) => auditEvent(memory.entityId, "memory.created", {
894
+ memoryId: memory.id,
895
+ type: memory.type,
896
+ importance: memory.importance
897
+ })));
898
+ return memories.map(cloneMemory);
899
+ }, (result) => ({ count: result.length }));
900
+ }
812
901
  const client = {
813
902
  remember,
814
- async rememberMany(inputs) {
815
- return observe(config, "in-memory", "rememberMany", { count: inputs.length }, async () => {
816
- assertOpen();
817
- if (inputs.length === 0) return [];
818
- for (const input of inputs) validateRememberInput(input);
819
- const embeddings = await embedMany(config.embedder, inputs.map((i) => i.content), {
820
- ...inputs[0]?.signal === void 0 ? {} : { signal: inputs[0].signal },
821
- ...config.provider === void 0 ? {} : { resilience: config.provider }
822
- });
823
- const now = /* @__PURE__ */ new Date();
824
- const result = [];
825
- for (let i = 0; i < inputs.length; i++) {
826
- const input = inputs[i];
827
- const memory = {
828
- id: createId(),
829
- entityId: input.entityId,
830
- content: input.content,
831
- type: input.type ?? "fact",
832
- importance: input.importance ?? "normal",
833
- tags: normalizeTags(input.tags),
834
- source: input.source ?? null,
835
- metadata: input.metadata ?? {},
836
- confidence: input.confidence ?? 1,
837
- embeddingModel: config.embedder.model,
838
- embeddingDimensions: config.embedder.dimensions,
839
- supersededBy: null,
840
- supersededAt: null,
841
- expiresAt: input.expiresAt ?? null,
842
- lastAccessedAt: null,
843
- accessCount: 0,
844
- createdAt: now,
845
- updatedAt: now,
846
- embedding: embeddings[i]
847
- };
848
- memories.set(memory.id, memory);
849
- recordAudit(memory.entityId, "memory.created", {
850
- memoryId: memory.id,
851
- type: memory.type,
852
- importance: memory.importance
853
- });
854
- result.push(cloneMemory(memory));
855
- }
856
- return result;
857
- }, (result) => ({ count: result.length }));
858
- },
903
+ rememberMany,
859
904
  async recall(input) {
860
- return observe(config, "in-memory", "recall", { entityId: input.entityId }, async () => {
905
+ return observe(config, store.backend, "recall", { entityId: input.entityId }, async () => {
861
906
  assertOpen();
862
907
  validateRecallInput(input);
863
908
  const limit = input.limit ?? config.defaults?.limit ?? 10;
864
909
  const minScore = input.minScore ?? config.defaults?.minScore ?? 0;
865
910
  assertLimit(limit);
866
911
  assertMinScore(minScore);
867
- const queryEmbedding = await embedOne(config.embedder, input.query, {
868
- ...input.signal === void 0 ? {} : { signal: input.signal },
869
- ...config.provider === void 0 ? {} : { resilience: config.provider }
870
- });
871
- const now = /* @__PURE__ */ new Date();
912
+ await ensureEmbeddingCompatibility();
913
+ const queryEmbedding = await embedOne(config.embedder, input.query, providerOptions(config, input.signal));
914
+ const candidateMultiplier = config.retrieval?.candidateMultiplier ?? 3;
915
+ const candidateLimit = Math.max(limit, limit * candidateMultiplier);
872
916
  const scoreLexical = createLexicalScorer(input.query);
873
917
  const scoringConfig = createScoringConfig(config.retrieval);
874
- const scored = Array.from(memories.values()).filter((memory) => matchesRecallFilter(memory, input, now)).map((memory) => toScoredMemoryWithConfig(memory, Math.max(0, cosineSimilarity(memory.embedding, queryEmbedding)), scoreLexical(memory.content), input, scoringConfig)).filter((memory) => memory.score >= minScore).sort((a, b) => b.score - a.score).slice(0, limit);
875
- for (const memory of scored) {
876
- const stored = memories.get(memory.id);
877
- if (stored) {
878
- stored.lastAccessedAt = now;
879
- stored.accessCount += 1;
880
- stored.updatedAt = now;
918
+ const [vectorCandidates, lexicalCandidates] = await Promise.all([store.vectorSearch({
919
+ ...input,
920
+ embedding: queryEmbedding,
921
+ limit: candidateLimit,
922
+ minScore: 0
923
+ }), store.lexicalSearch({
924
+ ...input,
925
+ limit: candidateLimit
926
+ })]);
927
+ const merged = /* @__PURE__ */ new Map();
928
+ for (const candidate of vectorCandidates) merged.set(candidate.memory.id, {
929
+ memory: candidate.memory,
930
+ vectorScore: candidate.vectorScore,
931
+ lexicalScore: scoreLexical(candidate.memory.content)
932
+ });
933
+ const lexicalOnly = lexicalCandidates.filter((candidate) => !merged.has(candidate.memory.id));
934
+ const lexicalOnlyEmbeddings = await store.getMemoryEmbeddings(lexicalOnly.map((candidate) => candidate.memory.id));
935
+ for (const candidate of lexicalCandidates) {
936
+ const existing = merged.get(candidate.memory.id);
937
+ if (existing) {
938
+ existing.lexicalScore = candidate.lexicalScore;
939
+ continue;
881
940
  }
941
+ const embedding = lexicalOnlyEmbeddings.get(candidate.memory.id);
942
+ const similarity = embedding ? cosineSimilarity(embedding, queryEmbedding) : 0;
943
+ merged.set(candidate.memory.id, {
944
+ memory: candidate.memory,
945
+ vectorScore: Math.max(0, similarity),
946
+ lexicalScore: candidate.lexicalScore
947
+ });
882
948
  }
949
+ const scored = Array.from(merged.values()).map((entry) => toScoredMemoryWithConfig(entry.memory, Math.max(0, entry.vectorScore), Math.max(0, entry.lexicalScore), input, scoringConfig)).filter((memory) => memory.score >= minScore).sort((a, b) => b.score - a.score).slice(0, limit);
950
+ await store.markMemoriesAccessed(scored.map((memory) => memory.id));
883
951
  return scored;
884
952
  }, (result) => ({ count: result.length }));
885
953
  },
886
954
  async buildContext(input) {
887
- return observe(config, "in-memory", "buildContext", { entityId: input.entityId }, async () => {
955
+ return observe(config, store.backend, "buildContext", { entityId: input.entityId }, async () => {
888
956
  assertOpen();
957
+ validateBuildContextInput(input);
889
958
  return buildContext({
890
959
  input,
891
960
  recall: (contextInput) => {
@@ -899,178 +968,375 @@ function createInMemoryClient(config) {
899
968
  });
900
969
  },
901
970
  async forget(input) {
902
- return observe(config, "in-memory", "forget", {
971
+ return observe(config, store.backend, "forget", {
903
972
  entityId: input.entityId,
904
973
  memoryId: input.memoryId
905
974
  }, async () => {
906
975
  assertOpen();
907
976
  assertNonEmptyString(input.entityId, "entityId");
908
977
  assertNonEmptyString(input.memoryId, "memoryId");
909
- const memory = memories.get(input.memoryId);
910
- if (!memory || memory.entityId !== input.entityId) throw new MnemocyteError("Memory was not found.", "NOT_FOUND");
911
- memories.delete(input.memoryId);
912
- recordAudit(memory.entityId, "memory.deleted", { memoryId: memory.id });
978
+ await ensureSchema();
979
+ if (!await store.deleteMemory(input.entityId, input.memoryId)) throw new MnemocyteError("Memory was not found.", "NOT_FOUND");
980
+ await recordAudit([auditEvent(input.entityId, "memory.deleted", { memoryId: input.memoryId })]);
913
981
  });
914
982
  },
915
983
  async forgetAll(input) {
916
- return observe(config, "in-memory", "forgetAll", { entityId: input.entityId }, async () => {
984
+ return observe(config, store.backend, "forgetAll", { entityId: input.entityId }, async () => {
917
985
  assertOpen();
918
986
  assertNonEmptyString(input.entityId, "entityId");
919
- let deleted = 0;
920
- for (const [id, memory] of memories) if (memory.entityId === input.entityId) {
921
- memories.delete(id);
922
- deleted += 1;
923
- }
924
- recordAudit(input.entityId, "entity.cleared", { count: deleted });
925
- return deleted;
987
+ await ensureSchema();
988
+ const deletedCount = await store.deleteMemoriesForEntity(input.entityId);
989
+ await recordAudit([auditEvent(input.entityId, "entity.cleared", { count: deletedCount })]);
990
+ return deletedCount;
926
991
  }, (count) => ({ count })).then(() => void 0);
927
992
  },
928
993
  async prune(input) {
929
- return observe(config, "in-memory", "prune", input.entityId === void 0 ? {} : { entityId: input.entityId }, async () => {
994
+ return observe(config, store.backend, "prune", input.entityId === void 0 ? {} : { entityId: input.entityId }, async () => {
930
995
  assertOpen();
931
996
  validatePruneInput(input);
932
- const now = /* @__PURE__ */ new Date();
933
- const matched = [];
934
- for (const memory of memories.values()) if (matchesPruneFilter(memory, input, now)) matched.push(memory.id);
935
- const dryRun = input.dryRun === true;
936
- if (!dryRun) {
937
- for (const id of matched) memories.delete(id);
938
- if (matched.length > 0 && input.entityId !== void 0) recordAudit(input.entityId, "memory.pruned", { count: matched.length });
939
- }
997
+ await ensureSchema();
998
+ const result = await store.prune(input);
999
+ if (result.deletedCount > 0 && input.entityId !== void 0 && result.dryRun === false) await recordAudit([auditEvent(input.entityId, "memory.pruned", { count: result.deletedCount })]);
1000
+ return result;
1001
+ }, (result) => ({ count: result.deletedCount }));
1002
+ },
1003
+ async findDuplicates(input) {
1004
+ return observe(config, store.backend, "findDuplicates", { entityId: input.entityId }, async () => {
1005
+ assertOpen();
1006
+ validateFindDuplicatesInput(input);
1007
+ await ensureEmbeddingCompatibility();
1008
+ return (await store.findDuplicatePairs(input)).map(toDuplicatePair);
1009
+ }, (result) => ({ count: result.length }));
1010
+ },
1011
+ async listAuditLog(input) {
1012
+ return observe(config, store.backend, "listAuditLog", { entityId: input.entityId }, async () => {
1013
+ assertOpen();
1014
+ validateListAuditLogInput(input);
1015
+ await ensureSchema();
1016
+ return (await store.listAuditLog(input)).map((event) => ({
1017
+ id: event.id,
1018
+ entityId: event.entityId,
1019
+ description: event.description,
1020
+ metadata: cloneJsonObject(event.metadata),
1021
+ timestamp: new Date(event.timestamp)
1022
+ }));
1023
+ }, (result) => ({ count: result.length }));
1024
+ },
1025
+ async stats(input) {
1026
+ return observe(config, store.backend, "stats", input?.entityId ? { entityId: input.entityId } : {}, async () => {
1027
+ assertOpen();
1028
+ await ensureSchema();
1029
+ return store.stats(input, /* @__PURE__ */ new Date());
1030
+ });
1031
+ },
1032
+ experimental: { async consolidate(input) {
1033
+ return observe(config, store.backend, "consolidate", {
1034
+ entityId: input.entityId,
1035
+ memoryId: input.survivorId
1036
+ }, async () => {
1037
+ assertOpen();
1038
+ validateConsolidateInput(input);
1039
+ await ensureSchema();
1040
+ const survivor = await store.getMemory(input.entityId, input.survivorId);
1041
+ if (!survivor) throw new MnemocyteError("Survivor memory was not found.", "NOT_FOUND");
1042
+ if (survivor.supersededBy !== null) throw new MnemocyteError("Survivor memory is itself superseded.", "VALIDATION");
1043
+ const targets = await store.loadConsolidationTargets(input.entityId, input.supersededIds);
1044
+ const foundIds = new Set(targets.map((target) => target.id));
1045
+ for (const id of input.supersededIds) if (!foundIds.has(id)) throw new MnemocyteError("Superseded memory was not found.", "NOT_FOUND");
1046
+ const losers = targets.filter((target) => target.supersededBy === null);
1047
+ if (losers.length === 0) return {
1048
+ survivorId: survivor.id,
1049
+ supersededCount: 0,
1050
+ supersededIds: []
1051
+ };
1052
+ const result = await store.consolidate({
1053
+ entityId: input.entityId,
1054
+ survivorId: survivor.id,
1055
+ survivorTags: survivor.tags,
1056
+ supersededIds: losers.map((loser) => loser.id),
1057
+ mergeTags: input.mergeTags !== false,
1058
+ now: /* @__PURE__ */ new Date(),
1059
+ auditEnabled: config.audit?.enabled === true
1060
+ });
940
1061
  return {
941
- matchedCount: matched.length,
942
- deletedCount: dryRun ? 0 : matched.length,
943
- dryRun
1062
+ survivorId: survivor.id,
1063
+ supersededCount: result.supersededIds.length,
1064
+ supersededIds: result.supersededIds
944
1065
  };
945
- }, (result) => ({ count: result.deletedCount }));
1066
+ }, (result) => ({ count: result.supersededCount }));
1067
+ } },
1068
+ async close() {
1069
+ return observe(config, store.backend, "close", {}, async () => {
1070
+ if (!closed) {
1071
+ closed = true;
1072
+ await store.close();
1073
+ }
1074
+ });
1075
+ }
1076
+ };
1077
+ return client;
1078
+ }
1079
+ //#endregion
1080
+ //#region src/memory/filters.ts
1081
+ function isExpired(memory, now) {
1082
+ return memory.expiresAt !== null && memory.expiresAt.getTime() <= now.getTime();
1083
+ }
1084
+ /**
1085
+ * Test whether `memory` matches a {@link PruneInput}. All specified
1086
+ * selectors are AND-combined; unspecified selectors do not restrict.
1087
+ *
1088
+ * `now` is supplied by the caller so callers can share a consistent
1089
+ * timestamp across a batch.
1090
+ */
1091
+ function matchesPruneFilter(memory, input, now) {
1092
+ if (input.entityId !== void 0 && memory.entityId !== input.entityId) return false;
1093
+ if (input.expired === true && !isExpired(memory, now)) return false;
1094
+ if (input.superseded === true && memory.supersededBy === null) return false;
1095
+ if (input.createdBefore !== void 0 && memory.createdAt.getTime() >= input.createdBefore.getTime()) return false;
1096
+ if (input.notAccessedSince !== void 0) {
1097
+ const last = memory.lastAccessedAt;
1098
+ if (last !== null && last.getTime() >= input.notAccessedSince.getTime()) return false;
1099
+ }
1100
+ if (input.types !== void 0 && !input.types.includes(memory.type)) return false;
1101
+ if (input.tags !== void 0 && input.tags.length > 0 && !input.tags.every((tag) => memory.tags.includes(tag))) return false;
1102
+ if (input.maxImportance !== void 0 && IMPORTANCE_RANK[memory.importance] > IMPORTANCE_RANK[input.maxImportance]) return false;
1103
+ return true;
1104
+ }
1105
+ /**
1106
+ * Return `true` when a memory should participate in a duplicate scan,
1107
+ * given the filters on `input` and a shared `now` timestamp.
1108
+ */
1109
+ function matchesDuplicateFilter(memory, input, now) {
1110
+ if (memory.entityId !== input.entityId) return false;
1111
+ if (input.includeSuperseded !== true && memory.supersededBy !== null) return false;
1112
+ if (input.includeExpired !== true && isExpired(memory, now)) return false;
1113
+ if (input.types !== void 0 && !input.types.includes(memory.type)) return false;
1114
+ if (input.tags !== void 0 && input.tags.length > 0 && !input.tags.every((tag) => memory.tags.includes(tag))) return false;
1115
+ return true;
1116
+ }
1117
+ function matchesRecallFilter(memory, input, now) {
1118
+ if (memory.entityId !== input.entityId) return false;
1119
+ if (!input.includeSuperseded && memory.supersededBy !== null) return false;
1120
+ if (!input.includeExpired && isExpired(memory, now)) return false;
1121
+ if (input.types && !input.types.includes(memory.type)) return false;
1122
+ if (input.tags && !input.tags.every((tag) => memory.tags.includes(tag))) return false;
1123
+ if (input.before && memory.createdAt >= input.before) return false;
1124
+ if (input.after && memory.createdAt <= input.after) return false;
1125
+ return true;
1126
+ }
1127
+ //#endregion
1128
+ //#region src/memory/in-memory.ts
1129
+ function cloneStoredMemory(memory) {
1130
+ return {
1131
+ ...cloneMemory(memory),
1132
+ embedding: [...memory.embedding]
1133
+ };
1134
+ }
1135
+ function cloneAuditEvent(event) {
1136
+ return {
1137
+ id: event.id,
1138
+ entityId: event.entityId,
1139
+ description: event.description,
1140
+ metadata: cloneJsonObject(event.metadata),
1141
+ timestamp: new Date(event.timestamp)
1142
+ };
1143
+ }
1144
+ function createInMemoryStore() {
1145
+ const memories = /* @__PURE__ */ new Map();
1146
+ const auditLog = [];
1147
+ return {
1148
+ backend: "in-memory",
1149
+ async ensureSchema() {},
1150
+ async ensureEmbeddingCompatibility() {},
1151
+ async insertMemories(rows) {
1152
+ const inserted = [];
1153
+ for (const row of rows) {
1154
+ const stored = cloneStoredMemory(row);
1155
+ memories.set(stored.id, stored);
1156
+ inserted.push(cloneMemory(stored));
1157
+ }
1158
+ return inserted;
1159
+ },
1160
+ async vectorSearch(input) {
1161
+ const now = /* @__PURE__ */ new Date();
1162
+ const minScore = input.minScore ?? 0;
1163
+ return Array.from(memories.values()).filter((memory) => matchesRecallFilter(memory, input, now)).map((memory) => ({
1164
+ memory: cloneMemory(memory),
1165
+ vectorScore: Math.max(0, cosineSimilarity(memory.embedding, input.embedding))
1166
+ })).filter((candidate) => candidate.vectorScore >= minScore).sort((a, b) => b.vectorScore - a.vectorScore).slice(0, input.limit);
1167
+ },
1168
+ async lexicalSearch(input) {
1169
+ const now = /* @__PURE__ */ new Date();
1170
+ const scoreLexical = createLexicalScorer(input.query);
1171
+ return Array.from(memories.values()).filter((memory) => matchesRecallFilter(memory, input, now)).map((memory) => ({
1172
+ memory: cloneMemory(memory),
1173
+ lexicalScore: scoreLexical(memory.content)
1174
+ })).filter((candidate) => candidate.lexicalScore > 0).sort((a, b) => b.lexicalScore - a.lexicalScore).slice(0, input.limit);
1175
+ },
1176
+ async getMemoryEmbeddings(memoryIds) {
1177
+ const result = /* @__PURE__ */ new Map();
1178
+ for (const id of memoryIds) {
1179
+ const memory = memories.get(id);
1180
+ if (memory) result.set(id, [...memory.embedding]);
1181
+ }
1182
+ return result;
1183
+ },
1184
+ async markMemoriesAccessed(memoryIds) {
1185
+ const now = /* @__PURE__ */ new Date();
1186
+ for (const id of memoryIds) {
1187
+ const memory = memories.get(id);
1188
+ if (memory) {
1189
+ memory.lastAccessedAt = now;
1190
+ memory.accessCount += 1;
1191
+ memory.updatedAt = now;
1192
+ }
1193
+ }
1194
+ },
1195
+ async deleteMemory(entityId, memoryId) {
1196
+ const memory = memories.get(memoryId);
1197
+ if (!memory || memory.entityId !== entityId) return false;
1198
+ return memories.delete(memoryId);
1199
+ },
1200
+ async deleteMemoriesForEntity(entityId) {
1201
+ let deleted = 0;
1202
+ for (const [id, memory] of memories) if (memory.entityId === entityId) {
1203
+ memories.delete(id);
1204
+ deleted += 1;
1205
+ }
1206
+ return deleted;
946
1207
  },
947
- async findDuplicates(input) {
948
- return observe(config, "in-memory", "findDuplicates", { entityId: input.entityId }, async () => {
949
- assertOpen();
950
- validateFindDuplicatesInput(input);
951
- const threshold = input.threshold ?? .95;
952
- const limit = input.limit ?? 100;
953
- const now = /* @__PURE__ */ new Date();
954
- const candidates = Array.from(memories.values()).filter((memory) => matchesDuplicateFilter(memory, input, now));
955
- const pairs = [];
956
- for (let i = 0; i < candidates.length; i += 1) {
957
- const a = candidates[i];
958
- if (a === void 0) continue;
959
- for (let j = i + 1; j < candidates.length; j += 1) {
960
- const b = candidates[j];
961
- if (b === void 0) continue;
962
- const similarity = Math.max(0, Math.min(1, cosineSimilarity(a.embedding, b.embedding)));
963
- if (similarity >= threshold) pairs.push({
964
- a: cloneMemory(a),
965
- b: cloneMemory(b),
966
- similarity
967
- });
968
- }
1208
+ async prune(input) {
1209
+ const now = /* @__PURE__ */ new Date();
1210
+ const matched = [];
1211
+ for (const memory of memories.values()) if (matchesPruneFilter(memory, input, now)) matched.push(memory.id);
1212
+ const dryRun = input.dryRun === true;
1213
+ if (!dryRun) for (const id of matched) memories.delete(id);
1214
+ return {
1215
+ matchedCount: matched.length,
1216
+ deletedCount: dryRun ? 0 : matched.length,
1217
+ dryRun
1218
+ };
1219
+ },
1220
+ async findDuplicatePairs(input) {
1221
+ const threshold = input.threshold ?? .95;
1222
+ const limit = input.limit ?? 100;
1223
+ const now = /* @__PURE__ */ new Date();
1224
+ const candidates = Array.from(memories.values()).filter((memory) => matchesDuplicateFilter(memory, input, now));
1225
+ const pairs = [];
1226
+ for (let i = 0; i < candidates.length; i += 1) {
1227
+ const a = candidates[i];
1228
+ if (a === void 0) continue;
1229
+ for (let j = i + 1; j < candidates.length; j += 1) {
1230
+ const b = candidates[j];
1231
+ if (b === void 0) continue;
1232
+ const similarity = Math.max(0, Math.min(1, cosineSimilarity(a.embedding, b.embedding)));
1233
+ if (similarity >= threshold) pairs.push({
1234
+ a: cloneMemory(a),
1235
+ b: cloneMemory(b),
1236
+ similarity
1237
+ });
969
1238
  }
970
- pairs.sort((x, y) => y.similarity - x.similarity);
971
- return pairs.slice(0, limit);
972
- }, (result) => ({ count: result.length }));
1239
+ }
1240
+ pairs.sort((x, y) => y.similarity - x.similarity);
1241
+ return pairs.slice(0, limit);
1242
+ },
1243
+ async addAuditEvents(events) {
1244
+ auditLog.push(...events.map(cloneAuditEvent));
973
1245
  },
974
1246
  async listAuditLog(input) {
975
- return observe(config, "in-memory", "listAuditLog", { entityId: input.entityId }, async () => {
976
- assertOpen();
977
- validateListAuditLogInput(input);
978
- const limit = input.limit ?? 50;
979
- return auditLog.map((event, idx) => ({
980
- event,
981
- idx
982
- })).filter(({ event }) => {
983
- if (event.entityId !== input.entityId) return false;
984
- if (input.before !== void 0 && event.timestamp.getTime() >= input.before.getTime()) return false;
985
- if (input.after !== void 0 && event.timestamp.getTime() <= input.after.getTime()) return false;
986
- return true;
987
- }).sort((a, b) => {
988
- const dt = b.event.timestamp.getTime() - a.event.timestamp.getTime();
989
- if (dt !== 0) return dt;
990
- return b.idx - a.idx;
991
- }).slice(0, limit).map(({ event }) => cloneAuditEvent(event));
992
- }, (result) => ({ count: result.length }));
1247
+ const limit = input.limit ?? 50;
1248
+ return auditLog.map((event, idx) => ({
1249
+ event,
1250
+ idx
1251
+ })).filter(({ event }) => {
1252
+ if (event.entityId !== input.entityId) return false;
1253
+ if (input.before !== void 0 && event.timestamp.getTime() >= input.before.getTime()) return false;
1254
+ if (input.after !== void 0 && event.timestamp.getTime() <= input.after.getTime()) return false;
1255
+ return true;
1256
+ }).sort((a, b) => {
1257
+ const dt = b.event.timestamp.getTime() - a.event.timestamp.getTime();
1258
+ return dt === 0 ? b.idx - a.idx : dt;
1259
+ }).slice(0, limit).map(({ event }) => cloneAuditEvent(event));
993
1260
  },
994
- async stats(input) {
995
- return observe(config, "in-memory", "stats", input?.entityId ? { entityId: input.entityId } : {}, async () => {
996
- assertOpen();
997
- const now = /* @__PURE__ */ new Date();
998
- const allMemories = Array.from(memories.values());
999
- const selected = input?.entityId ? allMemories.filter((memory) => memory.entityId === input.entityId) : allMemories;
1000
- const activeMemoryCount = selected.filter((memory) => !isExpired(memory, now) && memory.supersededBy === null).length;
1001
- const expiredMemoryCount = selected.filter((memory) => isExpired(memory, now)).length;
1002
- const supersededMemoryCount = selected.filter((memory) => memory.supersededBy !== null).length;
1003
- if (input?.entityId) return {
1004
- entityId: input.entityId,
1005
- memoryCount: selected.length,
1006
- activeMemoryCount,
1007
- expiredMemoryCount,
1008
- supersededMemoryCount
1009
- };
1010
- return {
1011
- entityCount: new Set(allMemories.map((memory) => memory.entityId)).size,
1012
- memoryCount: selected.length,
1013
- activeMemoryCount,
1014
- expiredMemoryCount,
1015
- supersededMemoryCount
1016
- };
1017
- });
1261
+ async getMemory(entityId, memoryId) {
1262
+ const memory = memories.get(memoryId);
1263
+ return memory && memory.entityId === entityId ? cloneMemory(memory) : null;
1018
1264
  },
1019
- experimental: createExperimental(),
1020
- async close() {
1021
- return observe(config, "in-memory", "close", {}, async () => {
1022
- closed = true;
1023
- memories.clear();
1265
+ async loadConsolidationTargets(entityId, ids) {
1266
+ return ids.flatMap((id) => {
1267
+ const memory = memories.get(id);
1268
+ if (!memory || memory.entityId !== entityId) return [];
1269
+ return [{
1270
+ id: memory.id,
1271
+ tags: [...memory.tags],
1272
+ supersededBy: memory.supersededBy
1273
+ }];
1024
1274
  });
1025
- }
1026
- };
1027
- return client;
1028
- function createExperimental() {
1029
- return { async consolidate(input) {
1030
- return observe(config, "in-memory", "consolidate", {
1031
- entityId: input.entityId,
1032
- memoryId: input.survivorId
1033
- }, async () => {
1034
- assertOpen();
1035
- validateConsolidateInput(input);
1036
- const survivor = memories.get(input.survivorId);
1037
- if (!survivor || survivor.entityId !== input.entityId) throw new MnemocyteError("Survivor memory was not found.", "NOT_FOUND");
1038
- if (survivor.supersededBy !== null) throw new MnemocyteError("Survivor memory is itself superseded.", "VALIDATION");
1039
- const losers = [];
1040
- for (const id of input.supersededIds) {
1275
+ },
1276
+ async consolidate(input) {
1277
+ const survivor = memories.get(input.survivorId);
1278
+ const newlySuperseded = [];
1279
+ for (const id of input.supersededIds) {
1280
+ const memory = memories.get(id);
1281
+ if (!memory || memory.entityId !== input.entityId) continue;
1282
+ if (memory.supersededBy !== null) continue;
1283
+ memory.supersededBy = input.survivorId;
1284
+ memory.supersededAt = input.now;
1285
+ memory.updatedAt = input.now;
1286
+ newlySuperseded.push(memory.id);
1287
+ }
1288
+ if (survivor && input.mergeTags && newlySuperseded.length > 0) {
1289
+ const mergedTags = new Set(input.survivorTags);
1290
+ for (const id of newlySuperseded) {
1041
1291
  const memory = memories.get(id);
1042
- if (!memory || memory.entityId !== input.entityId) throw new MnemocyteError("Superseded memory was not found.", "NOT_FOUND");
1043
- if (memory.supersededBy !== null) continue;
1044
- losers.push(memory);
1292
+ if (memory) for (const tag of memory.tags) mergedTags.add(tag);
1045
1293
  }
1046
- const now = /* @__PURE__ */ new Date();
1047
- const newSupersededIds = [];
1048
- for (const loser of losers) {
1049
- loser.supersededBy = survivor.id;
1050
- loser.supersededAt = now;
1051
- loser.updatedAt = now;
1052
- newSupersededIds.push(loser.id);
1053
- recordAudit(input.entityId, "memory.superseded", {
1054
- memoryId: loser.id,
1055
- supersededBy: survivor.id
1056
- });
1057
- }
1058
- if (input.mergeTags !== false && losers.length > 0) {
1059
- const mergedTags = new Set(survivor.tags);
1060
- for (const loser of losers) for (const tag of loser.tags) mergedTags.add(tag);
1061
- if (mergedTags.size !== survivor.tags.length) {
1062
- survivor.tags = [...mergedTags];
1063
- survivor.updatedAt = now;
1064
- }
1294
+ if (mergedTags.size !== survivor.tags.length) {
1295
+ survivor.tags = [...mergedTags];
1296
+ survivor.updatedAt = input.now;
1065
1297
  }
1066
- return {
1067
- survivorId: survivor.id,
1068
- supersededCount: newSupersededIds.length,
1069
- supersededIds: newSupersededIds
1070
- };
1071
- }, (result) => ({ count: result.supersededCount }));
1072
- } };
1073
- }
1298
+ }
1299
+ if (input.auditEnabled) for (const id of newlySuperseded) auditLog.push({
1300
+ id: createEventId(),
1301
+ entityId: input.entityId,
1302
+ description: "memory.superseded",
1303
+ metadata: {
1304
+ memoryId: id,
1305
+ supersededBy: input.survivorId
1306
+ },
1307
+ timestamp: input.now
1308
+ });
1309
+ return { supersededIds: newlySuperseded };
1310
+ },
1311
+ async stats(input, now) {
1312
+ const allMemories = Array.from(memories.values());
1313
+ const selected = input?.entityId ? allMemories.filter((memory) => memory.entityId === input.entityId) : allMemories;
1314
+ const activeMemoryCount = selected.filter((memory) => !isExpired(memory, now) && memory.supersededBy === null).length;
1315
+ const expiredMemoryCount = selected.filter((memory) => isExpired(memory, now)).length;
1316
+ const supersededMemoryCount = selected.filter((memory) => memory.supersededBy !== null).length;
1317
+ if (input?.entityId) return {
1318
+ entityId: input.entityId,
1319
+ memoryCount: selected.length,
1320
+ activeMemoryCount,
1321
+ expiredMemoryCount,
1322
+ supersededMemoryCount
1323
+ };
1324
+ return {
1325
+ entityCount: new Set(allMemories.map((memory) => memory.entityId)).size,
1326
+ memoryCount: selected.length,
1327
+ activeMemoryCount,
1328
+ expiredMemoryCount,
1329
+ supersededMemoryCount
1330
+ };
1331
+ },
1332
+ async close() {
1333
+ memories.clear();
1334
+ auditLog.length = 0;
1335
+ }
1336
+ };
1337
+ }
1338
+ function createInMemoryClient(config) {
1339
+ return createMemoryClient(config, createInMemoryStore());
1074
1340
  }
1075
1341
  //#endregion
1076
1342
  //#region src/db/queries/events.ts
@@ -1087,10 +1353,6 @@ async function listEvents(db, filter) {
1087
1353
  function vectorLiteral(embedding) {
1088
1354
  return `[${embedding.map(formatVectorComponent).join(",")}]`;
1089
1355
  }
1090
- function formatVectorComponent(value) {
1091
- if (!Number.isFinite(value)) throw new Error("Vector values must be finite numbers.");
1092
- return Object.is(value, -0) ? "0" : value.toFixed(17);
1093
- }
1094
1356
  function timestampParam(value) {
1095
1357
  return sql`${value.toISOString()}::timestamptz`;
1096
1358
  }
@@ -1132,11 +1394,6 @@ function emptyMemoryStatsCounts() {
1132
1394
  supersededMemoryCount: 0
1133
1395
  };
1134
1396
  }
1135
- async function insertMemory(db, row) {
1136
- const inserted = (await db.insert(memoriesTable).values(row).returning())[0];
1137
- if (!inserted) throw new MnemocyteError("Memory insert returned no rows.", "DB");
1138
- return inserted;
1139
- }
1140
1397
  async function insertMemories(db, rows) {
1141
1398
  if (rows.length === 0) return [];
1142
1399
  return db.insert(memoriesTable).values(rows).returning();
@@ -1301,7 +1558,7 @@ async function setMemoryTags(db, params) {
1301
1558
  updatedAt: params.now
1302
1559
  }).where(and(eq(memoriesTable.entityId, params.entityId), eq(memoriesTable.id, params.memoryId)));
1303
1560
  }
1304
- async function markMemoryAccessed(db, memoryIds) {
1561
+ async function markMemoriesAccessed(db, memoryIds) {
1305
1562
  const now = /* @__PURE__ */ new Date();
1306
1563
  if (memoryIds.length === 0) return;
1307
1564
  await db.update(memoriesTable).set({
@@ -1393,71 +1650,84 @@ async function lexicalSearch(db, input) {
1393
1650
  LIMIT ${input.limit}
1394
1651
  `);
1395
1652
  }
1396
- //#endregion
1397
- //#region src/retrieval/index.ts
1398
- async function hybridRecall(input) {
1399
- const queryEmbedding = await embedOne(input.embedder, input.input.query, {
1400
- ...input.signal === void 0 ? {} : { signal: input.signal },
1401
- ...input.resilience === void 0 ? {} : { resilience: input.resilience }
1402
- });
1403
- const candidateMultiplier = input.retrieval?.candidateMultiplier ?? 3;
1404
- const candidateLimit = Math.max(input.limit, input.limit * candidateMultiplier);
1405
- const scoreLexical = createLexicalScorer(input.input.query);
1406
- const scoringConfig = createScoringConfig(input.retrieval);
1407
- const [vectorRows, lexicalRows] = await Promise.all([vectorSearch(input.db, {
1408
- ...input.input,
1409
- embedding: queryEmbedding,
1410
- limit: candidateLimit,
1411
- minScore: 0
1412
- }), lexicalSearch(input.db, {
1413
- ...input.input,
1414
- limit: candidateLimit
1415
- })]);
1416
- const merged = /* @__PURE__ */ new Map();
1417
- for (const row of vectorRows) merged.set(row.id, {
1418
- row,
1419
- vectorScore: row.vectorScore,
1420
- lexicalScore: scoreLexical(row.content)
1421
- });
1422
- const lexicalOnlyRows = lexicalRows.filter((row) => !merged.has(row.id));
1423
- const lexicalOnlyEmbeddings = await getMemoryEmbeddings(input.db, lexicalOnlyRows.map((row) => row.id));
1424
- for (const row of lexicalRows) {
1425
- const existing = merged.get(row.id);
1426
- if (existing) existing.lexicalScore = row.lexicalScore;
1427
- else {
1428
- const embedding = lexicalOnlyEmbeddings.get(row.id);
1429
- const sim = embedding ? cosineSimilarity(embedding, queryEmbedding) : 0;
1430
- merged.set(row.id, {
1431
- row,
1432
- vectorScore: Math.max(0, sim),
1433
- lexicalScore: row.lexicalScore
1434
- });
1435
- }
1436
- }
1437
- const scored = Array.from(merged.values()).map((entry) => toScoredMemoryWithConfig(rowToMemory(entry.row), Math.max(0, entry.vectorScore), Math.max(0, entry.lexicalScore), input.input, scoringConfig)).filter((memory) => memory.score >= input.minScore).sort((a, b) => b.score - a.score).slice(0, input.limit);
1438
- await markMemoryAccessed(input.db, scored.map((memory) => memory.id));
1439
- return scored;
1653
+ async function getInstallationMeta(db) {
1654
+ return (await db.select().from(metaTable).where(eq(metaTable.key, "installation")).limit(1))[0] ?? null;
1440
1655
  }
1441
1656
  //#endregion
1442
- //#region src/memory/postgres.ts
1443
- function rowToAuditEvent(row) {
1657
+ //#region src/memory/postgres-records.ts
1658
+ function toDate(value) {
1659
+ return value instanceof Date ? value : new Date(value);
1660
+ }
1661
+ function toNullableDate(value) {
1662
+ return value === null ? null : toDate(value);
1663
+ }
1664
+ function rowToMemory(row) {
1444
1665
  return {
1445
1666
  id: row.id,
1446
1667
  entityId: row.entityId,
1447
- description: row.description,
1448
- metadata: row.metadata,
1449
- timestamp: row.timestamp
1668
+ content: row.content,
1669
+ type: row.type,
1670
+ importance: row.importance,
1671
+ tags: row.tags,
1672
+ source: row.source,
1673
+ metadata: cloneJsonObject(row.metadata),
1674
+ confidence: row.confidence,
1675
+ embeddingModel: row.embeddingModel,
1676
+ embeddingDimensions: row.embeddingDimensions,
1677
+ supersededBy: row.supersededBy,
1678
+ supersededAt: toNullableDate(row.supersededAt),
1679
+ expiresAt: toNullableDate(row.expiresAt),
1680
+ lastAccessedAt: toNullableDate(row.lastAccessedAt),
1681
+ accessCount: row.accessCount,
1682
+ createdAt: toDate(row.createdAt),
1683
+ updatedAt: toDate(row.updatedAt)
1450
1684
  };
1451
1685
  }
1686
+ //#endregion
1687
+ //#region src/memory/postgres.ts
1452
1688
  const IMPORTANCE_LEVELS = [
1453
1689
  "low",
1454
1690
  "normal",
1455
1691
  "high",
1456
1692
  "critical"
1457
1693
  ];
1694
+ const MIGRATION_ERROR_CODES = new Set([
1695
+ "42P01",
1696
+ "42703",
1697
+ "42704",
1698
+ "42883"
1699
+ ]);
1700
+ function rowToAuditEvent(row) {
1701
+ return {
1702
+ id: row.id,
1703
+ entityId: row.entityId,
1704
+ description: row.description,
1705
+ metadata: cloneJsonObject(row.metadata),
1706
+ timestamp: row.timestamp
1707
+ };
1708
+ }
1458
1709
  function importanceCeilingLevels(max) {
1459
1710
  return IMPORTANCE_LEVELS.filter((level) => IMPORTANCE_RANK[level] <= IMPORTANCE_RANK[max]);
1460
1711
  }
1712
+ function hasPostgresErrorCode(error, code) {
1713
+ return typeof error === "object" && error !== null && "code" in error && error.code === code;
1714
+ }
1715
+ function getPostgresErrorCode(error) {
1716
+ return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0;
1717
+ }
1718
+ function normalizePostgresError(error) {
1719
+ if (error instanceof MnemocyteError) return error;
1720
+ const code = getPostgresErrorCode(error);
1721
+ if (code && MIGRATION_ERROR_CODES.has(code)) return new MnemocyteError("Postgres schema is missing or incompatible. Apply the bundled Mnemocyte migrations before using the Postgres backend.", "MIGRATION", error);
1722
+ return new MnemocyteError("Postgres operation failed.", "DB", error);
1723
+ }
1724
+ async function runPostgresOperation(operation) {
1725
+ try {
1726
+ return await operation();
1727
+ } catch (error) {
1728
+ throw normalizePostgresError(error);
1729
+ }
1730
+ }
1461
1731
  function toPruneFilter(input) {
1462
1732
  const filter = {};
1463
1733
  if (input.entityId !== void 0) filter.entityId = input.entityId;
@@ -1470,166 +1740,76 @@ function toPruneFilter(input) {
1470
1740
  if (input.maxImportance !== void 0) filter.maxImportanceLevels = importanceCeilingLevels(input.maxImportance);
1471
1741
  return filter;
1472
1742
  }
1473
- function createPostgresClient(config, handle) {
1474
- let closed = false;
1475
- function assertOpen() {
1476
- if (closed) throw new MnemocyteError("Mnemocyte client is closed.", "DB");
1743
+ function toMemoryRow(memory) {
1744
+ return {
1745
+ ...memory,
1746
+ tags: [...memory.tags],
1747
+ metadata: cloneJsonObject(memory.metadata),
1748
+ embedding: [...memory.embedding]
1749
+ };
1750
+ }
1751
+ function createPostgresStore(handle) {
1752
+ let schemaValidated = false;
1753
+ let schemaValidationPromise;
1754
+ async function ensureEmbeddingCompatibility(embedder) {
1755
+ if (schemaValidated) return;
1756
+ if (!schemaValidationPromise) schemaValidationPromise = validateEmbeddingCompatibility(embedder).catch((error) => {
1757
+ schemaValidationPromise = void 0;
1758
+ throw error;
1759
+ });
1760
+ await schemaValidationPromise;
1477
1761
  }
1478
- async function recordAudit(entityId, description, metadata) {
1479
- if (config.audit?.enabled !== true) return;
1762
+ async function validateEmbeddingCompatibility(embedder) {
1763
+ let meta;
1480
1764
  try {
1481
- await insertEvent(handle.db, {
1482
- id: createEventId(),
1483
- entityId,
1484
- description,
1485
- metadata,
1486
- timestamp: /* @__PURE__ */ new Date()
1487
- });
1488
- } catch {}
1765
+ meta = await getInstallationMeta(handle.db);
1766
+ } catch (error) {
1767
+ if (hasPostgresErrorCode(error, "42P01")) throw new MnemocyteError("mnemocyte_meta is missing. Apply the v0.2.0 migration or render a dimension-specific initial migration before using the Postgres backend.", "MIGRATION", error);
1768
+ throw normalizePostgresError(error);
1769
+ }
1770
+ if (!meta) throw new MnemocyteError("mnemocyte_meta is missing the \"installation\" row. Apply the v0.2.0 migration or render a dimension-specific initial migration before using the Postgres backend.", "MIGRATION");
1771
+ if (meta.embeddingDimensions !== embedder.dimensions) throw new MnemocyteError(`embedder.dimensions (${embedder.dimensions}) must match mnemocyte_meta.embedding_dimensions (${meta.embeddingDimensions}). Render and apply a migration for the selected embedding dimension, or configure an embedder that matches this installation.`, "CONFIG");
1772
+ schemaValidated = true;
1489
1773
  }
1490
- async function remember(input) {
1491
- return observe(config, "postgres", "remember", { entityId: input.entityId }, async () => {
1492
- assertOpen();
1493
- validateRememberInput(input);
1494
- const embedding = await embedOne(config.embedder, input.content, {
1495
- ...input.signal === void 0 ? {} : { signal: input.signal },
1496
- ...config.provider === void 0 ? {} : { resilience: config.provider }
1774
+ return {
1775
+ backend: "postgres",
1776
+ async ensureSchema() {},
1777
+ ensureEmbeddingCompatibility,
1778
+ async insertMemories(memories) {
1779
+ return runPostgresOperation(async () => {
1780
+ return (await insertMemories(handle.db, memories.map(toMemoryRow))).map(rowToMemory);
1497
1781
  });
1498
- const now = /* @__PURE__ */ new Date();
1499
- const memory = rowToMemory(await insertMemory(handle.db, {
1500
- id: createId(),
1501
- entityId: input.entityId,
1502
- content: input.content,
1503
- type: input.type ?? "fact",
1504
- importance: input.importance ?? "normal",
1505
- tags: normalizeTags(input.tags),
1506
- source: input.source ?? null,
1507
- metadata: input.metadata ?? {},
1508
- confidence: input.confidence ?? 1,
1509
- embedding,
1510
- embeddingModel: config.embedder.model,
1511
- embeddingDimensions: config.embedder.dimensions,
1512
- supersededBy: null,
1513
- supersededAt: null,
1514
- expiresAt: input.expiresAt ?? null,
1515
- lastAccessedAt: null,
1516
- accessCount: 0,
1517
- createdAt: now,
1518
- updatedAt: now
1519
- }));
1520
- await recordAudit(memory.entityId, "memory.created", {
1521
- memoryId: memory.id,
1522
- type: memory.type,
1523
- importance: memory.importance
1782
+ },
1783
+ async vectorSearch(input) {
1784
+ return runPostgresOperation(async () => {
1785
+ return (await vectorSearch(handle.db, input)).map((row) => ({
1786
+ memory: rowToMemory(row),
1787
+ vectorScore: row.vectorScore
1788
+ }));
1524
1789
  });
1525
- return memory;
1526
- }, (memory) => ({
1527
- memoryId: memory.id,
1528
- count: 1
1529
- }));
1530
- }
1531
- const client = {
1532
- remember,
1533
- async rememberMany(inputs) {
1534
- return observe(config, "postgres", "rememberMany", { count: inputs.length }, async () => {
1535
- assertOpen();
1536
- if (inputs.length === 0) return [];
1537
- for (const input of inputs) validateRememberInput(input);
1538
- const embeddings = await embedMany(config.embedder, inputs.map((i) => i.content), {
1539
- ...inputs[0]?.signal === void 0 ? {} : { signal: inputs[0].signal },
1540
- ...config.provider === void 0 ? {} : { resilience: config.provider }
1541
- });
1542
- const now = /* @__PURE__ */ new Date();
1543
- const rows = inputs.map((input, idx) => ({
1544
- id: createId(),
1545
- entityId: input.entityId,
1546
- content: input.content,
1547
- type: input.type ?? "fact",
1548
- importance: input.importance ?? "normal",
1549
- tags: normalizeTags(input.tags),
1550
- source: input.source ?? null,
1551
- metadata: input.metadata ?? {},
1552
- confidence: input.confidence ?? 1,
1553
- embedding: embeddings[idx],
1554
- embeddingModel: config.embedder.model,
1555
- embeddingDimensions: config.embedder.dimensions,
1556
- supersededBy: null,
1557
- supersededAt: null,
1558
- expiresAt: input.expiresAt ?? null,
1559
- lastAccessedAt: null,
1560
- accessCount: 0,
1561
- createdAt: now,
1562
- updatedAt: now
1790
+ },
1791
+ async lexicalSearch(input) {
1792
+ return runPostgresOperation(async () => {
1793
+ return (await lexicalSearch(handle.db, input)).map((row) => ({
1794
+ memory: rowToMemory(row),
1795
+ lexicalScore: row.lexicalScore
1563
1796
  }));
1564
- const memories = (await insertMemories(handle.db, rows)).map(rowToMemory);
1565
- for (const memory of memories) recordAudit(memory.entityId, "memory.created", {
1566
- memoryId: memory.id,
1567
- type: memory.type,
1568
- importance: memory.importance
1569
- });
1570
- return memories;
1571
- }, (result) => ({ count: result.length }));
1797
+ });
1572
1798
  },
1573
- async recall(input) {
1574
- return observe(config, "postgres", "recall", { entityId: input.entityId }, async () => {
1575
- assertOpen();
1576
- validateRecallInput(input);
1577
- const limit = input.limit ?? config.defaults?.limit ?? 10;
1578
- const minScore = input.minScore ?? config.defaults?.minScore ?? 0;
1579
- assertLimit(limit);
1580
- assertMinScore(minScore);
1581
- return hybridRecall({
1582
- db: handle.db,
1583
- embedder: config.embedder,
1584
- input,
1585
- limit,
1586
- minScore,
1587
- retrieval: config.retrieval,
1588
- ...input.signal === void 0 ? {} : { signal: input.signal },
1589
- ...config.provider === void 0 ? {} : { resilience: config.provider }
1590
- });
1591
- }, (result) => ({ count: result.length }));
1799
+ async getMemoryEmbeddings(memoryIds) {
1800
+ return runPostgresOperation(() => getMemoryEmbeddings(handle.db, memoryIds));
1592
1801
  },
1593
- async buildContext(input) {
1594
- return observe(config, "postgres", "buildContext", { entityId: input.entityId }, async () => {
1595
- assertOpen();
1596
- return buildContext({
1597
- input,
1598
- recall: (contextInput) => {
1599
- const recallInput = contextInputToRecallInput(contextInput);
1600
- return client.recall(input.signal === void 0 ? recallInput : {
1601
- ...recallInput,
1602
- signal: input.signal
1603
- });
1604
- }
1605
- });
1606
- });
1802
+ async markMemoriesAccessed(memoryIds) {
1803
+ return runPostgresOperation(() => markMemoriesAccessed(handle.db, memoryIds));
1607
1804
  },
1608
- async forget(input) {
1609
- return observe(config, "postgres", "forget", {
1610
- entityId: input.entityId,
1611
- memoryId: input.memoryId
1612
- }, async () => {
1613
- assertOpen();
1614
- assertNonEmptyString(input.entityId, "entityId");
1615
- assertNonEmptyString(input.memoryId, "memoryId");
1616
- if (!await deleteMemory(handle.db, input.entityId, input.memoryId)) throw new MnemocyteError("Memory was not found.", "NOT_FOUND");
1617
- await recordAudit(input.entityId, "memory.deleted", { memoryId: input.memoryId });
1618
- });
1805
+ async deleteMemory(entityId, memoryId) {
1806
+ return runPostgresOperation(() => deleteMemory(handle.db, entityId, memoryId));
1619
1807
  },
1620
- async forgetAll(input) {
1621
- return observe(config, "postgres", "forgetAll", { entityId: input.entityId }, async () => {
1622
- assertOpen();
1623
- assertNonEmptyString(input.entityId, "entityId");
1624
- const deletedCount = await deleteMemoriesForEntity(handle.db, input.entityId);
1625
- await recordAudit(input.entityId, "entity.cleared", { count: deletedCount });
1626
- return deletedCount;
1627
- }, (count) => ({ count })).then(() => void 0);
1808
+ async deleteMemoriesForEntity(entityId) {
1809
+ return runPostgresOperation(() => deleteMemoriesForEntity(handle.db, entityId));
1628
1810
  },
1629
1811
  async prune(input) {
1630
- return observe(config, "postgres", "prune", input.entityId === void 0 ? {} : { entityId: input.entityId }, async () => {
1631
- assertOpen();
1632
- validatePruneInput(input);
1812
+ return runPostgresOperation(async () => {
1633
1813
  const filter = toPruneFilter(input);
1634
1814
  if (input.dryRun === true) return {
1635
1815
  matchedCount: await countPruneMatches(handle.db, filter),
@@ -1637,24 +1817,19 @@ function createPostgresClient(config, handle) {
1637
1817
  dryRun: true
1638
1818
  };
1639
1819
  const deletedCount = await pruneMemories(handle.db, filter);
1640
- if (deletedCount > 0 && input.entityId !== void 0) await recordAudit(input.entityId, "memory.pruned", { count: deletedCount });
1641
1820
  return {
1642
1821
  matchedCount: deletedCount,
1643
1822
  deletedCount,
1644
1823
  dryRun: false
1645
1824
  };
1646
- }, (result) => ({ count: result.deletedCount }));
1825
+ });
1647
1826
  },
1648
- async findDuplicates(input) {
1649
- return observe(config, "postgres", "findDuplicates", { entityId: input.entityId }, async () => {
1650
- assertOpen();
1651
- validateFindDuplicatesInput(input);
1652
- const threshold = input.threshold ?? .95;
1653
- const limit = input.limit ?? 100;
1827
+ async findDuplicatePairs(input) {
1828
+ return runPostgresOperation(async () => {
1654
1829
  return (await findDuplicatePairs(handle.db, {
1655
1830
  entityId: input.entityId,
1656
- threshold,
1657
- limit,
1831
+ threshold: input.threshold ?? .95,
1832
+ limit: input.limit ?? 100,
1658
1833
  ...input.types === void 0 ? {} : { types: input.types },
1659
1834
  ...input.tags === void 0 ? {} : { tags: input.tags },
1660
1835
  ...input.includeSuperseded === void 0 ? {} : { includeSuperseded: input.includeSuperseded },
@@ -1664,110 +1839,94 @@ function createPostgresClient(config, handle) {
1664
1839
  b: rowToMemory(row.b),
1665
1840
  similarity: Math.max(0, Math.min(1, row.similarity))
1666
1841
  }));
1667
- }, (result) => ({ count: result.length }));
1842
+ });
1843
+ },
1844
+ async addAuditEvents(events) {
1845
+ return runPostgresOperation(async () => {
1846
+ for (const event of events) await insertEvent(handle.db, {
1847
+ id: event.id,
1848
+ entityId: event.entityId,
1849
+ description: event.description,
1850
+ metadata: cloneJsonObject(event.metadata),
1851
+ timestamp: event.timestamp
1852
+ });
1853
+ });
1668
1854
  },
1669
1855
  async listAuditLog(input) {
1670
- return observe(config, "postgres", "listAuditLog", { entityId: input.entityId }, async () => {
1671
- assertOpen();
1672
- validateListAuditLogInput(input);
1856
+ return runPostgresOperation(async () => {
1673
1857
  return (await listEvents(handle.db, {
1674
1858
  entityId: input.entityId,
1675
1859
  limit: input.limit ?? 50,
1676
1860
  ...input.before === void 0 ? {} : { before: input.before },
1677
1861
  ...input.after === void 0 ? {} : { after: input.after }
1678
1862
  })).map(rowToAuditEvent);
1679
- }, (result) => ({ count: result.length }));
1680
- },
1681
- async stats(input) {
1682
- return observe(config, "postgres", "stats", input?.entityId ? { entityId: input.entityId } : {}, async () => {
1683
- assertOpen();
1684
- const now = /* @__PURE__ */ new Date();
1685
- if (input?.entityId) {
1686
- const counts = await getEntityMemoryStatsCounts(handle.db, input.entityId, now);
1687
- return {
1688
- entityId: input.entityId,
1689
- ...counts
1690
- };
1691
- }
1692
- return await getGlobalMemoryStatsCounts(handle.db, now);
1693
1863
  });
1694
1864
  },
1695
- experimental: createExperimental(),
1696
- async close() {
1697
- return observe(config, "postgres", "close", {}, async () => {
1698
- closed = true;
1699
- await handle.close();
1865
+ async getMemory(entityId, memoryId) {
1866
+ return runPostgresOperation(async () => {
1867
+ const row = await getMemoryById(handle.db, entityId, memoryId);
1868
+ return row ? rowToMemory(row) : null;
1700
1869
  });
1701
- }
1702
- };
1703
- return client;
1704
- function createExperimental() {
1705
- return { async consolidate(input) {
1706
- return observe(config, "postgres", "consolidate", {
1707
- entityId: input.entityId,
1708
- memoryId: input.survivorId
1709
- }, async () => {
1710
- assertOpen();
1711
- validateConsolidateInput(input);
1712
- const survivor = await getMemoryById(handle.db, input.entityId, input.survivorId);
1713
- if (!survivor) throw new MnemocyteError("Survivor memory was not found.", "NOT_FOUND");
1714
- if (survivor.supersededBy !== null) throw new MnemocyteError("Survivor memory is itself superseded.", "VALIDATION");
1715
- const targets = await loadConsolidationTargets(handle.db, input.entityId, input.supersededIds);
1716
- const foundIds = new Set(targets.map((t) => t.id));
1717
- for (const id of input.supersededIds) if (!foundIds.has(id)) throw new MnemocyteError("Superseded memory was not found.", "NOT_FOUND");
1718
- const losers = targets.filter((target) => target.supersededBy === null);
1719
- if (losers.length === 0) return {
1720
- survivorId: survivor.id,
1721
- supersededCount: 0,
1722
- supersededIds: []
1723
- };
1724
- const now = /* @__PURE__ */ new Date();
1725
- const { newSupersededIds } = await handle.db.transaction(async (tx) => {
1870
+ },
1871
+ async loadConsolidationTargets(entityId, ids) {
1872
+ return runPostgresOperation(() => loadConsolidationTargets(handle.db, entityId, ids));
1873
+ },
1874
+ async consolidate(input) {
1875
+ return runPostgresOperation(async () => {
1876
+ return { supersededIds: await handle.db.transaction(async (tx) => {
1726
1877
  const updated = await markMemoriesSuperseded(tx, {
1727
- survivorId: survivor.id,
1878
+ survivorId: input.survivorId,
1728
1879
  entityId: input.entityId,
1729
- ids: losers.map((loser) => loser.id),
1730
- now
1880
+ ids: input.supersededIds,
1881
+ now: input.now
1731
1882
  });
1732
- const newSupersededIds = updated.map((row) => row.id);
1733
- if (config.audit?.enabled === true) for (const id of newSupersededIds) await insertEvent(tx, {
1883
+ const ids = updated.map((row) => row.id);
1884
+ if (input.auditEnabled) for (const id of ids) await insertEvent(tx, {
1734
1885
  id: createEventId(),
1735
1886
  entityId: input.entityId,
1736
1887
  description: "memory.superseded",
1737
1888
  metadata: {
1738
1889
  memoryId: id,
1739
- supersededBy: survivor.id
1890
+ supersededBy: input.survivorId
1740
1891
  },
1741
- timestamp: now
1892
+ timestamp: input.now
1742
1893
  });
1743
- if (input.mergeTags !== false && updated.length > 0) {
1744
- const mergedTags = new Set(survivor.tags);
1894
+ if (input.mergeTags && updated.length > 0) {
1895
+ const mergedTags = new Set(input.survivorTags);
1745
1896
  for (const row of updated) for (const tag of row.tags) mergedTags.add(tag);
1746
- if (mergedTags.size !== survivor.tags.length) await setMemoryTags(tx, {
1897
+ if (mergedTags.size !== input.survivorTags.length) await setMemoryTags(tx, {
1747
1898
  entityId: input.entityId,
1748
- memoryId: survivor.id,
1899
+ memoryId: input.survivorId,
1749
1900
  tags: [...mergedTags],
1750
- now
1901
+ now: input.now
1751
1902
  });
1752
1903
  }
1904
+ return ids;
1905
+ }) };
1906
+ });
1907
+ },
1908
+ async stats(input, now) {
1909
+ return runPostgresOperation(async () => {
1910
+ if (input?.entityId) {
1911
+ const counts = await getEntityMemoryStatsCounts(handle.db, input.entityId, now);
1753
1912
  return {
1754
- updated,
1755
- newSupersededIds
1913
+ entityId: input.entityId,
1914
+ ...counts
1756
1915
  };
1757
- });
1758
- return {
1759
- survivorId: survivor.id,
1760
- supersededCount: newSupersededIds.length,
1761
- supersededIds: newSupersededIds
1762
- };
1763
- }, (result) => ({ count: result.supersededCount }));
1764
- } };
1765
- }
1916
+ }
1917
+ return await getGlobalMemoryStatsCounts(handle.db, now);
1918
+ });
1919
+ },
1920
+ async close() {
1921
+ return runPostgresOperation(() => handle.close());
1922
+ }
1923
+ };
1924
+ }
1925
+ function createPostgresClient(config, handle) {
1926
+ return createMemoryClient(config, createPostgresStore(handle));
1766
1927
  }
1767
1928
  //#endregion
1768
1929
  //#region src/client.ts
1769
- /** Embedding dimensionality pinned by `migrations/0000_initial.sql`. */
1770
- const POSTGRES_EMBEDDING_DIMENSIONS = 1536;
1771
1930
  /**
1772
1931
  * Create a Mnemocyte client.
1773
1932
  *
@@ -1782,24 +1941,23 @@ const POSTGRES_EMBEDDING_DIMENSIONS = 1536;
1782
1941
  *
1783
1942
  * @param config - Client configuration. {@link MnemocyteConfig.embedder} is required.
1784
1943
  * @returns A {@link MnemocyteClient} instance.
1785
- * @throws {MnemocyteError} With code `"CONFIG"` if `embedder` is missing or
1786
- * invalid (including when `databaseUrl` is set and `embedder.dimensions`
1787
- * is not 1536 — the dimensionality pinned by the bundled migration), or
1788
- * `"VALIDATION"` if `databaseUrl` is provided but empty.
1944
+ * @throws {MnemocyteError} With code `"CONFIG"` if `embedder`, retrieval
1945
+ * tuning, or the database URL is malformed, or `"VALIDATION"` if
1946
+ * `databaseUrl` is explicitly empty.
1789
1947
  *
1790
1948
  * @example Postgres-backed client
1791
1949
  * ```ts
1792
1950
  * const client = createMnemocyte({
1793
1951
  * databaseUrl: process.env.DATABASE_URL,
1794
- * embedder, // embedder.dimensions MUST be 1536 for the Postgres backend
1952
+ * embedder, // embedder.dimensions must match mnemocyte_meta
1795
1953
  * });
1796
1954
  * ```
1797
1955
  */
1798
1956
  function createMnemocyte(config) {
1799
1957
  assertEmbedder(config.embedder);
1800
- if (config.databaseUrl) {
1958
+ validateRetrievalConfig(config.retrieval);
1959
+ if (config.databaseUrl !== void 0) {
1801
1960
  assertNonEmptyString(config.databaseUrl, "databaseUrl");
1802
- if (config.embedder.dimensions !== POSTGRES_EMBEDDING_DIMENSIONS) throw new MnemocyteError(`embedder.dimensions must be ${POSTGRES_EMBEDDING_DIMENSIONS} for the Postgres backend (got ${config.embedder.dimensions}). The bundled migration pins the vector column to vector(${POSTGRES_EMBEDDING_DIMENSIONS}).`, "CONFIG");
1803
1961
  return createPostgresClient(config, createDatabase(config.databaseUrl));
1804
1962
  }
1805
1963
  return createInMemoryClient(config);