openclaw-amem 1.3.0 → 1.4.1
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 +13 -8
- package/dist/index.js +225 -17
- package/openclaw.plugin.json +9 -3
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -1,21 +1,26 @@
|
|
|
1
1
|
# openclaw-amem
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
|
-
<img src="https://raw.githubusercontent.com/
|
|
4
|
+
<img src="https://raw.githubusercontent.com/amemhq/amem/main/docs/public/logo.webp" width="120" alt="amem logo" />
|
|
5
5
|
</p>
|
|
6
6
|
|
|
7
7
|
[](../../LICENSE)
|
|
8
8
|
[](https://www.npmjs.com/package/openclaw-amem)
|
|
9
9
|
[](https://arxiv.org/abs/2502.12110)
|
|
10
10
|
|
|
11
|
-
**
|
|
11
|
+
**Memory for [OpenClaw](https://github.com/openclaw/openclaw) agents that catches its own contradictions.**
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
A nightly pass re-reads what changed and flags memories that no longer agree with each other — the failure mode every long-lived memory store eventually has. The rest: facts extracted instead of transcripts stored, notes rewritten as new ones arrive, linked into a Zettelkasten-style graph, retrieved with hybrid BM25 + dense search over 2-hop expansion. Memories are scoped per agent (`owner`/`readers`/`writers`) and per subject, both enforced inside the Qdrant query rather than filtered after it.
|
|
14
|
+
|
|
15
|
+
Runs on a small model — `gpt-4o-mini`, `haiku`, a local Ollama. No Python.
|
|
16
|
+
|
|
17
|
+
中文走 [jieba](https://github.com/messense/node-jieba) 分词,提示词有完整中文版(`AMEM_PROMPT_LOCALE=zh`),embedding 模型本身多语言。
|
|
18
|
+
|
|
19
|
+
Requires a local [Qdrant](https://qdrant.tech). Implements [A-MEM](https://arxiv.org/abs/2502.12110) (arXiv 2502.12110); the engine itself is [`@amemhq/core`](../amem-core).
|
|
14
20
|
|
|
15
|
-
> 🧠 The memory **engine** lives in **[`amem-core`](../amem-core)**; this package is the thin OpenClaw plugin around it.
|
|
16
21
|
> 📖 Full guides, architecture & references: **[amem.owo.lc](https://amem.owo.lc)**.
|
|
17
22
|
|
|
18
|
-
⭐ Useful? [Star it on GitHub](https://github.com/
|
|
23
|
+
⭐ Useful? [Star it on GitHub](https://github.com/amemhq/amem).
|
|
19
24
|
|
|
20
25
|
## Highlights
|
|
21
26
|
|
|
@@ -26,7 +31,7 @@ The first open-source A-MEM memory plugin for OpenClaw: dynamic graph linking, h
|
|
|
26
31
|
- 🔐 **Per-agent isolation** — private by default; explicit `owner`/`readers`/`writers`; Mode A (shared collection) or Mode B (dedicated collection).
|
|
27
32
|
- 🀄 **Chinese-optimized** & local embeddings (Transformers.js, 384-dim) — no Python, no external embedding API.
|
|
28
33
|
|
|
29
|
-
→ Full feature list & internals: **[
|
|
34
|
+
→ Full feature list & internals: **[@amemhq/core README](../amem-core)** · **[docs](https://amem.owo.lc)**.
|
|
30
35
|
|
|
31
36
|
## Requirements
|
|
32
37
|
|
|
@@ -41,7 +46,7 @@ The first open-source A-MEM memory plugin for OpenClaw: dynamic graph linking, h
|
|
|
41
46
|
|
|
42
47
|
```bash
|
|
43
48
|
# From ClawHub (recommended)
|
|
44
|
-
openclaw plugins install clawhub
|
|
49
|
+
openclaw plugins install clawhub:openclaw-amem
|
|
45
50
|
|
|
46
51
|
# From npm
|
|
47
52
|
openclaw plugins install openclaw-amem
|
|
@@ -136,7 +141,7 @@ pnpm --filter openclaw-amem test # vitest (needs Qdrant on :6333)
|
|
|
136
141
|
|
|
137
142
|
## Docs & References
|
|
138
143
|
|
|
139
|
-
Full guides, architecture, and academic references: **[amem.owo.lc](https://amem.owo.lc)** · engine: **[
|
|
144
|
+
Full guides, architecture, and academic references: **[amem.owo.lc](https://amem.owo.lc)** · engine: **[@amemhq/core](../amem-core)** · paper: [A-MEM (arXiv:2502.12110)](https://arxiv.org/abs/2502.12110).
|
|
140
145
|
|
|
141
146
|
## License
|
|
142
147
|
|
package/dist/index.js
CHANGED
|
@@ -53,17 +53,29 @@ var init_config = __esm({
|
|
|
53
53
|
});
|
|
54
54
|
|
|
55
55
|
// ../amem-core/src/embedding.ts
|
|
56
|
+
function getEmbeddingModel() {
|
|
57
|
+
return process.env.AMEM_EMBED_MODEL?.trim() || DEFAULT_EMBEDDING_MODEL;
|
|
58
|
+
}
|
|
56
59
|
async function getExtractor() {
|
|
57
|
-
|
|
60
|
+
const wanted = getEmbeddingModel();
|
|
61
|
+
if (extractor && loadedModelName === wanted) return extractor;
|
|
58
62
|
if (!pipeline) {
|
|
59
63
|
const mod = await import("@huggingface/transformers");
|
|
60
64
|
pipeline = mod.pipeline;
|
|
61
65
|
}
|
|
62
|
-
extractor = await pipeline("feature-extraction",
|
|
66
|
+
extractor = await pipeline("feature-extraction", wanted, {
|
|
63
67
|
revision: "main"
|
|
64
68
|
});
|
|
69
|
+
loadedModelName = wanted;
|
|
70
|
+
cachedDim = null;
|
|
65
71
|
return extractor;
|
|
66
72
|
}
|
|
73
|
+
async function getEmbeddingDim() {
|
|
74
|
+
if (cachedDim !== null && loadedModelName === getEmbeddingModel()) return cachedDim;
|
|
75
|
+
const probe = await encode("dimension probe");
|
|
76
|
+
cachedDim = probe.length;
|
|
77
|
+
return cachedDim;
|
|
78
|
+
}
|
|
67
79
|
function meanPoolingNormalize(output, attentionMask) {
|
|
68
80
|
const seqLen = output.length;
|
|
69
81
|
const dim = output[0].length;
|
|
@@ -117,13 +129,15 @@ function cosineSimilarity(a, b) {
|
|
|
117
129
|
for (let i = 0; i < a.length; i++) dot += a[i] * b[i];
|
|
118
130
|
return dot;
|
|
119
131
|
}
|
|
120
|
-
var pipeline, extractor,
|
|
132
|
+
var pipeline, extractor, loadedModelName, cachedDim, DEFAULT_EMBEDDING_MODEL;
|
|
121
133
|
var init_embedding = __esm({
|
|
122
134
|
"../amem-core/src/embedding.ts"() {
|
|
123
135
|
"use strict";
|
|
124
136
|
pipeline = null;
|
|
125
137
|
extractor = null;
|
|
126
|
-
|
|
138
|
+
loadedModelName = null;
|
|
139
|
+
cachedDim = null;
|
|
140
|
+
DEFAULT_EMBEDDING_MODEL = "Xenova/paraphrase-multilingual-MiniLM-L12-v2";
|
|
127
141
|
}
|
|
128
142
|
});
|
|
129
143
|
|
|
@@ -168,15 +182,26 @@ async function ensureCollection(collectionName) {
|
|
|
168
182
|
if (collectionName) _collectionReadyMap.set(col, true);
|
|
169
183
|
else _collectionReady = true;
|
|
170
184
|
};
|
|
185
|
+
let existing = null;
|
|
171
186
|
try {
|
|
172
|
-
await qdrant("GET", `/collections/${col}`);
|
|
187
|
+
existing = await qdrant("GET", `/collections/${col}`);
|
|
188
|
+
} catch {
|
|
189
|
+
}
|
|
190
|
+
if (existing) {
|
|
191
|
+
const collectionDim = existing.config?.params?.vectors?.size;
|
|
192
|
+
if (typeof collectionDim === "number") {
|
|
193
|
+
const modelDim = await getEmbeddingDim();
|
|
194
|
+
if (collectionDim !== modelDim) {
|
|
195
|
+
throw new EmbeddingDimensionMismatchError(col, collectionDim, modelDim, getEmbeddingModel());
|
|
196
|
+
}
|
|
197
|
+
}
|
|
173
198
|
markReady();
|
|
174
199
|
return;
|
|
175
|
-
} catch {
|
|
176
200
|
}
|
|
177
201
|
try {
|
|
202
|
+
const size = await getEmbeddingDim();
|
|
178
203
|
await qdrant("PUT", `/collections/${col}`, {
|
|
179
|
-
vectors: { size
|
|
204
|
+
vectors: { size, distance: "Cosine" }
|
|
180
205
|
});
|
|
181
206
|
} catch (err) {
|
|
182
207
|
if (!(err instanceof Error) || !err.message.includes("already exists")) throw err;
|
|
@@ -199,6 +224,40 @@ async function ensureCollection(collectionName) {
|
|
|
199
224
|
});
|
|
200
225
|
markReady();
|
|
201
226
|
}
|
|
227
|
+
async function scrollAllRaw(collection, limit = 1e4) {
|
|
228
|
+
const out = [];
|
|
229
|
+
let offset = void 0;
|
|
230
|
+
for (; ; ) {
|
|
231
|
+
const body = { with_payload: true, with_vector: true, limit };
|
|
232
|
+
if (offset !== void 0 && offset !== null) body.offset = offset;
|
|
233
|
+
const res = await qdrant("POST", `/collections/${collection}/points/scroll`, body);
|
|
234
|
+
out.push(...res.points);
|
|
235
|
+
offset = res.next_page_offset;
|
|
236
|
+
if (offset === void 0 || offset === null || res.points.length === 0) break;
|
|
237
|
+
}
|
|
238
|
+
return out;
|
|
239
|
+
}
|
|
240
|
+
async function countPointsRaw(collection) {
|
|
241
|
+
const res = await qdrant("POST", `/collections/${collection}/points/count`, { exact: true });
|
|
242
|
+
return res.count;
|
|
243
|
+
}
|
|
244
|
+
async function collectionDimRaw(collection) {
|
|
245
|
+
try {
|
|
246
|
+
const info = await qdrant("GET", `/collections/${collection}`);
|
|
247
|
+
return info.config?.params?.vectors?.size ?? null;
|
|
248
|
+
} catch {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
async function createCollectionRaw(collection, size) {
|
|
253
|
+
await qdrant("PUT", `/collections/${collection}`, { vectors: { size, distance: "Cosine" } });
|
|
254
|
+
for (const field_name of ["agent_id", "hash", "topics", "subjects"]) {
|
|
255
|
+
await qdrant("PUT", `/collections/${collection}/index`, { field_name, field_schema: "keyword" });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
async function upsertPointsRaw(collection, points) {
|
|
259
|
+
await qdrant("PUT", `/collections/${collection}/points?wait=true`, { points });
|
|
260
|
+
}
|
|
202
261
|
function noteToPoint(note) {
|
|
203
262
|
return {
|
|
204
263
|
id: note.id,
|
|
@@ -231,6 +290,7 @@ function noteToPoint(note) {
|
|
|
231
290
|
conflict: note.conflict ?? false,
|
|
232
291
|
conflicts_with: note.conflicts_with ?? [],
|
|
233
292
|
conflict_reason: note.conflict_reason ?? "",
|
|
293
|
+
conflict_scanned_at: note.conflict_scanned_at ?? "",
|
|
234
294
|
subjects: note.subjects ?? [],
|
|
235
295
|
// 31
|
|
236
296
|
ephemeral: note.ephemeral ?? false,
|
|
@@ -286,6 +346,7 @@ function pointToNote(point) {
|
|
|
286
346
|
conflict: p.conflict === true,
|
|
287
347
|
conflicts_with: Array.isArray(p.conflicts_with) ? p.conflicts_with.filter((v) => typeof v === "string") : [],
|
|
288
348
|
conflict_reason: typeof p.conflict_reason === "string" ? p.conflict_reason : "",
|
|
349
|
+
conflict_scanned_at: typeof p.conflict_scanned_at === "string" ? p.conflict_scanned_at : "",
|
|
289
350
|
subjects: Array.isArray(p.subjects) ? p.subjects.filter((v) => typeof v === "string") : [],
|
|
290
351
|
// 31
|
|
291
352
|
ephemeral: p.ephemeral === true,
|
|
@@ -582,14 +643,31 @@ async function invalidateNote(id, callerAgentId) {
|
|
|
582
643
|
async function patchNotePayload(id, fields) {
|
|
583
644
|
return makeCrud(getCollection()).patchNotePayload(id, fields);
|
|
584
645
|
}
|
|
585
|
-
var QDRANT_URL, getCollection,
|
|
646
|
+
var QDRANT_URL, getCollection, EmbeddingDimensionMismatchError, _collectionReady, _collectionReadyMap;
|
|
586
647
|
var init_storage = __esm({
|
|
587
648
|
"../amem-core/src/storage.ts"() {
|
|
588
649
|
"use strict";
|
|
589
650
|
init_auth();
|
|
651
|
+
init_embedding();
|
|
590
652
|
QDRANT_URL = "http://localhost:6333";
|
|
591
653
|
getCollection = () => process.env.AMEM_COLLECTION || "amem_notes";
|
|
592
|
-
|
|
654
|
+
EmbeddingDimensionMismatchError = class extends Error {
|
|
655
|
+
constructor(collection, collectionDim, modelDim, model) {
|
|
656
|
+
super(
|
|
657
|
+
`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.
|
|
658
|
+
Either set AMEM_EMBED_MODEL back to the model this collection was built with, or migrate: build a new collection with the new model, backfill it, then point AMEM_COLLECTION at it. See docs/reference/embedding-models.md.`
|
|
659
|
+
);
|
|
660
|
+
this.collection = collection;
|
|
661
|
+
this.collectionDim = collectionDim;
|
|
662
|
+
this.modelDim = modelDim;
|
|
663
|
+
this.model = model;
|
|
664
|
+
this.name = "EmbeddingDimensionMismatchError";
|
|
665
|
+
}
|
|
666
|
+
collection;
|
|
667
|
+
collectionDim;
|
|
668
|
+
modelDim;
|
|
669
|
+
model;
|
|
670
|
+
};
|
|
593
671
|
_collectionReady = false;
|
|
594
672
|
_collectionReadyMap = /* @__PURE__ */ new Map();
|
|
595
673
|
}
|
|
@@ -1907,6 +1985,7 @@ function resolveConflictMode(override) {
|
|
|
1907
1985
|
}
|
|
1908
1986
|
async function conflictSweep(agentId, opts) {
|
|
1909
1987
|
const ctx = opts?.storageCtx ?? defaultCtx();
|
|
1988
|
+
const force = opts?.force === true;
|
|
1910
1989
|
const mode = resolveConflictMode(opts?.mode);
|
|
1911
1990
|
const log = opts?.logger?.info ?? ((m) => console.log(m));
|
|
1912
1991
|
const raw = await ctx.listNotes(agentId);
|
|
@@ -1919,10 +1998,18 @@ async function conflictSweep(agentId, opts) {
|
|
|
1919
1998
|
}
|
|
1920
1999
|
let pairsFound = 0;
|
|
1921
2000
|
let retired = 0;
|
|
2001
|
+
let batchesScanned = 0;
|
|
2002
|
+
let batchesSkipped = 0;
|
|
1922
2003
|
for (const [category, groupNotes] of groups.entries()) {
|
|
2004
|
+
groupNotes.sort((a, b) => Date.parse(b.timestamp) - Date.parse(a.timestamp));
|
|
1923
2005
|
for (let start = 0; start < groupNotes.length; start += CONFLICT_BATCH_SIZE) {
|
|
1924
2006
|
const batch = groupNotes.slice(start, start + CONFLICT_BATCH_SIZE);
|
|
1925
2007
|
if (batch.length < 2) continue;
|
|
2008
|
+
if (!force && batch.every((n) => n.conflict_scanned_at)) {
|
|
2009
|
+
batchesSkipped++;
|
|
2010
|
+
continue;
|
|
2011
|
+
}
|
|
2012
|
+
batchesScanned++;
|
|
1926
2013
|
const pairs = await llmConflictScan(batch.map((n) => n.content));
|
|
1927
2014
|
for (const { a, b, reason, supersededIndex } of pairs) {
|
|
1928
2015
|
const noteA = batch[a];
|
|
@@ -1955,10 +2042,16 @@ async function conflictSweep(agentId, opts) {
|
|
|
1955
2042
|
}
|
|
1956
2043
|
}
|
|
1957
2044
|
}
|
|
2045
|
+
const scannedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2046
|
+
for (const n of batch) {
|
|
2047
|
+
await ctx.patchNotePayload(n.id, { conflict_scanned_at: scannedAt });
|
|
2048
|
+
}
|
|
1958
2049
|
}
|
|
1959
2050
|
}
|
|
1960
|
-
log(
|
|
1961
|
-
|
|
2051
|
+
log(
|
|
2052
|
+
`[conflict] ${batchesScanned} batch(es) scanned, ${batchesSkipped} already up to date; ${pairsFound} pair(s) found, ${retired} retired`
|
|
2053
|
+
);
|
|
2054
|
+
return { scanned: notes.length, pairsFound, retired, batchesScanned, batchesSkipped };
|
|
1962
2055
|
}
|
|
1963
2056
|
var import_uuid, import_crypto, fs2, path3, import_jieba, logSafe, _jieba, EPHEMERAL_SIGNALS, CONFLICT_BATCH_SIZE;
|
|
1964
2057
|
var init_memory = __esm({
|
|
@@ -2073,7 +2166,7 @@ async function generateReviewBatch(agentId, outputPath) {
|
|
|
2073
2166
|
const title = LOCALE2 === "zh" ? "A-MEM \u8D28\u91CF\u5BA1\u6838" : "A-MEM Quality Review";
|
|
2074
2167
|
const genLabel = LOCALE2 === "zh" ? "\u751F\u6210\u65F6\u95F4" : "Generated";
|
|
2075
2168
|
const countLabel = LOCALE2 === "zh" ? `\u5171 ${items.length} \u6761\u4F4E\u8D28\u91CF\u6761\u76EE` : `${items.length} low-quality item(s)`;
|
|
2076
|
-
const applyHint = LOCALE2 === "zh" ? "\u9009\
|
|
2169
|
+
const applyHint = LOCALE2 === "zh" ? "\u52FE\u9009\u540E\u4EA4\u7ED9\u52A9\u624B\u5904\u7406\u8FD9\u4E9B\u6761\u76EE" : "Tick your choices, then ask the assistant to act on them";
|
|
2077
2170
|
lines.push(`# ${title} \u2014 Batch ${batchN || "custom"}`);
|
|
2078
2171
|
lines.push("");
|
|
2079
2172
|
lines.push(`> ${genLabel}\uFF1A${now} | ${countLabel}`);
|
|
@@ -2172,6 +2265,93 @@ var init_quality = __esm({
|
|
|
2172
2265
|
}
|
|
2173
2266
|
});
|
|
2174
2267
|
|
|
2268
|
+
// ../amem-core/src/migrate.ts
|
|
2269
|
+
function missingDerivedFields(n) {
|
|
2270
|
+
return n.keywords.length === 0 || n.tags.length === 0;
|
|
2271
|
+
}
|
|
2272
|
+
async function migrateCollection(opts) {
|
|
2273
|
+
const { from, to } = opts;
|
|
2274
|
+
const refreshFields = opts.refreshFields !== false;
|
|
2275
|
+
const dryRun = opts.dryRun !== false;
|
|
2276
|
+
const log = opts.logger?.info ?? ((m) => console.log(m));
|
|
2277
|
+
const warn = opts.logger?.warn ?? ((m) => console.warn(m));
|
|
2278
|
+
if (from === to) throw new Error(`migrate: source and target are the same collection ("${from}")`);
|
|
2279
|
+
const model = getEmbeddingModel();
|
|
2280
|
+
const targetDim = await getEmbeddingDim();
|
|
2281
|
+
const sourceDim = await collectionDimRaw(from);
|
|
2282
|
+
if (sourceDim === null) throw new Error(`migrate: source collection "${from}" does not exist`);
|
|
2283
|
+
const points = await scrollAllRaw(from);
|
|
2284
|
+
const notes = points.map(pointToNote);
|
|
2285
|
+
const missingDerived = notes.filter(missingDerivedFields).length;
|
|
2286
|
+
log(
|
|
2287
|
+
`[migrate] ${from} (${sourceDim}d, ${notes.length} notes) \u2192 ${to} (${targetDim}d, ${model}); ${missingDerived} note(s) missing keywords/tags`
|
|
2288
|
+
);
|
|
2289
|
+
if (dryRun) {
|
|
2290
|
+
log("[migrate] dry run \u2014 nothing written. Pass dryRun: false to apply.");
|
|
2291
|
+
return { total: notes.length, missingDerived, refreshed: 0, migrated: 0, sourceDim, targetDim, model, dryRun: true };
|
|
2292
|
+
}
|
|
2293
|
+
const existingTargetDim = await collectionDimRaw(to);
|
|
2294
|
+
if (existingTargetDim === null) {
|
|
2295
|
+
await createCollectionRaw(to, targetDim);
|
|
2296
|
+
log(`[migrate] created ${to} at ${targetDim}d`);
|
|
2297
|
+
} else {
|
|
2298
|
+
if (existingTargetDim !== targetDim) {
|
|
2299
|
+
throw new Error(`migrate: target "${to}" exists at ${existingTargetDim}d but the model produces ${targetDim}d`);
|
|
2300
|
+
}
|
|
2301
|
+
const existingCount = await countPointsRaw(to);
|
|
2302
|
+
if (existingCount > 0) {
|
|
2303
|
+
throw new Error(`migrate: target "${to}" already holds ${existingCount} point(s); use an empty collection`);
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
let refreshed = 0;
|
|
2307
|
+
let migrated = 0;
|
|
2308
|
+
const BATCH = 64;
|
|
2309
|
+
let buffer = [];
|
|
2310
|
+
const flush = async () => {
|
|
2311
|
+
if (!buffer.length) return;
|
|
2312
|
+
await upsertPointsRaw(to, buffer);
|
|
2313
|
+
migrated += buffer.length;
|
|
2314
|
+
buffer = [];
|
|
2315
|
+
};
|
|
2316
|
+
for (const note of notes) {
|
|
2317
|
+
if (refreshFields && missingDerivedFields(note)) {
|
|
2318
|
+
try {
|
|
2319
|
+
const built = await llmConstructNote(note.content);
|
|
2320
|
+
if (note.keywords.length === 0) note.keywords = built.keywords;
|
|
2321
|
+
if (note.tags.length === 0) note.tags = built.tags;
|
|
2322
|
+
if (!note.context) note.context = built.context;
|
|
2323
|
+
refreshed++;
|
|
2324
|
+
} catch (e) {
|
|
2325
|
+
warn(`[migrate] re-extract failed for ${note.id.slice(0, 8)} \u2014 keeping as-is: ${e.message}`);
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
const point = noteToPoint({ ...note, embedding: await encode(buildEmbedText(note)) });
|
|
2329
|
+
buffer.push(point);
|
|
2330
|
+
if (buffer.length >= BATCH) {
|
|
2331
|
+
await flush();
|
|
2332
|
+
log(`[migrate] ${migrated}/${notes.length}`);
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
await flush();
|
|
2336
|
+
const finalCount = await countPointsRaw(to);
|
|
2337
|
+
if (finalCount !== notes.length) {
|
|
2338
|
+
warn(`[migrate] target holds ${finalCount} point(s) but the source had ${notes.length} \u2014 check before switching`);
|
|
2339
|
+
}
|
|
2340
|
+
log(
|
|
2341
|
+
`[migrate] done: ${migrated} migrated, ${refreshed} re-extracted. "${from}" is untouched \u2014 switch with AMEM_COLLECTION=${to}, and keep the old one until you are satisfied.`
|
|
2342
|
+
);
|
|
2343
|
+
return { total: notes.length, missingDerived, refreshed, migrated, sourceDim, targetDim, model, dryRun: false };
|
|
2344
|
+
}
|
|
2345
|
+
var init_migrate = __esm({
|
|
2346
|
+
"../amem-core/src/migrate.ts"() {
|
|
2347
|
+
"use strict";
|
|
2348
|
+
init_storage();
|
|
2349
|
+
init_embedding();
|
|
2350
|
+
init_llm();
|
|
2351
|
+
init_memory();
|
|
2352
|
+
}
|
|
2353
|
+
});
|
|
2354
|
+
|
|
2175
2355
|
// ../amem-core/src/crud-guard.ts
|
|
2176
2356
|
function resolveCrudUpdateMinSim(override) {
|
|
2177
2357
|
const envVal = Number(process.env.AMEM_CRUD_UPDATE_MIN_SIM);
|
|
@@ -2197,6 +2377,8 @@ var init_crud_guard = __esm({
|
|
|
2197
2377
|
var src_exports = {};
|
|
2198
2378
|
__export(src_exports, {
|
|
2199
2379
|
DEFAULT_CRUD_UPDATE_MIN_SIM: () => DEFAULT_CRUD_UPDATE_MIN_SIM,
|
|
2380
|
+
DEFAULT_EMBEDDING_MODEL: () => DEFAULT_EMBEDDING_MODEL,
|
|
2381
|
+
EmbeddingDimensionMismatchError: () => EmbeddingDimensionMismatchError,
|
|
2200
2382
|
addEpisodic: () => addEpisodic,
|
|
2201
2383
|
addMemory: () => addMemory,
|
|
2202
2384
|
canRead: () => canRead,
|
|
@@ -2211,6 +2393,8 @@ __export(src_exports, {
|
|
|
2211
2393
|
encode: () => encode,
|
|
2212
2394
|
ensureCollection: () => ensureCollection,
|
|
2213
2395
|
generateReviewBatch: () => generateReviewBatch,
|
|
2396
|
+
getEmbeddingDim: () => getEmbeddingDim,
|
|
2397
|
+
getEmbeddingModel: () => getEmbeddingModel,
|
|
2214
2398
|
getNote: () => getNote,
|
|
2215
2399
|
invalidateNote: () => invalidateNote,
|
|
2216
2400
|
isModelLoaded: () => isModelLoaded,
|
|
@@ -2220,6 +2404,7 @@ __export(src_exports, {
|
|
|
2220
2404
|
llmCrudDecision: () => llmCrudDecision,
|
|
2221
2405
|
loadModel: () => loadModel,
|
|
2222
2406
|
mergeSimilarNotes: () => mergeSimilarNotes,
|
|
2407
|
+
migrateCollection: () => migrateCollection,
|
|
2223
2408
|
patchNotePayload: () => patchNotePayload,
|
|
2224
2409
|
pingQdrant: () => pingQdrant,
|
|
2225
2410
|
resolveCrudUpdateMinSim: () => resolveCrudUpdateMinSim,
|
|
@@ -2236,6 +2421,9 @@ var init_src = __esm({
|
|
|
2236
2421
|
init_quality();
|
|
2237
2422
|
init_storage();
|
|
2238
2423
|
init_auth();
|
|
2424
|
+
init_embedding();
|
|
2425
|
+
init_storage();
|
|
2426
|
+
init_migrate();
|
|
2239
2427
|
init_memory();
|
|
2240
2428
|
init_crud_guard();
|
|
2241
2429
|
init_llm();
|
|
@@ -2329,6 +2517,7 @@ function register(api) {
|
|
|
2329
2517
|
}
|
|
2330
2518
|
});
|
|
2331
2519
|
}
|
|
2520
|
+
const conflictSweepEnabled = pluginConfig.conflictSweep !== false;
|
|
2332
2521
|
const crudUpdateMinSim = pluginConfig.crudUpdateMinSim;
|
|
2333
2522
|
const resolveAgentId2 = (ctx) => resolveAgentId(ctx, pluginConfig);
|
|
2334
2523
|
const buildScope2 = (rawAgentId) => buildScope(rawAgentId, pluginConfig, createStorageContext);
|
|
@@ -2337,9 +2526,13 @@ function register(api) {
|
|
|
2337
2526
|
logger.info(
|
|
2338
2527
|
`openclaw-amem: registered (native TS, Qdrant, default agent_id=${defaultScope.agentId}, default collection=${pluginConfig.collection ?? "amem_notes (default)"}, per-agent scope resolved per call)`
|
|
2339
2528
|
);
|
|
2340
|
-
ensureCollection(pluginConfig.collection).catch(
|
|
2341
|
-
(e
|
|
2342
|
-
|
|
2529
|
+
ensureCollection(pluginConfig.collection).catch((e) => {
|
|
2530
|
+
if (e instanceof EmbeddingDimensionMismatchError) {
|
|
2531
|
+
logger.error(`openclaw-amem: memory is UNUSABLE \u2014 ${e.message}`);
|
|
2532
|
+
} else {
|
|
2533
|
+
logger.warn(`openclaw-amem: ensureCollection failed \u2014 ${e.message}`);
|
|
2534
|
+
}
|
|
2535
|
+
});
|
|
2343
2536
|
if (typeof api.registerMemoryCapability === "function") {
|
|
2344
2537
|
api.registerMemoryCapability({
|
|
2345
2538
|
publicArtifacts: {
|
|
@@ -2753,6 +2946,21 @@ ${mergeErr.stack}`
|
|
|
2753
2946
|
} catch (err) {
|
|
2754
2947
|
logger.warn(`openclaw-amem: Scheduled daily consolidation failed \u2014 ${err.message}`);
|
|
2755
2948
|
}
|
|
2949
|
+
if (conflictSweepEnabled) {
|
|
2950
|
+
try {
|
|
2951
|
+
const res = await conflictSweep(defaultScope.agentId, {
|
|
2952
|
+
storageCtx: defaultScope.storageCtx,
|
|
2953
|
+
logger
|
|
2954
|
+
});
|
|
2955
|
+
if (res.pairsFound > 0) {
|
|
2956
|
+
logger.info(
|
|
2957
|
+
`openclaw-amem: Contradiction sweep flagged ${res.pairsFound} pair(s)` + (res.retired > 0 ? `, retired ${res.retired}` : "") + ` (${res.batchesScanned} batch(es) read, ${res.batchesSkipped} unchanged).`
|
|
2958
|
+
);
|
|
2959
|
+
}
|
|
2960
|
+
} catch (err) {
|
|
2961
|
+
logger.warn(`openclaw-amem: Scheduled contradiction sweep failed \u2014 ${err.message}`);
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2756
2964
|
scheduleNextRun();
|
|
2757
2965
|
}, delay);
|
|
2758
2966
|
}
|
|
@@ -2773,8 +2981,8 @@ ${mergeErr.stack}`
|
|
|
2773
2981
|
}
|
|
2774
2982
|
var plugin = (0, import_plugin_entry.definePluginEntry)({
|
|
2775
2983
|
id: "openclaw-amem",
|
|
2776
|
-
name: "
|
|
2777
|
-
description: "
|
|
2984
|
+
name: "amem",
|
|
2985
|
+
description: "Agentic memory for OpenClaw \u2014 memories evolve, link into a graph, and stay separated per agent and per person.",
|
|
2778
2986
|
register
|
|
2779
2987
|
});
|
|
2780
2988
|
var index_default = plugin;
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "openclaw-amem",
|
|
3
|
-
"name": "
|
|
4
|
-
"description": "
|
|
5
|
-
"version": "1.
|
|
3
|
+
"name": "amem",
|
|
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
|
+
"version": "1.4.1",
|
|
6
6
|
"kind": "memory",
|
|
7
7
|
"openclaw": {
|
|
8
8
|
"compat": {
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"AMEM_CONFLICT_MODE",
|
|
39
39
|
"AMEM_LLM_TIMEOUT",
|
|
40
40
|
"AMEM_CRUD_UPDATE_MIN_SIM",
|
|
41
|
+
"AMEM_EMBED_MODEL",
|
|
41
42
|
"AMEM_COLLECTION",
|
|
42
43
|
"AMEM_DATA_DIR",
|
|
43
44
|
"AMEM_EVO_COUNTER_PATH",
|
|
@@ -139,6 +140,11 @@
|
|
|
139
140
|
"default": "fast",
|
|
140
141
|
"description": "Which tier the agent_end CRUD decision runs on. Defaults to fast: it runs every turn, and its destructive failure mode is handled by the update guard rather than by model tier. Overridden by AMEM_LLM_CRUD_ROLE."
|
|
141
142
|
},
|
|
143
|
+
"conflictSweep": {
|
|
144
|
+
"type": "boolean",
|
|
145
|
+
"default": true,
|
|
146
|
+
"description": "Run the nightly contradiction sweep after daily consolidation. Only batches that gained a memory are re-read, so a typical night costs one or two LLM calls. Set false to disable."
|
|
147
|
+
},
|
|
142
148
|
"crudUpdateMinSim": {
|
|
143
149
|
"type": "number",
|
|
144
150
|
"default": 0.35,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclaw-amem",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.4.1",
|
|
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",
|
|
7
7
|
"openclaw": {
|
|
@@ -20,13 +20,13 @@
|
|
|
20
20
|
"@huggingface/transformers": "^4.2.0",
|
|
21
21
|
"@node-rs/jieba": "^2.0.1",
|
|
22
22
|
"@qdrant/js-client-rest": "^1.18.0",
|
|
23
|
-
"@types/uuid": "^11.0.0",
|
|
24
23
|
"openai": "^6.48.0",
|
|
25
24
|
"uuid": "^14.0.0"
|
|
26
25
|
},
|
|
27
26
|
"devDependencies": {
|
|
28
27
|
"@eslint/js": "^10.0.1",
|
|
29
28
|
"@types/node": "^26.1.1",
|
|
29
|
+
"@types/uuid": "^11.0.0",
|
|
30
30
|
"eslint": "^10.7.0",
|
|
31
31
|
"prettier": "^3.9.5",
|
|
32
32
|
"tsup": "^8.4.0",
|
|
@@ -41,16 +41,17 @@
|
|
|
41
41
|
},
|
|
42
42
|
"repository": {
|
|
43
43
|
"type": "git",
|
|
44
|
-
"url": "git+https://github.com/
|
|
44
|
+
"url": "git+https://github.com/amemhq/amem.git",
|
|
45
45
|
"directory": "packages/openclaw-amem"
|
|
46
46
|
},
|
|
47
47
|
"homepage": "https://amem.owo.lc",
|
|
48
48
|
"bugs": {
|
|
49
|
-
"url": "https://github.com/
|
|
49
|
+
"url": "https://github.com/amemhq/amem/issues"
|
|
50
50
|
},
|
|
51
51
|
"keywords": [
|
|
52
52
|
"openclaw",
|
|
53
53
|
"openclaw-plugin",
|
|
54
|
+
"amem",
|
|
54
55
|
"a-mem",
|
|
55
56
|
"agent-memory",
|
|
56
57
|
"agentic-memory",
|