openclaw-amem 2.1.1 → 2.1.3

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
@@ -61,12 +61,11 @@ Updating:
61
61
 
62
62
  ```bash
63
63
  openclaw plugins update openclaw-amem
64
- openclaw gateway restart
65
64
  ```
66
65
 
67
- An update rebuilds `node_modules`, and the model cache lives there by default — so
68
- it re-downloads 2.27 GB unless `AMEM_MODEL_CACHE` points somewhere outside the
69
- plugin directory. Set that once and updates cost nothing.
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.
70
69
 
71
70
  ### 2. Configure `~/.openclaw/openclaw.json`
72
71
 
@@ -92,7 +91,7 @@ Add `openclaw-amem` to your allowed plugins and hook it into the `memory` slot:
92
91
  }
93
92
  ```
94
93
 
95
- > **⚠️ 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.
96
95
 
97
96
  ### 3. Restart OpenClaw
98
97
 
@@ -103,11 +102,11 @@ openclaw gateway restart
103
102
  First run downloads the embedding model (`bge-m3`, 2.27 GB) and caches it. Later
104
103
  restarts are instant. If that is too much, the only smaller model worth setting is
105
104
  `AMEM_EMBED_MODEL=Xenova/bge-small-zh-v1.5` — 25 MB, Chinese only, and it caps at
106
- 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
107
106
  a [migration](https://amem.owo.lc/reference/embedding-models#changing-the-model-on-a-store-you-already-have).
108
107
 
109
108
  Upgrading from 1.x downloads nothing. Your existing memories keep the model that
110
- 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
111
110
  moves them.
112
111
 
113
112
  ## LLM provider
@@ -115,7 +114,7 @@ moves them.
115
114
  The plugin calls an LLM for note construction, linking, and evolution. Pick the backend with `AMEM_LLM_PROVIDER`:
116
115
 
117
116
  - **`anthropic`** (default) — the Anthropic Messages API. Set `ANTHROPIC_API_KEY`.
118
- - **`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.
119
118
 
120
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)**.
121
120
 
@@ -142,13 +141,13 @@ Once installed, the plugin exposes five tools to the agent:
142
141
 
143
142
  ## Security & data flow
144
143
 
145
- 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.
146
145
 
147
146
  What it actually does — all of it declared in [`openclaw.plugin.json`](openclaw.plugin.json):
148
147
 
149
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.
150
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.
151
- - **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.
152
151
 
153
152
  ## Development
154
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
  }
@@ -246,15 +257,15 @@ async function recordCollectionModel(collection, model) {
246
257
  } catch {
247
258
  }
248
259
  }
