openclaw-amem 2.1.0 → 2.1.2

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
@@ -10,9 +10,9 @@
10
10
 
11
11
  **Memory for [OpenClaw](https://github.com/openclaw/openclaw) agents that catches its own contradictions.**
12
12
 
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.
13
+ A nightly pass re-reads what changed and flags memories that no longer agree with each other. This is the failure mode every long-lived memory store eventually has. The plugin also extracts facts instead of storing transcripts. It rewrites notes as new ones arrive, links them into a Zettelkasten-style graph, and retrieves them with hybrid BM25 + dense search over 2-hop expansion. The plugin scopes memories per agent (`owner`/`readers`/`writers`) and per subject, and enforces both inside the Qdrant query rather than filtering after it.
14
14
 
15
- Runs on a small model — `gpt-4o-mini`, `haiku`, a local Ollama. No Python.
15
+ The plugin runs on a small model — `gpt-4o-mini`, `haiku`, a local Ollama. It does not require Python.
16
16
 
17
17
  中文走 [jieba](https://github.com/messense/node-jieba) 分词,提示词有完整中文版(`AMEM_PROMPT_LOCALE=zh`),embedding 模型本身多语言。
18
18
 
@@ -26,9 +26,9 @@ Requires a local [Qdrant](https://qdrant.tech). Implements [A-MEM](https://arxiv
26
26
 
27
27
  - 🔄 **Memories evolve** — new facts update/link related memories (EVOLVE / CONFLICT / EXPAND / NEW), not silent overwrite.
28
28
  - 🔍 **Hybrid retrieval** — BM25 (Jieba for Chinese) + dense vectors (RRF) + 2-hop graph expansion with a relevance gate.
29
- - 🧠 **Knowledge vs episodic** — durable knowledge notes skip consolidation & time-decay; topic tags for precise recall.
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
- - 🔐 **Per-agent isolation** — private by default; explicit `owner`/`readers`/`writers`; Mode A (shared collection) or Mode B (dedicated collection).
31
+ - 🔐 **Per-agent isolation** — private by default. Explicit `owner`/`readers`/`writers`. Mode A (shared collection) or Mode B (dedicated collection).
32
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)**.
@@ -36,7 +36,7 @@ Requires a local [Qdrant](https://qdrant.tech). Implements [A-MEM](https://arxiv
36
36
  ## Requirements
37
37
 
38
38
  - OpenClaw v2026.4+
39
- - Node.js 24 (18+ works; 24/26 supported)
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
42
  - ~1.1 GB of disk for the embedding model, downloaded once on first run
@@ -57,6 +57,16 @@ pnpm --filter openclaw-amem build
57
57
  openclaw plugins install --link ./packages/openclaw-amem
58
58
  ```
59
59
 
60
+ Updating:
61
+
62
+ ```bash
63
+ openclaw plugins update openclaw-amem
64
+ ```
65
+
66
+ The gateway does not load the new build until it restarts. Run `openclaw gateway restart` after the update.
67
+
68
+ An update rebuilds `node_modules`. The model cache lives there by default, so the plugin re-downloads 2.27 GB unless `AMEM_MODEL_CACHE` points somewhere outside the plugin directory. Set that once and updates cost nothing.
69
+
60
70
  ### 2. Configure `~/.openclaw/openclaw.json`
61
71
 
62
72
  Add `openclaw-amem` to your allowed plugins and hook it into the `memory` slot:
@@ -81,7 +91,7 @@ Add `openclaw-amem` to your allowed plugins and hook it into the `memory` slot:
81
91
  }
82
92
  ```
83
93
 
84
- > **⚠️ Memory slot conflict:** If your `openclaw.json` already assigns the `memory` slot to another plugin (e.g. `memory-core`), you **must** change it to `openclaw-amem`. The gateway only loads one plugin per slot any additional `memory`-kind plugins are **silently skipped**. Set `"memory-core": { "enabled": false }` in `entries` to disable the old plugin.
94
+ > **⚠️ Memory slot conflict:** If your `openclaw.json` already assigns the `memory` slot to another plugin (for example, `memory-core`), you **must** change it to `openclaw-amem`. On OpenClaw 2026.8.1 the slot alone is not enough. The other plugin still loads and takes the `memory_search` tool name, because the gateway loads built-in plugins before installed ones. Memory keeps working through the slot, but the tool an agent calls is then the other plugin's. Set `"memory-core": { "enabled": false }` in `entries` as well. Note that `memory-core` also writes its dream diary, and disabling it stops that too.
85
95
 
86
96
  ### 3. Restart OpenClaw
87
97
 
@@ -92,11 +102,11 @@ openclaw gateway restart
92
102
  First run downloads the embedding model (`bge-m3`, 2.27 GB) and caches it. Later
93
103
  restarts are instant. If that is too much, the only smaller model worth setting is
94
104
  `AMEM_EMBED_MODEL=Xenova/bge-small-zh-v1.5` — 25 MB, Chinese only, and it caps at
95
- 512 tokens. Set it **before** you have memories, since changing it afterwards means
105
+ 512 tokens. Set it **before** you have memories, because changing it afterwards means
96
106
  a [migration](https://amem.owo.lc/reference/embedding-models#changing-the-model-on-a-store-you-already-have).
97
107
 
98
108
  Upgrading from 1.x downloads nothing. Your existing memories keep the model that
99
- built them, and the plugin says so at startup along with the one command that
109
+ built them, and the plugin shows this at startup along with the one command that
100
110
  moves them.
101
111
 
102
112
  ## LLM provider
@@ -104,7 +114,7 @@ moves them.
104
114
  The plugin calls an LLM for note construction, linking, and evolution. Pick the backend with `AMEM_LLM_PROVIDER`:
105
115
 
106
116
  - **`anthropic`** (default) — the Anthropic Messages API. Set `ANTHROPIC_API_KEY`.
107
- - **`openai`** — the OpenAI Chat Completions API, which every OpenAI-compatible endpoint speaks. Set `AMEM_LLM_PROVIDER=openai`, point `AMEM_LLM_BASE_URL` at the endpoint, and set `AMEM_LLM_API_KEY` (or the standard `OPENAI_API_KEY`). Covers **OpenAI, DeepSeek, OpenRouter, Groq, Together**, and local servers (**Ollama, vLLM, LM Studio** — no key needed). Reasoning models (`o1`, `o3`, `gpt-5`) are handled automatically.
117
+ - **`openai`** — the OpenAI Chat Completions API, which every OpenAI-compatible endpoint speaks. Set `AMEM_LLM_PROVIDER=openai`, point `AMEM_LLM_BASE_URL` at the endpoint, and set `AMEM_LLM_API_KEY` (or the standard `OPENAI_API_KEY`). Covers **OpenAI, DeepSeek, OpenRouter, Groq, Together**, and local servers (**Ollama, vLLM, LM Studio** — no key needed). The plugin handles reasoning models (`o1`, `o3`, `gpt-5`) automatically.
108
118
 
109
119
  Choose the model with `AMEM_LLM_MODEL`. Full env-var reference and examples: **[amem.owo.lc/reference/configuration](https://amem.owo.lc/reference/configuration)**.
110
120
 
@@ -131,13 +141,13 @@ Once installed, the plugin exposes five tools to the agent:
131
141
 
132
142
  ## Security & data flow
133
143
 
134
- A memory plugin's job is to read configuration from the environment and send memory data to backends **you** control, so registry static scanners flag its `env` + `network` pattern (e.g. ClawHub's `suspicious.env_credential_access`). This is structurally expected for any configurable memory/LLM plugin the audit outcome is **advisory (`Review`), not `Malicious`**, and VirusTotal reports the bundle clean.
144
+ A memory plugin's job is to read configuration from the environment and send memory data to backends **you** control, so registry static scanners flag its `env` + `network` pattern (for example, ClawHub's `suspicious.env_credential_access`). This is structurally expected for any configurable memory/LLM plugin. The audit outcome is **advisory (`Review`), not `Malicious`**, and VirusTotal reports the bundle clean.
135
145
 
136
146
  What it actually does — all of it declared in [`openclaw.plugin.json`](openclaw.plugin.json):
137
147
 
138
148
  - **Environment variables it reads** (its configuration surface, supplied by you): `AMEM_LLM_PROVIDER`, `AMEM_LLM_API_KEY`, `OPENAI_API_KEY`, `AMEM_LLM_BASE_URL`, `AMEM_LLM_MODEL`, `AMEM_COLLECTION`, `AMEM_DATA_DIR`, `AMEM_EVO_COUNTER_PATH`, `AMEM_REVIEW_DIR`, `AMEM_PROMPT_LOCALE`. No credential is bundled, hardcoded, or logged.
139
149
  - **Network destinations**: only your **local Qdrant** (`http://localhost:6333`) and your configured **LLM endpoint** (Anthropic by default, or any OpenAI-compatible endpoint via `AMEM_LLM_PROVIDER=openai` + `AMEM_LLM_BASE_URL`). It sends memory text/embeddings there to store and evolve notes — its stated purpose. It does not phone home.
140
- - **Conversation content** is processed for memory only when you set `hooks.allowConversationAccess: true`. Keep Qdrant and review-output paths scoped to locations you control.
150
+ - The plugin processes **conversation content** for memory only when you set `hooks.allowConversationAccess: true`. Keep Qdrant and review-output paths scoped to locations you control.
141
151
 
142
152
  ## Development
143
153
 
package/dist/index.js CHANGED
@@ -38,17 +38,22 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
38
38
  // ../amem-core/src/config.ts
39
39
  function configure(opts) {
40
40
  if (opts.dataDir) _dataDir = opts.dataDir;
41
+ if (opts.warn) _warn = opts.warn;
41
42
  }
42
43
  function getDataDir() {
43
44
  return _dataDir;
44
45
  }
45
- var os, path, _dataDir;
46
+ function warn(msg) {
47
+ _warn(msg);
48
+ }
49
+ var os, path, _dataDir, _warn;
46
50
  var init_config = __esm({
47
51
  "../amem-core/src/config.ts"() {
48
52
  "use strict";
49
53
  os = __toESM(require("os"), 1);
50
54
  path = __toESM(require("path"), 1);
51
55
  _dataDir = process.env.AMEM_DATA_DIR || path.join(os.homedir(), ".amem");
56
+ _warn = (msg) => console.warn(msg);
52
57
  }
53
58
  });
54
59
 
@@ -87,6 +92,12 @@ function applyModelPaths(env) {
87
92
  env.allowLocalModels = true;
88
93
  }
89
94
  }
95
+ function humanBytes(n) {
96
+ if (n >= 1e9) return `${(n / 1e9).toFixed(2)} GB`;
97
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)} MB`;
98
+ if (n >= 1e3) return `${Math.round(n / 1e3)} kB`;
99
+ return `${n} B`;
100
+ }
90
101
  function makeProgressReporter() {
91
102
  const lastPct = /* @__PURE__ */ new Map();
92
103
  return (e) => {
@@ -95,7 +106,7 @@ function makeProgressReporter() {
95
106
  const pct = Math.floor(e.progress / 10) * 10;
96
107
  if (lastPct.get(e.file) === pct) return;
97
108
  lastPct.set(e.file, pct);
98
- const size = e.total ? ` of ${(e.total / 1e9).toFixed(2)} GB` : "";
109
+ const size = e.total ? ` of ${humanBytes(e.total)}` : "";
99
110
  console.log(`[amem] downloading ${e.file}: ${pct}%${size}`);
100
111
  };
101
112
  }
@@ -294,7 +305,7 @@ async function ensureCollection(collectionName) {
294
305
  if (inUse !== null && inUse !== wanted) throw new MixedEmbeddingModelsError(col, wanted, inUse);
295
306
  pinEmbeddingModel(wanted);
296
307
  if (wanted === LEGACY_DEFAULT_EMBEDDING_MODEL) {
297
- console.warn(
308
+ warn(
298
309
  `[amem] "${col}" is on ${LEGACY_DEFAULT_EMBEDDING_MODEL} (${LEGACY_DEFAULT_DIM}-dim).
299
310
  [amem] ${DEFAULT_EMBEDDING_MODEL} reads 8192 tokens where that one stops at 128, so anything longer is being truncated before it reaches the vector.
300
311
  [amem] To move: npx --package=@amemhq/core amem-migrate --from-collection ${col}`
@@ -698,7 +709,7 @@ function makeCrud(collectionName, modeBIsolated = false) {
698
709
  })
699
710
  )
700
711
  ]).catch((err) => {
701
- console.error(`[amem] retrieval tracking patch failed: ${err.message}`);
712
+ warn(`[amem] retrieval tracking patch failed: ${err.message}`);
702
713
  });
703
714
  for (const r of queryResults) {
704
715
  r.note.retrieval_count = (r.note.retrieval_count || 0) + 1;
@@ -818,6 +829,7 @@ var init_storage = __esm({
818
829
  "../amem-core/src/storage.ts"() {
819
830
  "use strict";
820
831
  init_auth();
832
+ init_config();
821
833
  init_embedding();
822
834
  QDRANT_URL = "http://localhost:6333";
823
835
  getCollection = () => process.env.AMEM_COLLECTION || "amem_notes";
@@ -1128,7 +1140,7 @@ function configureLlm(cfg) {
1128
1140
  function warnOnce(key, message) {
1129
1141
  if (_warned.has(key)) return;
1130
1142
  _warned.add(key);
1131
- console.error(message);
1143
+ warn(message);
1132
1144
  }
1133
1145
  function resolveProvider(role = "fast") {
1134
1146
  const raw = role === "strong" ? process.env.AMEM_LLM_STRONG_PROVIDER || _override.strong?.provider || void 0 : void 0;
@@ -1197,9 +1209,11 @@ async function llmCall(prompt, maxTokens = 500, role = "fast") {
1197
1209
  const isThinking = model.includes("gemini") || model.includes("pro-agent");
1198
1210
  const effectiveMaxTokens = isThinking ? Math.max(maxTokens * 8, 4e3) : maxTokens;
1199
1211
  try {
1200
- return provider === "openai" ? await openaiCall(prompt, model, effectiveMaxTokens, baseURL) : await anthropicCall(prompt, model, effectiveMaxTokens, baseURL);
1212
+ const text = provider === "openai" ? await openaiCall(prompt, model, effectiveMaxTokens, baseURL) : await anthropicCall(prompt, model, effectiveMaxTokens, baseURL);
1213
+ if (text === null) warn(`[amem] ${provider} answered with no text (model ${model})`);
1214
+ return text;
1201
1215
  } catch (e) {
1202
- console.error(`[amem] LLM call failed: ${e.message}`);
1216
+ warn(`[amem] LLM call failed: ${e.message}`);
1203
1217
  return null;
1204
1218
  }
1205
1219
  }
@@ -1288,7 +1302,8 @@ confidence guide (Story 27):
1288
1302
 
1289
1303
  Text: ${content}`;
1290
1304
  const raw = await llmCall(prompt, 400);
1291
- if (!raw)
1305
+ if (!raw) {
1306
+ warn("[amem] note construction got nothing back; storing with no keywords, tags or context");
1292
1307
  return {
1293
1308
  keywords: [],
1294
1309
  tags: [],
@@ -1298,6 +1313,7 @@ Text: ${content}`;
1298
1313
  topics: [],
1299
1314
  confidence: "medium"
1300
1315
  };
1316
+ }
1301
1317
  try {
1302
1318
  const data = parseJsonLoose(raw);
1303
1319
  const rawCategory = typeof data.category === "string" ? data.category : "General";
@@ -1316,7 +1332,7 @@ Text: ${content}`;
1316
1332
  confidence
1317
1333
  };
1318
1334
  } catch (e) {
1319
- console.error(`[amem] Note construction parse failed: ${e.message}`);
1335
+ warn(`[amem] Note construction parse failed: ${e.message}`);
1320
1336
  return {
1321
1337
  keywords: [],
1322
1338
  tags: [],
@@ -1345,9 +1361,15 @@ async function llmCrudDecision(userText, assistantText, existingMemories) {
1345
1361
  const raw = await llmCall(prompt, 400, resolveCrudRole());
1346
1362
  if (!raw) return [];
1347
1363
  const match = stripReasoning(raw).match(/\[.*\]/s);
1348
- if (!match) return [];
1364
+ if (!match) {
1365
+ warn("[amem] llmCrudDecision found no array in the response; nothing from this turn is stored");
1366
+ return [];
1367
+ }
1349
1368
  const parsed = JSON.parse(match[0]);
1350
- if (!Array.isArray(parsed)) return [];
1369
+ if (!Array.isArray(parsed)) {
1370
+ warn("[amem] llmCrudDecision parsed a non-array; nothing from this turn is stored");
1371
+ return [];
1372
+ }
1351
1373
  const ops = [];
1352
1374
  for (const item of parsed) {
1353
1375
  if (!item || typeof item !== "object") continue;
@@ -1366,7 +1388,7 @@ async function llmCrudDecision(userText, assistantText, existingMemories) {
1366
1388
  }
1367
1389
  return ops.slice(0, 3);
1368
1390
  } catch (e) {
1369
- console.error(`[amem] llmCrudDecision failed: ${e.message}`);
1391
+ warn(`[amem] llmCrudDecision failed: ${e.message}`);
1370
1392
  return [];
1371
1393
  }
1372
1394
  }
@@ -1376,20 +1398,26 @@ async function llmShouldMerge(contentA, contentB) {
1376
1398
  if (!raw) return { shouldMerge: false };
1377
1399
  try {
1378
1400
  const data = parseJsonLoose(raw);
1379
- if (typeof data.shouldMerge !== "boolean") return { shouldMerge: false };
1401
+ if (typeof data.shouldMerge !== "boolean") {
1402
+ warn("[amem] llmShouldMerge got no boolean verdict; treating the pair as distinct");
1403
+ return { shouldMerge: false };
1404
+ }
1380
1405
  if (data.shouldMerge && typeof data.merged === "string") {
1381
1406
  return { shouldMerge: true, merged: data.merged };
1382
1407
  }
1383
1408
  return { shouldMerge: false };
1384
1409
  } catch (e) {
1385
- console.error(`[amem] llmShouldMerge parse failed: ${e.message}`);
1410
+ warn(`[amem] llmShouldMerge parse failed: ${e.message}`);
1386
1411
  return { shouldMerge: false };
1387
1412
  }
1388
1413
  }
1389
1414
  async function llmEvolutionJudge(oldContent, newContent) {
1390
1415
  const prompt = t.evolutionJudge(oldContent, newContent);
1391
1416
  const raw = await llmCall(prompt, 300, "strong");
1392
- if (!raw) return { type: "NEW" };
1417
+ if (!raw) {
1418
+ warn("[amem] evolution judge got nothing back; defaulting the pair to NEW");
1419
+ return { type: "NEW" };
1420
+ }
1393
1421
  try {
1394
1422
  const data = parseJsonLoose(raw);
1395
1423
  const type = VALID_EVOLUTION_TYPES.has(data.type) ? data.type : "NEW";
@@ -1398,7 +1426,7 @@ async function llmEvolutionJudge(oldContent, newContent) {
1398
1426
  mergedContent: typeof data.mergedContent === "string" ? data.mergedContent : void 0
1399
1427
  };
1400
1428
  } catch (e) {
1401
- console.error(`[amem] llmEvolutionJudge parse failed: ${e.message}`);
1429
+ warn(`[amem] llmEvolutionJudge parse failed: ${e.message}`);
1402
1430
  return { type: "NEW" };
1403
1431
  }
1404
1432
  }
@@ -1437,7 +1465,7 @@ ${linkedStr}`;
1437
1465
  tagsToUpdate: Array.isArray(data.tags_to_update) ? data.tags_to_update.map(String) : []
1438
1466
  };
1439
1467
  } catch (e) {
1440
- console.error(`[amem] Evolution parse failed: ${e.message}`);
1468
+ warn(`[amem] Evolution parse failed: ${e.message}`);
1441
1469
  return { tags: null, context: null, shouldStrengthen: false, suggestedConnections: [], tagsToUpdate: [] };
1442
1470
  }
1443
1471
  }
@@ -1449,9 +1477,15 @@ async function llmConflictScan(contents) {
1449
1477
  if (!raw) return [];
1450
1478
  const cleaned = stripReasoning(raw);
1451
1479
  const match = cleaned.match(/\[[\s\S]*\]/);
1452
- if (!match) return [];
1480
+ if (!match) {
1481
+ warn("[amem] llmConflictScan found no array in the response; this sweep reports no pairs");
1482
+ return [];
1483
+ }
1453
1484
  const parsed = JSON.parse(match[0]);
1454
- if (!Array.isArray(parsed)) return [];
1485
+ if (!Array.isArray(parsed)) {
1486
+ warn("[amem] llmConflictScan parsed a non-array; this sweep reports no pairs");
1487
+ return [];
1488
+ }
1455
1489
  const pairs = [];
1456
1490
  const seen = /* @__PURE__ */ new Set();
1457
1491
  for (const item of parsed) {
@@ -1475,7 +1509,7 @@ async function llmConflictScan(contents) {
1475
1509
  }
1476
1510
  return pairs;
1477
1511
  } catch (e) {
1478
- console.error(`[amem] llmConflictScan failed: ${e.message}`);
1512
+ warn(`[amem] llmConflictScan failed: ${e.message}`);
1479
1513
  return [];
1480
1514
  }
1481
1515
  }
@@ -1486,6 +1520,7 @@ var init_llm = __esm({
1486
1520
  import_sdk = __toESM(require("@anthropic-ai/sdk"), 1);
1487
1521
  import_openai = __toESM(require("openai"), 1);
1488
1522
  init_prompts();
1523
+ init_config();
1489
1524
  _override = {};
1490
1525
  _warned = /* @__PURE__ */ new Set();
1491
1526
  DEFAULT_TIMEOUT_MS = 3e4;
@@ -1817,7 +1852,7 @@ async function addMemory(content, agentId = "main", opts) {
1817
1852
  }
1818
1853
  }
1819
1854
  } catch (e) {
1820
- console.error(`[warn] Link/Evolution phase failed: ${e.message}`);
1855
+ warn(`[amem] link/evolution phase failed: ${e.message}`);
1821
1856
  }
1822
1857
  console.log(`[done] Note added: ${note.id}`);
1823
1858
  return note.id;
@@ -1942,7 +1977,12 @@ async function searchMemory(query, topK = 5, agentId = "main", opts) {
1942
1977
  keywords: note.keywords,
1943
1978
  links: note.links,
1944
1979
  timestamp: note.timestamp,
1945
- similarity: embSimMap.get(id) ?? bfsSimMap.get(id) ?? 0,
1980
+ // Neither map covers a note that got here on BM25 alone: it was never in
1981
+ // the dense results, and it was not expanded into. That is a real cosine
1982
+ // nobody had measured, not a zero — and reporting 0 made a lexical match
1983
+ // look like the least relevant row in the list. Both vectors are already
1984
+ // in hand, so measuring it is one dot product.
1985
+ similarity: embSimMap.get(id) ?? bfsSimMap.get(id) ?? cosineSimilarity(queryEmbedding, note.embedding),
1946
1986
  rrf: rrfMap.get(id) ?? 0,
1947
1987
  via,
1948
1988
  topics: note.topics ?? [],
@@ -2078,8 +2118,8 @@ async function consolidateMemories(agentId, logger, storageCtx) {
2078
2118
  const ctx = storageCtx ?? defaultCtx();
2079
2119
  const log = {
2080
2120
  info: (msg) => logger ? logger.info(msg) : console.log(msg),
2081
- warn: (msg) => logger ? logger.warn(msg) : console.warn(msg),
2082
- error: (msg) => logger ? logger.error(msg) : console.error(msg)
2121
+ warn: (msg) => logger ? logger.warn(msg) : warn(msg),
2122
+ error: (msg) => logger ? logger.error(msg) : warn(msg)
2083
2123
  };
2084
2124
  log.info(`[Consolidation] Starting consolidation for agentId: ${agentId}`);
2085
2125
  const rawNotes = await ctx.listNotes(agentId);
@@ -2483,7 +2523,7 @@ async function migrateCollection(opts) {
2483
2523
  const refreshFields = opts.refreshFields !== false;
2484
2524
  const dryRun = opts.dryRun !== false;
2485
2525
  const log = opts.logger?.info ?? ((m) => console.log(m));
2486
- const warn = opts.logger?.warn ?? ((m) => console.warn(m));
2526
+ const warn2 = opts.logger?.warn ?? warn;
2487
2527
  if (from === to) throw new Error(`migrate: source and target are the same collection ("${from}")`);
2488
2528
  const model = getEmbeddingModel();
2489
2529
  const targetDim = await getEmbeddingDim();
@@ -2551,7 +2591,7 @@ async function migrateCollection(opts) {
2551
2591
  if (!note.context) note.context = built.context;
2552
2592
  refreshed++;
2553
2593
  } catch (e) {
2554
- warn(`[migrate] re-extract failed for ${note.id.slice(0, 8)} \u2014 keeping as-is: ${e.message}`);
2594
+ warn2(`[migrate] re-extract failed for ${note.id.slice(0, 8)} \u2014 keeping as-is: ${e.message}`);
2555
2595
  }
2556
2596
  }
2557
2597
  const point = noteToPoint({ ...note, embedding: await encode(buildEmbedText(note)) });
@@ -2564,7 +2604,7 @@ async function migrateCollection(opts) {
2564
2604
  await flush();
2565
2605
  const finalCount = await countPointsRaw(to);
2566
2606
  if (finalCount !== notes.length) {
2567
- warn(`[migrate] target holds ${finalCount} point(s) but the source had ${notes.length} \u2014 check before switching`);
2607
+ warn2(`[migrate] target holds ${finalCount} point(s) but the source had ${notes.length} \u2014 check before switching`);
2568
2608
  }
2569
2609
  log(`[migrate] done: ${migrated} written, ${alreadyDone.size} already present, ${refreshed} re-extracted.`);
2570
2610
  return {
@@ -2619,6 +2659,7 @@ var init_migrate = __esm({
2619
2659
  init_embedding();
2620
2660
  init_llm();
2621
2661
  init_memory();
2662
+ init_config();
2622
2663
  }
2623
2664
  });
2624
2665
 
@@ -2777,7 +2818,10 @@ function register(api) {
2777
2818
  const pluginId = api.id ?? "openclaw-amem";
2778
2819
  const convBlocked = isConvAccessBlocked(api.config, pluginId);
2779
2820
  if (convBlocked) logger.warn(BLOCKED_WARNING_LOG);
2780
- configure({ dataDir: path5.join(os2.homedir(), ".openclaw") });
2821
+ configure({
2822
+ dataDir: path5.join(os2.homedir(), ".openclaw"),
2823
+ warn: (msg) => logger.warn(msg)
2824
+ });
2781
2825
  const hasStrong = !!(pluginConfig.llmStrongProvider || pluginConfig.llmStrongModel || pluginConfig.llmStrongBaseURL);
2782
2826
  if (pluginConfig.llmProvider || pluginConfig.llmModel || pluginConfig.llmBaseURL || pluginConfig.llmCrudRole || hasStrong) {
2783
2827
  configureLlm({
@@ -3127,7 +3171,12 @@ ${text}${hookWarning}` }],
3127
3171
  const msgs = event.messages || [];
3128
3172
  const lastUser = [...msgs].reverse().find((m) => m.role === "user");
3129
3173
  const lastAssistant = [...msgs].reverse().find((m) => m.role === "assistant");
3130
- if (!lastUser || !lastAssistant) return;
3174
+ if (!lastUser || !lastAssistant) {
3175
+ logger.info(
3176
+ `openclaw-amem: agent_end skipped \u2014 no ${!lastUser ? "user" : "assistant"} message in ${msgs.length} messages`
3177
+ );
3178
+ return;
3179
+ }
3131
3180
  const userText = typeof lastUser.content === "string" ? lastUser.content : lastUser.content.filter((b) => b.type === "text").map((b) => b.text).join(" ");
3132
3181
  const assistantText = typeof lastAssistant.content === "string" ? lastAssistant.content : lastAssistant.content.filter((b) => b.type === "text").map((b) => b.text).join(" ");
3133
3182
  if (!userText || !shouldProcessTurn2(userText)) return;
@@ -3,7 +3,7 @@
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.1.0",
6
+ "version": "2.1.2",
7
7
  "kind": "memory",
8
8
  "contracts": {
9
9
  "tools": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-amem",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
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",
@@ -16,7 +16,7 @@
16
16
  }
17
17
  },
18
18
  "dependencies": {
19
- "@anthropic-ai/sdk": "^0.112.5",
19
+ "@anthropic-ai/sdk": "^0.122.0",
20
20
  "@huggingface/transformers": "^4.2.0",
21
21
  "@node-rs/jieba": "^2.0.1",
22
22
  "@qdrant/js-client-rest": "^1.18.0",
@@ -26,7 +26,6 @@
26
26
  "devDependencies": {
27
27
  "@eslint/js": "^10.0.1",
28
28
  "@types/node": "^26.1.1",
29
- "@types/uuid": "^11.0.0",
30
29
  "eslint": "^10.7.0",
31
30
  "prettier": "^3.9.6",
32
31
  "tsup": "^8.4.0",