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 CHANGED
@@ -1,21 +1,26 @@
1
1
  # openclaw-amem
2
2
 
3
3
  <p align="center">
4
- <img src="https://raw.githubusercontent.com/heichaowo/amem/main/docs/public/logo.webp" width="120" alt="A-MEM Logo" />
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: MIT](https://img.shields.io/badge/License-MIT-yellow?style=for-the-badge)](../../LICENSE)
8
8
  [![npm](https://img.shields.io/npm/v/openclaw-amem?style=for-the-badge&logo=npm&logoColor=white)](https://www.npmjs.com/package/openclaw-amem)
9
9
  [![arXiv](https://img.shields.io/badge/arXiv-2502.12110-b31b1b?style=for-the-badge)](https://arxiv.org/abs/2502.12110)
10
10
 
11
- **A-MEM agentic memory backend for [OpenClaw](https://github.com/openclaw/openclaw)** memories **evolve**, not just accumulate.
11
+ **Memory for [OpenClaw](https://github.com/openclaw/openclaw) agents that catches its own contradictions.**
12
12
 
13
- The first open-source A-MEM memory plugin for OpenClaw: dynamic graph linking, hybrid (BM25 + dense) retrieval with 2-hop graph expansion, and LLM-driven memory evolution. Backed by Qdrant + local Transformers.js. **No Python required.**
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/heichaowo/amem).
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: **[amem-core README](../amem-core)** · **[docs](https://amem.owo.lc)**.
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:@heichaowo/openclaw-amem
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: **[amem-core](../amem-core)** · paper: [A-MEM (arXiv:2502.12110)](https://arxiv.org/abs/2502.12110).
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
- if (extractor) return extractor;
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", MODEL_NAME, {
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, MODEL_NAME;
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
- MODEL_NAME = "Xenova/paraphrase-multilingual-MiniLM-L12-v2";
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: VECTOR_DIM, distance: "Cosine" }
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, VECTOR_DIM, _collectionReady, _collectionReadyMap;
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
- VECTOR_DIM = 384;
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(`[conflict] scanned ${notes.length} notes, found ${pairsFound} pair(s), retired ${retired}`);
1961
- return { scanned: notes.length, pairsFound, retired };
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\u597D\u540E\u53EF\u4F7F\u7528 memory_quality_apply \u6279\u91CF\u5904\u7406" : "Use memory_quality_apply to batch-process selected items";
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) => logger.warn(`openclaw-amem: ensureCollection failed \u2014 ${e.message}`)
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: "Memory (A-MEM v2)",
2777
- description: "A-MEM agentic memory backend for OpenClaw \u2014 Qdrant + Transformers.js, no Python required.",
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;
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "id": "openclaw-amem",
3
- "name": "Memory (A-MEM v2)",
4
- "description": "OpenClaw memory plugin implementing A-MEM memories evolve, not just accumulate. Graph linking, hybrid retrieval, LLM-driven evolution. No Python.",
5
- "version": "1.3.0",
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.3.0",
4
- "description": "OpenClaw memory plugin implementing A-MEM memories evolve, not just accumulate. Graph linking, hybrid retrieval, LLM-driven evolution. No Python.",
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/heichaowo/amem.git",
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/heichaowo/amem/issues"
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",