mnemocyte 0.0.7 → 0.1.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,6 +1,6 @@
1
1
  import { drizzle } from "drizzle-orm/postgres-js";
2
2
  import postgres from "postgres";
3
- import { and, eq, gt, inArray, isNull, lt, sql } from "drizzle-orm";
3
+ import { and, desc, eq, gt, inArray, isNotNull, isNull, lt, or, sql } from "drizzle-orm";
4
4
  import { customType, index, integer, jsonb, pgTable, real, text, timestamp } from "drizzle-orm/pg-core";
5
5
  //#region \0rolldown/runtime.js
6
6
  var __defProp = Object.defineProperty;
@@ -74,6 +74,56 @@ function createDatabase(databaseUrl) {
74
74
  };
75
75
  }
76
76
  //#endregion
77
+ //#region src/errors.ts
78
+ /**
79
+ * Error type thrown by every public Mnemocyte API.
80
+ *
81
+ * Use the {@link MnemocyteError.code} property — not the message — to branch
82
+ * on failure modes, or call {@link isMnemocyteError} as a type guard when
83
+ * working with `unknown` errors.
84
+ *
85
+ * @example
86
+ * ```ts
87
+ * try {
88
+ * await client.recall({ entityId: "u1", query: "hi" });
89
+ * } catch (err) {
90
+ * if (isMnemocyteError(err) && err.code === "DB") {
91
+ * // handle database failure
92
+ * }
93
+ * throw err;
94
+ * }
95
+ * ```
96
+ */
97
+ var MnemocyteError = class extends Error {
98
+ /** Stable, machine-readable failure code. See {@link MnemocyteErrorCode}. */
99
+ code;
100
+ /** Underlying cause, if any (e.g. an original driver or embedder error). */
101
+ cause;
102
+ /**
103
+ * @param message - Human-readable error message.
104
+ * @param code - Stable failure code; see {@link MnemocyteErrorCode}.
105
+ * @param cause - Optional underlying cause (driver error, network error, etc.).
106
+ */
107
+ constructor(message, code, cause) {
108
+ super(message);
109
+ this.name = "MnemocyteError";
110
+ this.code = code;
111
+ this.cause = cause;
112
+ }
113
+ };
114
+ /**
115
+ * Type guard for {@link MnemocyteError}.
116
+ *
117
+ * Prefer this over `instanceof` when working across module boundaries or
118
+ * bundlers where multiple copies of the class may exist.
119
+ *
120
+ * @param error - Value to test (typically a caught `unknown`).
121
+ * @returns `true` if `error` is a {@link MnemocyteError}.
122
+ */
123
+ function isMnemocyteError(error) {
124
+ return error instanceof MnemocyteError;
125
+ }
126
+ //#endregion
77
127
  //#region src/context/formatter.ts
