@modusensus/dsh-mneme 0.1.4 → 0.1.5
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/lib/api.js +92 -3
- package/lib/client.js +135 -60
- package/lib/embedding.js +97 -0
- package/lib/index.js +7 -1
- package/lib/service.js +17 -0
- package/lib/settings.js +22 -0
- package/lib/store.js +84 -5
- package/package.json +58 -58
- package/src/api.js +92 -3
- package/src/embedding.js +97 -0
- package/src/index.js +7 -1
- package/src/service.js +17 -0
- package/src/settings.js +22 -0
- package/src/store.js +84 -5
package/src/store.js
CHANGED
|
@@ -12,6 +12,7 @@ CREATE TABLE IF NOT EXISTS memories (
|
|
|
12
12
|
forgotten INTEGER NOT NULL DEFAULT 0,
|
|
13
13
|
archived INTEGER NOT NULL DEFAULT 0,
|
|
14
14
|
source TEXT,
|
|
15
|
+
embedding TEXT,
|
|
15
16
|
created_at TEXT NOT NULL,
|
|
16
17
|
updated_at TEXT NOT NULL
|
|
17
18
|
);
|
|
@@ -64,11 +65,14 @@ export function createStore(path) {
|
|
|
64
65
|
db.exec("PRAGMA journal_mode = WAL;");
|
|
65
66
|
db.exec(SCHEMA);
|
|
66
67
|
|
|
67
|
-
// Schema
|
|
68
|
+
// Schema migrations for legacy databases (idempotent).
|
|
68
69
|
const columns = db.prepare("PRAGMA table_info(memories)").all().map((c) => c.name);
|
|
69
70
|
if (!columns.includes("archived")) {
|
|
70
71
|
db.exec("ALTER TABLE memories ADD COLUMN archived INTEGER NOT NULL DEFAULT 0");
|
|
71
72
|
}
|
|
73
|
+
if (!columns.includes("embedding")) {
|
|
74
|
+
db.exec("ALTER TABLE memories ADD COLUMN embedding TEXT");
|
|
75
|
+
}
|
|
72
76
|
|
|
73
77
|
// Per-instance monotonic timestamp guard: consecutive writes within the same
|
|
74
78
|
// millisecond must still produce strictly increasing timestamps (test asserts
|
|
@@ -117,10 +121,13 @@ export function createStore(path) {
|
|
|
117
121
|
const now = nowIso();
|
|
118
122
|
const tags = JSON.stringify(memory.tags ?? []);
|
|
119
123
|
const importance = Number.isInteger(memory.importance) ? memory.importance : 3;
|
|
124
|
+
const embedding = Array.isArray(memory.embedding) && memory.embedding.length
|
|
125
|
+
? JSON.stringify(memory.embedding)
|
|
126
|
+
: null;
|
|
120
127
|
db.prepare(
|
|
121
|
-
`INSERT INTO memories (id, type, title, content, tags, importance, forgotten, source, created_at, updated_at)
|
|
122
|
-
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?)`
|
|
123
|
-
).run(id, type, memory.title, memory.content, tags, importance, memory.source ?? null, now, now);
|
|
128
|
+
`INSERT INTO memories (id, type, title, content, tags, importance, forgotten, source, embedding, created_at, updated_at)
|
|
129
|
+
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?)`
|
|
130
|
+
).run(id, type, memory.title, memory.content, tags, importance, memory.source ?? null, embedding, now, now);
|
|
124
131
|
return getById(id);
|
|
125
132
|
}
|
|
126
133
|
|
|
@@ -133,8 +140,11 @@ export function createStore(path) {
|
|
|
133
140
|
throw new Error("tags must be an array");
|
|
134
141
|
}
|
|
135
142
|
const now = nowIso();
|
|
143
|
+
const embedding = patch.embedding !== undefined
|
|
144
|
+
? (Array.isArray(patch.embedding) && patch.embedding.length ? JSON.stringify(patch.embedding) : null)
|
|
145
|
+
: existing.embedding ?? null;
|
|
136
146
|
db.prepare(
|
|
137
|
-
`UPDATE memories SET type=?, title=?, content=?, tags=?, importance=?, source=?, updated_at=? WHERE id=?`
|
|
147
|
+
`UPDATE memories SET type=?, title=?, content=?, tags=?, importance=?, source=?, embedding=?, updated_at=? WHERE id=?`
|
|
138
148
|
).run(
|
|
139
149
|
type,
|
|
140
150
|
patch.title ?? existing.title,
|
|
@@ -142,6 +152,7 @@ export function createStore(path) {
|
|
|
142
152
|
JSON.stringify(patch.tags ?? existing.tags),
|
|
143
153
|
Number.isInteger(patch.importance) ? patch.importance : existing.importance,
|
|
144
154
|
patch.source !== undefined ? patch.source : (existing.source ?? null),
|
|
155
|
+
embedding,
|
|
145
156
|
now,
|
|
146
157
|
id
|
|
147
158
|
);
|
|
@@ -190,6 +201,27 @@ export function createStore(path) {
|
|
|
190
201
|
return rows.map(toRow);
|
|
191
202
|
}
|
|
192
203
|
|
|
204
|
+
/** Set (or clear with null) the embedding vector of a memory. */
|
|
205
|
+
function setEmbedding(id, vector) {
|
|
206
|
+
const json = Array.isArray(vector) && vector.length ? JSON.stringify(vector) : null;
|
|
207
|
+
db.prepare("UPDATE memories SET embedding = ? WHERE id = ?").run(json, id);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function embeddedCount() {
|
|
211
|
+
return db.prepare(
|
|
212
|
+
"SELECT count(*) AS c FROM memories WHERE embedding IS NOT NULL AND embedding != ''"
|
|
213
|
+
).get().c;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Candidate rows still missing an embedding, for incremental re-indexing. */
|
|
217
|
+
function needsEmbedding(limit = 50) {
|
|
218
|
+
return db.prepare(
|
|
219
|
+
`SELECT id, title, content FROM memories
|
|
220
|
+
WHERE embedding IS NULL OR embedding = ''
|
|
221
|
+
ORDER BY updated_at DESC LIMIT ?`
|
|
222
|
+
).all(limit);
|
|
223
|
+
}
|
|
224
|
+
|
|
193
225
|
function search(query, { limit = 20, includeArchived = false } = {}) {
|
|
194
226
|
const q = String(query).trim();
|
|
195
227
|
if (!q) return [];
|
|
@@ -212,6 +244,49 @@ export function createStore(path) {
|
|
|
212
244
|
return rows.map(toRow);
|
|
213
245
|
}
|
|
214
246
|
|
|
247
|
+
// --- vector search ------------------------------------------------------
|
|
248
|
+
|
|
249
|
+
function cosine(a, b) {
|
|
250
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length || !a.length) return 0;
|
|
251
|
+
let dot = 0;
|
|
252
|
+
let na = 0;
|
|
253
|
+
let nb = 0;
|
|
254
|
+
for (let i = 0; i < a.length; i++) {
|
|
255
|
+
dot += a[i] * b[i];
|
|
256
|
+
na += a[i] * a[i];
|
|
257
|
+
nb += b[i] * b[i];
|
|
258
|
+
}
|
|
259
|
+
if (na === 0 || nb === 0) return 0;
|
|
260
|
+
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Brute-force cosine similarity over embedded rows. Returns rows decorated
|
|
265
|
+
* with a `score` (0..1). Only rows with a stored embedding participate.
|
|
266
|
+
*/
|
|
267
|
+
function searchVector(vector, { limit = 20, includeArchived = false, threshold = 0 } = {}) {
|
|
268
|
+
if (!Array.isArray(vector) || !vector.length) return [];
|
|
269
|
+
const archivedFilter = includeArchived ? "" : "archived = 0 AND ";
|
|
270
|
+
const rows = db.prepare(
|
|
271
|
+
`SELECT * FROM memories
|
|
272
|
+
WHERE ${archivedFilter}forgotten = 0 AND embedding IS NOT NULL AND embedding != ''`
|
|
273
|
+
).all();
|
|
274
|
+
const scored = [];
|
|
275
|
+
for (const row of rows) {
|
|
276
|
+
let v;
|
|
277
|
+
try {
|
|
278
|
+
v = JSON.parse(row.embedding);
|
|
279
|
+
} catch {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
const score = cosine(vector, v);
|
|
283
|
+
if (score >= threshold) scored.push({ row, score });
|
|
284
|
+
}
|
|
285
|
+
scored.sort((a, b) => b.score - a.score);
|
|
286
|
+
const { limit: lim } = sanitizePage(limit, 0, 20);
|
|
287
|
+
return scored.slice(0, lim).map(({ row, score }) => ({ ...toRow(row), score }));
|
|
288
|
+
}
|
|
289
|
+
|
|
215
290
|
return {
|
|
216
291
|
db,
|
|
217
292
|
count,
|
|
@@ -224,6 +299,10 @@ export function createStore(path) {
|
|
|
224
299
|
list,
|
|
225
300
|
all,
|
|
226
301
|
search,
|
|
302
|
+
setEmbedding,
|
|
303
|
+
embeddedCount,
|
|
304
|
+
needsEmbedding,
|
|
305
|
+
searchVector,
|
|
227
306
|
close() {
|
|
228
307
|
db.close();
|
|
229
308
|
}
|