mnemocyte 0.3.0 → 0.5.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 +266 -22
- package/dist/embedders/openai.d.mts +1 -1
- package/dist/embedders/openai.d.mts.map +1 -1
- package/dist/embedders/openai.mjs +24 -9
- package/dist/embedders/openai.mjs.map +1 -1
- package/dist/{errors-B_7ZdjEO.mjs → errors-DIPysVWl.mjs} +1 -1
- package/dist/errors-DIPysVWl.mjs.map +1 -0
- package/dist/index.d.mts +16 -12
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +155 -1699
- package/dist/index.mjs.map +1 -1
- package/dist/postgres-BHQ1pj16.mjs +913 -0
- package/dist/postgres-BHQ1pj16.mjs.map +1 -0
- package/dist/postgres-runtime-DxJxns07.mjs +9 -0
- package/dist/postgres-runtime-DxJxns07.mjs.map +1 -0
- package/dist/store-config-BFFTm1sj.mjs +1255 -0
- package/dist/store-config-BFFTm1sj.mjs.map +1 -0
- package/dist/stores/drizzle.d.mts +18 -0
- package/dist/stores/drizzle.d.mts.map +1 -0
- package/dist/stores/drizzle.mjs +20 -0
- package/dist/stores/drizzle.mjs.map +1 -0
- package/dist/{types-gOPLre4V.d.mts → types-XUWYDoXM.d.mts} +136 -34
- package/dist/types-XUWYDoXM.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 +7 -3
- package/dist/errors-B_7ZdjEO.mjs.map +0 -1
- package/dist/types-gOPLre4V.d.mts.map +0 -1
|
@@ -0,0 +1,913 @@
|
|
|
1
|
+
import { t as MnemocyteError } from "./errors-DIPysVWl.mjs";
|
|
2
|
+
import { _ as cloneJsonObject, d as IMPORTANCE_RANK, g as createEventId, l as assertPruneFilterHasSelector, r as memoryHasDependentsError, v as throwIfAborted, y as parsePostgresDatabaseUrl } from "./store-config-BFFTm1sj.mjs";
|
|
3
|
+
import { drizzle } from "drizzle-orm/postgres-js";
|
|
4
|
+
import postgres from "postgres";
|
|
5
|
+
import { and, count, countDistinct, eq, inArray, isNotNull, isNull, lt, or, sql } from "drizzle-orm";
|
|
6
|
+
import { PgDialect, customType, index, integer, jsonb, pgTable, real, text, timestamp } from "drizzle-orm/pg-core";
|
|
7
|
+
//#region \0rolldown/runtime.js
|
|
8
|
+
var __defProp = Object.defineProperty;
|
|
9
|
+
var __exportAll = (all, no_symbols) => {
|
|
10
|
+
let target = {};
|
|
11
|
+
for (var name in all) __defProp(target, name, {
|
|
12
|
+
get: all[name],
|
|
13
|
+
enumerable: true
|
|
14
|
+
});
|
|
15
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
16
|
+
return target;
|
|
17
|
+
};
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/db/vector.ts
|
|
20
|
+
function formatVectorComponent(value) {
|
|
21
|
+
if (!Number.isFinite(value)) throw new Error("Vector values must be finite numbers.");
|
|
22
|
+
return Object.is(value, -0) ? "0" : value.toString();
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/db/schema.ts
|
|
26
|
+
var schema_exports = /* @__PURE__ */ __exportAll({
|
|
27
|
+
eventsTable: () => eventsTable,
|
|
28
|
+
memoriesTable: () => memoriesTable,
|
|
29
|
+
metaTable: () => metaTable
|
|
30
|
+
});
|
|
31
|
+
const vector = customType({
|
|
32
|
+
dataType(config) {
|
|
33
|
+
return `vector(${config?.dimensions ?? 1536})`;
|
|
34
|
+
},
|
|
35
|
+
toDriver(value) {
|
|
36
|
+
return `[${value.map(formatVectorComponent).join(",")}]`;
|
|
37
|
+
},
|
|
38
|
+
fromDriver(value) {
|
|
39
|
+
return value.slice(1, -1).split(",").filter(Boolean).map(Number);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
const memoriesTable = pgTable("mnemocyte_memories", {
|
|
43
|
+
id: text("id").primaryKey(),
|
|
44
|
+
entityId: text("entity_id").notNull(),
|
|
45
|
+
content: text("content").notNull(),
|
|
46
|
+
type: text("type").notNull().default("fact"),
|
|
47
|
+
importance: text("importance").notNull().default("normal"),
|
|
48
|
+
tags: text("tags").array().notNull().default(sql`'{}'::text[]`),
|
|
49
|
+
source: text("source"),
|
|
50
|
+
metadata: jsonb("metadata").$type().notNull().default({}),
|
|
51
|
+
confidence: real("confidence").notNull().default(1),
|
|
52
|
+
embedding: vector("embedding", { dimensions: 1536 }),
|
|
53
|
+
embeddingModel: text("embedding_model").notNull(),
|
|
54
|
+
embeddingDimensions: integer("embedding_dimensions").notNull(),
|
|
55
|
+
supersededBy: text("superseded_by").references(() => memoriesTable.id),
|
|
56
|
+
supersededAt: timestamp("superseded_at", { withTimezone: true }),
|
|
57
|
+
expiresAt: timestamp("expires_at", { withTimezone: true }),
|
|
58
|
+
lastAccessedAt: timestamp("last_accessed_at", { withTimezone: true }),
|
|
59
|
+
accessCount: integer("access_count").notNull().default(0),
|
|
60
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
61
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
62
|
+
}, (table) => [
|
|
63
|
+
index("mnemocyte_memories_entity_idx").on(table.entityId),
|
|
64
|
+
index("mnemocyte_memories_entity_type_idx").on(table.entityId, table.type),
|
|
65
|
+
index("mnemocyte_memories_embedding_hnsw_idx").using("hnsw", table.embedding.op("vector_cosine_ops"))
|
|
66
|
+
]);
|
|
67
|
+
const eventsTable = pgTable("mnemocyte_events", {
|
|
68
|
+
id: text("id").primaryKey(),
|
|
69
|
+
entityId: text("entity_id").notNull(),
|
|
70
|
+
description: text("description").notNull(),
|
|
71
|
+
metadata: jsonb("metadata").$type().notNull().default({}),
|
|
72
|
+
timestamp: timestamp("timestamp", { withTimezone: true }).notNull().defaultNow()
|
|
73
|
+
}, (table) => [index("mnemocyte_events_entity_time_idx").on(table.entityId, table.timestamp)]);
|
|
74
|
+
const metaTable = pgTable("mnemocyte_meta", {
|
|
75
|
+
key: text("key").primaryKey(),
|
|
76
|
+
embeddingDimensions: integer("embedding_dimensions").notNull(),
|
|
77
|
+
embeddingModel: text("embedding_model")
|
|
78
|
+
});
|
|
79
|
+
//#endregion
|
|
80
|
+
//#region src/db/index.ts
|
|
81
|
+
function createCallerOwnedDatabaseHandle(db) {
|
|
82
|
+
return {
|
|
83
|
+
db,
|
|
84
|
+
async close() {}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function parseSslFromUrl(url) {
|
|
88
|
+
const mode = url.searchParams.get("sslmode");
|
|
89
|
+
if (!mode || mode === "disable") return void 0;
|
|
90
|
+
if (mode === "require" || mode === "prefer" || mode === "allow") return { rejectUnauthorized: false };
|
|
91
|
+
if (mode === "verify-full") return "verify-full";
|
|
92
|
+
}
|
|
93
|
+
function createDatabase(databaseUrl) {
|
|
94
|
+
const url = parsePostgresDatabaseUrl(databaseUrl);
|
|
95
|
+
const ssl = parseSslFromUrl(url);
|
|
96
|
+
const isPooler = url.port === "6543" || url.searchParams.get("pgbouncer") === "true";
|
|
97
|
+
const client = postgres(databaseUrl, {
|
|
98
|
+
max: 10,
|
|
99
|
+
idle_timeout: 20,
|
|
100
|
+
connect_timeout: 10,
|
|
101
|
+
...ssl ? { ssl } : {},
|
|
102
|
+
...isPooler ? { prepare: false } : {}
|
|
103
|
+
});
|
|
104
|
+
return {
|
|
105
|
+
db: drizzle(client, { schema: schema_exports }),
|
|
106
|
+
async close() {
|
|
107
|
+
await client.end();
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
//#endregion
|
|
112
|
+
//#region src/db/cancellation.ts
|
|
113
|
+
const postgresDialect = new PgDialect();
|
|
114
|
+
async function awaitCancelableQuery(query, signal) {
|
|
115
|
+
throwIfAborted(signal);
|
|
116
|
+
if (!signal) return await query;
|
|
117
|
+
const onAbort = () => {
|
|
118
|
+
query.cancel();
|
|
119
|
+
};
|
|
120
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
121
|
+
try {
|
|
122
|
+
return await query;
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (signal.aborted) throw new MnemocyteError("Operation was aborted.", "ABORTED", signal.reason);
|
|
125
|
+
throw error;
|
|
126
|
+
} finally {
|
|
127
|
+
signal.removeEventListener("abort", onAbort);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async function executeCancelableSql(db, statement, signal) {
|
|
131
|
+
const query = postgresDialect.sqlToQuery(statement);
|
|
132
|
+
const client = db.$client;
|
|
133
|
+
if (!client) throw new MnemocyteError("Cancelable Postgres query requires the postgres.js client.", "DB");
|
|
134
|
+
return await awaitCancelableQuery(client.unsafe(query.sql, query.params), signal);
|
|
135
|
+
}
|
|
136
|
+
//#endregion
|
|
137
|
+
//#region src/db/queries/events.ts
|
|
138
|
+
function timestampParam$1(value) {
|
|
139
|
+
return sql`${value.toISOString()}::timestamptz`;
|
|
140
|
+
}
|
|
141
|
+
async function insertEvent(db, row) {
|
|
142
|
+
const inserted = (await db.insert(eventsTable).values(row).returning())[0];
|
|
143
|
+
if (!inserted) throw new MnemocyteError("Event insert returned no rows.", "DB");
|
|
144
|
+
return inserted;
|
|
145
|
+
}
|
|
146
|
+
async function listEvents(db, filter, signal) {
|
|
147
|
+
return executeCancelableSql(db, sql`
|
|
148
|
+
SELECT
|
|
149
|
+
id,
|
|
150
|
+
entity_id AS "entityId",
|
|
151
|
+
description,
|
|
152
|
+
metadata,
|
|
153
|
+
timestamp
|
|
154
|
+
FROM mnemocyte_events
|
|
155
|
+
WHERE entity_id = ${filter.entityId}
|
|
156
|
+
${filter.before ? sql`AND timestamp < ${timestampParam$1(filter.before)}` : sql``}
|
|
157
|
+
${filter.after ? sql`AND timestamp > ${timestampParam$1(filter.after)}` : sql``}
|
|
158
|
+
${filter.beforeCursor ? sql`AND (timestamp, id) < (${timestampParam$1(filter.beforeCursor.timestamp)}, ${filter.beforeCursor.id})` : sql``}
|
|
159
|
+
${filter.afterCursor ? sql`AND (timestamp, id) > (${timestampParam$1(filter.afterCursor.timestamp)}, ${filter.afterCursor.id})` : sql``}
|
|
160
|
+
ORDER BY timestamp DESC, id DESC
|
|
161
|
+
LIMIT ${filter.limit ?? 50}
|
|
162
|
+
`, signal);
|
|
163
|
+
}
|
|
164
|
+
//#endregion
|
|
165
|
+
//#region src/db/queries/memories.ts
|
|
166
|
+
function vectorLiteral(embedding) {
|
|
167
|
+
return `[${embedding.map(formatVectorComponent).join(",")}]`;
|
|
168
|
+
}
|
|
169
|
+
function clampedVectorScore(embedding) {
|
|
170
|
+
const rawScore = sql`1 - (embedding <=> ${embedding}::vector)`;
|
|
171
|
+
return sql`
|
|
172
|
+
CASE
|
|
173
|
+
WHEN ${rawScore} = 'NaN'::double precision
|
|
174
|
+
OR ${rawScore} = 'Infinity'::double precision
|
|
175
|
+
OR ${rawScore} = '-Infinity'::double precision
|
|
176
|
+
THEN 0::double precision
|
|
177
|
+
ELSE GREATEST(
|
|
178
|
+
0::double precision,
|
|
179
|
+
LEAST(1::double precision, ${rawScore})
|
|
180
|
+
)
|
|
181
|
+
END
|
|
182
|
+
`;
|
|
183
|
+
}
|
|
184
|
+
function timestampParam(value) {
|
|
185
|
+
return sql`${value.toISOString()}::timestamptz`;
|
|
186
|
+
}
|
|
187
|
+
function textArrayLiteral(values) {
|
|
188
|
+
return sql`ARRAY[${sql.join(values.map((value) => sql`${value}`), sql`, `)}]::text[]`;
|
|
189
|
+
}
|
|
190
|
+
function tagsContainAll(tags) {
|
|
191
|
+
return tags && tags.length > 0 ? sql`${memoriesTable.tags} @> ${textArrayLiteral(tags)}` : void 0;
|
|
192
|
+
}
|
|
193
|
+
function rawTagsFilter(tags) {
|
|
194
|
+
return tags && tags.length > 0 ? sql`AND tags @> ${textArrayLiteral(tags)}` : sql``;
|
|
195
|
+
}
|
|
196
|
+
function duplicateTagsFilter(tags) {
|
|
197
|
+
return tags && tags.length > 0 ? sql`AND a.tags @> ${textArrayLiteral(tags)} AND b.tags @> ${textArrayLiteral(tags)}` : sql``;
|
|
198
|
+
}
|
|
199
|
+
function memoryStatsCountFields(now) {
|
|
200
|
+
return {
|
|
201
|
+
memoryCount: count(),
|
|
202
|
+
activeMemoryCount: sql`
|
|
203
|
+
(count(*) FILTER (
|
|
204
|
+
WHERE ${memoriesTable.supersededBy} IS NULL
|
|
205
|
+
AND (${memoriesTable.expiresAt} IS NULL OR ${memoriesTable.expiresAt} > ${timestampParam(now)})
|
|
206
|
+
))::int
|
|
207
|
+
`.mapWith(Number),
|
|
208
|
+
expiredMemoryCount: sql`
|
|
209
|
+
(count(*) FILTER (
|
|
210
|
+
WHERE ${memoriesTable.expiresAt} IS NOT NULL
|
|
211
|
+
AND ${memoriesTable.expiresAt} <= ${timestampParam(now)}
|
|
212
|
+
))::int
|
|
213
|
+
`.mapWith(Number),
|
|
214
|
+
supersededMemoryCount: sql`(count(*) FILTER (WHERE ${memoriesTable.supersededBy} IS NOT NULL))::int`.mapWith(Number)
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
function emptyMemoryStatsCounts() {
|
|
218
|
+
return {
|
|
219
|
+
memoryCount: 0,
|
|
220
|
+
activeMemoryCount: 0,
|
|
221
|
+
expiredMemoryCount: 0,
|
|
222
|
+
supersededMemoryCount: 0
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
async function insertMemories(db, rows) {
|
|
226
|
+
if (rows.length === 0) return [];
|
|
227
|
+
return db.insert(memoriesTable).values(rows).returning();
|
|
228
|
+
}
|
|
229
|
+
async function getMemoryById(db, entityId, memoryId) {
|
|
230
|
+
return (await db.select().from(memoriesTable).where(and(eq(memoriesTable.entityId, entityId), eq(memoriesTable.id, memoryId))).limit(1))[0] ?? null;
|
|
231
|
+
}
|
|
232
|
+
async function getEntityMemoryStatsCounts(db, entityId, now) {
|
|
233
|
+
return (await db.select(memoryStatsCountFields(now)).from(memoriesTable).where(eq(memoriesTable.entityId, entityId)))[0] ?? emptyMemoryStatsCounts();
|
|
234
|
+
}
|
|
235
|
+
async function getGlobalMemoryStatsCounts(db, now) {
|
|
236
|
+
return (await db.select({
|
|
237
|
+
entityCount: countDistinct(memoriesTable.entityId),
|
|
238
|
+
...memoryStatsCountFields(now)
|
|
239
|
+
}).from(memoriesTable))[0] ?? {
|
|
240
|
+
entityCount: 0,
|
|
241
|
+
...emptyMemoryStatsCounts()
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
async function deleteMemory(db, entityId, memoryId) {
|
|
245
|
+
return deleteMemoriesWithDependentGuard(db, and(eq(memoriesTable.entityId, entityId), eq(memoriesTable.id, memoryId)));
|
|
246
|
+
}
|
|
247
|
+
async function deleteMemoriesForEntity(db, entityId) {
|
|
248
|
+
return deleteMemoriesWithDependentGuard(db, eq(memoriesTable.entityId, entityId));
|
|
249
|
+
}
|
|
250
|
+
function pruneConditions(filter) {
|
|
251
|
+
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);
|
|
252
|
+
}
|
|
253
|
+
async function countPruneMatches(db, filter, signal) {
|
|
254
|
+
const conditions = pruneConditions(filter);
|
|
255
|
+
const rows = await executeCancelableSql(db, sql`
|
|
256
|
+
SELECT count(*)::int AS count
|
|
257
|
+
FROM ${memoriesTable}
|
|
258
|
+
${conditions ? sql`WHERE ${conditions}` : sql``}
|
|
259
|
+
`, signal);
|
|
260
|
+
return Number(rows[0]?.count ?? 0);
|
|
261
|
+
}
|
|
262
|
+
async function pruneMemories(db, filter, signal) {
|
|
263
|
+
return deleteMemoriesWithDependentGuard(db, pruneConditions(filter), signal);
|
|
264
|
+
}
|
|
265
|
+
async function deleteMemoriesWithDependentGuard(db, conditions, signal) {
|
|
266
|
+
const rows = await executeCancelableSql(db, sql`
|
|
267
|
+
WITH candidates AS MATERIALIZED (
|
|
268
|
+
SELECT id, entity_id
|
|
269
|
+
FROM ${memoriesTable}
|
|
270
|
+
${conditions ? sql`WHERE ${conditions}` : sql``}
|
|
271
|
+
),
|
|
272
|
+
dependents AS MATERIALIZED (
|
|
273
|
+
SELECT 1
|
|
274
|
+
FROM ${memoriesTable} AS dependent
|
|
275
|
+
INNER JOIN candidates AS target
|
|
276
|
+
ON dependent.superseded_by = target.id
|
|
277
|
+
LIMIT 1
|
|
278
|
+
),
|
|
279
|
+
deleted AS (
|
|
280
|
+
DELETE FROM ${memoriesTable}
|
|
281
|
+
WHERE ${memoriesTable.id} IN (SELECT id FROM candidates)
|
|
282
|
+
AND NOT EXISTS (SELECT 1 FROM dependents)
|
|
283
|
+
RETURNING id, entity_id
|
|
284
|
+
),
|
|
285
|
+
deleted_counts AS (
|
|
286
|
+
SELECT entity_id, count(*)::int AS deleted_count
|
|
287
|
+
FROM deleted
|
|
288
|
+
GROUP BY entity_id
|
|
289
|
+
),
|
|
290
|
+
summary AS (
|
|
291
|
+
SELECT
|
|
292
|
+
(SELECT count(*)::int FROM candidates) AS matched_count,
|
|
293
|
+
(SELECT count(*)::int FROM deleted) AS deleted_count,
|
|
294
|
+
EXISTS (SELECT 1 FROM dependents) AS has_dependents
|
|
295
|
+
)
|
|
296
|
+
SELECT
|
|
297
|
+
summary.matched_count AS "matchedCount",
|
|
298
|
+
summary.deleted_count AS "deletedCount",
|
|
299
|
+
summary.has_dependents AS "hasDependents",
|
|
300
|
+
deleted_counts.entity_id AS "entityId",
|
|
301
|
+
deleted_counts.deleted_count AS "entityDeletedCount"
|
|
302
|
+
FROM summary
|
|
303
|
+
LEFT JOIN deleted_counts ON true
|
|
304
|
+
ORDER BY deleted_counts.entity_id
|
|
305
|
+
`, signal);
|
|
306
|
+
const row = rows[0];
|
|
307
|
+
return {
|
|
308
|
+
matchedCount: Number(row?.matchedCount ?? 0),
|
|
309
|
+
deletedCount: Number(row?.deletedCount ?? 0),
|
|
310
|
+
hasDependents: row?.hasDependents === true,
|
|
311
|
+
deletedByEntity: rows.flatMap((detail) => detail.entityId === null ? [] : [{
|
|
312
|
+
entityId: detail.entityId,
|
|
313
|
+
deletedCount: Number(detail.entityDeletedCount ?? 0)
|
|
314
|
+
}])
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
async function findDuplicatePairs(db, input, signal) {
|
|
318
|
+
return (await executeCancelableSql(db, sql`
|
|
319
|
+
SELECT
|
|
320
|
+
a.id AS "aId",
|
|
321
|
+
a.entity_id AS "aEntityId",
|
|
322
|
+
a.content AS "aContent",
|
|
323
|
+
a.type AS "aType",
|
|
324
|
+
a.importance AS "aImportance",
|
|
325
|
+
a.tags AS "aTags",
|
|
326
|
+
a.source AS "aSource",
|
|
327
|
+
a.metadata AS "aMetadata",
|
|
328
|
+
a.confidence AS "aConfidence",
|
|
329
|
+
a.embedding AS "aEmbedding",
|
|
330
|
+
a.embedding_model AS "aEmbeddingModel",
|
|
331
|
+
a.embedding_dimensions AS "aEmbeddingDimensions",
|
|
332
|
+
a.superseded_by AS "aSupersededBy",
|
|
333
|
+
a.superseded_at AS "aSupersededAt",
|
|
334
|
+
a.expires_at AS "aExpiresAt",
|
|
335
|
+
a.last_accessed_at AS "aLastAccessedAt",
|
|
336
|
+
a.access_count AS "aAccessCount",
|
|
337
|
+
a.created_at AS "aCreatedAt",
|
|
338
|
+
a.updated_at AS "aUpdatedAt",
|
|
339
|
+
b.id AS "bId",
|
|
340
|
+
b.entity_id AS "bEntityId",
|
|
341
|
+
b.content AS "bContent",
|
|
342
|
+
b.type AS "bType",
|
|
343
|
+
b.importance AS "bImportance",
|
|
344
|
+
b.tags AS "bTags",
|
|
345
|
+
b.source AS "bSource",
|
|
346
|
+
b.metadata AS "bMetadata",
|
|
347
|
+
b.confidence AS "bConfidence",
|
|
348
|
+
b.embedding AS "bEmbedding",
|
|
349
|
+
b.embedding_model AS "bEmbeddingModel",
|
|
350
|
+
b.embedding_dimensions AS "bEmbeddingDimensions",
|
|
351
|
+
b.superseded_by AS "bSupersededBy",
|
|
352
|
+
b.superseded_at AS "bSupersededAt",
|
|
353
|
+
b.expires_at AS "bExpiresAt",
|
|
354
|
+
b.last_accessed_at AS "bLastAccessedAt",
|
|
355
|
+
b.access_count AS "bAccessCount",
|
|
356
|
+
b.created_at AS "bCreatedAt",
|
|
357
|
+
b.updated_at AS "bUpdatedAt",
|
|
358
|
+
1 - (a.embedding <=> b.embedding) AS "similarity"
|
|
359
|
+
FROM mnemocyte_memories a
|
|
360
|
+
JOIN mnemocyte_memories b
|
|
361
|
+
ON b.entity_id = a.entity_id
|
|
362
|
+
AND a.id < b.id
|
|
363
|
+
AND a.embedding IS NOT NULL
|
|
364
|
+
AND b.embedding IS NOT NULL
|
|
365
|
+
WHERE
|
|
366
|
+
a.entity_id = ${input.entityId}
|
|
367
|
+
${input.includeSuperseded ? sql`` : sql`AND a.superseded_by IS NULL AND b.superseded_by IS NULL`}
|
|
368
|
+
${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())`}
|
|
369
|
+
${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``}
|
|
370
|
+
${duplicateTagsFilter(input.tags)}
|
|
371
|
+
AND 1 - (a.embedding <=> b.embedding) >= ${input.threshold}
|
|
372
|
+
ORDER BY "similarity" DESC
|
|
373
|
+
LIMIT ${input.limit}
|
|
374
|
+
`, signal)).map((row) => {
|
|
375
|
+
return {
|
|
376
|
+
a: {
|
|
377
|
+
id: row.aId,
|
|
378
|
+
entityId: row.aEntityId,
|
|
379
|
+
content: row.aContent,
|
|
380
|
+
type: row.aType,
|
|
381
|
+
importance: row.aImportance,
|
|
382
|
+
tags: row.aTags,
|
|
383
|
+
source: row.aSource,
|
|
384
|
+
metadata: row.aMetadata,
|
|
385
|
+
confidence: row.aConfidence,
|
|
386
|
+
embedding: row.aEmbedding,
|
|
387
|
+
embeddingModel: row.aEmbeddingModel,
|
|
388
|
+
embeddingDimensions: row.aEmbeddingDimensions,
|
|
389
|
+
supersededBy: row.aSupersededBy,
|
|
390
|
+
supersededAt: row.aSupersededAt,
|
|
391
|
+
expiresAt: row.aExpiresAt,
|
|
392
|
+
lastAccessedAt: row.aLastAccessedAt,
|
|
393
|
+
accessCount: row.aAccessCount,
|
|
394
|
+
createdAt: row.aCreatedAt,
|
|
395
|
+
updatedAt: row.aUpdatedAt
|
|
396
|
+
},
|
|
397
|
+
b: {
|
|
398
|
+
id: row.bId,
|
|
399
|
+
entityId: row.bEntityId,
|
|
400
|
+
content: row.bContent,
|
|
401
|
+
type: row.bType,
|
|
402
|
+
importance: row.bImportance,
|
|
403
|
+
tags: row.bTags,
|
|
404
|
+
source: row.bSource,
|
|
405
|
+
metadata: row.bMetadata,
|
|
406
|
+
confidence: row.bConfidence,
|
|
407
|
+
embedding: row.bEmbedding,
|
|
408
|
+
embeddingModel: row.bEmbeddingModel,
|
|
409
|
+
embeddingDimensions: row.bEmbeddingDimensions,
|
|
410
|
+
supersededBy: row.bSupersededBy,
|
|
411
|
+
supersededAt: row.bSupersededAt,
|
|
412
|
+
expiresAt: row.bExpiresAt,
|
|
413
|
+
lastAccessedAt: row.bLastAccessedAt,
|
|
414
|
+
accessCount: row.bAccessCount,
|
|
415
|
+
createdAt: row.bCreatedAt,
|
|
416
|
+
updatedAt: row.bUpdatedAt
|
|
417
|
+
},
|
|
418
|
+
similarity: Number(row.similarity) || 0
|
|
419
|
+
};
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
async function loadConsolidationTargets(db, entityId, ids) {
|
|
423
|
+
if (ids.length === 0) return [];
|
|
424
|
+
return await db.select({
|
|
425
|
+
id: memoriesTable.id,
|
|
426
|
+
tags: memoriesTable.tags,
|
|
427
|
+
supersededBy: memoriesTable.supersededBy
|
|
428
|
+
}).from(memoriesTable).where(and(eq(memoriesTable.entityId, entityId), inArray(memoriesTable.id, [...ids])));
|
|
429
|
+
}
|
|
430
|
+
async function lockConsolidationMemories(db, entityId, survivorId, supersededIds) {
|
|
431
|
+
const ids = [...new Set([survivorId, ...supersededIds])].sort();
|
|
432
|
+
return db.select({
|
|
433
|
+
id: memoriesTable.id,
|
|
434
|
+
tags: memoriesTable.tags,
|
|
435
|
+
supersededBy: memoriesTable.supersededBy
|
|
436
|
+
}).from(memoriesTable).where(and(eq(memoriesTable.entityId, entityId), inArray(memoriesTable.id, ids))).orderBy(memoriesTable.id).for("update");
|
|
437
|
+
}
|
|
438
|
+
async function markMemoriesSuperseded(db, params) {
|
|
439
|
+
if (params.ids.length === 0) return [];
|
|
440
|
+
return await db.update(memoriesTable).set({
|
|
441
|
+
supersededBy: params.survivorId,
|
|
442
|
+
supersededAt: params.now,
|
|
443
|
+
updatedAt: params.now
|
|
444
|
+
}).where(and(eq(memoriesTable.entityId, params.entityId), inArray(memoriesTable.id, [...params.ids]), isNull(memoriesTable.supersededBy))).returning({
|
|
445
|
+
id: memoriesTable.id,
|
|
446
|
+
tags: memoriesTable.tags
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
async function setMemoryTags(db, params) {
|
|
450
|
+
await db.update(memoriesTable).set({
|
|
451
|
+
tags: [...params.tags],
|
|
452
|
+
updatedAt: params.now
|
|
453
|
+
}).where(and(eq(memoriesTable.entityId, params.entityId), eq(memoriesTable.id, params.memoryId)));
|
|
454
|
+
}
|
|
455
|
+
async function markMemoriesAccessed(db, memoryIds) {
|
|
456
|
+
const now = /* @__PURE__ */ new Date();
|
|
457
|
+
if (memoryIds.length === 0) return [];
|
|
458
|
+
return db.update(memoriesTable).set({
|
|
459
|
+
lastAccessedAt: now,
|
|
460
|
+
accessCount: sql`${memoriesTable.accessCount} + 1`,
|
|
461
|
+
updatedAt: now
|
|
462
|
+
}).where(inArray(memoriesTable.id, [...memoryIds])).returning({
|
|
463
|
+
id: memoriesTable.id,
|
|
464
|
+
lastAccessedAt: memoriesTable.lastAccessedAt,
|
|
465
|
+
accessCount: memoriesTable.accessCount,
|
|
466
|
+
updatedAt: memoriesTable.updatedAt
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
async function getMemoryEmbeddings(db, memoryIds) {
|
|
470
|
+
if (memoryIds.length === 0) return /* @__PURE__ */ new Map();
|
|
471
|
+
const rows = await db.select({
|
|
472
|
+
id: memoriesTable.id,
|
|
473
|
+
embedding: memoriesTable.embedding
|
|
474
|
+
}).from(memoriesTable).where(inArray(memoriesTable.id, [...memoryIds]));
|
|
475
|
+
return new Map(rows.flatMap((row) => row.embedding ? [[row.id, row.embedding]] : []));
|
|
476
|
+
}
|
|
477
|
+
async function vectorSearch(db, input) {
|
|
478
|
+
const embedding = vectorLiteral(input.embedding);
|
|
479
|
+
const vectorScore = clampedVectorScore(embedding);
|
|
480
|
+
const minVectorScore = input.minVectorScore ?? 0;
|
|
481
|
+
return await db.execute(sql`
|
|
482
|
+
SELECT
|
|
483
|
+
id,
|
|
484
|
+
entity_id AS "entityId",
|
|
485
|
+
content,
|
|
486
|
+
type,
|
|
487
|
+
importance,
|
|
488
|
+
tags,
|
|
489
|
+
source,
|
|
490
|
+
metadata,
|
|
491
|
+
confidence,
|
|
492
|
+
embedding_model AS "embeddingModel",
|
|
493
|
+
embedding_dimensions AS "embeddingDimensions",
|
|
494
|
+
superseded_by AS "supersededBy",
|
|
495
|
+
superseded_at AS "supersededAt",
|
|
496
|
+
expires_at AS "expiresAt",
|
|
497
|
+
last_accessed_at AS "lastAccessedAt",
|
|
498
|
+
access_count AS "accessCount",
|
|
499
|
+
created_at AS "createdAt",
|
|
500
|
+
updated_at AS "updatedAt",
|
|
501
|
+
${vectorScore} AS "vectorScore"
|
|
502
|
+
FROM mnemocyte_memories
|
|
503
|
+
WHERE
|
|
504
|
+
entity_id = ${input.entityId}
|
|
505
|
+
AND embedding IS NOT NULL
|
|
506
|
+
${input.includeSuperseded ? sql`` : sql`AND superseded_by IS NULL`}
|
|
507
|
+
${input.includeExpired ? sql`` : sql`AND (expires_at IS NULL OR expires_at > now())`}
|
|
508
|
+
${input.types && input.types.length > 0 ? sql`AND type IN (${sql.join(input.types.map((type) => sql`${type}`), sql`, `)})` : sql``}
|
|
509
|
+
${rawTagsFilter(input.tags)}
|
|
510
|
+
${input.before ? sql`AND created_at < ${timestampParam(input.before)}` : sql``}
|
|
511
|
+
${input.after ? sql`AND created_at > ${timestampParam(input.after)}` : sql``}
|
|
512
|
+
AND ${vectorScore} >= ${minVectorScore}
|
|
513
|
+
ORDER BY embedding <=> ${embedding}::vector
|
|
514
|
+
LIMIT ${input.limit}
|
|
515
|
+
`);
|
|
516
|
+
}
|
|
517
|
+
async function lexicalSearch(db, input) {
|
|
518
|
+
return await db.execute(sql`
|
|
519
|
+
SELECT
|
|
520
|
+
id,
|
|
521
|
+
entity_id AS "entityId",
|
|
522
|
+
content,
|
|
523
|
+
type,
|
|
524
|
+
importance,
|
|
525
|
+
tags,
|
|
526
|
+
source,
|
|
527
|
+
metadata,
|
|
528
|
+
confidence,
|
|
529
|
+
embedding_model AS "embeddingModel",
|
|
530
|
+
embedding_dimensions AS "embeddingDimensions",
|
|
531
|
+
superseded_by AS "supersededBy",
|
|
532
|
+
superseded_at AS "supersededAt",
|
|
533
|
+
expires_at AS "expiresAt",
|
|
534
|
+
last_accessed_at AS "lastAccessedAt",
|
|
535
|
+
access_count AS "accessCount",
|
|
536
|
+
created_at AS "createdAt",
|
|
537
|
+
updated_at AS "updatedAt",
|
|
538
|
+
ts_rank(to_tsvector('english', content), websearch_to_tsquery('english', ${input.query})) AS "lexicalScore"
|
|
539
|
+
FROM mnemocyte_memories
|
|
540
|
+
WHERE
|
|
541
|
+
entity_id = ${input.entityId}
|
|
542
|
+
${input.includeSuperseded ? sql`` : sql`AND superseded_by IS NULL`}
|
|
543
|
+
${input.includeExpired ? sql`` : sql`AND (expires_at IS NULL OR expires_at > now())`}
|
|
544
|
+
${input.types && input.types.length > 0 ? sql`AND type IN (${sql.join(input.types.map((type) => sql`${type}`), sql`, `)})` : sql``}
|
|
545
|
+
${rawTagsFilter(input.tags)}
|
|
546
|
+
${input.before ? sql`AND created_at < ${timestampParam(input.before)}` : sql``}
|
|
547
|
+
${input.after ? sql`AND created_at > ${timestampParam(input.after)}` : sql``}
|
|
548
|
+
AND to_tsvector('english', content) @@ websearch_to_tsquery('english', ${input.query})
|
|
549
|
+
ORDER BY "lexicalScore" DESC
|
|
550
|
+
LIMIT ${input.limit}
|
|
551
|
+
`);
|
|
552
|
+
}
|
|
553
|
+
async function getInstallationMeta(db) {
|
|
554
|
+
return (await db.select().from(metaTable).where(eq(metaTable.key, "installation")).limit(1))[0] ?? null;
|
|
555
|
+
}
|
|
556
|
+
async function getStoredEmbeddingModels(db) {
|
|
557
|
+
return (await db.selectDistinct({ embeddingModel: memoriesTable.embeddingModel }).from(memoriesTable).limit(2)).map((row) => row.embeddingModel);
|
|
558
|
+
}
|
|
559
|
+
async function recordInstallationEmbeddingModel(db, embeddingModel) {
|
|
560
|
+
return (await db.update(metaTable).set({ embeddingModel }).where(and(eq(metaTable.key, "installation"), isNull(metaTable.embeddingModel))).returning())[0] ?? null;
|
|
561
|
+
}
|
|
562
|
+
//#endregion
|
|
563
|
+
//#region src/memory/postgres-records.ts
|
|
564
|
+
function toDate(value) {
|
|
565
|
+
return value instanceof Date ? value : new Date(value);
|
|
566
|
+
}
|
|
567
|
+
function toNullableDate(value) {
|
|
568
|
+
return value === null ? null : toDate(value);
|
|
569
|
+
}
|
|
570
|
+
function rowToMemory(row) {
|
|
571
|
+
return {
|
|
572
|
+
id: row.id,
|
|
573
|
+
entityId: row.entityId,
|
|
574
|
+
content: row.content,
|
|
575
|
+
type: row.type,
|
|
576
|
+
importance: row.importance,
|
|
577
|
+
tags: row.tags,
|
|
578
|
+
source: row.source,
|
|
579
|
+
metadata: cloneJsonObject(row.metadata),
|
|
580
|
+
confidence: row.confidence,
|
|
581
|
+
embeddingModel: row.embeddingModel,
|
|
582
|
+
embeddingDimensions: row.embeddingDimensions,
|
|
583
|
+
supersededBy: row.supersededBy,
|
|
584
|
+
supersededAt: toNullableDate(row.supersededAt),
|
|
585
|
+
expiresAt: toNullableDate(row.expiresAt),
|
|
586
|
+
lastAccessedAt: toNullableDate(row.lastAccessedAt),
|
|
587
|
+
accessCount: row.accessCount,
|
|
588
|
+
createdAt: toDate(row.createdAt),
|
|
589
|
+
updatedAt: toDate(row.updatedAt)
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
//#endregion
|
|
593
|
+
//#region src/memory/postgres.ts
|
|
594
|
+
const IMPORTANCE_LEVELS = [
|
|
595
|
+
"low",
|
|
596
|
+
"normal",
|
|
597
|
+
"high",
|
|
598
|
+
"critical"
|
|
599
|
+
];
|
|
600
|
+
const MIGRATION_ERROR_CODES = new Set([
|
|
601
|
+
"42P01",
|
|
602
|
+
"42703",
|
|
603
|
+
"42704",
|
|
604
|
+
"42883"
|
|
605
|
+
]);
|
|
606
|
+
const CONSOLIDATION_SURVIVOR_FOREIGN_KEY = "mnemocyte_memories_superseded_by_mnemocyte_memories_id_fk";
|
|
607
|
+
function rowToAuditEvent(row) {
|
|
608
|
+
return {
|
|
609
|
+
id: row.id,
|
|
610
|
+
entityId: row.entityId,
|
|
611
|
+
description: row.description,
|
|
612
|
+
metadata: cloneJsonObject(row.metadata),
|
|
613
|
+
timestamp: new Date(row.timestamp)
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
function importanceCeilingLevels(max) {
|
|
617
|
+
return IMPORTANCE_LEVELS.filter((level) => IMPORTANCE_RANK[level] <= IMPORTANCE_RANK[max]);
|
|
618
|
+
}
|
|
619
|
+
function hasPostgresErrorCode(error, code) {
|
|
620
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
621
|
+
}
|
|
622
|
+
function hasPostgresConstraintName(error, name) {
|
|
623
|
+
return typeof error === "object" && error !== null && "constraint_name" in error && error.constraint_name === name;
|
|
624
|
+
}
|
|
625
|
+
function getPostgresErrorCode(error) {
|
|
626
|
+
return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0;
|
|
627
|
+
}
|
|
628
|
+
function normalizePostgresError(error) {
|
|
629
|
+
if (error instanceof MnemocyteError) return error;
|
|
630
|
+
const code = getPostgresErrorCode(error);
|
|
631
|
+
if (code && MIGRATION_ERROR_CODES.has(code)) return new MnemocyteError("Postgres schema is missing or incompatible. Apply the bundled Mnemocyte migrations before using the Postgres backend.", "MIGRATION", error);
|
|
632
|
+
return new MnemocyteError("Postgres operation failed.", "DB", error);
|
|
633
|
+
}
|
|
634
|
+
async function runPostgresOperation(operation) {
|
|
635
|
+
try {
|
|
636
|
+
return await operation();
|
|
637
|
+
} catch (error) {
|
|
638
|
+
throw normalizePostgresError(error);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
async function runPostgresDeleteOperation(operation) {
|
|
642
|
+
try {
|
|
643
|
+
return await operation();
|
|
644
|
+
} catch (error) {
|
|
645
|
+
if (error instanceof MnemocyteError) throw error;
|
|
646
|
+
if (hasPostgresErrorCode(error, "23503") && hasPostgresConstraintName(error, CONSOLIDATION_SURVIVOR_FOREIGN_KEY)) throw memoryHasDependentsError(error);
|
|
647
|
+
throw normalizePostgresError(error);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
function toPruneFilter(input) {
|
|
651
|
+
const filter = {};
|
|
652
|
+
if (input.entityId !== void 0) filter.entityId = input.entityId;
|
|
653
|
+
if (input.expired === true) filter.expired = true;
|
|
654
|
+
if (input.superseded === true) filter.superseded = true;
|
|
655
|
+
if (input.createdBefore !== void 0) filter.createdBefore = input.createdBefore;
|
|
656
|
+
if (input.notAccessedSince !== void 0) filter.notAccessedSince = input.notAccessedSince;
|
|
657
|
+
if (input.types !== void 0 && input.types.length > 0) filter.types = input.types;
|
|
658
|
+
if (input.tags !== void 0 && input.tags.length > 0) filter.tags = input.tags;
|
|
659
|
+
if (input.maxImportance !== void 0) filter.maxImportanceLevels = importanceCeilingLevels(input.maxImportance);
|
|
660
|
+
return filter;
|
|
661
|
+
}
|
|
662
|
+
function toMemoryRow(memory) {
|
|
663
|
+
return {
|
|
664
|
+
...memory,
|
|
665
|
+
tags: [...memory.tags],
|
|
666
|
+
embedding: [...memory.embedding]
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
function createPostgresStore(handle) {
|
|
670
|
+
let schemaValidated = false;
|
|
671
|
+
let schemaValidationPromise;
|
|
672
|
+
async function ensureEmbeddingCompatibility(embedder) {
|
|
673
|
+
if (schemaValidated) return;
|
|
674
|
+
if (!schemaValidationPromise) schemaValidationPromise = validateEmbeddingCompatibility(embedder).catch((error) => {
|
|
675
|
+
schemaValidationPromise = void 0;
|
|
676
|
+
throw error;
|
|
677
|
+
});
|
|
678
|
+
await schemaValidationPromise;
|
|
679
|
+
}
|
|
680
|
+
async function validateEmbeddingCompatibility(embedder) {
|
|
681
|
+
let meta;
|
|
682
|
+
try {
|
|
683
|
+
meta = await getInstallationMeta(handle.db);
|
|
684
|
+
} catch (error) {
|
|
685
|
+
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);
|
|
686
|
+
throw normalizePostgresError(error);
|
|
687
|
+
}
|
|
688
|
+
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");
|
|
689
|
+
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");
|
|
690
|
+
let installationModel = meta.embeddingModel;
|
|
691
|
+
if (installationModel === null) {
|
|
692
|
+
try {
|
|
693
|
+
const storedModels = await getStoredEmbeddingModels(handle.db);
|
|
694
|
+
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");
|
|
695
|
+
const inferredModel = storedModels[0] ?? embedder.model;
|
|
696
|
+
const recorded = await recordInstallationEmbeddingModel(handle.db, inferredModel);
|
|
697
|
+
if (recorded) installationModel = recorded.embeddingModel;
|
|
698
|
+
else installationModel = (await getInstallationMeta(handle.db))?.embeddingModel ?? null;
|
|
699
|
+
} catch (error) {
|
|
700
|
+
throw normalizePostgresError(error);
|
|
701
|
+
}
|
|
702
|
+
if (installationModel === null) throw new MnemocyteError("mnemocyte_meta.embedding_model could not be recorded. Verify the installation metadata row and retry.", "MIGRATION");
|
|
703
|
+
}
|
|
704
|
+
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");
|
|
705
|
+
schemaValidated = true;
|
|
706
|
+
}
|
|
707
|
+
return {
|
|
708
|
+
backend: "postgres",
|
|
709
|
+
async ensureSchema() {},
|
|
710
|
+
ensureEmbeddingCompatibility,
|
|
711
|
+
async insertMemories(memories) {
|
|
712
|
+
return runPostgresOperation(async () => {
|
|
713
|
+
return (await insertMemories(handle.db, memories.map(toMemoryRow))).map(rowToMemory);
|
|
714
|
+
});
|
|
715
|
+
},
|
|
716
|
+
async vectorSearch(input) {
|
|
717
|
+
return runPostgresOperation(async () => {
|
|
718
|
+
return (await vectorSearch(handle.db, input)).map((row) => ({
|
|
719
|
+
memory: rowToMemory(row),
|
|
720
|
+
vectorScore: row.vectorScore
|
|
721
|
+
}));
|
|
722
|
+
});
|
|
723
|
+
},
|
|
724
|
+
async lexicalSearch(input) {
|
|
725
|
+
return runPostgresOperation(async () => {
|
|
726
|
+
return (await lexicalSearch(handle.db, input)).map((row) => ({
|
|
727
|
+
memory: rowToMemory(row),
|
|
728
|
+
lexicalScore: row.lexicalScore
|
|
729
|
+
}));
|
|
730
|
+
});
|
|
731
|
+
},
|
|
732
|
+
async getMemoryEmbeddings(memoryIds) {
|
|
733
|
+
return runPostgresOperation(() => getMemoryEmbeddings(handle.db, memoryIds));
|
|
734
|
+
},
|
|
735
|
+
async markMemoriesAccessed(memoryIds) {
|
|
736
|
+
return runPostgresOperation(async () => {
|
|
737
|
+
return (await markMemoriesAccessed(handle.db, memoryIds)).map((row) => {
|
|
738
|
+
if (row.lastAccessedAt === null) throw new MnemocyteError("Postgres access update returned no timestamp.", "DB");
|
|
739
|
+
return {
|
|
740
|
+
...row,
|
|
741
|
+
lastAccessedAt: row.lastAccessedAt
|
|
742
|
+
};
|
|
743
|
+
});
|
|
744
|
+
});
|
|
745
|
+
},
|
|
746
|
+
async deleteMemory(entityId, memoryId) {
|
|
747
|
+
return runPostgresDeleteOperation(async () => {
|
|
748
|
+
const result = await deleteMemory(handle.db, entityId, memoryId);
|
|
749
|
+
if (result.hasDependents) throw memoryHasDependentsError();
|
|
750
|
+
return result.deletedCount > 0;
|
|
751
|
+
});
|
|
752
|
+
},
|
|
753
|
+
async deleteMemoriesForEntity(entityId) {
|
|
754
|
+
return runPostgresDeleteOperation(async () => {
|
|
755
|
+
const result = await deleteMemoriesForEntity(handle.db, entityId);
|
|
756
|
+
if (result.hasDependents) throw memoryHasDependentsError();
|
|
757
|
+
return result.deletedCount;
|
|
758
|
+
});
|
|
759
|
+
},
|
|
760
|
+
async prune(input, options) {
|
|
761
|
+
return runPostgresDeleteOperation(async () => {
|
|
762
|
+
throwIfAborted(options?.signal);
|
|
763
|
+
assertPruneFilterHasSelector(input);
|
|
764
|
+
const filter = toPruneFilter(input);
|
|
765
|
+
if (input.dryRun === true) return {
|
|
766
|
+
matchedCount: await countPruneMatches(handle.db, filter, options?.signal),
|
|
767
|
+
deletedCount: 0,
|
|
768
|
+
dryRun: true,
|
|
769
|
+
deletedByEntity: []
|
|
770
|
+
};
|
|
771
|
+
const result = await pruneMemories(handle.db, filter, options?.signal);
|
|
772
|
+
if (result.hasDependents) throw memoryHasDependentsError();
|
|
773
|
+
return {
|
|
774
|
+
matchedCount: result.matchedCount,
|
|
775
|
+
deletedCount: result.deletedCount,
|
|
776
|
+
dryRun: false,
|
|
777
|
+
deletedByEntity: result.deletedByEntity
|
|
778
|
+
};
|
|
779
|
+
});
|
|
780
|
+
},
|
|
781
|
+
async findDuplicatePairs(input, options) {
|
|
782
|
+
return runPostgresOperation(async () => {
|
|
783
|
+
return (await findDuplicatePairs(handle.db, {
|
|
784
|
+
entityId: input.entityId,
|
|
785
|
+
threshold: input.threshold ?? .95,
|
|
786
|
+
limit: input.limit ?? 100,
|
|
787
|
+
...input.types === void 0 ? {} : { types: input.types },
|
|
788
|
+
...input.tags === void 0 ? {} : { tags: input.tags },
|
|
789
|
+
...input.includeSuperseded === void 0 ? {} : { includeSuperseded: input.includeSuperseded },
|
|
790
|
+
...input.includeExpired === void 0 ? {} : { includeExpired: input.includeExpired }
|
|
791
|
+
}, options?.signal)).map((row) => ({
|
|
792
|
+
a: rowToMemory(row.a),
|
|
793
|
+
b: rowToMemory(row.b),
|
|
794
|
+
similarity: Math.max(0, Math.min(1, row.similarity))
|
|
795
|
+
}));
|
|
796
|
+
});
|
|
797
|
+
},
|
|
798
|
+
async addAuditEvents(events) {
|
|
799
|
+
return runPostgresOperation(async () => {
|
|
800
|
+
for (const event of events) await insertEvent(handle.db, {
|
|
801
|
+
id: event.id,
|
|
802
|
+
entityId: event.entityId,
|
|
803
|
+
description: event.description,
|
|
804
|
+
metadata: cloneJsonObject(event.metadata),
|
|
805
|
+
timestamp: event.timestamp
|
|
806
|
+
});
|
|
807
|
+
});
|
|
808
|
+
},
|
|
809
|
+
async listAuditLog(input, options) {
|
|
810
|
+
return runPostgresOperation(async () => {
|
|
811
|
+
return (await listEvents(handle.db, {
|
|
812
|
+
entityId: input.entityId,
|
|
813
|
+
limit: input.limit ?? 50,
|
|
814
|
+
...input.before === void 0 ? {} : { before: input.before },
|
|
815
|
+
...input.after === void 0 ? {} : { after: input.after },
|
|
816
|
+
...input.beforeCursor === void 0 ? {} : { beforeCursor: input.beforeCursor },
|
|
817
|
+
...input.afterCursor === void 0 ? {} : { afterCursor: input.afterCursor }
|
|
818
|
+
}, options?.signal)).map(rowToAuditEvent);
|
|
819
|
+
});
|
|
820
|
+
},
|
|
821
|
+
async getMemory(entityId, memoryId, options) {
|
|
822
|
+
return runPostgresOperation(async () => {
|
|
823
|
+
throwIfAborted(options?.signal);
|
|
824
|
+
const row = await getMemoryById(handle.db, entityId, memoryId);
|
|
825
|
+
throwIfAborted(options?.signal);
|
|
826
|
+
return row ? rowToMemory(row) : null;
|
|
827
|
+
});
|
|
828
|
+
},
|
|
829
|
+
async loadConsolidationTargets(entityId, ids, options) {
|
|
830
|
+
return runPostgresOperation(async () => {
|
|
831
|
+
throwIfAborted(options?.signal);
|
|
832
|
+
const targets = await loadConsolidationTargets(handle.db, entityId, ids);
|
|
833
|
+
throwIfAborted(options?.signal);
|
|
834
|
+
return targets;
|
|
835
|
+
});
|
|
836
|
+
},
|
|
837
|
+
async consolidate(input, options) {
|
|
838
|
+
return runPostgresOperation(async () => {
|
|
839
|
+
throwIfAborted(options?.signal);
|
|
840
|
+
return { supersededIds: await handle.db.transaction(async (tx) => {
|
|
841
|
+
throwIfAborted(options?.signal);
|
|
842
|
+
const lockedMemories = await lockConsolidationMemories(tx, input.entityId, input.survivorId, input.supersededIds);
|
|
843
|
+
throwIfAborted(options?.signal);
|
|
844
|
+
const survivor = lockedMemories.find((memory) => memory.id === input.survivorId);
|
|
845
|
+
if (!survivor || survivor.supersededBy !== null) throw new MnemocyteError("Consolidation survivor changed before mutation.", "CONFLICT");
|
|
846
|
+
const targets = lockedMemories.filter((memory) => memory.id !== input.survivorId);
|
|
847
|
+
if (targets.some((target) => target.supersededBy !== null && target.supersededBy !== input.survivorId)) throw new MnemocyteError("Superseded memory already belongs to a different survivor.", "CONFLICT");
|
|
848
|
+
const activeIds = targets.filter((target) => target.supersededBy === null).map((target) => target.id);
|
|
849
|
+
const updated = await markMemoriesSuperseded(tx, {
|
|
850
|
+
survivorId: input.survivorId,
|
|
851
|
+
entityId: input.entityId,
|
|
852
|
+
ids: activeIds,
|
|
853
|
+
now: input.now
|
|
854
|
+
});
|
|
855
|
+
throwIfAborted(options?.signal);
|
|
856
|
+
const ids = updated.map((row) => row.id);
|
|
857
|
+
if (input.auditEnabled) for (const id of ids) {
|
|
858
|
+
throwIfAborted(options?.signal);
|
|
859
|
+
await insertEvent(tx, {
|
|
860
|
+
id: createEventId(),
|
|
861
|
+
entityId: input.entityId,
|
|
862
|
+
description: "memory.superseded",
|
|
863
|
+
metadata: {
|
|
864
|
+
memoryId: id,
|
|
865
|
+
supersededBy: input.survivorId
|
|
866
|
+
},
|
|
867
|
+
timestamp: input.now
|
|
868
|
+
});
|
|
869
|
+
throwIfAborted(options?.signal);
|
|
870
|
+
}
|
|
871
|
+
if (input.mergeTags && updated.length > 0) {
|
|
872
|
+
const mergedTags = new Set(survivor.tags);
|
|
873
|
+
for (const row of updated) {
|
|
874
|
+
throwIfAborted(options?.signal);
|
|
875
|
+
for (const tag of row.tags) mergedTags.add(tag);
|
|
876
|
+
}
|
|
877
|
+
if (mergedTags.size !== survivor.tags.length) {
|
|
878
|
+
throwIfAborted(options?.signal);
|
|
879
|
+
await setMemoryTags(tx, {
|
|
880
|
+
entityId: input.entityId,
|
|
881
|
+
memoryId: input.survivorId,
|
|
882
|
+
tags: [...mergedTags],
|
|
883
|
+
now: input.now
|
|
884
|
+
});
|
|
885
|
+
throwIfAborted(options?.signal);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
throwIfAborted(options?.signal);
|
|
889
|
+
return ids;
|
|
890
|
+
}) };
|
|
891
|
+
});
|
|
892
|
+
},
|
|
893
|
+
async stats(input, now) {
|
|
894
|
+
return runPostgresOperation(async () => {
|
|
895
|
+
if (input?.entityId) {
|
|
896
|
+
const counts = await getEntityMemoryStatsCounts(handle.db, input.entityId, now);
|
|
897
|
+
return {
|
|
898
|
+
entityId: input.entityId,
|
|
899
|
+
...counts
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
return await getGlobalMemoryStatsCounts(handle.db, now);
|
|
903
|
+
});
|
|
904
|
+
},
|
|
905
|
+
async close() {
|
|
906
|
+
return runPostgresOperation(() => handle.close());
|
|
907
|
+
}
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
//#endregion
|
|
911
|
+
export { createCallerOwnedDatabaseHandle as n, createDatabase as r, createPostgresStore as t };
|
|
912
|
+
|
|
913
|
+
//# sourceMappingURL=postgres-BHQ1pj16.mjs.map
|