249
- async function qdrant(method, path6, body) {
250
- const res = await fetch(`${QDRANT_URL}${path6}`, {
260
+ async function qdrant(method, path7, body) {
261
+ const res = await fetch(`${QDRANT_URL}${path7}`, {
251
262
  method,
252
263
  headers: { "Content-Type": "application/json" },
253
264
  body: body ? JSON.stringify(body) : void 0
254
265
  });
255
266
  const data = await res.json();
256
267
  if (!res.ok || data.status && data.status !== "ok" && data.status !== "acknowledged") {
257
- throw new Error(`Qdrant ${method} ${path6} failed: ${data.error || JSON.stringify(data)}`);
268
+ throw new Error(`Qdrant ${method} ${path7} failed: ${data.error || JSON.stringify(data)}`);
258
269
  }
259
270
  return data.result;
260
271
  }
@@ -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}`
@@ -307,12 +318,12 @@ async function ensureCollection(collectionName) {
307
318
  throw new EmbeddingDimensionMismatchError(col, collectionDim, modelDim, getEmbeddingModel());
308
319
  }
309
320
  }
310
- const current = getEmbeddingModel();
311
- if (typeof recorded === "string" && recorded !== current) {
312
- throw new EmbeddingModelMismatchError(col, recorded, current);
321
+ const current2 = getEmbeddingModel();
322
+ if (typeof recorded === "string" && recorded !== current2) {
323
+ throw new EmbeddingModelMismatchError(col, recorded, current2);
313
324
  }
314
325
  if (recorded === void 0 && !inferLegacy) {
315
- await recordCollectionModel(col, current);
326
+ await recordCollectionModel(col, current2);
316
327
  }
317
328
  markReady();
318
329
  return;
@@ -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";
@@ -1119,6 +1131,45 @@ ${numberedNotes}
1119
1131
  }
1120
1132
  });
1121
1133
 
1134
+ // ../amem-core/src/deadline.ts
1135
+ function hasTimeFor(deadline) {
1136
+ return deadline === void 0 || deadline - Date.now() >= MIN_CALL_MS;
1137
+ }
1138
+ var MIN_CALL_MS;
1139
+ var init_deadline = __esm({
1140
+ "../amem-core/src/deadline.ts"() {
1141
+ "use strict";
1142
+ MIN_CALL_MS = 3e3;
1143
+ }
1144
+ });
1145
+
1146
+ // ../amem-core/src/background.ts
1147
+ function runInBackground(busy, job) {
1148
+ return current.run(busy, job);
1149
+ }
1150
+ function stopIfForegroundBusy() {
1151
+ if (current.getStore()?.()) throw new BackgroundPreempted();
1152
+ }
1153
+ async function pauseForForeground() {
1154
+ if (!current.getStore()) return;
1155
+ await new Promise((resolve2) => setImmediate(resolve2));
1156
+ stopIfForegroundBusy();
1157
+ }
1158
+ var import_node_async_hooks, current, BackgroundPreempted;
1159
+ var init_background = __esm({
1160
+ "../amem-core/src/background.ts"() {
1161
+ "use strict";
1162
+ import_node_async_hooks = require("async_hooks");
1163
+ current = new import_node_async_hooks.AsyncLocalStorage();
1164
+ BackgroundPreempted = class extends Error {
1165
+ constructor() {
1166
+ super("stopped for a run in the foreground");
1167
+ this.name = "BackgroundPreempted";
1168
+ }
1169
+ };
1170
+ }
1171
+ });
1172
+
1122
1173
  // ../amem-core/src/llm.ts
1123
1174
  function configureLlm(cfg) {
1124
1175
  _override = { ...cfg };
@@ -1128,7 +1179,7 @@ function configureLlm(cfg) {
1128
1179
  function warnOnce(key, message) {
1129
1180
  if (_warned.has(key)) return;
1130
1181
  _warned.add(key);
1131
- console.error(message);
1182
+ warn(message);
1132
1183
  }
1133
1184
  function resolveProvider(role = "fast") {
1134
1185
  const raw = role === "strong" ? process.env.AMEM_LLM_STRONG_PROVIDER || _override.strong?.provider || void 0 : void 0;
@@ -1154,6 +1205,12 @@ function resolveCrudRole() {
1154
1205
  }
1155
1206
  return "fast";
1156
1207
  }
1208
+ function resolveThinking() {
1209
+ const raw = (process.env.AMEM_LLM_THINKING || _override.thinking || "off").trim().toLowerCase();
1210
+ if (raw === "auto") return "auto";
1211
+ if (raw !== "off") warnOnce(`thinking:${raw}`, `[amem] unknown AMEM_LLM_THINKING "${raw}"; using off`);
1212
+ return "off";
1213
+ }
1157
1214
  function resolveTimeoutMs() {
1158
1215
  const envVal = Number(process.env.AMEM_LLM_TIMEOUT);
1159
1216
  if (Number.isFinite(envVal) && envVal > 0) return envVal;
@@ -1190,37 +1247,70 @@ function openai(baseURL) {
1190
1247
  }
1191
1248
  return client;
1192
1249
  }
1193
- async function llmCall(prompt, maxTokens = 500, role = "fast") {
1250
+ async function llmCall(prompt, maxTokens = 500, role = "fast", opts = {}) {
1251
+ stopIfForegroundBusy();
1252
+ if (!hasTimeFor(opts.deadline)) {
1253
+ warn(`[amem] skipped an LLM call: less than ${MIN_CALL_MS} ms left before the deadline`);
1254
+ return null;
1255
+ }
1256
+ const request = () => opts.deadline === void 0 ? void 0 : { timeout: Math.min(resolveTimeoutMs(), opts.deadline - Date.now()), maxRetries: 0 };
1194
1257
  const provider = resolveProvider(role);
1195
1258
  const model = resolveModel(role);
1196
1259
  const baseURL = resolveBaseURL(role);
1197
- const isThinking = model.includes("gemini") || model.includes("pro-agent");
1198
- const effectiveMaxTokens = isThinking ? Math.max(maxTokens * 8, 4e3) : maxTokens;
1260
+ const pair = `${baseURL ?? ""}|${model}`;
1261
+ const thinkingOff = provider !== "openai" && role === "fast" && resolveThinking() === "off" && !_thinkingStaysOn.has(pair);
1262
+ const mayThink = model.includes("gemini") || model.includes("pro-agent") || (provider === "openai" ? isOpenAIReasoning(model) : !thinkingOff);
1263
+ const effectiveMaxTokens = mayThink ? roomToThink(maxTokens) : maxTokens;
1264
+ let text = null;
1199
1265
  try {
1200
- return provider === "openai" ? await openaiCall(prompt, model, effectiveMaxTokens, baseURL) : await anthropicCall(prompt, model, effectiveMaxTokens, baseURL);
1266
+ if (provider === "openai") {
1267
+ text = await openaiCall(prompt, model, effectiveMaxTokens, baseURL, request());
1268
+ } else if (!thinkingOff) {
1269
+ text = await anthropicCall(prompt, model, effectiveMaxTokens, baseURL, request());
1270
+ } else {
1271
+ try {
1272
+ text = await anthropicCall(prompt, model, effectiveMaxTokens, baseURL, request(), { type: "disabled" });
1273
+ } catch (e) {
1274
+ if (e.status !== 400 || !hasTimeFor(opts.deadline)) throw e;
1275
+ text = await anthropicCall(prompt, model, roomToThink(maxTokens), baseURL, request());
1276
+ _thinkingStaysOn.add(pair);
1277
+ warnOnce(`thinking-on:${pair}`, `[amem] ${model} refused thinking disabled; its fast calls keep thinking`);
1278
+ }
1279
+ }
1280
+ if (text === null) warn(`[amem] ${provider} answered with no text (model ${model})`);
1201
1281
  } catch (e) {
1202
- console.error(`[amem] LLM call failed: ${e.message}`);
1203
- return null;
1282
+ warn(`[amem] LLM call failed: ${e.message}`);
1204
1283
  }
1284
+ stopIfForegroundBusy();
1285
+ return text;
1205
1286
  }
1206
- async function anthropicCall(prompt, model, maxTokens, baseURL) {
1207
- const resp = await anthropic(baseURL).messages.create({
1208
- model,
1209
- max_tokens: maxTokens,
1210
- messages: [{ role: "user", content: prompt }]
1211
- });
1287
+ async function anthropicCall(prompt, model, maxTokens, baseURL, request, thinking) {
1288
+ const resp = await anthropic(baseURL).messages.create(
1289
+ {
1290
+ model,
1291
+ max_tokens: maxTokens,
1292
+ messages: [{ role: "user", content: prompt }],
1293
+ ...thinking && { thinking }
1294
+ },
1295
+ request
1296
+ );
1212
1297
  for (const block of resp.content) {
1213
1298
  if (block.type === "text") return block.text.trim();
1214
1299
  }
1215
1300
  return null;
1216
1301
  }
1217
- async function openaiCall(prompt, model, maxTokens, baseURL) {
1218
- const isReasoning = /^o\d/.test(model) || model.startsWith("gpt-5");
1219
- const resp = await openai(baseURL).chat.completions.create({
1220
- model,
1221
- ...isReasoning ? { max_completion_tokens: maxTokens } : { max_tokens: maxTokens },
1222
- messages: [{ role: "user", content: prompt }]
1223
- });
1302
+ function isOpenAIReasoning(model) {
1303
+ return /^o\d/.test(model) || model.startsWith("gpt-5");
1304
+ }
1305
+ async function openaiCall(prompt, model, maxTokens, baseURL, request) {
1306
+ const resp = await openai(baseURL).chat.completions.create(
1307
+ {
1308
+ model,
1309
+ ...isOpenAIReasoning(model) ? { max_completion_tokens: maxTokens } : { max_tokens: maxTokens },
1310
+ messages: [{ role: "user", content: prompt }]
1311
+ },
1312
+ request
1313
+ );
1224
1314
  return resp.choices[0]?.message?.content?.trim() ?? null;
1225
1315
  }
1226
1316
  function stripReasoning(raw) {
@@ -1252,7 +1342,7 @@ function parseJsonLoose(raw) {
1252
1342
  throw e;
1253
1343
  }
1254
1344
  }
1255
- async function llmConstructNote(content) {
1345
+ async function llmConstructNote(content, opts = {}) {
1256
1346
  const prompt = `Analyze the following text and respond with valid JSON only (no markdown fences, no explanation, no comments). All string values must use standard double quotes and be properly escaped:
1257
1347
  {
1258
1348
  "keywords": ["keyword1", "keyword2"],
@@ -1287,8 +1377,9 @@ confidence guide (Story 27):
1287
1377
  - low: LLM is uncertain \u2014 vague, fragmentary, or mixed content
1288
1378
 
1289
1379
  Text: ${content}`;
1290
- const raw = await llmCall(prompt, 400);
1291
- if (!raw)
1380
+ const raw = await llmCall(prompt, 400, "fast", opts);
1381
+ if (!raw) {
1382
+ warn("[amem] note construction got nothing back; storing with no keywords, tags or context");
1292
1383
  return {
1293
1384
  keywords: [],
1294
1385
  tags: [],
@@ -1298,6 +1389,7 @@ Text: ${content}`;
1298
1389
  topics: [],
1299
1390
  confidence: "medium"
1300
1391
  };
1392
+ }
1301
1393
  try {
1302
1394
  const data = parseJsonLoose(raw);
1303
1395
  const rawCategory = typeof data.category === "string" ? data.category : "General";
@@ -1316,7 +1408,7 @@ Text: ${content}`;
1316
1408
  confidence
1317
1409
  };
1318
1410
  } catch (e) {
1319
- console.error(`[amem] Note construction parse failed: ${e.message}`);
1411
+ warn(`[amem] Note construction parse failed: ${e.message}`);
1320
1412
  return {
1321
1413
  keywords: [],
1322
1414
  tags: [],
@@ -1328,26 +1420,32 @@ Text: ${content}`;
1328
1420
  };
1329
1421
  }
1330
1422
  }
