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 +21 -11
- package/dist/index.js +78 -29
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -3
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
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,
|
|
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
|
|
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).
|
|
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 (
|
|
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
|
-
- **
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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))
|
|
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
|
-
|
|
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")
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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))
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) :
|
|
2082
|
-
error: (msg) => logger ? logger.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
|
|
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
|
-
|
|
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
|
-
|
|
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({
|
|
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)
|
|
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;
|
package/openclaw.plugin.json
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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",
|