mnemocyte 0.0.6 → 0.0.8

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, 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;
@@ -153,9 +153,35 @@ async function buildContext(options) {
153
153
  }
154
154
  //#endregion
155
155
  //#region src/errors.ts
156
+ /**
157
+ * Error type thrown by every public Mnemocyte API.
158
+ *
159
+ * Use the {@link MnemocyteError.code} property — not the message — to branch
160
+ * on failure modes, or call {@link isMnemocyteError} as a type guard when
161
+ * working with `unknown` errors.
162
+ *
163
+ * @example
164
+ * ```ts
165
+ * try {
166
+ * await client.recall({ entityId: "u1", query: "hi" });
167
+ * } catch (err) {
168
+ * if (isMnemocyteError(err) && err.code === "DB") {
169
+ * // handle database failure
170
+ * }
171
+ * throw err;
172
+ * }
173
+ * ```
174
+ */
156
175
  var MnemocyteError = class extends Error {
176
+ /** Stable, machine-readable failure code. See {@link MnemocyteErrorCode}. */
157
177
  code;
178
+ /** Underlying cause, if any (e.g. an original driver or embedder error). */
158
179
  cause;
180
+ /**
181
+ * @param message - Human-readable error message.
182
+ * @param code - Stable failure code; see {@link MnemocyteErrorCode}.
183
+ * @param cause - Optional underlying cause (driver error, network error, etc.).
184
+ */
159
185
  constructor(message, code, cause) {
160
186
  super(message);
161
187
  this.name = "MnemocyteError";
@@ -163,6 +189,238 @@ var MnemocyteError = class extends Error {
163
189
  this.cause = cause;
164
190
  }
165
191
  };
192
+ /**
193
+ * Type guard for {@link MnemocyteError}.
194
+ *
195
+ * Prefer this over `instanceof` when working across module boundaries or
196
+ * bundlers where multiple copies of the class may exist.
197
+ *
198
+ * @param error - Value to test (typically a caught `unknown`).
199
+ * @returns `true` if `error` is a {@link MnemocyteError}.
200
+ */
201
+ function isMnemocyteError(error) {
202
+ return error instanceof MnemocyteError;
203
+ }
204
+ //#endregion
205
+ //#region src/observability.ts
206
+ function withMetadata(event, metadata) {
207
+ return {
208
+ ...event,
209
+ ...metadata.entityId === void 0 ? {} : { entityId: metadata.entityId },
210
+ ...metadata.memoryId === void 0 ? {} : { memoryId: metadata.memoryId },
211
+ ...metadata.count === void 0 ? {} : { count: metadata.count }
212
+ };
213
+ }
214
+ async function emitObservation(config, event) {
215
+ try {
216
+ await config.observability?.onEvent?.(event);
217
+ } catch {
218
+ return;
219
+ }
220
+ }
221
+ async function observe(config, backend, operation, metadata, action, successMetadata) {
222
+ const startedAt = Date.now();
223
+ await emitObservation(config, withMetadata({
224
+ operation,
225
+ phase: "start",
226
+ backend,
227
+ timestamp: new Date(startedAt)
228
+ }, metadata));
229
+ try {
230
+ const result = await action();
231
+ await emitObservation(config, withMetadata({
232
+ operation,
233
+ phase: "success",
234
+ backend,
235
+ timestamp: /* @__PURE__ */ new Date(),
236
+ durationMs: Date.now() - startedAt
237
+ }, {
238
+ ...metadata,
239
+ ...successMetadata?.(result)
240
+ }));
241
+ return result;
242
+ } catch (error) {
243
+ await emitObservation(config, withMetadata({
244
+ operation,
245
+ phase: "error",
246
+ backend,
247
+ timestamp: /* @__PURE__ */ new Date(),
248
+ durationMs: Date.now() - startedAt,
249
+ error
250
+ }, metadata));
251
+ throw error;
252
+ }
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
+ };
166
424
  function assertNonEmptyString(value, field) {
167
425
  if (value.trim().length === 0) throw new MnemocyteError(`${field} must be a non-empty string.`, "VALIDATION");
168
426
  }
@@ -219,11 +477,15 @@ function rowToMemory(row) {
219
477
  updatedAt: row.updatedAt
220
478
  };
221
479
  }
