mnemocyte 0.0.8 → 0.1.1
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 +168 -77
- package/dist/index.d.mts +263 -6
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +531 -75
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -1
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { drizzle } from "drizzle-orm/postgres-js";
|
|
2
2
|
import postgres from "postgres";
|
|
3
|
-
import { and, eq, gt, inArray, isNotNull, isNull, lt, or, sql } from "drizzle-orm";
|
|
3
|
+
import { and, desc, eq, gt, inArray, isNotNull, isNull, lt, or, sql } from "drizzle-orm";
|
|
4
4
|
import { customType, index, integer, jsonb, pgTable, real, text, timestamp } from "drizzle-orm/pg-core";
|
|
5
5
|
//#region \0rolldown/runtime.js
|
|
6
6
|
var __defProp = Object.defineProperty;
|
|
@@ -24,12 +24,16 @@ const vector = customType({
|
|
|
24
24
|
return `vector(${typeof config === "number" ? config : 1536})`;
|
|
25
25
|
},
|
|
26
26
|
toDriver(value) {
|
|
27
|
-
return `[${value.join(",")}]`;
|
|
27
|
+
return `[${value.map(formatVectorComponent$1).join(",")}]`;
|
|
28
28
|
},
|
|
29
29
|
fromDriver(value) {
|
|
30
30
|
return value.slice(1, -1).split(",").filter(Boolean).map(Number);
|
|
31
31
|
}
|
|
32
32
|
});
|
|
33
|
+
function formatVectorComponent$1(value) {
|
|
34
|
+
if (!Number.isFinite(value)) throw new Error("Vector values must be finite numbers.");
|
|
35
|
+
return Object.is(value, -0) ? "0" : value.toFixed(17);
|
|
36
|
+
}
|
|
33
37
|
const memoriesTable = pgTable("mnemocyte_memories", {
|
|
34
38
|
id: text("id").primaryKey(),
|
|
35
39
|
entityId: text("entity_id").notNull(),
|
|
@@ -43,7 +47,7 @@ const memoriesTable = pgTable("mnemocyte_memories", {
|
|
|
43
47
|
embedding: vector("embedding", { dimensions: 1536 }),
|
|
44
48
|
embeddingModel: text("embedding_model").notNull(),
|
|
45
49
|
embeddingDimensions: integer("embedding_dimensions").notNull(),
|
|
46
|
-
supersededBy: text("superseded_by"),
|
|
50
|
+
supersededBy: text("superseded_by").references(() => memoriesTable.id),
|
|
47
51
|
supersededAt: timestamp("superseded_at", { withTimezone: true }),
|
|
48
52
|
expiresAt: timestamp("expires_at", { withTimezone: true }),
|
|
49
53
|
lastAccessedAt: timestamp("last_accessed_at", { withTimezone: true }),
|
|
@@ -74,6 +78,56 @@ function createDatabase(databaseUrl) {
|
|
|
74
78
|
};
|
|
75
79
|
}
|
|
76
80
|
//#endregion
|
|
81
|
+
//#region src/errors.ts
|
|
82
|
+
/**
|
|
83
|
+
* Error type thrown by every public Mnemocyte API.
|
|
84
|
+
*
|
|
85
|
+
* Use the {@link MnemocyteError.code} property — not the message — to branch
|
|
86
|
+
* on failure modes, or call {@link isMnemocyteError} as a type guard when
|
|
87
|
+
* working with `unknown` errors.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```ts
|
|
91
|
+
* try {
|
|
92
|
+
* await client.recall({ entityId: "u1", query: "hi" });
|
|
93
|
+
* } catch (err) {
|
|
94
|
+
* if (isMnemocyteError(err) && err.code === "DB") {
|
|
95
|
+
* // handle database failure
|
|
96
|
+
* }
|
|
97
|
+
* throw err;
|
|
98
|
+
* }
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
var MnemocyteError = class extends Error {
|
|
102
|
+
/** Stable, machine-readable failure code. See {@link MnemocyteErrorCode}. */
|
|
103
|
+
code;
|
|
104
|
+
/** Underlying cause, if any (e.g. an original driver or embedder error). */
|
|
105
|
+
cause;
|
|
106
|
+
/**
|
|
107
|
+
* @param message - Human-readable error message.
|
|
108
|
+
* @param code - Stable failure code; see {@link MnemocyteErrorCode}.
|
|
109
|
+
* @param cause - Optional underlying cause (driver error, network error, etc.).
|
|
110
|
+
*/
|
|
111
|
+
constructor(message, code, cause) {
|
|
112
|
+
super(message);
|
|
113
|
+
this.name = "MnemocyteError";
|
|
114
|
+
this.code = code;
|
|
115
|
+
this.cause = cause;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* Type guard for {@link MnemocyteError}.
|
|
120
|
+
*
|
|
121
|
+
* Prefer this over `instanceof` when working across module boundaries or
|
|
122
|
+
* bundlers where multiple copies of the class may exist.
|
|
123
|
+
*
|
|
124
|
+
* @param error - Value to test (typically a caught `unknown`).
|
|
125
|
+
* @returns `true` if `error` is a {@link MnemocyteError}.
|
|
126
|
+
*/
|
|
127
|
+
function isMnemocyteError(error) {
|
|
128
|
+
return error instanceof MnemocyteError;
|
|
129
|
+
}
|
|
130
|
+
//#endregion
|
|
77
131
|
//#region src/context/formatter.ts
|
|
78
132
|
function escapeXml(value) {
|
|
79
133
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
@@ -88,7 +142,11 @@ function formatMetadata(memory) {
|
|
|
88
142
|
if (memory.tags.length > 0) parts.push(`tags=${memory.tags.join(",")}`);
|
|
89
143
|
return parts.join("; ");
|
|
90
144
|
}
|
|
91
|
-
function
|
|
145
|
+
function markdownFence(value) {
|
|
146
|
+
const longestBacktickRun = Math.max(0, ...Array.from(value.matchAll(/`+/g), (match) => match[0].length));
|
|
147
|
+
return "`".repeat(Math.max(3, longestBacktickRun + 1));
|
|
148
|
+
}
|
|
149
|
+
function formatMarkdown(memories, query, omittedCount) {
|
|
92
150
|
const lines = [
|
|
93
151
|
"# Memory Context",
|
|
94
152
|
"",
|
|
@@ -96,10 +154,14 @@ function formatMarkdown(memories, query) {
|
|
|
96
154
|
"",
|
|
97
155
|
"## Relevant Memories"
|
|
98
156
|
];
|
|
99
|
-
for (const memory of memories)
|
|
157
|
+
for (const memory of memories) {
|
|
158
|
+
const fence = markdownFence(memory.content);
|
|
159
|
+
lines.push("", `- ${formatMetadata(memory)}`, ` ${fence}text`, ...memory.content.split("\n").map((line) => ` ${line}`), ` ${fence}`);
|
|
160
|
+
}
|
|
161
|
+
if (omittedCount > 0) lines.push("", `[${omittedCount} memories omitted to fit token budget]`);
|
|
100
162
|
return lines.join("\n");
|
|
101
163
|
}
|
|
102
|
-
function formatPlain(memories, query) {
|
|
164
|
+
function formatPlain(memories, query, omittedCount) {
|
|
103
165
|
const lines = [
|
|
104
166
|
"MEMORY CONTEXT",
|
|
105
167
|
"",
|
|
@@ -107,20 +169,22 @@ function formatPlain(memories, query) {
|
|
|
107
169
|
"",
|
|
108
170
|
"RELEVANT MEMORIES"
|
|
109
171
|
];
|
|
110
|
-
for (const memory of memories) lines.push("", memory.content,
|
|
172
|
+
for (const [index, memory] of memories.entries()) lines.push("", `--- MEMORY ${index + 1} START ---`, formatMetadata(memory), memory.content, `--- MEMORY ${index + 1} END ---`);
|
|
173
|
+
if (omittedCount > 0) lines.push("", `[${omittedCount} memories omitted to fit token budget]`);
|
|
111
174
|
return lines.join("\n");
|
|
112
175
|
}
|
|
113
|
-
function formatXml(memories, query) {
|
|
176
|
+
function formatXml(memories, query, omittedCount) {
|
|
114
177
|
const lines = [`<memory_context query="${escapeXml(query)}">`];
|
|
115
178
|
for (const memory of memories) lines.push(` <memory id="${escapeXml(memory.id)}" type="${escapeXml(memory.type)}" importance="${escapeXml(memory.importance)}" score="${memory.score.toFixed(3)}">`, ` <content>${escapeXml(memory.content)}</content>`, ` <metadata>${escapeXml(formatMetadata(memory))}</metadata>`, " </memory>");
|
|
179
|
+
if (omittedCount > 0) lines.push(` <omitted count="${omittedCount}" reason="token_budget" />`);
|
|
116
180
|
lines.push("</memory_context>");
|
|
117
181
|
return lines.join("\n");
|
|
118
182
|
}
|
|
119
|
-
function formatContext(memories, query, format) {
|
|
183
|
+
function formatContext(memories, query, format, omittedCount = 0) {
|
|
120
184
|
switch (format) {
|
|
121
|
-
case "markdown": return formatMarkdown(memories, query);
|
|
122
|
-
case "plain": return formatPlain(memories, query);
|
|
123
|
-
case "xml": return formatXml(memories, query);
|
|
185
|
+
case "markdown": return formatMarkdown(memories, query, omittedCount);
|
|
186
|
+
case "plain": return formatPlain(memories, query, omittedCount);
|
|
187
|
+
case "xml": return formatXml(memories, query, omittedCount);
|
|
124
188
|
}
|
|
125
189
|
}
|
|
126
190
|
//#endregion
|
|
@@ -149,57 +213,13 @@ async function buildContext(options) {
|
|
|
149
213
|
const format = options.input.format ?? "markdown";
|
|
150
214
|
const maxTokens = options.input.maxTokens ?? 1200;
|
|
151
215
|
const tokenCounter = options.input.tokenCounter ?? heuristicTokenCounter;
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
* Error type thrown by every public Mnemocyte API.
|
|
158
|
-
*
|
|
159
|
-
* Use the {@link MnemocyteError.code} property — not the message — to branch
|
|
160
|
-
* on failure modes, or call {@link isMnemocyteError} as a type guard when
|
|
161
|
-
* working with `unknown` errors.
|
|
162
|
-
*
|
|
163
|
-
* @example
|
|
164
|
-
* ```ts
|
|
165
|
-
* try {
|
|
166
|
-
* await client.recall({ entityId: "u1", query: "hi" });
|
|
167
|
-
* } catch (err) {
|
|
168
|
-
* if (isMnemocyteError(err) && err.code === "DB") {
|
|
169
|
-
* // handle database failure
|
|
170
|
-
* }
|
|
171
|
-
* throw err;
|
|
172
|
-
* }
|
|
173
|
-
* ```
|
|
174
|
-
*/
|
|
175
|
-
var MnemocyteError = class extends Error {
|
|
176
|
-
/** Stable, machine-readable failure code. See {@link MnemocyteErrorCode}. */
|
|
177
|
-
code;
|
|
178
|
-
/** Underlying cause, if any (e.g. an original driver or embedder error). */
|
|
179
|
-
cause;
|
|
180
|
-
/**
|
|
181
|
-
* @param message - Human-readable error message.
|
|
182
|
-
* @param code - Stable failure code; see {@link MnemocyteErrorCode}.
|
|
183
|
-
* @param cause - Optional underlying cause (driver error, network error, etc.).
|
|
184
|
-
*/
|
|
185
|
-
constructor(message, code, cause) {
|
|
186
|
-
super(message);
|
|
187
|
-
this.name = "MnemocyteError";
|
|
188
|
-
this.code = code;
|
|
189
|
-
this.cause = cause;
|
|
216
|
+
const memories = await options.recall(options.input);
|
|
217
|
+
for (let count = memories.length; count >= 0; count -= 1) {
|
|
218
|
+
const omittedCount = memories.length - count;
|
|
219
|
+
const context = formatContext(memories.slice(0, count), options.input.query, format, omittedCount);
|
|
220
|
+
if (tokenCounter.count(context) <= maxTokens) return context;
|
|
190
221
|
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Type guard for {@link MnemocyteError}.
|
|
194
|
-
*
|
|
195
|
-
* Prefer this over `instanceof` when working across module boundaries or
|
|
196
|
-
* bundlers where multiple copies of the class may exist.
|
|
197
|
-
*
|
|
198
|
-
* @param error - Value to test (typically a caught `unknown`).
|
|
199
|
-
* @returns `true` if `error` is a {@link MnemocyteError}.
|
|
200
|
-
*/
|
|
201
|
-
function isMnemocyteError(error) {
|
|
202
|
-
return error instanceof MnemocyteError;
|
|
222
|
+
return trimToTokenBudget(formatContext([], options.input.query, format, memories.length), maxTokens, tokenCounter);
|
|
203
223
|
}
|
|
204
224
|
//#endregion
|
|
205
225
|
//#region src/observability.ts
|
|
@@ -442,6 +462,10 @@ function normalizeTags(tags) {
|
|
|
442
462
|
function createId() {
|
|
443
463
|
return `mem_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
444
464
|
}
|
|
465
|
+
/** Generate a unique audit-event id (`evt_*`). */
|
|
466
|
+
function createEventId() {
|
|
467
|
+
return `evt_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
468
|
+
}
|
|
445
469
|
function isExpired(memory, now) {
|
|
446
470
|
return memory.expiresAt !== null && memory.expiresAt.getTime() <= now.getTime();
|
|
447
471
|
}
|
|
@@ -450,6 +474,7 @@ function cloneMemory(memory) {
|
|
|
450
474
|
...memory,
|
|
451
475
|
tags: [...memory.tags],
|
|
452
476
|
metadata: { ...memory.metadata },
|
|
477
|
+
supersededAt: memory.supersededAt ? new Date(memory.supersededAt) : null,
|
|
453
478
|
expiresAt: memory.expiresAt ? new Date(memory.expiresAt) : null,
|
|
454
479
|
lastAccessedAt: memory.lastAccessedAt ? new Date(memory.lastAccessedAt) : null,
|
|
455
480
|
createdAt: new Date(memory.createdAt),
|
|
@@ -470,6 +495,7 @@ function rowToMemory(row) {
|
|
|
470
495
|
embeddingModel: row.embeddingModel,
|
|
471
496
|
embeddingDimensions: row.embeddingDimensions,
|
|
472
497
|
supersededBy: row.supersededBy,
|
|
498
|
+
supersededAt: row.supersededAt,
|
|
473
499
|
expiresAt: row.expiresAt,
|
|
474
500
|
lastAccessedAt: row.lastAccessedAt,
|
|
475
501
|
accessCount: row.accessCount,
|
|
@@ -546,6 +572,49 @@ function matchesPruneFilter(memory, input, now) {
|
|
|
546
572
|
if (input.maxImportance !== void 0 && IMPORTANCE_RANK[memory.importance] > IMPORTANCE_RANK[input.maxImportance]) return false;
|
|
547
573
|
return true;
|
|
548
574
|
}
|
|
575
|
+
/**
|
|
576
|
+
* Validate a {@link FindDuplicatesInput} and throw a `"VALIDATION"`
|
|
577
|
+
* {@link MnemocyteError} for malformed `threshold` / `limit` values.
|
|
578
|
+
*/
|
|
579
|
+
function validateFindDuplicatesInput(input) {
|
|
580
|
+
assertNonEmptyString(input.entityId, "entityId");
|
|
581
|
+
if (input.threshold !== void 0) {
|
|
582
|
+
if (!Number.isFinite(input.threshold) || input.threshold < 0 || input.threshold > 1) throw new MnemocyteError("threshold must be a number between 0 and 1.", "VALIDATION");
|
|
583
|
+
}
|
|
584
|
+
if (input.limit !== void 0) assertLimit(input.limit);
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Return `true` when a memory should participate in a duplicate scan,
|
|
588
|
+
* given the filters on `input` and a shared `now` timestamp.
|
|
589
|
+
*/
|
|
590
|
+
function matchesDuplicateFilter(memory, input, now) {
|
|
591
|
+
if (memory.entityId !== input.entityId) return false;
|
|
592
|
+
if (input.includeSuperseded !== true && memory.supersededBy !== null) return false;
|
|
593
|
+
if (input.includeExpired !== true && isExpired(memory, now)) return false;
|
|
594
|
+
if (input.types !== void 0 && !input.types.includes(memory.type)) return false;
|
|
595
|
+
if (input.tags !== void 0 && input.tags.length > 0 && !input.tags.every((tag) => memory.tags.includes(tag))) return false;
|
|
596
|
+
return true;
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Validate a {@link ConsolidateInput} and throw a `"VALIDATION"`
|
|
600
|
+
* {@link MnemocyteError} for malformed inputs (empty `supersededIds`
|
|
601
|
+
* or a `survivorId` appearing in `supersededIds`).
|
|
602
|
+
*/
|
|
603
|
+
function validateConsolidateInput(input) {
|
|
604
|
+
assertNonEmptyString(input.entityId, "entityId");
|
|
605
|
+
assertNonEmptyString(input.survivorId, "survivorId");
|
|
606
|
+
if (input.supersededIds.length === 0) throw new MnemocyteError("supersededIds must contain at least one memory id.", "VALIDATION");
|
|
607
|
+
if (input.supersededIds.includes(input.survivorId)) throw new MnemocyteError("survivorId must not appear in supersededIds.", "VALIDATION");
|
|
608
|
+
for (const id of input.supersededIds) assertNonEmptyString(id, "supersededIds[*]");
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Validate a {@link ListAuditLogInput} and throw a `"VALIDATION"`
|
|
612
|
+
* {@link MnemocyteError} when malformed.
|
|
613
|
+
*/
|
|
614
|
+
function validateListAuditLogInput(input) {
|
|
615
|
+
assertNonEmptyString(input.entityId, "entityId");
|
|
616
|
+
if (input.limit !== void 0) assertLimit(input.limit);
|
|
617
|
+
}
|
|
549
618
|
function matchesRecallFilter(memory, input, now) {
|
|
550
619
|
if (memory.entityId !== input.entityId) return false;
|
|
551
620
|
if (!input.includeSuperseded && memory.supersededBy !== null) return false;
|
|
@@ -658,10 +727,30 @@ function toScoredMemory(memory, vector, lexical, input, config) {
|
|
|
658
727
|
//#region src/memory/in-memory.ts
|
|
659
728
|
function createInMemoryClient(config) {
|
|
660
729
|
const memories = /* @__PURE__ */ new Map();
|
|
730
|
+
const auditLog = [];
|
|
661
731
|
let closed = false;
|
|
662
732
|
function assertOpen() {
|
|
663
733
|
if (closed) throw new MnemocyteError("Mnemocyte client is closed.", "DB");
|
|
664
734
|
}
|
|
735
|
+
function recordAudit(entityId, description, metadata) {
|
|
736
|
+
if (config.audit?.enabled !== true) return;
|
|
737
|
+
auditLog.push({
|
|
738
|
+
id: createEventId(),
|
|
739
|
+
entityId,
|
|
740
|
+
description,
|
|
741
|
+
metadata,
|
|
742
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
function cloneAuditEvent(event) {
|
|
746
|
+
return {
|
|
747
|
+
id: event.id,
|
|
748
|
+
entityId: event.entityId,
|
|
749
|
+
description: event.description,
|
|
750
|
+
metadata: { ...event.metadata },
|
|
751
|
+
timestamp: new Date(event.timestamp)
|
|
752
|
+
};
|
|
753
|
+
}
|
|
665
754
|
async function remember(input) {
|
|
666
755
|
return observe(config, "in-memory", "remember", { entityId: input.entityId }, async () => {
|
|
667
756
|
assertOpen();
|
|
@@ -684,6 +773,7 @@ function createInMemoryClient(config) {
|
|
|
684
773
|
embeddingModel: config.embedder.model,
|
|
685
774
|
embeddingDimensions: config.embedder.dimensions,
|
|
686
775
|
supersededBy: null,
|
|
776
|
+
supersededAt: null,
|
|
687
777
|
expiresAt: input.expiresAt ?? null,
|
|
688
778
|
lastAccessedAt: null,
|
|
689
779
|
accessCount: 0,
|
|
@@ -692,6 +782,11 @@ function createInMemoryClient(config) {
|
|
|
692
782
|
embedding
|
|
693
783
|
};
|
|
694
784
|
memories.set(memory.id, memory);
|
|
785
|
+
recordAudit(memory.entityId, "memory.created", {
|
|
786
|
+
memoryId: memory.id,
|
|
787
|
+
type: memory.type,
|
|
788
|
+
importance: memory.importance
|
|
789
|
+
});
|
|
695
790
|
return cloneMemory(memory);
|
|
696
791
|
}, (memory) => ({
|
|
697
792
|
memoryId: memory.id,
|
|
@@ -759,6 +854,7 @@ function createInMemoryClient(config) {
|
|
|
759
854
|
const memory = memories.get(input.memoryId);
|
|
760
855
|
if (!memory || memory.entityId !== input.entityId) throw new MnemocyteError("Memory was not found.", "NOT_FOUND");
|
|
761
856
|
memories.delete(input.memoryId);
|
|
857
|
+
recordAudit(memory.entityId, "memory.deleted", { memoryId: memory.id });
|
|
762
858
|
});
|
|
763
859
|
},
|
|
764
860
|
async forgetAll(input) {
|
|
@@ -770,6 +866,7 @@ function createInMemoryClient(config) {
|
|
|
770
866
|
memories.delete(id);
|
|
771
867
|
deleted += 1;
|
|
772
868
|
}
|
|
869
|
+
recordAudit(input.entityId, "entity.cleared", { count: deleted });
|
|
773
870
|
return deleted;
|
|
774
871
|
}, (count) => ({ count })).then(() => void 0);
|
|
775
872
|
},
|
|
@@ -781,7 +878,10 @@ function createInMemoryClient(config) {
|
|
|
781
878
|
const matched = [];
|
|
782
879
|
for (const memory of memories.values()) if (matchesPruneFilter(memory, input, now)) matched.push(memory.id);
|
|
783
880
|
const dryRun = input.dryRun === true;
|
|
784
|
-
if (!dryRun)
|
|
881
|
+
if (!dryRun) {
|
|
882
|
+
for (const id of matched) memories.delete(id);
|
|
883
|
+
if (matched.length > 0 && input.entityId !== void 0) recordAudit(input.entityId, "memory.pruned", { count: matched.length });
|
|
884
|
+
}
|
|
785
885
|
return {
|
|
786
886
|
matchedCount: matched.length,
|
|
787
887
|
deletedCount: dryRun ? 0 : matched.length,
|
|
@@ -789,6 +889,53 @@ function createInMemoryClient(config) {
|
|
|
789
889
|
};
|
|
790
890
|
}, (result) => ({ count: result.deletedCount }));
|
|
791
891
|
},
|
|
892
|
+
async findDuplicates(input) {
|
|
893
|
+
return observe(config, "in-memory", "findDuplicates", { entityId: input.entityId }, async () => {
|
|
894
|
+
assertOpen();
|
|
895
|
+
validateFindDuplicatesInput(input);
|
|
896
|
+
const threshold = input.threshold ?? .95;
|
|
897
|
+
const limit = input.limit ?? 100;
|
|
898
|
+
const now = /* @__PURE__ */ new Date();
|
|
899
|
+
const candidates = Array.from(memories.values()).filter((memory) => matchesDuplicateFilter(memory, input, now));
|
|
900
|
+
const pairs = [];
|
|
901
|
+
for (let i = 0; i < candidates.length; i += 1) {
|
|
902
|
+
const a = candidates[i];
|
|
903
|
+
if (a === void 0) continue;
|
|
904
|
+
for (let j = i + 1; j < candidates.length; j += 1) {
|
|
905
|
+
const b = candidates[j];
|
|
906
|
+
if (b === void 0) continue;
|
|
907
|
+
const similarity = Math.max(0, Math.min(1, cosineSimilarity(a.embedding, b.embedding)));
|
|
908
|
+
if (similarity >= threshold) pairs.push({
|
|
909
|
+
a: cloneMemory(a),
|
|
910
|
+
b: cloneMemory(b),
|
|
911
|
+
similarity
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
pairs.sort((x, y) => y.similarity - x.similarity);
|
|
916
|
+
return pairs.slice(0, limit);
|
|
917
|
+
}, (result) => ({ count: result.length }));
|
|
918
|
+
},
|
|
919
|
+
async listAuditLog(input) {
|
|
920
|
+
return observe(config, "in-memory", "listAuditLog", { entityId: input.entityId }, async () => {
|
|
921
|
+
assertOpen();
|
|
922
|
+
validateListAuditLogInput(input);
|
|
923
|
+
const limit = input.limit ?? 50;
|
|
924
|
+
return auditLog.map((event, idx) => ({
|
|
925
|
+
event,
|
|
926
|
+
idx
|
|
927
|
+
})).filter(({ event }) => {
|
|
928
|
+
if (event.entityId !== input.entityId) return false;
|
|
929
|
+
if (input.before !== void 0 && event.timestamp.getTime() >= input.before.getTime()) return false;
|
|
930
|
+
if (input.after !== void 0 && event.timestamp.getTime() <= input.after.getTime()) return false;
|
|
931
|
+
return true;
|
|
932
|
+
}).sort((a, b) => {
|
|
933
|
+
const dt = b.event.timestamp.getTime() - a.event.timestamp.getTime();
|
|
934
|
+
if (dt !== 0) return dt;
|
|
935
|
+
return b.idx - a.idx;
|
|
936
|
+
}).slice(0, limit).map(({ event }) => cloneAuditEvent(event));
|
|
937
|
+
}, (result) => ({ count: result.length }));
|
|
938
|
+
},
|
|
792
939
|
async stats(input) {
|
|
793
940
|
return observe(config, "in-memory", "stats", input?.entityId ? { entityId: input.entityId } : {}, async () => {
|
|
794
941
|
assertOpen();
|
|
@@ -814,6 +961,7 @@ function createInMemoryClient(config) {
|
|
|
814
961
|
};
|
|
815
962
|
});
|
|
816
963
|
},
|
|
964
|
+
experimental: createExperimental(),
|
|
817
965
|
async close() {
|
|
818
966
|
return observe(config, "in-memory", "close", {}, async () => {
|
|
819
967
|
closed = true;
|
|
@@ -821,25 +969,83 @@ function createInMemoryClient(config) {
|
|
|
821
969
|
});
|
|
822
970
|
}
|
|
823
971
|
};
|
|
972
|
+
function createExperimental() {
|
|
973
|
+
return { async consolidate(input) {
|
|
974
|
+
return observe(config, "in-memory", "consolidate", {
|
|
975
|
+
entityId: input.entityId,
|
|
976
|
+
memoryId: input.survivorId
|
|
977
|
+
}, async () => {
|
|
978
|
+
assertOpen();
|
|
979
|
+
validateConsolidateInput(input);
|
|
980
|
+
const survivor = memories.get(input.survivorId);
|
|
981
|
+
if (!survivor || survivor.entityId !== input.entityId) throw new MnemocyteError("Survivor memory was not found.", "NOT_FOUND");
|
|
982
|
+
if (survivor.supersededBy !== null) throw new MnemocyteError("Survivor memory is itself superseded.", "VALIDATION");
|
|
983
|
+
const losers = [];
|
|
984
|
+
for (const id of input.supersededIds) {
|
|
985
|
+
const memory = memories.get(id);
|
|
986
|
+
if (!memory || memory.entityId !== input.entityId) throw new MnemocyteError("Superseded memory was not found.", "NOT_FOUND");
|
|
987
|
+
if (memory.supersededBy !== null) continue;
|
|
988
|
+
losers.push(memory);
|
|
989
|
+
}
|
|
990
|
+
const now = /* @__PURE__ */ new Date();
|
|
991
|
+
const newSupersededIds = [];
|
|
992
|
+
for (const loser of losers) {
|
|
993
|
+
loser.supersededBy = survivor.id;
|
|
994
|
+
loser.supersededAt = now;
|
|
995
|
+
loser.updatedAt = now;
|
|
996
|
+
newSupersededIds.push(loser.id);
|
|
997
|
+
recordAudit(input.entityId, "memory.superseded", {
|
|
998
|
+
memoryId: loser.id,
|
|
999
|
+
supersededBy: survivor.id
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
if (input.mergeTags !== false && losers.length > 0) {
|
|
1003
|
+
const mergedTags = new Set(survivor.tags);
|
|
1004
|
+
for (const loser of losers) for (const tag of loser.tags) mergedTags.add(tag);
|
|
1005
|
+
if (mergedTags.size !== survivor.tags.length) {
|
|
1006
|
+
survivor.tags = [...mergedTags];
|
|
1007
|
+
survivor.updatedAt = now;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
return {
|
|
1011
|
+
survivorId: survivor.id,
|
|
1012
|
+
supersededCount: newSupersededIds.length,
|
|
1013
|
+
supersededIds: newSupersededIds
|
|
1014
|
+
};
|
|
1015
|
+
}, (result) => ({ count: result.supersededCount }));
|
|
1016
|
+
} };
|
|
1017
|
+
}
|
|
824
1018
|
}
|
|
825
1019
|
//#endregion
|
|
826
1020
|
//#region src/db/queries/events.ts
|
|
827
|
-
async function
|
|
828
|
-
|
|
1021
|
+
async function insertEvent(db, row) {
|
|
1022
|
+
const inserted = (await db.insert(eventsTable).values(row).returning())[0];
|
|
1023
|
+
if (!inserted) throw new Error("Event insert returned no rows.");
|
|
1024
|
+
return inserted;
|
|
1025
|
+
}
|
|
1026
|
+
async function listEvents(db, filter) {
|
|
1027
|
+
return db.select().from(eventsTable).where(and(eq(eventsTable.entityId, filter.entityId), filter.before ? lt(eventsTable.timestamp, filter.before) : void 0, filter.after ? gt(eventsTable.timestamp, filter.after) : void 0)).orderBy(desc(eventsTable.timestamp)).limit(filter.limit ?? 50);
|
|
829
1028
|
}
|
|
830
1029
|
//#endregion
|
|
831
1030
|
//#region src/db/queries/memories.ts
|
|
832
1031
|
function vectorLiteral(embedding) {
|
|
833
|
-
return `[${embedding.join(",")}]`;
|
|
1032
|
+
return `[${embedding.map(formatVectorComponent).join(",")}]`;
|
|
1033
|
+
}
|
|
1034
|
+
function formatVectorComponent(value) {
|
|
1035
|
+
if (!Number.isFinite(value)) throw new Error("Vector values must be finite numbers.");
|
|
1036
|
+
return Object.is(value, -0) ? "0" : value.toFixed(17);
|
|
834
1037
|
}
|
|
835
1038
|
function filterConditions(filter) {
|
|
836
|
-
return and(eq(memoriesTable.entityId, filter.entityId), filter.includeSuperseded ? void 0 : isNull(memoriesTable.supersededBy), filter.includeExpired ? void 0 : sql`(${memoriesTable.expiresAt} IS NULL OR ${memoriesTable.expiresAt} > now())`, filter.types && filter.types.length > 0 ? inArray(memoriesTable.type, [...filter.types]) : void 0, filter.tags && filter.tags.length > 0 ? sql`${memoriesTable.tags}
|
|
1039
|
+
return and(eq(memoriesTable.entityId, filter.entityId), filter.includeSuperseded ? void 0 : isNull(memoriesTable.supersededBy), filter.includeExpired ? void 0 : sql`(${memoriesTable.expiresAt} IS NULL OR ${memoriesTable.expiresAt} > now())`, filter.types && filter.types.length > 0 ? inArray(memoriesTable.type, [...filter.types]) : void 0, filter.tags && filter.tags.length > 0 ? sql`${memoriesTable.tags} @> ${filter.tags}` : void 0, filter.before ? lt(memoriesTable.createdAt, filter.before) : void 0, filter.after ? gt(memoriesTable.createdAt, filter.after) : void 0);
|
|
837
1040
|
}
|
|
838
1041
|
async function insertMemory(db, row) {
|
|
839
1042
|
const inserted = (await db.insert(memoriesTable).values(row).returning())[0];
|
|
840
1043
|
if (!inserted) throw new Error("Memory insert returned no rows.");
|
|
841
1044
|
return inserted;
|
|
842
1045
|
}
|
|
1046
|
+
async function getMemoryById(db, entityId, memoryId) {
|
|
1047
|
+
return (await db.select().from(memoriesTable).where(and(eq(memoriesTable.entityId, entityId), eq(memoriesTable.id, memoryId))).limit(1))[0] ?? null;
|
|
1048
|
+
}
|
|
843
1049
|
async function listMemories(db, filter) {
|
|
844
1050
|
return db.select().from(memoriesTable).where(filterConditions(filter));
|
|
845
1051
|
}
|
|
@@ -858,6 +1064,136 @@ async function countPruneMatches(db, filter) {
|
|
|
858
1064
|
async function pruneMemories(db, filter) {
|
|
859
1065
|
return (await db.delete(memoriesTable).where(pruneConditions(filter)).returning({ id: memoriesTable.id })).length;
|
|
860
1066
|
}
|
|
1067
|
+
async function findDuplicatePairs(db, input) {
|
|
1068
|
+
return (await db.execute(sql`
|
|
1069
|
+
SELECT
|
|
1070
|
+
a.id AS "aId",
|
|
1071
|
+
a.entity_id AS "aEntityId",
|
|
1072
|
+
a.content AS "aContent",
|
|
1073
|
+
a.type AS "aType",
|
|
1074
|
+
a.importance AS "aImportance",
|
|
1075
|
+
a.tags AS "aTags",
|
|
1076
|
+
a.source AS "aSource",
|
|
1077
|
+
a.metadata AS "aMetadata",
|
|
1078
|
+
a.confidence AS "aConfidence",
|
|
1079
|
+
a.embedding AS "aEmbedding",
|
|
1080
|
+
a.embedding_model AS "aEmbeddingModel",
|
|
1081
|
+
a.embedding_dimensions AS "aEmbeddingDimensions",
|
|
1082
|
+
a.superseded_by AS "aSupersededBy",
|
|
1083
|
+
a.superseded_at AS "aSupersededAt",
|
|
1084
|
+
a.expires_at AS "aExpiresAt",
|
|
1085
|
+
a.last_accessed_at AS "aLastAccessedAt",
|
|
1086
|
+
a.access_count AS "aAccessCount",
|
|
1087
|
+
a.created_at AS "aCreatedAt",
|
|
1088
|
+
a.updated_at AS "aUpdatedAt",
|
|
1089
|
+
b.id AS "bId",
|
|
1090
|
+
b.entity_id AS "bEntityId",
|
|
1091
|
+
b.content AS "bContent",
|
|
1092
|
+
b.type AS "bType",
|
|
1093
|
+
b.importance AS "bImportance",
|
|
1094
|
+
b.tags AS "bTags",
|
|
1095
|
+
b.source AS "bSource",
|
|
1096
|
+
b.metadata AS "bMetadata",
|
|
1097
|
+
b.confidence AS "bConfidence",
|
|
1098
|
+
b.embedding AS "bEmbedding",
|
|
1099
|
+
b.embedding_model AS "bEmbeddingModel",
|
|
1100
|
+
b.embedding_dimensions AS "bEmbeddingDimensions",
|
|
1101
|
+
b.superseded_by AS "bSupersededBy",
|
|
1102
|
+
b.superseded_at AS "bSupersededAt",
|
|
1103
|
+
b.expires_at AS "bExpiresAt",
|
|
1104
|
+
b.last_accessed_at AS "bLastAccessedAt",
|
|
1105
|
+
b.access_count AS "bAccessCount",
|
|
1106
|
+
b.created_at AS "bCreatedAt",
|
|
1107
|
+
b.updated_at AS "bUpdatedAt",
|
|
1108
|
+
1 - (a.embedding <=> b.embedding) AS "similarity"
|
|
1109
|
+
FROM mnemocyte_memories a
|
|
1110
|
+
JOIN mnemocyte_memories b
|
|
1111
|
+
ON b.entity_id = a.entity_id
|
|
1112
|
+
AND a.id < b.id
|
|
1113
|
+
AND a.embedding IS NOT NULL
|
|
1114
|
+
AND b.embedding IS NOT NULL
|
|
1115
|
+
WHERE
|
|
1116
|
+
a.entity_id = ${input.entityId}
|
|
1117
|
+
${input.includeSuperseded ? sql`` : sql`AND a.superseded_by IS NULL AND b.superseded_by IS NULL`}
|
|
1118
|
+
${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())`}
|
|
1119
|
+
${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``}
|
|
1120
|
+
${input.tags && input.tags.length > 0 ? sql`AND a.tags @> ${input.tags} AND b.tags @> ${input.tags}` : sql``}
|
|
1121
|
+
AND 1 - (a.embedding <=> b.embedding) >= ${input.threshold}
|
|
1122
|
+
ORDER BY "similarity" DESC
|
|
1123
|
+
LIMIT ${input.limit}
|
|
1124
|
+
`)).map((row) => {
|
|
1125
|
+
return {
|
|
1126
|
+
a: {
|
|
1127
|
+
id: row.aId,
|
|
1128
|
+
entityId: row.aEntityId,
|
|
1129
|
+
content: row.aContent,
|
|
1130
|
+
type: row.aType,
|
|
1131
|
+
importance: row.aImportance,
|
|
1132
|
+
tags: row.aTags,
|
|
1133
|
+
source: row.aSource,
|
|
1134
|
+
metadata: row.aMetadata,
|
|
1135
|
+
confidence: row.aConfidence,
|
|
1136
|
+
embedding: row.aEmbedding,
|
|
1137
|
+
embeddingModel: row.aEmbeddingModel,
|
|
1138
|
+
embeddingDimensions: row.aEmbeddingDimensions,
|
|
1139
|
+
supersededBy: row.aSupersededBy,
|
|
1140
|
+
supersededAt: row.aSupersededAt,
|
|
1141
|
+
expiresAt: row.aExpiresAt,
|
|
1142
|
+
lastAccessedAt: row.aLastAccessedAt,
|
|
1143
|
+
accessCount: row.aAccessCount,
|
|
1144
|
+
createdAt: row.aCreatedAt,
|
|
1145
|
+
updatedAt: row.aUpdatedAt
|
|
1146
|
+
},
|
|
1147
|
+
b: {
|
|
1148
|
+
id: row.bId,
|
|
1149
|
+
entityId: row.bEntityId,
|
|
1150
|
+
content: row.bContent,
|
|
1151
|
+
type: row.bType,
|
|
1152
|
+
importance: row.bImportance,
|
|
1153
|
+
tags: row.bTags,
|
|
1154
|
+
source: row.bSource,
|
|
1155
|
+
metadata: row.bMetadata,
|
|
1156
|
+
confidence: row.bConfidence,
|
|
1157
|
+
embedding: row.bEmbedding,
|
|
1158
|
+
embeddingModel: row.bEmbeddingModel,
|
|
1159
|
+
embeddingDimensions: row.bEmbeddingDimensions,
|
|
1160
|
+
supersededBy: row.bSupersededBy,
|
|
1161
|
+
supersededAt: row.bSupersededAt,
|
|
1162
|
+
expiresAt: row.bExpiresAt,
|
|
1163
|
+
lastAccessedAt: row.bLastAccessedAt,
|
|
1164
|
+
accessCount: row.bAccessCount,
|
|
1165
|
+
createdAt: row.bCreatedAt,
|
|
1166
|
+
updatedAt: row.bUpdatedAt
|
|
1167
|
+
},
|
|
1168
|
+
similarity: Number(row.similarity) || 0
|
|
1169
|
+
};
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
async function loadConsolidationTargets(db, entityId, ids) {
|
|
1173
|
+
if (ids.length === 0) return [];
|
|
1174
|
+
return await db.select({
|
|
1175
|
+
id: memoriesTable.id,
|
|
1176
|
+
tags: memoriesTable.tags,
|
|
1177
|
+
supersededBy: memoriesTable.supersededBy
|
|
1178
|
+
}).from(memoriesTable).where(and(eq(memoriesTable.entityId, entityId), inArray(memoriesTable.id, [...ids])));
|
|
1179
|
+
}
|
|
1180
|
+
async function markMemoriesSuperseded(db, params) {
|
|
1181
|
+
if (params.ids.length === 0) return [];
|
|
1182
|
+
return await db.update(memoriesTable).set({
|
|
1183
|
+
supersededBy: params.survivorId,
|
|
1184
|
+
supersededAt: params.now,
|
|
1185
|
+
updatedAt: params.now
|
|
1186
|
+
}).where(and(eq(memoriesTable.entityId, params.entityId), inArray(memoriesTable.id, [...params.ids]), isNull(memoriesTable.supersededBy))).returning({
|
|
1187
|
+
id: memoriesTable.id,
|
|
1188
|
+
tags: memoriesTable.tags
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
async function setMemoryTags(db, params) {
|
|
1192
|
+
await db.update(memoriesTable).set({
|
|
1193
|
+
tags: [...params.tags],
|
|
1194
|
+
updatedAt: params.now
|
|
1195
|
+
}).where(and(eq(memoriesTable.entityId, params.entityId), eq(memoriesTable.id, params.memoryId)));
|
|
1196
|
+
}
|
|
861
1197
|
async function markMemoryAccessed(db, memoryIds) {
|
|
862
1198
|
if (memoryIds.length === 0) return;
|
|
863
1199
|
await db.update(memoriesTable).set({
|
|
@@ -898,7 +1234,7 @@ async function vectorSearch(db, input) {
|
|
|
898
1234
|
${input.includeSuperseded ? sql`` : sql`AND superseded_by IS NULL`}
|
|
899
1235
|
${input.includeExpired ? sql`` : sql`AND (expires_at IS NULL OR expires_at > now())`}
|
|
900
1236
|
${input.types && input.types.length > 0 ? sql`AND type IN (${sql.join(input.types.map((type) => sql`${type}`), sql`, `)})` : sql``}
|
|
901
|
-
${input.tags && input.tags.length > 0 ? sql`AND tags
|
|
1237
|
+
${input.tags && input.tags.length > 0 ? sql`AND tags @> ${input.tags}` : sql``}
|
|
902
1238
|
${input.before ? sql`AND created_at < ${input.before}` : sql``}
|
|
903
1239
|
${input.after ? sql`AND created_at > ${input.after}` : sql``}
|
|
904
1240
|
AND 1 - (embedding <=> ${embedding}::vector) >= ${minScore}
|
|
@@ -935,7 +1271,7 @@ async function lexicalSearch(db, input) {
|
|
|
935
1271
|
${input.includeSuperseded ? sql`` : sql`AND superseded_by IS NULL`}
|
|
936
1272
|
${input.includeExpired ? sql`` : sql`AND (expires_at IS NULL OR expires_at > now())`}
|
|
937
1273
|
${input.types && input.types.length > 0 ? sql`AND type IN (${sql.join(input.types.map((type) => sql`${type}`), sql`, `)})` : sql``}
|
|
938
|
-
${input.tags && input.tags.length > 0 ? sql`AND tags
|
|
1274
|
+
${input.tags && input.tags.length > 0 ? sql`AND tags @> ${input.tags}` : sql``}
|
|
939
1275
|
${input.before ? sql`AND created_at < ${input.before}` : sql``}
|
|
940
1276
|
${input.after ? sql`AND created_at > ${input.after}` : sql``}
|
|
941
1277
|
AND to_tsvector('english', content) @@ websearch_to_tsquery('english', ${input.query})
|
|
@@ -982,6 +1318,15 @@ async function hybridRecall(input) {
|
|
|
982
1318
|
}
|
|
983
1319
|
//#endregion
|
|
984
1320
|
//#region src/memory/postgres.ts
|
|
1321
|
+
function rowToAuditEvent(row) {
|
|
1322
|
+
return {
|
|
1323
|
+
id: row.id,
|
|
1324
|
+
entityId: row.entityId,
|
|
1325
|
+
description: row.description,
|
|
1326
|
+
metadata: row.metadata,
|
|
1327
|
+
timestamp: row.timestamp
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
985
1330
|
const IMPORTANCE_LEVELS = [
|
|
986
1331
|
"low",
|
|
987
1332
|
"normal",
|
|
@@ -1008,6 +1353,18 @@ function createPostgresClient(config, handle) {
|
|
|
1008
1353
|
function assertOpen() {
|
|
1009
1354
|
if (closed) throw new MnemocyteError("Mnemocyte client is closed.", "DB");
|
|
1010
1355
|
}
|
|
1356
|
+
async function recordAudit(entityId, description, metadata) {
|
|
1357
|
+
if (config.audit?.enabled !== true) return;
|
|
1358
|
+
try {
|
|
1359
|
+
await insertEvent(handle.db, {
|
|
1360
|
+
id: createEventId(),
|
|
1361
|
+
entityId,
|
|
1362
|
+
description,
|
|
1363
|
+
metadata,
|
|
1364
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
1365
|
+
});
|
|
1366
|
+
} catch {}
|
|
1367
|
+
}
|
|
1011
1368
|
async function remember(input) {
|
|
1012
1369
|
return observe(config, "postgres", "remember", { entityId: input.entityId }, async () => {
|
|
1013
1370
|
assertOpen();
|
|
@@ -1017,7 +1374,7 @@ function createPostgresClient(config, handle) {
|
|
|
1017
1374
|
...config.provider === void 0 ? {} : { resilience: config.provider }
|
|
1018
1375
|
});
|
|
1019
1376
|
const now = /* @__PURE__ */ new Date();
|
|
1020
|
-
|
|
1377
|
+
const memory = rowToMemory(await insertMemory(handle.db, {
|
|
1021
1378
|
id: createId(),
|
|
1022
1379
|
entityId: input.entityId,
|
|
1023
1380
|
content: input.content,
|
|
@@ -1038,6 +1395,12 @@ function createPostgresClient(config, handle) {
|
|
|
1038
1395
|
createdAt: now,
|
|
1039
1396
|
updatedAt: now
|
|
1040
1397
|
}));
|
|
1398
|
+
await recordAudit(memory.entityId, "memory.created", {
|
|
1399
|
+
memoryId: memory.id,
|
|
1400
|
+
type: memory.type,
|
|
1401
|
+
importance: memory.importance
|
|
1402
|
+
});
|
|
1403
|
+
return memory;
|
|
1041
1404
|
}, (memory) => ({
|
|
1042
1405
|
memoryId: memory.id,
|
|
1043
1406
|
count: 1
|
|
@@ -1097,14 +1460,17 @@ function createPostgresClient(config, handle) {
|
|
|
1097
1460
|
assertNonEmptyString(input.entityId, "entityId");
|
|
1098
1461
|
assertNonEmptyString(input.memoryId, "memoryId");
|
|
1099
1462
|
if (!await deleteMemory(handle.db, input.entityId, input.memoryId)) throw new MnemocyteError("Memory was not found.", "NOT_FOUND");
|
|
1463
|
+
await recordAudit(input.entityId, "memory.deleted", { memoryId: input.memoryId });
|
|
1100
1464
|
});
|
|
1101
1465
|
},
|
|
1102
1466
|
async forgetAll(input) {
|
|
1103
1467
|
return observe(config, "postgres", "forgetAll", { entityId: input.entityId }, async () => {
|
|
1104
1468
|
assertOpen();
|
|
1105
1469
|
assertNonEmptyString(input.entityId, "entityId");
|
|
1106
|
-
await
|
|
1107
|
-
|
|
1470
|
+
const deletedCount = await deleteMemoriesForEntity(handle.db, input.entityId);
|
|
1471
|
+
await recordAudit(input.entityId, "entity.cleared", { count: deletedCount });
|
|
1472
|
+
return deletedCount;
|
|
1473
|
+
}, (count) => ({ count })).then(() => void 0);
|
|
1108
1474
|
},
|
|
1109
1475
|
async prune(input) {
|
|
1110
1476
|
return observe(config, "postgres", "prune", input.entityId === void 0 ? {} : { entityId: input.entityId }, async () => {
|
|
@@ -1117,6 +1483,7 @@ function createPostgresClient(config, handle) {
|
|
|
1117
1483
|
dryRun: true
|
|
1118
1484
|
};
|
|
1119
1485
|
const deletedCount = await pruneMemories(handle.db, filter);
|
|
1486
|
+
if (deletedCount > 0 && input.entityId !== void 0) await recordAudit(input.entityId, "memory.pruned", { count: deletedCount });
|
|
1120
1487
|
return {
|
|
1121
1488
|
matchedCount: deletedCount,
|
|
1122
1489
|
deletedCount,
|
|
@@ -1124,6 +1491,39 @@ function createPostgresClient(config, handle) {
|
|
|
1124
1491
|
};
|
|
1125
1492
|
}, (result) => ({ count: result.deletedCount }));
|
|
1126
1493
|
},
|
|
1494
|
+
async findDuplicates(input) {
|
|
1495
|
+
return observe(config, "postgres", "findDuplicates", { entityId: input.entityId }, async () => {
|
|
1496
|
+
assertOpen();
|
|
1497
|
+
validateFindDuplicatesInput(input);
|
|
1498
|
+
const threshold = input.threshold ?? .95;
|
|
1499
|
+
const limit = input.limit ?? 100;
|
|
1500
|
+
return (await findDuplicatePairs(handle.db, {
|
|
1501
|
+
entityId: input.entityId,
|
|
1502
|
+
threshold,
|
|
1503
|
+
limit,
|
|
1504
|
+
...input.types === void 0 ? {} : { types: input.types },
|
|
1505
|
+
...input.tags === void 0 ? {} : { tags: input.tags },
|
|
1506
|
+
...input.includeSuperseded === void 0 ? {} : { includeSuperseded: input.includeSuperseded },
|
|
1507
|
+
...input.includeExpired === void 0 ? {} : { includeExpired: input.includeExpired }
|
|
1508
|
+
})).map((row) => ({
|
|
1509
|
+
a: rowToMemory(row.a),
|
|
1510
|
+
b: rowToMemory(row.b),
|
|
1511
|
+
similarity: Math.max(0, Math.min(1, row.similarity))
|
|
1512
|
+
}));
|
|
1513
|
+
}, (result) => ({ count: result.length }));
|
|
1514
|
+
},
|
|
1515
|
+
async listAuditLog(input) {
|
|
1516
|
+
return observe(config, "postgres", "listAuditLog", { entityId: input.entityId }, async () => {
|
|
1517
|
+
assertOpen();
|
|
1518
|
+
validateListAuditLogInput(input);
|
|
1519
|
+
return (await listEvents(handle.db, {
|
|
1520
|
+
entityId: input.entityId,
|
|
1521
|
+
limit: input.limit ?? 50,
|
|
1522
|
+
...input.before === void 0 ? {} : { before: input.before },
|
|
1523
|
+
...input.after === void 0 ? {} : { after: input.after }
|
|
1524
|
+
})).map(rowToAuditEvent);
|
|
1525
|
+
}, (result) => ({ count: result.length }));
|
|
1526
|
+
},
|
|
1127
1527
|
async stats(input) {
|
|
1128
1528
|
return observe(config, "postgres", "stats", input?.entityId ? { entityId: input.entityId } : {}, async () => {
|
|
1129
1529
|
assertOpen();
|
|
@@ -1152,6 +1552,7 @@ function createPostgresClient(config, handle) {
|
|
|
1152
1552
|
};
|
|
1153
1553
|
});
|
|
1154
1554
|
},
|
|
1555
|
+
experimental: createExperimental(),
|
|
1155
1556
|
async close() {
|
|
1156
1557
|
return observe(config, "postgres", "close", {}, async () => {
|
|
1157
1558
|
closed = true;
|
|
@@ -1159,9 +1560,61 @@ function createPostgresClient(config, handle) {
|
|
|
1159
1560
|
});
|
|
1160
1561
|
}
|
|
1161
1562
|
};
|
|
1563
|
+
function createExperimental() {
|
|
1564
|
+
return { async consolidate(input) {
|
|
1565
|
+
return observe(config, "postgres", "consolidate", {
|
|
1566
|
+
entityId: input.entityId,
|
|
1567
|
+
memoryId: input.survivorId
|
|
1568
|
+
}, async () => {
|
|
1569
|
+
assertOpen();
|
|
1570
|
+
validateConsolidateInput(input);
|
|
1571
|
+
const survivor = await getMemoryById(handle.db, input.entityId, input.survivorId);
|
|
1572
|
+
if (!survivor) throw new MnemocyteError("Survivor memory was not found.", "NOT_FOUND");
|
|
1573
|
+
if (survivor.supersededBy !== null) throw new MnemocyteError("Survivor memory is itself superseded.", "VALIDATION");
|
|
1574
|
+
const targets = await loadConsolidationTargets(handle.db, input.entityId, input.supersededIds);
|
|
1575
|
+
const foundIds = new Set(targets.map((t) => t.id));
|
|
1576
|
+
for (const id of input.supersededIds) if (!foundIds.has(id)) throw new MnemocyteError("Superseded memory was not found.", "NOT_FOUND");
|
|
1577
|
+
const losers = targets.filter((target) => target.supersededBy === null);
|
|
1578
|
+
if (losers.length === 0) return {
|
|
1579
|
+
survivorId: survivor.id,
|
|
1580
|
+
supersededCount: 0,
|
|
1581
|
+
supersededIds: []
|
|
1582
|
+
};
|
|
1583
|
+
const now = /* @__PURE__ */ new Date();
|
|
1584
|
+
const updated = await markMemoriesSuperseded(handle.db, {
|
|
1585
|
+
survivorId: survivor.id,
|
|
1586
|
+
entityId: input.entityId,
|
|
1587
|
+
ids: losers.map((loser) => loser.id),
|
|
1588
|
+
now
|
|
1589
|
+
});
|
|
1590
|
+
const newSupersededIds = updated.map((row) => row.id);
|
|
1591
|
+
for (const id of newSupersededIds) await recordAudit(input.entityId, "memory.superseded", {
|
|
1592
|
+
memoryId: id,
|
|
1593
|
+
supersededBy: survivor.id
|
|
1594
|
+
});
|
|
1595
|
+
if (input.mergeTags !== false && updated.length > 0) {
|
|
1596
|
+
const mergedTags = new Set(survivor.tags);
|
|
1597
|
+
for (const row of updated) for (const tag of row.tags) mergedTags.add(tag);
|
|
1598
|
+
if (mergedTags.size !== survivor.tags.length) await setMemoryTags(handle.db, {
|
|
1599
|
+
entityId: input.entityId,
|
|
1600
|
+
memoryId: survivor.id,
|
|
1601
|
+
tags: [...mergedTags],
|
|
1602
|
+
now
|
|
1603
|
+
});
|
|
1604
|
+
}
|
|
1605
|
+
return {
|
|
1606
|
+
survivorId: survivor.id,
|
|
1607
|
+
supersededCount: newSupersededIds.length,
|
|
1608
|
+
supersededIds: newSupersededIds
|
|
1609
|
+
};
|
|
1610
|
+
}, (result) => ({ count: result.supersededCount }));
|
|
1611
|
+
} };
|
|
1612
|
+
}
|
|
1162
1613
|
}
|
|
1163
1614
|
//#endregion
|
|
1164
1615
|
//#region src/client.ts
|
|
1616
|
+
/** Embedding dimensionality pinned by `migrations/0000_initial.sql`. */
|
|
1617
|
+
const POSTGRES_EMBEDDING_DIMENSIONS = 1536;
|
|
1165
1618
|
/**
|
|
1166
1619
|
* Create a Mnemocyte client.
|
|
1167
1620
|
*
|
|
@@ -1176,14 +1629,16 @@ function createPostgresClient(config, handle) {
|
|
|
1176
1629
|
*
|
|
1177
1630
|
* @param config - Client configuration. {@link MnemocyteConfig.embedder} is required.
|
|
1178
1631
|
* @returns A {@link MnemocyteClient} instance.
|
|
1179
|
-
* @throws {MnemocyteError} With code `"CONFIG"` if `embedder` is missing or
|
|
1180
|
-
*
|
|
1632
|
+
* @throws {MnemocyteError} With code `"CONFIG"` if `embedder` is missing or
|
|
1633
|
+
* invalid (including when `databaseUrl` is set and `embedder.dimensions`
|
|
1634
|
+
* is not 1536 — the dimensionality pinned by the bundled migration), or
|
|
1635
|
+
* `"VALIDATION"` if `databaseUrl` is provided but empty.
|
|
1181
1636
|
*
|
|
1182
1637
|
* @example Postgres-backed client
|
|
1183
1638
|
* ```ts
|
|
1184
1639
|
* const client = createMnemocyte({
|
|
1185
1640
|
* databaseUrl: process.env.DATABASE_URL,
|
|
1186
|
-
* embedder,
|
|
1641
|
+
* embedder, // embedder.dimensions MUST be 1536 for the Postgres backend
|
|
1187
1642
|
* });
|
|
1188
1643
|
* ```
|
|
1189
1644
|
*/
|
|
@@ -1191,6 +1646,7 @@ function createMnemocyte(config) {
|
|
|
1191
1646
|
assertEmbedder(config.embedder);
|
|
1192
1647
|
if (config.databaseUrl) {
|
|
1193
1648
|
assertNonEmptyString(config.databaseUrl, "databaseUrl");
|
|
1649
|
+
if (config.embedder.dimensions !== POSTGRES_EMBEDDING_DIMENSIONS) throw new MnemocyteError(`embedder.dimensions must be ${POSTGRES_EMBEDDING_DIMENSIONS} for the Postgres backend (got ${config.embedder.dimensions}). The bundled migration pins the vector column to vector(${POSTGRES_EMBEDDING_DIMENSIONS}).`, "CONFIG");
|
|
1194
1650
|
return createPostgresClient(config, createDatabase(config.databaseUrl));
|
|
1195
1651
|
}
|
|
1196
1652
|
return createInMemoryClient(config);
|