78
128
  function escapeXml(value) {
79
129
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&apos;");
@@ -152,21 +202,6 @@ async function buildContext(options) {
152
202
  return trimToTokenBudget(formatContext(await options.recall(options.input), options.input.query, format), maxTokens, tokenCounter);
153
203
  }
154
204
  //#endregion
155
- //#region src/errors.ts
156
- var MnemocyteError = class extends Error {
157
- code;
158
- cause;
159
- constructor(message, code, cause) {
160
- super(message);
161
- this.name = "MnemocyteError";
162
- this.code = code;
163
- this.cause = cause;
164
- }
165
- };
166
- function isMnemocyteError(error) {
167
- return error instanceof MnemocyteError;
168
- }
169
- //#endregion
170
205
  //#region src/observability.ts
171
206
  function withMetadata(event, metadata) {
172
207
  return {
@@ -216,6 +251,176 @@ async function observe(config, backend, operation, metadata, action, successMeta
216
251
  throw error;
217
252
  }
218
253
  }
254
+ //#endregion
255
+ //#region src/resilience.ts
256
+ /**
257
+ * Default per-provider-call timeout. Disabled by default so that callers
258
+ * who do not configure {@link ProviderResilienceConfig.timeoutMs} are
259
+ * unaffected.
260
+ */
261
+ const DEFAULT_TIMEOUT_MS = 0;
262
+ /** Default number of retry attempts on transient provider failures. */
263
+ const DEFAULT_RETRIES = 0;
264
+ /** Default base backoff delay used between retry attempts. */
265
+ const DEFAULT_BASE_DELAY_MS = 100;
266
+ /** Default ceiling for retry backoff delay. */
267
+ const DEFAULT_MAX_DELAY_MS = 2e3;
268
+ /**
269
+ * Throw a {@link MnemocyteError} with code `"ABORTED"` if `signal` is
270
+ * already aborted. Safe to call with `undefined`.
271
+ */
272
+ function throwIfAborted(signal) {
273
+ if (signal?.aborted) throw new MnemocyteError("Operation was aborted.", "ABORTED", signal.reason);
274
+ }
275
+ /**
276
+ * Heuristic default for whether an error should be retried.
277
+ *
278
+ * Conservative by design: only retries on common transient failure
279
+ * indicators (network errors, 5xx-ish messages, `ECONN*` etc.). Never
280
+ * retries on validation, configuration, or {@link MnemocyteError} of
281
+ * code `"VALIDATION"` / `"CONFIG"` / `"ABORTED"`.
282
+ */
283
+ function defaultShouldRetry(error) {
284
+ if (error instanceof MnemocyteError) {
285
+ if (error.code === "VALIDATION" || error.code === "CONFIG" || error.code === "ABORTED") return false;
286
+ return true;
287
+ }
288
+ if (error instanceof Error) {
289
+ const message = error.message.toLowerCase();
290
+ if (message.includes("econn") || message.includes("etimedout") || message.includes("network") || message.includes("timeout") || message.includes("temporarily") || message.includes("rate limit") || message.includes("503") || message.includes("502") || message.includes("504") || message.includes("500")) return true;
291
+ }
292
+ return false;
293
+ }
294
+ /**
295
+ * Compute the backoff delay for the given retry attempt, using exponential
296
+ * growth capped at {@link ProviderResilienceConfig.maxDelayMs}.
297
+ *
298
+ * `attempt` is 1-indexed: the delay before the first retry uses
299
+ * `attempt = 1`, before the second retry `attempt = 2`, etc.
300
+ */
301
+ function computeDelayMs(attempt, baseDelayMs, maxDelayMs) {
302
+ const exponential = baseDelayMs * 2 ** (attempt - 1);
303
+ return Math.max(0, Math.min(maxDelayMs, exponential));
304
+ }
305
+ /**
306
+ * Promise-based `setTimeout` that rejects with an `"ABORTED"`
307
+ * {@link MnemocyteError} if `signal` aborts while waiting.
308
+ */
309
+ function delay(ms, signal) {
310
+ if (ms <= 0) {
311
+ throwIfAborted(signal);
312
+ return Promise.resolve();
313
+ }
314
+ return new Promise((resolve, reject) => {
315
+ const timer = setTimeout(() => {
316
+ signal?.removeEventListener("abort", onAbort);
317
+ resolve();
318
+ }, ms);
319
+ const onAbort = () => {
320
+ clearTimeout(timer);
321
+ reject(new MnemocyteError("Operation was aborted.", "ABORTED", signal?.reason));
322
+ };
323
+ if (signal) {
324
+ if (signal.aborted) {
325
+ clearTimeout(timer);
326
+ reject(new MnemocyteError("Operation was aborted.", "ABORTED", signal.reason));
327
+ return;
328
+ }
329
+ signal.addEventListener("abort", onAbort, { once: true });
330
+ }
331
+ });
332
+ }
333
+ /**
334
+ * Race `promise` against the configured `timeoutMs` and the caller's
335
+ * `signal`. Rejects with `"TIMEOUT"` or `"ABORTED"` respectively. Resolves
336
+ * with the original promise value if it settles first.
337
+ */
338
+ function withTimeout(promise, timeoutMs, signal) {
339
+ if (timeoutMs <= 0 && !signal) return promise;
340
+ return new Promise((resolve, reject) => {
341
+ let settled = false;
342
+ const timer = timeoutMs > 0 ? setTimeout(() => {
343
+ if (settled) return;
344
+ settled = true;
345
+ signal?.removeEventListener("abort", onAbort);
346
+ reject(new MnemocyteError(`Provider call timed out after ${timeoutMs}ms.`, "TIMEOUT"));
347
+ }, timeoutMs) : void 0;
348
+ const onAbort = () => {
349
+ if (settled) return;
350
+ settled = true;
351
+ if (timer) clearTimeout(timer);
352
+ reject(new MnemocyteError("Operation was aborted.", "ABORTED", signal?.reason));
353
+ };
354
+ if (signal) {
355
+ if (signal.aborted) {
356
+ if (timer) clearTimeout(timer);
357
+ settled = true;
358
+ reject(new MnemocyteError("Operation was aborted.", "ABORTED", signal.reason));
359
+ return;
360
+ }
361
+ signal.addEventListener("abort", onAbort, { once: true });
362
+ }
363
+ promise.then((value) => {
364
+ if (settled) return;
365
+ settled = true;
366
+ if (timer) clearTimeout(timer);
367
+ signal?.removeEventListener("abort", onAbort);
368
+ resolve(value);
369
+ }, (error) => {
370
+ if (settled) return;
371
+ settled = true;
372
+ if (timer) clearTimeout(timer);
373
+ signal?.removeEventListener("abort", onAbort);
374
+ reject(error);
375
+ });
376
+ });
377
+ }
378
+ /**
379
+ * Run `action` with timeout, retry, and abort handling derived from
380
+ * `options.resilience` and `options.signal`. Each retry attempt receives
381
+ * a fresh `AbortSignal` linked to the caller's signal — so callers may
382
+ * abort mid-retry and the in-flight attempt will cancel promptly if the
383
+ * underlying provider honours signals.
384
+ *
385
+ * Errors from {@link MnemocyteError} with code `"VALIDATION"` /
386
+ * `"CONFIG"` / `"ABORTED"` are never retried.
387
+ */
388
+ async function withResilience(action, options = {}) {
389
+ const config = options.resilience ?? {};
390
+ const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
391
+ const retries = Math.max(0, config.retries ?? DEFAULT_RETRIES);
392
+ const baseDelayMs = Math.max(0, config.baseDelayMs ?? DEFAULT_BASE_DELAY_MS);
393
+ const maxDelayMs = Math.max(baseDelayMs, config.maxDelayMs ?? DEFAULT_MAX_DELAY_MS);
394
+ const shouldRetry = config.shouldRetry ?? defaultShouldRetry;
395
+ const externalSignal = options.signal;
396
+ throwIfAborted(externalSignal);
397
+ const maxAttempts = retries + 1;
398
+ let lastError;
399
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
400
+ throwIfAborted(externalSignal);
401
+ try {
402
+ return await withTimeout(action(externalSignal), timeoutMs, externalSignal);
403
+ } catch (error) {
404
+ lastError = error;
405
+ if (error instanceof MnemocyteError && error.code === "ABORTED") throw error;
406
+ if (attempt >= maxAttempts) throw error;
407
+ if (!shouldRetry(error, attempt)) throw error;
408
+ await delay(computeDelayMs(attempt, baseDelayMs, maxDelayMs), externalSignal);
409
+ }
410
+ }
411
+ throw lastError;
412
+ }
413
+ /**
414
+ * Ordering of {@link ImportanceLevel} from least to most important. Used
415
+ * by `prune({ maxImportance })` to compare a memory's importance against
416
+ * the caller-supplied ceiling.
417
+ */
418
+ const IMPORTANCE_RANK = {
419
+ low: 0,
420
+ normal: 1,
421
+ high: 2,
422
+ critical: 3
423
+ };
219
424
  function assertNonEmptyString(value, field) {
220
425
  if (value.trim().length === 0) throw new MnemocyteError(`${field} must be a non-empty string.`, "VALIDATION");
221
426
  }
@@ -237,6 +442,10 @@ function normalizeTags(tags) {
237
442
  function createId() {
238
443
  return `mem_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
239
444
  }
445
+ /** Generate a unique audit-event id (`evt_*`). */
446
+ function createEventId() {
447
+ return `evt_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
448
+ }
240
449
  function isExpired(memory, now) {
241
450
  return memory.expiresAt !== null && memory.expiresAt.getTime() <= now.getTime();
242
451
  }
@@ -245,6 +454,7 @@ function cloneMemory(memory) {
245
454
  ...memory,
246
455
  tags: [...memory.tags],
247
456
  metadata: { ...memory.metadata },
457
+ supersededAt: memory.supersededAt ? new Date(memory.supersededAt) : null,
248
458
  expiresAt: memory.expiresAt ? new Date(memory.expiresAt) : null,
249
459
  lastAccessedAt: memory.lastAccessedAt ? new Date(memory.lastAccessedAt) : null,
250
460
  createdAt: new Date(memory.createdAt),
@@ -265,6 +475,7 @@ function rowToMemory(row) {
265
475
  embeddingModel: row.embeddingModel,
266
476
  embeddingDimensions: row.embeddingDimensions,
267
477
  supersededBy: row.supersededBy,
478
+ supersededAt: row.supersededAt,
268
479
  expiresAt: row.expiresAt,
269
480
  lastAccessedAt: row.lastAccessedAt,
270
481
  accessCount: row.accessCount,
@@ -272,11 +483,15 @@ function rowToMemory(row) {
272
483
  updatedAt: row.updatedAt
273
484
  };
274
485
  }
275
- async function embedOne(embedder, text) {
486
+ async function embedOne(embedder, text, options = {}) {
276
487
  let embeddings;
277
488
  try {
278
- embeddings = await embedder.embed([text]);
489
+ embeddings = await withResilience((signal) => signal === void 0 ? embedder.embed([text]) : embedder.embed([text], { signal }), {
490
+ ...options.signal === void 0 ? {} : { signal: options.signal },
491
+ ...options.resilience === void 0 ? {} : { resilience: options.resilience }
492
+ });
279
493
  } catch (error) {
494
+ if (error instanceof MnemocyteError && (error.code === "TIMEOUT" || error.code === "ABORTED")) throw error;
280
495
  throw new MnemocyteError("Failed to embed text.", "EMBEDDING", error);
281
496
  }
282
497
  const embedding = embeddings[0];
@@ -307,6 +522,79 @@ function contextInputToRecallInput(input) {
307
522
  ...input.includeExpired === void 0 ? {} : { includeExpired: input.includeExpired }
308
523
  };
309
524
  }
525
+ /**
526
+ * Validate a {@link PruneInput} and throw a `"VALIDATION"`
527
+ * {@link MnemocyteError} when no selector is supplied. Mnemocyte
528
+ * deliberately rejects `prune({})` to avoid surprise full-table deletes.
529
+ */
530
+ function validatePruneInput(input) {
531
+ if (input.entityId !== void 0) assertNonEmptyString(input.entityId, "entityId");
532
+ 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");
533
+ }
534
+ /**
535
+ * Test whether `memory` matches a {@link PruneInput}. All specified
536
+ * selectors are AND-combined; unspecified selectors do not restrict.
537
+ *
538
+ * `now` is supplied by the caller so callers can share a consistent
539
+ * timestamp across a batch.
540
+ */
541
+ function matchesPruneFilter(memory, input, now) {
542
+ if (input.entityId !== void 0 && memory.entityId !== input.entityId) return false;
543
+ if (input.expired === true && !isExpired(memory, now)) return false;
544
+ if (input.superseded === true && memory.supersededBy === null) return false;
545
+ if (input.createdBefore !== void 0 && memory.createdAt.getTime() >= input.createdBefore.getTime()) return false;
546
+ if (input.notAccessedSince !== void 0) {
547
+ const last = memory.lastAccessedAt;
548
+ if (last !== null && last.getTime() >= input.notAccessedSince.getTime()) return false;
549
+ }
550
+ if (input.types !== void 0 && !input.types.includes(memory.type)) return false;
551
+ if (input.tags !== void 0 && input.tags.length > 0 && !input.tags.every((tag) => memory.tags.includes(tag))) return false;
552
+ if (input.maxImportance !== void 0 && IMPORTANCE_RANK[memory.importance] > IMPORTANCE_RANK[input.maxImportance]) return false;
553
+ return true;
554
+ }
555
+ /**
556
+ * Validate a {@link FindDuplicatesInput} and throw a `"VALIDATION"`
557
+ * {@link MnemocyteError} for malformed `threshold` / `limit` values.
558
+ */
559
+ function validateFindDuplicatesInput(input) {
560
+ assertNonEmptyString(input.entityId, "entityId");
561
+ if (input.threshold !== void 0) {
562
+ if (!Number.isFinite(input.threshold) || input.threshold < 0 || input.threshold > 1) throw new MnemocyteError("threshold must be a number between 0 and 1.", "VALIDATION");
563
+ }
564
+ if (input.limit !== void 0) assertLimit(input.limit);
565
+ }
566
+ /**
567
+ * Return `true` when a memory should participate in a duplicate scan,
568
+ * given the filters on `input` and a shared `now` timestamp.
569
+ */
570
+ function matchesDuplicateFilter(memory, input, now) {
571
+ if (memory.entityId !== input.entityId) return false;
572
+ if (input.includeSuperseded !== true && memory.supersededBy !== null) return false;
573
+ if (input.includeExpired !== true && isExpired(memory, now)) return false;
574
+ if (input.types !== void 0 && !input.types.includes(memory.type)) return false;
575
+ if (input.tags !== void 0 && input.tags.length > 0 && !input.tags.every((tag) => memory.tags.includes(tag))) return false;
576
+ return true;
577
+ }
578
+ /**
579
+ * Validate a {@link ConsolidateInput} and throw a `"VALIDATION"`
580
+ * {@link MnemocyteError} for malformed inputs (empty `supersededIds`
581
+ * or a `survivorId` appearing in `supersededIds`).
582
+ */
583
+ function validateConsolidateInput(input) {
584
+ assertNonEmptyString(input.entityId, "entityId");
585
+ assertNonEmptyString(input.survivorId, "survivorId");
586
+ if (input.supersededIds.length === 0) throw new MnemocyteError("supersededIds must contain at least one memory id.", "VALIDATION");
587
+ if (input.supersededIds.includes(input.survivorId)) throw new MnemocyteError("survivorId must not appear in supersededIds.", "VALIDATION");
588
+ for (const id of input.supersededIds) assertNonEmptyString(id, "supersededIds[*]");
589
+ }
590
+ /**
591
+ * Validate a {@link ListAuditLogInput} and throw a `"VALIDATION"`
592
+ * {@link MnemocyteError} when malformed.
593
+ */
594
+ function validateListAuditLogInput(input) {
595
+ assertNonEmptyString(input.entityId, "entityId");
596
+ if (input.limit !== void 0) assertLimit(input.limit);
597
+ }
310
598
  function matchesRecallFilter(memory, input, now) {
311
599
  if (memory.entityId !== input.entityId) return false;
312
600
  if (!input.includeSuperseded && memory.supersededBy !== null) return false;
@@ -419,15 +707,38 @@ function toScoredMemory(memory, vector, lexical, input, config) {
419
707
  //#region src/memory/in-memory.ts
420
708
  function createInMemoryClient(config) {
421
709
  const memories = /* @__PURE__ */ new Map();
710
+ const auditLog = [];
422
711
  let closed = false;
423
712
  function assertOpen() {
424
713
  if (closed) throw new MnemocyteError("Mnemocyte client is closed.", "DB");
425
714
  }
715
+ function recordAudit(entityId, description, metadata) {
716
+ if (config.audit?.enabled !== true) return;
717
+ auditLog.push({
718
+ id: createEventId(),
719
+ entityId,
720
+ description,
721
+ metadata,
722
+ timestamp: /* @__PURE__ */ new Date()
723
+ });
724
+ }
725
+ function cloneAuditEvent(event) {
726
+ return {
727
+ id: event.id,
728
+ entityId: event.entityId,
729
+ description: event.description,
730
+ metadata: { ...event.metadata },
731
+ timestamp: new Date(event.timestamp)
732
+ };
733
+ }
426
734
  async function remember(input) {
427
735
  return observe(config, "in-memory", "remember", { entityId: input.entityId }, async () => {
428
736
  assertOpen();
429
737
  validateRememberInput(input);
430
- const embedding = await embedOne(config.embedder, input.content);
738
+ const embedding = await embedOne(config.embedder, input.content, {
739
+ ...input.signal === void 0 ? {} : { signal: input.signal },
740
+ ...config.provider === void 0 ? {} : { resilience: config.provider }
741
+ });
431
742
  const now = /* @__PURE__ */ new Date();
432
743
  const memory = {
433
744
  id: createId(),
@@ -442,6 +753,7 @@ function createInMemoryClient(config) {
442
753
  embeddingModel: config.embedder.model,
443
754
  embeddingDimensions: config.embedder.dimensions,
444
755
  supersededBy: null,
756
+ supersededAt: null,
445
757
  expiresAt: input.expiresAt ?? null,
446
758
  lastAccessedAt: null,
447
759
  accessCount: 0,
@@ -450,6 +762,11 @@ function createInMemoryClient(config) {
450
762
  embedding
451
763
  };
452
764
  memories.set(memory.id, memory);
765
+ recordAudit(memory.entityId, "memory.created", {
766
+ memoryId: memory.id,
767
+ type: memory.type,
768
+ importance: memory.importance
769
+ });
453
770
  return cloneMemory(memory);
454
771
  }, (memory) => ({
455
772
  memoryId: memory.id,
@@ -474,7 +791,10 @@ function createInMemoryClient(config) {
474
791
  const minScore = input.minScore ?? config.defaults?.minScore ?? 0;
475
792
  assertLimit(limit);
476
793
  assertMinScore(minScore);
477
- const queryEmbedding = await embedOne(config.embedder, input.query);
794
+ const queryEmbedding = await embedOne(config.embedder, input.query, {
795
+ ...input.signal === void 0 ? {} : { signal: input.signal },
796
+ ...config.provider === void 0 ? {} : { resilience: config.provider }
797
+ });
478
798
  const now = /* @__PURE__ */ new Date();
479
799
  const scored = Array.from(memories.values()).filter((memory) => matchesRecallFilter(memory, input, now)).map((memory) => toScoredMemory(memory, Math.max(0, cosineSimilarity(memory.embedding, queryEmbedding)), lexicalScore(memory.content, input.query), input, config.retrieval)).filter((memory) => memory.score >= minScore).sort((a, b) => b.score - a.score).slice(0, limit);
480
800
  for (const memory of scored) {
@@ -493,7 +813,13 @@ function createInMemoryClient(config) {
493
813
  assertOpen();
494
814
  return buildContext({
495
815
  input,
496
- recall: (contextInput) => this.recall(contextInputToRecallInput(contextInput))
816
+ recall: (contextInput) => {
817
+ const recallInput = contextInputToRecallInput(contextInput);
818
+ return this.recall(input.signal === void 0 ? recallInput : {
819
+ ...recallInput,
820
+ signal: input.signal
821
+ });
822
+ }
497
823
  });
498
824
  });
499
825
  },
@@ -508,6 +834,7 @@ function createInMemoryClient(config) {
508
834
  const memory = memories.get(input.memoryId);
509
835
  if (!memory || memory.entityId !== input.entityId) throw new MnemocyteError("Memory was not found.", "NOT_FOUND");
510
836
  memories.delete(input.memoryId);
837
+ recordAudit(memory.entityId, "memory.deleted", { memoryId: memory.id });
511
838
  });
512
839
  },
513
840
  async forgetAll(input) {
@@ -519,9 +846,76 @@ function createInMemoryClient(config) {
519
846
  memories.delete(id);
520
847
  deleted += 1;
521
848
  }
849
+ recordAudit(input.entityId, "entity.cleared", { count: deleted });
522
850
  return deleted;
523
851
  }, (count) => ({ count })).then(() => void 0);
524
852
  },
853
+ async prune(input) {
854
+ return observe(config, "in-memory", "prune", input.entityId === void 0 ? {} : { entityId: input.entityId }, async () => {
855
+ assertOpen();
856
+ validatePruneInput(input);
857
+ const now = /* @__PURE__ */ new Date();
858
+ const matched = [];
859
+ for (const memory of memories.values()) if (matchesPruneFilter(memory, input, now)) matched.push(memory.id);
860
+ const dryRun = input.dryRun === true;
861
+ if (!dryRun) {
862
+ for (const id of matched) memories.delete(id);
863
+ if (matched.length > 0 && input.entityId !== void 0) recordAudit(input.entityId, "memory.pruned", { count: matched.length });
864
+ }
865
+ return {
866
+ matchedCount: matched.length,
867
+ deletedCount: dryRun ? 0 : matched.length,
868
+ dryRun
869
+ };
870
+ }, (result) => ({ count: result.deletedCount }));
871
+ },
872
+ async findDuplicates(input) {
873
+ return observe(config, "in-memory", "findDuplicates", { entityId: input.entityId }, async () => {
874
+ assertOpen();
875
+ validateFindDuplicatesInput(input);
876
+ const threshold = input.threshold ?? .95;
877
+ const limit = input.limit ?? 100;
878
+ const now = /* @__PURE__ */ new Date();
879
+ const candidates = Array.from(memories.values()).filter((memory) => matchesDuplicateFilter(memory, input, now));
880
+ const pairs = [];
881
+ for (let i = 0; i < candidates.length; i += 1) {
882
+ const a = candidates[i];
883
+ if (a === void 0) continue;
884
+ for (let j = i + 1; j < candidates.length; j += 1) {
885
+ const b = candidates[j];
886
+ if (b === void 0) continue;
887
+ const similarity = Math.max(0, Math.min(1, cosineSimilarity(a.embedding, b.embedding)));
888
+ if (similarity >= threshold) pairs.push({
889
+ a: cloneMemory(a),
890
+ b: cloneMemory(b),
891
+ similarity
892
+ });
893
+ }
894
+ }
895
+ pairs.sort((x, y) => y.similarity - x.similarity);
896
+ return pairs.slice(0, limit);
897
+ }, (result) => ({ count: result.length }));
898
+ },
899
+ async listAuditLog(input) {
900
+ return observe(config, "in-memory", "listAuditLog", { entityId: input.entityId }, async () => {
901
+ assertOpen();
902
+ validateListAuditLogInput(input);
903
+ const limit = input.limit ?? 50;
904
+ return auditLog.map((event, idx) => ({
905
+ event,
906
+ idx
907
+ })).filter(({ event }) => {
908
+ if (event.entityId !== input.entityId) return false;
909
+ if (input.before !== void 0 && event.timestamp.getTime() >= input.before.getTime()) return false;
910
+ if (input.after !== void 0 && event.timestamp.getTime() <= input.after.getTime()) return false;
911
+ return true;
912
+ }).sort((a, b) => {
913
+ const dt = b.event.timestamp.getTime() - a.event.timestamp.getTime();
914
+ if (dt !== 0) return dt;
915
+ return b.idx - a.idx;
916
+ }).slice(0, limit).map(({ event }) => cloneAuditEvent(event));
917
+ }, (result) => ({ count: result.length }));
918
+ },
525
919
  async stats(input) {
526
920
  return observe(config, "in-memory", "stats", input?.entityId ? { entityId: input.entityId } : {}, async () => {
527
921
  assertOpen();
@@ -547,6 +941,7 @@ function createInMemoryClient(config) {
547
941
  };
548
942
  });
549
943
  },
944
+ experimental: createExperimental(),
550
945
  async close() {
551
946
  return observe(config, "in-memory", "close", {}, async () => {
552
947
  closed = true;
@@ -554,11 +949,62 @@ function createInMemoryClient(config) {
554
949
  });
555
950
  }
556
951
  };
952
+ function createExperimental() {
953
+ return { async consolidate(input) {
954
+ return observe(config, "in-memory", "consolidate", {
955
+ entityId: input.entityId,
956
+ memoryId: input.survivorId
957
+ }, async () => {
958
+ assertOpen();
959
+ validateConsolidateInput(input);
960
+ const survivor = memories.get(input.survivorId);
961
+ if (!survivor || survivor.entityId !== input.entityId) throw new MnemocyteError("Survivor memory was not found.", "NOT_FOUND");
962
+ if (survivor.supersededBy !== null) throw new MnemocyteError("Survivor memory is itself superseded.", "VALIDATION");
963
+ const losers = [];
964
+ for (const id of input.supersededIds) {
965
+ const memory = memories.get(id);
966
+ if (!memory || memory.entityId !== input.entityId) throw new MnemocyteError("Superseded memory was not found.", "NOT_FOUND");
967
+ if (memory.supersededBy !== null) continue;
968
+ losers.push(memory);
969
+ }
970
+ const now = /* @__PURE__ */ new Date();
971
+ const newSupersededIds = [];
972
+ for (const loser of losers) {
973
+ loser.supersededBy = survivor.id;
974
+ loser.supersededAt = now;
975
+ loser.updatedAt = now;
976
+ newSupersededIds.push(loser.id);
977
+ recordAudit(input.entityId, "memory.superseded", {
978
+ memoryId: loser.id,
979
+ supersededBy: survivor.id
980
+ });
981
+ }
982
+ if (input.mergeTags !== false && losers.length > 0) {
983
+ const mergedTags = new Set(survivor.tags);
984
+ for (const loser of losers) for (const tag of loser.tags) mergedTags.add(tag);
985
+ if (mergedTags.size !== survivor.tags.length) {
986
+ survivor.tags = [...mergedTags];
987
+ survivor.updatedAt = now;
988
+ }
989
+ }
990
+ return {
991
+ survivorId: survivor.id,
992
+ supersededCount: newSupersededIds.length,
993
+ supersededIds: newSupersededIds
994
+ };
995
+ }, (result) => ({ count: result.supersededCount }));
996
+ } };
997
+ }
557
998
  }
558
999
  //#endregion
559
1000
  //#region src/db/queries/events.ts
560
- async function deleteEventsForEntity(db, entityId) {
561
- return (await db.delete(eventsTable).where(eq(eventsTable.entityId, entityId)).returning({ id: eventsTable.id })).length;
1001
+ async function insertEvent(db, row) {
1002
+ const inserted = (await db.insert(eventsTable).values(row).returning())[0];
1003
+ if (!inserted) throw new Error("Event insert returned no rows.");
1004
+ return inserted;
1005
+ }
1006
+ async function listEvents(db, filter) {
1007
+ return db.select().from(eventsTable).where(and(eq(eventsTable.entityId, filter.entityId), filter.before ? lt(eventsTable.timestamp, filter.before) : void 0, filter.after ? gt(eventsTable.timestamp, filter.after) : void 0)).orderBy(desc(eventsTable.timestamp)).limit(filter.limit ?? 50);
562
1008
  }
563
1009
  //#endregion
564
1010
  //#region src/db/queries/memories.ts
@@ -573,6 +1019,9 @@ async function insertMemory(db, row) {
573
1019
  if (!inserted) throw new Error("Memory insert returned no rows.");
574
1020
  return inserted;
575
1021
  }
1022
+ async function getMemoryById(db, entityId, memoryId) {
1023
+ return (await db.select().from(memoriesTable).where(and(eq(memoriesTable.entityId, entityId), eq(memoriesTable.id, memoryId))).limit(1))[0] ?? null;
1024
+ }
576
1025
  async function listMemories(db, filter) {
577
1026
  return db.select().from(memoriesTable).where(filterConditions(filter));
578
1027
  }
@@ -582,6 +1031,145 @@ async function deleteMemory(db, entityId, memoryId) {
582
1031
  async function deleteMemoriesForEntity(db, entityId) {
583
1032
  return (await db.delete(memoriesTable).where(eq(memoriesTable.entityId, entityId)).returning({ id: memoriesTable.id })).length;
584
1033
  }
1034
+ function pruneConditions(filter) {
1035
+ return and(filter.entityId !== void 0 ? eq(memoriesTable.entityId, filter.entityId) : void 0, filter.expired ? and(isNotNull(memoriesTable.expiresAt), sql`${memoriesTable.expiresAt} <= now()`) : void 0, filter.superseded ? isNotNull(memoriesTable.supersededBy) : void 0, filter.createdBefore ? lt(memoriesTable.createdAt, filter.createdBefore) : void 0, filter.notAccessedSince ? or(isNull(memoriesTable.lastAccessedAt), lt(memoriesTable.lastAccessedAt, filter.notAccessedSince)) : void 0, filter.types && filter.types.length > 0 ? inArray(memoriesTable.type, [...filter.types]) : void 0, filter.tags && filter.tags.length > 0 ? sql`${memoriesTable.tags} @> ${filter.tags}` : void 0, filter.maxImportanceLevels && filter.maxImportanceLevels.length > 0 ? inArray(memoriesTable.importance, [...filter.maxImportanceLevels]) : void 0);
1036
+ }
1037
+ async function countPruneMatches(db, filter) {
1038
+ return (await db.select({ count: sql`count(*)::int` }).from(memoriesTable).where(pruneConditions(filter)))[0]?.count ?? 0;
1039
+ }
1040
+ async function pruneMemories(db, filter) {
1041
+ return (await db.delete(memoriesTable).where(pruneConditions(filter)).returning({ id: memoriesTable.id })).length;
1042
+ }
1043
+ async function findDuplicatePairs(db, input) {
1044
+ return (await db.execute(sql`
1045
+ SELECT
1046
+ a.id AS "aId",
1047
+ a.entity_id AS "aEntityId",
1048
+ a.content AS "aContent",
1049
+ a.type AS "aType",
1050
+ a.importance AS "aImportance",
1051
+ a.tags AS "aTags",
1052
+ a.source AS "aSource",
1053
+ a.metadata AS "aMetadata",
1054
+ a.confidence AS "aConfidence",
1055
+ a.embedding AS "aEmbedding",
1056
+ a.embedding_model AS "aEmbeddingModel",
1057
+ a.embedding_dimensions AS "aEmbeddingDimensions",
1058
+ a.superseded_by AS "aSupersededBy",
1059
+ a.superseded_at AS "aSupersededAt",
1060
+ a.expires_at AS "aExpiresAt",
1061
+ a.last_accessed_at AS "aLastAccessedAt",
1062
+ a.access_count AS "aAccessCount",
1063
+ a.created_at AS "aCreatedAt",
1064
+ a.updated_at AS "aUpdatedAt",
1065
+ b.id AS "bId",
1066
+ b.entity_id AS "bEntityId",
1067
+ b.content AS "bContent",
1068
+ b.type AS "bType",
1069
+ b.importance AS "bImportance",
1070
+ b.tags AS "bTags",
1071
+ b.source AS "bSource",
1072
+ b.metadata AS "bMetadata",
1073
+ b.confidence AS "bConfidence",
1074
+ b.embedding AS "bEmbedding",
1075
+ b.embedding_model AS "bEmbeddingModel",
1076
+ b.embedding_dimensions AS "bEmbeddingDimensions",
1077
+ b.superseded_by AS "bSupersededBy",
1078
+ b.superseded_at AS "bSupersededAt",
1079
+ b.expires_at AS "bExpiresAt",
1080
+ b.last_accessed_at AS "bLastAccessedAt",
1081
+ b.access_count AS "bAccessCount",
1082
+ b.created_at AS "bCreatedAt",
1083
+ b.updated_at AS "bUpdatedAt",
1084
+ 1 - (a.embedding <=> b.embedding) AS "similarity"
1085
+ FROM mnemocyte_memories a
1086
+ JOIN mnemocyte_memories b
1087
+ ON b.entity_id = a.entity_id
1088
+ AND a.id < b.id
1089
+ AND a.embedding IS NOT NULL
1090
+ AND b.embedding IS NOT NULL
1091
+ WHERE
1092
+ a.entity_id = ${input.entityId}
1093
+ ${input.includeSuperseded ? sql`` : sql`AND a.superseded_by IS NULL AND b.superseded_by IS NULL`}
1094
+ ${input.includeExpired ? sql`` : sql`AND (a.expires_at IS NULL OR a.expires_at > now()) AND (b.expires_at IS NULL OR b.expires_at > now())`}
1095
+ ${input.types && input.types.length > 0 ? sql`AND a.type IN (${sql.join(input.types.map((type) => sql`${type}`), sql`, `)}) AND b.type IN (${sql.join(input.types.map((type) => sql`${type}`), sql`, `)})` : sql``}
1096
+ ${input.tags && input.tags.length > 0 ? sql`AND a.tags @> ${input.tags} AND b.tags @> ${input.tags}` : sql``}
1097
+ AND 1 - (a.embedding <=> b.embedding) >= ${input.threshold}
1098
+ ORDER BY "similarity" DESC
1099
+ LIMIT ${input.limit}
1100
+ `)).map((row) => {
1101
+ return {
1102
+ a: {
1103
+ id: row.aId,
1104
+ entityId: row.aEntityId,
1105
+ content: row.aContent,
1106
+ type: row.aType,
1107
+ importance: row.aImportance,
1108
+ tags: row.aTags,
1109
+ source: row.aSource,
1110
+ metadata: row.aMetadata,
1111
+ confidence: row.aConfidence,
1112
+ embedding: row.aEmbedding,
1113
+ embeddingModel: row.aEmbeddingModel,
1114
+ embeddingDimensions: row.aEmbeddingDimensions,
1115
+ supersededBy: row.aSupersededBy,
1116
+ supersededAt: row.aSupersededAt,
1117
+ expiresAt: row.aExpiresAt,
1118
+ lastAccessedAt: row.aLastAccessedAt,
1119
+ accessCount: row.aAccessCount,
1120
+ createdAt: row.aCreatedAt,
1121
+ updatedAt: row.aUpdatedAt
1122
+ },
1123
+ b: {
1124
+ id: row.bId,
1125
+ entityId: row.bEntityId,
1126
+ content: row.bContent,
1127
+ type: row.bType,
1128
+ importance: row.bImportance,
1129
+ tags: row.bTags,
1130
+ source: row.bSource,
1131
+ metadata: row.bMetadata,
1132
+ confidence: row.bConfidence,
1133
+ embedding: row.bEmbedding,
1134
+ embeddingModel: row.bEmbeddingModel,
1135
+ embeddingDimensions: row.bEmbeddingDimensions,
1136
+ supersededBy: row.bSupersededBy,
1137
+ supersededAt: row.bSupersededAt,
1138
+ expiresAt: row.bExpiresAt,
1139
+ lastAccessedAt: row.bLastAccessedAt,
1140
+ accessCount: row.bAccessCount,
1141
+ createdAt: row.bCreatedAt,
1142
+ updatedAt: row.bUpdatedAt
1143
+ },
1144
+ similarity: Number(row.similarity) || 0
1145
+ };
1146
+ });
1147
+ }
1148
+ async function loadConsolidationTargets(db, entityId, ids) {
1149
+ if (ids.length === 0) return [];
1150
+ return await db.select({
1151
+ id: memoriesTable.id,
1152
+ tags: memoriesTable.tags,
1153
+ supersededBy: memoriesTable.supersededBy
1154
+ }).from(memoriesTable).where(and(eq(memoriesTable.entityId, entityId), inArray(memoriesTable.id, [...ids])));
1155
+ }
1156
+ async function markMemoriesSuperseded(db, params) {
1157
+ if (params.ids.length === 0) return [];
1158
+ return await db.update(memoriesTable).set({
1159
+ supersededBy: params.survivorId,
1160
+ supersededAt: params.now,
1161
+ updatedAt: params.now
1162
+ }).where(and(eq(memoriesTable.entityId, params.entityId), inArray(memoriesTable.id, [...params.ids]), isNull(memoriesTable.supersededBy))).returning({
1163
+ id: memoriesTable.id,
1164
+ tags: memoriesTable.tags
1165
+ });
1166
+ }
1167
+ async function setMemoryTags(db, params) {
1168
+ await db.update(memoriesTable).set({
1169
+ tags: [...params.tags],
1170
+ updatedAt: params.now
1171
+ }).where(and(eq(memoriesTable.entityId, params.entityId), eq(memoriesTable.id, params.memoryId)));
1172
+ }
585
1173
  async function markMemoryAccessed(db, memoryIds) {
586
1174
  if (memoryIds.length === 0) return;
587
1175
  await db.update(memoriesTable).set({
@@ -670,7 +1258,10 @@ async function lexicalSearch(db, input) {
670
1258
  //#endregion
671
1259
  //#region src/retrieval/index.ts
672
1260
  async function hybridRecall(input) {
673
- const queryEmbedding = await embedOne(input.embedder, input.input.query);
1261
+ const queryEmbedding = await embedOne(input.embedder, input.input.query, {
1262
+ ...input.signal === void 0 ? {} : { signal: input.signal },
1263
+ ...input.resilience === void 0 ? {} : { resilience: input.resilience }
1264
+ });
674
1265
  const candidateMultiplier = input.retrieval?.candidateMultiplier ?? 3;
675
1266
  const candidateLimit = Math.max(input.limit, input.limit * candidateMultiplier);
676
1267
  const [vectorRows, lexicalRows] = await Promise.all([vectorSearch(input.db, {
@@ -703,18 +1294,63 @@ async function hybridRecall(input) {
703
1294
  }
704
1295
  //#endregion
705
1296
  //#region src/memory/postgres.ts
1297
+ function rowToAuditEvent(row) {
1298
+ return {
1299
+ id: row.id,
1300
+ entityId: row.entityId,
1301
+ description: row.description,
1302
+ metadata: row.metadata,
1303
+ timestamp: row.timestamp
1304
+ };
1305
+ }
1306
+ const IMPORTANCE_LEVELS = [
1307
+ "low",
1308
+ "normal",
1309
+ "high",
1310
+ "critical"
1311
+ ];
1312
+ function importanceCeilingLevels(max) {
1313
+ return IMPORTANCE_LEVELS.filter((level) => IMPORTANCE_RANK[level] <= IMPORTANCE_RANK[max]);
1314
+ }
1315
+ function toPruneFilter(input) {
1316
+ const filter = {};
1317
+ if (input.entityId !== void 0) filter.entityId = input.entityId;
1318
+ if (input.expired === true) filter.expired = true;
1319
+ if (input.superseded === true) filter.superseded = true;
1320
+ if (input.createdBefore !== void 0) filter.createdBefore = input.createdBefore;
1321
+ if (input.notAccessedSince !== void 0) filter.notAccessedSince = input.notAccessedSince;
1322
+ if (input.types !== void 0 && input.types.length > 0) filter.types = input.types;
1323
+ if (input.tags !== void 0 && input.tags.length > 0) filter.tags = input.tags;
1324
+ if (input.maxImportance !== void 0) filter.maxImportanceLevels = importanceCeilingLevels(input.maxImportance);
1325
+ return filter;
1326
+ }
706
1327
  function createPostgresClient(config, handle) {
707
1328
  let closed = false;
708
1329
  function assertOpen() {
709
1330
  if (closed) throw new MnemocyteError("Mnemocyte client is closed.", "DB");
710
1331
  }
1332
+ async function recordAudit(entityId, description, metadata) {
1333
+ if (config.audit?.enabled !== true) return;
1334
+ try {
1335
+ await insertEvent(handle.db, {
1336
+ id: createEventId(),
1337
+ entityId,
1338
+ description,
1339
+ metadata,
1340
+ timestamp: /* @__PURE__ */ new Date()
1341
+ });
1342
+ } catch {}
1343
+ }
711
1344
  async function remember(input) {
712
1345
  return observe(config, "postgres", "remember", { entityId: input.entityId }, async () => {
713
1346
  assertOpen();
714
1347
  validateRememberInput(input);
715
- const embedding = await embedOne(config.embedder, input.content);
1348
+ const embedding = await embedOne(config.embedder, input.content, {
1349
+ ...input.signal === void 0 ? {} : { signal: input.signal },
1350
+ ...config.provider === void 0 ? {} : { resilience: config.provider }
1351
+ });
716
1352
  const now = /* @__PURE__ */ new Date();
717
- return rowToMemory(await insertMemory(handle.db, {
1353
+ const memory = rowToMemory(await insertMemory(handle.db, {
718
1354
  id: createId(),
719
1355
  entityId: input.entityId,
720
1356
  content: input.content,
@@ -735,6 +1371,12 @@ function createPostgresClient(config, handle) {
735
1371
  createdAt: now,
736
1372
  updatedAt: now
737
1373
  }));
1374
+ await recordAudit(memory.entityId, "memory.created", {
1375
+ memoryId: memory.id,
1376
+ type: memory.type,
1377
+ importance: memory.importance
1378
+ });
1379
+ return memory;
738
1380
  }, (memory) => ({
739
1381
  memoryId: memory.id,
740
1382
  count: 1
@@ -764,7 +1406,9 @@ function createPostgresClient(config, handle) {
764
1406
  input,
765
1407
  limit,
766
1408
  minScore,
767
- retrieval: config.retrieval
1409
+ retrieval: config.retrieval,
1410
+ ...input.signal === void 0 ? {} : { signal: input.signal },
1411
+ ...config.provider === void 0 ? {} : { resilience: config.provider }
768
1412
  });
769
1413
  }, (result) => ({ count: result.length }));
770
1414
  },
@@ -773,7 +1417,13 @@ function createPostgresClient(config, handle) {
773
1417
  assertOpen();
774
1418
  return buildContext({
775
1419
  input,
776
- recall: (contextInput) => this.recall(contextInputToRecallInput(contextInput))
1420
+ recall: (contextInput) => {
1421
+ const recallInput = contextInputToRecallInput(contextInput);
1422
+ return this.recall(input.signal === void 0 ? recallInput : {
1423
+ ...recallInput,
1424
+ signal: input.signal
1425
+ });
1426
+ }
777
1427
  });
778
1428
  });
779
1429
  },
@@ -786,14 +1436,69 @@ function createPostgresClient(config, handle) {
786
1436
  assertNonEmptyString(input.entityId, "entityId");
787
1437
  assertNonEmptyString(input.memoryId, "memoryId");
788
1438
  if (!await deleteMemory(handle.db, input.entityId, input.memoryId)) throw new MnemocyteError("Memory was not found.", "NOT_FOUND");
1439
+ await recordAudit(input.entityId, "memory.deleted", { memoryId: input.memoryId });
789
1440
  });
790
1441
  },
791
1442
  async forgetAll(input) {
792
1443
  return observe(config, "postgres", "forgetAll", { entityId: input.entityId }, async () => {
793
1444
  assertOpen();
794
1445
  assertNonEmptyString(input.entityId, "entityId");
795
- await Promise.all([deleteMemoriesForEntity(handle.db, input.entityId), deleteEventsForEntity(handle.db, input.entityId)]);
796
- });
1446
+ const deletedCount = await deleteMemoriesForEntity(handle.db, input.entityId);
1447
+ await recordAudit(input.entityId, "entity.cleared", { count: deletedCount });
1448
+ return deletedCount;
1449
+ }, (count) => ({ count })).then(() => void 0);
1450
+ },
1451
+ async prune(input) {
1452
+ return observe(config, "postgres", "prune", input.entityId === void 0 ? {} : { entityId: input.entityId }, async () => {
1453
+ assertOpen();
1454
+ validatePruneInput(input);
1455
+ const filter = toPruneFilter(input);
1456
+ if (input.dryRun === true) return {
1457
+ matchedCount: await countPruneMatches(handle.db, filter),
1458
+ deletedCount: 0,
1459
+ dryRun: true
1460
+ };
1461
+ const deletedCount = await pruneMemories(handle.db, filter);
1462
+ if (deletedCount > 0 && input.entityId !== void 0) await recordAudit(input.entityId, "memory.pruned", { count: deletedCount });
1463
+ return {
1464
+ matchedCount: deletedCount,
1465
+ deletedCount,
1466
+ dryRun: false
1467
+ };
1468
+ }, (result) => ({ count: result.deletedCount }));
1469
+ },
1470
+ async findDuplicates(input) {
1471
+ return observe(config, "postgres", "findDuplicates", { entityId: input.entityId }, async () => {
1472
+ assertOpen();
1473
+ validateFindDuplicatesInput(input);
1474
+ const threshold = input.threshold ?? .95;
1475
+ const limit = input.limit ?? 100;
1476
+ return (await findDuplicatePairs(handle.db, {
1477
+ entityId: input.entityId,
1478
+ threshold,
1479
+ limit,
1480
+ ...input.types === void 0 ? {} : { types: input.types },
1481
+ ...input.tags === void 0 ? {} : { tags: input.tags },
1482
+ ...input.includeSuperseded === void 0 ? {} : { includeSuperseded: input.includeSuperseded },
1483
+ ...input.includeExpired === void 0 ? {} : { includeExpired: input.includeExpired }
1484
+ })).map((row) => ({
1485
+ a: rowToMemory(row.a),
1486
+ b: rowToMemory(row.b),
1487
+ similarity: Math.max(0, Math.min(1, row.similarity))
1488
+ }));
1489
+ }, (result) => ({ count: result.length }));
1490
+ },
1491
+ async listAuditLog(input) {
1492
+ return observe(config, "postgres", "listAuditLog", { entityId: input.entityId }, async () => {
1493
+ assertOpen();
1494
+ validateListAuditLogInput(input);
1495
+ return (await listEvents(handle.db, {
1496
+ entityId: input.entityId,
1497
+ limit: input.limit ?? 50,
1498
+ ...input.before === void 0 ? {} : { before: input.before },
1499
+ ...input.after === void 0 ? {} : { after: input.after }
1500
+ })).map(rowToAuditEvent);
1501
+ }, (result) => ({ count: result.length }));
797
1502
  },
798
1503
  async stats(input) {
799
1504
  return observe(config, "postgres", "stats", input?.entityId ? { entityId: input.entityId } : {}, async () => {
@@ -823,6 +1528,7 @@ function createPostgresClient(config, handle) {
823
1528
  };
824
1529
  });
825
1530
  },
1531
+ experimental: createExperimental(),
826
1532
  async close() {
827
1533
  return observe(config, "postgres", "close", {}, async () => {
828
1534
  closed = true;
@@ -830,13 +1536,93 @@ function createPostgresClient(config, handle) {
830
1536
  });
831
1537
  }
832
1538
  };
1539
+ function createExperimental() {
1540
+ return { async consolidate(input) {
1541
+ return observe(config, "postgres", "consolidate", {
1542
+ entityId: input.entityId,
1543
+ memoryId: input.survivorId
1544
+ }, async () => {
1545
+ assertOpen();
1546
+ validateConsolidateInput(input);
1547
+ const survivor = await getMemoryById(handle.db, input.entityId, input.survivorId);
1548
+ if (!survivor) throw new MnemocyteError("Survivor memory was not found.", "NOT_FOUND");
1549
+ if (survivor.supersededBy !== null) throw new MnemocyteError("Survivor memory is itself superseded.", "VALIDATION");
1550
+ const targets = await loadConsolidationTargets(handle.db, input.entityId, input.supersededIds);
1551
+ const foundIds = new Set(targets.map((t) => t.id));
1552
+ for (const id of input.supersededIds) if (!foundIds.has(id)) throw new MnemocyteError("Superseded memory was not found.", "NOT_FOUND");
1553
+ const losers = targets.filter((target) => target.supersededBy === null);
1554
+ if (losers.length === 0) return {
1555
+ survivorId: survivor.id,
1556
+ supersededCount: 0,
1557
+ supersededIds: []
1558
+ };
1559
+ const now = /* @__PURE__ */ new Date();
1560
+ const updated = await markMemoriesSuperseded(handle.db, {
1561
+ survivorId: survivor.id,
1562
+ entityId: input.entityId,
1563
+ ids: losers.map((loser) => loser.id),
1564
+ now
1565
+ });
1566
+ const newSupersededIds = updated.map((row) => row.id);
1567
+ for (const id of newSupersededIds) await recordAudit(input.entityId, "memory.superseded", {
1568
+ memoryId: id,
1569
+ supersededBy: survivor.id
1570
+ });
1571
+ if (input.mergeTags !== false && updated.length > 0) {
1572
+ const mergedTags = new Set(survivor.tags);
1573
+ for (const row of updated) for (const tag of row.tags) mergedTags.add(tag);
1574
+ if (mergedTags.size !== survivor.tags.length) await setMemoryTags(handle.db, {
1575
+ entityId: input.entityId,
1576
+ memoryId: survivor.id,
1577
+ tags: [...mergedTags],
1578
+ now
1579
+ });
1580
+ }
1581
+ return {
1582
+ survivorId: survivor.id,
1583
+ supersededCount: newSupersededIds.length,
1584
+ supersededIds: newSupersededIds
1585
+ };
1586
+ }, (result) => ({ count: result.supersededCount }));
1587
+ } };
1588
+ }
833
1589
  }
834
1590
  //#endregion
835
1591
  //#region src/client.ts
1592
+ /** Embedding dimensionality pinned by `migrations/0000_initial.sql`. */
1593
+ const POSTGRES_EMBEDDING_DIMENSIONS = 1536;
1594
+ /**
1595
+ * Create a Mnemocyte client.
1596
+ *
1597
+ * When {@link MnemocyteConfig.databaseUrl} is provided, a Postgres-backed
1598
+ * client is returned (requires the `pgvector` extension and the bundled
1599
+ * migration applied). Otherwise an in-memory client is returned, intended
1600
+ * for tests, demos, and short-lived processes.
1601
+ *
1602
+ * The returned client owns any underlying resources (e.g. the Postgres
1603
+ * connection pool). Call {@link MnemocyteClient.close} when finished to
1604
+ * release them.
1605
+ *
1606
+ * @param config - Client configuration. {@link MnemocyteConfig.embedder} is required.
1607
+ * @returns A {@link MnemocyteClient} instance.
1608
+ * @throws {MnemocyteError} With code `"CONFIG"` if `embedder` is missing or
1609
+ * invalid (including when `databaseUrl` is set and `embedder.dimensions`
1610
+ * is not 1536 — the dimensionality pinned by the bundled migration), or
1611
+ * `"VALIDATION"` if `databaseUrl` is provided but empty.
1612
+ *
1613
+ * @example Postgres-backed client
1614
+ * ```ts
1615
+ * const client = createMnemocyte({
1616
+ * databaseUrl: process.env.DATABASE_URL,
1617
+ * embedder, // embedder.dimensions MUST be 1536 for the Postgres backend
1618
+ * });
1619
+ * ```
1620
+ */
836
1621
  function createMnemocyte(config) {
837
1622
  assertEmbedder(config.embedder);
838
1623
  if (config.databaseUrl) {
839
1624
  assertNonEmptyString(config.databaseUrl, "databaseUrl");
1625
+ if (config.embedder.dimensions !== POSTGRES_EMBEDDING_DIMENSIONS) throw new MnemocyteError(`embedder.dimensions must be ${POSTGRES_EMBEDDING_DIMENSIONS} for the Postgres backend (got ${config.embedder.dimensions}). The bundled migration pins the vector column to vector(${POSTGRES_EMBEDDING_DIMENSIONS}).`, "CONFIG");
840
1626
  return createPostgresClient(config, createDatabase(config.databaseUrl));
841
1627
  }
842
1628
  return createInMemoryClient(config);