@amemhq/core 1.1.0 → 2.0.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 +2 -2
- package/dist/{chunk-B2NS7WAM.js → chunk-K6WZTDM7.js} +229 -58
- package/dist/chunk-K6WZTDM7.js.map +1 -0
- package/dist/cli-migrate.cjs +206 -52
- package/dist/cli-migrate.cjs.map +1 -1
- package/dist/cli-migrate.d.cts +21 -7
- package/dist/cli-migrate.d.ts +21 -7
- package/dist/cli-migrate.js +103 -42
- package/dist/cli-migrate.js.map +1 -1
- package/dist/index.cjs +228 -56
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +145 -29
- package/dist/index.d.ts +145 -29
- package/dist/index.js +11 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-B2NS7WAM.js.map +0 -1
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ Unlike a flat vector store, A-MEM maintains memory as a living, self-evolving se
|
|
|
17
17
|
1. **Note Construction** — an LLM extracts keywords, tags, and a context summary; categorizes the note; and classifies it as `memory` (episodic) or `knowledge` (durable), extracting 1–5 `topics` for knowledge notes.
|
|
18
18
|
2. **Link Generation** — retrieves top-6 candidates; the LLM judges whether to link bidirectionally (similarity > 0.3).
|
|
19
19
|
3. **Memory Evolution** — up to 3 linked notes have their attributes evolved from the new context, possibly triggering further links.
|
|
20
|
-
4. **Hybrid Retrieval** — fuses dense vectors (Transformers.js `
|
|
20
|
+
4. **Hybrid Retrieval** — fuses dense vectors (Transformers.js `bge-m3`, 1024-dim) and BM25 via Reciprocal Rank Fusion (RRF), boosted by retrieval heat.
|
|
21
21
|
5. **2-hop BFS Graph Expansion** — after RRF top-K, BFS walks the link graph up to 2 hops, admitting up to 8 graph-connected notes that pass an embedding relevance gate (cos-sim ≥ 0.25). This is the key advantage over flat vector systems.
|
|
22
22
|
|
|
23
23
|
## Features
|
|
@@ -47,7 +47,7 @@ host (OpenClaw plugin / amem-api / game agent)
|
|
|
47
47
|
▼
|
|
48
48
|
@amemhq/core (TypeScript)
|
|
49
49
|
├── LLM (Anthropic) note construction · link judgment · CRUD · evolution
|
|
50
|
-
├── Transformers.js (ONNX)
|
|
50
|
+
├── Transformers.js (ONNX) local embeddings + Jieba BM25
|
|
51
51
|
└── Qdrant :6333 vector store · owner/readers/writers · agent_id isolation
|
|
52
52
|
```
|
|
53
53
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// src/auth.ts
|
|
2
|
+
var SYSTEM_ACTOR = "__amem_system__";
|
|
2
3
|
function canWrite(note, callerAgentId) {
|
|
3
4
|
return note.owner === callerAgentId || note.writers.includes(callerAgentId) || note.writers.includes("*");
|
|
4
5
|
}
|
|
@@ -11,9 +12,23 @@ var pipeline = null;
|
|
|
11
12
|
var extractor = null;
|
|
12
13
|
var loadedKey = null;
|
|
13
14
|
var cachedDim = null;
|
|
14
|
-
var DEFAULT_EMBEDDING_MODEL = "Xenova/
|
|
15
|
+
var DEFAULT_EMBEDDING_MODEL = "Xenova/bge-m3";
|
|
16
|
+
var LEGACY_DEFAULT_EMBEDDING_MODEL = "Xenova/paraphrase-multilingual-MiniLM-L12-v2";
|
|
17
|
+
var LEGACY_DEFAULT_DIM = 384;
|
|
18
|
+
var DEFAULT_MODEL_DTYPE = "fp16";
|
|
19
|
+
var pinnedModel = null;
|
|
20
|
+
function pinEmbeddingModel(model) {
|
|
21
|
+
if (pinnedModel === model) return;
|
|
22
|
+
pinnedModel = model;
|
|
23
|
+
extractor = null;
|
|
24
|
+
loadedKey = null;
|
|
25
|
+
cachedDim = null;
|
|
26
|
+
}
|
|
27
|
+
function getPinnedEmbeddingModel() {
|
|
28
|
+
return pinnedModel;
|
|
29
|
+
}
|
|
15
30
|
function getEmbeddingModel() {
|
|
16
|
-
return process.env.AMEM_EMBED_MODEL?.trim() || DEFAULT_EMBEDDING_MODEL;
|
|
31
|
+
return process.env.AMEM_EMBED_MODEL?.trim() || pinnedModel || DEFAULT_EMBEDDING_MODEL;
|
|
17
32
|
}
|
|
18
33
|
var CLS_POOLED_MODELS = /* @__PURE__ */ new Set([
|
|
19
34
|
"bge-m3",
|
|
@@ -38,7 +53,9 @@ function getEmbeddingDevice() {
|
|
|
38
53
|
return process.env.AMEM_EMBED_DEVICE?.trim() || void 0;
|
|
39
54
|
}
|
|
40
55
|
function getEmbeddingDtype() {
|
|
41
|
-
|
|
56
|
+
const explicit = process.env.AMEM_EMBED_DTYPE?.trim();
|
|
57
|
+
if (explicit) return explicit;
|
|
58
|
+
return getEmbeddingModel() === DEFAULT_EMBEDDING_MODEL ? DEFAULT_MODEL_DTYPE : void 0;
|
|
42
59
|
}
|
|
43
60
|
function extractorKey() {
|
|
44
61
|
return `${getEmbeddingModel()}|${getEmbeddingDevice() ?? ""}|${getEmbeddingDtype() ?? ""}`;
|
|
@@ -149,12 +166,12 @@ Either set AMEM_EMBED_MODEL back to the model this collection was built with, or
|
|
|
149
166
|
model;
|
|
150
167
|
};
|
|
151
168
|
function migrationHint(collection, targetModel) {
|
|
152
|
-
return `migrate
|
|
169
|
+
return `migrate onto it:
|
|
153
170
|
|
|
154
171
|
AMEM_EMBED_MODEL=${targetModel} \\
|
|
155
|
-
npx --package=@amemhq/core amem-migrate --
|
|
172
|
+
npx --package=@amemhq/core amem-migrate --from-collection ${collection}
|
|
156
173
|
|
|
157
|
-
That
|
|
174
|
+
That only reports; it takes --apply to write anything, and "${collection}" is read either way. The new store ends up behind the name you already use, so there is nothing to change in your config afterwards. See https://amem.owo.lc/reference/embedding-models.`;
|
|
158
175
|
}
|
|
159
176
|
var EmbeddingModelMismatchError = class extends Error {
|
|
160
177
|
constructor(collection, collectionModel, configuredModel) {
|
|
@@ -171,6 +188,25 @@ Either set AMEM_EMBED_MODEL back to "${collectionModel}", or ` + migrationHint(c
|
|
|
171
188
|
collectionModel;
|
|
172
189
|
configuredModel;
|
|
173
190
|
};
|
|
191
|
+
var MixedEmbeddingModelsError = class extends Error {
|
|
192
|
+
constructor(collection, wanted, inUse) {
|
|
193
|
+
super(
|
|
194
|
+
`Collection "${collection}" was built with "${wanted}", but this process is already embedding with "${inUse}" for another collection. One process can only use one model.
|
|
195
|
+
Migrate the remaining collections so they all agree:
|
|
196
|
+
|
|
197
|
+
npx --package=@amemhq/core amem-migrate --from-collection ${collection}
|
|
198
|
+
|
|
199
|
+
Or set AMEM_EMBED_MODEL to pin every collection to one model, which is only correct if they really were all built with it.`
|
|
200
|
+
);
|
|
201
|
+
this.collection = collection;
|
|
202
|
+
this.wanted = wanted;
|
|
203
|
+
this.inUse = inUse;
|
|
204
|
+
this.name = "MixedEmbeddingModelsError";
|
|
205
|
+
}
|
|
206
|
+
collection;
|
|
207
|
+
wanted;
|
|
208
|
+
inUse;
|
|
209
|
+
};
|
|
174
210
|
async function recordCollectionModel(collection, model) {
|
|
175
211
|
try {
|
|
176
212
|
await qdrant("PATCH", `/collections/${collection}`, { metadata: { embedding_model: model } });
|
|
@@ -213,18 +249,38 @@ async function ensureCollection(collectionName) {
|
|
|
213
249
|
}
|
|
214
250
|
if (existing) {
|
|
215
251
|
const collectionDim = existing.config?.params?.vectors?.size;
|
|
252
|
+
const recorded = existing.config?.metadata?.embedding_model;
|
|
253
|
+
const explicit = process.env.AMEM_EMBED_MODEL?.trim();
|
|
254
|
+
const inferLegacy = !explicit && recorded === void 0 && collectionDim === LEGACY_DEFAULT_DIM;
|
|
255
|
+
if (!explicit) {
|
|
256
|
+
const wanted = (
|
|
257
|
+
// The collection says what built it, which outranks whatever the shipped
|
|
258
|
+
// default happens to be today. This is what keeps changing the default
|
|
259
|
+
// from breaking every install that already has data.
|
|
260
|
+
typeof recorded === "string" ? recorded : inferLegacy ? LEGACY_DEFAULT_EMBEDDING_MODEL : DEFAULT_EMBEDDING_MODEL
|
|
261
|
+
);
|
|
262
|
+
const inUse = getPinnedEmbeddingModel();
|
|
263
|
+
if (inUse !== null && inUse !== wanted) throw new MixedEmbeddingModelsError(col, wanted, inUse);
|
|
264
|
+
pinEmbeddingModel(wanted);
|
|
265
|
+
if (wanted === LEGACY_DEFAULT_EMBEDDING_MODEL) {
|
|
266
|
+
console.warn(
|
|
267
|
+
`[amem] "${col}" is on ${LEGACY_DEFAULT_EMBEDDING_MODEL} (${LEGACY_DEFAULT_DIM}-dim).
|
|
268
|
+
[amem] ${DEFAULT_EMBEDDING_MODEL} reads 8192 tokens where that one stops at 128, so anything longer is being truncated before it reaches the vector.
|
|
269
|
+
[amem] To move: npx --package=@amemhq/core amem-migrate --from-collection ${col}`
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
216
273
|
if (typeof collectionDim === "number") {
|
|
217
274
|
const modelDim = await getEmbeddingDim();
|
|
218
275
|
if (collectionDim !== modelDim) {
|
|
219
276
|
throw new EmbeddingDimensionMismatchError(col, collectionDim, modelDim, getEmbeddingModel());
|
|
220
277
|
}
|
|
221
278
|
}
|
|
222
|
-
const recorded = existing.config?.metadata?.embedding_model;
|
|
223
279
|
const current = getEmbeddingModel();
|
|
224
280
|
if (typeof recorded === "string" && recorded !== current) {
|
|
225
281
|
throw new EmbeddingModelMismatchError(col, recorded, current);
|
|
226
282
|
}
|
|
227
|
-
if (recorded === void 0) {
|
|
283
|
+
if (recorded === void 0 && !inferLegacy) {
|
|
228
284
|
await recordCollectionModel(col, current);
|
|
229
285
|
}
|
|
230
286
|
markReady();
|
|
@@ -238,7 +294,9 @@ async function ensureCollection(collectionName) {
|
|
|
238
294
|
} catch (err) {
|
|
239
295
|
if (!(err instanceof Error) || !err.message.includes("already exists")) throw err;
|
|
240
296
|
}
|
|
241
|
-
|
|
297
|
+
const created = getEmbeddingModel();
|
|
298
|
+
await recordCollectionModel(col, created);
|
|
299
|
+
if (!process.env.AMEM_EMBED_MODEL?.trim()) pinEmbeddingModel(created);
|
|
242
300
|
await qdrant("PUT", `/collections/${col}/index`, {
|
|
243
301
|
field_name: "agent_id",
|
|
244
302
|
field_schema: "keyword"
|
|
@@ -292,6 +350,43 @@ async function createCollectionRaw(collection, size) {
|
|
|
292
350
|
async function upsertPointsRaw(collection, points) {
|
|
293
351
|
await qdrant("PUT", `/collections/${collection}/points?wait=true`, { points });
|
|
294
352
|
}
|
|
353
|
+
async function scrollIdsRaw(collection, limit = 1e4) {
|
|
354
|
+
const ids = /* @__PURE__ */ new Set();
|
|
355
|
+
let offset = void 0;
|
|
356
|
+
for (; ; ) {
|
|
357
|
+
const body = { with_payload: false, with_vector: false, limit };
|
|
358
|
+
if (offset !== void 0 && offset !== null) body.offset = offset;
|
|
359
|
+
const res = await qdrant("POST", `/collections/${collection}/points/scroll`, body);
|
|
360
|
+
for (const p of res.points) ids.add(String(p.id));
|
|
361
|
+
offset = res.next_page_offset;
|
|
362
|
+
if (offset === void 0 || offset === null || res.points.length === 0) break;
|
|
363
|
+
}
|
|
364
|
+
return ids;
|
|
365
|
+
}
|
|
366
|
+
async function deleteCollectionRaw(collection) {
|
|
367
|
+
await qdrant("DELETE", `/collections/${collection}`);
|
|
368
|
+
}
|
|
369
|
+
async function resolveAliasRaw(alias) {
|
|
370
|
+
try {
|
|
371
|
+
const res = await qdrant("GET", `/aliases`);
|
|
372
|
+
return res.aliases.find((a) => a.alias_name === alias)?.collection_name ?? null;
|
|
373
|
+
} catch {
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
async function createAliasRaw(alias, collection) {
|
|
378
|
+
await qdrant("POST", `/collections/aliases`, {
|
|
379
|
+
actions: [{ create_alias: { collection_name: collection, alias_name: alias } }]
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
async function setAliasRaw(alias, collection) {
|
|
383
|
+
await qdrant("POST", `/collections/aliases`, {
|
|
384
|
+
actions: [
|
|
385
|
+
{ delete_alias: { alias_name: alias } },
|
|
386
|
+
{ create_alias: { collection_name: collection, alias_name: alias } }
|
|
387
|
+
]
|
|
388
|
+
});
|
|
389
|
+
}
|
|
295
390
|
function noteToPoint(note) {
|
|
296
391
|
return {
|
|
297
392
|
id: note.id,
|
|
@@ -436,12 +531,15 @@ function makeCrud(collectionName, modeBIsolated = false) {
|
|
|
436
531
|
},
|
|
437
532
|
/**
|
|
438
533
|
* Story 36: this is the one read that bypasses the agent filter — it fetches
|
|
439
|
-
* straight by UUID.
|
|
440
|
-
*
|
|
441
|
-
*
|
|
442
|
-
*
|
|
534
|
+
* straight by UUID. An unreadable note comes back as `null`, indistinguishable
|
|
535
|
+
* from missing, so nothing leaks and callers already handle it.
|
|
536
|
+
*
|
|
537
|
+
* `reader` is required. It used to be optional, and omitting it skipped the
|
|
538
|
+
* check — which meant the safe behaviour was the one you had to remember to
|
|
539
|
+
* ask for. Pass `SYSTEM_ACTOR` to read as the engine itself; that reads as a
|
|
540
|
+
* deliberate act at the call site, where an absent argument did not.
|
|
443
541
|
*/
|
|
444
|
-
async getNote(id,
|
|
542
|
+
async getNote(id, reader) {
|
|
445
543
|
await ensureCollection(col);
|
|
446
544
|
try {
|
|
447
545
|
const result = await qdrant("POST", `/collections/${col}/points`, {
|
|
@@ -451,7 +549,7 @@ function makeCrud(collectionName, modeBIsolated = false) {
|
|
|
451
549
|
});
|
|
452
550
|
if (!result.length) return null;
|
|
453
551
|
const note = pointToNote(result[0]);
|
|
454
|
-
if (
|
|
552
|
+
if (reader !== SYSTEM_ACTOR && !canRead(note, reader)) return null;
|
|
455
553
|
return note;
|
|
456
554
|
} catch {
|
|
457
555
|
return null;
|
|
@@ -489,19 +587,22 @@ function makeCrud(collectionName, modeBIsolated = false) {
|
|
|
489
587
|
return pointToNote(result.points[0]);
|
|
490
588
|
},
|
|
491
589
|
/**
|
|
492
|
-
* Story 33:
|
|
493
|
-
*
|
|
494
|
-
*
|
|
495
|
-
*
|
|
496
|
-
*
|
|
497
|
-
*
|
|
590
|
+
* Story 33: enforces the writers policy. Returns false — without writing —
|
|
591
|
+
* when the caller may not write. This fetch-then-check path exists for callers
|
|
592
|
+
* that only have an id (the plugin's CRUD hook); callers already holding the
|
|
593
|
+
* note can check `canWrite` themselves and skip a round trip.
|
|
594
|
+
*
|
|
595
|
+
* `caller` is required. Pass `SYSTEM_ACTOR` for the engine's own maintenance
|
|
596
|
+
* writes. The self-read below is a genuine `SYSTEM_ACTOR` case: it fetches the
|
|
597
|
+
* note in order to decide whether the caller may write it, and gating that
|
|
598
|
+
* fetch on the same policy it exists to evaluate would be circular.
|
|
498
599
|
*/
|
|
499
|
-
async updateNoteContent(id, content, embedding, hash,
|
|
600
|
+
async updateNoteContent(id, content, embedding, hash, caller) {
|
|
500
601
|
await ensureCollection(col);
|
|
501
602
|
let existing = null;
|
|
502
|
-
if (
|
|
503
|
-
existing = await this.getNote(id);
|
|
504
|
-
if (existing && !canWrite(existing,
|
|
603
|
+
if (caller !== SYSTEM_ACTOR) {
|
|
604
|
+
existing = await this.getNote(id, SYSTEM_ACTOR);
|
|
605
|
+
if (existing && !canWrite(existing, caller)) return false;
|
|
505
606
|
}
|
|
506
607
|
await qdrant("PUT", `/collections/${col}/points/vectors?wait=true`, {
|
|
507
608
|
points: [{ id, vector: embedding }]
|
|
@@ -589,11 +690,11 @@ function makeCrud(collectionName, modeBIsolated = false) {
|
|
|
589
690
|
});
|
|
590
691
|
},
|
|
591
692
|
/** Story 33: see `updateNoteContent` — returns false, unwritten, when denied. */
|
|
592
|
-
async invalidateNote(id,
|
|
693
|
+
async invalidateNote(id, caller) {
|
|
593
694
|
await ensureCollection(col);
|
|
594
|
-
if (
|
|
595
|
-
const existing = await this.getNote(id);
|
|
596
|
-
if (existing && !canWrite(existing,
|
|
695
|
+
if (caller !== SYSTEM_ACTOR) {
|
|
696
|
+
const existing = await this.getNote(id, SYSTEM_ACTOR);
|
|
697
|
+
if (existing && !canWrite(existing, caller)) return false;
|
|
597
698
|
}
|
|
598
699
|
await qdrant("POST", `/collections/${col}/points/payload?wait=true`, {
|
|
599
700
|
payload: { is_active: false },
|
|
@@ -659,8 +760,8 @@ function makeCrud(collectionName, modeBIsolated = false) {
|
|
|
659
760
|
function createStorageContext(collectionName, modeBIsolated = false) {
|
|
660
761
|
return makeCrud(collectionName || getCollection(), modeBIsolated);
|
|
661
762
|
}
|
|
662
|
-
async function getNote(id,
|
|
663
|
-
return makeCrud(getCollection()).getNote(id,
|
|
763
|
+
async function getNote(id, reader) {
|
|
764
|
+
return makeCrud(getCollection()).getNote(id, reader);
|
|
664
765
|
}
|
|
665
766
|
async function updateNote(note) {
|
|
666
767
|
return makeCrud(getCollection()).updateNote(note);
|
|
@@ -671,8 +772,8 @@ async function listNotes(agentId, subject) {
|
|
|
671
772
|
async function deleteNote(id) {
|
|
672
773
|
return makeCrud(getCollection()).deleteNote(id);
|
|
673
774
|
}
|
|
674
|
-
async function invalidateNote(id,
|
|
675
|
-
return makeCrud(getCollection()).invalidateNote(id,
|
|
775
|
+
async function invalidateNote(id, caller) {
|
|
776
|
+
return makeCrud(getCollection()).invalidateNote(id, caller);
|
|
676
777
|
}
|
|
677
778
|
async function patchNotePayload(id, fields) {
|
|
678
779
|
return makeCrud(getCollection()).patchNotePayload(id, fields);
|
|
@@ -1441,7 +1542,7 @@ async function addMemory(content, agentId = "main", opts) {
|
|
|
1441
1542
|
if (topMatch.length > 0 && topMatch[0].score >= 0.85) {
|
|
1442
1543
|
if (canWrite(topMatch[0].note, agentId)) {
|
|
1443
1544
|
console.log(`[add] dedup: high-sim match (sim=${topMatch[0].score.toFixed(3)}), updating existing`);
|
|
1444
|
-
await ctx.updateNoteContent(topMatch[0].note.id, content, embedding, hash);
|
|
1545
|
+
await ctx.updateNoteContent(topMatch[0].note.id, content, embedding, hash, agentId);
|
|
1445
1546
|
return topMatch[0].note.id;
|
|
1446
1547
|
}
|
|
1447
1548
|
console.log(
|
|
@@ -1513,7 +1614,7 @@ async function addMemory(content, agentId = "main", opts) {
|
|
|
1513
1614
|
note.links = linkedIds;
|
|
1514
1615
|
await ctx.updateNote(note);
|
|
1515
1616
|
for (const lid of linkedIds) {
|
|
1516
|
-
const linked = await ctx.getNote(lid);
|
|
1617
|
+
const linked = await ctx.getNote(lid, agentId);
|
|
1517
1618
|
if (linked && !linked.links.includes(note.id)) {
|
|
1518
1619
|
if (!canWrite(linked, agentId)) {
|
|
1519
1620
|
console.log(`[link] back-link into ${lid.slice(0, 8)} skipped \u2014 not writable by ${logSafe(agentId)}`);
|
|
@@ -1526,7 +1627,7 @@ async function addMemory(content, agentId = "main", opts) {
|
|
|
1526
1627
|
if (shouldRunEvolution()) {
|
|
1527
1628
|
console.log(` [evo] threshold reached, running evolution for ${Math.min(linkedIds.length, 3)} linked notes`);
|
|
1528
1629
|
for (const lid of linkedIds.slice(0, 3)) {
|
|
1529
|
-
const linked = await ctx.getNote(lid);
|
|
1630
|
+
const linked = await ctx.getNote(lid, agentId);
|
|
1530
1631
|
if (!linked) continue;
|
|
1531
1632
|
if (!canWrite(linked, agentId)) {
|
|
1532
1633
|
console.log(` [evo] skipping ${lid.slice(0, 8)} \u2014 not writable by ${logSafe(agentId)}`);
|
|
@@ -1679,7 +1780,7 @@ async function searchMemory(query, topK = 5, agentId = "main", opts) {
|
|
|
1679
1780
|
const allNotes = await ctx.listNotes(agentId, subject);
|
|
1680
1781
|
const bm25State = buildBM25(allNotes);
|
|
1681
1782
|
const queryTokens = simpleTokenize(query);
|
|
1682
|
-
const bm25Ranked = bm25Score(bm25State, queryTokens).slice(0, n);
|
|
1783
|
+
const bm25Ranked = bm25Score(bm25State, queryTokens).filter(([, score]) => score > 0).slice(0, n);
|
|
1683
1784
|
const merged = rrfMerge(
|
|
1684
1785
|
embResults.map((r) => r.note.id),
|
|
1685
1786
|
bm25Ranked.map((r) => r[0])
|
|
@@ -1702,6 +1803,7 @@ async function searchMemory(query, topK = 5, agentId = "main", opts) {
|
|
|
1702
1803
|
const visitedIds = new Set(topIds);
|
|
1703
1804
|
const bfsQueue = useBfs ? topIds.map((id) => ({ id, hop: 0 })) : [];
|
|
1704
1805
|
const bfsExtra = [];
|
|
1806
|
+
const bfsSimMap = /* @__PURE__ */ new Map();
|
|
1705
1807
|
while (bfsQueue.length > 0 && bfsExtra.length < BFS_MAX_EXPAND) {
|
|
1706
1808
|
const item = bfsQueue.shift();
|
|
1707
1809
|
if (item.hop >= BFS_MAX_HOPS) continue;
|
|
@@ -1712,10 +1814,9 @@ async function searchMemory(query, topK = 5, agentId = "main", opts) {
|
|
|
1712
1814
|
visitedIds.add(linkedId);
|
|
1713
1815
|
const linked = noteMap.get(linkedId);
|
|
1714
1816
|
if (!linked || linked.is_active === false) continue;
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
}
|
|
1817
|
+
const sim = cosineSimilarity(queryEmbedding, linked.embedding);
|
|
1818
|
+
if (bfsSimThreshold > 0 && sim < bfsSimThreshold) continue;
|
|
1819
|
+
bfsSimMap.set(linkedId, sim);
|
|
1719
1820
|
bfsExtra.push(linkedId);
|
|
1720
1821
|
bfsQueue.push({ id: linkedId, hop: item.hop + 1 });
|
|
1721
1822
|
if (bfsExtra.length >= BFS_MAX_EXPAND) break;
|
|
@@ -1731,7 +1832,11 @@ async function searchMemory(query, topK = 5, agentId = "main", opts) {
|
|
|
1731
1832
|
const embSimMap = new Map(embResults.map((r) => [r.note.id, r.score]));
|
|
1732
1833
|
const rrfMap = new Map(boostedMerged.map(([id, score]) => [id, score]));
|
|
1733
1834
|
const results = [];
|
|
1734
|
-
|
|
1835
|
+
const ordered = [
|
|
1836
|
+
...filteredTopIds.map((id) => [id, "match"]),
|
|
1837
|
+
...bfsExtra.map((id) => [id, "link"])
|
|
1838
|
+
];
|
|
1839
|
+
for (const [id, via] of ordered) {
|
|
1735
1840
|
const note = noteMap.get(id);
|
|
1736
1841
|
if (!note) continue;
|
|
1737
1842
|
results.push({
|
|
@@ -1742,8 +1847,9 @@ async function searchMemory(query, topK = 5, agentId = "main", opts) {
|
|
|
1742
1847
|
keywords: note.keywords,
|
|
1743
1848
|
links: note.links,
|
|
1744
1849
|
timestamp: note.timestamp,
|
|
1745
|
-
similarity: embSimMap.get(id) ?? 0,
|
|
1850
|
+
similarity: embSimMap.get(id) ?? bfsSimMap.get(id) ?? 0,
|
|
1746
1851
|
rrf: rrfMap.get(id) ?? 0,
|
|
1852
|
+
via,
|
|
1747
1853
|
topics: note.topics ?? [],
|
|
1748
1854
|
note_type: note.note_type ?? "memory"
|
|
1749
1855
|
});
|
|
@@ -1800,7 +1906,7 @@ async function mergeSimilarNotes(agentId, storageCtx) {
|
|
|
1800
1906
|
const mergedContent = judgment.mergedContent || pendingNote.content;
|
|
1801
1907
|
const newEmbedding = await encode(buildEmbedText({ ...bestNeighbor, content: mergedContent }));
|
|
1802
1908
|
const newHash = createHash("md5").update(mergedContent).digest("hex");
|
|
1803
|
-
await ctx.updateNoteContent(bestNeighbor.id, mergedContent, newEmbedding, newHash);
|
|
1909
|
+
await ctx.updateNoteContent(bestNeighbor.id, mergedContent, newEmbedding, newHash, agentId);
|
|
1804
1910
|
await ctx.patchNotePayload(bestNeighbor.id, {
|
|
1805
1911
|
evolution_history: JSON.stringify(oldHistory),
|
|
1806
1912
|
evolution_type: "EVOLVE"
|
|
@@ -1824,7 +1930,7 @@ async function mergeSimilarNotes(agentId, storageCtx) {
|
|
|
1824
1930
|
const mergedContent = judgment.mergedContent || `${bestNeighbor.content}\uFF1B${pendingNote.content}`;
|
|
1825
1931
|
const newEmbedding = await encode(buildEmbedText({ ...bestNeighbor, content: mergedContent }));
|
|
1826
1932
|
const newHash = createHash("md5").update(mergedContent).digest("hex");
|
|
1827
|
-
await ctx.updateNoteContent(bestNeighbor.id, mergedContent, newEmbedding, newHash);
|
|
1933
|
+
await ctx.updateNoteContent(bestNeighbor.id, mergedContent, newEmbedding, newHash, agentId);
|
|
1828
1934
|
await ctx.patchNotePayload(bestNeighbor.id, {
|
|
1829
1935
|
evolution_history: JSON.stringify(oldHistory),
|
|
1830
1936
|
evolution_type: "EXPAND"
|
|
@@ -1864,7 +1970,7 @@ async function mergeSimilarNotes(agentId, storageCtx) {
|
|
|
1864
1970
|
const [keepNote, dropNote] = noteA.content.length >= noteB.content.length ? [noteA, noteB] : [noteB, noteA];
|
|
1865
1971
|
const newEmbedding = await encode(result.merged);
|
|
1866
1972
|
const newHash = createHash("md5").update(result.merged).digest("hex");
|
|
1867
|
-
await ctx.updateNoteContent(keepNote.id, result.merged, newEmbedding, newHash);
|
|
1973
|
+
await ctx.updateNoteContent(keepNote.id, result.merged, newEmbedding, newHash, agentId);
|
|
1868
1974
|
await ctx.deleteNote(dropNote.id);
|
|
1869
1975
|
deletedIds.add(dropNote.id);
|
|
1870
1976
|
mergedCount++;
|
|
@@ -1973,7 +2079,7 @@ async function consolidateMemories(agentId, logger, storageCtx) {
|
|
|
1973
2079
|
action: "consolidate"
|
|
1974
2080
|
});
|
|
1975
2081
|
await ctx.updateNote(keepNote);
|
|
1976
|
-
await ctx.invalidateNote(dropNote.id);
|
|
2082
|
+
await ctx.invalidateNote(dropNote.id, agentId);
|
|
1977
2083
|
await ctx.replaceLinkReferences(dropNote.id, keepNote.id, agentId);
|
|
1978
2084
|
logMergeToFile(keepNote.id, dropNote.id, keepNote.content);
|
|
1979
2085
|
processedIds.add(keepNote.id);
|
|
@@ -2086,8 +2192,19 @@ async function migrateCollection(opts) {
|
|
|
2086
2192
|
);
|
|
2087
2193
|
if (dryRun) {
|
|
2088
2194
|
log("[migrate] dry run \u2014 nothing written. Pass dryRun: false to apply.");
|
|
2089
|
-
return {
|
|
2195
|
+
return {
|
|
2196
|
+
total: notes.length,
|
|
2197
|
+
missingDerived,
|
|
2198
|
+
refreshed: 0,
|
|
2199
|
+
migrated: 0,
|
|
2200
|
+
skipped: 0,
|
|
2201
|
+
sourceDim,
|
|
2202
|
+
targetDim,
|
|
2203
|
+
model,
|
|
2204
|
+
dryRun: true
|
|
2205
|
+
};
|
|
2090
2206
|
}
|
|
2207
|
+
let alreadyDone = /* @__PURE__ */ new Set();
|
|
2091
2208
|
const existingTargetDim = await collectionDimRaw(to);
|
|
2092
2209
|
if (existingTargetDim === null) {
|
|
2093
2210
|
await createCollectionRaw(to, targetDim);
|
|
@@ -2096,9 +2213,17 @@ async function migrateCollection(opts) {
|
|
|
2096
2213
|
if (existingTargetDim !== targetDim) {
|
|
2097
2214
|
throw new Error(`migrate: target "${to}" exists at ${existingTargetDim}d but the model produces ${targetDim}d`);
|
|
2098
2215
|
}
|
|
2099
|
-
const
|
|
2100
|
-
if (
|
|
2101
|
-
|
|
2216
|
+
const present = await scrollIdsRaw(to);
|
|
2217
|
+
if (present.size > 0) {
|
|
2218
|
+
const sourceIds = new Set(notes.map((n) => n.id));
|
|
2219
|
+
const foreign = [...present].filter((id) => !sourceIds.has(id));
|
|
2220
|
+
if (foreign.length > 0) {
|
|
2221
|
+
throw new Error(
|
|
2222
|
+
`migrate: target "${to}" holds ${foreign.length} point(s) that are not in "${from}" (e.g. ${foreign[0]}). That is not an interrupted migration \u2014 use a different target.`
|
|
2223
|
+
);
|
|
2224
|
+
}
|
|
2225
|
+
alreadyDone = present;
|
|
2226
|
+
log(`[migrate] resuming: ${present.size} of ${notes.length} already in ${to}`);
|
|
2102
2227
|
}
|
|
2103
2228
|
}
|
|
2104
2229
|
let refreshed = 0;
|
|
@@ -2112,6 +2237,7 @@ async function migrateCollection(opts) {
|
|
|
2112
2237
|
buffer = [];
|
|
2113
2238
|
};
|
|
2114
2239
|
for (const note of notes) {
|
|
2240
|
+
if (alreadyDone.has(note.id)) continue;
|
|
2115
2241
|
if (refreshFields && missingDerivedFields(note)) {
|
|
2116
2242
|
try {
|
|
2117
2243
|
const built = await llmConstructNote(note.content);
|
|
@@ -2127,7 +2253,7 @@ async function migrateCollection(opts) {
|
|
|
2127
2253
|
buffer.push(point);
|
|
2128
2254
|
if (buffer.length >= BATCH) {
|
|
2129
2255
|
await flush();
|
|
2130
|
-
log(`[migrate] ${migrated}/${notes.length}`);
|
|
2256
|
+
log(`[migrate] ${alreadyDone.size + migrated}/${notes.length}`);
|
|
2131
2257
|
}
|
|
2132
2258
|
}
|
|
2133
2259
|
await flush();
|
|
@@ -2135,16 +2261,55 @@ async function migrateCollection(opts) {
|
|
|
2135
2261
|
if (finalCount !== notes.length) {
|
|
2136
2262
|
warn(`[migrate] target holds ${finalCount} point(s) but the source had ${notes.length} \u2014 check before switching`);
|
|
2137
2263
|
}
|
|
2138
|
-
log(
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2264
|
+
log(`[migrate] done: ${migrated} written, ${alreadyDone.size} already present, ${refreshed} re-extracted.`);
|
|
2265
|
+
return {
|
|
2266
|
+
total: notes.length,
|
|
2267
|
+
missingDerived,
|
|
2268
|
+
refreshed,
|
|
2269
|
+
migrated,
|
|
2270
|
+
skipped: alreadyDone.size,
|
|
2271
|
+
sourceDim,
|
|
2272
|
+
targetDim,
|
|
2273
|
+
model,
|
|
2274
|
+
dryRun: false
|
|
2275
|
+
};
|
|
2276
|
+
}
|
|
2277
|
+
async function switchToMigrated(opts) {
|
|
2278
|
+
const { name, to } = opts;
|
|
2279
|
+
const log = opts.logger?.info ?? ((m) => console.log(m));
|
|
2280
|
+
if (name === to) throw new Error(`switch: "${name}" and "${to}" are the same collection`);
|
|
2281
|
+
const already = await resolveAliasRaw(name);
|
|
2282
|
+
if (already === to) {
|
|
2283
|
+
log(`[switch] "${name}" already points at "${to}" \u2014 nothing to do`);
|
|
2284
|
+
return { name, to, moved: await countPointsRaw(to) };
|
|
2285
|
+
}
|
|
2286
|
+
const targetCount = await countPointsRaw(to);
|
|
2287
|
+
if (targetCount === 0) throw new Error(`switch: "${to}" is empty \u2014 migrate into it first`);
|
|
2288
|
+
if (already === null) {
|
|
2289
|
+
const sourceCount = await countPointsRaw(name);
|
|
2290
|
+
if (targetCount < sourceCount) {
|
|
2291
|
+
throw new Error(
|
|
2292
|
+
`switch: "${to}" holds ${targetCount} point(s) but "${name}" still holds ${sourceCount}. The migration is not finished \u2014 run it again before switching.`
|
|
2293
|
+
);
|
|
2294
|
+
}
|
|
2295
|
+
log(`[switch] verified ${targetCount} in "${to}" against ${sourceCount} in "${name}"`);
|
|
2296
|
+
await deleteCollectionRaw(name);
|
|
2297
|
+
log(`[switch] dropped "${name}"`);
|
|
2298
|
+
await createAliasRaw(name, to);
|
|
2299
|
+
} else {
|
|
2300
|
+
await setAliasRaw(name, to);
|
|
2301
|
+
}
|
|
2302
|
+
log(`[switch] "${name}" now resolves to "${to}"`);
|
|
2303
|
+
return { name, to, moved: targetCount };
|
|
2142
2304
|
}
|
|
2143
2305
|
|
|
2144
2306
|
export {
|
|
2307
|
+
SYSTEM_ACTOR,
|
|
2145
2308
|
canWrite,
|
|
2146
2309
|
canRead,
|
|
2147
2310
|
DEFAULT_EMBEDDING_MODEL,
|
|
2311
|
+
LEGACY_DEFAULT_EMBEDDING_MODEL,
|
|
2312
|
+
LEGACY_DEFAULT_DIM,
|
|
2148
2313
|
getEmbeddingModel,
|
|
2149
2314
|
getEmbeddingPooling,
|
|
2150
2315
|
getEmbeddingDevice,
|
|
@@ -2157,8 +2322,13 @@ export {
|
|
|
2157
2322
|
getCollection,
|
|
2158
2323
|
EmbeddingDimensionMismatchError,
|
|
2159
2324
|
EmbeddingModelMismatchError,
|
|
2325
|
+
MixedEmbeddingModelsError,
|
|
2160
2326
|
pingQdrant,
|
|
2161
2327
|
ensureCollection,
|
|
2328
|
+
countPointsRaw,
|
|
2329
|
+
collectionDimRaw,
|
|
2330
|
+
scrollIdsRaw,
|
|
2331
|
+
resolveAliasRaw,
|
|
2162
2332
|
createStorageContext,
|
|
2163
2333
|
getNote,
|
|
2164
2334
|
updateNote,
|
|
@@ -2177,6 +2347,7 @@ export {
|
|
|
2177
2347
|
mergeSimilarNotes,
|
|
2178
2348
|
consolidateMemories,
|
|
2179
2349
|
conflictSweep,
|
|
2180
|
-
migrateCollection
|
|
2350
|
+
migrateCollection,
|
|
2351
|
+
switchToMigrated
|
|
2181
2352
|
};
|
|
2182
|
-
//# sourceMappingURL=chunk-
|
|
2353
|
+
//# sourceMappingURL=chunk-K6WZTDM7.js.map
|