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/README.md +172 -2
- package/dist/index.d.mts +757 -5
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +816 -30
- package/dist/index.mjs.map +1 -1
- package/package.json +15 -16
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("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
@@ -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) =>
|
|
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
|
|
561
|
-
|
|
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
|
-
|
|
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) =>
|
|
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
|
|
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);
|