1331
- async function llmShouldLink(noteContent, candidateContent) {
1423
+ async function llmShouldLink(noteContent, candidateContent, opts = {}) {
1332
1424
  const prompt = `Do these two memory notes have a meaningful relationship that would be useful to link?
1333
1425
  Reply with only "yes" or "no".
1334
1426
 
1335
1427
  Note A: ${noteContent}
1336
1428
  Note B: ${candidateContent}`;
1337
- const raw = await llmCall(prompt, 10);
1429
+ const raw = await llmCall(prompt, 10, "fast", opts);
1338
1430
  if (!raw) return false;
1339
1431
  return raw.toLowerCase().startsWith("yes");
1340
1432
  }
1341
- async function llmCrudDecision(userText, assistantText, existingMemories) {
1433
+ async function llmCrudDecision(userText, assistantText, existingMemories, opts = {}) {
1342
1434
  const memoryList = existingMemories.length > 0 ? existingMemories.map((m) => `[${m.idx}] ${m.content}`).join("\n") : "(none)";
1343
1435
  const prompt = t.crudDecision(userText.slice(0, 500), assistantText.slice(0, 500), memoryList);
1344
1436
  try {
1345
- const raw = await llmCall(prompt, 400, resolveCrudRole());
1437
+ const raw = await llmCall(prompt, 400, resolveCrudRole(), opts);
1346
1438
  if (!raw) return [];
1347
1439
  const match = stripReasoning(raw).match(/\[.*\]/s);
1348
- if (!match) return [];
1440
+ if (!match) {
1441
+ warn("[amem] llmCrudDecision found no array in the response; nothing from this turn is stored");
1442
+ return [];
1443
+ }
1349
1444
  const parsed = JSON.parse(match[0]);
1350
- if (!Array.isArray(parsed)) return [];
1445
+ if (!Array.isArray(parsed)) {
1446
+ warn("[amem] llmCrudDecision parsed a non-array; nothing from this turn is stored");
1447
+ return [];
1448
+ }
1351
1449
  const ops = [];
1352
1450
  for (const item of parsed) {
1353
1451
  if (!item || typeof item !== "object") continue;
@@ -1366,7 +1464,7 @@ async function llmCrudDecision(userText, assistantText, existingMemories) {
1366
1464
  }
1367
1465
  return ops.slice(0, 3);
1368
1466
  } catch (e) {
1369
- console.error(`[amem] llmCrudDecision failed: ${e.message}`);
1467
+ warn(`[amem] llmCrudDecision failed: ${e.message}`);
1370
1468
  return [];
1371
1469
  }
1372
1470
  }
@@ -1376,20 +1474,26 @@ async function llmShouldMerge(contentA, contentB) {
1376
1474
  if (!raw) return { shouldMerge: false };
1377
1475
  try {
1378
1476
  const data = parseJsonLoose(raw);
1379
- if (typeof data.shouldMerge !== "boolean") return { shouldMerge: false };
1477
+ if (typeof data.shouldMerge !== "boolean") {
1478
+ warn("[amem] llmShouldMerge got no boolean verdict; treating the pair as distinct");
1479
+ return { shouldMerge: false };
1480
+ }
1380
1481
  if (data.shouldMerge && typeof data.merged === "string") {
1381
1482
  return { shouldMerge: true, merged: data.merged };
1382
1483
  }
1383
1484
  return { shouldMerge: false };
1384
1485
  } catch (e) {
1385
- console.error(`[amem] llmShouldMerge parse failed: ${e.message}`);
1486
+ warn(`[amem] llmShouldMerge parse failed: ${e.message}`);
1386
1487
  return { shouldMerge: false };
1387
1488
  }
1388
1489
  }
1389
1490
  async function llmEvolutionJudge(oldContent, newContent) {
1390
1491
  const prompt = t.evolutionJudge(oldContent, newContent);
1391
1492
  const raw = await llmCall(prompt, 300, "strong");
1392
- if (!raw) return { type: "NEW" };
1493
+ if (!raw) {
1494
+ warn("[amem] evolution judge got nothing back; defaulting the pair to NEW");
1495
+ return { type: "NEW" };
1496
+ }
1393
1497
  try {
1394
1498
  const data = parseJsonLoose(raw);
1395
1499
  const type = VALID_EVOLUTION_TYPES.has(data.type) ? data.type : "NEW";
@@ -1398,11 +1502,11 @@ async function llmEvolutionJudge(oldContent, newContent) {
1398
1502
  mergedContent: typeof data.mergedContent === "string" ? data.mergedContent : void 0
1399
1503
  };
1400
1504
  } catch (e) {
1401
- console.error(`[amem] llmEvolutionJudge parse failed: ${e.message}`);
1505
+ warn(`[amem] llmEvolutionJudge parse failed: ${e.message}`);
1402
1506
  return { type: "NEW" };
1403
1507
  }
1404
1508
  }
1405
- async function llmEvolveNote(content, linkedNotes) {
1509
+ async function llmEvolveNote(content, linkedNotes, opts = {}) {
1406
1510
  const linkedStr = linkedNotes.map((n) => `- ID: ${n.id}
1407
1511
  Content: ${n.content}`).join("\n");
1408
1512
  const prompt = `A memory note has gained new connections. Update its context, tags, and decide whether to strengthen connections with specific neighbors.
@@ -1425,7 +1529,7 @@ Original note content: ${content}
1425
1529
 
1426
1530
  Newly linked notes (neighbors):
1427
1531
  ${linkedStr}`;
1428
- const raw = await llmCall(prompt, 500);
1532
+ const raw = await llmCall(prompt, 500, "fast", opts);
1429
1533
  if (!raw) return { tags: null, context: null, shouldStrengthen: false, suggestedConnections: [], tagsToUpdate: [] };
1430
1534
  try {
1431
1535
  const data = parseJsonLoose(raw);
@@ -1437,21 +1541,27 @@ ${linkedStr}`;
1437
1541
  tagsToUpdate: Array.isArray(data.tags_to_update) ? data.tags_to_update.map(String) : []
1438
1542
  };
1439
1543
  } catch (e) {
1440
- console.error(`[amem] Evolution parse failed: ${e.message}`);
1544
+ warn(`[amem] Evolution parse failed: ${e.message}`);
1441
1545
  return { tags: null, context: null, shouldStrengthen: false, suggestedConnections: [], tagsToUpdate: [] };
1442
1546
  }
1443
1547
  }
1444
1548
  async function llmConflictScan(contents) {
1445
1549
  if (contents.length < 2) return [];
1446
1550
  const numbered = contents.map((c, i) => `[${i}] ${c}`).join("\n");
1551
+ const raw = await llmCall(t.conflictScan(numbered), 600, "strong");
1552
+ if (!raw) return null;
1447
1553
  try {
1448
- const raw = await llmCall(t.conflictScan(numbered), 600, "strong");
1449
- if (!raw) return [];
1450
1554
  const cleaned = stripReasoning(raw);
1451
1555
  const match = cleaned.match(/\[[\s\S]*\]/);
1452
- if (!match) return [];
1556
+ if (!match) {
1557
+ warn("[amem] llmConflictScan found no array in the response; the batch is read again next run");
1558
+ return null;
1559
+ }
1453
1560
  const parsed = JSON.parse(match[0]);
1454
- if (!Array.isArray(parsed)) return [];
1561
+ if (!Array.isArray(parsed)) {
1562
+ warn("[amem] llmConflictScan parsed a non-array; the batch is read again next run");
1563
+ return null;
1564
+ }
1455
1565
  const pairs = [];
1456
1566
  const seen = /* @__PURE__ */ new Set();
1457
1567
  for (const item of parsed) {
@@ -1473,21 +1583,30 @@ async function llmConflictScan(contents) {
1473
1583
  supersededIndex
1474
1584
  });
1475
1585
  }
1586
+ if (parsed.length > 0 && pairs.length === 0) {
1587
+ warn("[amem] llmConflictScan got no valid pair in the response; the batch is read again next run");
1588
+ return null;
1589
+ }
1476
1590
  return pairs;
1477
1591
  } catch (e) {
1478
- console.error(`[amem] llmConflictScan failed: ${e.message}`);
1479
- return [];
1592
+ warn(`[amem] llmConflictScan failed: ${e.message}`);
1593
+ return null;
1480
1594
  }
1481
1595
  }
1482
- var import_sdk, import_openai, _override, _warned, DEFAULT_TIMEOUT_MS, _anthropicClients, _openaiClients, VALID_CONFIDENCE, VALID_CATEGORIES, VALID_EVOLUTION_TYPES;
1596
+ var import_sdk, import_openai, _override, _warned, _thinkingStaysOn, roomToThink, DEFAULT_TIMEOUT_MS, _anthropicClients, _openaiClients, VALID_CONFIDENCE, VALID_CATEGORIES, VALID_EVOLUTION_TYPES;
1483
1597
  var init_llm = __esm({
1484
1598
  "../amem-core/src/llm.ts"() {
1485
1599
  "use strict";
1486
1600
  import_sdk = __toESM(require("@anthropic-ai/sdk"), 1);
1487
1601
  import_openai = __toESM(require("openai"), 1);
1488
1602
  init_prompts();
1603
+ init_config();
1604
+ init_deadline();
1605
+ init_background();
1489
1606
  _override = {};
1490
1607
  _warned = /* @__PURE__ */ new Set();
1608
+ _thinkingStaysOn = /* @__PURE__ */ new Set();
1609
+ roomToThink = (maxTokens) => Math.max(maxTokens * 8, 4e3);
1491
1610
  DEFAULT_TIMEOUT_MS = 3e4;
1492
1611
  _anthropicClients = /* @__PURE__ */ new Map();
1493
1612
  _openaiClients = /* @__PURE__ */ new Map();
@@ -1624,7 +1743,8 @@ async function addMemory(content, agentId = "main", opts) {
1624
1743
  return existingByHash.id;
1625
1744
  }
1626
1745
  console.log("[add] Constructing note...");
1627
- const { keywords, tags, context, category, note_type, topics } = await llmConstructNote(content);
1746
+ const deadline = opts?.deadline;
1747
+ const { keywords, tags, context, category, note_type, topics } = await llmConstructNote(content, { deadline });
1628
1748
  console.log(` keywords: ${keywords.join(", ")}`);
1629
1749
  console.log(` tags: ${tags.join(", ")}`);
1630
1750
  console.log(` context: ${context}`);
@@ -1697,8 +1817,9 @@ async function addMemory(content, agentId = "main", opts) {
1697
1817
  for (const { note: cand, score } of candidates) {
1698
1818
  if (cand.id === note.id) continue;
1699
1819
  if (score < 0.3) continue;
1820
+ if (!hasTimeFor(deadline)) break;
1700
1821
  console.log(` candidate ${cand.id.slice(0, 8)}... sim=${score.toFixed(3)}, asking LLM...`);
1701
- const shouldLink = await llmShouldLink(content, cand.content);
1822
+ const shouldLink = await llmShouldLink(content, cand.content, { deadline });
1702
1823
  if (shouldLink) {
1703
1824
  linkedIds.push(cand.id);
1704
1825
  linkedContents.push(cand.content);
@@ -1722,6 +1843,7 @@ async function addMemory(content, agentId = "main", opts) {
1722
1843
  if (shouldRunEvolution()) {
1723
1844
  console.log(` [evo] threshold reached, running evolution for ${Math.min(linkedIds.length, 3)} linked notes`);
1724
1845
  for (const lid of linkedIds.slice(0, 3)) {
1846
+ if (!hasTimeFor(deadline)) break;
1725
1847
  const linked = await ctx.getNote(lid, agentId);
1726
1848
  if (!linked) continue;
1727
1849
  if (!canWrite(linked, agentId)) {
@@ -1743,7 +1865,7 @@ async function addMemory(content, agentId = "main", opts) {
1743
1865
  shouldStrengthen,
1744
1866
  suggestedConnections,
1745
1867
  tagsToUpdate
1746
- } = await llmEvolveNote(linked.content, linkedNotes);
1868
+ } = await llmEvolveNote(linked.content, linkedNotes, { deadline });
1747
1869
  let evolved = false;
1748
1870
  if (newTags !== null || newContext !== null) {
1749
1871
  if (newTags !== null) linked.tags = newTags;
@@ -1817,7 +1939,7 @@ async function addMemory(content, agentId = "main", opts) {
1817
1939
  }
1818
1940
  }
1819
1941
  } catch (e) {
1820
- console.error(`[warn] Link/Evolution phase failed: ${e.message}`);
1942
+ warn(`[amem] link/evolution phase failed: ${e.message}`);
1821
1943
  }
1822
1944
  console.log(`[done] Note added: ${note.id}`);
1823
1945
  return note.id;
@@ -1964,10 +2086,9 @@ async function listMemories(agentId = "main", storageCtx) {
1964
2086
  function sleep(ms) {
1965
2087
  return new Promise((resolve2) => setTimeout(resolve2, ms));
1966
2088
  }
1967
- async function mergeSimilarNotes(agentId, storageCtx) {
2089
+ async function mergeSimilarNotes(agentId, storageCtx, date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)) {
1968
2090
  const ctx = storageCtx ?? defaultCtx();
1969
- const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1970
- const allNotes = await ctx.getNotesByDatePrefix(today, agentId);
2091
+ const allNotes = await ctx.getNotesByDatePrefix(date, agentId);
1971
2092
  const notes = allNotes.filter((n) => n.agent_id !== "shared");
1972
2093
  const pendingNotes = notes.filter((n) => n.pending_merge === true);
1973
2094
  let evolvedCount = 0;
@@ -1985,6 +2106,7 @@ async function mergeSimilarNotes(agentId, storageCtx) {
1985
2106
  }
1986
2107
  }
1987
2108
  if (!bestNeighbor) {
2109
+ stopIfForegroundBusy();
1988
2110
  await ctx.patchNotePayload(pendingNote.id, { pending_merge: false });
1989
2111
  continue;
1990
2112
  }
@@ -2083,8 +2205,8 @@ async function consolidateMemories(agentId, logger, storageCtx) {
2083
2205
  const ctx = storageCtx ?? defaultCtx();
2084
2206
  const log = {
2085
2207
  info: (msg) => logger ? logger.info(msg) : console.log(msg),
2086
- warn: (msg) => logger ? logger.warn(msg) : console.warn(msg),
2087
- error: (msg) => logger ? logger.error(msg) : console.error(msg)
2208
+ warn: (msg) => logger ? logger.warn(msg) : warn(msg),
2209
+ error: (msg) => logger ? logger.error(msg) : warn(msg)
2088
2210
  };
2089
2211
  log.info(`[Consolidation] Starting consolidation for agentId: ${agentId}`);
2090
2212
  const rawNotes = await ctx.listNotes(agentId);
@@ -2105,6 +2227,7 @@ async function consolidateMemories(agentId, logger, storageCtx) {
2105
2227
  for (const [category, groupNotes] of groups.entries()) {
2106
2228
  log.info(`[Consolidation] Category "${category}" has ${groupNotes.length} notes.`);
2107
2229
  for (let i = 0; i < groupNotes.length; i++) {
2230
+ await pauseForForeground();
2108
2231
  for (let j = i + 1; j < groupNotes.length; j++) {
2109
2232
  const noteA = groupNotes[i];
2110
2233
  const noteB = groupNotes[j];
@@ -2214,6 +2337,7 @@ async function conflictSweep(agentId, opts) {
2214
2337
  let retired = 0;
2215
2338
  let batchesScanned = 0;
2216
2339
  let batchesSkipped = 0;
2340
+ let batchesFailed = 0;
2217
2341
  for (const [category, groupNotes] of groups.entries()) {
2218
2342
  groupNotes.sort((a, b) => Date.parse(b.timestamp) - Date.parse(a.timestamp));
2219
2343
  for (let start = 0; start < groupNotes.length; start += CONFLICT_BATCH_SIZE) {
@@ -2225,6 +2349,10 @@ async function conflictSweep(agentId, opts) {
2225
2349
  }
2226
2350
  batchesScanned++;
2227
2351
  const pairs = await llmConflictScan(batch.map((n) => n.content));
2352
+ if (pairs === null) {
2353
+ batchesFailed++;
2354
+ continue;
2355
+ }
2228
2356
  for (const { a, b, reason, supersededIndex } of pairs) {
2229
2357
  const noteA = batch[a];
2230
2358
  const noteB = batch[b];
@@ -2263,9 +2391,9 @@ async function conflictSweep(agentId, opts) {
2263
2391
  }
2264
2392
  }
2265
2393
  log(
2266
- `[conflict] ${batchesScanned} batch(es) scanned, ${batchesSkipped} already up to date; ${pairsFound} pair(s) found, ${retired} retired`
2394
+ `[conflict] ${batchesScanned} batch(es) scanned, ${batchesSkipped} already up to date` + (batchesFailed > 0 ? `, ${batchesFailed} with no answer (read again next run)` : "") + `; ${pairsFound} pair(s) found, ${retired} retired`
2267
2395
  );
2268
- return { scanned: notes.length, pairsFound, retired, batchesScanned, batchesSkipped };
2396
+ return { scanned: notes.length, pairsFound, retired, batchesScanned, batchesSkipped, batchesFailed };
2269
2397
  }
2270
2398
  var import_uuid, import_crypto, fs2, path3, import_jieba, logSafe, _jieba, EPHEMERAL_SIGNALS, CONFLICT_BATCH_SIZE;
2271
2399
  var init_memory = __esm({
@@ -2280,6 +2408,8 @@ var init_memory = __esm({
2280
2408
  init_auth();
2281
2409
  init_llm();
2282
2410
  init_evo_counter();
2411
+ init_deadline();
2412
+ init_background();
2283
2413
  init_config();
2284
2414
  import_jieba = require("@node-rs/jieba");
2285
2415
  logSafe = (id) => id.replace(/[\r\n]/g, "");
@@ -2488,7 +2618,7 @@ async function migrateCollection(opts) {
2488
2618
  const refreshFields = opts.refreshFields !== false;
2489
2619
  const dryRun = opts.dryRun !== false;
2490
2620
  const log = opts.logger?.info ?? ((m) => console.log(m));
2491
- const warn = opts.logger?.warn ?? ((m) => console.warn(m));
2621
+ const warn2 = opts.logger?.warn ?? warn;
2492
2622
  if (from === to) throw new Error(`migrate: source and target are the same collection ("${from}")`);
2493
2623
  const model = getEmbeddingModel();
2494
2624
  const targetDim = await getEmbeddingDim();
@@ -2556,7 +2686,7 @@ async function migrateCollection(opts) {
2556
2686
  if (!note.context) note.context = built.context;
2557
2687
  refreshed++;
2558
2688
  } catch (e) {
2559
- warn(`[migrate] re-extract failed for ${note.id.slice(0, 8)} \u2014 keeping as-is: ${e.message}`);
2689
+ warn2(`[migrate] re-extract failed for ${note.id.slice(0, 8)} \u2014 keeping as-is: ${e.message}`);
2560
2690
  }
2561
2691
  }
2562
2692
  const point = noteToPoint({ ...note, embedding: await encode(buildEmbedText(note)) });
@@ -2569,7 +2699,7 @@ async function migrateCollection(opts) {
2569
2699
  await flush();
2570
2700
  const finalCount = await countPointsRaw(to);
2571
2701
  if (finalCount !== notes.length) {
2572
- warn(`[migrate] target holds ${finalCount} point(s) but the source had ${notes.length} \u2014 check before switching`);
2702
+ warn2(`[migrate] target holds ${finalCount} point(s) but the source had ${notes.length} \u2014 check before switching`);
2573
2703
  }
2574
2704
  log(`[migrate] done: ${migrated} written, ${alreadyDone.size} already present, ${refreshed} re-extracted.`);
2575
2705
  return {
@@ -2624,6 +2754,7 @@ var init_migrate = __esm({
2624
2754
  init_embedding();
2625
2755
  init_llm();
2626
2756
  init_memory();
2757
+ init_config();
2627
2758
  }
2628
2759
  });
2629
2760
 
@@ -2651,6 +2782,7 @@ var init_crud_guard = __esm({
2651
2782
  // ../amem-core/src/index.ts
2652
2783
  var src_exports = {};
2653
2784
  __export(src_exports, {
2785
+ BackgroundPreempted: () => BackgroundPreempted,
2654
2786
  DEFAULT_CRUD_UPDATE_MIN_SIM: () => DEFAULT_CRUD_UPDATE_MIN_SIM,
2655
2787
  DEFAULT_EMBEDDING_MODEL: () => DEFAULT_EMBEDDING_MODEL,
2656
2788
  EmbeddingDimensionMismatchError: () => EmbeddingDimensionMismatchError,
@@ -2679,6 +2811,7 @@ __export(src_exports, {
2679
2811
  getEmbeddingModel: () => getEmbeddingModel,
2680
2812
  getEmbeddingPooling: () => getEmbeddingPooling,
2681
2813
  getNote: () => getNote,
2814
+ hasTimeFor: () => hasTimeFor,
2682
2815
  invalidateNote: () => invalidateNote,
2683
2816
  isModelLoaded: () => isModelLoaded,
2684
2817
  isPlausibleUpdateTarget: () => isPlausibleUpdateTarget,
@@ -2691,6 +2824,7 @@ __export(src_exports, {
2691
2824
  patchNotePayload: () => patchNotePayload,
2692
2825
  pingQdrant: () => pingQdrant,
2693
2826
  resolveCrudUpdateMinSim: () => resolveCrudUpdateMinSim,
2827
+ runInBackground: () => runInBackground,
2694
2828
  scanLowQuality: () => scanLowQuality,
2695
2829
  searchMemory: () => searchMemory,
2696
2830
  switchToMigrated: () => switchToMigrated,
@@ -2711,6 +2845,8 @@ var init_src = __esm({
2711
2845
  init_memory();
2712
2846
  init_crud_guard();
2713
2847
  init_llm();
2848
+ init_deadline();
2849
+ init_background();
2714
2850
  }
2715
2851
  });
2716
2852
 
@@ -2737,7 +2873,7 @@ __export(index_exports, {
2737
2873
  });
2738
2874
  module.exports = __toCommonJS(index_exports);
2739
2875
  var os2 = __toESM(require("os"));
2740
- var path5 = __toESM(require("path"));
2876
+ var path6 = __toESM(require("path"));
2741
2877
  var import_plugin_entry = require("openclaw/plugin-sdk/plugin-entry");
2742
2878
  init_src();
2743
2879
  var import_crypto2 = require("crypto");
@@ -2750,6 +2886,237 @@ function isConvAccessBlocked(config, pluginId) {
2750
2886
  return entry !== void 0 && entry.hooks?.allowConversationAccess !== true;
2751
2887
  }
2752
2888
 
2889
+ // src/nightly.ts
2890
+ var fs4 = __toESM(require("fs"));
2891
+ var path5 = __toESM(require("path"));
2892
+ var SLOT = /* @__PURE__ */ Symbol.for("openclaw-amem.nightly-timer");
2893
+ function msUntil(hour, minute, now = /* @__PURE__ */ new Date()) {
2894
+ const target = new Date(now);
2895
+ target.setHours(hour, minute, 0, 0);
2896
+ if (target.getTime() <= now.getTime()) target.setDate(target.getDate() + 1);
2897
+ return target.getTime() - now.getTime();
2898
+ }
2899
+ function scheduleNightly(job, onError) {
2900
+ const holder = globalThis;
2901
+ if (holder[SLOT]) clearTimeout(holder[SLOT]);
2902
+ const timer = setTimeout(
2903
+ async () => {
2904
+ try {
2905
+ await job();
2906
+ } catch (err) {
2907
+ onError(err);
2908
+ } finally {
2909
+ if (holder[SLOT] === timer) scheduleNightly(job, onError);
2910
+ }
2911
+ },
2912
+ msUntil(2, 30)
2913
+ );
2914
+ timer.unref();
2915
+ holder[SLOT] = timer;
2916
+ }
2917
+ function cancelNightly() {
2918
+ const holder = globalThis;
2919
+ if (holder[SLOT]) clearTimeout(holder[SLOT]);
2920
+ delete holder[SLOT];
2921
+ }
2922
+ function readFile(file) {
2923
+ try {
2924
+ const data = JSON.parse(fs4.readFileSync(file, "utf-8"));
2925
+ const agents = {};
2926
+ for (const [id, entry] of Object.entries(data.agents ?? {})) {
2927
+ if (typeof entry?.from === "string" && typeof entry.wrote === "string") {
2928
+ agents[id] = { from: entry.from, wrote: entry.wrote };
2929
+ }
2930
+ }
2931
+ return { agents };
2932
+ } catch {
2933
+ return { agents: {} };
2934
+ }
2935
+ }
2936
+ function writeFile(file, owed) {
2937
+ fs4.mkdirSync(path5.dirname(file), { recursive: true });
2938
+ fs4.writeFileSync(file, JSON.stringify(owed));
2939
+ }
2940
+ function markOwed(file, rawAgentId, since, now = /* @__PURE__ */ new Date()) {
2941
+ const owed = readFile(file);
2942
+ owed.agents[rawAgentId] = {
2943
+ from: owed.agents[rawAgentId]?.from ?? since.toISOString(),
2944
+ wrote: now.toISOString()
2945
+ };
2946
+ writeFile(file, owed);
2947
+ }
2948
+ function readOwed(file) {
2949
+ return new Map(Object.entries(readFile(file).agents).map(([id, entry]) => [id, new Date(entry.from)]));
2950
+ }
2951
+ function settleAgent(file, rawAgentId, startedAt) {
2952
+ const owed = readFile(file);
2953
+ const entry = owed.agents[rawAgentId];
2954
+ if (!entry) return;
2955
+ if (Date.parse(entry.wrote) < startedAt.getTime()) delete owed.agents[rawAgentId];
2956
+ else entry.from = startedAt.toISOString();
2957
+ writeFile(file, owed);
2958
+ }
2959
+ function utcDatesSince(since, now, maxDays = 7) {
2960
+ const DAY = 864e5;
2961
+ const utcMidnight = (t2) => t2 - t2 % DAY;
2962
+ const last = utcMidnight(now.getTime());
2963
+ const earliest = last - (maxDays - 1) * DAY;
2964
+ const first = Math.max(utcMidnight(since ? since.getTime() : now.getTime() - DAY), earliest);
2965
+ const days = [];
2966
+ for (let t2 = first; t2 <= last; t2 += DAY) days.push(new Date(t2).toISOString().slice(0, 10));
2967
+ return days;
2968
+ }
2969
+ var FOREGROUND = /* @__PURE__ */ Symbol.for("openclaw-amem.foreground");
2970
+ var STALE_MS = 30 * 6e4;
2971
+ var QUIET_MS = 6e4;
2972
+ var NIGHTLY_WAIT_CAP_MS = 60 * 6e4;
2973
+ var MAX_ATTEMPTS = 3;
2974
+ function foreground() {
2975
+ const holder = globalThis;
2976
+ return holder[FOREGROUND] ??= { inFlight: /* @__PURE__ */ new Map(), lastActivity: 0, events: 0 };
2977
+ }
2978
+ function touch(fg, now) {
2979
+ fg.lastActivity = now;
2980
+ fg.events++;
2981
+ }
2982
+ var compactionKey = (session) => `compaction:${session}`;
2983
+ function noteAgentEvent(evt, now = Date.now()) {
2984
+ const fg = foreground();
2985
+ touch(fg, now);
2986
+ if (!evt.runId) return;
2987
+ const phase = evt.stream === "lifecycle" ? evt.data?.phase : void 0;
2988
+ if (phase === "start") {
2989
+ fg.inFlight.set(evt.runId, now);
2990
+ } else if (phase === "end" || phase === "error") {
2991
+ fg.inFlight.delete(evt.runId);
2992
+ for (const session of [evt.sessionKey, evt.sessionId]) if (session) fg.inFlight.delete(compactionKey(session));
2993
+ } else if (fg.inFlight.has(evt.runId)) {
2994
+ fg.inFlight.set(evt.runId, now);
2995
+ }
2996
+ }
2997
+ function watchAgentEvents(subscribe) {
2998
+ const fg = foreground();
2999
+ fg.unsubscribe?.();
3000
+ fg.unsubscribe = subscribe((evt) => noteAgentEvent(evt));
3001
+ }
3002
+ function unwatchAgentEvents() {
3003
+ const fg = foreground();
3004
+ fg.unsubscribe?.();
3005
+ delete fg.unsubscribe;
3006
+ }
3007
+ function foregroundWorkStarted(now = Date.now()) {
3008
+ const fg = foreground();
3009
+ const token = /* @__PURE__ */ Symbol("agent_end");
3010
+ fg.inFlight.set(token, now);
3011
+ touch(fg, now);
3012
+ return () => {
3013
+ fg.inFlight.delete(token);
3014
+ touch(fg, Date.now());
3015
+ };
3016
+ }
3017
+ function compactionStarted(ctx, now = Date.now()) {
3018
+ const fg = foreground();
3019
+ touch(fg, now);
3020
+ const session = ctx?.sessionKey ?? ctx?.sessionId;
3021
+ if (session) fg.inFlight.set(compactionKey(session), now);
3022
+ }
3023
+ function compactionEnded(ctx, now = Date.now()) {
3024
+ const fg = foreground();
3025
+ touch(fg, now);
3026
+ const session = ctx?.sessionKey ?? ctx?.sessionId;
3027
+ if (session) fg.inFlight.delete(compactionKey(session));
3028
+ }
3029
+ function noteMessageReceived(event, now = Date.now()) {
3030
+ const update = event?.providerUpdate;
3031
+ if (update?.editedTimestamp !== void 0 || update?.kind?.startsWith("edited_")) return;
3032
+ touch(foreground(), now);
3033
+ }
3034
+ function foregroundBusy(now = Date.now()) {
3035
+ const { inFlight } = foreground();
3036
+ for (const [key, seen] of inFlight) if (now - seen > STALE_MS) inFlight.delete(key);
3037
+ return inFlight.size > 0;
3038
+ }
3039
+ function busySinceNow() {
3040
+ const fg = foreground();
3041
+ const seen = fg.events;
3042
+ return () => foregroundBusy() || fg.events !== seen;
3043
+ }
3044
+ function makeIdleWaiter(pollMs = 15e3, capMs = NIGHTLY_WAIT_CAP_MS) {
3045
+ let waited = 0;
3046
+ return {
3047
+ async untilIdle() {
3048
+ for (; ; ) {
3049
+ const now = Date.now();
3050
+ if (!foregroundBusy(now) && now - foreground().lastActivity >= QUIET_MS) return true;
3051
+ if (waited >= capMs) return false;
3052
+ await new Promise((resolve2) => setTimeout(resolve2, pollMs).unref());
3053
+ waited += Date.now() - now;
3054
+ }
3055
+ },
3056
+ waitedMs: () => waited
3057
+ };
3058
+ }
3059
+ async function runStep(idle, attempt, preempted, onPreempted) {
3060
+ for (let tries = 1; ; tries++) {
3061
+ if (!await idle.untilIdle()) return "out of waiting";
3062
+ try {
3063
+ await attempt();
3064
+ return "done";
3065
+ } catch (err) {
3066
+ if (!preempted(err)) throw err;
3067
+ if (tries >= MAX_ATTEMPTS) return "stopped too often";
3068
+ onPreempted();
3069
+ }
3070
+ }
3071
+ }
3072
+ async function runNightly(work, startedAt = /* @__PURE__ */ new Date()) {
3073
+ const { logger } = work;
3074
+ const idle = work.idle ?? makeIdleWaiter();
3075
+ let stopped = false;
3076
+ const step = async (name, run) => {
3077
+ if (stopped) return false;
3078
+ const result = await runStep(
3079
+ idle,
3080
+ () => work.inBackground(run),
3081
+ work.preempted,
3082
+ () => logger.info(`openclaw-amem: nightly ${name} stopped for a running task; it runs again when the gateway is idle`)
3083
+ );
3084
+ if (result === "done") return true;
3085
+ stopped = true;
3086
+ logger.warn(
3087
+ result === "out of waiting" ? `openclaw-amem: nightly job waited ${Math.round(idle.waitedMs() / 6e4)} min for running tasks; the rest runs tomorrow` : `openclaw-amem: nightly ${name} was stopped ${MAX_ATTEMPTS} times by running tasks; the rest runs tomorrow`
3088
+ );
3089
+ return false;
3090
+ };
3091
+ try {
3092
+ const owed = readOwed(work.owedFile);
3093
+ merging: for (const agent of /* @__PURE__ */ new Set([work.defaultAgent, ...owed.keys()])) {
3094
+ let failed = false;
3095
+ for (const day of utcDatesSince(owed.get(agent), startedAt)) {
3096
+ try {
3097
+ if (!await step(`merge (${agent}, ${day})`, () => work.mergeDay(agent, day))) break merging;
3098
+ } catch (err) {
3099
+ failed = true;
3100
+ logger.warn(`openclaw-amem: merge failed (${agent}, ${day}) \u2014 ${err.message}`);
3101
+ }
3102
+ }
3103
+ if (!failed) settleAgent(work.owedFile, agent, startedAt);
3104
+ }
3105
+ } catch (err) {
3106
+ logger.warn(`openclaw-amem: nightly merge failed \u2014 ${err.message}`);
3107
+ }
3108
+ for (const { name, run } of work.after) {
3109
+ try {
3110
+ await step(name, run);
3111
+ } catch (err) {
3112
+ logger.warn(`openclaw-amem: nightly ${name} failed \u2014 ${err.message}`);
3113
+ }
3114
+ }
3115
+ if (idle.waitedMs() > 0) {
3116
+ logger.info(`openclaw-amem: nightly job waited ${Math.round(idle.waitedMs() / 1e3)} s for running tasks`);
3117
+ }
3118
+ }
3119
+
2753
3120
  // src/scope.ts
2754
3121
  function parseAgentIdFromSessionKey(sessionKey) {
2755
3122
  if (!sessionKey) return void 0;
@@ -2775,6 +3142,8 @@ function buildScope(rawAgentId, pluginConfig, createStorageContext2) {
2775
3142
  // src/index.ts
2776
3143
  init_src();
2777
3144
  var _config = {};
3145
+ var HOOK_BUDGET_MS = 3e4;
3146
+ var HOOK_MARGIN_MS = 2e3;
2778
3147
  function register(api) {
2779
3148
  const logger = api.logger;
2780
3149
  _config = api.pluginConfig || {};
@@ -2782,14 +3151,18 @@ function register(api) {
2782
3151
  const pluginId = api.id ?? "openclaw-amem";
2783
3152
  const convBlocked = isConvAccessBlocked(api.config, pluginId);
2784
3153
  if (convBlocked) logger.warn(BLOCKED_WARNING_LOG);
2785
- configure({ dataDir: path5.join(os2.homedir(), ".openclaw") });
3154
+ configure({
3155
+ dataDir: path6.join(os2.homedir(), ".openclaw"),
3156
+ warn: (msg) => logger.warn(msg)
3157
+ });
2786
3158
  const hasStrong = !!(pluginConfig.llmStrongProvider || pluginConfig.llmStrongModel || pluginConfig.llmStrongBaseURL);
2787
- if (pluginConfig.llmProvider || pluginConfig.llmModel || pluginConfig.llmBaseURL || pluginConfig.llmCrudRole || hasStrong) {
3159
+ if (pluginConfig.llmProvider || pluginConfig.llmModel || pluginConfig.llmBaseURL || pluginConfig.llmCrudRole || pluginConfig.llmThinking || hasStrong) {
2788
3160
  configureLlm({
2789
3161
  provider: pluginConfig.llmProvider,
2790
3162
  model: pluginConfig.llmModel,
2791
3163
  baseURL: pluginConfig.llmBaseURL,
2792
3164
  crudRole: pluginConfig.llmCrudRole,
3165
+ thinking: pluginConfig.llmThinking,
2793
3166
  // Omit the whole block when unset so `strong` transparently falls back to
2794
3167
  // `fast` — the zero-config path stays byte-for-byte today's behaviour.
2795
3168
  ...hasStrong && {
@@ -2806,7 +3179,15 @@ function register(api) {
2806
3179
  const resolveAgentId2 = (ctx) => resolveAgentId(ctx, pluginConfig);
2807
3180
  const buildScope2 = (rawAgentId) => buildScope(rawAgentId, pluginConfig, createStorageContext);
2808
3181
  const defaultScope = buildScope2(resolveAgentId2());
2809
- const dbPath = path5.join(os2.homedir(), ".openclaw", "amem_db");
3182
+ const dbPath = path6.join(os2.homedir(), ".openclaw", "amem_db");
3183
+ const owedFile = path6.join(os2.homedir(), ".openclaw", "amem_nightly_owed.json");
3184
+ const owe = (rawAgentId, since) => {
3185
+ try {
3186
+ markOwed(owedFile, rawAgentId, since);
3187
+ } catch (err) {
3188
+ logger.warn(`openclaw-amem: could not record ${rawAgentId} for the nightly merge \u2014 ${err.message}`);
3189
+ }
3190
+ };
2810
3191
  logger.info(
2811
3192
  `openclaw-amem: registered (native TS, Qdrant, default agent_id=${defaultScope.agentId}, default collection=${pluginConfig.collection ?? "amem_notes (default)"}, per-agent scope resolved per call)`
2812
3193
  );
@@ -2860,7 +3241,9 @@ function register(api) {
2860
3241
  },
2861
3242
  async add(text) {
2862
3243
  try {
3244
+ const since = /* @__PURE__ */ new Date();
2863
3245
  await addMemory(text, scope.agentId, { storageCtx: scope.storageCtx });
3246
+ owe(resolveAgentId2(params), since);
2864
3247
  return { ok: true };
2865
3248
  } catch (err) {
2866
3249
  logger.warn(`openclaw-amem: add failed \u2014 ${err.message}`);
@@ -2981,6 +3364,7 @@ ${text}${hookWarning}` }],
2981
3364
  const start = Date.now();
2982
3365
  try {
2983
3366
  const id = await addMemory(text, scope.agentId, { subjects, storageCtx: scope.storageCtx });
3367
+ owe(resolveAgentId2(ctx), new Date(start));
2984
3368
  logger.info(`openclaw-amem: memory_add OK id=${id} (${Date.now() - start}ms)`);
2985
3369
  return {
2986
3370
  content: [{ type: "text", text: "Memory saved successfully." }],
@@ -3107,15 +3491,30 @@ ${text}${hookWarning}` }],
3107
3491
  }
3108
3492
  if (typeof api.registerHook === "function" || typeof api.on === "function") {
3109
3493
  const hookFn = (typeof api.on === "function" ? api.on : api.registerHook).bind(api);
3494
+ hookFn(
3495
+ "before_compaction",
3496
+ (_event, ctx) => compactionStarted(ctx)
3497
+ );
3498
+ hookFn(
3499
+ "after_compaction",
3500
+ (_event, ctx) => compactionEnded(ctx)
3501
+ );
3502
+ hookFn(
3503
+ "message_received",
3504
+ (event) => noteMessageReceived(event)
3505
+ );
3110
3506
  hookFn(
3111
3507
  "agent_end",
3112
3508
  async (event, ctx) => {
3509
+ const deadline = Date.now() + HOOK_BUDGET_MS - HOOK_MARGIN_MS;
3113
3510
  const scope = buildScope2(resolveAgentId2(ctx));
3114
3511
  const agentId = scope.agentId;
3115
3512
  const storageCtx = scope.storageCtx;
3116
3513
  logger.info(
3117
3514
  `openclaw-amem: agent_end hook triggered (agent_id=${agentId}, success=${event.success}, messages=${event.messages?.length ?? 0})`
3118
3515
  );
3516
+ const done = foregroundWorkStarted();
3517
+ let since;
3119
3518
  try {
3120
3519
  let shouldProcessTurn2 = function(text) {
3121
3520
  if (text.trim().length < 10) return false;
@@ -3132,7 +3531,12 @@ ${text}${hookWarning}` }],
3132
3531
  const msgs = event.messages || [];
3133
3532
  const lastUser = [...msgs].reverse().find((m) => m.role === "user");
3134
3533
  const lastAssistant = [...msgs].reverse().find((m) => m.role === "assistant");
3135
- if (!lastUser || !lastAssistant) return;
3534
+ if (!lastUser || !lastAssistant) {
3535
+ logger.info(
3536
+ `openclaw-amem: agent_end skipped \u2014 no ${!lastUser ? "user" : "assistant"} message in ${msgs.length} messages`
3537
+ );
3538
+ return;
3539
+ }
3136
3540
  const userText = typeof lastUser.content === "string" ? lastUser.content : lastUser.content.filter((b) => b.type === "text").map((b) => b.text).join(" ");
3137
3541
  const assistantText = typeof lastAssistant.content === "string" ? lastAssistant.content : lastAssistant.content.filter((b) => b.type === "text").map((b) => b.text).join(" ");
3138
3542
  if (!userText || !shouldProcessTurn2(userText)) return;
@@ -3146,12 +3550,18 @@ ${text}${hookWarning}` }],
3146
3550
  const operations = await llmCrudDecision2(
3147
3551
  userText,
3148
3552
  assistantText,
3149
- existingMemories.map((m) => ({ idx: m.idx, content: m.content }))
3553
+ existingMemories.map((m) => ({ idx: m.idx, content: m.content })),
3554
+ { deadline }
3150
3555
  );
3151
3556
  if (!operations || operations.length === 0) return;
3152
- for (const op of operations) {
3557
+ since = /* @__PURE__ */ new Date();
3558
+ for (const [i, op] of operations.entries()) {
3559
+ if (!hasTimeFor(deadline)) {
3560
+ logger.warn(`openclaw-amem: agent_end out of time, stored ${i} of ${operations.length} operations`);
3561
+ break;
3562
+ }
3153
3563
  if (op.action === "NEW") {
3154
- await addMemory(op.fact, agentId, { storageCtx });
3564
+ await addMemory(op.fact, agentId, { storageCtx, deadline });
3155
3565
  logger.info(`openclaw-amem: CRUD NEW: "${op.fact.slice(0, 60)}${op.fact.length > 60 ? "..." : ""}"`);
3156
3566
  } else if (op.action === "UPDATE" && op.existingIdx !== void 0) {
3157
3567
  const target = existingMemories[op.existingIdx];
@@ -3160,7 +3570,7 @@ ${text}${hookWarning}` }],
3160
3570
  const hash = (0, import_crypto2.createHash)("md5").update(op.fact).digest("hex");
3161
3571
  const targetNote = await storageCtx.getNote(target.id, agentId);
3162
3572
  if (!targetNote || !isPlausibleUpdateTarget(newEmbedding, targetNote.embedding, crudUpdateMinSim)) {
3163
- await addMemory(op.fact, agentId, { storageCtx });
3573
+ await addMemory(op.fact, agentId, { storageCtx, deadline });
3164
3574
  logger.warn(
3165
3575
  `openclaw-amem: CRUD UPDATE on ${target.id.slice(0, 8)} looks mis-targeted \u2014 stored as a new memory instead`
3166
3576
  );
@@ -3193,73 +3603,84 @@ ${text}${hookWarning}` }],
3193
3603
  }
3194
3604
  }
3195
3605
  }
3196
- const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3197
- logger.info(`openclaw-amem: starting mergeSimilarNotes for agent=${agentId}, date=${today}`);
3198
- try {
3199
- const merged = await mergeSimilarNotes(agentId, storageCtx);
3200
- if (merged > 0) {
3201
- logger.info(`openclaw-amem: merged ${merged} similar notes today (${today})`);
3202
- } else {
3203
- logger.info(`openclaw-amem: mergeSimilarNotes completed, 0 pairs merged (${today})`);
3204
- }
3205
- } catch (mergeErr) {
3206
- logger.error(
3207
- `openclaw-amem: mergeSimilarNotes FAILED \u2014 ${mergeErr.message}
3208
- ${mergeErr.stack}`
3209
- );
3210
- }
3211
3606
  } catch (e) {
3212
3607
  logger.warn(`openclaw-amem: agent_end CRUD hook failed \u2014 ${e.message}`);
3608
+ } finally {
3609
+ if (since) owe(resolveAgentId2(ctx), since);
3610
+ done();
3213
3611
  }
3214
3612
  },
3215
- { timeoutMs: 3e4 }
3613
+ { timeoutMs: HOOK_BUDGET_MS }
3216
3614
  );
3217
3615
  logger.info("openclaw-amem: agent_end CRUD decision hook registered");
3218
3616
  }
3219
- function scheduleNextRun() {
3220
- const now = /* @__PURE__ */ new Date();
3221
- const target = /* @__PURE__ */ new Date();
3222
- target.setHours(2, 30, 0, 0);
3223
- if (target.getTime() <= now.getTime()) {
3224
- target.setDate(target.getDate() + 1);
3225
- }
3226
- const delay = target.getTime() - now.getTime();
3227
- setTimeout(async () => {
3228
- try {
3229
- logger.info("openclaw-amem: Running scheduled daily consolidation...");
3230
- const merged = await consolidateMemories(defaultScope.agentId, logger, defaultScope.storageCtx);
3231
- if (merged > 0) {
3232
- logger.info(`openclaw-amem: Scheduled daily consolidation merged ${merged} pairs.`);
3233
- }
3234
- } catch (err) {
3235
- logger.warn(`openclaw-amem: Scheduled daily consolidation failed \u2014 ${err.message}`);
3236
- }
3237
- if (conflictSweepEnabled) {
3238
- try {
3239
- const res = await conflictSweep(defaultScope.agentId, {
3240
- storageCtx: defaultScope.storageCtx,
3241
- logger
3242
- });
3243
- if (res.pairsFound > 0) {
3244
- logger.info(
3245
- `openclaw-amem: Contradiction sweep flagged ${res.pairsFound} pair(s)` + (res.retired > 0 ? `, retired ${res.retired}` : "") + ` (${res.batchesScanned} batch(es) read, ${res.batchesSkipped} unchanged).`
3246
- );
3617
+ scheduleNightly(
3618
+ () => runNightly({
3619
+ owedFile,
3620
+ defaultAgent: resolveAgentId2(),
3621
+ mergeDay: async (rawAgentId, day) => {
3622
+ const scope = buildScope2(rawAgentId);
3623
+ const merged = await mergeSimilarNotes(scope.agentId, scope.storageCtx, day);
3624
+ if (merged > 0) logger.info(`openclaw-amem: merged ${merged} similar notes (${scope.agentId}, ${day})`);
3625
+ },
3626
+ after: [
3627
+ {
3628
+ name: "consolidation",
3629
+ run: async () => {
3630
+ logger.info("openclaw-amem: Running scheduled daily consolidation...");
3631
+ const merged = await consolidateMemories(defaultScope.agentId, logger, defaultScope.storageCtx);
3632
+ if (merged > 0) {
3633
+ logger.info(`openclaw-amem: Scheduled daily consolidation merged ${merged} pairs.`);
3634
+ }
3247
3635
  }
3248
- } catch (err) {
3249
- logger.warn(`openclaw-amem: Scheduled contradiction sweep failed \u2014 ${err.message}`);
3250
- }
3251
- }
3252
- scheduleNextRun();
3253
- }, delay);
3254
- }
3255
- scheduleNextRun();
3636
+ },
3637
+ // Story 43: the cold half of the tiering split. The per-turn CRUD decision
3638
+ // runs on the fast model, which is safe but misses contradictions; this is
3639
+ // what catches them. Runs AFTER consolidation, as its own step, so neither
3640
+ // task can take the other down.
3641
+ //
3642
+ // Only batches that gained a note since the last run are re-read, so a
3643
+ // steady-state night costs a call or two rather than a full re-read.
3644
+ ...conflictSweepEnabled ? [
3645
+ {
3646
+ name: "contradiction sweep",
3647
+ run: async () => {
3648
+ const res = await conflictSweep(defaultScope.agentId, {
3649
+ storageCtx: defaultScope.storageCtx,
3650
+ logger
3651
+ });
3652
+ if (res.pairsFound > 0) {
3653
+ logger.info(
3654
+ `openclaw-amem: Contradiction sweep flagged ${res.pairsFound} pair(s)` + (res.retired > 0 ? `, retired ${res.retired}` : "") + ` (${res.batchesScanned} batch(es) read, ${res.batchesSkipped} unchanged).`
3655
+ );
3656
+ }
3657
+ }
3658
+ }
3659
+ ] : []
3660
+ ],
3661
+ inBackground: (work) => runInBackground(busySinceNow(), work),
3662
+ preempted: (err) => err instanceof BackgroundPreempted,
3663
+ logger
3664
+ }),
3665
+ (err) => logger.warn(`openclaw-amem: nightly job failed \u2014 ${err.message}`)
3666
+ );
3256
3667
  if (typeof api.registerService === "function") {
3257
3668
  api.registerService({
3258
3669
  id: "amem-plugin",
3259
3670
  start() {
3671
+ const events = api.runtime?.events;
3672
+ if (typeof events?.onAgentEvent === "function") {
3673
+ watchAgentEvents((listener) => events.onAgentEvent(listener));
3674
+ } else {
3675
+ logger.warn(
3676
+ "openclaw-amem: this OpenClaw does not report runs to plugins, so the nightly job cannot wait for them"
3677
+ );
3678
+ }
3260
3679
  logger.info(`openclaw-amem: started (backend: amem-qdrant, default agentId: ${defaultScope.agentId})`);
3261
3680
  },
3262
3681
  stop() {
3682
+ cancelNightly();
3683
+ unwatchAgentEvents();
3263
3684
  logger.info("openclaw-amem: stopped");
3264
3685
  }
3265
3686
  });
@@ -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.1",
6
+ "version": "2.1.3",
7
7
  "kind": "memory",
8
8
  "contracts": {
9
9
  "tools": [
@@ -28,6 +28,7 @@
28
28
  "AMEM_LLM_STRONG_MODEL",
29
29
  "AMEM_LLM_STRONG_BASE_URL",
30
30
  "AMEM_LLM_CRUD_ROLE",
31
+ "AMEM_LLM_THINKING",
31
32
  "AMEM_CONFLICT_MODE",
32
33
  "AMEM_LLM_TIMEOUT",
33
34
  "AMEM_CRUD_UPDATE_MIN_SIM",
@@ -138,6 +139,15 @@
138
139
  "default": "fast",
139
140
  "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."
140
141
  },
142
+ "llmThinking": {
143
+ "type": "string",
144
+ "enum": [
145
+ "off",
146
+ "auto"
147
+ ],
148
+ "default": "off",
149
+ "description": "Whether fast-tier calls on the Anthropic API turn thinking off. off sends thinking disabled; a model that refuses it, such as Opus 5.5 or Fable, keeps thinking. auto sends no thinking field. Overridden by AMEM_LLM_THINKING."
150
+ },
141
151
  "conflictSweep": {
142
152
  "type": "boolean",
143
153
  "default": true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-amem",
3
- "version": "2.1.1",
3
+ "version": "2.1.3",
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.126.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",