222
- async function embedOne(embedder, text) {
480
+ async function embedOne(embedder, text, options = {}) {
223
481
  let embeddings;
224
482
  try {
225
- embeddings = await embedder.embed([text]);
483
+ embeddings = await withResilience((signal) => signal === void 0 ? embedder.embed([text]) : embedder.embed([text], { signal }), {
484
+ ...options.signal === void 0 ? {} : { signal: options.signal },
485
+ ...options.resilience === void 0 ? {} : { resilience: options.resilience }
486
+ });
226
487
  } catch (error) {
488
+ if (error instanceof MnemocyteError && (error.code === "TIMEOUT" || error.code === "ABORTED")) throw error;
227
489
  throw new MnemocyteError("Failed to embed text.", "EMBEDDING", error);
228
490
  }
229
491
  const embedding = embeddings[0];
@@ -254,6 +516,36 @@ function contextInputToRecallInput(input) {
254
516
  ...input.includeExpired === void 0 ? {} : { includeExpired: input.includeExpired }
255
517
  };
256
518
  }
519
+ /**
520
+ * Validate a {@link PruneInput} and throw a `"VALIDATION"`
521
+ * {@link MnemocyteError} when no selector is supplied. Mnemocyte
522
+ * deliberately rejects `prune({})` to avoid surprise full-table deletes.
523
+ */
524
+ function validatePruneInput(input) {
525
+ if (input.entityId !== void 0) assertNonEmptyString(input.entityId, "entityId");
526
+ 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");
527
+ }
528
+ /**
529
+ * Test whether `memory` matches a {@link PruneInput}. All specified
530
+ * selectors are AND-combined; unspecified selectors do not restrict.
531
+ *
532
+ * `now` is supplied by the caller so callers can share a consistent
533
+ * timestamp across a batch.
534
+ */
535
+ function matchesPruneFilter(memory, input, now) {
536
+ if (input.entityId !== void 0 && memory.entityId !== input.entityId) return false;
537
+ if (input.expired === true && !isExpired(memory, now)) return false;
538
+ if (input.superseded === true && memory.supersededBy === null) return false;
539
+ if (input.createdBefore !== void 0 && memory.createdAt.getTime() >= input.createdBefore.getTime()) return false;
540
+ if (input.notAccessedSince !== void 0) {
541
+ const last = memory.lastAccessedAt;
542
+ if (last !== null && last.getTime() >= input.notAccessedSince.getTime()) return false;
543
+ }
544
+ if (input.types !== void 0 && !input.types.includes(memory.type)) return false;
545
+ if (input.tags !== void 0 && input.tags.length > 0 && !input.tags.every((tag) => memory.tags.includes(tag))) return false;
546
+ if (input.maxImportance !== void 0 && IMPORTANCE_RANK[memory.importance] > IMPORTANCE_RANK[input.maxImportance]) return false;
547
+ return true;
548
+ }
257
549
  function matchesRecallFilter(memory, input, now) {
258
550
  if (memory.entityId !== input.entityId) return false;
259
551
  if (!input.includeSuperseded && memory.supersededBy !== null) return false;
@@ -371,107 +663,162 @@ function createInMemoryClient(config) {
371
663
  if (closed) throw new MnemocyteError("Mnemocyte client is closed.", "DB");
372
664
  }
373
665
  async function remember(input) {
374
- assertOpen();
375
- validateRememberInput(input);
376
- const embedding = await embedOne(config.embedder, input.content);
377
- const now = /* @__PURE__ */ new Date();
378
- const memory = {
379
- id: createId(),
380
- entityId: input.entityId,
381
- content: input.content,
382
- type: input.type ?? "fact",
383
- importance: input.importance ?? "normal",
384
- tags: normalizeTags(input.tags),
385
- source: input.source ?? null,
386
- metadata: input.metadata ?? {},
387
- confidence: input.confidence ?? 1,
388
- embeddingModel: config.embedder.model,
389
- embeddingDimensions: config.embedder.dimensions,
390
- supersededBy: null,
391
- expiresAt: input.expiresAt ?? null,
392
- lastAccessedAt: null,
393
- accessCount: 0,
394
- createdAt: now,
395
- updatedAt: now,
396
- embedding
397
- };
398
- memories.set(memory.id, memory);
399
- return cloneMemory(memory);
666
+ return observe(config, "in-memory", "remember", { entityId: input.entityId }, async () => {
667
+ assertOpen();
668
+ validateRememberInput(input);
669
+ const embedding = await embedOne(config.embedder, input.content, {
670
+ ...input.signal === void 0 ? {} : { signal: input.signal },
671
+ ...config.provider === void 0 ? {} : { resilience: config.provider }
672
+ });
673
+ const now = /* @__PURE__ */ new Date();
674
+ const memory = {
675
+ id: createId(),
676
+ entityId: input.entityId,
677
+ content: input.content,
678
+ type: input.type ?? "fact",
679
+ importance: input.importance ?? "normal",
680
+ tags: normalizeTags(input.tags),
681
+ source: input.source ?? null,
682
+ metadata: input.metadata ?? {},
683
+ confidence: input.confidence ?? 1,
684
+ embeddingModel: config.embedder.model,
685
+ embeddingDimensions: config.embedder.dimensions,
686
+ supersededBy: null,
687
+ expiresAt: input.expiresAt ?? null,
688
+ lastAccessedAt: null,
689
+ accessCount: 0,
690
+ createdAt: now,
691
+ updatedAt: now,
692
+ embedding
693
+ };
694
+ memories.set(memory.id, memory);
695
+ return cloneMemory(memory);
696
+ }, (memory) => ({
697
+ memoryId: memory.id,
698
+ count: 1
699
+ }));
400
700
  }
401
701
  return {
402
702
  remember,
403
703
  async rememberMany(inputs) {
404
- assertOpen();
405
- const result = [];
406
- for (const input of inputs) result.push(await remember(input));
407
- return result;
704
+ return observe(config, "in-memory", "rememberMany", { count: inputs.length }, async () => {
705
+ assertOpen();
706
+ const result = [];
707
+ for (const input of inputs) result.push(await remember(input));
708
+ return result;
709
+ }, (result) => ({ count: result.length }));
408
710
  },
409
711
  async recall(input) {
410
- assertOpen();
411
- validateRecallInput(input);
412
- const limit = input.limit ?? config.defaults?.limit ?? 10;
413
- const minScore = input.minScore ?? config.defaults?.minScore ?? 0;
414
- assertLimit(limit);
415
- assertMinScore(minScore);
416
- const queryEmbedding = await embedOne(config.embedder, input.query);
417
- const now = /* @__PURE__ */ new Date();
418
- 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)).filter((memory) => memory.score >= minScore).sort((a, b) => b.score - a.score).slice(0, limit);
419
- for (const memory of scored) {
420
- const stored = memories.get(memory.id);
421
- if (stored) {
422
- stored.lastAccessedAt = now;
423
- stored.accessCount += 1;
424
- stored.updatedAt = now;
712
+ return observe(config, "in-memory", "recall", { entityId: input.entityId }, async () => {
713
+ assertOpen();
714
+ validateRecallInput(input);
715
+ const limit = input.limit ?? config.defaults?.limit ?? 10;
716
+ const minScore = input.minScore ?? config.defaults?.minScore ?? 0;
717
+ assertLimit(limit);
718
+ assertMinScore(minScore);
719
+ const queryEmbedding = await embedOne(config.embedder, input.query, {
720
+ ...input.signal === void 0 ? {} : { signal: input.signal },
721
+ ...config.provider === void 0 ? {} : { resilience: config.provider }
722
+ });
723
+ const now = /* @__PURE__ */ new Date();
724
+ 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);
725
+ for (const memory of scored) {
726
+ const stored = memories.get(memory.id);
727
+ if (stored) {
728
+ stored.lastAccessedAt = now;
729
+ stored.accessCount += 1;
730
+ stored.updatedAt = now;
731
+ }
425
732
  }
426
- }
427
- return scored;
733
+ return scored;
734
+ }, (result) => ({ count: result.length }));
428
735
  },
429
736
  async buildContext(input) {
430
- assertOpen();
431
- return buildContext({
432
- input,
433
- recall: (contextInput) => this.recall(contextInputToRecallInput(contextInput))
737
+ return observe(config, "in-memory", "buildContext", { entityId: input.entityId }, async () => {
738
+ assertOpen();
739
+ return buildContext({
740
+ input,
741
+ recall: (contextInput) => {
742
+ const recallInput = contextInputToRecallInput(contextInput);
743
+ return this.recall(input.signal === void 0 ? recallInput : {
744
+ ...recallInput,
745
+ signal: input.signal
746
+ });
747
+ }
748
+ });
434
749
  });
435
750
  },
436
751
  async forget(input) {
437
- assertOpen();
438
- assertNonEmptyString(input.entityId, "entityId");
439
- assertNonEmptyString(input.memoryId, "memoryId");
440
- const memory = memories.get(input.memoryId);
441
- if (!memory || memory.entityId !== input.entityId) throw new MnemocyteError("Memory was not found.", "NOT_FOUND");
442
- memories.delete(input.memoryId);
752
+ return observe(config, "in-memory", "forget", {
753
+ entityId: input.entityId,
754
+ memoryId: input.memoryId
755
+ }, async () => {
756
+ assertOpen();
757
+ assertNonEmptyString(input.entityId, "entityId");
758
+ assertNonEmptyString(input.memoryId, "memoryId");
759
+ const memory = memories.get(input.memoryId);
760
+ if (!memory || memory.entityId !== input.entityId) throw new MnemocyteError("Memory was not found.", "NOT_FOUND");
761
+ memories.delete(input.memoryId);
762
+ });
443
763
  },
444
764
  async forgetAll(input) {
445
- assertOpen();
446
- assertNonEmptyString(input.entityId, "entityId");
447
- for (const [id, memory] of memories) if (memory.entityId === input.entityId) memories.delete(id);
765
+ return observe(config, "in-memory", "forgetAll", { entityId: input.entityId }, async () => {
766
+ assertOpen();
767
+ assertNonEmptyString(input.entityId, "entityId");
768
+ let deleted = 0;
769
+ for (const [id, memory] of memories) if (memory.entityId === input.entityId) {
770
+ memories.delete(id);
771
+ deleted += 1;
772
+ }
773
+ return deleted;
774
+ }, (count) => ({ count })).then(() => void 0);
775
+ },
776
+ async prune(input) {
777
+ return observe(config, "in-memory", "prune", input.entityId === void 0 ? {} : { entityId: input.entityId }, async () => {
778
+ assertOpen();
779
+ validatePruneInput(input);
780
+ const now = /* @__PURE__ */ new Date();
781
+ const matched = [];
782
+ for (const memory of memories.values()) if (matchesPruneFilter(memory, input, now)) matched.push(memory.id);
783
+ const dryRun = input.dryRun === true;
784
+ if (!dryRun) for (const id of matched) memories.delete(id);
785
+ return {
786
+ matchedCount: matched.length,
787
+ deletedCount: dryRun ? 0 : matched.length,
788
+ dryRun
789
+ };
790
+ }, (result) => ({ count: result.deletedCount }));
448
791
  },
449
792
  async stats(input) {
450
- assertOpen();
451
- const now = /* @__PURE__ */ new Date();
452
- const allMemories = Array.from(memories.values());
453
- const selected = input?.entityId ? allMemories.filter((memory) => memory.entityId === input.entityId) : allMemories;
454
- const activeMemoryCount = selected.filter((memory) => !isExpired(memory, now) && memory.supersededBy === null).length;
455
- const expiredMemoryCount = selected.filter((memory) => isExpired(memory, now)).length;
456
- const supersededMemoryCount = selected.filter((memory) => memory.supersededBy !== null).length;
457
- if (input?.entityId) return {
458
- entityId: input.entityId,
459
- memoryCount: selected.length,
460
- activeMemoryCount,
461
- expiredMemoryCount,
462
- supersededMemoryCount
463
- };
464
- return {
465
- entityCount: new Set(allMemories.map((memory) => memory.entityId)).size,
466
- memoryCount: selected.length,
467
- activeMemoryCount,
468
- expiredMemoryCount,
469
- supersededMemoryCount
470
- };
793
+ return observe(config, "in-memory", "stats", input?.entityId ? { entityId: input.entityId } : {}, async () => {
794
+ assertOpen();
795
+ const now = /* @__PURE__ */ new Date();
796
+ const allMemories = Array.from(memories.values());
797
+ const selected = input?.entityId ? allMemories.filter((memory) => memory.entityId === input.entityId) : allMemories;
798
+ const activeMemoryCount = selected.filter((memory) => !isExpired(memory, now) && memory.supersededBy === null).length;
799
+ const expiredMemoryCount = selected.filter((memory) => isExpired(memory, now)).length;
800
+ const supersededMemoryCount = selected.filter((memory) => memory.supersededBy !== null).length;
801
+ if (input?.entityId) return {
802
+ entityId: input.entityId,
803
+ memoryCount: selected.length,
804
+ activeMemoryCount,
805
+ expiredMemoryCount,
806
+ supersededMemoryCount
807
+ };
808
+ return {
809
+ entityCount: new Set(allMemories.map((memory) => memory.entityId)).size,
810
+ memoryCount: selected.length,
811
+ activeMemoryCount,
812
+ expiredMemoryCount,
813
+ supersededMemoryCount
814
+ };
815
+ });
471
816
  },
472
817
  async close() {
473
- closed = true;
474
- memories.clear();
818
+ return observe(config, "in-memory", "close", {}, async () => {
819
+ closed = true;
820
+ memories.clear();
821
+ });
475
822
  }
476
823
  };
477
824
  }
@@ -502,6 +849,15 @@ async function deleteMemory(db, entityId, memoryId) {
502
849
  async function deleteMemoriesForEntity(db, entityId) {
503
850
  return (await db.delete(memoriesTable).where(eq(memoriesTable.entityId, entityId)).returning({ id: memoriesTable.id })).length;
504
851
  }
852
+ function pruneConditions(filter) {
853
+ 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);
854
+ }
855
+ async function countPruneMatches(db, filter) {
856
+ return (await db.select({ count: sql`count(*)::int` }).from(memoriesTable).where(pruneConditions(filter)))[0]?.count ?? 0;
857
+ }
858
+ async function pruneMemories(db, filter) {
859
+ return (await db.delete(memoriesTable).where(pruneConditions(filter)).returning({ id: memoriesTable.id })).length;
860
+ }
505
861
  async function markMemoryAccessed(db, memoryIds) {
506
862
  if (memoryIds.length === 0) return;
507
863
  await db.update(memoriesTable).set({
@@ -590,7 +946,10 @@ async function lexicalSearch(db, input) {
590
946
  //#endregion
591
947
  //#region src/retrieval/index.ts
592
948
  async function hybridRecall(input) {
593
- const queryEmbedding = await embedOne(input.embedder, input.input.query);
949
+ const queryEmbedding = await embedOne(input.embedder, input.input.query, {
950
+ ...input.signal === void 0 ? {} : { signal: input.signal },
951
+ ...input.resilience === void 0 ? {} : { resilience: input.resilience }
952
+ });
594
953
  const candidateMultiplier = input.retrieval?.candidateMultiplier ?? 3;
595
954
  const candidateLimit = Math.max(input.limit, input.limit * candidateMultiplier);
596
955
  const [vectorRows, lexicalRows] = await Promise.all([vectorSearch(input.db, {
@@ -623,114 +982,211 @@ async function hybridRecall(input) {
623
982
  }
624
983
  //#endregion
625
984
  //#region src/memory/postgres.ts
985
+ const IMPORTANCE_LEVELS = [
986
+ "low",
987
+ "normal",
988
+ "high",
989
+ "critical"
990
+ ];
991
+ function importanceCeilingLevels(max) {
992
+ return IMPORTANCE_LEVELS.filter((level) => IMPORTANCE_RANK[level] <= IMPORTANCE_RANK[max]);
993
+ }
994
+ function toPruneFilter(input) {
995
+ const filter = {};
996
+ if (input.entityId !== void 0) filter.entityId = input.entityId;
997
+ if (input.expired === true) filter.expired = true;
998
+ if (input.superseded === true) filter.superseded = true;
999
+ if (input.createdBefore !== void 0) filter.createdBefore = input.createdBefore;
1000
+ if (input.notAccessedSince !== void 0) filter.notAccessedSince = input.notAccessedSince;
1001
+ if (input.types !== void 0 && input.types.length > 0) filter.types = input.types;
1002
+ if (input.tags !== void 0 && input.tags.length > 0) filter.tags = input.tags;
1003
+ if (input.maxImportance !== void 0) filter.maxImportanceLevels = importanceCeilingLevels(input.maxImportance);
1004
+ return filter;
1005
+ }
626
1006
  function createPostgresClient(config, handle) {
627
1007
  let closed = false;
628
1008
  function assertOpen() {
629
1009
  if (closed) throw new MnemocyteError("Mnemocyte client is closed.", "DB");
630
1010
  }
631
1011
  async function remember(input) {
632
- assertOpen();
633
- validateRememberInput(input);
634
- const embedding = await embedOne(config.embedder, input.content);
635
- const now = /* @__PURE__ */ new Date();
636
- return rowToMemory(await insertMemory(handle.db, {
637
- id: createId(),
638
- entityId: input.entityId,
639
- content: input.content,
640
- type: input.type ?? "fact",
641
- importance: input.importance ?? "normal",
642
- tags: normalizeTags(input.tags),
643
- source: input.source ?? null,
644
- metadata: input.metadata ?? {},
645
- confidence: input.confidence ?? 1,
646
- embedding,
647
- embeddingModel: config.embedder.model,
648
- embeddingDimensions: config.embedder.dimensions,
649
- supersededBy: null,
650
- supersededAt: null,
651
- expiresAt: input.expiresAt ?? null,
652
- lastAccessedAt: null,
653
- accessCount: 0,
654
- createdAt: now,
655
- updatedAt: now
1012
+ return observe(config, "postgres", "remember", { entityId: input.entityId }, async () => {
1013
+ assertOpen();
1014
+ validateRememberInput(input);
1015
+ const embedding = await embedOne(config.embedder, input.content, {
1016
+ ...input.signal === void 0 ? {} : { signal: input.signal },
1017
+ ...config.provider === void 0 ? {} : { resilience: config.provider }
1018
+ });
1019
+ const now = /* @__PURE__ */ new Date();
1020
+ return rowToMemory(await insertMemory(handle.db, {
1021
+ id: createId(),
1022
+ entityId: input.entityId,
1023
+ content: input.content,
1024
+ type: input.type ?? "fact",
1025
+ importance: input.importance ?? "normal",
1026
+ tags: normalizeTags(input.tags),
1027
+ source: input.source ?? null,
1028
+ metadata: input.metadata ?? {},
1029
+ confidence: input.confidence ?? 1,
1030
+ embedding,
1031
+ embeddingModel: config.embedder.model,
1032
+ embeddingDimensions: config.embedder.dimensions,
1033
+ supersededBy: null,
1034
+ supersededAt: null,
1035
+ expiresAt: input.expiresAt ?? null,
1036
+ lastAccessedAt: null,
1037
+ accessCount: 0,
1038
+ createdAt: now,
1039
+ updatedAt: now
1040
+ }));
1041
+ }, (memory) => ({
1042
+ memoryId: memory.id,
1043
+ count: 1
656
1044
  }));
657
1045
  }
658
1046
  return {
659
1047
  remember,
660
1048
  async rememberMany(inputs) {
661
- assertOpen();
662
- const result = [];
663
- for (const input of inputs) result.push(await remember(input));
664
- return result;
1049
+ return observe(config, "postgres", "rememberMany", { count: inputs.length }, async () => {
1050
+ assertOpen();
1051
+ const result = [];
1052
+ for (const input of inputs) result.push(await remember(input));
1053
+ return result;
1054
+ }, (result) => ({ count: result.length }));
665
1055
  },
666
1056
  async recall(input) {
667
- assertOpen();
668
- validateRecallInput(input);
669
- const limit = input.limit ?? config.defaults?.limit ?? 10;
670
- const minScore = input.minScore ?? config.defaults?.minScore ?? 0;
671
- assertLimit(limit);
672
- assertMinScore(minScore);
673
- return hybridRecall({
674
- db: handle.db,
675
- embedder: config.embedder,
676
- input,
677
- limit,
678
- minScore,
679
- retrieval: config.retrieval
680
- });
1057
+ return observe(config, "postgres", "recall", { entityId: input.entityId }, async () => {
1058
+ assertOpen();
1059
+ validateRecallInput(input);
1060
+ const limit = input.limit ?? config.defaults?.limit ?? 10;
1061
+ const minScore = input.minScore ?? config.defaults?.minScore ?? 0;
1062
+ assertLimit(limit);
1063
+ assertMinScore(minScore);
1064
+ return hybridRecall({
1065
+ db: handle.db,
1066
+ embedder: config.embedder,
1067
+ input,
1068
+ limit,
1069
+ minScore,
1070
+ retrieval: config.retrieval,
1071
+ ...input.signal === void 0 ? {} : { signal: input.signal },
1072
+ ...config.provider === void 0 ? {} : { resilience: config.provider }
1073
+ });
1074
+ }, (result) => ({ count: result.length }));
681
1075
  },
682
1076
  async buildContext(input) {
683
- assertOpen();
684
- return buildContext({
685
- input,
686
- recall: (contextInput) => this.recall(contextInputToRecallInput(contextInput))
1077
+ return observe(config, "postgres", "buildContext", { entityId: input.entityId }, async () => {
1078
+ assertOpen();
1079
+ return buildContext({
1080
+ input,
1081
+ recall: (contextInput) => {
1082
+ const recallInput = contextInputToRecallInput(contextInput);
1083
+ return this.recall(input.signal === void 0 ? recallInput : {
1084
+ ...recallInput,
1085
+ signal: input.signal
1086
+ });
1087
+ }
1088
+ });
687
1089
  });
688
1090
  },
689
1091
  async forget(input) {
690
- assertOpen();
691
- assertNonEmptyString(input.entityId, "entityId");
692
- assertNonEmptyString(input.memoryId, "memoryId");
693
- if (!await deleteMemory(handle.db, input.entityId, input.memoryId)) throw new MnemocyteError("Memory was not found.", "NOT_FOUND");
1092
+ return observe(config, "postgres", "forget", {
1093
+ entityId: input.entityId,
1094
+ memoryId: input.memoryId
1095
+ }, async () => {
1096
+ assertOpen();
1097
+ assertNonEmptyString(input.entityId, "entityId");
1098
+ assertNonEmptyString(input.memoryId, "memoryId");
1099
+ if (!await deleteMemory(handle.db, input.entityId, input.memoryId)) throw new MnemocyteError("Memory was not found.", "NOT_FOUND");
1100
+ });
694
1101
  },
695
1102
  async forgetAll(input) {
696
- assertOpen();
697
- assertNonEmptyString(input.entityId, "entityId");
698
- await Promise.all([deleteMemoriesForEntity(handle.db, input.entityId), deleteEventsForEntity(handle.db, input.entityId)]);
1103
+ return observe(config, "postgres", "forgetAll", { entityId: input.entityId }, async () => {
1104
+ assertOpen();
1105
+ assertNonEmptyString(input.entityId, "entityId");
1106
+ await Promise.all([deleteMemoriesForEntity(handle.db, input.entityId), deleteEventsForEntity(handle.db, input.entityId)]);
1107
+ });
1108
+ },
1109
+ async prune(input) {
1110
+ return observe(config, "postgres", "prune", input.entityId === void 0 ? {} : { entityId: input.entityId }, async () => {
1111
+ assertOpen();
1112
+ validatePruneInput(input);
1113
+ const filter = toPruneFilter(input);
1114
+ if (input.dryRun === true) return {
1115
+ matchedCount: await countPruneMatches(handle.db, filter),
1116
+ deletedCount: 0,
1117
+ dryRun: true
1118
+ };
1119
+ const deletedCount = await pruneMemories(handle.db, filter);
1120
+ return {
1121
+ matchedCount: deletedCount,
1122
+ deletedCount,
1123
+ dryRun: false
1124
+ };
1125
+ }, (result) => ({ count: result.deletedCount }));
699
1126
  },
700
1127
  async stats(input) {
701
- assertOpen();
702
- const now = /* @__PURE__ */ new Date();
703
- if (input?.entityId) {
704
- const selected = await listMemories(handle.db, {
705
- entityId: input.entityId,
706
- includeExpired: true,
707
- includeSuperseded: true
708
- });
1128
+ return observe(config, "postgres", "stats", input?.entityId ? { entityId: input.entityId } : {}, async () => {
1129
+ assertOpen();
1130
+ const now = /* @__PURE__ */ new Date();
1131
+ if (input?.entityId) {
1132
+ const selected = await listMemories(handle.db, {
1133
+ entityId: input.entityId,
1134
+ includeExpired: true,
1135
+ includeSuperseded: true
1136
+ });
1137
+ return {
1138
+ entityId: input.entityId,
1139
+ memoryCount: selected.length,
1140
+ activeMemoryCount: selected.filter((memory) => !isExpired(rowToMemory(memory), now) && memory.supersededBy === null).length,
1141
+ expiredMemoryCount: selected.filter((memory) => isExpired(rowToMemory(memory), now)).length,
1142
+ supersededMemoryCount: selected.filter((memory) => memory.supersededBy !== null).length
1143
+ };
1144
+ }
1145
+ const allMemories = await handle.db.select().from(memoriesTable);
709
1146
  return {
710
- entityId: input.entityId,
711
- memoryCount: selected.length,
712
- activeMemoryCount: selected.filter((memory) => !isExpired(rowToMemory(memory), now) && memory.supersededBy === null).length,
713
- expiredMemoryCount: selected.filter((memory) => isExpired(rowToMemory(memory), now)).length,
714
- supersededMemoryCount: selected.filter((memory) => memory.supersededBy !== null).length
1147
+ entityCount: new Set(allMemories.map((memory) => memory.entityId)).size,
1148
+ memoryCount: allMemories.length,
1149
+ activeMemoryCount: allMemories.filter((memory) => !isExpired(rowToMemory(memory), now) && memory.supersededBy === null).length,
1150
+ expiredMemoryCount: allMemories.filter((memory) => isExpired(rowToMemory(memory), now)).length,
1151
+ supersededMemoryCount: allMemories.filter((memory) => memory.supersededBy !== null).length
715
1152
  };
716
- }
717
- const allMemories = await handle.db.select().from(memoriesTable);
718
- return {
719
- entityCount: new Set(allMemories.map((memory) => memory.entityId)).size,
720
- memoryCount: allMemories.length,
721
- activeMemoryCount: allMemories.filter((memory) => !isExpired(rowToMemory(memory), now) && memory.supersededBy === null).length,
722
- expiredMemoryCount: allMemories.filter((memory) => isExpired(rowToMemory(memory), now)).length,
723
- supersededMemoryCount: allMemories.filter((memory) => memory.supersededBy !== null).length
724
- };
1153
+ });
725
1154
  },
726
1155
  async close() {
727
- closed = true;
728
- await handle.close();
1156
+ return observe(config, "postgres", "close", {}, async () => {
1157
+ closed = true;
1158
+ await handle.close();
1159
+ });
729
1160
  }
730
1161
  };
731
1162
  }
732
1163
  //#endregion
733
1164
  //#region src/client.ts
1165
+ /**
1166
+ * Create a Mnemocyte client.
1167
+ *
1168
+ * When {@link MnemocyteConfig.databaseUrl} is provided, a Postgres-backed
1169
+ * client is returned (requires the `pgvector` extension and the bundled
1170
+ * migration applied). Otherwise an in-memory client is returned, intended
1171
+ * for tests, demos, and short-lived processes.
1172
+ *
1173
+ * The returned client owns any underlying resources (e.g. the Postgres
1174
+ * connection pool). Call {@link MnemocyteClient.close} when finished to
1175
+ * release them.
1176
+ *
1177
+ * @param config - Client configuration. {@link MnemocyteConfig.embedder} is required.
1178
+ * @returns A {@link MnemocyteClient} instance.
1179
+ * @throws {MnemocyteError} With code `"CONFIG"` if `embedder` is missing or invalid,
1180
+ * or `"VALIDATION"` if `databaseUrl` is provided but empty.
1181
+ *
1182
+ * @example Postgres-backed client
1183
+ * ```ts
1184
+ * const client = createMnemocyte({
1185
+ * databaseUrl: process.env.DATABASE_URL,
1186
+ * embedder,
1187
+ * });
1188
+ * ```
1189
+ */
734
1190
  function createMnemocyte(config) {
735
1191
  assertEmbedder(config.embedder);
736
1192
  if (config.databaseUrl) {
@@ -740,6 +1196,6 @@ function createMnemocyte(config) {
740
1196
  return createInMemoryClient(config);
741
1197
  }
742
1198
  //#endregion
743
- export { MnemocyteError, createMnemocyte };
1199
+ export { MnemocyteError, createMnemocyte, isMnemocyteError };
744
1200
 
745
1201
  //# sourceMappingURL=index.mjs.map