mnemocyte 0.2.0 → 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,6 +15,12 @@ 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,
@@ -26,16 +32,12 @@ const vector = customType({
26
32
  return `vector(${config?.dimensions ?? 1536})`;
27
33
  },
28
34
  toDriver(value) {
29
- return `[${value.map(formatVectorComponent$1).join(",")}]`;
35
+ return `[${value.map(formatVectorComponent).join(",")}]`;
30
36
  },
31
37
  fromDriver(value) {
32
38
  return value.slice(1, -1).split(",").filter(Boolean).map(Number);
33
39
  }
34
40
  });
35
- function formatVectorComponent$1(value) {
36
- if (!Number.isFinite(value)) throw new Error("Vector values must be finite numbers.");
37
- return Object.is(value, -0) ? "0" : value.toFixed(17);
38
- }
39
41
  const memoriesTable = pgTable("mnemocyte_memories", {
40
42
  id: text("id").primaryKey(),
41
43
  entityId: text("entity_id").notNull(),
@@ -44,7 +46,7 @@ const memoriesTable = pgTable("mnemocyte_memories", {
44
46
  importance: text("importance").notNull().default("normal"),
45
47
  tags: text("tags").array().notNull().default(sql`'{}'::text[]`),
46
48
  source: text("source"),
47
- metadata: jsonb("metadata").notNull().default({}),
49
+ metadata: jsonb("metadata").$type().notNull().default({}),
48
50
  confidence: real("confidence").notNull().default(1),
49
51
  embedding: vector("embedding", { dimensions: 1536 }),
50
52
  embeddingModel: text("embedding_model").notNull(),
@@ -65,7 +67,7 @@ const eventsTable = pgTable("mnemocyte_events", {
65
67
  id: text("id").primaryKey(),
66
68
  entityId: text("entity_id").notNull(),
67
69
  description: text("description").notNull(),
68
- metadata: jsonb("metadata").notNull().default({}),
70
+ metadata: jsonb("metadata").$type().notNull().default({}),
69
71
  timestamp: timestamp("timestamp", { withTimezone: true }).notNull().defaultNow()
70
72
  }, (table) => [index("mnemocyte_events_entity_time_idx").on(table.entityId, table.timestamp)]);
71
73
  const metaTable = pgTable("mnemocyte_meta", {
@@ -81,7 +83,12 @@ function parseSslFromUrl(url) {
81
83
  if (mode === "verify-full") return "verify-full";
82
84
  }
83
85
  function createDatabase(databaseUrl) {
84
- 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
+ }
85
92
  const ssl = parseSslFromUrl(url);
86
93
  const isPooler = url.port === "6543" || url.searchParams.get("pgbouncer") === "true";
87
94
  const client = postgres(databaseUrl, {
@@ -99,6 +106,203 @@ function createDatabase(databaseUrl) {
99
106
  };
100
107
  }
101
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
102
306
  //#region src/context/formatter.ts
103
307
  function escapeXml(value) {
104
308
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&apos;");
@@ -250,6 +454,17 @@ async function observe(config, backend, operation, metadata, action, successMeta
250
454
  throw error;
251
455
  }
252
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
+ };
253
468
  //#endregion
254
469
  //#region src/resilience.ts
255
470
  /**
@@ -336,37 +551,44 @@ function delay(ms, signal) {
336
551
  }
337
552
  });
338
553
  }
554
+ function timeoutError(timeoutMs) {
555
+ return new MnemocyteError(`Provider call timed out after ${timeoutMs}ms.`, "TIMEOUT");
556
+ }
339
557
  /**
340
- * Race `promise` against the configured `timeoutMs` and the caller's
341
- * `signal`. Rejects with `"TIMEOUT"` or `"ABORTED"` respectively. Resolves
342
- * 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`.
343
561
  */
344
- function withTimeout(promise, timeoutMs, signal) {
345
- if (timeoutMs <= 0 && !signal) return promise;
562
+ function runAttempt(action, timeoutMs, signal) {
563
+ if (timeoutMs <= 0 && !signal) return action(void 0);
346
564
  return new Promise((resolve, reject) => {
347
565
  let settled = false;
566
+ const controller = new AbortController();
348
567
  const timer = timeoutMs > 0 ? setTimeout(() => {
349
568
  if (settled) return;
350
569
  settled = true;
351
570
  signal?.removeEventListener("abort", onAbort);
352
- reject(new MnemocyteError(`Provider call timed out after ${timeoutMs}ms.`, "TIMEOUT"));
571
+ controller.abort(timeoutError(timeoutMs));
572
+ reject(timeoutError(timeoutMs));
353
573
  }, timeoutMs) : void 0;
354
574
  const onAbort = () => {
355
575
  if (settled) return;
356
576
  settled = true;
357
577
  if (timer) clearTimeout(timer);
578
+ controller.abort(signal?.reason);
358
579
  reject(new MnemocyteError("Operation was aborted.", "ABORTED", signal?.reason));
359
580
  };
360
581
  if (signal) {
361
582
  if (signal.aborted) {
362
583
  if (timer) clearTimeout(timer);
363
584
  settled = true;
585
+ controller.abort(signal.reason);
364
586
  reject(new MnemocyteError("Operation was aborted.", "ABORTED", signal.reason));
365
587
  return;
366
588
  }
367
589
  signal.addEventListener("abort", onAbort, { once: true });
368
590
  }
369
- promise.then((value) => {
591
+ action(controller.signal).then((value) => {
370
592
  if (settled) return;
371
593
  settled = true;
372
594
  if (timer) clearTimeout(timer);
@@ -405,7 +627,7 @@ async function withResilience(action, options = {}) {
405
627
  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
406
628
  throwIfAborted(externalSignal);
407
629
  try {
408
- return await withTimeout(action(externalSignal), timeoutMs, externalSignal);
630
+ return await runAttempt(action, timeoutMs, externalSignal);
409
631
  } catch (error) {
410
632
  lastError = error;
411
633
  if (error instanceof MnemocyteError && error.code === "ABORTED") throw error;
@@ -416,84 +638,11 @@ async function withResilience(action, options = {}) {
416
638
  }
417
639
  throw lastError;
418
640
  }
419
- /**
420
- * Ordering of {@link ImportanceLevel} from least to most important. Used
421
- * by `prune({ maxImportance })` to compare a memory's importance against
422
- * the caller-supplied ceiling.
423
- */
424
- const IMPORTANCE_RANK = {
425
- low: 0,
426
- normal: 1,
427
- high: 2,
428
- critical: 3
429
- };
430
- function assertNonEmptyString(value, field) {
431
- if (value.trim().length === 0) throw new MnemocyteError(`${field} must be a non-empty string.`, "VALIDATION");
432
- }
433
- function assertLimit(value) {
434
- if (!Number.isInteger(value) || value < 1) throw new MnemocyteError("limit must be a positive integer.", "VALIDATION");
435
- }
436
- function assertMinScore(value) {
437
- if (!Number.isFinite(value) || value < 0 || value > 1) throw new MnemocyteError("minScore must be a number between 0 and 1.", "VALIDATION");
438
- }
439
- function assertEmbedder(embedder) {
440
- if (!embedder || typeof embedder.embed !== "function") throw new MnemocyteError("embedder with an embed function is required.", "CONFIG");
441
- assertNonEmptyString(embedder.model, "embedder.model");
442
- if (!Number.isInteger(embedder.dimensions) || embedder.dimensions < 1) throw new MnemocyteError("embedder.dimensions must be a positive integer.", "CONFIG");
443
- }
444
- function normalizeTags(tags) {
445
- if (!tags) return [];
446
- return [...new Set(tags.map((tag) => tag.trim()).filter(Boolean))];
447
- }
448
- function createId() {
449
- return `mem_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
450
- }
451
- /** Generate a unique audit-event id (`evt_*`). */
452
- function createEventId() {
453
- return `evt_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
454
- }
455
- function isExpired(memory, now) {
456
- return memory.expiresAt !== null && memory.expiresAt.getTime() <= now.getTime();
457
- }
458
- function cloneMemory(memory) {
459
- return {
460
- ...memory,
461
- tags: [...memory.tags],
462
- metadata: { ...memory.metadata },
463
- supersededAt: memory.supersededAt ? new Date(memory.supersededAt) : null,
464
- expiresAt: memory.expiresAt ? new Date(memory.expiresAt) : null,
465
- lastAccessedAt: memory.lastAccessedAt ? new Date(memory.lastAccessedAt) : null,
466
- createdAt: new Date(memory.createdAt),
467
- updatedAt: new Date(memory.updatedAt)
468
- };
469
- }
470
- function toDate(value) {
471
- return value instanceof Date ? value : new Date(value);
472
- }
473
- function toNullableDate(value) {
474
- return value === null ? null : toDate(value);
475
- }
476
- function rowToMemory(row) {
477
- return {
478
- id: row.id,
479
- entityId: row.entityId,
480
- content: row.content,
481
- type: row.type,
482
- importance: row.importance,
483
- tags: row.tags,
484
- source: row.source,
485
- metadata: row.metadata,
486
- confidence: row.confidence,
487
- embeddingModel: row.embeddingModel,
488
- embeddingDimensions: row.embeddingDimensions,
489
- supersededBy: row.supersededBy,
490
- supersededAt: toNullableDate(row.supersededAt),
491
- expiresAt: toNullableDate(row.expiresAt),
492
- lastAccessedAt: toNullableDate(row.lastAccessedAt),
493
- accessCount: row.accessCount,
494
- createdAt: toDate(row.createdAt),
495
- updatedAt: toDate(row.updatedAt)
496
- };
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");
497
646
  }
498
647
  async function embedOne(embedder, text, options = {}) {
499
648
  let embeddings;
@@ -508,7 +657,7 @@ async function embedOne(embedder, text, options = {}) {
508
657
  }
509
658
  const embedding = embeddings[0];
510
659
  if (!embedding) throw new MnemocyteError("Embedder returned no embedding.", "EMBEDDING");
511
- if (embedding.length !== embedder.dimensions) throw new MnemocyteError("Embedder returned an embedding with unexpected dimensions.", "EMBEDDING");
660
+ validateEmbedding(embedding, embedder.dimensions);
512
661
  return embedding;
513
662
  }
514
663
  async function embedMany(embedder, texts, options = {}) {
@@ -524,12 +673,56 @@ async function embedMany(embedder, texts, options = {}) {
524
673
  throw new MnemocyteError("Failed to embed texts.", "EMBEDDING", error);
525
674
  }
526
675
  if (embeddings.length !== texts.length) throw new MnemocyteError(`Embedder returned ${embeddings.length} embeddings for ${texts.length} texts.`, "EMBEDDING");
527
- 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);
528
677
  return embeddings;
529
678
  }
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);
683
+ }
684
+ function assertLimit(value) {
685
+ if (!Number.isInteger(value) || value < 1) throw new MnemocyteError("limit must be a positive integer.", "VALIDATION");
686
+ }
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
+ }
530
722
  function validateRememberInput(input) {
531
723
  assertNonEmptyString(input.entityId, "entityId");
532
724
  assertNonEmptyString(input.content, "content");
725
+ if (input.metadata !== void 0) validateJsonObject(input.metadata);
533
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");
534
727
  }
535
728
  function validateRecallInput(input) {
@@ -560,27 +753,6 @@ function validatePruneInput(input) {
560
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");
561
754
  }
562
755
  /**
563
- * Test whether `memory` matches a {@link PruneInput}. All specified
564
- * selectors are AND-combined; unspecified selectors do not restrict.
565
- *
566
- * `now` is supplied by the caller so callers can share a consistent
567
- * timestamp across a batch.
568
- */
569
- function matchesPruneFilter(memory, input, now) {
570
- if (input.entityId !== void 0 && memory.entityId !== input.entityId) return false;
571
- if (input.expired === true && !isExpired(memory, now)) return false;
572
- if (input.superseded === true && memory.supersededBy === null) return false;
573
- if (input.createdBefore !== void 0 && memory.createdAt.getTime() >= input.createdBefore.getTime()) return false;
574
- if (input.notAccessedSince !== void 0) {
575
- const last = memory.lastAccessedAt;
576
- if (last !== null && last.getTime() >= input.notAccessedSince.getTime()) return false;
577
- }
578
- if (input.types !== void 0 && !input.types.includes(memory.type)) return false;
579
- if (input.tags !== void 0 && input.tags.length > 0 && !input.tags.every((tag) => memory.tags.includes(tag))) return false;
580
- if (input.maxImportance !== void 0 && IMPORTANCE_RANK[memory.importance] > IMPORTANCE_RANK[input.maxImportance]) return false;
581
- return true;
582
- }
583
- /**
584
756
  * Validate a {@link FindDuplicatesInput} and throw a `"VALIDATION"`
585
757
  * {@link MnemocyteError} for malformed `threshold` / `limit` values.
586
758
  */
@@ -592,18 +764,6 @@ function validateFindDuplicatesInput(input) {
592
764
  if (input.limit !== void 0) assertLimit(input.limit);
593
765
  }
594
766
  /**
595
- * Return `true` when a memory should participate in a duplicate scan,
596
- * given the filters on `input` and a shared `now` timestamp.
597
- */
598
- function matchesDuplicateFilter(memory, input, now) {
599
- if (memory.entityId !== input.entityId) return false;
600
- if (input.includeSuperseded !== true && memory.supersededBy !== null) return false;
601
- if (input.includeExpired !== true && isExpired(memory, now)) return false;
602
- if (input.types !== void 0 && !input.types.includes(memory.type)) return false;
603
- if (input.tags !== void 0 && input.tags.length > 0 && !input.tags.every((tag) => memory.tags.includes(tag))) return false;
604
- return true;
605
- }
606
- /**
607
767
  * Validate a {@link ConsolidateInput} and throw a `"VALIDATION"`
608
768
  * {@link MnemocyteError} for malformed inputs (empty `supersededIds`
609
769
  * or a `survivorId` appearing in `supersededIds`).
@@ -623,274 +783,178 @@ function validateListAuditLogInput(input) {
623
783
  assertNonEmptyString(input.entityId, "entityId");
624
784
  if (input.limit !== void 0) assertLimit(input.limit);
625
785
  }
626
- function matchesRecallFilter(memory, input, now) {
627
- if (memory.entityId !== input.entityId) return false;
628
- if (!input.includeSuperseded && memory.supersededBy !== null) return false;
629
- if (!input.includeExpired && isExpired(memory, now)) return false;
630
- if (input.types && !input.types.includes(memory.type)) return false;
631
- if (input.tags && !input.tags.every((tag) => memory.tags.includes(tag))) return false;
632
- if (input.before && memory.createdAt >= input.before) return false;
633
- if (input.after && memory.createdAt <= input.after) return false;
634
- return true;
635
- }
636
786
  //#endregion
637
- //#region src/retrieval/scorer.ts
638
- const IMPORTANCE_SCORES = {
639
- low: 0,
640
- normal: .5,
641
- high: .8,
642
- critical: 1
643
- };
644
- const DEFAULT_WEIGHTS = {
645
- vector: .55,
646
- lexical: .2,
647
- recency: .1,
648
- confidence: .05,
649
- access: .05,
650
- importance: .05
651
- };
652
- const DEFAULT_RECENCY_HALF_LIFE_DAYS = 90;
653
- const DEFAULT_ACCESS_SATURATION = 10;
654
- function cosineSimilarity(a, b) {
655
- let dot = 0;
656
- let aMagnitude = 0;
657
- let bMagnitude = 0;
658
- for (let i = 0; i < a.length; i += 1) {
659
- const av = a[i] ?? 0;
660
- const bv = b[i] ?? 0;
661
- dot += av * bv;
662
- aMagnitude += av * av;
663
- bMagnitude += bv * bv;
664
- }
665
- if (aMagnitude === 0 || bMagnitude === 0) return 0;
666
- return dot / (Math.sqrt(aMagnitude) * Math.sqrt(bMagnitude));
667
- }
668
- function tokenizeQuery(query) {
669
- return query.toLowerCase().split(/\s+/).filter(Boolean);
787
+ //#region src/memory/client-core.ts
788
+ function isPositionalRememberManyInput(input) {
789
+ return Array.isArray(input);
670
790
  }
671
- function lexicalScoreWithTerms(content, terms) {
672
- if (terms.length === 0) return 0;
673
- const lowerContent = content.toLowerCase();
674
- return terms.filter((term) => lowerContent.includes(term)).length / terms.length;
675
- }
676
- function createLexicalScorer(query) {
677
- const terms = tokenizeQuery(query);
678
- return (content) => lexicalScoreWithTerms(content, terms);
679
- }
680
- function clampScore(value) {
681
- if (!Number.isFinite(value)) return 0;
682
- return Math.max(0, Math.min(1, value));
683
- }
684
- function normalizeWeights(weights) {
685
- const merged = {
686
- ...DEFAULT_WEIGHTS,
687
- ...weights
688
- };
689
- const total = merged.vector + merged.lexical + merged.recency + merged.confidence + merged.access + merged.importance;
690
- if (total <= 0) return DEFAULT_WEIGHTS;
791
+ function providerOptions(config, signal) {
691
792
  return {
692
- vector: merged.vector / total,
693
- lexical: merged.lexical / total,
694
- recency: merged.recency / total,
695
- confidence: merged.confidence / total,
696
- access: merged.access / total,
697
- importance: merged.importance / total
793
+ ...signal === void 0 ? {} : { signal },
794
+ ...config.provider === void 0 ? {} : { resilience: config.provider }
698
795
  };
699
796
  }
700
- function createScoringConfig(config) {
797
+ function createStoredMemory(config, input, embedding, now) {
701
798
  return {
702
- weights: normalizeWeights(config?.weights),
703
- recencyHalfLifeDays: config?.recencyHalfLifeDays ?? DEFAULT_RECENCY_HALF_LIFE_DAYS,
704
- 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
705
818
  };
706
819
  }
707
- function recencyScore(createdAt, halfLifeDays) {
708
- const ageMs = Date.now() - createdAt.getTime();
709
- const ageDays = Math.max(0, ageMs / 864e5);
710
- return Math.exp(-Math.LN2 * ageDays / halfLifeDays);
711
- }
712
- function accessScore(accessCount, saturation) {
713
- if (accessCount <= 0) return 0;
714
- 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
+ };
715
828
  }
716
- function toScoredMemoryWithConfig(memory, vector, lexical, input, config) {
717
- const { weights } = config;
718
- const recency = recencyScore(memory.createdAt, config.recencyHalfLifeDays);
719
- const confidence = clampScore(memory.confidence);
720
- const access = accessScore(memory.accessCount, config.accessSaturation);
721
- const importance = IMPORTANCE_SCORES[memory.importance];
722
- 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) {
723
830
  return {
724
- ...cloneMemory(memory),
725
- score,
726
- scores: {
727
- vector: clampScore(vector),
728
- lexical: clampScore(lexical),
729
- recency,
730
- confidence,
731
- access,
732
- importance
733
- },
734
- explanation: input.explain ? {
735
- vectorScore: clampScore(vector),
736
- lexicalScore: clampScore(lexical),
737
- recencyScore: recency,
738
- confidenceScore: confidence,
739
- accessScore: access,
740
- importanceScore: importance,
741
- importanceBoost: importance * weights.importance,
742
- weights,
743
- finalScore: score
744
- } : null
831
+ a: cloneMemory(pair.a),
832
+ b: cloneMemory(pair.b),
833
+ similarity: Math.max(0, Math.min(1, pair.similarity))
745
834
  };
746
835
  }
747
- //#endregion
748
- //#region src/memory/in-memory.ts
749
- function createInMemoryClient(config) {
750
- const memories = /* @__PURE__ */ new Map();
751
- const auditLog = [];
836
+ function createMemoryClient(config, store) {
752
837
  let closed = false;
753
838
  function assertOpen() {
754
839
  if (closed) throw new MnemocyteError("Mnemocyte client is closed.", "DB");
755
840
  }
756
- function recordAudit(entityId, description, metadata) {
757
- if (config.audit?.enabled !== true) return;
758
- auditLog.push({
759
- id: createEventId(),
760
- entityId,
761
- description,
762
- metadata,
763
- timestamp: /* @__PURE__ */ new Date()
764
- });
841
+ async function ensureSchema() {
842
+ await store.ensureSchema();
765
843
  }
766
- function cloneAuditEvent(event) {
767
- return {
768
- id: event.id,
769
- entityId: event.entityId,
770
- description: event.description,
771
- metadata: { ...event.metadata },
772
- timestamp: new Date(event.timestamp)
773
- };
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 {}
774
852
  }
775
853
  async function remember(input) {
776
- 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 () => {
777
859
  assertOpen();
778
- validateRememberInput(input);
779
- const embedding = await embedOne(config.embedder, input.content, {
780
- ...input.signal === void 0 ? {} : { signal: input.signal },
781
- ...config.provider === void 0 ? {} : { resilience: config.provider }
782
- });
783
- const now = /* @__PURE__ */ new Date();
784
- const memory = {
785
- id: createId(),
786
- entityId: input.entityId,
787
- content: input.content,
788
- type: input.type ?? "fact",
789
- importance: input.importance ?? "normal",
790
- tags: normalizeTags(input.tags),
791
- source: input.source ?? null,
792
- metadata: input.metadata ?? {},
793
- confidence: input.confidence ?? 1,
794
- embeddingModel: config.embedder.model,
795
- embeddingDimensions: config.embedder.dimensions,
796
- supersededBy: null,
797
- supersededAt: null,
798
- expiresAt: input.expiresAt ?? null,
799
- lastAccessedAt: null,
800
- accessCount: 0,
801
- createdAt: now,
802
- updatedAt: now,
803
- embedding
804
- };
805
- memories.set(memory.id, memory);
806
- 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", {
807
866
  memoryId: memory.id,
808
867
  type: memory.type,
809
868
  importance: memory.importance
810
- });
869
+ })]);
811
870
  return cloneMemory(memory);
812
871
  }, (memory) => ({
813
872
  memoryId: memory.id,
814
873
  count: 1
815
874
  }));
816
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
+ }
817
901
  const client = {
818
902
  remember,
819
- async rememberMany(inputs) {
820
- return observe(config, "in-memory", "rememberMany", { count: inputs.length }, async () => {
821
- assertOpen();
822
- if (inputs.length === 0) return [];
823
- for (const input of inputs) validateRememberInput(input);
824
- const embeddings = await embedMany(config.embedder, inputs.map((i) => i.content), {
825
- ...inputs[0]?.signal === void 0 ? {} : { signal: inputs[0].signal },
826
- ...config.provider === void 0 ? {} : { resilience: config.provider }
827
- });
828
- const now = /* @__PURE__ */ new Date();
829
- const result = [];
830
- for (let i = 0; i < inputs.length; i++) {
831
- const input = inputs[i];
832
- const memory = {
833
- id: createId(),
834
- entityId: input.entityId,
835
- content: input.content,
836
- type: input.type ?? "fact",
837
- importance: input.importance ?? "normal",
838
- tags: normalizeTags(input.tags),
839
- source: input.source ?? null,
840
- metadata: input.metadata ?? {},
841
- confidence: input.confidence ?? 1,
842
- embeddingModel: config.embedder.model,
843
- embeddingDimensions: config.embedder.dimensions,
844
- supersededBy: null,
845
- supersededAt: null,
846
- expiresAt: input.expiresAt ?? null,
847
- lastAccessedAt: null,
848
- accessCount: 0,
849
- createdAt: now,
850
- updatedAt: now,
851
- embedding: embeddings[i]
852
- };
853
- memories.set(memory.id, memory);
854
- recordAudit(memory.entityId, "memory.created", {
855
- memoryId: memory.id,
856
- type: memory.type,
857
- importance: memory.importance
858
- });
859
- result.push(cloneMemory(memory));
860
- }
861
- return result;
862
- }, (result) => ({ count: result.length }));
863
- },
903
+ rememberMany,
864
904
  async recall(input) {
865
- return observe(config, "in-memory", "recall", { entityId: input.entityId }, async () => {
905
+ return observe(config, store.backend, "recall", { entityId: input.entityId }, async () => {
866
906
  assertOpen();
867
907
  validateRecallInput(input);
868
908
  const limit = input.limit ?? config.defaults?.limit ?? 10;
869
909
  const minScore = input.minScore ?? config.defaults?.minScore ?? 0;
870
910
  assertLimit(limit);
871
911
  assertMinScore(minScore);
872
- const queryEmbedding = await embedOne(config.embedder, input.query, {
873
- ...input.signal === void 0 ? {} : { signal: input.signal },
874
- ...config.provider === void 0 ? {} : { resilience: config.provider }
875
- });
876
- 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);
877
916
  const scoreLexical = createLexicalScorer(input.query);
878
917
  const scoringConfig = createScoringConfig(config.retrieval);
879
- 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);
880
- for (const memory of scored) {
881
- const stored = memories.get(memory.id);
882
- if (stored) {
883
- stored.lastAccessedAt = now;
884
- stored.accessCount += 1;
885
- 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;
886
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
+ });
887
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));
888
951
  return scored;
889
952
  }, (result) => ({ count: result.length }));
890
953
  },
891
954
  async buildContext(input) {
892
- return observe(config, "in-memory", "buildContext", { entityId: input.entityId }, async () => {
955
+ return observe(config, store.backend, "buildContext", { entityId: input.entityId }, async () => {
893
956
  assertOpen();
957
+ validateBuildContextInput(input);
894
958
  return buildContext({
895
959
  input,
896
960
  recall: (contextInput) => {
@@ -904,178 +968,375 @@ function createInMemoryClient(config) {
904
968
  });
905
969
  },
906
970
  async forget(input) {
907
- return observe(config, "in-memory", "forget", {
971
+ return observe(config, store.backend, "forget", {
908
972
  entityId: input.entityId,
909
973
  memoryId: input.memoryId
910
974
  }, async () => {
911
975
  assertOpen();
912
976
  assertNonEmptyString(input.entityId, "entityId");
913
977
  assertNonEmptyString(input.memoryId, "memoryId");
914
- const memory = memories.get(input.memoryId);
915
- if (!memory || memory.entityId !== input.entityId) throw new MnemocyteError("Memory was not found.", "NOT_FOUND");
916
- memories.delete(input.memoryId);
917
- 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 })]);
918
981
  });
919
982
  },
920
983
  async forgetAll(input) {
921
- return observe(config, "in-memory", "forgetAll", { entityId: input.entityId }, async () => {
984
+ return observe(config, store.backend, "forgetAll", { entityId: input.entityId }, async () => {
922
985
  assertOpen();
923
986
  assertNonEmptyString(input.entityId, "entityId");
924
- let deleted = 0;
925
- for (const [id, memory] of memories) if (memory.entityId === input.entityId) {
926
- memories.delete(id);
927
- deleted += 1;
928
- }
929
- recordAudit(input.entityId, "entity.cleared", { count: deleted });
930
- 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;
931
991
  }, (count) => ({ count })).then(() => void 0);
932
992
  },
933
993
  async prune(input) {
934
- 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 () => {
935
995
  assertOpen();
936
996
  validatePruneInput(input);
937
- const now = /* @__PURE__ */ new Date();
938
- const matched = [];
939
- for (const memory of memories.values()) if (matchesPruneFilter(memory, input, now)) matched.push(memory.id);
940
- const dryRun = input.dryRun === true;
941
- if (!dryRun) {
942
- for (const id of matched) memories.delete(id);
943
- if (matched.length > 0 && input.entityId !== void 0) recordAudit(input.entityId, "memory.pruned", { count: matched.length });
944
- }
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
+ });
945
1061
  return {
946
- matchedCount: matched.length,
947
- deletedCount: dryRun ? 0 : matched.length,
948
- dryRun
1062
+ survivorId: survivor.id,
1063
+ supersededCount: result.supersededIds.length,
1064
+ supersededIds: result.supersededIds
949
1065
  };
950
- }, (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;
951
1207
  },
952
- async findDuplicates(input) {
953
- return observe(config, "in-memory", "findDuplicates", { entityId: input.entityId }, async () => {
954
- assertOpen();
955
- validateFindDuplicatesInput(input);
956
- const threshold = input.threshold ?? .95;
957
- const limit = input.limit ?? 100;
958
- const now = /* @__PURE__ */ new Date();
959
- const candidates = Array.from(memories.values()).filter((memory) => matchesDuplicateFilter(memory, input, now));
960
- const pairs = [];
961
- for (let i = 0; i < candidates.length; i += 1) {
962
- const a = candidates[i];
963
- if (a === void 0) continue;
964
- for (let j = i + 1; j < candidates.length; j += 1) {
965
- const b = candidates[j];
966
- if (b === void 0) continue;
967
- const similarity = Math.max(0, Math.min(1, cosineSimilarity(a.embedding, b.embedding)));
968
- if (similarity >= threshold) pairs.push({
969
- a: cloneMemory(a),
970
- b: cloneMemory(b),
971
- similarity
972
- });
973
- }
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
+ });
974
1238
  }
975
- pairs.sort((x, y) => y.similarity - x.similarity);
976
- return pairs.slice(0, limit);
977
- }, (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));
978
1245
  },
979
1246
  async listAuditLog(input) {
980
- return observe(config, "in-memory", "listAuditLog", { entityId: input.entityId }, async () => {
981
- assertOpen();
982
- validateListAuditLogInput(input);
983
- const limit = input.limit ?? 50;
984
- return auditLog.map((event, idx) => ({
985
- event,
986
- idx
987
- })).filter(({ event }) => {
988
- if (event.entityId !== input.entityId) return false;
989
- if (input.before !== void 0 && event.timestamp.getTime() >= input.before.getTime()) return false;
990
- if (input.after !== void 0 && event.timestamp.getTime() <= input.after.getTime()) return false;
991
- return true;
992
- }).sort((a, b) => {
993
- const dt = b.event.timestamp.getTime() - a.event.timestamp.getTime();
994
- if (dt !== 0) return dt;
995
- return b.idx - a.idx;
996
- }).slice(0, limit).map(({ event }) => cloneAuditEvent(event));
997
- }, (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));
998
1260
  },
999
- async stats(input) {
1000
- return observe(config, "in-memory", "stats", input?.entityId ? { entityId: input.entityId } : {}, async () => {
1001
- assertOpen();
1002
- const now = /* @__PURE__ */ new Date();
1003
- const allMemories = Array.from(memories.values());
1004
- const selected = input?.entityId ? allMemories.filter((memory) => memory.entityId === input.entityId) : allMemories;
1005
- const activeMemoryCount = selected.filter((memory) => !isExpired(memory, now) && memory.supersededBy === null).length;
1006
- const expiredMemoryCount = selected.filter((memory) => isExpired(memory, now)).length;
1007
- const supersededMemoryCount = selected.filter((memory) => memory.supersededBy !== null).length;
1008
- if (input?.entityId) return {
1009
- entityId: input.entityId,
1010
- memoryCount: selected.length,
1011
- activeMemoryCount,
1012
- expiredMemoryCount,
1013
- supersededMemoryCount
1014
- };
1015
- return {
1016
- entityCount: new Set(allMemories.map((memory) => memory.entityId)).size,
1017
- memoryCount: selected.length,
1018
- activeMemoryCount,
1019
- expiredMemoryCount,
1020
- supersededMemoryCount
1021
- };
1022
- });
1261
+ async getMemory(entityId, memoryId) {
1262
+ const memory = memories.get(memoryId);
1263
+ return memory && memory.entityId === entityId ? cloneMemory(memory) : null;
1023
1264
  },
1024
- experimental: createExperimental(),
1025
- async close() {
1026
- return observe(config, "in-memory", "close", {}, async () => {
1027
- closed = true;
1028
- 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
+ }];
1029
1274
  });
1030
- }
1031
- };
1032
- return client;
1033
- function createExperimental() {
1034
- return { async consolidate(input) {
1035
- return observe(config, "in-memory", "consolidate", {
1036
- entityId: input.entityId,
1037
- memoryId: input.survivorId
1038
- }, async () => {
1039
- assertOpen();
1040
- validateConsolidateInput(input);
1041
- const survivor = memories.get(input.survivorId);
1042
- if (!survivor || survivor.entityId !== input.entityId) throw new MnemocyteError("Survivor memory was not found.", "NOT_FOUND");
1043
- if (survivor.supersededBy !== null) throw new MnemocyteError("Survivor memory is itself superseded.", "VALIDATION");
1044
- const losers = [];
1045
- 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) {
1046
1291
  const memory = memories.get(id);
1047
- if (!memory || memory.entityId !== input.entityId) throw new MnemocyteError("Superseded memory was not found.", "NOT_FOUND");
1048
- if (memory.supersededBy !== null) continue;
1049
- losers.push(memory);
1050
- }
1051
- const now = /* @__PURE__ */ new Date();
1052
- const newSupersededIds = [];
1053
- for (const loser of losers) {
1054
- loser.supersededBy = survivor.id;
1055
- loser.supersededAt = now;
1056
- loser.updatedAt = now;
1057
- newSupersededIds.push(loser.id);
1058
- recordAudit(input.entityId, "memory.superseded", {
1059
- memoryId: loser.id,
1060
- supersededBy: survivor.id
1061
- });
1292
+ if (memory) for (const tag of memory.tags) mergedTags.add(tag);
1062
1293
  }
1063
- if (input.mergeTags !== false && losers.length > 0) {
1064
- const mergedTags = new Set(survivor.tags);
1065
- for (const loser of losers) for (const tag of loser.tags) mergedTags.add(tag);
1066
- if (mergedTags.size !== survivor.tags.length) {
1067
- survivor.tags = [...mergedTags];
1068
- survivor.updatedAt = now;
1069
- }
1294
+ if (mergedTags.size !== survivor.tags.length) {
1295
+ survivor.tags = [...mergedTags];
1296
+ survivor.updatedAt = input.now;
1070
1297
  }
1071
- return {
1072
- survivorId: survivor.id,
1073
- supersededCount: newSupersededIds.length,
1074
- supersededIds: newSupersededIds
1075
- };
1076
- }, (result) => ({ count: result.supersededCount }));
1077
- } };
1078
- }
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());
1079
1340
  }
1080
1341
  //#endregion
1081
1342
  //#region src/db/queries/events.ts
@@ -1092,10 +1353,6 @@ async function listEvents(db, filter) {
1092
1353
  function vectorLiteral(embedding) {
1093
1354
  return `[${embedding.map(formatVectorComponent).join(",")}]`;
1094
1355
  }
1095
- function formatVectorComponent(value) {
1096
- if (!Number.isFinite(value)) throw new Error("Vector values must be finite numbers.");
1097
- return Object.is(value, -0) ? "0" : value.toFixed(17);
1098
- }
1099
1356
  function timestampParam(value) {
1100
1357
  return sql`${value.toISOString()}::timestamptz`;
1101
1358
  }
@@ -1137,11 +1394,6 @@ function emptyMemoryStatsCounts() {
1137
1394
  supersededMemoryCount: 0
1138
1395
  };
1139
1396
  }
1140
- async function insertMemory(db, row) {
1141
- const inserted = (await db.insert(memoriesTable).values(row).returning())[0];
1142
- if (!inserted) throw new MnemocyteError("Memory insert returned no rows.", "DB");
1143
- return inserted;
1144
- }
1145
1397
  async function insertMemories(db, rows) {
1146
1398
  if (rows.length === 0) return [];
1147
1399
  return db.insert(memoriesTable).values(rows).returning();
@@ -1306,7 +1558,7 @@ async function setMemoryTags(db, params) {
1306
1558
  updatedAt: params.now
1307
1559
  }).where(and(eq(memoriesTable.entityId, params.entityId), eq(memoriesTable.id, params.memoryId)));
1308
1560
  }
1309
- async function markMemoryAccessed(db, memoryIds) {
1561
+ async function markMemoriesAccessed(db, memoryIds) {
1310
1562
  const now = /* @__PURE__ */ new Date();
1311
1563
  if (memoryIds.length === 0) return;
1312
1564
  await db.update(memoriesTable).set({
@@ -1402,73 +1654,80 @@ async function getInstallationMeta(db) {
1402
1654
  return (await db.select().from(metaTable).where(eq(metaTable.key, "installation")).limit(1))[0] ?? null;
1403
1655
  }
1404
1656
  //#endregion
1405
- //#region src/retrieval/index.ts
1406
- async function hybridRecall(input) {
1407
- const queryEmbedding = await embedOne(input.embedder, input.input.query, {
1408
- ...input.signal === void 0 ? {} : { signal: input.signal },
1409
- ...input.resilience === void 0 ? {} : { resilience: input.resilience }
1410
- });
1411
- const candidateMultiplier = input.retrieval?.candidateMultiplier ?? 3;
1412
- const candidateLimit = Math.max(input.limit, input.limit * candidateMultiplier);
1413
- const scoreLexical = createLexicalScorer(input.input.query);
1414
- const scoringConfig = createScoringConfig(input.retrieval);
1415
- const [vectorRows, lexicalRows] = await Promise.all([vectorSearch(input.db, {
1416
- ...input.input,
1417
- embedding: queryEmbedding,
1418
- limit: candidateLimit,
1419
- minScore: 0
1420
- }), lexicalSearch(input.db, {
1421
- ...input.input,
1422
- limit: candidateLimit
1423
- })]);
1424
- const merged = /* @__PURE__ */ new Map();
1425
- for (const row of vectorRows) merged.set(row.id, {
1426
- row,
1427
- vectorScore: row.vectorScore,
1428
- lexicalScore: scoreLexical(row.content)
1429
- });
1430
- const lexicalOnlyRows = lexicalRows.filter((row) => !merged.has(row.id));
1431
- const lexicalOnlyEmbeddings = await getMemoryEmbeddings(input.db, lexicalOnlyRows.map((row) => row.id));
1432
- for (const row of lexicalRows) {
1433
- const existing = merged.get(row.id);
1434
- if (existing) existing.lexicalScore = row.lexicalScore;
1435
- else {
1436
- const embedding = lexicalOnlyEmbeddings.get(row.id);
1437
- const sim = embedding ? cosineSimilarity(embedding, queryEmbedding) : 0;
1438
- merged.set(row.id, {
1439
- row,
1440
- vectorScore: Math.max(0, sim),
1441
- lexicalScore: row.lexicalScore
1442
- });
1443
- }
1444
- }
1445
- 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);
1446
- await markMemoryAccessed(input.db, scored.map((memory) => memory.id));
1447
- return scored;
1657
+ //#region src/memory/postgres-records.ts
1658
+ function toDate(value) {
1659
+ return value instanceof Date ? value : new Date(value);
1448
1660
  }
1449
- //#endregion
1450
- //#region src/memory/postgres.ts
1451
- function rowToAuditEvent(row) {
1661
+ function toNullableDate(value) {
1662
+ return value === null ? null : toDate(value);
1663
+ }
1664
+ function rowToMemory(row) {
1452
1665
  return {
1453
1666
  id: row.id,
1454
1667
  entityId: row.entityId,
1455
- description: row.description,
1456
- metadata: row.metadata,
1457
- 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)
1458
1684
  };
1459
1685
  }
1686
+ //#endregion
1687
+ //#region src/memory/postgres.ts
1460
1688
  const IMPORTANCE_LEVELS = [
1461
1689
  "low",
1462
1690
  "normal",
1463
1691
  "high",
1464
1692
  "critical"
1465
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
+ }
1466
1709
  function importanceCeilingLevels(max) {
1467
1710
  return IMPORTANCE_LEVELS.filter((level) => IMPORTANCE_RANK[level] <= IMPORTANCE_RANK[max]);
1468
1711
  }
1469
1712
  function hasPostgresErrorCode(error, code) {
1470
1713
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
1471
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
+ }
1472
1731
  function toPruneFilter(input) {
1473
1732
  const filter = {};
1474
1733
  if (input.entityId !== void 0) filter.entityId = input.entityId;
@@ -1481,195 +1740,76 @@ function toPruneFilter(input) {
1481
1740
  if (input.maxImportance !== void 0) filter.maxImportanceLevels = importanceCeilingLevels(input.maxImportance);
1482
1741
  return filter;
1483
1742
  }
1484
- function createPostgresClient(config, handle) {
1485
- let closed = false;
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) {
1486
1752
  let schemaValidated = false;
1487
1753
  let schemaValidationPromise;
1488
- function assertOpen() {
1489
- if (closed) throw new MnemocyteError("Mnemocyte client is closed.", "DB");
1490
- }
1491
- async function ensureSchemaMatchesEmbedder() {
1754
+ async function ensureEmbeddingCompatibility(embedder) {
1492
1755
  if (schemaValidated) return;
1493
- if (!schemaValidationPromise) schemaValidationPromise = validateSchemaMatchesEmbedder().catch((error) => {
1756
+ if (!schemaValidationPromise) schemaValidationPromise = validateEmbeddingCompatibility(embedder).catch((error) => {
1494
1757
  schemaValidationPromise = void 0;
1495
1758
  throw error;
1496
1759
  });
1497
1760
  await schemaValidationPromise;
1498
1761
  }
1499
- async function validateSchemaMatchesEmbedder() {
1762
+ async function validateEmbeddingCompatibility(embedder) {
1500
1763
  let meta;
1501
1764
  try {
1502
1765
  meta = await getInstallationMeta(handle.db);
1503
1766
  } catch (error) {
1504
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);
1505
- throw error;
1768
+ throw normalizePostgresError(error);
1506
1769
  }
1507
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");
1508
- if (meta.embeddingDimensions !== config.embedder.dimensions) throw new MnemocyteError(`embedder.dimensions (${config.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");
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");
1509
1772
  schemaValidated = true;
1510
1773
  }
1511
- async function recordAudit(entityId, description, metadata) {
1512
- if (config.audit?.enabled !== true) return;
1513
- try {
1514
- await insertEvent(handle.db, {
1515
- id: createEventId(),
1516
- entityId,
1517
- description,
1518
- metadata,
1519
- timestamp: /* @__PURE__ */ new Date()
1520
- });
1521
- } catch {}
1522
- }
1523
- async function remember(input) {
1524
- return observe(config, "postgres", "remember", { entityId: input.entityId }, async () => {
1525
- assertOpen();
1526
- validateRememberInput(input);
1527
- await ensureSchemaMatchesEmbedder();
1528
- const embedding = await embedOne(config.embedder, input.content, {
1529
- ...input.signal === void 0 ? {} : { signal: input.signal },
1530
- ...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);
1531
1781
  });
1532
- const now = /* @__PURE__ */ new Date();
1533
- const memory = rowToMemory(await insertMemory(handle.db, {
1534
- id: createId(),
1535
- entityId: input.entityId,
1536
- content: input.content,
1537
- type: input.type ?? "fact",
1538
- importance: input.importance ?? "normal",
1539
- tags: normalizeTags(input.tags),
1540
- source: input.source ?? null,
1541
- metadata: input.metadata ?? {},
1542
- confidence: input.confidence ?? 1,
1543
- embedding,
1544
- embeddingModel: config.embedder.model,
1545
- embeddingDimensions: config.embedder.dimensions,
1546
- supersededBy: null,
1547
- supersededAt: null,
1548
- expiresAt: input.expiresAt ?? null,
1549
- lastAccessedAt: null,
1550
- accessCount: 0,
1551
- createdAt: now,
1552
- updatedAt: now
1553
- }));
1554
- await recordAudit(memory.entityId, "memory.created", {
1555
- memoryId: memory.id,
1556
- type: memory.type,
1557
- 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
+ }));
1558
1789
  });
1559
- return memory;
1560
- }, (memory) => ({
1561
- memoryId: memory.id,
1562
- count: 1
1563
- }));
1564
- }
1565
- const client = {
1566
- remember,
1567
- async rememberMany(inputs) {
1568
- return observe(config, "postgres", "rememberMany", { count: inputs.length }, async () => {
1569
- assertOpen();
1570
- if (inputs.length === 0) return [];
1571
- for (const input of inputs) validateRememberInput(input);
1572
- await ensureSchemaMatchesEmbedder();
1573
- const embeddings = await embedMany(config.embedder, inputs.map((i) => i.content), {
1574
- ...inputs[0]?.signal === void 0 ? {} : { signal: inputs[0].signal },
1575
- ...config.provider === void 0 ? {} : { resilience: config.provider }
1576
- });
1577
- const now = /* @__PURE__ */ new Date();
1578
- const rows = inputs.map((input, idx) => ({
1579
- id: createId(),
1580
- entityId: input.entityId,
1581
- content: input.content,
1582
- type: input.type ?? "fact",
1583
- importance: input.importance ?? "normal",
1584
- tags: normalizeTags(input.tags),
1585
- source: input.source ?? null,
1586
- metadata: input.metadata ?? {},
1587
- confidence: input.confidence ?? 1,
1588
- embedding: embeddings[idx],
1589
- embeddingModel: config.embedder.model,
1590
- embeddingDimensions: config.embedder.dimensions,
1591
- supersededBy: null,
1592
- supersededAt: null,
1593
- expiresAt: input.expiresAt ?? null,
1594
- lastAccessedAt: null,
1595
- accessCount: 0,
1596
- createdAt: now,
1597
- 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
1598
1796
  }));
1599
- const memories = (await insertMemories(handle.db, rows)).map(rowToMemory);
1600
- for (const memory of memories) recordAudit(memory.entityId, "memory.created", {
1601
- memoryId: memory.id,
1602
- type: memory.type,
1603
- importance: memory.importance
1604
- });
1605
- return memories;
1606
- }, (result) => ({ count: result.length }));
1797
+ });
1607
1798
  },
1608
- async recall(input) {
1609
- return observe(config, "postgres", "recall", { entityId: input.entityId }, async () => {
1610
- assertOpen();
1611
- validateRecallInput(input);
1612
- const limit = input.limit ?? config.defaults?.limit ?? 10;
1613
- const minScore = input.minScore ?? config.defaults?.minScore ?? 0;
1614
- assertLimit(limit);
1615
- assertMinScore(minScore);
1616
- await ensureSchemaMatchesEmbedder();
1617
- return hybridRecall({
1618
- db: handle.db,
1619
- embedder: config.embedder,
1620
- input,
1621
- limit,
1622
- minScore,
1623
- retrieval: config.retrieval,
1624
- ...input.signal === void 0 ? {} : { signal: input.signal },
1625
- ...config.provider === void 0 ? {} : { resilience: config.provider }
1626
- });
1627
- }, (result) => ({ count: result.length }));
1799
+ async getMemoryEmbeddings(memoryIds) {
1800
+ return runPostgresOperation(() => getMemoryEmbeddings(handle.db, memoryIds));
1628
1801
  },
1629
- async buildContext(input) {
1630
- return observe(config, "postgres", "buildContext", { entityId: input.entityId }, async () => {
1631
- assertOpen();
1632
- await ensureSchemaMatchesEmbedder();
1633
- return buildContext({
1634
- input,
1635
- recall: (contextInput) => {
1636
- const recallInput = contextInputToRecallInput(contextInput);
1637
- return client.recall(input.signal === void 0 ? recallInput : {
1638
- ...recallInput,
1639
- signal: input.signal
1640
- });
1641
- }
1642
- });
1643
- });
1802
+ async markMemoriesAccessed(memoryIds) {
1803
+ return runPostgresOperation(() => markMemoriesAccessed(handle.db, memoryIds));
1644
1804
  },
1645
- async forget(input) {
1646
- return observe(config, "postgres", "forget", {
1647
- entityId: input.entityId,
1648
- memoryId: input.memoryId
1649
- }, async () => {
1650
- assertOpen();
1651
- assertNonEmptyString(input.entityId, "entityId");
1652
- assertNonEmptyString(input.memoryId, "memoryId");
1653
- await ensureSchemaMatchesEmbedder();
1654
- if (!await deleteMemory(handle.db, input.entityId, input.memoryId)) throw new MnemocyteError("Memory was not found.", "NOT_FOUND");
1655
- await recordAudit(input.entityId, "memory.deleted", { memoryId: input.memoryId });
1656
- });
1805
+ async deleteMemory(entityId, memoryId) {
1806
+ return runPostgresOperation(() => deleteMemory(handle.db, entityId, memoryId));
1657
1807
  },
1658
- async forgetAll(input) {
1659
- return observe(config, "postgres", "forgetAll", { entityId: input.entityId }, async () => {
1660
- assertOpen();
1661
- assertNonEmptyString(input.entityId, "entityId");
1662
- await ensureSchemaMatchesEmbedder();
1663
- const deletedCount = await deleteMemoriesForEntity(handle.db, input.entityId);
1664
- await recordAudit(input.entityId, "entity.cleared", { count: deletedCount });
1665
- return deletedCount;
1666
- }, (count) => ({ count })).then(() => void 0);
1808
+ async deleteMemoriesForEntity(entityId) {
1809
+ return runPostgresOperation(() => deleteMemoriesForEntity(handle.db, entityId));
1667
1810
  },
1668
1811
  async prune(input) {
1669
- return observe(config, "postgres", "prune", input.entityId === void 0 ? {} : { entityId: input.entityId }, async () => {
1670
- assertOpen();
1671
- validatePruneInput(input);
1672
- await ensureSchemaMatchesEmbedder();
1812
+ return runPostgresOperation(async () => {
1673
1813
  const filter = toPruneFilter(input);
1674
1814
  if (input.dryRun === true) return {
1675
1815
  matchedCount: await countPruneMatches(handle.db, filter),
@@ -1677,25 +1817,19 @@ function createPostgresClient(config, handle) {
1677
1817
  dryRun: true
1678
1818
  };
1679
1819
  const deletedCount = await pruneMemories(handle.db, filter);
1680
- if (deletedCount > 0 && input.entityId !== void 0) await recordAudit(input.entityId, "memory.pruned", { count: deletedCount });
1681
1820
  return {
1682
1821
  matchedCount: deletedCount,
1683
1822
  deletedCount,
1684
1823
  dryRun: false
1685
1824
  };
1686
- }, (result) => ({ count: result.deletedCount }));
1825
+ });
1687
1826
  },
1688
- async findDuplicates(input) {
1689
- return observe(config, "postgres", "findDuplicates", { entityId: input.entityId }, async () => {
1690
- assertOpen();
1691
- validateFindDuplicatesInput(input);
1692
- await ensureSchemaMatchesEmbedder();
1693
- const threshold = input.threshold ?? .95;
1694
- const limit = input.limit ?? 100;
1827
+ async findDuplicatePairs(input) {
1828
+ return runPostgresOperation(async () => {
1695
1829
  return (await findDuplicatePairs(handle.db, {
1696
1830
  entityId: input.entityId,
1697
- threshold,
1698
- limit,
1831
+ threshold: input.threshold ?? .95,
1832
+ limit: input.limit ?? 100,
1699
1833
  ...input.types === void 0 ? {} : { types: input.types },
1700
1834
  ...input.tags === void 0 ? {} : { tags: input.tags },
1701
1835
  ...input.includeSuperseded === void 0 ? {} : { includeSuperseded: input.includeSuperseded },
@@ -1705,108 +1839,91 @@ function createPostgresClient(config, handle) {
1705
1839
  b: rowToMemory(row.b),
1706
1840
  similarity: Math.max(0, Math.min(1, row.similarity))
1707
1841
  }));
1708
- }, (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
+ });
1709
1854
  },
1710
1855
  async listAuditLog(input) {
1711
- return observe(config, "postgres", "listAuditLog", { entityId: input.entityId }, async () => {
1712
- assertOpen();
1713
- validateListAuditLogInput(input);
1714
- await ensureSchemaMatchesEmbedder();
1856
+ return runPostgresOperation(async () => {
1715
1857
  return (await listEvents(handle.db, {
1716
1858
  entityId: input.entityId,
1717
1859
  limit: input.limit ?? 50,
1718
1860
  ...input.before === void 0 ? {} : { before: input.before },
1719
1861
  ...input.after === void 0 ? {} : { after: input.after }
1720
1862
  })).map(rowToAuditEvent);
1721
- }, (result) => ({ count: result.length }));
1722
- },
1723
- async stats(input) {
1724
- return observe(config, "postgres", "stats", input?.entityId ? { entityId: input.entityId } : {}, async () => {
1725
- assertOpen();
1726
- await ensureSchemaMatchesEmbedder();
1727
- const now = /* @__PURE__ */ new Date();
1728
- if (input?.entityId) {
1729
- const counts = await getEntityMemoryStatsCounts(handle.db, input.entityId, now);
1730
- return {
1731
- entityId: input.entityId,
1732
- ...counts
1733
- };
1734
- }
1735
- return await getGlobalMemoryStatsCounts(handle.db, now);
1736
1863
  });
1737
1864
  },
1738
- experimental: createExperimental(),
1739
- async close() {
1740
- return observe(config, "postgres", "close", {}, async () => {
1741
- closed = true;
1742
- 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;
1743
1869
  });
1744
- }
1745
- };
1746
- return client;
1747
- function createExperimental() {
1748
- return { async consolidate(input) {
1749
- return observe(config, "postgres", "consolidate", {
1750
- entityId: input.entityId,
1751
- memoryId: input.survivorId
1752
- }, async () => {
1753
- assertOpen();
1754
- validateConsolidateInput(input);
1755
- await ensureSchemaMatchesEmbedder();
1756
- const survivor = await getMemoryById(handle.db, input.entityId, input.survivorId);
1757
- if (!survivor) throw new MnemocyteError("Survivor memory was not found.", "NOT_FOUND");
1758
- if (survivor.supersededBy !== null) throw new MnemocyteError("Survivor memory is itself superseded.", "VALIDATION");
1759
- const targets = await loadConsolidationTargets(handle.db, input.entityId, input.supersededIds);
1760
- const foundIds = new Set(targets.map((t) => t.id));
1761
- for (const id of input.supersededIds) if (!foundIds.has(id)) throw new MnemocyteError("Superseded memory was not found.", "NOT_FOUND");
1762
- const losers = targets.filter((target) => target.supersededBy === null);
1763
- if (losers.length === 0) return {
1764
- survivorId: survivor.id,
1765
- supersededCount: 0,
1766
- supersededIds: []
1767
- };
1768
- const now = /* @__PURE__ */ new Date();
1769
- 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) => {
1770
1877
  const updated = await markMemoriesSuperseded(tx, {
1771
- survivorId: survivor.id,
1878
+ survivorId: input.survivorId,
1772
1879
  entityId: input.entityId,
1773
- ids: losers.map((loser) => loser.id),
1774
- now
1880
+ ids: input.supersededIds,
1881
+ now: input.now
1775
1882
  });
1776
- const newSupersededIds = updated.map((row) => row.id);
1777
- 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, {
1778
1885
  id: createEventId(),
1779
1886
  entityId: input.entityId,
1780
1887
  description: "memory.superseded",
1781
1888
  metadata: {
1782
1889
  memoryId: id,
1783
- supersededBy: survivor.id
1890
+ supersededBy: input.survivorId
1784
1891
  },
1785
- timestamp: now
1892
+ timestamp: input.now
1786
1893
  });
1787
- if (input.mergeTags !== false && updated.length > 0) {
1788
- const mergedTags = new Set(survivor.tags);
1894
+ if (input.mergeTags && updated.length > 0) {
1895
+ const mergedTags = new Set(input.survivorTags);
1789
1896
  for (const row of updated) for (const tag of row.tags) mergedTags.add(tag);
1790
- if (mergedTags.size !== survivor.tags.length) await setMemoryTags(tx, {
1897
+ if (mergedTags.size !== input.survivorTags.length) await setMemoryTags(tx, {
1791
1898
  entityId: input.entityId,
1792
- memoryId: survivor.id,
1899
+ memoryId: input.survivorId,
1793
1900
  tags: [...mergedTags],
1794
- now
1901
+ now: input.now
1795
1902
  });
1796
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);
1797
1912
  return {
1798
- updated,
1799
- newSupersededIds
1913
+ entityId: input.entityId,
1914
+ ...counts
1800
1915
  };
1801
- });
1802
- return {
1803
- survivorId: survivor.id,
1804
- supersededCount: newSupersededIds.length,
1805
- supersededIds: newSupersededIds
1806
- };
1807
- }, (result) => ({ count: result.supersededCount }));
1808
- } };
1809
- }
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));
1810
1927
  }
1811
1928
  //#endregion
1812
1929
  //#region src/client.ts
@@ -1824,8 +1941,9 @@ function createPostgresClient(config, handle) {
1824
1941
  *
1825
1942
  * @param config - Client configuration. {@link MnemocyteConfig.embedder} is required.
1826
1943
  * @returns A {@link MnemocyteClient} instance.
1827
- * @throws {MnemocyteError} With code `"CONFIG"` if `embedder` is missing or
1828
- * invalid, or `"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.
1829
1947
  *
1830
1948
  * @example Postgres-backed client
1831
1949
  * ```ts
@@ -1837,7 +1955,8 @@ function createPostgresClient(config, handle) {
1837
1955
  */
1838
1956
  function createMnemocyte(config) {
1839
1957
  assertEmbedder(config.embedder);
1840
- if (config.databaseUrl) {
1958
+ validateRetrievalConfig(config.retrieval);
1959
+ if (config.databaseUrl !== void 0) {
1841
1960
  assertNonEmptyString(config.databaseUrl, "databaseUrl");
1842
1961
  return createPostgresClient(config, createDatabase(config.databaseUrl));
1843
1962
  }