openclaw-amem 1.4.2 β 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 +11 -1
- package/dist/index.js +291 -62
- package/openclaw.plugin.json +4 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ Requires a local [Qdrant](https://qdrant.tech). Implements [A-MEM](https://arxiv
|
|
|
29
29
|
- π§ **Knowledge vs episodic** β durable knowledge notes skip consolidation & time-decay; topic tags for precise recall.
|
|
30
30
|
- π§Ή **Self-consolidating** β daily 02:30 in-process merge of semantic duplicates with link cascading.
|
|
31
31
|
- π **Per-agent isolation** β private by default; explicit `owner`/`readers`/`writers`; Mode A (shared collection) or Mode B (dedicated collection).
|
|
32
|
-
- π **Chinese-optimized** & local embeddings (Transformers.js
|
|
32
|
+
- π **Chinese-optimized** & local embeddings (Transformers.js) β no Python, no external embedding API.
|
|
33
33
|
|
|
34
34
|
β Full feature list & internals: **[@amemhq/core README](../amem-core)** Β· **[docs](https://amem.owo.lc)**.
|
|
35
35
|
|
|
@@ -39,6 +39,7 @@ Requires a local [Qdrant](https://qdrant.tech). Implements [A-MEM](https://arxiv
|
|
|
39
39
|
- Node.js 24 (18+ works; 24/26 supported)
|
|
40
40
|
- Qdrant running on `:6333`
|
|
41
41
|
- An LLM: `ANTHROPIC_API_KEY` by default, or any OpenAI-compatible provider β see [LLM provider](#llm-provider)
|
|
42
|
+
- ~1.1 GB of disk for the embedding model, downloaded once on first run
|
|
42
43
|
|
|
43
44
|
## Installation
|
|
44
45
|
|
|
@@ -88,6 +89,15 @@ Add `openclaw-amem` to your allowed plugins and hook it into the `memory` slot:
|
|
|
88
89
|
openclaw gateway restart
|
|
89
90
|
```
|
|
90
91
|
|
|
92
|
+
First run downloads the embedding model (`bge-m3`, 1.08 GB) and caches it. Later
|
|
93
|
+
restarts are instant. Want something smaller? `AMEM_EMBED_MODEL=onnx-community/gte-multilingual-base`
|
|
94
|
+
is about a third the size and reads text just as long β set it **before** you have
|
|
95
|
+
memories, since changing it afterwards means a [migration](https://amem.owo.lc/reference/embedding-models#changing-the-model-on-a-store-you-already-have).
|
|
96
|
+
|
|
97
|
+
Upgrading from 1.x downloads nothing. Your existing memories keep the model that
|
|
98
|
+
built them, and the plugin says so at startup along with the one command that
|
|
99
|
+
moves them.
|
|
100
|
+
|
|
91
101
|
## LLM provider
|
|
92
102
|
|
|
93
103
|
The plugin calls an LLM for note construction, linking, and evolution. Pick the backend with `AMEM_LLM_PROVIDER`:
|
package/dist/index.js
CHANGED
|
@@ -53,8 +53,18 @@ var init_config = __esm({
|
|
|
53
53
|
});
|
|
54
54
|
|
|
55
55
|
// ../amem-core/src/embedding.ts
|
|
56
|
+
function pinEmbeddingModel(model) {
|
|
57
|
+
if (pinnedModel === model) return;
|
|
58
|
+
pinnedModel = model;
|
|
59
|
+
extractor = null;
|
|
60
|
+
loadedKey = null;
|
|
61
|
+
cachedDim = null;
|
|
62
|
+
}
|
|
63
|
+
function getPinnedEmbeddingModel() {
|
|
64
|
+
return pinnedModel;
|
|
65
|
+
}
|
|
56
66
|
function getEmbeddingModel() {
|
|
57
|
-
return process.env.AMEM_EMBED_MODEL?.trim() || DEFAULT_EMBEDDING_MODEL;
|
|
67
|
+
return process.env.AMEM_EMBED_MODEL?.trim() || pinnedModel || DEFAULT_EMBEDDING_MODEL;
|
|
58
68
|
}
|
|
59
69
|
function getEmbeddingPooling() {
|
|
60
70
|
const explicit = process.env.AMEM_EMBED_POOLING?.trim().toLowerCase();
|
|
@@ -62,22 +72,39 @@ function getEmbeddingPooling() {
|
|
|
62
72
|
const basename2 = getEmbeddingModel().split("/").pop()?.toLowerCase() ?? "";
|
|
63
73
|
return CLS_POOLED_MODELS.has(basename2) ? "cls" : "mean";
|
|
64
74
|
}
|
|
75
|
+
function getEmbeddingDevice() {
|
|
76
|
+
return process.env.AMEM_EMBED_DEVICE?.trim() || void 0;
|
|
77
|
+
}
|
|
78
|
+
function getEmbeddingDtype() {
|
|
79
|
+
const explicit = process.env.AMEM_EMBED_DTYPE?.trim();
|
|
80
|
+
if (explicit) return explicit;
|
|
81
|
+
return getEmbeddingModel() === DEFAULT_EMBEDDING_MODEL ? DEFAULT_MODEL_DTYPE : void 0;
|
|
82
|
+
}
|
|
83
|
+
function extractorKey() {
|
|
84
|
+
return `${getEmbeddingModel()}|${getEmbeddingDevice() ?? ""}|${getEmbeddingDtype() ?? ""}`;
|
|
85
|
+
}
|
|
65
86
|
async function getExtractor() {
|
|
66
|
-
const wanted =
|
|
67
|
-
if (extractor &&
|
|
87
|
+
const wanted = extractorKey();
|
|
88
|
+
if (extractor && loadedKey === wanted) return extractor;
|
|
68
89
|
if (!pipeline) {
|
|
69
90
|
const mod = await import("@huggingface/transformers");
|
|
70
91
|
pipeline = mod.pipeline;
|
|
71
92
|
}
|
|
72
|
-
|
|
73
|
-
|
|
93
|
+
const device = getEmbeddingDevice();
|
|
94
|
+
const dtype = getEmbeddingDtype();
|
|
95
|
+
extractor = await pipeline("feature-extraction", getEmbeddingModel(), {
|
|
96
|
+
revision: "main",
|
|
97
|
+
// Omitted entirely when unset, so an unconfigured install gets exactly the
|
|
98
|
+
// library defaults it got before these existed.
|
|
99
|
+
...device ? { device } : {},
|
|
100
|
+
...dtype ? { dtype } : {}
|
|
74
101
|
});
|
|
75
|
-
|
|
102
|
+
loadedKey = wanted;
|
|
76
103
|
cachedDim = null;
|
|
77
104
|
return extractor;
|
|
78
105
|
}
|
|
79
106
|
async function getEmbeddingDim() {
|
|
80
|
-
if (cachedDim !== null &&
|
|
107
|
+
if (cachedDim !== null && loadedKey === extractorKey()) return cachedDim;
|
|
81
108
|
const probe = await encode("dimension probe");
|
|
82
109
|
cachedDim = probe.length;
|
|
83
110
|
return cachedDim;
|
|
@@ -140,15 +167,19 @@ function cosineSimilarity(a, b) {
|
|
|
140
167
|
for (let i = 0; i < a.length; i++) dot += a[i] * b[i];
|
|
141
168
|
return dot;
|
|
142
169
|
}
|
|
143
|
-
var pipeline, extractor,
|
|
170
|
+
var pipeline, extractor, loadedKey, cachedDim, DEFAULT_EMBEDDING_MODEL, LEGACY_DEFAULT_EMBEDDING_MODEL, LEGACY_DEFAULT_DIM, DEFAULT_MODEL_DTYPE, pinnedModel, CLS_POOLED_MODELS;
|
|
144
171
|
var init_embedding = __esm({
|
|
145
172
|
"../amem-core/src/embedding.ts"() {
|
|
146
173
|
"use strict";
|
|
147
174
|
pipeline = null;
|
|
148
175
|
extractor = null;
|
|
149
|
-
|
|
176
|
+
loadedKey = null;
|
|
150
177
|
cachedDim = null;
|
|
151
|
-
DEFAULT_EMBEDDING_MODEL = "Xenova/
|
|
178
|
+
DEFAULT_EMBEDDING_MODEL = "Xenova/bge-m3";
|
|
179
|
+
LEGACY_DEFAULT_EMBEDDING_MODEL = "Xenova/paraphrase-multilingual-MiniLM-L12-v2";
|
|
180
|
+
LEGACY_DEFAULT_DIM = 384;
|
|
181
|
+
DEFAULT_MODEL_DTYPE = "fp16";
|
|
182
|
+
pinnedModel = null;
|
|
152
183
|
CLS_POOLED_MODELS = /* @__PURE__ */ new Set([
|
|
153
184
|
"bge-m3",
|
|
154
185
|
"bge-base-zh-v1.5",
|
|
@@ -172,13 +203,29 @@ function canWrite(note, callerAgentId) {
|
|
|
172
203
|
function canRead(note, callerAgentId) {
|
|
173
204
|
return note.owner === callerAgentId || note.readers.includes(callerAgentId) || note.readers.includes("*");
|
|
174
205
|
}
|
|
206
|
+
var SYSTEM_ACTOR;
|
|
175
207
|
var init_auth = __esm({
|
|
176
208
|
"../amem-core/src/auth.ts"() {
|
|
177
209
|
"use strict";
|
|
210
|
+
SYSTEM_ACTOR = "__amem_system__";
|
|
178
211
|
}
|
|
179
212
|
});
|
|
180
213
|
|
|
181
214
|
// ../amem-core/src/storage.ts
|
|
215
|
+
function migrationHint(collection, targetModel) {
|
|
216
|
+
return `migrate onto it:
|
|
217
|
+
|
|
218
|
+
AMEM_EMBED_MODEL=${targetModel} \\
|
|
219
|
+
npx --package=@amemhq/core amem-migrate --from-collection ${collection}
|
|
220
|
+
|
|
221
|
+
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.`;
|
|
222
|
+
}
|
|
223
|
+
async function recordCollectionModel(collection, model) {
|
|
224
|
+
try {
|
|
225
|
+
await qdrant("PATCH", `/collections/${collection}`, { metadata: { embedding_model: model } });
|
|
226
|
+
} catch {
|
|
227
|
+
}
|
|
228
|
+
}
|
|
182
229
|
async function qdrant(method, path6, body) {
|
|
183
230
|
const res = await fetch(`${QDRANT_URL}${path6}`, {
|
|
184
231
|
method,
|
|
@@ -213,12 +260,40 @@ async function ensureCollection(collectionName) {
|
|
|
213
260
|
}
|
|
214
261
|
if (existing) {
|
|
215
262
|
const collectionDim = existing.config?.params?.vectors?.size;
|
|
263
|
+
const recorded = existing.config?.metadata?.embedding_model;
|
|
264
|
+
const explicit = process.env.AMEM_EMBED_MODEL?.trim();
|
|
265
|
+
const inferLegacy = !explicit && recorded === void 0 && collectionDim === LEGACY_DEFAULT_DIM;
|
|
266
|
+
if (!explicit) {
|
|
267
|
+
const wanted = (
|
|
268
|
+
// The collection says what built it, which outranks whatever the shipped
|
|
269
|
+
// default happens to be today. This is what keeps changing the default
|
|
270
|
+
// from breaking every install that already has data.
|
|
271
|
+
typeof recorded === "string" ? recorded : inferLegacy ? LEGACY_DEFAULT_EMBEDDING_MODEL : DEFAULT_EMBEDDING_MODEL
|
|
272
|
+
);
|
|
273
|
+
const inUse = getPinnedEmbeddingModel();
|
|
274
|
+
if (inUse !== null && inUse !== wanted) throw new MixedEmbeddingModelsError(col, wanted, inUse);
|
|
275
|
+
pinEmbeddingModel(wanted);
|
|
276
|
+
if (wanted === LEGACY_DEFAULT_EMBEDDING_MODEL) {
|
|
277
|
+
console.warn(
|
|
278
|
+
`[amem] "${col}" is on ${LEGACY_DEFAULT_EMBEDDING_MODEL} (${LEGACY_DEFAULT_DIM}-dim).
|
|
279
|
+
[amem] ${DEFAULT_EMBEDDING_MODEL} reads 8192 tokens where that one stops at 128, so anything longer is being truncated before it reaches the vector.
|
|
280
|
+
[amem] To move: npx --package=@amemhq/core amem-migrate --from-collection ${col}`
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
216
284
|
if (typeof collectionDim === "number") {
|
|
217
285
|
const modelDim = await getEmbeddingDim();
|
|
218
286
|
if (collectionDim !== modelDim) {
|
|
219
287
|
throw new EmbeddingDimensionMismatchError(col, collectionDim, modelDim, getEmbeddingModel());
|
|
220
288
|
}
|
|
221
289
|
}
|
|
290
|
+
const current = getEmbeddingModel();
|
|
291
|
+
if (typeof recorded === "string" && recorded !== current) {
|
|
292
|
+
throw new EmbeddingModelMismatchError(col, recorded, current);
|
|
293
|
+
}
|
|
294
|
+
if (recorded === void 0 && !inferLegacy) {
|
|
295
|
+
await recordCollectionModel(col, current);
|
|
296
|
+
}
|
|
222
297
|
markReady();
|
|
223
298
|
return;
|
|
224
299
|
}
|
|
@@ -230,6 +305,9 @@ async function ensureCollection(collectionName) {
|
|
|
230
305
|
} catch (err) {
|
|
231
306
|
if (!(err instanceof Error) || !err.message.includes("already exists")) throw err;
|
|
232
307
|
}
|
|
308
|
+
const created = getEmbeddingModel();
|
|
309
|
+
await recordCollectionModel(col, created);
|
|
310
|
+
if (!process.env.AMEM_EMBED_MODEL?.trim()) pinEmbeddingModel(created);
|
|
233
311
|
await qdrant("PUT", `/collections/${col}/index`, {
|
|
234
312
|
field_name: "agent_id",
|
|
235
313
|
field_schema: "keyword"
|
|
@@ -275,6 +353,7 @@ async function collectionDimRaw(collection) {
|
|
|
275
353
|
}
|
|
276
354
|
async function createCollectionRaw(collection, size) {
|
|
277
355
|
await qdrant("PUT", `/collections/${collection}`, { vectors: { size, distance: "Cosine" } });
|
|
356
|
+
await recordCollectionModel(collection, getEmbeddingModel());
|
|
278
357
|
for (const field_name of ["agent_id", "hash", "topics", "subjects"]) {
|
|
279
358
|
await qdrant("PUT", `/collections/${collection}/index`, { field_name, field_schema: "keyword" });
|
|
280
359
|
}
|
|
@@ -282,6 +361,43 @@ async function createCollectionRaw(collection, size) {
|
|
|
282
361
|
async function upsertPointsRaw(collection, points) {
|
|
283
362
|
await qdrant("PUT", `/collections/${collection}/points?wait=true`, { points });
|
|
284
363
|
}
|
|
364
|
+
async function scrollIdsRaw(collection, limit = 1e4) {
|
|
365
|
+
const ids = /* @__PURE__ */ new Set();
|
|
366
|
+
let offset = void 0;
|
|
367
|
+
for (; ; ) {
|
|
368
|
+
const body = { with_payload: false, with_vector: false, limit };
|
|
369
|
+
if (offset !== void 0 && offset !== null) body.offset = offset;
|
|
370
|
+
const res = await qdrant("POST", `/collections/${collection}/points/scroll`, body);
|
|
371
|
+
for (const p of res.points) ids.add(String(p.id));
|
|
372
|
+
offset = res.next_page_offset;
|
|
373
|
+
if (offset === void 0 || offset === null || res.points.length === 0) break;
|
|
374
|
+
}
|
|
375
|
+
return ids;
|
|
376
|
+
}
|
|
377
|
+
async function deleteCollectionRaw(collection) {
|
|
378
|
+
await qdrant("DELETE", `/collections/${collection}`);
|
|
379
|
+
}
|
|
380
|
+
async function resolveAliasRaw(alias) {
|
|
381
|
+
try {
|
|
382
|
+
const res = await qdrant("GET", `/aliases`);
|
|
383
|
+
return res.aliases.find((a) => a.alias_name === alias)?.collection_name ?? null;
|
|
384
|
+
} catch {
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
async function createAliasRaw(alias, collection) {
|
|
389
|
+
await qdrant("POST", `/collections/aliases`, {
|
|
390
|
+
actions: [{ create_alias: { collection_name: collection, alias_name: alias } }]
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
async function setAliasRaw(alias, collection) {
|
|
394
|
+
await qdrant("POST", `/collections/aliases`, {
|
|
395
|
+
actions: [
|
|
396
|
+
{ delete_alias: { alias_name: alias } },
|
|
397
|
+
{ create_alias: { collection_name: collection, alias_name: alias } }
|
|
398
|
+
]
|
|
399
|
+
});
|
|
400
|
+
}
|
|
285
401
|
function noteToPoint(note) {
|
|
286
402
|
return {
|
|
287
403
|
id: note.id,
|
|
@@ -426,12 +542,15 @@ function makeCrud(collectionName, modeBIsolated = false) {
|
|
|
426
542
|
},
|
|
427
543
|
/**
|
|
428
544
|
* Story 36: this is the one read that bypasses the agent filter β it fetches
|
|
429
|
-
* straight by UUID.
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
*
|
|
545
|
+
* straight by UUID. An unreadable note comes back as `null`, indistinguishable
|
|
546
|
+
* from missing, so nothing leaks and callers already handle it.
|
|
547
|
+
*
|
|
548
|
+
* `reader` is required. It used to be optional, and omitting it skipped the
|
|
549
|
+
* check β which meant the safe behaviour was the one you had to remember to
|
|
550
|
+
* ask for. Pass `SYSTEM_ACTOR` to read as the engine itself; that reads as a
|
|
551
|
+
* deliberate act at the call site, where an absent argument did not.
|
|
433
552
|
*/
|
|
434
|
-
async getNote(id,
|
|
553
|
+
async getNote(id, reader) {
|
|
435
554
|
await ensureCollection(col);
|
|
436
555
|
try {
|
|
437
556
|
const result = await qdrant("POST", `/collections/${col}/points`, {
|
|
@@ -441,7 +560,7 @@ function makeCrud(collectionName, modeBIsolated = false) {
|
|
|
441
560
|
});
|
|
442
561
|
if (!result.length) return null;
|
|
443
562
|
const note = pointToNote(result[0]);
|
|
444
|
-
if (
|
|
563
|
+
if (reader !== SYSTEM_ACTOR && !canRead(note, reader)) return null;
|
|
445
564
|
return note;
|
|
446
565
|
} catch {
|
|
447
566
|
return null;
|
|
@@ -479,19 +598,22 @@ function makeCrud(collectionName, modeBIsolated = false) {
|
|
|
479
598
|
return pointToNote(result.points[0]);
|
|
480
599
|
},
|
|
481
600
|
/**
|
|
482
|
-
* Story 33:
|
|
483
|
-
*
|
|
484
|
-
*
|
|
485
|
-
*
|
|
486
|
-
*
|
|
487
|
-
*
|
|
601
|
+
* Story 33: enforces the writers policy. Returns false β without writing β
|
|
602
|
+
* when the caller may not write. This fetch-then-check path exists for callers
|
|
603
|
+
* that only have an id (the plugin's CRUD hook); callers already holding the
|
|
604
|
+
* note can check `canWrite` themselves and skip a round trip.
|
|
605
|
+
*
|
|
606
|
+
* `caller` is required. Pass `SYSTEM_ACTOR` for the engine's own maintenance
|
|
607
|
+
* writes. The self-read below is a genuine `SYSTEM_ACTOR` case: it fetches the
|
|
608
|
+
* note in order to decide whether the caller may write it, and gating that
|
|
609
|
+
* fetch on the same policy it exists to evaluate would be circular.
|
|
488
610
|
*/
|
|
489
|
-
async updateNoteContent(id, content, embedding, hash,
|
|
611
|
+
async updateNoteContent(id, content, embedding, hash, caller) {
|
|
490
612
|
await ensureCollection(col);
|
|
491
613
|
let existing = null;
|
|
492
|
-
if (
|
|
493
|
-
existing = await this.getNote(id);
|
|
494
|
-
if (existing && !canWrite(existing,
|
|
614
|
+
if (caller !== SYSTEM_ACTOR) {
|
|
615
|
+
existing = await this.getNote(id, SYSTEM_ACTOR);
|
|
616
|
+
if (existing && !canWrite(existing, caller)) return false;
|
|
495
617
|
}
|
|
496
618
|
await qdrant("PUT", `/collections/${col}/points/vectors?wait=true`, {
|
|
497
619
|
points: [{ id, vector: embedding }]
|
|
@@ -579,11 +701,11 @@ function makeCrud(collectionName, modeBIsolated = false) {
|
|
|
579
701
|
});
|
|
580
702
|
},
|
|
581
703
|
/** Story 33: see `updateNoteContent` β returns false, unwritten, when denied. */
|
|
582
|
-
async invalidateNote(id,
|
|
704
|
+
async invalidateNote(id, caller) {
|
|
583
705
|
await ensureCollection(col);
|
|
584
|
-
if (
|
|
585
|
-
const existing = await this.getNote(id);
|
|
586
|
-
if (existing && !canWrite(existing,
|
|
706
|
+
if (caller !== SYSTEM_ACTOR) {
|
|
707
|
+
const existing = await this.getNote(id, SYSTEM_ACTOR);
|
|
708
|
+
if (existing && !canWrite(existing, caller)) return false;
|
|
587
709
|
}
|
|
588
710
|
await qdrant("POST", `/collections/${col}/points/payload?wait=true`, {
|
|
589
711
|
payload: { is_active: false },
|
|
@@ -649,8 +771,8 @@ function makeCrud(collectionName, modeBIsolated = false) {
|
|
|
649
771
|
function createStorageContext(collectionName, modeBIsolated = false) {
|
|
650
772
|
return makeCrud(collectionName || getCollection(), modeBIsolated);
|
|
651
773
|
}
|
|
652
|
-
async function getNote(id,
|
|
653
|
-
return makeCrud(getCollection()).getNote(id,
|
|
774
|
+
async function getNote(id, reader) {
|
|
775
|
+
return makeCrud(getCollection()).getNote(id, reader);
|
|
654
776
|
}
|
|
655
777
|
async function updateNote(note) {
|
|
656
778
|
return makeCrud(getCollection()).updateNote(note);
|
|
@@ -661,13 +783,13 @@ async function listNotes(agentId, subject) {
|
|
|
661
783
|
async function deleteNote(id) {
|
|
662
784
|
return makeCrud(getCollection()).deleteNote(id);
|
|
663
785
|
}
|
|
664
|
-
async function invalidateNote(id,
|
|
665
|
-
return makeCrud(getCollection()).invalidateNote(id,
|
|
786
|
+
async function invalidateNote(id, caller) {
|
|
787
|
+
return makeCrud(getCollection()).invalidateNote(id, caller);
|
|
666
788
|
}
|
|
667
789
|
async function patchNotePayload(id, fields) {
|
|
668
790
|
return makeCrud(getCollection()).patchNotePayload(id, fields);
|
|
669
791
|
}
|
|
670
|
-
var QDRANT_URL, getCollection, EmbeddingDimensionMismatchError, _collectionReady, _collectionReadyMap;
|
|
792
|
+
var QDRANT_URL, getCollection, EmbeddingDimensionMismatchError, EmbeddingModelMismatchError, MixedEmbeddingModelsError, _collectionReady, _collectionReadyMap;
|
|
671
793
|
var init_storage = __esm({
|
|
672
794
|
"../amem-core/src/storage.ts"() {
|
|
673
795
|
"use strict";
|
|
@@ -679,7 +801,7 @@ var init_storage = __esm({
|
|
|
679
801
|
constructor(collection, collectionDim, modelDim, model) {
|
|
680
802
|
super(
|
|
681
803
|
`Collection "${collection}" stores ${collectionDim}-dimension vectors, but the embedding model "${model}" produces ${modelDim}. Qdrant fixes a collection's vector size at creation and cannot change it, so writes and searches would both fail.
|
|
682
|
-
Either set AMEM_EMBED_MODEL back to the model this collection was built with, or
|
|
804
|
+
Either set AMEM_EMBED_MODEL back to the model this collection was built with, or ${migrationHint(collection, model)}`
|
|
683
805
|
);
|
|
684
806
|
this.collection = collection;
|
|
685
807
|
this.collectionDim = collectionDim;
|
|
@@ -692,6 +814,40 @@ Either set AMEM_EMBED_MODEL back to the model this collection was built with, or
|
|
|
692
814
|
modelDim;
|
|
693
815
|
model;
|
|
694
816
|
};
|
|
817
|
+
EmbeddingModelMismatchError = class extends Error {
|
|
818
|
+
constructor(collection, collectionModel, configuredModel) {
|
|
819
|
+
super(
|
|
820
|
+
`Collection "${collection}" was built with the embedding model "${collectionModel}", but this process is configured for "${configuredModel}". Both produce vectors of the same width, so nothing would fail \u2014 searches would just quietly compare vectors from two different models.
|
|
821
|
+
Either set AMEM_EMBED_MODEL back to "${collectionModel}", or ` + migrationHint(collection, configuredModel)
|
|
822
|
+
);
|
|
823
|
+
this.collection = collection;
|
|
824
|
+
this.collectionModel = collectionModel;
|
|
825
|
+
this.configuredModel = configuredModel;
|
|
826
|
+
this.name = "EmbeddingModelMismatchError";
|
|
827
|
+
}
|
|
828
|
+
collection;
|
|
829
|
+
collectionModel;
|
|
830
|
+
configuredModel;
|
|
831
|
+
};
|
|
832
|
+
MixedEmbeddingModelsError = class extends Error {
|
|
833
|
+
constructor(collection, wanted, inUse) {
|
|
834
|
+
super(
|
|
835
|
+
`Collection "${collection}" was built with "${wanted}", but this process is already embedding with "${inUse}" for another collection. One process can only use one model.
|
|
836
|
+
Migrate the remaining collections so they all agree:
|
|
837
|
+
|
|
838
|
+
npx --package=@amemhq/core amem-migrate --from-collection ${collection}
|
|
839
|
+
|
|
840
|
+
Or set AMEM_EMBED_MODEL to pin every collection to one model, which is only correct if they really were all built with it.`
|
|
841
|
+
);
|
|
842
|
+
this.collection = collection;
|
|
843
|
+
this.wanted = wanted;
|
|
844
|
+
this.inUse = inUse;
|
|
845
|
+
this.name = "MixedEmbeddingModelsError";
|
|
846
|
+
}
|
|
847
|
+
collection;
|
|
848
|
+
wanted;
|
|
849
|
+
inUse;
|
|
850
|
+
};
|
|
695
851
|
_collectionReady = false;
|
|
696
852
|
_collectionReadyMap = /* @__PURE__ */ new Map();
|
|
697
853
|
}
|
|
@@ -1457,7 +1613,7 @@ async function addMemory(content, agentId = "main", opts) {
|
|
|
1457
1613
|
if (topMatch.length > 0 && topMatch[0].score >= 0.85) {
|
|
1458
1614
|
if (canWrite(topMatch[0].note, agentId)) {
|
|
1459
1615
|
console.log(`[add] dedup: high-sim match (sim=${topMatch[0].score.toFixed(3)}), updating existing`);
|
|
1460
|
-
await ctx.updateNoteContent(topMatch[0].note.id, content, embedding, hash);
|
|
1616
|
+
await ctx.updateNoteContent(topMatch[0].note.id, content, embedding, hash, agentId);
|
|
1461
1617
|
return topMatch[0].note.id;
|
|
1462
1618
|
}
|
|
1463
1619
|
console.log(
|
|
@@ -1529,7 +1685,7 @@ async function addMemory(content, agentId = "main", opts) {
|
|
|
1529
1685
|
note.links = linkedIds;
|
|
1530
1686
|
await ctx.updateNote(note);
|
|
1531
1687
|
for (const lid of linkedIds) {
|
|
1532
|
-
const linked = await ctx.getNote(lid);
|
|
1688
|
+
const linked = await ctx.getNote(lid, agentId);
|
|
1533
1689
|
if (linked && !linked.links.includes(note.id)) {
|
|
1534
1690
|
if (!canWrite(linked, agentId)) {
|
|
1535
1691
|
console.log(`[link] back-link into ${lid.slice(0, 8)} skipped \u2014 not writable by ${logSafe(agentId)}`);
|
|
@@ -1542,7 +1698,7 @@ async function addMemory(content, agentId = "main", opts) {
|
|
|
1542
1698
|
if (shouldRunEvolution()) {
|
|
1543
1699
|
console.log(` [evo] threshold reached, running evolution for ${Math.min(linkedIds.length, 3)} linked notes`);
|
|
1544
1700
|
for (const lid of linkedIds.slice(0, 3)) {
|
|
1545
|
-
const linked = await ctx.getNote(lid);
|
|
1701
|
+
const linked = await ctx.getNote(lid, agentId);
|
|
1546
1702
|
if (!linked) continue;
|
|
1547
1703
|
if (!canWrite(linked, agentId)) {
|
|
1548
1704
|
console.log(` [evo] skipping ${lid.slice(0, 8)} \u2014 not writable by ${logSafe(agentId)}`);
|
|
@@ -1695,7 +1851,7 @@ async function searchMemory(query, topK = 5, agentId = "main", opts) {
|
|
|
1695
1851
|
const allNotes = await ctx.listNotes(agentId, subject);
|
|
1696
1852
|
const bm25State = buildBM25(allNotes);
|
|
1697
1853
|
const queryTokens = simpleTokenize(query);
|
|
1698
|
-
const bm25Ranked = bm25Score(bm25State, queryTokens).slice(0, n);
|
|
1854
|
+
const bm25Ranked = bm25Score(bm25State, queryTokens).filter(([, score]) => score > 0).slice(0, n);
|
|
1699
1855
|
const merged = rrfMerge(
|
|
1700
1856
|
embResults.map((r) => r.note.id),
|
|
1701
1857
|
bm25Ranked.map((r) => r[0])
|
|
@@ -1718,6 +1874,7 @@ async function searchMemory(query, topK = 5, agentId = "main", opts) {
|
|
|
1718
1874
|
const visitedIds = new Set(topIds);
|
|
1719
1875
|
const bfsQueue = useBfs ? topIds.map((id) => ({ id, hop: 0 })) : [];
|
|
1720
1876
|
const bfsExtra = [];
|
|
1877
|
+
const bfsSimMap = /* @__PURE__ */ new Map();
|
|
1721
1878
|
while (bfsQueue.length > 0 && bfsExtra.length < BFS_MAX_EXPAND) {
|
|
1722
1879
|
const item = bfsQueue.shift();
|
|
1723
1880
|
if (item.hop >= BFS_MAX_HOPS) continue;
|
|
@@ -1728,10 +1885,9 @@ async function searchMemory(query, topK = 5, agentId = "main", opts) {
|
|
|
1728
1885
|
visitedIds.add(linkedId);
|
|
1729
1886
|
const linked = noteMap.get(linkedId);
|
|
1730
1887
|
if (!linked || linked.is_active === false) continue;
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
}
|
|
1888
|
+
const sim = cosineSimilarity(queryEmbedding, linked.embedding);
|
|
1889
|
+
if (bfsSimThreshold > 0 && sim < bfsSimThreshold) continue;
|
|
1890
|
+
bfsSimMap.set(linkedId, sim);
|
|
1735
1891
|
bfsExtra.push(linkedId);
|
|
1736
1892
|
bfsQueue.push({ id: linkedId, hop: item.hop + 1 });
|
|
1737
1893
|
if (bfsExtra.length >= BFS_MAX_EXPAND) break;
|
|
@@ -1747,7 +1903,11 @@ async function searchMemory(query, topK = 5, agentId = "main", opts) {
|
|
|
1747
1903
|
const embSimMap = new Map(embResults.map((r) => [r.note.id, r.score]));
|
|
1748
1904
|
const rrfMap = new Map(boostedMerged.map(([id, score]) => [id, score]));
|
|
1749
1905
|
const results = [];
|
|
1750
|
-
|
|
1906
|
+
const ordered = [
|
|
1907
|
+
...filteredTopIds.map((id) => [id, "match"]),
|
|
1908
|
+
...bfsExtra.map((id) => [id, "link"])
|
|
1909
|
+
];
|
|
1910
|
+
for (const [id, via] of ordered) {
|
|
1751
1911
|
const note = noteMap.get(id);
|
|
1752
1912
|
if (!note) continue;
|
|
1753
1913
|
results.push({
|
|
@@ -1758,8 +1918,9 @@ async function searchMemory(query, topK = 5, agentId = "main", opts) {
|
|
|
1758
1918
|
keywords: note.keywords,
|
|
1759
1919
|
links: note.links,
|
|
1760
1920
|
timestamp: note.timestamp,
|
|
1761
|
-
similarity: embSimMap.get(id) ?? 0,
|
|
1921
|
+
similarity: embSimMap.get(id) ?? bfsSimMap.get(id) ?? 0,
|
|
1762
1922
|
rrf: rrfMap.get(id) ?? 0,
|
|
1923
|
+
via,
|
|
1763
1924
|
topics: note.topics ?? [],
|
|
1764
1925
|
note_type: note.note_type ?? "memory"
|
|
1765
1926
|
});
|
|
@@ -1816,7 +1977,7 @@ async function mergeSimilarNotes(agentId, storageCtx) {
|
|
|
1816
1977
|
const mergedContent = judgment.mergedContent || pendingNote.content;
|
|
1817
1978
|
const newEmbedding = await encode(buildEmbedText({ ...bestNeighbor, content: mergedContent }));
|
|
1818
1979
|
const newHash = (0, import_crypto.createHash)("md5").update(mergedContent).digest("hex");
|
|
1819
|
-
await ctx.updateNoteContent(bestNeighbor.id, mergedContent, newEmbedding, newHash);
|
|
1980
|
+
await ctx.updateNoteContent(bestNeighbor.id, mergedContent, newEmbedding, newHash, agentId);
|
|
1820
1981
|
await ctx.patchNotePayload(bestNeighbor.id, {
|
|
1821
1982
|
evolution_history: JSON.stringify(oldHistory),
|
|
1822
1983
|
evolution_type: "EVOLVE"
|
|
@@ -1840,7 +2001,7 @@ async function mergeSimilarNotes(agentId, storageCtx) {
|
|
|
1840
2001
|
const mergedContent = judgment.mergedContent || `${bestNeighbor.content}\uFF1B${pendingNote.content}`;
|
|
1841
2002
|
const newEmbedding = await encode(buildEmbedText({ ...bestNeighbor, content: mergedContent }));
|
|
1842
2003
|
const newHash = (0, import_crypto.createHash)("md5").update(mergedContent).digest("hex");
|
|
1843
|
-
await ctx.updateNoteContent(bestNeighbor.id, mergedContent, newEmbedding, newHash);
|
|
2004
|
+
await ctx.updateNoteContent(bestNeighbor.id, mergedContent, newEmbedding, newHash, agentId);
|
|
1844
2005
|
await ctx.patchNotePayload(bestNeighbor.id, {
|
|
1845
2006
|
evolution_history: JSON.stringify(oldHistory),
|
|
1846
2007
|
evolution_type: "EXPAND"
|
|
@@ -1880,7 +2041,7 @@ async function mergeSimilarNotes(agentId, storageCtx) {
|
|
|
1880
2041
|
const [keepNote, dropNote] = noteA.content.length >= noteB.content.length ? [noteA, noteB] : [noteB, noteA];
|
|
1881
2042
|
const newEmbedding = await encode(result.merged);
|
|
1882
2043
|
const newHash = (0, import_crypto.createHash)("md5").update(result.merged).digest("hex");
|
|
1883
|
-
await ctx.updateNoteContent(keepNote.id, result.merged, newEmbedding, newHash);
|
|
2044
|
+
await ctx.updateNoteContent(keepNote.id, result.merged, newEmbedding, newHash, agentId);
|
|
1884
2045
|
await ctx.deleteNote(dropNote.id);
|
|
1885
2046
|
deletedIds.add(dropNote.id);
|
|
1886
2047
|
mergedCount++;
|
|
@@ -1989,7 +2150,7 @@ async function consolidateMemories(agentId, logger, storageCtx) {
|
|
|
1989
2150
|
action: "consolidate"
|
|
1990
2151
|
});
|
|
1991
2152
|
await ctx.updateNote(keepNote);
|
|
1992
|
-
await ctx.invalidateNote(dropNote.id);
|
|
2153
|
+
await ctx.invalidateNote(dropNote.id, agentId);
|
|
1993
2154
|
await ctx.replaceLinkReferences(dropNote.id, keepNote.id, agentId);
|
|
1994
2155
|
logMergeToFile(keepNote.id, dropNote.id, keepNote.content);
|
|
1995
2156
|
processedIds.add(keepNote.id);
|
|
@@ -2312,8 +2473,19 @@ async function migrateCollection(opts) {
|
|
|
2312
2473
|
);
|
|
2313
2474
|
if (dryRun) {
|
|
2314
2475
|
log("[migrate] dry run \u2014 nothing written. Pass dryRun: false to apply.");
|
|
2315
|
-
return {
|
|
2476
|
+
return {
|
|
2477
|
+
total: notes.length,
|
|
2478
|
+
missingDerived,
|
|
2479
|
+
refreshed: 0,
|
|
2480
|
+
migrated: 0,
|
|
2481
|
+
skipped: 0,
|
|
2482
|
+
sourceDim,
|
|
2483
|
+
targetDim,
|
|
2484
|
+
model,
|
|
2485
|
+
dryRun: true
|
|
2486
|
+
};
|
|
2316
2487
|
}
|
|
2488
|
+
let alreadyDone = /* @__PURE__ */ new Set();
|
|
2317
2489
|
const existingTargetDim = await collectionDimRaw(to);
|
|
2318
2490
|
if (existingTargetDim === null) {
|
|
2319
2491
|
await createCollectionRaw(to, targetDim);
|
|
@@ -2322,9 +2494,17 @@ async function migrateCollection(opts) {
|
|
|
2322
2494
|
if (existingTargetDim !== targetDim) {
|
|
2323
2495
|
throw new Error(`migrate: target "${to}" exists at ${existingTargetDim}d but the model produces ${targetDim}d`);
|
|
2324
2496
|
}
|
|
2325
|
-
const
|
|
2326
|
-
if (
|
|
2327
|
-
|
|
2497
|
+
const present = await scrollIdsRaw(to);
|
|
2498
|
+
if (present.size > 0) {
|
|
2499
|
+
const sourceIds = new Set(notes.map((n) => n.id));
|
|
2500
|
+
const foreign = [...present].filter((id) => !sourceIds.has(id));
|
|
2501
|
+
if (foreign.length > 0) {
|
|
2502
|
+
throw new Error(
|
|
2503
|
+
`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.`
|
|
2504
|
+
);
|
|
2505
|
+
}
|
|
2506
|
+
alreadyDone = present;
|
|
2507
|
+
log(`[migrate] resuming: ${present.size} of ${notes.length} already in ${to}`);
|
|
2328
2508
|
}
|
|
2329
2509
|
}
|
|
2330
2510
|
let refreshed = 0;
|
|
@@ -2338,6 +2518,7 @@ async function migrateCollection(opts) {
|
|
|
2338
2518
|
buffer = [];
|
|
2339
2519
|
};
|
|
2340
2520
|
for (const note of notes) {
|
|
2521
|
+
if (alreadyDone.has(note.id)) continue;
|
|
2341
2522
|
if (refreshFields && missingDerivedFields(note)) {
|
|
2342
2523
|
try {
|
|
2343
2524
|
const built = await llmConstructNote(note.content);
|
|
@@ -2353,7 +2534,7 @@ async function migrateCollection(opts) {
|
|
|
2353
2534
|
buffer.push(point);
|
|
2354
2535
|
if (buffer.length >= BATCH) {
|
|
2355
2536
|
await flush();
|
|
2356
|
-
log(`[migrate] ${migrated}/${notes.length}`);
|
|
2537
|
+
log(`[migrate] ${alreadyDone.size + migrated}/${notes.length}`);
|
|
2357
2538
|
}
|
|
2358
2539
|
}
|
|
2359
2540
|
await flush();
|
|
@@ -2361,10 +2542,46 @@ async function migrateCollection(opts) {
|
|
|
2361
2542
|
if (finalCount !== notes.length) {
|
|
2362
2543
|
warn(`[migrate] target holds ${finalCount} point(s) but the source had ${notes.length} \u2014 check before switching`);
|
|
2363
2544
|
}
|
|
2364
|
-
log(
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2545
|
+
log(`[migrate] done: ${migrated} written, ${alreadyDone.size} already present, ${refreshed} re-extracted.`);
|
|
2546
|
+
return {
|
|
2547
|
+
total: notes.length,
|
|
2548
|
+
missingDerived,
|
|
2549
|
+
refreshed,
|
|
2550
|
+
migrated,
|
|
2551
|
+
skipped: alreadyDone.size,
|
|
2552
|
+
sourceDim,
|
|
2553
|
+
targetDim,
|
|
2554
|
+
model,
|
|
2555
|
+
dryRun: false
|
|
2556
|
+
};
|
|
2557
|
+
}
|
|
2558
|
+
async function switchToMigrated(opts) {
|
|
2559
|
+
const { name, to } = opts;
|
|
2560
|
+
const log = opts.logger?.info ?? ((m) => console.log(m));
|
|
2561
|
+
if (name === to) throw new Error(`switch: "${name}" and "${to}" are the same collection`);
|
|
2562
|
+
const already = await resolveAliasRaw(name);
|
|
2563
|
+
if (already === to) {
|
|
2564
|
+
log(`[switch] "${name}" already points at "${to}" \u2014 nothing to do`);
|
|
2565
|
+
return { name, to, moved: await countPointsRaw(to) };
|
|
2566
|
+
}
|
|
2567
|
+
const targetCount = await countPointsRaw(to);
|
|
2568
|
+
if (targetCount === 0) throw new Error(`switch: "${to}" is empty \u2014 migrate into it first`);
|
|
2569
|
+
if (already === null) {
|
|
2570
|
+
const sourceCount = await countPointsRaw(name);
|
|
2571
|
+
if (targetCount < sourceCount) {
|
|
2572
|
+
throw new Error(
|
|
2573
|
+
`switch: "${to}" holds ${targetCount} point(s) but "${name}" still holds ${sourceCount}. The migration is not finished \u2014 run it again before switching.`
|
|
2574
|
+
);
|
|
2575
|
+
}
|
|
2576
|
+
log(`[switch] verified ${targetCount} in "${to}" against ${sourceCount} in "${name}"`);
|
|
2577
|
+
await deleteCollectionRaw(name);
|
|
2578
|
+
log(`[switch] dropped "${name}"`);
|
|
2579
|
+
await createAliasRaw(name, to);
|
|
2580
|
+
} else {
|
|
2581
|
+
await setAliasRaw(name, to);
|
|
2582
|
+
}
|
|
2583
|
+
log(`[switch] "${name}" now resolves to "${to}"`);
|
|
2584
|
+
return { name, to, moved: targetCount };
|
|
2368
2585
|
}
|
|
2369
2586
|
var init_migrate = __esm({
|
|
2370
2587
|
"../amem-core/src/migrate.ts"() {
|
|
@@ -2403,6 +2620,11 @@ __export(src_exports, {
|
|
|
2403
2620
|
DEFAULT_CRUD_UPDATE_MIN_SIM: () => DEFAULT_CRUD_UPDATE_MIN_SIM,
|
|
2404
2621
|
DEFAULT_EMBEDDING_MODEL: () => DEFAULT_EMBEDDING_MODEL,
|
|
2405
2622
|
EmbeddingDimensionMismatchError: () => EmbeddingDimensionMismatchError,
|
|
2623
|
+
EmbeddingModelMismatchError: () => EmbeddingModelMismatchError,
|
|
2624
|
+
LEGACY_DEFAULT_DIM: () => LEGACY_DEFAULT_DIM,
|
|
2625
|
+
LEGACY_DEFAULT_EMBEDDING_MODEL: () => LEGACY_DEFAULT_EMBEDDING_MODEL,
|
|
2626
|
+
MixedEmbeddingModelsError: () => MixedEmbeddingModelsError,
|
|
2627
|
+
SYSTEM_ACTOR: () => SYSTEM_ACTOR,
|
|
2406
2628
|
addEpisodic: () => addEpisodic,
|
|
2407
2629
|
addMemory: () => addMemory,
|
|
2408
2630
|
canRead: () => canRead,
|
|
@@ -2417,7 +2639,9 @@ __export(src_exports, {
|
|
|
2417
2639
|
encode: () => encode,
|
|
2418
2640
|
ensureCollection: () => ensureCollection,
|
|
2419
2641
|
generateReviewBatch: () => generateReviewBatch,
|
|
2642
|
+
getEmbeddingDevice: () => getEmbeddingDevice,
|
|
2420
2643
|
getEmbeddingDim: () => getEmbeddingDim,
|
|
2644
|
+
getEmbeddingDtype: () => getEmbeddingDtype,
|
|
2421
2645
|
getEmbeddingModel: () => getEmbeddingModel,
|
|
2422
2646
|
getEmbeddingPooling: () => getEmbeddingPooling,
|
|
2423
2647
|
getNote: () => getNote,
|
|
@@ -2435,6 +2659,7 @@ __export(src_exports, {
|
|
|
2435
2659
|
resolveCrudUpdateMinSim: () => resolveCrudUpdateMinSim,
|
|
2436
2660
|
scanLowQuality: () => scanLowQuality,
|
|
2437
2661
|
searchMemory: () => searchMemory,
|
|
2662
|
+
switchToMigrated: () => switchToMigrated,
|
|
2438
2663
|
updateNote: () => updateNote
|
|
2439
2664
|
});
|
|
2440
2665
|
var init_src = __esm({
|
|
@@ -2552,7 +2777,7 @@ function register(api) {
|
|
|
2552
2777
|
`openclaw-amem: registered (native TS, Qdrant, default agent_id=${defaultScope.agentId}, default collection=${pluginConfig.collection ?? "amem_notes (default)"}, per-agent scope resolved per call)`
|
|
2553
2778
|
);
|
|
2554
2779
|
ensureCollection(pluginConfig.collection).catch((e) => {
|
|
2555
|
-
if (e instanceof EmbeddingDimensionMismatchError) {
|
|
2780
|
+
if (e instanceof EmbeddingDimensionMismatchError || e instanceof EmbeddingModelMismatchError || e instanceof MixedEmbeddingModelsError) {
|
|
2556
2781
|
logger.error(`openclaw-amem: memory is UNUSABLE \u2014 ${e.message}`);
|
|
2557
2782
|
} else {
|
|
2558
2783
|
logger.warn(`openclaw-amem: ensureCollection failed \u2014 ${e.message}`);
|
|
@@ -2675,9 +2900,13 @@ function register(api) {
|
|
|
2675
2900
|
details: { count: 0 }
|
|
2676
2901
|
};
|
|
2677
2902
|
}
|
|
2678
|
-
const
|
|
2903
|
+
const linked = results.filter((r) => r.via === "link").length;
|
|
2904
|
+
const text = results.map(
|
|
2905
|
+
(r, i) => `${i + 1}. ${r.content} (similarity ${(r.similarity * 100).toFixed(0)}%${r.via === "link" ? ", linked \u2014 did not match the query itself" : ""}, id: ${r.id})`
|
|
2906
|
+
).join("\n");
|
|
2907
|
+
const header = linked ? `Found ${results.length - linked} matching memories, plus ${linked} linked to them:` : `Found ${results.length} memories:`;
|
|
2679
2908
|
return {
|
|
2680
|
-
content: [{ type: "text", text:
|
|
2909
|
+
content: [{ type: "text", text: `${header}
|
|
2681
2910
|
|
|
2682
2911
|
${text}${hookWarning}` }],
|
|
2683
2912
|
details: { count: results.length, memories: results }
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
"id": "openclaw-amem",
|
|
3
3
|
"name": "amem",
|
|
4
4
|
"description": "Catches memories that contradict each other. Notes rewrite themselves as new ones arrive, link into a graph, and stay separated per agent and per person. Runs on a small model. Requires a local Qdrant. δΈζθ΅° jieba εθ―οΌζη€Ίθ―ζδΈζηγ",
|
|
5
|
-
"
|
|
5
|
+
"icon": "https://amem.owo.lc/logo.png",
|
|
6
|
+
"version": "2.0.0",
|
|
6
7
|
"kind": "memory",
|
|
7
8
|
"openclaw": {
|
|
8
9
|
"compat": {
|
|
@@ -40,6 +41,8 @@
|
|
|
40
41
|
"AMEM_CRUD_UPDATE_MIN_SIM",
|
|
41
42
|
"AMEM_EMBED_MODEL",
|
|
42
43
|
"AMEM_EMBED_POOLING",
|
|
44
|
+
"AMEM_EMBED_DEVICE",
|
|
45
|
+
"AMEM_EMBED_DTYPE",
|
|
43
46
|
"AMEM_COLLECTION",
|
|
44
47
|
"AMEM_DATA_DIR",
|
|
45
48
|
"AMEM_EVO_COUNTER_PATH",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclaw-amem",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Catches memories that contradict each other. Notes rewrite themselves as new ones arrive, link into a graph, and stay separated per agent and per person. Runs on a small model. Requires a local Qdrant. δΈζθ΅° jieba εθ―οΌζη€Ίθ―ζδΈζηγ",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|