mnemocyte 0.3.0 → 0.4.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 +171 -17
- package/dist/embedders/openai.d.mts +1 -1
- package/dist/embedders/openai.d.mts.map +1 -1
- package/dist/embedders/openai.mjs +23 -8
- package/dist/embedders/openai.mjs.map +1 -1
- package/dist/errors-B_7ZdjEO.mjs.map +1 -1
- package/dist/index.d.mts +6 -4
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1036 -523
- package/dist/index.mjs.map +1 -1
- package/dist/{types-gOPLre4V.d.mts → types-DH0inEay.d.mts} +107 -27
- package/dist/types-DH0inEay.d.mts.map +1 -0
- package/migrations/0000_initial.sql.template +2 -1
- package/migrations/0002_add_embedding_model.sql +15 -0
- package/migrations/meta/0002_snapshot.json +315 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +1 -1
- package/dist/types-gOPLre4V.d.mts.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { n as isMnemocyteError, t as MnemocyteError } from "./errors-B_7ZdjEO.mjs";
|
|
2
2
|
import { drizzle } from "drizzle-orm/postgres-js";
|
|
3
3
|
import postgres from "postgres";
|
|
4
|
-
import { and, count, countDistinct,
|
|
5
|
-
import { customType, index, integer, jsonb, pgTable, real, text, timestamp } from "drizzle-orm/pg-core";
|
|
4
|
+
import { and, count, countDistinct, eq, inArray, isNotNull, isNull, lt, or, sql } from "drizzle-orm";
|
|
5
|
+
import { PgDialect, customType, index, integer, jsonb, pgTable, real, text, timestamp } from "drizzle-orm/pg-core";
|
|
6
6
|
//#region \0rolldown/runtime.js
|
|
7
7
|
var __defProp = Object.defineProperty;
|
|
8
8
|
var __exportAll = (all, no_symbols) => {
|
|
@@ -18,7 +18,7 @@ var __exportAll = (all, no_symbols) => {
|
|
|
18
18
|
//#region src/db/vector.ts
|
|
19
19
|
function formatVectorComponent(value) {
|
|
20
20
|
if (!Number.isFinite(value)) throw new Error("Vector values must be finite numbers.");
|
|
21
|
-
return Object.is(value, -0) ? "0" : value.
|
|
21
|
+
return Object.is(value, -0) ? "0" : value.toString();
|
|
22
22
|
}
|
|
23
23
|
//#endregion
|
|
24
24
|
//#region src/db/schema.ts
|
|
@@ -72,7 +72,8 @@ const eventsTable = pgTable("mnemocyte_events", {
|
|
|
72
72
|
}, (table) => [index("mnemocyte_events_entity_time_idx").on(table.entityId, table.timestamp)]);
|
|
73
73
|
const metaTable = pgTable("mnemocyte_meta", {
|
|
74
74
|
key: text("key").primaryKey(),
|
|
75
|
-
embeddingDimensions: integer("embedding_dimensions").notNull()
|
|
75
|
+
embeddingDimensions: integer("embedding_dimensions").notNull(),
|
|
76
|
+
embeddingModel: text("embedding_model")
|
|
76
77
|
});
|
|
77
78
|
//#endregion
|
|
78
79
|
//#region src/db/index.ts
|
|
@@ -89,6 +90,7 @@ function createDatabase(databaseUrl) {
|
|
|
89
90
|
} catch (error) {
|
|
90
91
|
throw new MnemocyteError("databaseUrl must be a valid Postgres connection URL.", "CONFIG", error);
|
|
91
92
|
}
|
|
93
|
+
if (url.protocol !== "postgres:" && url.protocol !== "postgresql:") throw new MnemocyteError("databaseUrl must use the postgres: or postgresql: protocol.", "CONFIG");
|
|
92
94
|
const ssl = parseSslFromUrl(url);
|
|
93
95
|
const isPooler = url.port === "6543" || url.searchParams.get("pgbouncer") === "true";
|
|
94
96
|
const client = postgres(databaseUrl, {
|
|
@@ -106,6 +108,179 @@ function createDatabase(databaseUrl) {
|
|
|
106
108
|
};
|
|
107
109
|
}
|
|
108
110
|
//#endregion
|
|
111
|
+
//#region src/resilience.ts
|
|
112
|
+
/**
|
|
113
|
+
* Default per-provider-call timeout. Disabled by default so that callers
|
|
114
|
+
* who do not configure {@link ProviderResilienceConfig.timeoutMs} are
|
|
115
|
+
* unaffected.
|
|
116
|
+
*/
|
|
117
|
+
const DEFAULT_TIMEOUT_MS = 0;
|
|
118
|
+
/** Default number of retry attempts on transient provider failures. */
|
|
119
|
+
const DEFAULT_RETRIES = 0;
|
|
120
|
+
/** Default base backoff delay used between retry attempts. */
|
|
121
|
+
const DEFAULT_BASE_DELAY_MS = 100;
|
|
122
|
+
/** Default ceiling for retry backoff delay. */
|
|
123
|
+
const DEFAULT_MAX_DELAY_MS = 2e3;
|
|
124
|
+
/**
|
|
125
|
+
* Throw a {@link MnemocyteError} with code `"ABORTED"` if `signal` is
|
|
126
|
+
* already aborted. Safe to call with `undefined`.
|
|
127
|
+
*/
|
|
128
|
+
function throwIfAborted(signal) {
|
|
129
|
+
if (signal?.aborted) throw new MnemocyteError("Operation was aborted.", "ABORTED", signal.reason);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Heuristic default for whether an error should be retried.
|
|
133
|
+
*
|
|
134
|
+
* Conservative by design: only retries on common transient failure
|
|
135
|
+
* indicators (network errors, 5xx-ish messages, `ECONN*` etc.). Never
|
|
136
|
+
* retries on validation, configuration, or {@link MnemocyteError} of
|
|
137
|
+
* code `"VALIDATION"` / `"CONFIG"` / `"CONFLICT"` / `"ABORTED"`.
|
|
138
|
+
*/
|
|
139
|
+
function defaultShouldRetry(error) {
|
|
140
|
+
if (error instanceof MnemocyteError) {
|
|
141
|
+
if (error.code === "VALIDATION" || error.code === "CONFIG" || error.code === "CONFLICT" || error.code === "ABORTED") return false;
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
if (typeof error === "object" && error !== null && "status" in error && typeof error.status === "number" && [
|
|
145
|
+
429,
|
|
146
|
+
500,
|
|
147
|
+
502,
|
|
148
|
+
503,
|
|
149
|
+
504
|
|
150
|
+
].includes(error.status)) return true;
|
|
151
|
+
if (error instanceof Error) {
|
|
152
|
+
const message = error.message.toLowerCase();
|
|
153
|
+
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;
|
|
154
|
+
}
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Compute the backoff delay for the given retry attempt, using exponential
|
|
159
|
+
* growth capped at {@link ProviderResilienceConfig.maxDelayMs}.
|
|
160
|
+
*
|
|
161
|
+
* `attempt` is 1-indexed: the delay before the first retry uses
|
|
162
|
+
* `attempt = 1`, before the second retry `attempt = 2`, etc.
|
|
163
|
+
*/
|
|
164
|
+
function computeDelayMs(attempt, baseDelayMs, maxDelayMs) {
|
|
165
|
+
const exponential = baseDelayMs * 2 ** (attempt - 1);
|
|
166
|
+
return Math.max(0, Math.min(maxDelayMs, exponential));
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Promise-based `setTimeout` that rejects with an `"ABORTED"`
|
|
170
|
+
* {@link MnemocyteError} if `signal` aborts while waiting.
|
|
171
|
+
*/
|
|
172
|
+
function delay(ms, signal) {
|
|
173
|
+
if (ms <= 0) {
|
|
174
|
+
throwIfAborted(signal);
|
|
175
|
+
return Promise.resolve();
|
|
176
|
+
}
|
|
177
|
+
return new Promise((resolve, reject) => {
|
|
178
|
+
const timer = setTimeout(() => {
|
|
179
|
+
signal?.removeEventListener("abort", onAbort);
|
|
180
|
+
resolve();
|
|
181
|
+
}, ms);
|
|
182
|
+
const onAbort = () => {
|
|
183
|
+
clearTimeout(timer);
|
|
184
|
+
reject(new MnemocyteError("Operation was aborted.", "ABORTED", signal?.reason));
|
|
185
|
+
};
|
|
186
|
+
if (signal) {
|
|
187
|
+
if (signal.aborted) {
|
|
188
|
+
clearTimeout(timer);
|
|
189
|
+
reject(new MnemocyteError("Operation was aborted.", "ABORTED", signal.reason));
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
function timeoutError(timeoutMs) {
|
|
197
|
+
return new MnemocyteError(`Provider call timed out after ${timeoutMs}ms.`, "TIMEOUT");
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Run one provider attempt with a signal linked to the caller's signal and
|
|
201
|
+
* Mnemocyte's configured timeout. Timeout aborts are actively propagated to
|
|
202
|
+
* providers that honor `AbortSignal`.
|
|
203
|
+
*/
|
|
204
|
+
function runAttempt(action, timeoutMs, signal) {
|
|
205
|
+
if (timeoutMs <= 0 && !signal) return action(void 0);
|
|
206
|
+
return new Promise((resolve, reject) => {
|
|
207
|
+
let settled = false;
|
|
208
|
+
const controller = new AbortController();
|
|
209
|
+
const timer = timeoutMs > 0 ? setTimeout(() => {
|
|
210
|
+
if (settled) return;
|
|
211
|
+
settled = true;
|
|
212
|
+
signal?.removeEventListener("abort", onAbort);
|
|
213
|
+
controller.abort(timeoutError(timeoutMs));
|
|
214
|
+
reject(timeoutError(timeoutMs));
|
|
215
|
+
}, timeoutMs) : void 0;
|
|
216
|
+
const onAbort = () => {
|
|
217
|
+
if (settled) return;
|
|
218
|
+
settled = true;
|
|
219
|
+
if (timer) clearTimeout(timer);
|
|
220
|
+
controller.abort(signal?.reason);
|
|
221
|
+
reject(new MnemocyteError("Operation was aborted.", "ABORTED", signal?.reason));
|
|
222
|
+
};
|
|
223
|
+
if (signal) {
|
|
224
|
+
if (signal.aborted) {
|
|
225
|
+
if (timer) clearTimeout(timer);
|
|
226
|
+
settled = true;
|
|
227
|
+
controller.abort(signal.reason);
|
|
228
|
+
reject(new MnemocyteError("Operation was aborted.", "ABORTED", signal.reason));
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
232
|
+
}
|
|
233
|
+
action(controller.signal).then((value) => {
|
|
234
|
+
if (settled) return;
|
|
235
|
+
settled = true;
|
|
236
|
+
if (timer) clearTimeout(timer);
|
|
237
|
+
signal?.removeEventListener("abort", onAbort);
|
|
238
|
+
resolve(value);
|
|
239
|
+
}, (error) => {
|
|
240
|
+
if (settled) return;
|
|
241
|
+
settled = true;
|
|
242
|
+
if (timer) clearTimeout(timer);
|
|
243
|
+
signal?.removeEventListener("abort", onAbort);
|
|
244
|
+
reject(error);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Run `action` with timeout, retry, and abort handling derived from
|
|
250
|
+
* `options.resilience` and `options.signal`. Each retry attempt receives
|
|
251
|
+
* a fresh `AbortSignal` linked to the caller's signal — so callers may
|
|
252
|
+
* abort mid-retry and the in-flight attempt will cancel promptly if the
|
|
253
|
+
* underlying provider honours signals.
|
|
254
|
+
*
|
|
255
|
+
* Errors from {@link MnemocyteError} with code `"VALIDATION"` /
|
|
256
|
+
* `"CONFIG"` / `"CONFLICT"` / `"ABORTED"` are never retried.
|
|
257
|
+
*/
|
|
258
|
+
async function withResilience(action, options = {}) {
|
|
259
|
+
const config = options.resilience ?? {};
|
|
260
|
+
const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
261
|
+
const retries = Math.max(0, config.retries ?? DEFAULT_RETRIES);
|
|
262
|
+
const baseDelayMs = Math.max(0, config.baseDelayMs ?? DEFAULT_BASE_DELAY_MS);
|
|
263
|
+
const maxDelayMs = Math.max(baseDelayMs, config.maxDelayMs ?? DEFAULT_MAX_DELAY_MS);
|
|
264
|
+
const shouldRetry = config.shouldRetry ?? defaultShouldRetry;
|
|
265
|
+
const externalSignal = options.signal;
|
|
266
|
+
throwIfAborted(externalSignal);
|
|
267
|
+
const maxAttempts = retries + 1;
|
|
268
|
+
let lastError;
|
|
269
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
270
|
+
throwIfAborted(externalSignal);
|
|
271
|
+
try {
|
|
272
|
+
return await runAttempt(action, timeoutMs, externalSignal);
|
|
273
|
+
} catch (error) {
|
|
274
|
+
lastError = error;
|
|
275
|
+
if (error instanceof MnemocyteError && error.code === "ABORTED") throw error;
|
|
276
|
+
if (attempt >= maxAttempts) throw error;
|
|
277
|
+
if (!shouldRetry(error, attempt)) throw error;
|
|
278
|
+
await delay(computeDelayMs(attempt, baseDelayMs, maxDelayMs), externalSignal);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
throw lastError;
|
|
282
|
+
}
|
|
283
|
+
//#endregion
|
|
109
284
|
//#region src/memory/json.ts
|
|
110
285
|
function invalidJsonValue(path, cause) {
|
|
111
286
|
return new MnemocyteError(`${path} must contain only JSON-compatible value data.`, "VALIDATION", cause);
|
|
@@ -152,10 +327,6 @@ function cloneJsonObject(value, path = "metadata") {
|
|
|
152
327
|
throw invalidJsonValue(path, error);
|
|
153
328
|
}
|
|
154
329
|
}
|
|
155
|
-
/** Assert that a runtime value is a JSON-compatible object. */
|
|
156
|
-
function validateJsonObject(value, path = "metadata") {
|
|
157
|
-
cloneJsonObject(value, path);
|
|
158
|
-
}
|
|
159
330
|
//#endregion
|
|
160
331
|
//#region src/memory/records.ts
|
|
161
332
|
function normalizeTags(tags) {
|
|
@@ -239,6 +410,9 @@ function clampScore(value) {
|
|
|
239
410
|
if (!Number.isFinite(value)) return 0;
|
|
240
411
|
return Math.max(0, Math.min(1, value));
|
|
241
412
|
}
|
|
413
|
+
function toVectorScore(cosineSimilarity) {
|
|
414
|
+
return clampScore(cosineSimilarity);
|
|
415
|
+
}
|
|
242
416
|
function normalizeWeights(weights) {
|
|
243
417
|
const merged = {
|
|
244
418
|
...DEFAULT_RETRIEVAL_WEIGHTS,
|
|
@@ -321,6 +495,10 @@ function markdownFence(value) {
|
|
|
321
495
|
const longestBacktickRun = Math.max(0, ...Array.from(value.matchAll(/`+/g), (match) => match[0].length));
|
|
322
496
|
return "`".repeat(Math.max(3, longestBacktickRun + 1));
|
|
323
497
|
}
|
|
498
|
+
function plainFence(values) {
|
|
499
|
+
const longestEqualsRun = Math.max(0, ...values.flatMap((value) => Array.from(value.matchAll(/=+/g), (match) => match[0].length)));
|
|
500
|
+
return "=".repeat(Math.max(8, longestEqualsRun + 1));
|
|
501
|
+
}
|
|
324
502
|
function formatMarkdown(memories, query, omittedCount) {
|
|
325
503
|
const lines = [
|
|
326
504
|
"# Memory Context",
|
|
@@ -337,6 +515,11 @@ function formatMarkdown(memories, query, omittedCount) {
|
|
|
337
515
|
return lines.join("\n");
|
|
338
516
|
}
|
|
339
517
|
function formatPlain(memories, query, omittedCount) {
|
|
518
|
+
const formattedMemories = memories.map((memory) => ({
|
|
519
|
+
memory,
|
|
520
|
+
metadata: formatMetadata(memory)
|
|
521
|
+
}));
|
|
522
|
+
const fence = plainFence([query, ...formattedMemories.flatMap(({ memory, metadata }) => [metadata, memory.content])]);
|
|
340
523
|
const lines = [
|
|
341
524
|
"MEMORY CONTEXT",
|
|
342
525
|
"",
|
|
@@ -344,7 +527,7 @@ function formatPlain(memories, query, omittedCount) {
|
|
|
344
527
|
"",
|
|
345
528
|
"RELEVANT MEMORIES"
|
|
346
529
|
];
|
|
347
|
-
for (const [index, memory] of
|
|
530
|
+
for (const [index, { memory, metadata }] of formattedMemories.entries()) lines.push("", `${fence} MEMORY ${index + 1} START ${fence}`, metadata, memory.content, `${fence} MEMORY ${index + 1} END ${fence}`);
|
|
348
531
|
if (omittedCount > 0) lines.push("", `[${omittedCount} memories omitted to fit token budget]`);
|
|
349
532
|
return lines.join("\n");
|
|
350
533
|
}
|
|
@@ -364,6 +547,7 @@ function formatContext(memories, query, format, omittedCount = 0) {
|
|
|
364
547
|
}
|
|
365
548
|
//#endregion
|
|
366
549
|
//#region src/context/tokens.ts
|
|
550
|
+
const TRUNCATION_MARKER = "[truncated to fit token budget]";
|
|
367
551
|
const heuristicTokenCounter = { count(text) {
|
|
368
552
|
return Math.ceil(text.length / 4);
|
|
369
553
|
} };
|
|
@@ -374,13 +558,18 @@ function trimToTokenBudget(text, maxTokens, tokenCounter) {
|
|
|
374
558
|
let best = "";
|
|
375
559
|
while (low <= high) {
|
|
376
560
|
const mid = Math.floor((low + high) / 2);
|
|
377
|
-
const output = `${text.slice(0, mid).trimEnd()}\n
|
|
561
|
+
const output = `${text.slice(0, mid).trimEnd()}\n${TRUNCATION_MARKER}`;
|
|
378
562
|
if (tokenCounter.count(output) <= maxTokens) {
|
|
379
563
|
best = output;
|
|
380
564
|
low = mid + 1;
|
|
381
565
|
} else high = mid - 1;
|
|
382
566
|
}
|
|
383
|
-
|
|
567
|
+
if (best) return best;
|
|
568
|
+
for (let length = 31; length > 0; length -= 1) {
|
|
569
|
+
const markerFragment = TRUNCATION_MARKER.slice(0, length);
|
|
570
|
+
if (tokenCounter.count(markerFragment) <= maxTokens) return markerFragment;
|
|
571
|
+
}
|
|
572
|
+
return "";
|
|
384
573
|
}
|
|
385
574
|
//#endregion
|
|
386
575
|
//#region src/context/builder.ts
|
|
@@ -419,230 +608,71 @@ async function emitObservation(config, event) {
|
|
|
419
608
|
await config.observability?.onEvent?.(event);
|
|
420
609
|
} catch {
|
|
421
610
|
return;
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
async function
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
...
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
const
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
};
|
|
468
|
-
//#endregion
|
|
469
|
-
//#region src/resilience.ts
|
|
470
|
-
/**
|
|
471
|
-
* Default per-provider-call timeout. Disabled by default so that callers
|
|
472
|
-
* who do not configure {@link ProviderResilienceConfig.timeoutMs} are
|
|
473
|
-
* unaffected.
|
|
474
|
-
*/
|
|
475
|
-
const DEFAULT_TIMEOUT_MS = 0;
|
|
476
|
-
/** Default number of retry attempts on transient provider failures. */
|
|
477
|
-
const DEFAULT_RETRIES = 0;
|
|
478
|
-
/** Default base backoff delay used between retry attempts. */
|
|
479
|
-
const DEFAULT_BASE_DELAY_MS = 100;
|
|
480
|
-
/** Default ceiling for retry backoff delay. */
|
|
481
|
-
const DEFAULT_MAX_DELAY_MS = 2e3;
|
|
482
|
-
/**
|
|
483
|
-
* Throw a {@link MnemocyteError} with code `"ABORTED"` if `signal` is
|
|
484
|
-
* already aborted. Safe to call with `undefined`.
|
|
485
|
-
*/
|
|
486
|
-
function throwIfAborted(signal) {
|
|
487
|
-
if (signal?.aborted) throw new MnemocyteError("Operation was aborted.", "ABORTED", signal.reason);
|
|
488
|
-
}
|
|
489
|
-
/**
|
|
490
|
-
* Heuristic default for whether an error should be retried.
|
|
491
|
-
*
|
|
492
|
-
* Conservative by design: only retries on common transient failure
|
|
493
|
-
* indicators (network errors, 5xx-ish messages, `ECONN*` etc.). Never
|
|
494
|
-
* retries on validation, configuration, or {@link MnemocyteError} of
|
|
495
|
-
* code `"VALIDATION"` / `"CONFIG"` / `"ABORTED"`.
|
|
496
|
-
*/
|
|
497
|
-
function defaultShouldRetry(error) {
|
|
498
|
-
if (error instanceof MnemocyteError) {
|
|
499
|
-
if (error.code === "VALIDATION" || error.code === "CONFIG" || error.code === "ABORTED") return false;
|
|
500
|
-
return true;
|
|
501
|
-
}
|
|
502
|
-
if (typeof error === "object" && error !== null && "status" in error && typeof error.status === "number" && [
|
|
503
|
-
429,
|
|
504
|
-
500,
|
|
505
|
-
502,
|
|
506
|
-
503,
|
|
507
|
-
504
|
|
508
|
-
].includes(error.status)) return true;
|
|
509
|
-
if (error instanceof Error) {
|
|
510
|
-
const message = error.message.toLowerCase();
|
|
511
|
-
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;
|
|
512
|
-
}
|
|
513
|
-
return false;
|
|
514
|
-
}
|
|
515
|
-
/**
|
|
516
|
-
* Compute the backoff delay for the given retry attempt, using exponential
|
|
517
|
-
* growth capped at {@link ProviderResilienceConfig.maxDelayMs}.
|
|
518
|
-
*
|
|
519
|
-
* `attempt` is 1-indexed: the delay before the first retry uses
|
|
520
|
-
* `attempt = 1`, before the second retry `attempt = 2`, etc.
|
|
521
|
-
*/
|
|
522
|
-
function computeDelayMs(attempt, baseDelayMs, maxDelayMs) {
|
|
523
|
-
const exponential = baseDelayMs * 2 ** (attempt - 1);
|
|
524
|
-
return Math.max(0, Math.min(maxDelayMs, exponential));
|
|
525
|
-
}
|
|
526
|
-
/**
|
|
527
|
-
* Promise-based `setTimeout` that rejects with an `"ABORTED"`
|
|
528
|
-
* {@link MnemocyteError} if `signal` aborts while waiting.
|
|
529
|
-
*/
|
|
530
|
-
function delay(ms, signal) {
|
|
531
|
-
if (ms <= 0) {
|
|
532
|
-
throwIfAborted(signal);
|
|
533
|
-
return Promise.resolve();
|
|
534
|
-
}
|
|
535
|
-
return new Promise((resolve, reject) => {
|
|
536
|
-
const timer = setTimeout(() => {
|
|
537
|
-
signal?.removeEventListener("abort", onAbort);
|
|
538
|
-
resolve();
|
|
539
|
-
}, ms);
|
|
540
|
-
const onAbort = () => {
|
|
541
|
-
clearTimeout(timer);
|
|
542
|
-
reject(new MnemocyteError("Operation was aborted.", "ABORTED", signal?.reason));
|
|
543
|
-
};
|
|
544
|
-
if (signal) {
|
|
545
|
-
if (signal.aborted) {
|
|
546
|
-
clearTimeout(timer);
|
|
547
|
-
reject(new MnemocyteError("Operation was aborted.", "ABORTED", signal.reason));
|
|
548
|
-
return;
|
|
549
|
-
}
|
|
550
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
551
|
-
}
|
|
552
|
-
});
|
|
553
|
-
}
|
|
554
|
-
function timeoutError(timeoutMs) {
|
|
555
|
-
return new MnemocyteError(`Provider call timed out after ${timeoutMs}ms.`, "TIMEOUT");
|
|
556
|
-
}
|
|
557
|
-
/**
|
|
558
|
-
* Run one provider attempt with a signal linked to the caller's signal and
|
|
559
|
-
* Mnemocyte's configured timeout. Timeout aborts are actively propagated to
|
|
560
|
-
* providers that honor `AbortSignal`.
|
|
561
|
-
*/
|
|
562
|
-
function runAttempt(action, timeoutMs, signal) {
|
|
563
|
-
if (timeoutMs <= 0 && !signal) return action(void 0);
|
|
564
|
-
return new Promise((resolve, reject) => {
|
|
565
|
-
let settled = false;
|
|
566
|
-
const controller = new AbortController();
|
|
567
|
-
const timer = timeoutMs > 0 ? setTimeout(() => {
|
|
568
|
-
if (settled) return;
|
|
569
|
-
settled = true;
|
|
570
|
-
signal?.removeEventListener("abort", onAbort);
|
|
571
|
-
controller.abort(timeoutError(timeoutMs));
|
|
572
|
-
reject(timeoutError(timeoutMs));
|
|
573
|
-
}, timeoutMs) : void 0;
|
|
574
|
-
const onAbort = () => {
|
|
575
|
-
if (settled) return;
|
|
576
|
-
settled = true;
|
|
577
|
-
if (timer) clearTimeout(timer);
|
|
578
|
-
controller.abort(signal?.reason);
|
|
579
|
-
reject(new MnemocyteError("Operation was aborted.", "ABORTED", signal?.reason));
|
|
580
|
-
};
|
|
581
|
-
if (signal) {
|
|
582
|
-
if (signal.aborted) {
|
|
583
|
-
if (timer) clearTimeout(timer);
|
|
584
|
-
settled = true;
|
|
585
|
-
controller.abort(signal.reason);
|
|
586
|
-
reject(new MnemocyteError("Operation was aborted.", "ABORTED", signal.reason));
|
|
587
|
-
return;
|
|
588
|
-
}
|
|
589
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
590
|
-
}
|
|
591
|
-
action(controller.signal).then((value) => {
|
|
592
|
-
if (settled) return;
|
|
593
|
-
settled = true;
|
|
594
|
-
if (timer) clearTimeout(timer);
|
|
595
|
-
signal?.removeEventListener("abort", onAbort);
|
|
596
|
-
resolve(value);
|
|
597
|
-
}, (error) => {
|
|
598
|
-
if (settled) return;
|
|
599
|
-
settled = true;
|
|
600
|
-
if (timer) clearTimeout(timer);
|
|
601
|
-
signal?.removeEventListener("abort", onAbort);
|
|
602
|
-
reject(error);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
async function observeFromStartedAt(config, backend, operation, metadata, startedAt, action, successMetadata) {
|
|
614
|
+
await emitObservation(config, withMetadata({
|
|
615
|
+
operation,
|
|
616
|
+
phase: "start",
|
|
617
|
+
backend,
|
|
618
|
+
timestamp: new Date(startedAt)
|
|
619
|
+
}, metadata));
|
|
620
|
+
try {
|
|
621
|
+
const result = await action();
|
|
622
|
+
await emitObservation(config, withMetadata({
|
|
623
|
+
operation,
|
|
624
|
+
phase: "success",
|
|
625
|
+
backend,
|
|
626
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
627
|
+
durationMs: Date.now() - startedAt
|
|
628
|
+
}, {
|
|
629
|
+
...metadata,
|
|
630
|
+
...successMetadata?.(result)
|
|
631
|
+
}));
|
|
632
|
+
return result;
|
|
633
|
+
} catch (error) {
|
|
634
|
+
await emitObservation(config, withMetadata({
|
|
635
|
+
operation,
|
|
636
|
+
phase: "error",
|
|
637
|
+
backend,
|
|
638
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
639
|
+
durationMs: Date.now() - startedAt,
|
|
640
|
+
error
|
|
641
|
+
}, metadata));
|
|
642
|
+
throw error;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
function observe(config, backend, operation, metadata, action, successMetadata) {
|
|
646
|
+
return observeFromStartedAt(config, backend, operation, metadata, Date.now(), action, successMetadata);
|
|
647
|
+
}
|
|
648
|
+
function observePrepared(config, backend, operation, metadata, prepare, action, successMetadata) {
|
|
649
|
+
const startedAt = Date.now();
|
|
650
|
+
try {
|
|
651
|
+
const prepared = prepare();
|
|
652
|
+
return observeFromStartedAt(config, backend, operation, metadata, startedAt, () => action(prepared), successMetadata);
|
|
653
|
+
} catch (error) {
|
|
654
|
+
return observeFromStartedAt(config, backend, operation, metadata, startedAt, async () => {
|
|
655
|
+
throw error;
|
|
603
656
|
});
|
|
604
|
-
}
|
|
657
|
+
}
|
|
605
658
|
}
|
|
606
659
|
/**
|
|
607
|
-
*
|
|
608
|
-
* `
|
|
609
|
-
*
|
|
610
|
-
* abort mid-retry and the in-flight attempt will cancel promptly if the
|
|
611
|
-
* underlying provider honours signals.
|
|
612
|
-
*
|
|
613
|
-
* Errors from {@link MnemocyteError} with code `"VALIDATION"` /
|
|
614
|
-
* `"CONFIG"` / `"ABORTED"` are never retried.
|
|
660
|
+
* Ordering of {@link ImportanceLevel} from least to most important. Used
|
|
661
|
+
* by `prune({ maxImportance })` to compare a memory's importance against
|
|
662
|
+
* the caller-supplied ceiling.
|
|
615
663
|
*/
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
const shouldRetry = config.shouldRetry ?? defaultShouldRetry;
|
|
623
|
-
const externalSignal = options.signal;
|
|
624
|
-
throwIfAborted(externalSignal);
|
|
625
|
-
const maxAttempts = retries + 1;
|
|
626
|
-
let lastError;
|
|
627
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
628
|
-
throwIfAborted(externalSignal);
|
|
629
|
-
try {
|
|
630
|
-
return await runAttempt(action, timeoutMs, externalSignal);
|
|
631
|
-
} catch (error) {
|
|
632
|
-
lastError = error;
|
|
633
|
-
if (error instanceof MnemocyteError && error.code === "ABORTED") throw error;
|
|
634
|
-
if (attempt >= maxAttempts) throw error;
|
|
635
|
-
if (!shouldRetry(error, attempt)) throw error;
|
|
636
|
-
await delay(computeDelayMs(attempt, baseDelayMs, maxDelayMs), externalSignal);
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
throw lastError;
|
|
640
|
-
}
|
|
664
|
+
const IMPORTANCE_RANK = {
|
|
665
|
+
low: 0,
|
|
666
|
+
normal: 1,
|
|
667
|
+
high: 2,
|
|
668
|
+
critical: 3
|
|
669
|
+
};
|
|
641
670
|
//#endregion
|
|
642
671
|
//#region src/memory/embeddings.ts
|
|
643
672
|
function validateEmbedding(embedding, dimensions) {
|
|
644
673
|
if (embedding.length !== dimensions) throw new MnemocyteError("Embedder returned an embedding with unexpected dimensions.", "EMBEDDING");
|
|
645
674
|
if (embedding.some((component) => !Number.isFinite(component))) throw new MnemocyteError("Embedder returned an embedding with non-finite values.", "EMBEDDING");
|
|
675
|
+
if (embedding.every((component) => component === 0)) throw new MnemocyteError("Embedder returned an embedding with zero norm.", "EMBEDDING");
|
|
646
676
|
}
|
|
647
677
|
async function embedOne(embedder, text, options = {}) {
|
|
648
678
|
let embeddings;
|
|
@@ -677,7 +707,48 @@ async function embedMany(embedder, texts, options = {}) {
|
|
|
677
707
|
return embeddings;
|
|
678
708
|
}
|
|
679
709
|
//#endregion
|
|
710
|
+
//#region src/memory/store.ts
|
|
711
|
+
function hasPruneSelector(filter) {
|
|
712
|
+
return filter.entityId !== void 0 || filter.expired === true || filter.superseded === true || filter.createdBefore !== void 0 || filter.notAccessedSince !== void 0 || filter.types !== void 0 && filter.types.length > 0 || filter.tags !== void 0 && filter.tags.length > 0 || filter.maxImportance !== void 0;
|
|
713
|
+
}
|
|
714
|
+
function assertPruneFilterHasSelector(filter) {
|
|
715
|
+
if (!hasPruneSelector(filter)) throw new MnemocyteError("prune requires at least one selector (entityId, expired, superseded, createdBefore, notAccessedSince, types, tags, or maxImportance).", "VALIDATION");
|
|
716
|
+
}
|
|
717
|
+
//#endregion
|
|
680
718
|
//#region src/memory/validation.ts
|
|
719
|
+
const MEMORY_TYPES = [
|
|
720
|
+
"fact",
|
|
721
|
+
"preference",
|
|
722
|
+
"instruction",
|
|
723
|
+
"backstory",
|
|
724
|
+
"episode",
|
|
725
|
+
"session"
|
|
726
|
+
];
|
|
727
|
+
const IMPORTANCE_LEVELS$1 = [
|
|
728
|
+
"low",
|
|
729
|
+
"normal",
|
|
730
|
+
"high",
|
|
731
|
+
"critical"
|
|
732
|
+
];
|
|
733
|
+
function isMemoryType(value) {
|
|
734
|
+
return typeof value === "string" && MEMORY_TYPES.includes(value);
|
|
735
|
+
}
|
|
736
|
+
function isImportanceLevel(value) {
|
|
737
|
+
return typeof value === "string" && IMPORTANCE_LEVELS$1.includes(value);
|
|
738
|
+
}
|
|
739
|
+
function assertMemoryType(value, field) {
|
|
740
|
+
if (!isMemoryType(value)) throw new MnemocyteError(`${field} must be a known memory type.`, "VALIDATION");
|
|
741
|
+
}
|
|
742
|
+
function assertImportanceLevel(value, field) {
|
|
743
|
+
if (!isImportanceLevel(value)) throw new MnemocyteError(`${field} must be a known importance level.`, "VALIDATION");
|
|
744
|
+
}
|
|
745
|
+
function assertMemoryTypeArray(value, field) {
|
|
746
|
+
if (!Array.isArray(value)) throw new MnemocyteError(`${field} must be an array.`, "VALIDATION");
|
|
747
|
+
for (const item of value) if (!isMemoryType(item)) throw new MnemocyteError(`${field} must contain only known memory types.`, "VALIDATION");
|
|
748
|
+
}
|
|
749
|
+
function assertValidDate(value, field) {
|
|
750
|
+
if (!(value instanceof Date) || !Number.isFinite(value.getTime())) throw new MnemocyteError(`${field} must be a valid Date.`, "VALIDATION");
|
|
751
|
+
}
|
|
681
752
|
function assertNonEmptyString(value, field, code = "VALIDATION") {
|
|
682
753
|
if (typeof value !== "string" || value.trim().length === 0) throw new MnemocyteError(`${field} must be a non-empty string.`, code);
|
|
683
754
|
}
|
|
@@ -716,20 +787,39 @@ function validateRetrievalConfig(config) {
|
|
|
716
787
|
if (config.accessSaturation !== void 0) assertPositiveFiniteConfigValue(config.accessSaturation, "retrieval.accessSaturation");
|
|
717
788
|
if (config.candidateMultiplier !== void 0 && (!Number.isInteger(config.candidateMultiplier) || config.candidateMultiplier < 1)) throw new MnemocyteError("retrieval.candidateMultiplier must be an integer greater than or equal to 1.", "CONFIG");
|
|
718
789
|
}
|
|
790
|
+
function validateProviderResilienceConfig(config) {
|
|
791
|
+
if (config === void 0) return;
|
|
792
|
+
if (typeof config !== "object" || config === null || Array.isArray(config)) throw new MnemocyteError("provider must be an object.", "CONFIG");
|
|
793
|
+
for (const [field, value] of [
|
|
794
|
+
["provider.timeoutMs", config.timeoutMs],
|
|
795
|
+
["provider.baseDelayMs", config.baseDelayMs],
|
|
796
|
+
["provider.maxDelayMs", config.maxDelayMs]
|
|
797
|
+
]) if (value !== void 0 && (!Number.isFinite(value) || value < 0)) throw new MnemocyteError(`${field} must be a non-negative finite number.`, "CONFIG");
|
|
798
|
+
if (config.retries !== void 0 && (!Number.isInteger(config.retries) || config.retries < 0)) throw new MnemocyteError("provider.retries must be a non-negative integer.", "CONFIG");
|
|
799
|
+
if (config.shouldRetry !== void 0 && typeof config.shouldRetry !== "function") throw new MnemocyteError("provider.shouldRetry must be a function.", "CONFIG");
|
|
800
|
+
}
|
|
719
801
|
function validateBuildContextInput(input) {
|
|
720
802
|
if (input.maxTokens !== void 0 && (!Number.isInteger(input.maxTokens) || input.maxTokens < 1)) throw new MnemocyteError("maxTokens must be a positive integer when supplied.", "VALIDATION");
|
|
721
803
|
}
|
|
722
804
|
function validateRememberInput(input) {
|
|
723
805
|
assertNonEmptyString(input.entityId, "entityId");
|
|
724
806
|
assertNonEmptyString(input.content, "content");
|
|
725
|
-
if (input.
|
|
807
|
+
if (input.type !== void 0) assertMemoryType(input.type, "type");
|
|
808
|
+
if (input.importance !== void 0) assertImportanceLevel(input.importance, "importance");
|
|
809
|
+
if (input.tags !== void 0) {
|
|
810
|
+
if (!Array.isArray(input.tags)) throw new MnemocyteError("tags must be an array.", "VALIDATION");
|
|
811
|
+
for (const tag of input.tags) if (typeof tag !== "string") throw new MnemocyteError("tags must contain only strings.", "VALIDATION");
|
|
812
|
+
}
|
|
813
|
+
if (input.source !== void 0 && typeof input.source !== "string") throw new MnemocyteError("source must be a string.", "VALIDATION");
|
|
726
814
|
if (input.confidence !== void 0 && (!Number.isFinite(input.confidence) || input.confidence < 0 || input.confidence > 1)) throw new MnemocyteError("confidence must be a number between 0 and 1.", "VALIDATION");
|
|
815
|
+
if (input.expiresAt !== void 0) assertValidDate(input.expiresAt, "expiresAt");
|
|
727
816
|
}
|
|
728
817
|
function validateRecallInput(input) {
|
|
729
818
|
assertNonEmptyString(input.entityId, "entityId");
|
|
730
819
|
assertNonEmptyString(input.query, "query");
|
|
731
820
|
if (input.limit !== void 0) assertLimit(input.limit);
|
|
732
821
|
if (input.minScore !== void 0) assertMinScore(input.minScore);
|
|
822
|
+
if (input.types !== void 0) assertMemoryTypeArray(input.types, "types");
|
|
733
823
|
}
|
|
734
824
|
function contextInputToRecallInput(input) {
|
|
735
825
|
return {
|
|
@@ -744,13 +834,49 @@ function contextInputToRecallInput(input) {
|
|
|
744
834
|
};
|
|
745
835
|
}
|
|
746
836
|
/**
|
|
747
|
-
* Validate a {@link PruneInput}
|
|
748
|
-
* {@link MnemocyteError}
|
|
749
|
-
*
|
|
837
|
+
* Validate and normalize a {@link PruneInput}. Throws a `"VALIDATION"`
|
|
838
|
+
* {@link MnemocyteError} for malformed fields or when normalization leaves no
|
|
839
|
+
* effective selector, preventing surprise full-table deletes.
|
|
750
840
|
*/
|
|
751
841
|
function validatePruneInput(input) {
|
|
752
842
|
if (input.entityId !== void 0) assertNonEmptyString(input.entityId, "entityId");
|
|
753
|
-
|
|
843
|
+
for (const [field, value] of [
|
|
844
|
+
["expired", input.expired],
|
|
845
|
+
["superseded", input.superseded],
|
|
846
|
+
["dryRun", input.dryRun]
|
|
847
|
+
]) if (value !== void 0 && typeof value !== "boolean") throw new MnemocyteError(`${field} must be a boolean.`, "VALIDATION");
|
|
848
|
+
for (const [field, value] of [["createdBefore", input.createdBefore], ["notAccessedSince", input.notAccessedSince]]) if (value !== void 0) assertValidDate(value, field);
|
|
849
|
+
let types;
|
|
850
|
+
if (input.types !== void 0) {
|
|
851
|
+
assertMemoryTypeArray(input.types, "types");
|
|
852
|
+
const normalized = new Set(input.types);
|
|
853
|
+
if (normalized.size > 0) types = [...normalized];
|
|
854
|
+
}
|
|
855
|
+
let tags;
|
|
856
|
+
if (input.tags !== void 0) {
|
|
857
|
+
if (!Array.isArray(input.tags)) throw new MnemocyteError("tags must be an array.", "VALIDATION");
|
|
858
|
+
const normalized = /* @__PURE__ */ new Set();
|
|
859
|
+
for (const value of input.tags) {
|
|
860
|
+
if (typeof value !== "string" || value.trim().length === 0) throw new MnemocyteError("tags must contain only non-empty strings.", "VALIDATION");
|
|
861
|
+
normalized.add(value.trim());
|
|
862
|
+
}
|
|
863
|
+
if (normalized.size > 0) tags = [...normalized];
|
|
864
|
+
}
|
|
865
|
+
if (input.maxImportance !== void 0) assertImportanceLevel(input.maxImportance, "maxImportance");
|
|
866
|
+
if (input.signal !== void 0 && (typeof input.signal !== "object" || input.signal === null || typeof input.signal.aborted !== "boolean" || typeof input.signal.addEventListener !== "function" || typeof input.signal.removeEventListener !== "function")) throw new MnemocyteError("signal must be an AbortSignal.", "VALIDATION");
|
|
867
|
+
const filter = {
|
|
868
|
+
...input.entityId === void 0 ? {} : { entityId: input.entityId },
|
|
869
|
+
...input.expired === true ? { expired: true } : {},
|
|
870
|
+
...input.superseded === true ? { superseded: true } : {},
|
|
871
|
+
...input.createdBefore === void 0 ? {} : { createdBefore: new Date(input.createdBefore) },
|
|
872
|
+
...input.notAccessedSince === void 0 ? {} : { notAccessedSince: new Date(input.notAccessedSince) },
|
|
873
|
+
...types === void 0 ? {} : { types },
|
|
874
|
+
...tags === void 0 ? {} : { tags },
|
|
875
|
+
...input.maxImportance === void 0 ? {} : { maxImportance: input.maxImportance },
|
|
876
|
+
dryRun: input.dryRun === true
|
|
877
|
+
};
|
|
878
|
+
assertPruneFilterHasSelector(filter);
|
|
879
|
+
return filter;
|
|
754
880
|
}
|
|
755
881
|
/**
|
|
756
882
|
* Validate a {@link FindDuplicatesInput} and throw a `"VALIDATION"`
|
|
@@ -762,6 +888,7 @@ function validateFindDuplicatesInput(input) {
|
|
|
762
888
|
if (!Number.isFinite(input.threshold) || input.threshold < 0 || input.threshold > 1) throw new MnemocyteError("threshold must be a number between 0 and 1.", "VALIDATION");
|
|
763
889
|
}
|
|
764
890
|
if (input.limit !== void 0) assertLimit(input.limit);
|
|
891
|
+
if (input.types !== void 0) assertMemoryTypeArray(input.types, "types");
|
|
765
892
|
}
|
|
766
893
|
/**
|
|
767
894
|
* Validate a {@link ConsolidateInput} and throw a `"VALIDATION"`
|
|
@@ -782,6 +909,16 @@ function validateConsolidateInput(input) {
|
|
|
782
909
|
function validateListAuditLogInput(input) {
|
|
783
910
|
assertNonEmptyString(input.entityId, "entityId");
|
|
784
911
|
if (input.limit !== void 0) assertLimit(input.limit);
|
|
912
|
+
for (const [field, cursor] of [["beforeCursor", input.beforeCursor], ["afterCursor", input.afterCursor]]) {
|
|
913
|
+
if (cursor === void 0) continue;
|
|
914
|
+
assertAuditLogCursor(cursor, field);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
function assertAuditLogCursor(value, field) {
|
|
918
|
+
if (typeof value !== "object" || value === null) throw new MnemocyteError(`${field} must be an audit-log cursor.`, "VALIDATION");
|
|
919
|
+
const cursor = value;
|
|
920
|
+
assertValidDate(cursor.timestamp, `${field}.timestamp`);
|
|
921
|
+
assertNonEmptyString(cursor.id, `${field}.id`);
|
|
785
922
|
}
|
|
786
923
|
//#endregion
|
|
787
924
|
//#region src/memory/client-core.ts
|
|
@@ -794,6 +931,17 @@ function providerOptions(config, signal) {
|
|
|
794
931
|
...config.provider === void 0 ? {} : { resilience: config.provider }
|
|
795
932
|
};
|
|
796
933
|
}
|
|
934
|
+
function storeOptions(signal) {
|
|
935
|
+
return signal === void 0 ? void 0 : { signal };
|
|
936
|
+
}
|
|
937
|
+
function snapshotRememberInput(input) {
|
|
938
|
+
return {
|
|
939
|
+
...input,
|
|
940
|
+
...input.tags === void 0 ? {} : { tags: Array.isArray(input.tags) ? [...input.tags] : input.tags },
|
|
941
|
+
metadata: cloneJsonObject(input.metadata ?? {}),
|
|
942
|
+
...input.expiresAt === void 0 ? {} : { expiresAt: input.expiresAt instanceof Date ? new Date(input.expiresAt) : input.expiresAt }
|
|
943
|
+
};
|
|
944
|
+
}
|
|
797
945
|
function createStoredMemory(config, input, embedding, now) {
|
|
798
946
|
return {
|
|
799
947
|
id: createId(),
|
|
@@ -803,7 +951,7 @@ function createStoredMemory(config, input, embedding, now) {
|
|
|
803
951
|
importance: input.importance ?? "normal",
|
|
804
952
|
tags: normalizeTags(input.tags),
|
|
805
953
|
source: input.source ?? null,
|
|
806
|
-
metadata:
|
|
954
|
+
metadata: input.metadata,
|
|
807
955
|
confidence: input.confidence ?? 1,
|
|
808
956
|
embeddingModel: config.embedder.model,
|
|
809
957
|
embeddingDimensions: config.embedder.dimensions,
|
|
@@ -822,10 +970,35 @@ function auditEvent(entityId, description, metadata, timestamp = /* @__PURE__ */
|
|
|
822
970
|
id: createEventId(),
|
|
823
971
|
entityId,
|
|
824
972
|
description,
|
|
825
|
-
metadata
|
|
973
|
+
metadata,
|
|
826
974
|
timestamp
|
|
827
975
|
};
|
|
828
976
|
}
|
|
977
|
+
function normalizeStoreRows(expected, returned, operation) {
|
|
978
|
+
const expectedIds = new Set(expected.map((memory) => memory.id));
|
|
979
|
+
const returnedById = /* @__PURE__ */ new Map();
|
|
980
|
+
for (const row of returned) {
|
|
981
|
+
if (!expectedIds.has(row.id)) throw new MnemocyteError(`${operation} returned an unknown ID.`, "DB");
|
|
982
|
+
if (returnedById.has(row.id)) throw new MnemocyteError(`${operation} returned a duplicate ID.`, "DB");
|
|
983
|
+
returnedById.set(row.id, row);
|
|
984
|
+
}
|
|
985
|
+
return expected.map((row) => {
|
|
986
|
+
const returnedRow = returnedById.get(row.id);
|
|
987
|
+
if (!returnedRow) throw new MnemocyteError(`${operation} did not return every requested ID.`, "DB");
|
|
988
|
+
return returnedRow;
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
function normalizePruneEntityDeletions(result, expectedEntityId) {
|
|
992
|
+
const seenEntityIds = /* @__PURE__ */ new Set();
|
|
993
|
+
let detailedDeletedCount = 0;
|
|
994
|
+
for (const detail of result.deletedByEntity) {
|
|
995
|
+
if (detail.entityId.length === 0 || !Number.isInteger(detail.deletedCount) || detail.deletedCount <= 0 || seenEntityIds.has(detail.entityId) || expectedEntityId !== void 0 && detail.entityId !== expectedEntityId) throw new MnemocyteError("Memory prune returned invalid per-entity deletion details.", "DB");
|
|
996
|
+
seenEntityIds.add(detail.entityId);
|
|
997
|
+
detailedDeletedCount += detail.deletedCount;
|
|
998
|
+
}
|
|
999
|
+
if (detailedDeletedCount !== result.deletedCount || result.dryRun && result.deletedByEntity.length > 0) throw new MnemocyteError("Memory prune deletion details did not match the deleted count.", "DB");
|
|
1000
|
+
return [...result.deletedByEntity].sort((a, b) => a.entityId.localeCompare(b.entityId));
|
|
1001
|
+
}
|
|
829
1002
|
function toDuplicatePair(pair) {
|
|
830
1003
|
return {
|
|
831
1004
|
a: cloneMemory(pair.a),
|
|
@@ -834,9 +1007,58 @@ function toDuplicatePair(pair) {
|
|
|
834
1007
|
};
|
|
835
1008
|
}
|
|
836
1009
|
function createMemoryClient(config, store) {
|
|
837
|
-
let
|
|
1010
|
+
let state = "open";
|
|
1011
|
+
let activeOperations = 0;
|
|
1012
|
+
let operationsDrainedPromise;
|
|
1013
|
+
let resolveOperationsDrained;
|
|
1014
|
+
let closePromise;
|
|
838
1015
|
function assertOpen() {
|
|
839
|
-
if (closed) throw new MnemocyteError("Mnemocyte client is closed.", "DB");
|
|
1016
|
+
if (state === "closed") throw new MnemocyteError("Mnemocyte client is closed.", "DB");
|
|
1017
|
+
}
|
|
1018
|
+
function beginOperation() {
|
|
1019
|
+
if (state !== "open") throw new MnemocyteError("Mnemocyte client is closed.", "DB");
|
|
1020
|
+
activeOperations += 1;
|
|
1021
|
+
let completed = false;
|
|
1022
|
+
return () => {
|
|
1023
|
+
if (completed) return;
|
|
1024
|
+
completed = true;
|
|
1025
|
+
activeOperations -= 1;
|
|
1026
|
+
if (activeOperations === 0 && resolveOperationsDrained) {
|
|
1027
|
+
const resolve = resolveOperationsDrained;
|
|
1028
|
+
resolveOperationsDrained = void 0;
|
|
1029
|
+
operationsDrainedPromise = void 0;
|
|
1030
|
+
resolve();
|
|
1031
|
+
}
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
function runOperation(operationName, metadata, operation) {
|
|
1035
|
+
let complete;
|
|
1036
|
+
try {
|
|
1037
|
+
complete = beginOperation();
|
|
1038
|
+
} catch (error) {
|
|
1039
|
+
return observe(config, store.backend, operationName, metadata, async () => {
|
|
1040
|
+
throw error;
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
try {
|
|
1044
|
+
return operation().then((result) => {
|
|
1045
|
+
complete();
|
|
1046
|
+
return result;
|
|
1047
|
+
}, (error) => {
|
|
1048
|
+
complete();
|
|
1049
|
+
throw error;
|
|
1050
|
+
});
|
|
1051
|
+
} catch (error) {
|
|
1052
|
+
complete();
|
|
1053
|
+
return Promise.reject(error);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
function waitForActiveOperations() {
|
|
1057
|
+
if (activeOperations === 0) return Promise.resolve();
|
|
1058
|
+
if (!operationsDrainedPromise) operationsDrainedPromise = new Promise((resolve) => {
|
|
1059
|
+
resolveOperationsDrained = resolve;
|
|
1060
|
+
});
|
|
1061
|
+
return operationsDrainedPromise;
|
|
840
1062
|
}
|
|
841
1063
|
async function ensureSchema() {
|
|
842
1064
|
await store.ensureSchema();
|
|
@@ -850,38 +1072,34 @@ function createMemoryClient(config, store) {
|
|
|
850
1072
|
await store.addAuditEvents(events);
|
|
851
1073
|
} catch {}
|
|
852
1074
|
}
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
...input,
|
|
856
|
-
metadata: cloneJsonObject(input.metadata ?? {})
|
|
857
|
-
};
|
|
858
|
-
return observe(config, store.backend, "remember", { entityId: preparedInput.entityId }, async () => {
|
|
1075
|
+
function remember(input) {
|
|
1076
|
+
return runOperation("remember", { entityId: input.entityId }, () => observePrepared(config, store.backend, "remember", { entityId: input.entityId }, () => snapshotRememberInput(input), async (preparedInput) => {
|
|
859
1077
|
assertOpen();
|
|
860
1078
|
validateRememberInput(preparedInput);
|
|
861
1079
|
await ensureEmbeddingCompatibility();
|
|
862
|
-
const
|
|
863
|
-
const [memory] = await store.insertMemories(
|
|
864
|
-
if (!memory) throw new MnemocyteError("Memory insert
|
|
1080
|
+
const stored = [createStoredMemory(config, preparedInput, await embedOne(config.embedder, preparedInput.content, providerOptions(config, preparedInput.signal)), /* @__PURE__ */ new Date())];
|
|
1081
|
+
const [memory] = normalizeStoreRows(stored, await store.insertMemories(stored), "Memory insert");
|
|
1082
|
+
if (!memory) throw new MnemocyteError("Memory insert did not return the inserted ID.", "DB");
|
|
865
1083
|
await recordAudit([auditEvent(memory.entityId, "memory.created", {
|
|
866
1084
|
memoryId: memory.id,
|
|
867
1085
|
type: memory.type,
|
|
868
1086
|
importance: memory.importance
|
|
869
1087
|
})]);
|
|
870
|
-
return
|
|
1088
|
+
return memory;
|
|
871
1089
|
}, (memory) => ({
|
|
872
1090
|
memoryId: memory.id,
|
|
873
1091
|
count: 1
|
|
874
|
-
}));
|
|
1092
|
+
})));
|
|
875
1093
|
}
|
|
876
|
-
|
|
877
|
-
const
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
1094
|
+
function rememberMany(input) {
|
|
1095
|
+
const count = isPositionalRememberManyInput(input) ? input.length : input.inputs.length;
|
|
1096
|
+
return runOperation("rememberMany", { count }, () => observePrepared(config, store.backend, "rememberMany", { count }, () => {
|
|
1097
|
+
const positional = isPositionalRememberManyInput(input);
|
|
1098
|
+
return {
|
|
1099
|
+
preparedInputs: (positional ? input : input.inputs).map(snapshotRememberInput),
|
|
1100
|
+
signal: positional ? input[0]?.signal : input.signal
|
|
1101
|
+
};
|
|
1102
|
+
}, async ({ preparedInputs, signal }) => {
|
|
885
1103
|
assertOpen();
|
|
886
1104
|
if (preparedInputs.length === 0) return [];
|
|
887
1105
|
for (const item of preparedInputs) validateRememberInput(item);
|
|
@@ -889,86 +1107,101 @@ function createMemoryClient(config, store) {
|
|
|
889
1107
|
const embeddings = await embedMany(config.embedder, preparedInputs.map((item) => item.content), providerOptions(config, signal));
|
|
890
1108
|
const now = /* @__PURE__ */ new Date();
|
|
891
1109
|
const stored = preparedInputs.map((item, idx) => createStoredMemory(config, item, embeddings[idx], now));
|
|
892
|
-
const memories = await store.insertMemories(stored);
|
|
1110
|
+
const memories = normalizeStoreRows(stored, await store.insertMemories(stored), "Memory insert");
|
|
893
1111
|
await recordAudit(memories.map((memory) => auditEvent(memory.entityId, "memory.created", {
|
|
894
1112
|
memoryId: memory.id,
|
|
895
1113
|
type: memory.type,
|
|
896
1114
|
importance: memory.importance
|
|
897
1115
|
})));
|
|
898
|
-
return memories
|
|
1116
|
+
return memories;
|
|
1117
|
+
}, (result) => ({ count: result.length })));
|
|
1118
|
+
}
|
|
1119
|
+
function recall(input) {
|
|
1120
|
+
return observe(config, store.backend, "recall", { entityId: input.entityId }, async () => {
|
|
1121
|
+
assertOpen();
|
|
1122
|
+
validateRecallInput(input);
|
|
1123
|
+
const limit = input.limit ?? config.defaults?.limit ?? 10;
|
|
1124
|
+
const minScore = input.minScore ?? config.defaults?.minScore ?? 0;
|
|
1125
|
+
assertLimit(limit);
|
|
1126
|
+
assertMinScore(minScore);
|
|
1127
|
+
await ensureEmbeddingCompatibility();
|
|
1128
|
+
const queryEmbedding = await embedOne(config.embedder, input.query, providerOptions(config, input.signal));
|
|
1129
|
+
const candidateMultiplier = config.retrieval?.candidateMultiplier ?? 3;
|
|
1130
|
+
const candidateLimit = Math.max(limit, limit * candidateMultiplier);
|
|
1131
|
+
const scoreLexical = createLexicalScorer(input.query);
|
|
1132
|
+
const scoringConfig = createScoringConfig(config.retrieval);
|
|
1133
|
+
const [vectorCandidates, lexicalCandidates] = await Promise.all([store.vectorSearch({
|
|
1134
|
+
...input,
|
|
1135
|
+
embedding: queryEmbedding,
|
|
1136
|
+
limit: candidateLimit,
|
|
1137
|
+
minVectorScore: 0
|
|
1138
|
+
}), store.lexicalSearch({
|
|
1139
|
+
...input,
|
|
1140
|
+
limit: candidateLimit
|
|
1141
|
+
})]);
|
|
1142
|
+
const merged = /* @__PURE__ */ new Map();
|
|
1143
|
+
for (const candidate of vectorCandidates) merged.set(candidate.memory.id, {
|
|
1144
|
+
memory: candidate.memory,
|
|
1145
|
+
vectorScore: candidate.vectorScore,
|
|
1146
|
+
lexicalScore: scoreLexical(candidate.memory.content)
|
|
1147
|
+
});
|
|
1148
|
+
const lexicalOnly = lexicalCandidates.filter((candidate) => !merged.has(candidate.memory.id));
|
|
1149
|
+
const lexicalOnlyEmbeddings = await store.getMemoryEmbeddings(lexicalOnly.map((candidate) => candidate.memory.id));
|
|
1150
|
+
for (const candidate of lexicalCandidates) {
|
|
1151
|
+
const existing = merged.get(candidate.memory.id);
|
|
1152
|
+
if (existing) {
|
|
1153
|
+
existing.lexicalScore = candidate.lexicalScore;
|
|
1154
|
+
continue;
|
|
1155
|
+
}
|
|
1156
|
+
const embedding = lexicalOnlyEmbeddings.get(candidate.memory.id);
|
|
1157
|
+
const similarity = embedding ? cosineSimilarity(embedding, queryEmbedding) : 0;
|
|
1158
|
+
merged.set(candidate.memory.id, {
|
|
1159
|
+
memory: candidate.memory,
|
|
1160
|
+
vectorScore: toVectorScore(similarity),
|
|
1161
|
+
lexicalScore: candidate.lexicalScore
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
const scored = Array.from(merged.values()).map((entry) => toScoredMemoryWithConfig(entry.memory, Math.max(0, entry.vectorScore), Math.max(0, entry.lexicalScore), input, scoringConfig)).filter((memory) => memory.score >= minScore).sort((a, b) => b.score - a.score).slice(0, limit);
|
|
1165
|
+
const accessUpdates = normalizeStoreRows(scored, await store.markMemoriesAccessed(scored.map((memory) => memory.id)), "Memory access update");
|
|
1166
|
+
return scored.map((memory, index) => {
|
|
1167
|
+
const update = accessUpdates[index];
|
|
1168
|
+
if (!update) throw new MnemocyteError("Memory access update did not return every requested ID.", "DB");
|
|
1169
|
+
return {
|
|
1170
|
+
...memory,
|
|
1171
|
+
lastAccessedAt: new Date(update.lastAccessedAt),
|
|
1172
|
+
accessCount: update.accessCount,
|
|
1173
|
+
updatedAt: new Date(update.updatedAt)
|
|
1174
|
+
};
|
|
1175
|
+
});
|
|
899
1176
|
}, (result) => ({ count: result.length }));
|
|
900
1177
|
}
|
|
901
|
-
|
|
1178
|
+
return {
|
|
902
1179
|
remember,
|
|
903
1180
|
rememberMany,
|
|
904
|
-
|
|
905
|
-
return
|
|
906
|
-
assertOpen();
|
|
907
|
-
validateRecallInput(input);
|
|
908
|
-
const limit = input.limit ?? config.defaults?.limit ?? 10;
|
|
909
|
-
const minScore = input.minScore ?? config.defaults?.minScore ?? 0;
|
|
910
|
-
assertLimit(limit);
|
|
911
|
-
assertMinScore(minScore);
|
|
912
|
-
await ensureEmbeddingCompatibility();
|
|
913
|
-
const queryEmbedding = await embedOne(config.embedder, input.query, providerOptions(config, input.signal));
|
|
914
|
-
const candidateMultiplier = config.retrieval?.candidateMultiplier ?? 3;
|
|
915
|
-
const candidateLimit = Math.max(limit, limit * candidateMultiplier);
|
|
916
|
-
const scoreLexical = createLexicalScorer(input.query);
|
|
917
|
-
const scoringConfig = createScoringConfig(config.retrieval);
|
|
918
|
-
const [vectorCandidates, lexicalCandidates] = await Promise.all([store.vectorSearch({
|
|
919
|
-
...input,
|
|
920
|
-
embedding: queryEmbedding,
|
|
921
|
-
limit: candidateLimit,
|
|
922
|
-
minScore: 0
|
|
923
|
-
}), store.lexicalSearch({
|
|
924
|
-
...input,
|
|
925
|
-
limit: candidateLimit
|
|
926
|
-
})]);
|
|
927
|
-
const merged = /* @__PURE__ */ new Map();
|
|
928
|
-
for (const candidate of vectorCandidates) merged.set(candidate.memory.id, {
|
|
929
|
-
memory: candidate.memory,
|
|
930
|
-
vectorScore: candidate.vectorScore,
|
|
931
|
-
lexicalScore: scoreLexical(candidate.memory.content)
|
|
932
|
-
});
|
|
933
|
-
const lexicalOnly = lexicalCandidates.filter((candidate) => !merged.has(candidate.memory.id));
|
|
934
|
-
const lexicalOnlyEmbeddings = await store.getMemoryEmbeddings(lexicalOnly.map((candidate) => candidate.memory.id));
|
|
935
|
-
for (const candidate of lexicalCandidates) {
|
|
936
|
-
const existing = merged.get(candidate.memory.id);
|
|
937
|
-
if (existing) {
|
|
938
|
-
existing.lexicalScore = candidate.lexicalScore;
|
|
939
|
-
continue;
|
|
940
|
-
}
|
|
941
|
-
const embedding = lexicalOnlyEmbeddings.get(candidate.memory.id);
|
|
942
|
-
const similarity = embedding ? cosineSimilarity(embedding, queryEmbedding) : 0;
|
|
943
|
-
merged.set(candidate.memory.id, {
|
|
944
|
-
memory: candidate.memory,
|
|
945
|
-
vectorScore: Math.max(0, similarity),
|
|
946
|
-
lexicalScore: candidate.lexicalScore
|
|
947
|
-
});
|
|
948
|
-
}
|
|
949
|
-
const scored = Array.from(merged.values()).map((entry) => toScoredMemoryWithConfig(entry.memory, Math.max(0, entry.vectorScore), Math.max(0, entry.lexicalScore), input, scoringConfig)).filter((memory) => memory.score >= minScore).sort((a, b) => b.score - a.score).slice(0, limit);
|
|
950
|
-
await store.markMemoriesAccessed(scored.map((memory) => memory.id));
|
|
951
|
-
return scored;
|
|
952
|
-
}, (result) => ({ count: result.length }));
|
|
1181
|
+
recall(input) {
|
|
1182
|
+
return runOperation("recall", { entityId: input.entityId }, () => recall(input));
|
|
953
1183
|
},
|
|
954
|
-
|
|
955
|
-
return observe(config, store.backend, "buildContext", { entityId: input.entityId }, async () => {
|
|
1184
|
+
buildContext(input) {
|
|
1185
|
+
return runOperation("buildContext", { entityId: input.entityId }, () => observe(config, store.backend, "buildContext", { entityId: input.entityId }, async () => {
|
|
956
1186
|
assertOpen();
|
|
957
1187
|
validateBuildContextInput(input);
|
|
958
1188
|
return buildContext({
|
|
959
1189
|
input,
|
|
960
1190
|
recall: (contextInput) => {
|
|
961
1191
|
const recallInput = contextInputToRecallInput(contextInput);
|
|
962
|
-
return
|
|
1192
|
+
return recall(input.signal === void 0 ? recallInput : {
|
|
963
1193
|
...recallInput,
|
|
964
1194
|
signal: input.signal
|
|
965
1195
|
});
|
|
966
1196
|
}
|
|
967
1197
|
});
|
|
968
|
-
});
|
|
1198
|
+
}));
|
|
969
1199
|
},
|
|
970
|
-
|
|
971
|
-
return
|
|
1200
|
+
forget(input) {
|
|
1201
|
+
return runOperation("forget", {
|
|
1202
|
+
entityId: input.entityId,
|
|
1203
|
+
memoryId: input.memoryId
|
|
1204
|
+
}, () => observe(config, store.backend, "forget", {
|
|
972
1205
|
entityId: input.entityId,
|
|
973
1206
|
memoryId: input.memoryId
|
|
974
1207
|
}, async () => {
|
|
@@ -978,73 +1211,81 @@ function createMemoryClient(config, store) {
|
|
|
978
1211
|
await ensureSchema();
|
|
979
1212
|
if (!await store.deleteMemory(input.entityId, input.memoryId)) throw new MnemocyteError("Memory was not found.", "NOT_FOUND");
|
|
980
1213
|
await recordAudit([auditEvent(input.entityId, "memory.deleted", { memoryId: input.memoryId })]);
|
|
981
|
-
});
|
|
1214
|
+
}));
|
|
982
1215
|
},
|
|
983
|
-
|
|
984
|
-
return observe(config, store.backend, "forgetAll", { entityId: input.entityId }, async () => {
|
|
1216
|
+
forgetAll(input) {
|
|
1217
|
+
return runOperation("forgetAll", { entityId: input.entityId }, () => observe(config, store.backend, "forgetAll", { entityId: input.entityId }, async () => {
|
|
985
1218
|
assertOpen();
|
|
986
1219
|
assertNonEmptyString(input.entityId, "entityId");
|
|
987
1220
|
await ensureSchema();
|
|
988
1221
|
const deletedCount = await store.deleteMemoriesForEntity(input.entityId);
|
|
989
1222
|
await recordAudit([auditEvent(input.entityId, "entity.cleared", { count: deletedCount })]);
|
|
990
1223
|
return deletedCount;
|
|
991
|
-
}, (count) => ({ count })).then(() => void 0);
|
|
1224
|
+
}, (count) => ({ count })).then(() => void 0));
|
|
992
1225
|
},
|
|
993
|
-
|
|
994
|
-
return observe(config, store.backend, "prune", input.entityId === void 0 ? {} : { entityId: input.entityId }, async () => {
|
|
1226
|
+
prune(input) {
|
|
1227
|
+
return runOperation("prune", input.entityId === void 0 ? {} : { entityId: input.entityId }, () => observe(config, store.backend, "prune", input.entityId === void 0 ? {} : { entityId: input.entityId }, async () => {
|
|
995
1228
|
assertOpen();
|
|
996
|
-
|
|
1229
|
+
throwIfAborted(input.signal);
|
|
1230
|
+
const filter = validatePruneInput(input);
|
|
997
1231
|
await ensureSchema();
|
|
998
|
-
const result = await store.prune(input);
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1232
|
+
const result = await store.prune(filter, storeOptions(input.signal));
|
|
1233
|
+
const deletedByEntity = normalizePruneEntityDeletions(result, input.entityId);
|
|
1234
|
+
if (result.deletedCount > 0 && result.dryRun === false) await recordAudit(deletedByEntity.map((detail) => auditEvent(detail.entityId, "memory.pruned", { count: detail.deletedCount })));
|
|
1235
|
+
return {
|
|
1236
|
+
matchedCount: result.matchedCount,
|
|
1237
|
+
deletedCount: result.deletedCount,
|
|
1238
|
+
dryRun: result.dryRun
|
|
1239
|
+
};
|
|
1240
|
+
}, (result) => ({ count: result.deletedCount })));
|
|
1002
1241
|
},
|
|
1003
|
-
|
|
1004
|
-
return observe(config, store.backend, "findDuplicates", { entityId: input.entityId }, async () => {
|
|
1242
|
+
findDuplicates(input) {
|
|
1243
|
+
return runOperation("findDuplicates", { entityId: input.entityId }, () => observe(config, store.backend, "findDuplicates", { entityId: input.entityId }, async () => {
|
|
1005
1244
|
assertOpen();
|
|
1245
|
+
throwIfAborted(input.signal);
|
|
1006
1246
|
validateFindDuplicatesInput(input);
|
|
1007
1247
|
await ensureEmbeddingCompatibility();
|
|
1008
|
-
return (await store.findDuplicatePairs(input)).map(toDuplicatePair);
|
|
1009
|
-
}, (result) => ({ count: result.length }));
|
|
1248
|
+
return (await store.findDuplicatePairs(input, storeOptions(input.signal))).map(toDuplicatePair);
|
|
1249
|
+
}, (result) => ({ count: result.length })));
|
|
1010
1250
|
},
|
|
1011
|
-
|
|
1012
|
-
return observe(config, store.backend, "listAuditLog", { entityId: input.entityId }, async () => {
|
|
1251
|
+
listAuditLog(input) {
|
|
1252
|
+
return runOperation("listAuditLog", { entityId: input.entityId }, () => observe(config, store.backend, "listAuditLog", { entityId: input.entityId }, async () => {
|
|
1013
1253
|
assertOpen();
|
|
1254
|
+
throwIfAborted(input.signal);
|
|
1014
1255
|
validateListAuditLogInput(input);
|
|
1015
1256
|
await ensureSchema();
|
|
1016
|
-
return
|
|
1017
|
-
|
|
1018
|
-
entityId: event.entityId,
|
|
1019
|
-
description: event.description,
|
|
1020
|
-
metadata: cloneJsonObject(event.metadata),
|
|
1021
|
-
timestamp: new Date(event.timestamp)
|
|
1022
|
-
}));
|
|
1023
|
-
}, (result) => ({ count: result.length }));
|
|
1257
|
+
return await store.listAuditLog(input, storeOptions(input.signal));
|
|
1258
|
+
}, (result) => ({ count: result.length })));
|
|
1024
1259
|
},
|
|
1025
|
-
|
|
1026
|
-
return observe(config, store.backend, "stats", input?.entityId ? { entityId: input.entityId } : {}, async () => {
|
|
1260
|
+
stats(input) {
|
|
1261
|
+
return runOperation("stats", input?.entityId ? { entityId: input.entityId } : {}, () => observe(config, store.backend, "stats", input?.entityId ? { entityId: input.entityId } : {}, async () => {
|
|
1027
1262
|
assertOpen();
|
|
1028
1263
|
await ensureSchema();
|
|
1029
1264
|
return store.stats(input, /* @__PURE__ */ new Date());
|
|
1030
|
-
});
|
|
1265
|
+
}));
|
|
1031
1266
|
},
|
|
1032
|
-
experimental: {
|
|
1033
|
-
return
|
|
1267
|
+
experimental: { consolidate(input) {
|
|
1268
|
+
return runOperation("consolidate", {
|
|
1269
|
+
entityId: input.entityId,
|
|
1270
|
+
memoryId: input.survivorId
|
|
1271
|
+
}, () => observe(config, store.backend, "consolidate", {
|
|
1034
1272
|
entityId: input.entityId,
|
|
1035
1273
|
memoryId: input.survivorId
|
|
1036
1274
|
}, async () => {
|
|
1037
1275
|
assertOpen();
|
|
1276
|
+
throwIfAborted(input.signal);
|
|
1038
1277
|
validateConsolidateInput(input);
|
|
1039
1278
|
await ensureSchema();
|
|
1040
|
-
const survivor = await store.getMemory(input.entityId, input.survivorId);
|
|
1279
|
+
const survivor = await store.getMemory(input.entityId, input.survivorId, storeOptions(input.signal));
|
|
1280
|
+
throwIfAborted(input.signal);
|
|
1041
1281
|
if (!survivor) throw new MnemocyteError("Survivor memory was not found.", "NOT_FOUND");
|
|
1042
1282
|
if (survivor.supersededBy !== null) throw new MnemocyteError("Survivor memory is itself superseded.", "VALIDATION");
|
|
1043
|
-
const targets = await store.loadConsolidationTargets(input.entityId, input.supersededIds);
|
|
1283
|
+
const targets = await store.loadConsolidationTargets(input.entityId, input.supersededIds, storeOptions(input.signal));
|
|
1284
|
+
throwIfAborted(input.signal);
|
|
1044
1285
|
const foundIds = new Set(targets.map((target) => target.id));
|
|
1045
1286
|
for (const id of input.supersededIds) if (!foundIds.has(id)) throw new MnemocyteError("Superseded memory was not found.", "NOT_FOUND");
|
|
1046
|
-
|
|
1047
|
-
if (
|
|
1287
|
+
if (targets.some((target) => target.supersededBy !== null && target.supersededBy !== survivor.id)) throw new MnemocyteError("Superseded memory already belongs to a different survivor.", "CONFLICT");
|
|
1288
|
+
if (targets.filter((target) => target.supersededBy === null).length === 0) return {
|
|
1048
1289
|
survivorId: survivor.id,
|
|
1049
1290
|
supersededCount: 0,
|
|
1050
1291
|
supersededIds: []
|
|
@@ -1053,28 +1294,40 @@ function createMemoryClient(config, store) {
|
|
|
1053
1294
|
entityId: input.entityId,
|
|
1054
1295
|
survivorId: survivor.id,
|
|
1055
1296
|
survivorTags: survivor.tags,
|
|
1056
|
-
supersededIds:
|
|
1297
|
+
supersededIds: input.supersededIds,
|
|
1057
1298
|
mergeTags: input.mergeTags !== false,
|
|
1058
1299
|
now: /* @__PURE__ */ new Date(),
|
|
1059
1300
|
auditEnabled: config.audit?.enabled === true
|
|
1060
|
-
});
|
|
1301
|
+
}, storeOptions(input.signal));
|
|
1061
1302
|
return {
|
|
1062
1303
|
survivorId: survivor.id,
|
|
1063
1304
|
supersededCount: result.supersededIds.length,
|
|
1064
1305
|
supersededIds: result.supersededIds
|
|
1065
1306
|
};
|
|
1066
|
-
}, (result) => ({ count: result.supersededCount }));
|
|
1307
|
+
}, (result) => ({ count: result.supersededCount })));
|
|
1067
1308
|
} },
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1309
|
+
close() {
|
|
1310
|
+
if (closePromise) return closePromise;
|
|
1311
|
+
state = "closing";
|
|
1312
|
+
closePromise = observe(config, store.backend, "close", {}, async () => {
|
|
1313
|
+
await waitForActiveOperations();
|
|
1314
|
+
try {
|
|
1072
1315
|
await store.close();
|
|
1316
|
+
state = "closed";
|
|
1317
|
+
} catch (error) {
|
|
1318
|
+
state = "open";
|
|
1319
|
+
closePromise = void 0;
|
|
1320
|
+
throw error;
|
|
1073
1321
|
}
|
|
1074
1322
|
});
|
|
1323
|
+
return closePromise;
|
|
1075
1324
|
}
|
|
1076
1325
|
};
|
|
1077
|
-
|
|
1326
|
+
}
|
|
1327
|
+
//#endregion
|
|
1328
|
+
//#region src/memory/deletion.ts
|
|
1329
|
+
function memoryHasDependentsError(cause) {
|
|
1330
|
+
return new MnemocyteError("Memory cannot be deleted while other memories reference it as their consolidation survivor.", "CONFLICT", cause);
|
|
1078
1331
|
}
|
|
1079
1332
|
//#endregion
|
|
1080
1333
|
//#region src/memory/filters.ts
|
|
@@ -1082,7 +1335,7 @@ function isExpired(memory, now) {
|
|
|
1082
1335
|
return memory.expiresAt !== null && memory.expiresAt.getTime() <= now.getTime();
|
|
1083
1336
|
}
|
|
1084
1337
|
/**
|
|
1085
|
-
* Test whether `memory` matches a
|
|
1338
|
+
* Test whether `memory` matches a validated prune filter. All specified
|
|
1086
1339
|
* selectors are AND-combined; unspecified selectors do not restrict.
|
|
1087
1340
|
*
|
|
1088
1341
|
* `now` is supplied by the caller so callers can share a consistent
|
|
@@ -1126,12 +1379,6 @@ function matchesRecallFilter(memory, input, now) {
|
|
|
1126
1379
|
}
|
|
1127
1380
|
//#endregion
|
|
1128
1381
|
//#region src/memory/in-memory.ts
|
|
1129
|
-
function cloneStoredMemory(memory) {
|
|
1130
|
-
return {
|
|
1131
|
-
...cloneMemory(memory),
|
|
1132
|
-
embedding: [...memory.embedding]
|
|
1133
|
-
};
|
|
1134
|
-
}
|
|
1135
1382
|
function cloneAuditEvent(event) {
|
|
1136
1383
|
return {
|
|
1137
1384
|
id: event.id,
|
|
@@ -1141,6 +1388,15 @@ function cloneAuditEvent(event) {
|
|
|
1141
1388
|
timestamp: new Date(event.timestamp)
|
|
1142
1389
|
};
|
|
1143
1390
|
}
|
|
1391
|
+
function compareAuditPositions(a, b) {
|
|
1392
|
+
const timestampDifference = a.timestamp.getTime() - b.timestamp.getTime();
|
|
1393
|
+
if (timestampDifference !== 0) return timestampDifference;
|
|
1394
|
+
if (a.id === b.id) return 0;
|
|
1395
|
+
return a.id < b.id ? -1 : 1;
|
|
1396
|
+
}
|
|
1397
|
+
function assertNoDependentMemories(memories, targetIds) {
|
|
1398
|
+
for (const memory of memories.values()) if (memory.supersededBy !== null && targetIds.has(memory.supersededBy)) throw memoryHasDependentsError();
|
|
1399
|
+
}
|
|
1144
1400
|
function createInMemoryStore() {
|
|
1145
1401
|
const memories = /* @__PURE__ */ new Map();
|
|
1146
1402
|
const auditLog = [];
|
|
@@ -1151,19 +1407,18 @@ function createInMemoryStore() {
|
|
|
1151
1407
|
async insertMemories(rows) {
|
|
1152
1408
|
const inserted = [];
|
|
1153
1409
|
for (const row of rows) {
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
inserted.push(cloneMemory(stored));
|
|
1410
|
+
memories.set(row.id, row);
|
|
1411
|
+
inserted.push(cloneMemory(row));
|
|
1157
1412
|
}
|
|
1158
1413
|
return inserted;
|
|
1159
1414
|
},
|
|
1160
1415
|
async vectorSearch(input) {
|
|
1161
1416
|
const now = /* @__PURE__ */ new Date();
|
|
1162
|
-
const
|
|
1417
|
+
const minVectorScore = input.minVectorScore ?? 0;
|
|
1163
1418
|
return Array.from(memories.values()).filter((memory) => matchesRecallFilter(memory, input, now)).map((memory) => ({
|
|
1164
1419
|
memory: cloneMemory(memory),
|
|
1165
|
-
vectorScore:
|
|
1166
|
-
})).filter((candidate) => candidate.vectorScore >=
|
|
1420
|
+
vectorScore: toVectorScore(cosineSimilarity(memory.embedding, input.embedding))
|
|
1421
|
+
})).filter((candidate) => candidate.vectorScore >= minVectorScore).sort((a, b) => b.vectorScore - a.vectorScore).slice(0, input.limit);
|
|
1167
1422
|
},
|
|
1168
1423
|
async lexicalSearch(input) {
|
|
1169
1424
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -1183,50 +1438,82 @@ function createInMemoryStore() {
|
|
|
1183
1438
|
},
|
|
1184
1439
|
async markMemoriesAccessed(memoryIds) {
|
|
1185
1440
|
const now = /* @__PURE__ */ new Date();
|
|
1441
|
+
const updates = [];
|
|
1186
1442
|
for (const id of memoryIds) {
|
|
1187
1443
|
const memory = memories.get(id);
|
|
1188
1444
|
if (memory) {
|
|
1189
1445
|
memory.lastAccessedAt = now;
|
|
1190
1446
|
memory.accessCount += 1;
|
|
1191
1447
|
memory.updatedAt = now;
|
|
1448
|
+
updates.push({
|
|
1449
|
+
id: memory.id,
|
|
1450
|
+
lastAccessedAt: now,
|
|
1451
|
+
accessCount: memory.accessCount,
|
|
1452
|
+
updatedAt: now
|
|
1453
|
+
});
|
|
1192
1454
|
}
|
|
1193
1455
|
}
|
|
1456
|
+
return updates;
|
|
1194
1457
|
},
|
|
1195
1458
|
async deleteMemory(entityId, memoryId) {
|
|
1196
1459
|
const memory = memories.get(memoryId);
|
|
1197
1460
|
if (!memory || memory.entityId !== entityId) return false;
|
|
1461
|
+
assertNoDependentMemories(memories, new Set([memoryId]));
|
|
1198
1462
|
return memories.delete(memoryId);
|
|
1199
1463
|
},
|
|
1200
1464
|
async deleteMemoriesForEntity(entityId) {
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
}
|
|
1206
|
-
return deleted;
|
|
1465
|
+
const targetIds = new Set(Array.from(memories.values()).filter((memory) => memory.entityId === entityId).map((memory) => memory.id));
|
|
1466
|
+
assertNoDependentMemories(memories, targetIds);
|
|
1467
|
+
for (const id of targetIds) memories.delete(id);
|
|
1468
|
+
return targetIds.size;
|
|
1207
1469
|
},
|
|
1208
|
-
async prune(input) {
|
|
1470
|
+
async prune(input, options) {
|
|
1471
|
+
throwIfAborted(options?.signal);
|
|
1472
|
+
assertPruneFilterHasSelector(input);
|
|
1209
1473
|
const now = /* @__PURE__ */ new Date();
|
|
1210
1474
|
const matched = [];
|
|
1211
|
-
for (const memory of memories.values())
|
|
1475
|
+
for (const memory of memories.values()) {
|
|
1476
|
+
throwIfAborted(options?.signal);
|
|
1477
|
+
if (matchesPruneFilter(memory, input, now)) matched.push(memory);
|
|
1478
|
+
}
|
|
1212
1479
|
const dryRun = input.dryRun === true;
|
|
1213
|
-
|
|
1480
|
+
let deletedByEntity = [];
|
|
1481
|
+
if (!dryRun) {
|
|
1482
|
+
throwIfAborted(options?.signal);
|
|
1483
|
+
assertNoDependentMemories(memories, new Set(matched.map((memory) => memory.id)));
|
|
1484
|
+
const deletedCounts = /* @__PURE__ */ new Map();
|
|
1485
|
+
for (const memory of matched) {
|
|
1486
|
+
memories.delete(memory.id);
|
|
1487
|
+
deletedCounts.set(memory.entityId, (deletedCounts.get(memory.entityId) ?? 0) + 1);
|
|
1488
|
+
}
|
|
1489
|
+
deletedByEntity = Array.from(deletedCounts, ([entityId, deletedCount]) => ({
|
|
1490
|
+
entityId,
|
|
1491
|
+
deletedCount
|
|
1492
|
+
})).sort((a, b) => a.entityId.localeCompare(b.entityId));
|
|
1493
|
+
}
|
|
1214
1494
|
return {
|
|
1215
1495
|
matchedCount: matched.length,
|
|
1216
1496
|
deletedCount: dryRun ? 0 : matched.length,
|
|
1217
|
-
dryRun
|
|
1497
|
+
dryRun,
|
|
1498
|
+
deletedByEntity
|
|
1218
1499
|
};
|
|
1219
1500
|
},
|
|
1220
|
-
async findDuplicatePairs(input) {
|
|
1501
|
+
async findDuplicatePairs(input, options) {
|
|
1502
|
+
throwIfAborted(options?.signal);
|
|
1221
1503
|
const threshold = input.threshold ?? .95;
|
|
1222
1504
|
const limit = input.limit ?? 100;
|
|
1223
1505
|
const now = /* @__PURE__ */ new Date();
|
|
1224
|
-
const candidates = Array.from(memories.values()).filter((memory) =>
|
|
1506
|
+
const candidates = Array.from(memories.values()).filter((memory) => {
|
|
1507
|
+
throwIfAborted(options?.signal);
|
|
1508
|
+
return matchesDuplicateFilter(memory, input, now);
|
|
1509
|
+
});
|
|
1225
1510
|
const pairs = [];
|
|
1226
1511
|
for (let i = 0; i < candidates.length; i += 1) {
|
|
1512
|
+
throwIfAborted(options?.signal);
|
|
1227
1513
|
const a = candidates[i];
|
|
1228
1514
|
if (a === void 0) continue;
|
|
1229
1515
|
for (let j = i + 1; j < candidates.length; j += 1) {
|
|
1516
|
+
throwIfAborted(options?.signal);
|
|
1230
1517
|
const b = candidates[j];
|
|
1231
1518
|
if (b === void 0) continue;
|
|
1232
1519
|
const similarity = Math.max(0, Math.min(1, cosineSimilarity(a.embedding, b.embedding)));
|
|
@@ -1243,27 +1530,29 @@ function createInMemoryStore() {
|
|
|
1243
1530
|
async addAuditEvents(events) {
|
|
1244
1531
|
auditLog.push(...events.map(cloneAuditEvent));
|
|
1245
1532
|
},
|
|
1246
|
-
async listAuditLog(input) {
|
|
1533
|
+
async listAuditLog(input, options) {
|
|
1534
|
+
throwIfAborted(options?.signal);
|
|
1247
1535
|
const limit = input.limit ?? 50;
|
|
1248
|
-
return auditLog.
|
|
1249
|
-
|
|
1250
|
-
idx
|
|
1251
|
-
})).filter(({ event }) => {
|
|
1536
|
+
return auditLog.filter((event) => {
|
|
1537
|
+
throwIfAborted(options?.signal);
|
|
1252
1538
|
if (event.entityId !== input.entityId) return false;
|
|
1253
1539
|
if (input.before !== void 0 && event.timestamp.getTime() >= input.before.getTime()) return false;
|
|
1254
1540
|
if (input.after !== void 0 && event.timestamp.getTime() <= input.after.getTime()) return false;
|
|
1541
|
+
if (input.beforeCursor !== void 0 && compareAuditPositions(event, input.beforeCursor) >= 0) return false;
|
|
1542
|
+
if (input.afterCursor !== void 0 && compareAuditPositions(event, input.afterCursor) <= 0) return false;
|
|
1255
1543
|
return true;
|
|
1256
1544
|
}).sort((a, b) => {
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
}).slice(0, limit).map(({ event }) => cloneAuditEvent(event));
|
|
1545
|
+
return compareAuditPositions(b, a);
|
|
1546
|
+
}).slice(0, limit).map(cloneAuditEvent);
|
|
1260
1547
|
},
|
|
1261
|
-
async getMemory(entityId, memoryId) {
|
|
1548
|
+
async getMemory(entityId, memoryId, options) {
|
|
1549
|
+
throwIfAborted(options?.signal);
|
|
1262
1550
|
const memory = memories.get(memoryId);
|
|
1263
1551
|
return memory && memory.entityId === entityId ? cloneMemory(memory) : null;
|
|
1264
1552
|
},
|
|
1265
|
-
async loadConsolidationTargets(entityId, ids) {
|
|
1553
|
+
async loadConsolidationTargets(entityId, ids, options) {
|
|
1266
1554
|
return ids.flatMap((id) => {
|
|
1555
|
+
throwIfAborted(options?.signal);
|
|
1267
1556
|
const memory = memories.get(id);
|
|
1268
1557
|
if (!memory || memory.entityId !== entityId) return [];
|
|
1269
1558
|
return [{
|
|
@@ -1273,40 +1562,50 @@ function createInMemoryStore() {
|
|
|
1273
1562
|
}];
|
|
1274
1563
|
});
|
|
1275
1564
|
},
|
|
1276
|
-
async consolidate(input) {
|
|
1565
|
+
async consolidate(input, options) {
|
|
1566
|
+
throwIfAborted(options?.signal);
|
|
1277
1567
|
const survivor = memories.get(input.survivorId);
|
|
1278
1568
|
const newlySuperseded = [];
|
|
1569
|
+
const targets = [];
|
|
1279
1570
|
for (const id of input.supersededIds) {
|
|
1571
|
+
throwIfAborted(options?.signal);
|
|
1280
1572
|
const memory = memories.get(id);
|
|
1281
1573
|
if (!memory || memory.entityId !== input.entityId) continue;
|
|
1282
|
-
|
|
1283
|
-
memory.supersededBy = input.survivorId;
|
|
1284
|
-
memory.supersededAt = input.now;
|
|
1285
|
-
memory.updatedAt = input.now;
|
|
1286
|
-
newlySuperseded.push(memory.id);
|
|
1574
|
+
targets.push(memory);
|
|
1287
1575
|
}
|
|
1576
|
+
if (targets.some((memory) => memory.supersededBy !== null && memory.supersededBy !== input.survivorId)) throw new MnemocyteError("Superseded memory already belongs to a different survivor.", "CONFLICT");
|
|
1577
|
+
for (const memory of targets) if (memory.supersededBy === null) newlySuperseded.push(memory);
|
|
1578
|
+
let mergedSurvivorTags;
|
|
1288
1579
|
if (survivor && input.mergeTags && newlySuperseded.length > 0) {
|
|
1289
1580
|
const mergedTags = new Set(input.survivorTags);
|
|
1290
|
-
for (const
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
}
|
|
1294
|
-
if (mergedTags.size !== survivor.tags.length) {
|
|
1295
|
-
survivor.tags = [...mergedTags];
|
|
1296
|
-
survivor.updatedAt = input.now;
|
|
1581
|
+
for (const memory of newlySuperseded) {
|
|
1582
|
+
throwIfAborted(options?.signal);
|
|
1583
|
+
for (const tag of memory.tags) mergedTags.add(tag);
|
|
1297
1584
|
}
|
|
1585
|
+
if (mergedTags.size !== survivor.tags.length) mergedSurvivorTags = [...mergedTags];
|
|
1298
1586
|
}
|
|
1299
|
-
|
|
1587
|
+
const auditEvents = input.auditEnabled ? newlySuperseded.map((memory) => ({
|
|
1300
1588
|
id: createEventId(),
|
|
1301
1589
|
entityId: input.entityId,
|
|
1302
1590
|
description: "memory.superseded",
|
|
1303
1591
|
metadata: {
|
|
1304
|
-
memoryId: id,
|
|
1592
|
+
memoryId: memory.id,
|
|
1305
1593
|
supersededBy: input.survivorId
|
|
1306
1594
|
},
|
|
1307
1595
|
timestamp: input.now
|
|
1308
|
-
});
|
|
1309
|
-
|
|
1596
|
+
})) : [];
|
|
1597
|
+
throwIfAborted(options?.signal);
|
|
1598
|
+
for (const memory of newlySuperseded) {
|
|
1599
|
+
memory.supersededBy = input.survivorId;
|
|
1600
|
+
memory.supersededAt = input.now;
|
|
1601
|
+
memory.updatedAt = input.now;
|
|
1602
|
+
}
|
|
1603
|
+
if (survivor && mergedSurvivorTags) {
|
|
1604
|
+
survivor.tags = mergedSurvivorTags;
|
|
1605
|
+
survivor.updatedAt = input.now;
|
|
1606
|
+
}
|
|
1607
|
+
if (input.auditEnabled) auditLog.push(...auditEvents);
|
|
1608
|
+
return { supersededIds: newlySuperseded.map((memory) => memory.id) };
|
|
1310
1609
|
},
|
|
1311
1610
|
async stats(input, now) {
|
|
1312
1611
|
const allMemories = Array.from(memories.values());
|
|
@@ -1339,20 +1638,78 @@ function createInMemoryClient(config) {
|
|
|
1339
1638
|
return createMemoryClient(config, createInMemoryStore());
|
|
1340
1639
|
}
|
|
1341
1640
|
//#endregion
|
|
1641
|
+
//#region src/db/cancellation.ts
|
|
1642
|
+
const postgresDialect = new PgDialect();
|
|
1643
|
+
async function awaitCancelableQuery(query, signal) {
|
|
1644
|
+
throwIfAborted(signal);
|
|
1645
|
+
if (!signal) return await query;
|
|
1646
|
+
const onAbort = () => {
|
|
1647
|
+
query.cancel();
|
|
1648
|
+
};
|
|
1649
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1650
|
+
try {
|
|
1651
|
+
return await query;
|
|
1652
|
+
} catch (error) {
|
|
1653
|
+
if (signal.aborted) throw new MnemocyteError("Operation was aborted.", "ABORTED", signal.reason);
|
|
1654
|
+
throw error;
|
|
1655
|
+
} finally {
|
|
1656
|
+
signal.removeEventListener("abort", onAbort);
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
async function executeCancelableSql(db, statement, signal) {
|
|
1660
|
+
const query = postgresDialect.sqlToQuery(statement);
|
|
1661
|
+
const client = db.$client;
|
|
1662
|
+
if (!client) throw new MnemocyteError("Cancelable Postgres query requires the postgres.js client.", "DB");
|
|
1663
|
+
return await awaitCancelableQuery(client.unsafe(query.sql, query.params), signal);
|
|
1664
|
+
}
|
|
1665
|
+
//#endregion
|
|
1342
1666
|
//#region src/db/queries/events.ts
|
|
1667
|
+
function timestampParam$1(value) {
|
|
1668
|
+
return sql`${value.toISOString()}::timestamptz`;
|
|
1669
|
+
}
|
|
1343
1670
|
async function insertEvent(db, row) {
|
|
1344
1671
|
const inserted = (await db.insert(eventsTable).values(row).returning())[0];
|
|
1345
1672
|
if (!inserted) throw new MnemocyteError("Event insert returned no rows.", "DB");
|
|
1346
1673
|
return inserted;
|
|
1347
1674
|
}
|
|
1348
|
-
async function listEvents(db, filter) {
|
|
1349
|
-
return db
|
|
1675
|
+
async function listEvents(db, filter, signal) {
|
|
1676
|
+
return executeCancelableSql(db, sql`
|
|
1677
|
+
SELECT
|
|
1678
|
+
id,
|
|
1679
|
+
entity_id AS "entityId",
|
|
1680
|
+
description,
|
|
1681
|
+
metadata,
|
|
1682
|
+
timestamp
|
|
1683
|
+
FROM mnemocyte_events
|
|
1684
|
+
WHERE entity_id = ${filter.entityId}
|
|
1685
|
+
${filter.before ? sql`AND timestamp < ${timestampParam$1(filter.before)}` : sql``}
|
|
1686
|
+
${filter.after ? sql`AND timestamp > ${timestampParam$1(filter.after)}` : sql``}
|
|
1687
|
+
${filter.beforeCursor ? sql`AND (timestamp, id) < (${timestampParam$1(filter.beforeCursor.timestamp)}, ${filter.beforeCursor.id})` : sql``}
|
|
1688
|
+
${filter.afterCursor ? sql`AND (timestamp, id) > (${timestampParam$1(filter.afterCursor.timestamp)}, ${filter.afterCursor.id})` : sql``}
|
|
1689
|
+
ORDER BY timestamp DESC, id DESC
|
|
1690
|
+
LIMIT ${filter.limit ?? 50}
|
|
1691
|
+
`, signal);
|
|
1350
1692
|
}
|
|
1351
1693
|
//#endregion
|
|
1352
1694
|
//#region src/db/queries/memories.ts
|
|
1353
1695
|
function vectorLiteral(embedding) {
|
|
1354
1696
|
return `[${embedding.map(formatVectorComponent).join(",")}]`;
|
|
1355
1697
|
}
|
|
1698
|
+
function clampedVectorScore(embedding) {
|
|
1699
|
+
const rawScore = sql`1 - (embedding <=> ${embedding}::vector)`;
|
|
1700
|
+
return sql`
|
|
1701
|
+
CASE
|
|
1702
|
+
WHEN ${rawScore} = 'NaN'::double precision
|
|
1703
|
+
OR ${rawScore} = 'Infinity'::double precision
|
|
1704
|
+
OR ${rawScore} = '-Infinity'::double precision
|
|
1705
|
+
THEN 0::double precision
|
|
1706
|
+
ELSE GREATEST(
|
|
1707
|
+
0::double precision,
|
|
1708
|
+
LEAST(1::double precision, ${rawScore})
|
|
1709
|
+
)
|
|
1710
|
+
END
|
|
1711
|
+
`;
|
|
1712
|
+
}
|
|
1356
1713
|
function timestampParam(value) {
|
|
1357
1714
|
return sql`${value.toISOString()}::timestamptz`;
|
|
1358
1715
|
}
|
|
@@ -1414,78 +1771,136 @@ async function getGlobalMemoryStatsCounts(db, now) {
|
|
|
1414
1771
|
};
|
|
1415
1772
|
}
|
|
1416
1773
|
async function deleteMemory(db, entityId, memoryId) {
|
|
1417
|
-
return (
|
|
1774
|
+
return deleteMemoriesWithDependentGuard(db, and(eq(memoriesTable.entityId, entityId), eq(memoriesTable.id, memoryId)));
|
|
1418
1775
|
}
|
|
1419
1776
|
async function deleteMemoriesForEntity(db, entityId) {
|
|
1420
|
-
return (
|
|
1777
|
+
return deleteMemoriesWithDependentGuard(db, eq(memoriesTable.entityId, entityId));
|
|
1421
1778
|
}
|
|
1422
1779
|
function pruneConditions(filter) {
|
|
1423
1780
|
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, tagsContainAll(filter.tags), filter.maxImportanceLevels && filter.maxImportanceLevels.length > 0 ? inArray(memoriesTable.importance, [...filter.maxImportanceLevels]) : void 0);
|
|
1424
1781
|
}
|
|
1425
|
-
async function countPruneMatches(db, filter) {
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1782
|
+
async function countPruneMatches(db, filter, signal) {
|
|
1783
|
+
const conditions = pruneConditions(filter);
|
|
1784
|
+
const rows = await executeCancelableSql(db, sql`
|
|
1785
|
+
SELECT count(*)::int AS count
|
|
1786
|
+
FROM ${memoriesTable}
|
|
1787
|
+
${conditions ? sql`WHERE ${conditions}` : sql``}
|
|
1788
|
+
`, signal);
|
|
1789
|
+
return Number(rows[0]?.count ?? 0);
|
|
1790
|
+
}
|
|
1791
|
+
async function pruneMemories(db, filter, signal) {
|
|
1792
|
+
return deleteMemoriesWithDependentGuard(db, pruneConditions(filter), signal);
|
|
1793
|
+
}
|
|
1794
|
+
async function deleteMemoriesWithDependentGuard(db, conditions, signal) {
|
|
1795
|
+
const rows = await executeCancelableSql(db, sql`
|
|
1796
|
+
WITH candidates AS MATERIALIZED (
|
|
1797
|
+
SELECT id, entity_id
|
|
1798
|
+
FROM ${memoriesTable}
|
|
1799
|
+
${conditions ? sql`WHERE ${conditions}` : sql``}
|
|
1800
|
+
),
|
|
1801
|
+
dependents AS MATERIALIZED (
|
|
1802
|
+
SELECT 1
|
|
1803
|
+
FROM ${memoriesTable} AS dependent
|
|
1804
|
+
INNER JOIN candidates AS target
|
|
1805
|
+
ON dependent.superseded_by = target.id
|
|
1806
|
+
LIMIT 1
|
|
1807
|
+
),
|
|
1808
|
+
deleted AS (
|
|
1809
|
+
DELETE FROM ${memoriesTable}
|
|
1810
|
+
WHERE ${memoriesTable.id} IN (SELECT id FROM candidates)
|
|
1811
|
+
AND NOT EXISTS (SELECT 1 FROM dependents)
|
|
1812
|
+
RETURNING id, entity_id
|
|
1813
|
+
),
|
|
1814
|
+
deleted_counts AS (
|
|
1815
|
+
SELECT entity_id, count(*)::int AS deleted_count
|
|
1816
|
+
FROM deleted
|
|
1817
|
+
GROUP BY entity_id
|
|
1818
|
+
),
|
|
1819
|
+
summary AS (
|
|
1820
|
+
SELECT
|
|
1821
|
+
(SELECT count(*)::int FROM candidates) AS matched_count,
|
|
1822
|
+
(SELECT count(*)::int FROM deleted) AS deleted_count,
|
|
1823
|
+
EXISTS (SELECT 1 FROM dependents) AS has_dependents
|
|
1824
|
+
)
|
|
1825
|
+
SELECT
|
|
1826
|
+
summary.matched_count AS "matchedCount",
|
|
1827
|
+
summary.deleted_count AS "deletedCount",
|
|
1828
|
+
summary.has_dependents AS "hasDependents",
|
|
1829
|
+
deleted_counts.entity_id AS "entityId",
|
|
1830
|
+
deleted_counts.deleted_count AS "entityDeletedCount"
|
|
1831
|
+
FROM summary
|
|
1832
|
+
LEFT JOIN deleted_counts ON true
|
|
1833
|
+
ORDER BY deleted_counts.entity_id
|
|
1834
|
+
`, signal);
|
|
1835
|
+
const row = rows[0];
|
|
1836
|
+
return {
|
|
1837
|
+
matchedCount: Number(row?.matchedCount ?? 0),
|
|
1838
|
+
deletedCount: Number(row?.deletedCount ?? 0),
|
|
1839
|
+
hasDependents: row?.hasDependents === true,
|
|
1840
|
+
deletedByEntity: rows.flatMap((detail) => detail.entityId === null ? [] : [{
|
|
1841
|
+
entityId: detail.entityId,
|
|
1842
|
+
deletedCount: Number(detail.entityDeletedCount ?? 0)
|
|
1843
|
+
}])
|
|
1844
|
+
};
|
|
1430
1845
|
}
|
|
1431
|
-
async function findDuplicatePairs(db, input) {
|
|
1432
|
-
return (await db
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1846
|
+
async function findDuplicatePairs(db, input, signal) {
|
|
1847
|
+
return (await executeCancelableSql(db, sql`
|
|
1848
|
+
SELECT
|
|
1849
|
+
a.id AS "aId",
|
|
1850
|
+
a.entity_id AS "aEntityId",
|
|
1851
|
+
a.content AS "aContent",
|
|
1852
|
+
a.type AS "aType",
|
|
1853
|
+
a.importance AS "aImportance",
|
|
1854
|
+
a.tags AS "aTags",
|
|
1855
|
+
a.source AS "aSource",
|
|
1856
|
+
a.metadata AS "aMetadata",
|
|
1857
|
+
a.confidence AS "aConfidence",
|
|
1858
|
+
a.embedding AS "aEmbedding",
|
|
1859
|
+
a.embedding_model AS "aEmbeddingModel",
|
|
1860
|
+
a.embedding_dimensions AS "aEmbeddingDimensions",
|
|
1861
|
+
a.superseded_by AS "aSupersededBy",
|
|
1862
|
+
a.superseded_at AS "aSupersededAt",
|
|
1863
|
+
a.expires_at AS "aExpiresAt",
|
|
1864
|
+
a.last_accessed_at AS "aLastAccessedAt",
|
|
1865
|
+
a.access_count AS "aAccessCount",
|
|
1866
|
+
a.created_at AS "aCreatedAt",
|
|
1867
|
+
a.updated_at AS "aUpdatedAt",
|
|
1868
|
+
b.id AS "bId",
|
|
1869
|
+
b.entity_id AS "bEntityId",
|
|
1870
|
+
b.content AS "bContent",
|
|
1871
|
+
b.type AS "bType",
|
|
1872
|
+
b.importance AS "bImportance",
|
|
1873
|
+
b.tags AS "bTags",
|
|
1874
|
+
b.source AS "bSource",
|
|
1875
|
+
b.metadata AS "bMetadata",
|
|
1876
|
+
b.confidence AS "bConfidence",
|
|
1877
|
+
b.embedding AS "bEmbedding",
|
|
1878
|
+
b.embedding_model AS "bEmbeddingModel",
|
|
1879
|
+
b.embedding_dimensions AS "bEmbeddingDimensions",
|
|
1880
|
+
b.superseded_by AS "bSupersededBy",
|
|
1881
|
+
b.superseded_at AS "bSupersededAt",
|
|
1882
|
+
b.expires_at AS "bExpiresAt",
|
|
1883
|
+
b.last_accessed_at AS "bLastAccessedAt",
|
|
1884
|
+
b.access_count AS "bAccessCount",
|
|
1885
|
+
b.created_at AS "bCreatedAt",
|
|
1886
|
+
b.updated_at AS "bUpdatedAt",
|
|
1887
|
+
1 - (a.embedding <=> b.embedding) AS "similarity"
|
|
1888
|
+
FROM mnemocyte_memories a
|
|
1889
|
+
JOIN mnemocyte_memories b
|
|
1890
|
+
ON b.entity_id = a.entity_id
|
|
1891
|
+
AND a.id < b.id
|
|
1892
|
+
AND a.embedding IS NOT NULL
|
|
1893
|
+
AND b.embedding IS NOT NULL
|
|
1894
|
+
WHERE
|
|
1895
|
+
a.entity_id = ${input.entityId}
|
|
1896
|
+
${input.includeSuperseded ? sql`` : sql`AND a.superseded_by IS NULL AND b.superseded_by IS NULL`}
|
|
1897
|
+
${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())`}
|
|
1898
|
+
${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``}
|
|
1899
|
+
${duplicateTagsFilter(input.tags)}
|
|
1900
|
+
AND 1 - (a.embedding <=> b.embedding) >= ${input.threshold}
|
|
1901
|
+
ORDER BY "similarity" DESC
|
|
1902
|
+
LIMIT ${input.limit}
|
|
1903
|
+
`, signal)).map((row) => {
|
|
1489
1904
|
return {
|
|
1490
1905
|
a: {
|
|
1491
1906
|
id: row.aId,
|
|
@@ -1541,6 +1956,14 @@ async function loadConsolidationTargets(db, entityId, ids) {
|
|
|
1541
1956
|
supersededBy: memoriesTable.supersededBy
|
|
1542
1957
|
}).from(memoriesTable).where(and(eq(memoriesTable.entityId, entityId), inArray(memoriesTable.id, [...ids])));
|
|
1543
1958
|
}
|
|
1959
|
+
async function lockConsolidationTargets(db, entityId, ids) {
|
|
1960
|
+
if (ids.length === 0) return [];
|
|
1961
|
+
return db.select({
|
|
1962
|
+
id: memoriesTable.id,
|
|
1963
|
+
tags: memoriesTable.tags,
|
|
1964
|
+
supersededBy: memoriesTable.supersededBy
|
|
1965
|
+
}).from(memoriesTable).where(and(eq(memoriesTable.entityId, entityId), inArray(memoriesTable.id, [...ids]))).orderBy(memoriesTable.id).for("update");
|
|
1966
|
+
}
|
|
1544
1967
|
async function markMemoriesSuperseded(db, params) {
|
|
1545
1968
|
if (params.ids.length === 0) return [];
|
|
1546
1969
|
return await db.update(memoriesTable).set({
|
|
@@ -1560,12 +1983,17 @@ async function setMemoryTags(db, params) {
|
|
|
1560
1983
|
}
|
|
1561
1984
|
async function markMemoriesAccessed(db, memoryIds) {
|
|
1562
1985
|
const now = /* @__PURE__ */ new Date();
|
|
1563
|
-
if (memoryIds.length === 0) return;
|
|
1564
|
-
|
|
1986
|
+
if (memoryIds.length === 0) return [];
|
|
1987
|
+
return db.update(memoriesTable).set({
|
|
1565
1988
|
lastAccessedAt: now,
|
|
1566
1989
|
accessCount: sql`${memoriesTable.accessCount} + 1`,
|
|
1567
1990
|
updatedAt: now
|
|
1568
|
-
}).where(inArray(memoriesTable.id, [...memoryIds]))
|
|
1991
|
+
}).where(inArray(memoriesTable.id, [...memoryIds])).returning({
|
|
1992
|
+
id: memoriesTable.id,
|
|
1993
|
+
lastAccessedAt: memoriesTable.lastAccessedAt,
|
|
1994
|
+
accessCount: memoriesTable.accessCount,
|
|
1995
|
+
updatedAt: memoriesTable.updatedAt
|
|
1996
|
+
});
|
|
1569
1997
|
}
|
|
1570
1998
|
async function getMemoryEmbeddings(db, memoryIds) {
|
|
1571
1999
|
if (memoryIds.length === 0) return /* @__PURE__ */ new Map();
|
|
@@ -1577,7 +2005,8 @@ async function getMemoryEmbeddings(db, memoryIds) {
|
|
|
1577
2005
|
}
|
|
1578
2006
|
async function vectorSearch(db, input) {
|
|
1579
2007
|
const embedding = vectorLiteral(input.embedding);
|
|
1580
|
-
const
|
|
2008
|
+
const vectorScore = clampedVectorScore(embedding);
|
|
2009
|
+
const minVectorScore = input.minVectorScore ?? 0;
|
|
1581
2010
|
return await db.execute(sql`
|
|
1582
2011
|
SELECT
|
|
1583
2012
|
id,
|
|
@@ -1598,7 +2027,7 @@ async function vectorSearch(db, input) {
|
|
|
1598
2027
|
access_count AS "accessCount",
|
|
1599
2028
|
created_at AS "createdAt",
|
|
1600
2029
|
updated_at AS "updatedAt",
|
|
1601
|
-
|
|
2030
|
+
${vectorScore} AS "vectorScore"
|
|
1602
2031
|
FROM mnemocyte_memories
|
|
1603
2032
|
WHERE
|
|
1604
2033
|
entity_id = ${input.entityId}
|
|
@@ -1609,7 +2038,7 @@ async function vectorSearch(db, input) {
|
|
|
1609
2038
|
${rawTagsFilter(input.tags)}
|
|
1610
2039
|
${input.before ? sql`AND created_at < ${timestampParam(input.before)}` : sql``}
|
|
1611
2040
|
${input.after ? sql`AND created_at > ${timestampParam(input.after)}` : sql``}
|
|
1612
|
-
AND
|
|
2041
|
+
AND ${vectorScore} >= ${minVectorScore}
|
|
1613
2042
|
ORDER BY embedding <=> ${embedding}::vector
|
|
1614
2043
|
LIMIT ${input.limit}
|
|
1615
2044
|
`);
|
|
@@ -1653,6 +2082,12 @@ async function lexicalSearch(db, input) {
|
|
|
1653
2082
|
async function getInstallationMeta(db) {
|
|
1654
2083
|
return (await db.select().from(metaTable).where(eq(metaTable.key, "installation")).limit(1))[0] ?? null;
|
|
1655
2084
|
}
|
|
2085
|
+
async function getStoredEmbeddingModels(db) {
|
|
2086
|
+
return (await db.selectDistinct({ embeddingModel: memoriesTable.embeddingModel }).from(memoriesTable).limit(2)).map((row) => row.embeddingModel);
|
|
2087
|
+
}
|
|
2088
|
+
async function recordInstallationEmbeddingModel(db, embeddingModel) {
|
|
2089
|
+
return (await db.update(metaTable).set({ embeddingModel }).where(and(eq(metaTable.key, "installation"), isNull(metaTable.embeddingModel))).returning())[0] ?? null;
|
|
2090
|
+
}
|
|
1656
2091
|
//#endregion
|
|
1657
2092
|
//#region src/memory/postgres-records.ts
|
|
1658
2093
|
function toDate(value) {
|
|
@@ -1697,13 +2132,14 @@ const MIGRATION_ERROR_CODES = new Set([
|
|
|
1697
2132
|
"42704",
|
|
1698
2133
|
"42883"
|
|
1699
2134
|
]);
|
|
2135
|
+
const CONSOLIDATION_SURVIVOR_FOREIGN_KEY = "mnemocyte_memories_superseded_by_mnemocyte_memories_id_fk";
|
|
1700
2136
|
function rowToAuditEvent(row) {
|
|
1701
2137
|
return {
|
|
1702
2138
|
id: row.id,
|
|
1703
2139
|
entityId: row.entityId,
|
|
1704
2140
|
description: row.description,
|
|
1705
2141
|
metadata: cloneJsonObject(row.metadata),
|
|
1706
|
-
timestamp: row.timestamp
|
|
2142
|
+
timestamp: new Date(row.timestamp)
|
|
1707
2143
|
};
|
|
1708
2144
|
}
|
|
1709
2145
|
function importanceCeilingLevels(max) {
|
|
@@ -1712,6 +2148,9 @@ function importanceCeilingLevels(max) {
|
|
|
1712
2148
|
function hasPostgresErrorCode(error, code) {
|
|
1713
2149
|
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
1714
2150
|
}
|
|
2151
|
+
function hasPostgresConstraintName(error, name) {
|
|
2152
|
+
return typeof error === "object" && error !== null && "constraint_name" in error && error.constraint_name === name;
|
|
2153
|
+
}
|
|
1715
2154
|
function getPostgresErrorCode(error) {
|
|
1716
2155
|
return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0;
|
|
1717
2156
|
}
|
|
@@ -1728,6 +2167,15 @@ async function runPostgresOperation(operation) {
|
|
|
1728
2167
|
throw normalizePostgresError(error);
|
|
1729
2168
|
}
|
|
1730
2169
|
}
|
|
2170
|
+
async function runPostgresDeleteOperation(operation) {
|
|
2171
|
+
try {
|
|
2172
|
+
return await operation();
|
|
2173
|
+
} catch (error) {
|
|
2174
|
+
if (error instanceof MnemocyteError) throw error;
|
|
2175
|
+
if (hasPostgresErrorCode(error, "23503") && hasPostgresConstraintName(error, CONSOLIDATION_SURVIVOR_FOREIGN_KEY)) throw memoryHasDependentsError(error);
|
|
2176
|
+
throw normalizePostgresError(error);
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
1731
2179
|
function toPruneFilter(input) {
|
|
1732
2180
|
const filter = {};
|
|
1733
2181
|
if (input.entityId !== void 0) filter.entityId = input.entityId;
|
|
@@ -1744,7 +2192,6 @@ function toMemoryRow(memory) {
|
|
|
1744
2192
|
return {
|
|
1745
2193
|
...memory,
|
|
1746
2194
|
tags: [...memory.tags],
|
|
1747
|
-
metadata: cloneJsonObject(memory.metadata),
|
|
1748
2195
|
embedding: [...memory.embedding]
|
|
1749
2196
|
};
|
|
1750
2197
|
}
|
|
@@ -1764,11 +2211,26 @@ function createPostgresStore(handle) {
|
|
|
1764
2211
|
try {
|
|
1765
2212
|
meta = await getInstallationMeta(handle.db);
|
|
1766
2213
|
} catch (error) {
|
|
1767
|
-
if (hasPostgresErrorCode(error, "42P01")) throw new MnemocyteError("
|
|
2214
|
+
if (hasPostgresErrorCode(error, "42P01") || hasPostgresErrorCode(error, "42703")) throw new MnemocyteError("Postgres embedding metadata is missing or outdated. Apply the bundled migrations through 0002_add_embedding_model.sql, or render a fresh dimension-specific initial migration, before using embedding-dependent operations.", "MIGRATION", error);
|
|
1768
2215
|
throw normalizePostgresError(error);
|
|
1769
2216
|
}
|
|
1770
|
-
if (!meta) throw new MnemocyteError("mnemocyte_meta is missing the \"installation\" row. Apply the
|
|
2217
|
+
if (!meta) throw new MnemocyteError("mnemocyte_meta is missing the \"installation\" row. Apply the bundled migrations, or render a fresh dimension-specific initial migration, before using embedding-dependent operations.", "MIGRATION");
|
|
1771
2218
|
if (meta.embeddingDimensions !== embedder.dimensions) throw new MnemocyteError(`embedder.dimensions (${embedder.dimensions}) must match mnemocyte_meta.embedding_dimensions (${meta.embeddingDimensions}). Render and apply a migration for the selected embedding dimension, or configure an embedder that matches this installation.`, "CONFIG");
|
|
2219
|
+
let installationModel = meta.embeddingModel;
|
|
2220
|
+
if (installationModel === null) {
|
|
2221
|
+
try {
|
|
2222
|
+
const storedModels = await getStoredEmbeddingModels(handle.db);
|
|
2223
|
+
if (storedModels.length > 1) throw new MnemocyteError("mnemocyte_meta.embedding_model is unset, but stored memories contain multiple embedding_model values. Re-embed or remove the mixed historical rows, then explicitly record the installation model before using embedding-dependent operations.", "MIGRATION");
|
|
2224
|
+
const inferredModel = storedModels[0] ?? embedder.model;
|
|
2225
|
+
const recorded = await recordInstallationEmbeddingModel(handle.db, inferredModel);
|
|
2226
|
+
if (recorded) installationModel = recorded.embeddingModel;
|
|
2227
|
+
else installationModel = (await getInstallationMeta(handle.db))?.embeddingModel ?? null;
|
|
2228
|
+
} catch (error) {
|
|
2229
|
+
throw normalizePostgresError(error);
|
|
2230
|
+
}
|
|
2231
|
+
if (installationModel === null) throw new MnemocyteError("mnemocyte_meta.embedding_model could not be recorded. Verify the installation metadata row and retry.", "MIGRATION");
|
|
2232
|
+
}
|
|
2233
|
+
if (installationModel !== embedder.model) throw new MnemocyteError(`embedder.model ("${embedder.model}") must match mnemocyte_meta.embedding_model ("${installationModel}"). Configure the recorded model, or explicitly re-embed existing memories and update the installation metadata.`, "CONFIG");
|
|
1772
2234
|
schemaValidated = true;
|
|
1773
2235
|
}
|
|
1774
2236
|
return {
|
|
@@ -1800,31 +2262,52 @@ function createPostgresStore(handle) {
|
|
|
1800
2262
|
return runPostgresOperation(() => getMemoryEmbeddings(handle.db, memoryIds));
|
|
1801
2263
|
},
|
|
1802
2264
|
async markMemoriesAccessed(memoryIds) {
|
|
1803
|
-
return runPostgresOperation(() =>
|
|
2265
|
+
return runPostgresOperation(async () => {
|
|
2266
|
+
return (await markMemoriesAccessed(handle.db, memoryIds)).map((row) => {
|
|
2267
|
+
if (row.lastAccessedAt === null) throw new MnemocyteError("Postgres access update returned no timestamp.", "DB");
|
|
2268
|
+
return {
|
|
2269
|
+
...row,
|
|
2270
|
+
lastAccessedAt: row.lastAccessedAt
|
|
2271
|
+
};
|
|
2272
|
+
});
|
|
2273
|
+
});
|
|
1804
2274
|
},
|
|
1805
2275
|
async deleteMemory(entityId, memoryId) {
|
|
1806
|
-
return
|
|
2276
|
+
return runPostgresDeleteOperation(async () => {
|
|
2277
|
+
const result = await deleteMemory(handle.db, entityId, memoryId);
|
|
2278
|
+
if (result.hasDependents) throw memoryHasDependentsError();
|
|
2279
|
+
return result.deletedCount > 0;
|
|
2280
|
+
});
|
|
1807
2281
|
},
|
|
1808
2282
|
async deleteMemoriesForEntity(entityId) {
|
|
1809
|
-
return
|
|
2283
|
+
return runPostgresDeleteOperation(async () => {
|
|
2284
|
+
const result = await deleteMemoriesForEntity(handle.db, entityId);
|
|
2285
|
+
if (result.hasDependents) throw memoryHasDependentsError();
|
|
2286
|
+
return result.deletedCount;
|
|
2287
|
+
});
|
|
1810
2288
|
},
|
|
1811
|
-
async prune(input) {
|
|
1812
|
-
return
|
|
2289
|
+
async prune(input, options) {
|
|
2290
|
+
return runPostgresDeleteOperation(async () => {
|
|
2291
|
+
throwIfAborted(options?.signal);
|
|
2292
|
+
assertPruneFilterHasSelector(input);
|
|
1813
2293
|
const filter = toPruneFilter(input);
|
|
1814
2294
|
if (input.dryRun === true) return {
|
|
1815
|
-
matchedCount: await countPruneMatches(handle.db, filter),
|
|
2295
|
+
matchedCount: await countPruneMatches(handle.db, filter, options?.signal),
|
|
1816
2296
|
deletedCount: 0,
|
|
1817
|
-
dryRun: true
|
|
2297
|
+
dryRun: true,
|
|
2298
|
+
deletedByEntity: []
|
|
1818
2299
|
};
|
|
1819
|
-
const
|
|
2300
|
+
const result = await pruneMemories(handle.db, filter, options?.signal);
|
|
2301
|
+
if (result.hasDependents) throw memoryHasDependentsError();
|
|
1820
2302
|
return {
|
|
1821
|
-
matchedCount:
|
|
1822
|
-
deletedCount,
|
|
1823
|
-
dryRun: false
|
|
2303
|
+
matchedCount: result.matchedCount,
|
|
2304
|
+
deletedCount: result.deletedCount,
|
|
2305
|
+
dryRun: false,
|
|
2306
|
+
deletedByEntity: result.deletedByEntity
|
|
1824
2307
|
};
|
|
1825
2308
|
});
|
|
1826
2309
|
},
|
|
1827
|
-
async findDuplicatePairs(input) {
|
|
2310
|
+
async findDuplicatePairs(input, options) {
|
|
1828
2311
|
return runPostgresOperation(async () => {
|
|
1829
2312
|
return (await findDuplicatePairs(handle.db, {
|
|
1830
2313
|
entityId: input.entityId,
|
|
@@ -1834,7 +2317,7 @@ function createPostgresStore(handle) {
|
|
|
1834
2317
|
...input.tags === void 0 ? {} : { tags: input.tags },
|
|
1835
2318
|
...input.includeSuperseded === void 0 ? {} : { includeSuperseded: input.includeSuperseded },
|
|
1836
2319
|
...input.includeExpired === void 0 ? {} : { includeExpired: input.includeExpired }
|
|
1837
|
-
})).map((row) => ({
|
|
2320
|
+
}, options?.signal)).map((row) => ({
|
|
1838
2321
|
a: rowToMemory(row.a),
|
|
1839
2322
|
b: rowToMemory(row.b),
|
|
1840
2323
|
similarity: Math.max(0, Math.min(1, row.similarity))
|
|
@@ -1852,55 +2335,83 @@ function createPostgresStore(handle) {
|
|
|
1852
2335
|
});
|
|
1853
2336
|
});
|
|
1854
2337
|
},
|
|
1855
|
-
async listAuditLog(input) {
|
|
2338
|
+
async listAuditLog(input, options) {
|
|
1856
2339
|
return runPostgresOperation(async () => {
|
|
1857
2340
|
return (await listEvents(handle.db, {
|
|
1858
2341
|
entityId: input.entityId,
|
|
1859
2342
|
limit: input.limit ?? 50,
|
|
1860
2343
|
...input.before === void 0 ? {} : { before: input.before },
|
|
1861
|
-
...input.after === void 0 ? {} : { after: input.after }
|
|
1862
|
-
|
|
2344
|
+
...input.after === void 0 ? {} : { after: input.after },
|
|
2345
|
+
...input.beforeCursor === void 0 ? {} : { beforeCursor: input.beforeCursor },
|
|
2346
|
+
...input.afterCursor === void 0 ? {} : { afterCursor: input.afterCursor }
|
|
2347
|
+
}, options?.signal)).map(rowToAuditEvent);
|
|
1863
2348
|
});
|
|
1864
2349
|
},
|
|
1865
|
-
async getMemory(entityId, memoryId) {
|
|
2350
|
+
async getMemory(entityId, memoryId, options) {
|
|
1866
2351
|
return runPostgresOperation(async () => {
|
|
2352
|
+
throwIfAborted(options?.signal);
|
|
1867
2353
|
const row = await getMemoryById(handle.db, entityId, memoryId);
|
|
2354
|
+
throwIfAborted(options?.signal);
|
|
1868
2355
|
return row ? rowToMemory(row) : null;
|
|
1869
2356
|
});
|
|
1870
2357
|
},
|
|
1871
|
-
async loadConsolidationTargets(entityId, ids) {
|
|
1872
|
-
return runPostgresOperation(() =>
|
|
2358
|
+
async loadConsolidationTargets(entityId, ids, options) {
|
|
2359
|
+
return runPostgresOperation(async () => {
|
|
2360
|
+
throwIfAborted(options?.signal);
|
|
2361
|
+
const targets = await loadConsolidationTargets(handle.db, entityId, ids);
|
|
2362
|
+
throwIfAborted(options?.signal);
|
|
2363
|
+
return targets;
|
|
2364
|
+
});
|
|
1873
2365
|
},
|
|
1874
|
-
async consolidate(input) {
|
|
2366
|
+
async consolidate(input, options) {
|
|
1875
2367
|
return runPostgresOperation(async () => {
|
|
2368
|
+
throwIfAborted(options?.signal);
|
|
1876
2369
|
return { supersededIds: await handle.db.transaction(async (tx) => {
|
|
2370
|
+
throwIfAborted(options?.signal);
|
|
2371
|
+
const targets = await lockConsolidationTargets(tx, input.entityId, input.supersededIds);
|
|
2372
|
+
throwIfAborted(options?.signal);
|
|
2373
|
+
if (targets.some((target) => target.supersededBy !== null && target.supersededBy !== input.survivorId)) throw new MnemocyteError("Superseded memory already belongs to a different survivor.", "CONFLICT");
|
|
2374
|
+
const activeIds = targets.filter((target) => target.supersededBy === null).map((target) => target.id);
|
|
1877
2375
|
const updated = await markMemoriesSuperseded(tx, {
|
|
1878
2376
|
survivorId: input.survivorId,
|
|
1879
2377
|
entityId: input.entityId,
|
|
1880
|
-
ids:
|
|
2378
|
+
ids: activeIds,
|
|
1881
2379
|
now: input.now
|
|
1882
2380
|
});
|
|
2381
|
+
throwIfAborted(options?.signal);
|
|
1883
2382
|
const ids = updated.map((row) => row.id);
|
|
1884
|
-
if (input.auditEnabled) for (const id of ids)
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
metadata: {
|
|
1889
|
-
memoryId: id,
|
|
1890
|
-
supersededBy: input.survivorId
|
|
1891
|
-
},
|
|
1892
|
-
timestamp: input.now
|
|
1893
|
-
});
|
|
1894
|
-
if (input.mergeTags && updated.length > 0) {
|
|
1895
|
-
const mergedTags = new Set(input.survivorTags);
|
|
1896
|
-
for (const row of updated) for (const tag of row.tags) mergedTags.add(tag);
|
|
1897
|
-
if (mergedTags.size !== input.survivorTags.length) await setMemoryTags(tx, {
|
|
2383
|
+
if (input.auditEnabled) for (const id of ids) {
|
|
2384
|
+
throwIfAborted(options?.signal);
|
|
2385
|
+
await insertEvent(tx, {
|
|
2386
|
+
id: createEventId(),
|
|
1898
2387
|
entityId: input.entityId,
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
2388
|
+
description: "memory.superseded",
|
|
2389
|
+
metadata: {
|
|
2390
|
+
memoryId: id,
|
|
2391
|
+
supersededBy: input.survivorId
|
|
2392
|
+
},
|
|
2393
|
+
timestamp: input.now
|
|
1902
2394
|
});
|
|
2395
|
+
throwIfAborted(options?.signal);
|
|
2396
|
+
}
|
|
2397
|
+
if (input.mergeTags && updated.length > 0) {
|
|
2398
|
+
const mergedTags = new Set(input.survivorTags);
|
|
2399
|
+
for (const row of updated) {
|
|
2400
|
+
throwIfAborted(options?.signal);
|
|
2401
|
+
for (const tag of row.tags) mergedTags.add(tag);
|
|
2402
|
+
}
|
|
2403
|
+
if (mergedTags.size !== input.survivorTags.length) {
|
|
2404
|
+
throwIfAborted(options?.signal);
|
|
2405
|
+
await setMemoryTags(tx, {
|
|
2406
|
+
entityId: input.entityId,
|
|
2407
|
+
memoryId: input.survivorId,
|
|
2408
|
+
tags: [...mergedTags],
|
|
2409
|
+
now: input.now
|
|
2410
|
+
});
|
|
2411
|
+
throwIfAborted(options?.signal);
|
|
2412
|
+
}
|
|
1903
2413
|
}
|
|
2414
|
+
throwIfAborted(options?.signal);
|
|
1904
2415
|
return ids;
|
|
1905
2416
|
}) };
|
|
1906
2417
|
});
|
|
@@ -1942,7 +2453,8 @@ function createPostgresClient(config, handle) {
|
|
|
1942
2453
|
* @param config - Client configuration. {@link MnemocyteConfig.embedder} is required.
|
|
1943
2454
|
* @returns A {@link MnemocyteClient} instance.
|
|
1944
2455
|
* @throws {MnemocyteError} With code `"CONFIG"` if `embedder`, retrieval
|
|
1945
|
-
* tuning, or the database URL is malformed
|
|
2456
|
+
* tuning, provider resilience, or the database URL is malformed or does not
|
|
2457
|
+
* use the `postgres:` / `postgresql:` protocol, or `"VALIDATION"` if
|
|
1946
2458
|
* `databaseUrl` is explicitly empty.
|
|
1947
2459
|
*
|
|
1948
2460
|
* @example Postgres-backed client
|
|
@@ -1956,6 +2468,7 @@ function createPostgresClient(config, handle) {
|
|
|
1956
2468
|
function createMnemocyte(config) {
|
|
1957
2469
|
assertEmbedder(config.embedder);
|
|
1958
2470
|
validateRetrievalConfig(config.retrieval);
|
|
2471
|
+
validateProviderResilienceConfig(config.provider);
|
|
1959
2472
|
if (config.databaseUrl !== void 0) {
|
|
1960
2473
|
assertNonEmptyString(config.databaseUrl, "databaseUrl");
|
|
1961
2474
|
return createPostgresClient(config, createDatabase(config.databaseUrl));
|