mcp-super-memory 0.4.4
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/LICENSE +21 -0
- package/README.md +290 -0
- package/dist/embedding.d.ts +5 -0
- package/dist/embedding.d.ts.map +1 -0
- package/dist/embedding.js +59 -0
- package/dist/embedding.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/memoryGraph.d.ts +53 -0
- package/dist/memoryGraph.d.ts.map +1 -0
- package/dist/memoryGraph.js +728 -0
- package/dist/memoryGraph.js.map +1 -0
- package/dist/server.d.ts +38 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +346 -0
- package/dist/server.js.map +1 -0
- package/dist/types.d.ts +29 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +35 -0
|
@@ -0,0 +1,728 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir, appendFile } from "fs/promises";
|
|
2
|
+
import { randomBytes } from "crypto";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
import { Mutex } from "async-mutex";
|
|
6
|
+
import { embedTextAsync, EMBEDDING_BACKEND } from "./embedding.js";
|
|
7
|
+
const DATA_DIR = process.env.SUPER_MEMORY_DATA_DIR ?? join(homedir(), ".super-memory");
|
|
8
|
+
const GRAPH_FILE = join(DATA_DIR, "graph.json");
|
|
9
|
+
const CONVERSATIONS_DIR = join(DATA_DIR, "conversations");
|
|
10
|
+
const KEY_MERGE_THRESHOLD = 0.85;
|
|
11
|
+
const MEMORY_DEDUP_THRESHOLD = 0.9;
|
|
12
|
+
const KEY_AUTO_LINK_THRESHOLD = 0.5;
|
|
13
|
+
const DEPTH_INCREMENT = 0.05;
|
|
14
|
+
const DEPTH_MAX = 1.0;
|
|
15
|
+
const DEPTH_DEEP_THRESHOLD = 0.7;
|
|
16
|
+
// ── Vector math ──
|
|
17
|
+
function cosineSim(a, b) {
|
|
18
|
+
let dot = 0, normA = 0, normB = 0;
|
|
19
|
+
for (let i = 0; i < a.length; i++) {
|
|
20
|
+
dot += a[i] * b[i];
|
|
21
|
+
normA += a[i] * a[i];
|
|
22
|
+
normB += b[i] * b[i];
|
|
23
|
+
}
|
|
24
|
+
const norm = Math.sqrt(normA) * Math.sqrt(normB);
|
|
25
|
+
return norm === 0 ? 0 : dot / norm;
|
|
26
|
+
}
|
|
27
|
+
function batchCosineSim(query, matrix) {
|
|
28
|
+
if (matrix.length === 0)
|
|
29
|
+
return [];
|
|
30
|
+
return matrix.map((row) => cosineSim(query, row));
|
|
31
|
+
}
|
|
32
|
+
// ── Utils ──
|
|
33
|
+
function uid() {
|
|
34
|
+
return randomBytes(6).toString("hex");
|
|
35
|
+
}
|
|
36
|
+
export function sanitizeKeys(keys) {
|
|
37
|
+
let arr;
|
|
38
|
+
if (typeof keys === "string") {
|
|
39
|
+
try {
|
|
40
|
+
arr = JSON.parse(keys);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
arr = [keys];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
else if (Array.isArray(keys)) {
|
|
47
|
+
arr = keys;
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
return arr
|
|
53
|
+
.filter((k) => typeof k === "string" && k.trim().length >= 2)
|
|
54
|
+
.map((k) => k.trim());
|
|
55
|
+
}
|
|
56
|
+
// ── MemoryGraph ──
|
|
57
|
+
export class MemoryGraph {
|
|
58
|
+
keys = {};
|
|
59
|
+
memories = {};
|
|
60
|
+
_keyToMems = {};
|
|
61
|
+
_memToKeys = {};
|
|
62
|
+
_supersededBy = {};
|
|
63
|
+
_storedDim = null;
|
|
64
|
+
_lock = new Mutex();
|
|
65
|
+
_dirty = false;
|
|
66
|
+
static HOP_DECAY = 0.3;
|
|
67
|
+
static TIME_HALF_LIFE = 30 * 24 * 3600;
|
|
68
|
+
get linkCount() {
|
|
69
|
+
return Object.values(this._keyToMems).reduce((sum, mids) => sum + mids.size, 0);
|
|
70
|
+
}
|
|
71
|
+
_link(keyId, memId) {
|
|
72
|
+
if (!this._keyToMems[keyId])
|
|
73
|
+
this._keyToMems[keyId] = new Set();
|
|
74
|
+
this._keyToMems[keyId].add(memId);
|
|
75
|
+
if (!this._memToKeys[memId])
|
|
76
|
+
this._memToKeys[memId] = new Set();
|
|
77
|
+
this._memToKeys[memId].add(keyId);
|
|
78
|
+
}
|
|
79
|
+
_hasLink(keyId, memId) {
|
|
80
|
+
return this._keyToMems[keyId]?.has(memId) ?? false;
|
|
81
|
+
}
|
|
82
|
+
_unlinkMemory(memId) {
|
|
83
|
+
const kids = this._memToKeys[memId];
|
|
84
|
+
if (kids) {
|
|
85
|
+
for (const kid of kids) {
|
|
86
|
+
const mems = this._keyToMems[kid];
|
|
87
|
+
if (mems) {
|
|
88
|
+
mems.delete(memId);
|
|
89
|
+
if (mems.size === 0)
|
|
90
|
+
delete this._keyToMems[kid];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
delete this._memToKeys[memId];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
_pruneOrphanKeys() {
|
|
97
|
+
for (const kid of Object.keys(this.keys)) {
|
|
98
|
+
const mems = this._keyToMems[kid];
|
|
99
|
+
if (!mems || mems.size === 0)
|
|
100
|
+
delete this.keys[kid];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
_checkDim(embedding) {
|
|
104
|
+
const dim = embedding.length;
|
|
105
|
+
if (this._storedDim === null) {
|
|
106
|
+
this._storedDim = dim;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (dim !== this._storedDim) {
|
|
110
|
+
throw new Error(`Embedding dimension mismatch: existing data uses ${this._storedDim}-dim, ` +
|
|
111
|
+
`current backend (${EMBEDDING_BACKEND}) produces ${dim}-dim.\n` +
|
|
112
|
+
`To switch backends, delete ~/.super-memory/graph.json first.`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
_isExpired(mem) {
|
|
116
|
+
return mem.ttl != null && Date.now() / 1000 > mem.ttl;
|
|
117
|
+
}
|
|
118
|
+
_timeFactor(mem) {
|
|
119
|
+
const age = Date.now() / 1000 - mem.created_at;
|
|
120
|
+
const decayRate = 1.0 - mem.depth * 0.7;
|
|
121
|
+
const decay = Math.exp((-age * decayRate) / MemoryGraph.TIME_HALF_LIFE);
|
|
122
|
+
return 0.5 + 0.5 * decay;
|
|
123
|
+
}
|
|
124
|
+
_keyIdf(keyId) {
|
|
125
|
+
const freq = this._keyToMems[keyId]?.size ?? 0;
|
|
126
|
+
if (freq <= 1)
|
|
127
|
+
return 1.0;
|
|
128
|
+
let idf = 1.0 / freq;
|
|
129
|
+
const kt = this.keys[keyId]?.key_type;
|
|
130
|
+
if (kt === "name" || kt === "proper_noun")
|
|
131
|
+
idf *= 0.5;
|
|
132
|
+
return idf;
|
|
133
|
+
}
|
|
134
|
+
_findDuplicate(embedding) {
|
|
135
|
+
const activeMems = Object.entries(this.memories).filter(([mid]) => !(mid in this._supersededBy));
|
|
136
|
+
if (activeMems.length === 0)
|
|
137
|
+
return null;
|
|
138
|
+
const matrix = activeMems.map(([, mem]) => mem.embedding);
|
|
139
|
+
const sims = batchCosineSim(embedding, matrix);
|
|
140
|
+
let bestIdx = 0, bestSim = -Infinity;
|
|
141
|
+
for (let i = 0; i < sims.length; i++) {
|
|
142
|
+
if (sims[i] > bestSim) {
|
|
143
|
+
bestSim = sims[i];
|
|
144
|
+
bestIdx = i;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return bestSim >= MEMORY_DEDUP_THRESHOLD ? activeMems[bestIdx][0] : null;
|
|
148
|
+
}
|
|
149
|
+
_autoLinkKeys(memId, embedding) {
|
|
150
|
+
const keyIds = Object.keys(this.keys);
|
|
151
|
+
if (keyIds.length === 0)
|
|
152
|
+
return;
|
|
153
|
+
const matrix = keyIds.map((kid) => this.keys[kid].embedding);
|
|
154
|
+
const sims = batchCosineSim(embedding, matrix);
|
|
155
|
+
for (let i = 0; i < keyIds.length; i++) {
|
|
156
|
+
if (sims[i] >= KEY_AUTO_LINK_THRESHOLD && !this._hasLink(keyIds[i], memId)) {
|
|
157
|
+
this._link(keyIds[i], memId);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
getKeysForMemory(memId) {
|
|
162
|
+
const kids = this._memToKeys[memId];
|
|
163
|
+
if (!kids)
|
|
164
|
+
return [];
|
|
165
|
+
return [...kids]
|
|
166
|
+
.filter((kid) => kid in this.keys)
|
|
167
|
+
.map((kid) => this.keys[kid].concept);
|
|
168
|
+
}
|
|
169
|
+
// ── I/O ──
|
|
170
|
+
async load() {
|
|
171
|
+
let raw;
|
|
172
|
+
try {
|
|
173
|
+
const text = await readFile(GRAPH_FILE, "utf-8");
|
|
174
|
+
raw = JSON.parse(text);
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
for (const [kid, k] of Object.entries(raw.keys ?? {})) {
|
|
180
|
+
this.keys[kid] = k;
|
|
181
|
+
}
|
|
182
|
+
for (const [mid, m] of Object.entries(raw.memories ?? {})) {
|
|
183
|
+
const defaults = {
|
|
184
|
+
depth: 0.0,
|
|
185
|
+
access_count: 0,
|
|
186
|
+
last_accessed: 0,
|
|
187
|
+
namespace: "default",
|
|
188
|
+
ttl: null,
|
|
189
|
+
links: [],
|
|
190
|
+
source: null,
|
|
191
|
+
supersedes: null,
|
|
192
|
+
};
|
|
193
|
+
const mem = { ...defaults, ...m };
|
|
194
|
+
if (!mem.embedding || mem.embedding.length === 0) {
|
|
195
|
+
mem.embedding = await embedTextAsync(mem.content);
|
|
196
|
+
}
|
|
197
|
+
this.memories[mid] = mem;
|
|
198
|
+
}
|
|
199
|
+
if (Object.keys(this.memories).length > 0) {
|
|
200
|
+
const firstMem = Object.values(this.memories)[0];
|
|
201
|
+
this._storedDim = firstMem.embedding.length;
|
|
202
|
+
}
|
|
203
|
+
for (const lnk of raw.links ?? []) {
|
|
204
|
+
this._link(lnk.key_id, lnk.memory_id);
|
|
205
|
+
}
|
|
206
|
+
for (const [mid, mem] of Object.entries(this.memories)) {
|
|
207
|
+
if (mem.supersedes) {
|
|
208
|
+
this._supersededBy[mem.supersedes] = mid;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
console.error(`[graph] loaded ${Object.keys(this.keys).length} keys, ` +
|
|
212
|
+
`${Object.keys(this.memories).length} memories, ${this.linkCount} links`);
|
|
213
|
+
}
|
|
214
|
+
async save() {
|
|
215
|
+
await mkdir(DATA_DIR, { recursive: true });
|
|
216
|
+
const links = [];
|
|
217
|
+
for (const [kid, mids] of Object.entries(this._keyToMems)) {
|
|
218
|
+
for (const mid of mids) {
|
|
219
|
+
links.push({ key_id: kid, memory_id: mid });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const data = {
|
|
223
|
+
keys: this.keys,
|
|
224
|
+
memories: this.memories,
|
|
225
|
+
links,
|
|
226
|
+
};
|
|
227
|
+
await writeFile(GRAPH_FILE, JSON.stringify(data, null, 2), "utf-8");
|
|
228
|
+
this._dirty = false;
|
|
229
|
+
}
|
|
230
|
+
markDirty() {
|
|
231
|
+
this._dirty = true;
|
|
232
|
+
}
|
|
233
|
+
async flush() {
|
|
234
|
+
if (this._dirty)
|
|
235
|
+
await this.save();
|
|
236
|
+
}
|
|
237
|
+
// ── Key management ──
|
|
238
|
+
async findOrCreateKey(concept, keyType = "concept") {
|
|
239
|
+
if (keyType === "name" || keyType === "proper_noun") {
|
|
240
|
+
for (const [kid, key] of Object.entries(this.keys)) {
|
|
241
|
+
if (key.concept === concept && key.key_type === keyType)
|
|
242
|
+
return kid;
|
|
243
|
+
}
|
|
244
|
+
const kid = uid();
|
|
245
|
+
this.keys[kid] = {
|
|
246
|
+
id: kid,
|
|
247
|
+
concept,
|
|
248
|
+
embedding: await embedTextAsync(concept),
|
|
249
|
+
key_type: keyType,
|
|
250
|
+
};
|
|
251
|
+
return kid;
|
|
252
|
+
}
|
|
253
|
+
const emb = await embedTextAsync(concept);
|
|
254
|
+
const conceptKeys = Object.entries(this.keys).filter(([, k]) => k.key_type === "concept");
|
|
255
|
+
if (conceptKeys.length > 0) {
|
|
256
|
+
const matrix = conceptKeys.map(([, k]) => k.embedding);
|
|
257
|
+
const sims = batchCosineSim(emb, matrix);
|
|
258
|
+
let bestIdx = 0, bestSim = -Infinity;
|
|
259
|
+
for (let i = 0; i < sims.length; i++) {
|
|
260
|
+
if (sims[i] > bestSim) {
|
|
261
|
+
bestSim = sims[i];
|
|
262
|
+
bestIdx = i;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (bestSim >= KEY_MERGE_THRESHOLD)
|
|
266
|
+
return conceptKeys[bestIdx][0];
|
|
267
|
+
}
|
|
268
|
+
const kid = uid();
|
|
269
|
+
this.keys[kid] = { id: kid, concept, embedding: emb, key_type: "concept" };
|
|
270
|
+
return kid;
|
|
271
|
+
}
|
|
272
|
+
// ── Add ──
|
|
273
|
+
async add(content, keyConcepts, options = {}) {
|
|
274
|
+
const embedding = await embedTextAsync(content); // outside lock
|
|
275
|
+
let dupId = null;
|
|
276
|
+
await this._lock.runExclusive(async () => {
|
|
277
|
+
this._checkDim(embedding);
|
|
278
|
+
dupId = this._findDuplicate(embedding);
|
|
279
|
+
});
|
|
280
|
+
if (dupId !== null) {
|
|
281
|
+
const newId = await this.supersede(dupId, content, {
|
|
282
|
+
keyConcepts,
|
|
283
|
+
keyTypes: options.keyTypes ?? undefined,
|
|
284
|
+
source: options.source,
|
|
285
|
+
namespace: options.namespace,
|
|
286
|
+
relatedTo: options.relatedTo,
|
|
287
|
+
});
|
|
288
|
+
return [newId, true];
|
|
289
|
+
}
|
|
290
|
+
let resultMid = "";
|
|
291
|
+
await this._lock.runExclusive(async () => {
|
|
292
|
+
const mid = uid();
|
|
293
|
+
resultMid = mid;
|
|
294
|
+
const now = Date.now() / 1000;
|
|
295
|
+
const expiresAt = options.ttlSeconds != null ? now + options.ttlSeconds : null;
|
|
296
|
+
const validLinks = (options.relatedTo ?? []).filter((lid) => lid in this.memories);
|
|
297
|
+
this.memories[mid] = {
|
|
298
|
+
id: mid,
|
|
299
|
+
content,
|
|
300
|
+
embedding,
|
|
301
|
+
created_at: now,
|
|
302
|
+
source: options.source ?? null,
|
|
303
|
+
supersedes: null,
|
|
304
|
+
depth: 0.0,
|
|
305
|
+
access_count: 0,
|
|
306
|
+
last_accessed: now,
|
|
307
|
+
namespace: options.namespace ?? "default",
|
|
308
|
+
ttl: expiresAt,
|
|
309
|
+
links: validLinks,
|
|
310
|
+
};
|
|
311
|
+
const sanitized = sanitizeKeys(keyConcepts);
|
|
312
|
+
const keyTypes = options.keyTypes ?? {};
|
|
313
|
+
for (const concept of sanitized) {
|
|
314
|
+
const kt = (keyTypes[concept] ?? "concept");
|
|
315
|
+
const kid = await this.findOrCreateKey(concept, kt);
|
|
316
|
+
if (!this._hasLink(kid, mid))
|
|
317
|
+
this._link(kid, mid);
|
|
318
|
+
}
|
|
319
|
+
this._autoLinkKeys(mid, embedding);
|
|
320
|
+
await this.save();
|
|
321
|
+
});
|
|
322
|
+
return [resultMid, false];
|
|
323
|
+
}
|
|
324
|
+
// ── Supersede ──
|
|
325
|
+
async supersede(oldId, newContent, options = {}) {
|
|
326
|
+
const newEmbedding = await embedTextAsync(newContent); // outside lock
|
|
327
|
+
let resultMid = "";
|
|
328
|
+
await this._lock.runExclusive(async () => {
|
|
329
|
+
if (!(oldId in this.memories)) {
|
|
330
|
+
throw new Error(`Memory ${oldId} not found`);
|
|
331
|
+
}
|
|
332
|
+
const old = this.memories[oldId];
|
|
333
|
+
// Chain cleanup: keep depth max 1 (new → old; grandparent deleted)
|
|
334
|
+
const grandparentId = old.supersedes;
|
|
335
|
+
if (grandparentId && grandparentId in this.memories) {
|
|
336
|
+
delete this.memories[grandparentId];
|
|
337
|
+
this._unlinkMemory(grandparentId);
|
|
338
|
+
delete this._supersededBy[grandparentId];
|
|
339
|
+
this._pruneOrphanKeys();
|
|
340
|
+
}
|
|
341
|
+
const mid = uid();
|
|
342
|
+
resultMid = mid;
|
|
343
|
+
const now = Date.now() / 1000;
|
|
344
|
+
const ns = options.namespace ?? old.namespace;
|
|
345
|
+
const validLinks = (options.relatedTo ?? []).filter((lid) => lid in this.memories);
|
|
346
|
+
this.memories[mid] = {
|
|
347
|
+
id: mid,
|
|
348
|
+
content: newContent,
|
|
349
|
+
embedding: newEmbedding,
|
|
350
|
+
created_at: now,
|
|
351
|
+
source: options.source ?? null,
|
|
352
|
+
supersedes: oldId,
|
|
353
|
+
depth: 0.0,
|
|
354
|
+
access_count: 0,
|
|
355
|
+
last_accessed: now,
|
|
356
|
+
namespace: ns,
|
|
357
|
+
ttl: old.ttl,
|
|
358
|
+
links: validLinks,
|
|
359
|
+
};
|
|
360
|
+
// Weaken old memory depth
|
|
361
|
+
old.depth =
|
|
362
|
+
old.depth >= DEPTH_DEEP_THRESHOLD
|
|
363
|
+
? old.depth * 0.8
|
|
364
|
+
: old.depth * 0.3;
|
|
365
|
+
this._supersededBy[oldId] = mid;
|
|
366
|
+
const keyConcepts = options.keyConcepts;
|
|
367
|
+
if (keyConcepts && keyConcepts.length > 0) {
|
|
368
|
+
const sanitized = sanitizeKeys(keyConcepts);
|
|
369
|
+
const keyTypes = options.keyTypes ?? {};
|
|
370
|
+
for (const concept of sanitized) {
|
|
371
|
+
const kt = (keyTypes[concept] ?? "concept");
|
|
372
|
+
const kid = await this.findOrCreateKey(concept, kt);
|
|
373
|
+
this._link(kid, mid);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
// Copy old links (snapshot to avoid mutation during iteration)
|
|
378
|
+
for (const kid of [...(this._memToKeys[oldId] ?? new Set())]) {
|
|
379
|
+
this._link(kid, mid);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
this._autoLinkKeys(mid, newEmbedding);
|
|
383
|
+
await this.save();
|
|
384
|
+
});
|
|
385
|
+
return resultMid;
|
|
386
|
+
}
|
|
387
|
+
// ── Recall ──
|
|
388
|
+
async recall(query, topK = 5, namespace, expand = false) {
|
|
389
|
+
if (Object.keys(this.memories).length === 0)
|
|
390
|
+
return [];
|
|
391
|
+
const qEmb = await embedTextAsync(query); // outside lock
|
|
392
|
+
this._checkDim(qEmb);
|
|
393
|
+
const results = [];
|
|
394
|
+
await this._lock.runExclusive(async () => {
|
|
395
|
+
const queryLower = query.toLowerCase().trim();
|
|
396
|
+
const memScores = {};
|
|
397
|
+
const memMatchedKeys = {};
|
|
398
|
+
const memHop = {};
|
|
399
|
+
const skip = (mid) => {
|
|
400
|
+
if (!(mid in this.memories))
|
|
401
|
+
return true;
|
|
402
|
+
const mem = this.memories[mid];
|
|
403
|
+
if (this._isExpired(mem))
|
|
404
|
+
return true;
|
|
405
|
+
if (namespace && mem.namespace !== namespace)
|
|
406
|
+
return true;
|
|
407
|
+
if (mid in this._supersededBy)
|
|
408
|
+
return true;
|
|
409
|
+
return false;
|
|
410
|
+
};
|
|
411
|
+
// ── Path A: Key batch matching → links → memories ──
|
|
412
|
+
const keyIds = Object.keys(this.keys);
|
|
413
|
+
const keySims = keyIds.length > 0
|
|
414
|
+
? batchCosineSim(qEmb, keyIds.map((kid) => this.keys[kid].embedding))
|
|
415
|
+
: [];
|
|
416
|
+
const keyScores = [];
|
|
417
|
+
for (let i = 0; i < keyIds.length; i++) {
|
|
418
|
+
const kid = keyIds[i];
|
|
419
|
+
const key = this.keys[kid];
|
|
420
|
+
if (key.key_type === "name" || key.key_type === "proper_noun") {
|
|
421
|
+
if (queryLower.includes(key.concept.toLowerCase())) {
|
|
422
|
+
keyScores.push([1.0, kid]);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
else if (keySims[i] >= 0.35) {
|
|
426
|
+
keyScores.push([keySims[i], kid]);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
keyScores.sort((a, b) => b[0] - a[0]);
|
|
430
|
+
for (const [keySim, kid] of keyScores.slice(0, 10)) {
|
|
431
|
+
const idf = this._keyIdf(kid);
|
|
432
|
+
for (const memId of this._keyToMems[kid] ?? new Set()) {
|
|
433
|
+
if (skip(memId))
|
|
434
|
+
continue;
|
|
435
|
+
const mem = this.memories[memId];
|
|
436
|
+
const depthFactor = 0.9 + mem.depth * 0.1;
|
|
437
|
+
const tf = this._timeFactor(mem);
|
|
438
|
+
const score = keySim * idf * depthFactor * tf;
|
|
439
|
+
memScores[memId] = (memScores[memId] ?? 0) + score;
|
|
440
|
+
if (!memMatchedKeys[memId])
|
|
441
|
+
memMatchedKeys[memId] = [];
|
|
442
|
+
memMatchedKeys[memId].push(this.keys[kid].concept);
|
|
443
|
+
memHop[memId] = 1;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
// ── Path B: Content batch direct matching ──
|
|
447
|
+
const memIds = Object.keys(this.memories);
|
|
448
|
+
if (memIds.length > 0) {
|
|
449
|
+
const contentSims = batchCosineSim(qEmb, memIds.map((mid) => this.memories[mid].embedding));
|
|
450
|
+
for (let i = 0; i < memIds.length; i++) {
|
|
451
|
+
const mid = memIds[i];
|
|
452
|
+
if (skip(mid))
|
|
453
|
+
continue;
|
|
454
|
+
const cSim = contentSims[i];
|
|
455
|
+
if (cSim >= 0.35) {
|
|
456
|
+
const mem = this.memories[mid];
|
|
457
|
+
const depthFactor = 0.9 + mem.depth * 0.1;
|
|
458
|
+
const tf = this._timeFactor(mem);
|
|
459
|
+
const contentScore = cSim * depthFactor * tf * 0.8;
|
|
460
|
+
if (mid in memScores) {
|
|
461
|
+
memScores[mid] += contentScore * 0.2;
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
memScores[mid] = contentScore;
|
|
465
|
+
}
|
|
466
|
+
if (!memMatchedKeys[mid])
|
|
467
|
+
memMatchedKeys[mid] = [];
|
|
468
|
+
memMatchedKeys[mid].push("(content)");
|
|
469
|
+
if (!(mid in memHop))
|
|
470
|
+
memHop[mid] = 1;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
// ── 2-hop: via shared keys ──
|
|
475
|
+
for (const mid of Object.keys(memScores)) {
|
|
476
|
+
const hop1Score = memScores[mid];
|
|
477
|
+
for (const kid of this._memToKeys[mid] ?? new Set()) {
|
|
478
|
+
if (!(kid in this.keys))
|
|
479
|
+
continue;
|
|
480
|
+
const concept = this.keys[kid].concept;
|
|
481
|
+
const idf = this._keyIdf(kid);
|
|
482
|
+
for (const otherMid of this._keyToMems[kid] ?? new Set()) {
|
|
483
|
+
if (otherMid === mid || skip(otherMid))
|
|
484
|
+
continue;
|
|
485
|
+
const hop2Score = hop1Score * MemoryGraph.HOP_DECAY * idf;
|
|
486
|
+
memScores[otherMid] = (memScores[otherMid] ?? 0) + hop2Score;
|
|
487
|
+
if (!memMatchedKeys[otherMid])
|
|
488
|
+
memMatchedKeys[otherMid] = [];
|
|
489
|
+
memMatchedKeys[otherMid].push(`${concept}(via)`);
|
|
490
|
+
if (!(otherMid in memHop))
|
|
491
|
+
memHop[otherMid] = 2;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
// ── Explicit link traversal ──
|
|
496
|
+
for (const mid of Object.keys(memScores)) {
|
|
497
|
+
const hop1Score = memScores[mid];
|
|
498
|
+
const memObj = this.memories[mid];
|
|
499
|
+
if (!memObj)
|
|
500
|
+
continue;
|
|
501
|
+
for (const linkedId of memObj.links) {
|
|
502
|
+
if (linkedId === mid || skip(linkedId))
|
|
503
|
+
continue;
|
|
504
|
+
const linkScore = hop1Score * MemoryGraph.HOP_DECAY;
|
|
505
|
+
memScores[linkedId] = (memScores[linkedId] ?? 0) + linkScore;
|
|
506
|
+
if (!memMatchedKeys[linkedId])
|
|
507
|
+
memMatchedKeys[linkedId] = [];
|
|
508
|
+
memMatchedKeys[linkedId].push("(linked)");
|
|
509
|
+
if (!(linkedId in memHop))
|
|
510
|
+
memHop[linkedId] = 2;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
if (expand) {
|
|
514
|
+
for (const mid of Object.keys(memScores)) {
|
|
515
|
+
if ((memHop[mid] ?? 1) === 2)
|
|
516
|
+
memScores[mid] *= 0.7;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
const actualTopK = expand ? topK * 2 : topK;
|
|
520
|
+
const ranked = Object.entries(memScores)
|
|
521
|
+
.sort(([, a], [, b]) => b - a)
|
|
522
|
+
.slice(0, actualTopK);
|
|
523
|
+
for (const [mid, score] of ranked) {
|
|
524
|
+
const mem = this.memories[mid];
|
|
525
|
+
mem.depth = Math.min(mem.depth + DEPTH_INCREMENT, DEPTH_MAX);
|
|
526
|
+
mem.access_count += 1;
|
|
527
|
+
mem.last_accessed = Date.now() / 1000;
|
|
528
|
+
results.push({
|
|
529
|
+
id: mid,
|
|
530
|
+
content: mem.content,
|
|
531
|
+
keys: this.getKeysForMemory(mid),
|
|
532
|
+
matched_via: [...new Set(memMatchedKeys[mid] ?? [])],
|
|
533
|
+
hop: memHop[mid] ?? 1,
|
|
534
|
+
score: Math.round(score * 1000) / 1000,
|
|
535
|
+
depth: Math.round(mem.depth * 1000) / 1000,
|
|
536
|
+
access_count: mem.access_count,
|
|
537
|
+
source: mem.source,
|
|
538
|
+
supersedes: mem.supersedes,
|
|
539
|
+
superseded_by: this._supersededBy[mid] ?? null,
|
|
540
|
+
created_at: mem.created_at,
|
|
541
|
+
namespace: mem.namespace,
|
|
542
|
+
links: mem.links,
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
this.markDirty();
|
|
546
|
+
});
|
|
547
|
+
await this.flush(); // outside lock
|
|
548
|
+
return results;
|
|
549
|
+
}
|
|
550
|
+
// ── Related ──
|
|
551
|
+
getRelated(memoryId) {
|
|
552
|
+
if (!(memoryId in this.memories))
|
|
553
|
+
return [];
|
|
554
|
+
const related = {};
|
|
555
|
+
// Key-sharing
|
|
556
|
+
for (const kid of this._memToKeys[memoryId] ?? new Set()) {
|
|
557
|
+
const concept = this.keys[kid]?.concept ?? "?";
|
|
558
|
+
for (const mid of this._keyToMems[kid] ?? new Set()) {
|
|
559
|
+
if (mid === memoryId || !(mid in this.memories))
|
|
560
|
+
continue;
|
|
561
|
+
const mem = this.memories[mid];
|
|
562
|
+
if (this._isExpired(mem) || mid in this._supersededBy)
|
|
563
|
+
continue;
|
|
564
|
+
if (!related[mid]) {
|
|
565
|
+
related[mid] = {
|
|
566
|
+
id: mid,
|
|
567
|
+
content: mem.content,
|
|
568
|
+
shared_keys: [],
|
|
569
|
+
link_type: "key",
|
|
570
|
+
depth: Math.round(mem.depth * 1000) / 1000,
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
if (!related[mid].shared_keys.includes(concept)) {
|
|
574
|
+
related[mid].shared_keys.push(concept);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
// Explicit links (→)
|
|
579
|
+
const sourceMem = this.memories[memoryId];
|
|
580
|
+
for (const linkedId of sourceMem.links) {
|
|
581
|
+
if (!(linkedId in this.memories) || linkedId === memoryId)
|
|
582
|
+
continue;
|
|
583
|
+
const mem = this.memories[linkedId];
|
|
584
|
+
if (this._isExpired(mem))
|
|
585
|
+
continue;
|
|
586
|
+
if (!related[linkedId]) {
|
|
587
|
+
related[linkedId] = {
|
|
588
|
+
id: linkedId,
|
|
589
|
+
content: mem.content,
|
|
590
|
+
shared_keys: ["(explicit →)"],
|
|
591
|
+
link_type: "explicit",
|
|
592
|
+
depth: Math.round(mem.depth * 1000) / 1000,
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
related[linkedId].link_type = "both";
|
|
597
|
+
if (!related[linkedId].shared_keys.includes("(explicit →)")) {
|
|
598
|
+
related[linkedId].shared_keys.push("(explicit →)");
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
// Reverse links (←)
|
|
603
|
+
for (const [mid, mem] of Object.entries(this.memories)) {
|
|
604
|
+
if (mid === memoryId || this._isExpired(mem))
|
|
605
|
+
continue;
|
|
606
|
+
if (mem.links.includes(memoryId)) {
|
|
607
|
+
if (!related[mid]) {
|
|
608
|
+
related[mid] = {
|
|
609
|
+
id: mid,
|
|
610
|
+
content: mem.content,
|
|
611
|
+
shared_keys: ["(explicit ←)"],
|
|
612
|
+
link_type: "explicit",
|
|
613
|
+
depth: Math.round(mem.depth * 1000) / 1000,
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
else if (!related[mid].shared_keys.includes("(explicit ←)")) {
|
|
617
|
+
related[mid].shared_keys.push("(explicit ←)");
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
return Object.values(related);
|
|
622
|
+
}
|
|
623
|
+
// ── Delete ──
|
|
624
|
+
async delete(memoryId) {
|
|
625
|
+
return this._lock.runExclusive(async () => {
|
|
626
|
+
if (!(memoryId in this.memories))
|
|
627
|
+
return false;
|
|
628
|
+
delete this.memories[memoryId];
|
|
629
|
+
this._unlinkMemory(memoryId);
|
|
630
|
+
this._pruneOrphanKeys();
|
|
631
|
+
delete this._supersededBy[memoryId];
|
|
632
|
+
for (const [oldId, newId] of Object.entries(this._supersededBy)) {
|
|
633
|
+
if (newId === memoryId)
|
|
634
|
+
delete this._supersededBy[oldId];
|
|
635
|
+
}
|
|
636
|
+
await this.save();
|
|
637
|
+
return true;
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
// ── List all ──
|
|
641
|
+
listAll(namespace) {
|
|
642
|
+
return Object.entries(this.memories)
|
|
643
|
+
.filter(([mid, mem]) => {
|
|
644
|
+
if (this._isExpired(mem))
|
|
645
|
+
return false;
|
|
646
|
+
if (mid in this._supersededBy)
|
|
647
|
+
return false;
|
|
648
|
+
if (namespace && mem.namespace !== namespace)
|
|
649
|
+
return false;
|
|
650
|
+
return true;
|
|
651
|
+
})
|
|
652
|
+
.map(([mid, mem]) => ({
|
|
653
|
+
id: mid,
|
|
654
|
+
content: mem.content,
|
|
655
|
+
keys: this.getKeysForMemory(mid),
|
|
656
|
+
depth: Math.round(mem.depth * 1000) / 1000,
|
|
657
|
+
access_count: mem.access_count,
|
|
658
|
+
supersedes: mem.supersedes,
|
|
659
|
+
created_at: mem.created_at,
|
|
660
|
+
namespace: mem.namespace,
|
|
661
|
+
expires_at: mem.ttl,
|
|
662
|
+
links: mem.links,
|
|
663
|
+
}));
|
|
664
|
+
}
|
|
665
|
+
// ── Cleanup expired ──
|
|
666
|
+
async cleanupExpired() {
|
|
667
|
+
return this._lock.runExclusive(async () => {
|
|
668
|
+
const expired = Object.entries(this.memories)
|
|
669
|
+
.filter(([, mem]) => this._isExpired(mem))
|
|
670
|
+
.map(([mid]) => mid);
|
|
671
|
+
for (const mid of expired) {
|
|
672
|
+
delete this.memories[mid];
|
|
673
|
+
this._unlinkMemory(mid);
|
|
674
|
+
delete this._supersededBy[mid];
|
|
675
|
+
for (const [oldId, newId] of Object.entries(this._supersededBy)) {
|
|
676
|
+
if (newId === mid)
|
|
677
|
+
delete this._supersededBy[oldId];
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
this._pruneOrphanKeys();
|
|
681
|
+
if (expired.length > 0)
|
|
682
|
+
await this.save();
|
|
683
|
+
return expired.length;
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
// ── Conversation store ──
|
|
688
|
+
export async function saveTurn(sessionId, role, content) {
|
|
689
|
+
await mkdir(CONVERSATIONS_DIR, { recursive: true });
|
|
690
|
+
const path = join(CONVERSATIONS_DIR, `${sessionId}.jsonl`);
|
|
691
|
+
let turn = 0;
|
|
692
|
+
try {
|
|
693
|
+
const text = await readFile(path, "utf-8");
|
|
694
|
+
turn = text.split("\n").filter((l) => l.trim()).length;
|
|
695
|
+
}
|
|
696
|
+
catch {
|
|
697
|
+
// file does not exist yet
|
|
698
|
+
}
|
|
699
|
+
const entry = JSON.stringify({
|
|
700
|
+
turn,
|
|
701
|
+
role,
|
|
702
|
+
content,
|
|
703
|
+
ts: Date.now() / 1000,
|
|
704
|
+
});
|
|
705
|
+
await appendFile(path, entry + "\n", "utf-8");
|
|
706
|
+
return turn;
|
|
707
|
+
}
|
|
708
|
+
export async function loadConversation(sessionId, turn) {
|
|
709
|
+
const path = join(CONVERSATIONS_DIR, `${sessionId}.jsonl`);
|
|
710
|
+
let text;
|
|
711
|
+
try {
|
|
712
|
+
text = await readFile(path, "utf-8");
|
|
713
|
+
}
|
|
714
|
+
catch {
|
|
715
|
+
return [];
|
|
716
|
+
}
|
|
717
|
+
const lines = text
|
|
718
|
+
.split("\n")
|
|
719
|
+
.filter((l) => l.trim())
|
|
720
|
+
.map((l) => JSON.parse(l));
|
|
721
|
+
if (turn != null) {
|
|
722
|
+
const start = Math.max(0, turn - 2);
|
|
723
|
+
const end = Math.min(lines.length, turn + 3);
|
|
724
|
+
return lines.slice(start, end);
|
|
725
|
+
}
|
|
726
|
+
return lines;
|
|
727
|
+
}
|
|
728
|
+
//# sourceMappingURL=memoryGraph.js.map
|