openclaw-amem 2.1.2 → 2.1.4

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/dist/index.js CHANGED
@@ -257,15 +257,15 @@ async function recordCollectionModel(collection, model) {
257
257
  } catch {
258
258
  }
259
259
  }
260
- async function qdrant(method, path6, body) {
261
- const res = await fetch(`${QDRANT_URL}${path6}`, {
260
+ async function qdrant(method, path7, body) {
261
+ const res = await fetch(`${QDRANT_URL}${path7}`, {
262
262
  method,
263
263
  headers: { "Content-Type": "application/json" },
264
264
  body: body ? JSON.stringify(body) : void 0
265
265
  });
266
266
  const data = await res.json();
267
267
  if (!res.ok || data.status && data.status !== "ok" && data.status !== "acknowledged") {
268
- 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)}`);
269
269
  }
270
270
  return data.result;
271
271
  }
@@ -318,12 +318,12 @@ async function ensureCollection(collectionName) {
318
318
  throw new EmbeddingDimensionMismatchError(col, collectionDim, modelDim, getEmbeddingModel());
319
319
  }
320
320
  }
321
- const current = getEmbeddingModel();
322
- if (typeof recorded === "string" && recorded !== current) {
323
- throw new EmbeddingModelMismatchError(col, recorded, current);
321
+ const current2 = getEmbeddingModel();
322
+ if (typeof recorded === "string" && recorded !== current2) {
323
+ throw new EmbeddingModelMismatchError(col, recorded, current2);
324
324
  }
325
325
  if (recorded === void 0 && !inferLegacy) {
326
- await recordCollectionModel(col, current);
326
+ await recordCollectionModel(col, current2);
327
327
  }
328
328
  markReady();
329
329
  return;
@@ -1131,6 +1131,45 @@ ${numberedNotes}
1131
1131
  }
1132
1132
  });
1133
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
+
1134
1173
  // ../amem-core/src/llm.ts
1135
1174
  function configureLlm(cfg) {
1136
1175
  _override = { ...cfg };
@@ -1166,6 +1205,12 @@ function resolveCrudRole() {
1166
1205
  }
1167
1206
  return "fast";
1168
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
+ }
1169
1214
  function resolveTimeoutMs() {
1170
1215
  const envVal = Number(process.env.AMEM_LLM_TIMEOUT);
1171
1216
  if (Number.isFinite(envVal) && envVal > 0) return envVal;
@@ -1202,39 +1247,70 @@ function openai(baseURL) {
1202
1247
  }
1203
1248
  return client;
1204
1249
  }
1205
- 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 };
1206
1257
  const provider = resolveProvider(role);
1207
1258
  const model = resolveModel(role);
1208
1259
  const baseURL = resolveBaseURL(role);
1209
- const isThinking = model.includes("gemini") || model.includes("pro-agent");
1210
- 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;
1211
1265
  try {
1212
- const text = 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
+ }
1213
1280
  if (text === null) warn(`[amem] ${provider} answered with no text (model ${model})`);
1214
- return text;
1215
1281
  } catch (e) {
1216
1282
  warn(`[amem] LLM call failed: ${e.message}`);
1217
- return null;
1218
1283
  }
1284
+ stopIfForegroundBusy();
1285
+ return text;
1219
1286
  }
1220
- async function anthropicCall(prompt, model, maxTokens, baseURL) {
1221
- const resp = await anthropic(baseURL).messages.create({
1222
- model,
1223
- max_tokens: maxTokens,
1224
- messages: [{ role: "user", content: prompt }]
1225
- });
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
+ );
1226
1297
  for (const block of resp.content) {
1227
1298
  if (block.type === "text") return block.text.trim();
1228
1299
  }
1229
1300
  return null;
1230
1301
  }
1231
- async function openaiCall(prompt, model, maxTokens, baseURL) {
1232
- const isReasoning = /^o\d/.test(model) || model.startsWith("gpt-5");
1233
- const resp = await openai(baseURL).chat.completions.create({
1234
- model,
1235
- ...isReasoning ? { max_completion_tokens: maxTokens } : { max_tokens: maxTokens },
1236
- messages: [{ role: "user", content: prompt }]
1237
- });
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
+ );
1238
1314
  return resp.choices[0]?.message?.content?.trim() ?? null;
1239
1315
  }
1240
1316
  function stripReasoning(raw) {
@@ -1266,7 +1342,7 @@ function parseJsonLoose(raw) {
1266
1342
  throw e;
1267
1343
  }
1268
1344
  }
1269
- async function llmConstructNote(content) {
1345
+ async function llmConstructNote(content, opts = {}) {
1270
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:
1271
1347
  {
1272
1348
  "keywords": ["keyword1", "keyword2"],
@@ -1301,7 +1377,7 @@ confidence guide (Story 27):
1301
1377
  - low: LLM is uncertain \u2014 vague, fragmentary, or mixed content
1302
1378
 
1303
1379
  Text: ${content}`;
1304
- const raw = await llmCall(prompt, 400);
1380
+ const raw = await llmCall(prompt, 400, "fast", opts);
1305
1381
  if (!raw) {
1306
1382
  warn("[amem] note construction got nothing back; storing with no keywords, tags or context");
1307
1383
  return {
@@ -1344,21 +1420,21 @@ Text: ${content}`;
1344
1420
  };
1345
1421
  }
1346
1422
  }
1347
- async function llmShouldLink(noteContent, candidateContent) {
1423
+ async function llmShouldLink(noteContent, candidateContent, opts = {}) {
1348
1424
  const prompt = `Do these two memory notes have a meaningful relationship that would be useful to link?
1349
1425
  Reply with only "yes" or "no".
1350
1426
 
1351
1427
  Note A: ${noteContent}
1352
1428
  Note B: ${candidateContent}`;
1353
- const raw = await llmCall(prompt, 10);
1429
+ const raw = await llmCall(prompt, 10, "fast", opts);
1354
1430
  if (!raw) return false;
1355
1431
  return raw.toLowerCase().startsWith("yes");
1356
1432
  }
1357
- async function llmCrudDecision(userText, assistantText, existingMemories) {
1433
+ async function llmCrudDecision(userText, assistantText, existingMemories, opts = {}) {
1358
1434
  const memoryList = existingMemories.length > 0 ? existingMemories.map((m) => `[${m.idx}] ${m.content}`).join("\n") : "(none)";
1359
1435
  const prompt = t.crudDecision(userText.slice(0, 500), assistantText.slice(0, 500), memoryList);
1360
1436
  try {
1361
- const raw = await llmCall(prompt, 400, resolveCrudRole());
1437
+ const raw = await llmCall(prompt, 400, resolveCrudRole(), opts);
1362
1438
  if (!raw) return [];
1363
1439
  const match = stripReasoning(raw).match(/\[.*\]/s);
1364
1440
  if (!match) {
@@ -1430,7 +1506,7 @@ async function llmEvolutionJudge(oldContent, newContent) {
1430
1506
  return { type: "NEW" };
1431
1507
  }
1432
1508
  }
1433
- async function llmEvolveNote(content, linkedNotes) {
1509
+ async function llmEvolveNote(content, linkedNotes, opts = {}) {
1434
1510
  const linkedStr = linkedNotes.map((n) => `- ID: ${n.id}
1435
1511
  Content: ${n.content}`).join("\n");
1436
1512
  const prompt = `A memory note has gained new connections. Update its context, tags, and decide whether to strengthen connections with specific neighbors.
@@ -1453,7 +1529,7 @@ Original note content: ${content}
1453
1529
 
1454
1530
  Newly linked notes (neighbors):
1455
1531
  ${linkedStr}`;
1456
- const raw = await llmCall(prompt, 500);
1532
+ const raw = await llmCall(prompt, 500, "fast", opts);
1457
1533
  if (!raw) return { tags: null, context: null, shouldStrengthen: false, suggestedConnections: [], tagsToUpdate: [] };
1458
1534
  try {
1459
1535
  const data = parseJsonLoose(raw);
@@ -1472,19 +1548,19 @@ ${linkedStr}`;
1472
1548
  async function llmConflictScan(contents) {
1473
1549
  if (contents.length < 2) return [];
1474
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;
1475
1553
  try {
1476
- const raw = await llmCall(t.conflictScan(numbered), 600, "strong");
1477
- if (!raw) return [];
1478
1554
  const cleaned = stripReasoning(raw);
1479
1555
  const match = cleaned.match(/\[[\s\S]*\]/);
1480
1556
  if (!match) {
1481
- warn("[amem] llmConflictScan found no array in the response; this sweep reports no pairs");
1482
- return [];
1557
+ warn("[amem] llmConflictScan found no array in the response; the batch is read again next run");
1558
+ return null;
1483
1559
  }
1484
1560
  const parsed = JSON.parse(match[0]);
1485
1561
  if (!Array.isArray(parsed)) {
1486
- warn("[amem] llmConflictScan parsed a non-array; this sweep reports no pairs");
1487
- return [];
1562
+ warn("[amem] llmConflictScan parsed a non-array; the batch is read again next run");
1563
+ return null;
1488
1564
  }
1489
1565
  const pairs = [];
1490
1566
  const seen = /* @__PURE__ */ new Set();
@@ -1507,13 +1583,17 @@ async function llmConflictScan(contents) {
1507
1583
  supersededIndex
1508
1584
  });
1509
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
+ }
1510
1590
  return pairs;
1511
1591
  } catch (e) {
1512
1592
  warn(`[amem] llmConflictScan failed: ${e.message}`);
1513
- return [];
1593
+ return null;
1514
1594
  }
1515
1595
  }
1516
- 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;
1517
1597
  var init_llm = __esm({
1518
1598
  "../amem-core/src/llm.ts"() {
1519
1599
  "use strict";
@@ -1521,8 +1601,12 @@ var init_llm = __esm({
1521
1601
  import_openai = __toESM(require("openai"), 1);
1522
1602
  init_prompts();
1523
1603
  init_config();
1604
+ init_deadline();
1605
+ init_background();
1524
1606
  _override = {};
1525
1607
  _warned = /* @__PURE__ */ new Set();
1608
+ _thinkingStaysOn = /* @__PURE__ */ new Set();
1609
+ roomToThink = (maxTokens) => Math.max(maxTokens * 8, 4e3);
1526
1610
  DEFAULT_TIMEOUT_MS = 3e4;
1527
1611
  _anthropicClients = /* @__PURE__ */ new Map();
1528
1612
  _openaiClients = /* @__PURE__ */ new Map();
@@ -1659,7 +1743,8 @@ async function addMemory(content, agentId = "main", opts) {
1659
1743
  return existingByHash.id;
1660
1744
  }
1661
1745
  console.log("[add] Constructing note...");
1662
- 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 });
1663
1748
  console.log(` keywords: ${keywords.join(", ")}`);
1664
1749
  console.log(` tags: ${tags.join(", ")}`);
1665
1750
  console.log(` context: ${context}`);
@@ -1732,8 +1817,9 @@ async function addMemory(content, agentId = "main", opts) {
1732
1817
  for (const { note: cand, score } of candidates) {
1733
1818
  if (cand.id === note.id) continue;
1734
1819
  if (score < 0.3) continue;
1820
+ if (!hasTimeFor(deadline)) break;
1735
1821
  console.log(` candidate ${cand.id.slice(0, 8)}... sim=${score.toFixed(3)}, asking LLM...`);
1736
- const shouldLink = await llmShouldLink(content, cand.content);
1822
+ const shouldLink = await llmShouldLink(content, cand.content, { deadline });
1737
1823
  if (shouldLink) {
1738
1824
  linkedIds.push(cand.id);
1739
1825
  linkedContents.push(cand.content);
@@ -1757,6 +1843,7 @@ async function addMemory(content, agentId = "main", opts) {
1757
1843
  if (shouldRunEvolution()) {
1758
1844
  console.log(` [evo] threshold reached, running evolution for ${Math.min(linkedIds.length, 3)} linked notes`);
1759
1845
  for (const lid of linkedIds.slice(0, 3)) {
1846
+ if (!hasTimeFor(deadline)) break;
1760
1847
  const linked = await ctx.getNote(lid, agentId);
1761
1848
  if (!linked) continue;
1762
1849
  if (!canWrite(linked, agentId)) {
@@ -1778,7 +1865,7 @@ async function addMemory(content, agentId = "main", opts) {
1778
1865
  shouldStrengthen,
1779
1866
  suggestedConnections,
1780
1867
  tagsToUpdate
1781
- } = await llmEvolveNote(linked.content, linkedNotes);
1868
+ } = await llmEvolveNote(linked.content, linkedNotes, { deadline });
1782
1869
  let evolved = false;
1783
1870
  if (newTags !== null || newContext !== null) {
1784
1871
  if (newTags !== null) linked.tags = newTags;
@@ -1999,10 +2086,9 @@ async function listMemories(agentId = "main", storageCtx) {
1999
2086
  function sleep(ms) {
2000
2087
  return new Promise((resolve2) => setTimeout(resolve2, ms));
2001
2088
  }
2002
- async function mergeSimilarNotes(agentId, storageCtx) {
2089
+ async function mergeSimilarNotes(agentId, storageCtx, date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)) {
2003
2090
  const ctx = storageCtx ?? defaultCtx();
2004
- const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2005
- const allNotes = await ctx.getNotesByDatePrefix(today, agentId);
2091
+ const allNotes = await ctx.getNotesByDatePrefix(date, agentId);
2006
2092
  const notes = allNotes.filter((n) => n.agent_id !== "shared");
2007
2093
  const pendingNotes = notes.filter((n) => n.pending_merge === true);
2008
2094
  let evolvedCount = 0;
@@ -2020,6 +2106,7 @@ async function mergeSimilarNotes(agentId, storageCtx) {
2020
2106
  }
2021
2107
  }
2022
2108
  if (!bestNeighbor) {
2109
+ stopIfForegroundBusy();
2023
2110
  await ctx.patchNotePayload(pendingNote.id, { pending_merge: false });
2024
2111
  continue;
2025
2112
  }
@@ -2140,6 +2227,7 @@ async function consolidateMemories(agentId, logger, storageCtx) {
2140
2227
  for (const [category, groupNotes] of groups.entries()) {
2141
2228
  log.info(`[Consolidation] Category "${category}" has ${groupNotes.length} notes.`);
2142
2229
  for (let i = 0; i < groupNotes.length; i++) {
2230
+ await pauseForForeground();
2143
2231
  for (let j = i + 1; j < groupNotes.length; j++) {
2144
2232
  const noteA = groupNotes[i];
2145
2233
  const noteB = groupNotes[j];
@@ -2249,6 +2337,7 @@ async function conflictSweep(agentId, opts) {
2249
2337
  let retired = 0;
2250
2338
  let batchesScanned = 0;
2251
2339
  let batchesSkipped = 0;
2340
+ let batchesFailed = 0;
2252
2341
  for (const [category, groupNotes] of groups.entries()) {
2253
2342
  groupNotes.sort((a, b) => Date.parse(b.timestamp) - Date.parse(a.timestamp));
2254
2343
  for (let start = 0; start < groupNotes.length; start += CONFLICT_BATCH_SIZE) {
@@ -2260,6 +2349,10 @@ async function conflictSweep(agentId, opts) {
2260
2349
  }
2261
2350
  batchesScanned++;
2262
2351
  const pairs = await llmConflictScan(batch.map((n) => n.content));
2352
+ if (pairs === null) {
2353
+ batchesFailed++;
2354
+ continue;
2355
+ }
2263
2356
  for (const { a, b, reason, supersededIndex } of pairs) {
2264
2357
  const noteA = batch[a];
2265
2358
  const noteB = batch[b];
@@ -2298,9 +2391,9 @@ async function conflictSweep(agentId, opts) {
2298
2391
  }
2299
2392
  }
2300
2393
  log(
2301
- `[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`
2302
2395
  );
2303
- return { scanned: notes.length, pairsFound, retired, batchesScanned, batchesSkipped };
2396
+ return { scanned: notes.length, pairsFound, retired, batchesScanned, batchesSkipped, batchesFailed };
2304
2397
  }
2305
2398
  var import_uuid, import_crypto, fs2, path3, import_jieba, logSafe, _jieba, EPHEMERAL_SIGNALS, CONFLICT_BATCH_SIZE;
2306
2399
  var init_memory = __esm({
@@ -2315,6 +2408,8 @@ var init_memory = __esm({
2315
2408
  init_auth();
2316
2409
  init_llm();
2317
2410
  init_evo_counter();
2411
+ init_deadline();
2412
+ init_background();
2318
2413
  init_config();
2319
2414
  import_jieba = require("@node-rs/jieba");
2320
2415
  logSafe = (id) => id.replace(/[\r\n]/g, "");
@@ -2687,6 +2782,7 @@ var init_crud_guard = __esm({
2687
2782
  // ../amem-core/src/index.ts
2688
2783
  var src_exports = {};
2689
2784
  __export(src_exports, {
2785
+ BackgroundPreempted: () => BackgroundPreempted,
2690
2786
  DEFAULT_CRUD_UPDATE_MIN_SIM: () => DEFAULT_CRUD_UPDATE_MIN_SIM,
2691
2787
  DEFAULT_EMBEDDING_MODEL: () => DEFAULT_EMBEDDING_MODEL,
2692
2788
  EmbeddingDimensionMismatchError: () => EmbeddingDimensionMismatchError,
@@ -2715,6 +2811,7 @@ __export(src_exports, {
2715
2811
  getEmbeddingModel: () => getEmbeddingModel,
2716
2812
  getEmbeddingPooling: () => getEmbeddingPooling,
2717
2813
  getNote: () => getNote,
2814
+ hasTimeFor: () => hasTimeFor,
2718
2815
  invalidateNote: () => invalidateNote,
2719
2816
  isModelLoaded: () => isModelLoaded,
2720
2817
  isPlausibleUpdateTarget: () => isPlausibleUpdateTarget,
@@ -2727,6 +2824,7 @@ __export(src_exports, {
2727
2824
  patchNotePayload: () => patchNotePayload,
2728
2825
  pingQdrant: () => pingQdrant,
2729
2826
  resolveCrudUpdateMinSim: () => resolveCrudUpdateMinSim,
2827
+ runInBackground: () => runInBackground,
2730
2828
  scanLowQuality: () => scanLowQuality,
2731
2829
  searchMemory: () => searchMemory,
2732
2830
  switchToMigrated: () => switchToMigrated,
@@ -2747,6 +2845,8 @@ var init_src = __esm({
2747
2845
  init_memory();
2748
2846
  init_crud_guard();
2749
2847
  init_llm();
2848
+ init_deadline();
2849
+ init_background();
2750
2850
  }
2751
2851
  });
2752
2852
 
@@ -2773,7 +2873,7 @@ __export(index_exports, {
2773
2873
  });
2774
2874
  module.exports = __toCommonJS(index_exports);
2775
2875
  var os2 = __toESM(require("os"));
2776
- var path5 = __toESM(require("path"));
2876
+ var path6 = __toESM(require("path"));
2777
2877
  var import_plugin_entry = require("openclaw/plugin-sdk/plugin-entry");
2778
2878
  init_src();
2779
2879
  var import_crypto2 = require("crypto");
@@ -2786,6 +2886,237 @@ function isConvAccessBlocked(config, pluginId) {
2786
2886
  return entry !== void 0 && entry.hooks?.allowConversationAccess !== true;
2787
2887
  }
2788
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
+
2789
3120
  // src/scope.ts
2790
3121
  function parseAgentIdFromSessionKey(sessionKey) {
2791
3122
  if (!sessionKey) return void 0;
@@ -2811,6 +3142,8 @@ function buildScope(rawAgentId, pluginConfig, createStorageContext2) {
2811
3142
  // src/index.ts
2812
3143
  init_src();
2813
3144
  var _config = {};
3145
+ var HOOK_BUDGET_MS = 3e4;
3146
+ var HOOK_MARGIN_MS = 2e3;
2814
3147
  function register(api) {
2815
3148
  const logger = api.logger;
2816
3149
  _config = api.pluginConfig || {};
@@ -2819,16 +3152,17 @@ function register(api) {
2819
3152
  const convBlocked = isConvAccessBlocked(api.config, pluginId);
2820
3153
  if (convBlocked) logger.warn(BLOCKED_WARNING_LOG);
2821
3154
  configure({
2822
- dataDir: path5.join(os2.homedir(), ".openclaw"),
3155
+ dataDir: path6.join(os2.homedir(), ".openclaw"),
2823
3156
  warn: (msg) => logger.warn(msg)
2824
3157
  });
2825
3158
  const hasStrong = !!(pluginConfig.llmStrongProvider || pluginConfig.llmStrongModel || pluginConfig.llmStrongBaseURL);
2826
- if (pluginConfig.llmProvider || pluginConfig.llmModel || pluginConfig.llmBaseURL || pluginConfig.llmCrudRole || hasStrong) {
3159
+ if (pluginConfig.llmProvider || pluginConfig.llmModel || pluginConfig.llmBaseURL || pluginConfig.llmCrudRole || pluginConfig.llmThinking || hasStrong) {
2827
3160
  configureLlm({
2828
3161
  provider: pluginConfig.llmProvider,
2829
3162
  model: pluginConfig.llmModel,
2830
3163
  baseURL: pluginConfig.llmBaseURL,
2831
3164
  crudRole: pluginConfig.llmCrudRole,
3165
+ thinking: pluginConfig.llmThinking,
2832
3166
  // Omit the whole block when unset so `strong` transparently falls back to
2833
3167
  // `fast` — the zero-config path stays byte-for-byte today's behaviour.
2834
3168
  ...hasStrong && {
@@ -2845,11 +3179,19 @@ function register(api) {
2845
3179
  const resolveAgentId2 = (ctx) => resolveAgentId(ctx, pluginConfig);
2846
3180
  const buildScope2 = (rawAgentId) => buildScope(rawAgentId, pluginConfig, createStorageContext);
2847
3181
  const defaultScope = buildScope2(resolveAgentId2());
2848
- 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
+ };
2849
3191
  logger.info(
2850
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)`
2851
3193
  );
2852
- ensureCollection(pluginConfig.collection).catch((e) => {
3194
+ const checkCollection = () => ensureCollection(pluginConfig.collection).catch((e) => {
2853
3195
  if (e instanceof EmbeddingDimensionMismatchError || e instanceof EmbeddingModelMismatchError || e instanceof MixedEmbeddingModelsError) {
2854
3196
  logger.error(`openclaw-amem: memory is UNUSABLE \u2014 ${e.message}`);
2855
3197
  } else {
@@ -2899,7 +3241,9 @@ function register(api) {
2899
3241
  },
2900
3242
  async add(text) {
2901
3243
  try {
3244
+ const since = /* @__PURE__ */ new Date();
2902
3245
  await addMemory(text, scope.agentId, { storageCtx: scope.storageCtx });
3246
+ owe(resolveAgentId2(params), since);
2903
3247
  return { ok: true };
2904
3248
  } catch (err) {
2905
3249
  logger.warn(`openclaw-amem: add failed \u2014 ${err.message}`);
@@ -3020,6 +3364,7 @@ ${text}${hookWarning}` }],
3020
3364
  const start = Date.now();
3021
3365
  try {
3022
3366
  const id = await addMemory(text, scope.agentId, { subjects, storageCtx: scope.storageCtx });
3367
+ owe(resolveAgentId2(ctx), new Date(start));
3023
3368
  logger.info(`openclaw-amem: memory_add OK id=${id} (${Date.now() - start}ms)`);
3024
3369
  return {
3025
3370
  content: [{ type: "text", text: "Memory saved successfully." }],
@@ -3146,15 +3491,30 @@ ${text}${hookWarning}` }],
3146
3491
  }
3147
3492
  if (typeof api.registerHook === "function" || typeof api.on === "function") {
3148
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
+ );
3149
3506
  hookFn(
3150
3507
  "agent_end",
3151
3508
  async (event, ctx) => {
3509
+ const deadline = Date.now() + HOOK_BUDGET_MS - HOOK_MARGIN_MS;
3152
3510
  const scope = buildScope2(resolveAgentId2(ctx));
3153
3511
  const agentId = scope.agentId;
3154
3512
  const storageCtx = scope.storageCtx;
3155
3513
  logger.info(
3156
3514
  `openclaw-amem: agent_end hook triggered (agent_id=${agentId}, success=${event.success}, messages=${event.messages?.length ?? 0})`
3157
3515
  );
3516
+ const done = foregroundWorkStarted();
3517
+ let since;
3158
3518
  try {
3159
3519
  let shouldProcessTurn2 = function(text) {
3160
3520
  if (text.trim().length < 10) return false;
@@ -3190,12 +3550,18 @@ ${text}${hookWarning}` }],
3190
3550
  const operations = await llmCrudDecision2(
3191
3551
  userText,
3192
3552
  assistantText,
3193
- existingMemories.map((m) => ({ idx: m.idx, content: m.content }))
3553
+ existingMemories.map((m) => ({ idx: m.idx, content: m.content })),
3554
+ { deadline }
3194
3555
  );
3195
3556
  if (!operations || operations.length === 0) return;
3196
- 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
+ }
3197
3563
  if (op.action === "NEW") {
3198
- await addMemory(op.fact, agentId, { storageCtx });
3564
+ await addMemory(op.fact, agentId, { storageCtx, deadline });
3199
3565
  logger.info(`openclaw-amem: CRUD NEW: "${op.fact.slice(0, 60)}${op.fact.length > 60 ? "..." : ""}"`);
3200
3566
  } else if (op.action === "UPDATE" && op.existingIdx !== void 0) {
3201
3567
  const target = existingMemories[op.existingIdx];
@@ -3204,7 +3570,7 @@ ${text}${hookWarning}` }],
3204
3570
  const hash = (0, import_crypto2.createHash)("md5").update(op.fact).digest("hex");
3205
3571
  const targetNote = await storageCtx.getNote(target.id, agentId);
3206
3572
  if (!targetNote || !isPlausibleUpdateTarget(newEmbedding, targetNote.embedding, crudUpdateMinSim)) {
3207
- await addMemory(op.fact, agentId, { storageCtx });
3573
+ await addMemory(op.fact, agentId, { storageCtx, deadline });
3208
3574
  logger.warn(
3209
3575
  `openclaw-amem: CRUD UPDATE on ${target.id.slice(0, 8)} looks mis-targeted \u2014 stored as a new memory instead`
3210
3576
  );
@@ -3237,73 +3603,86 @@ ${text}${hookWarning}` }],
3237
3603
  }
3238
3604
  }
3239
3605
  }
3240
- const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3241
- logger.info(`openclaw-amem: starting mergeSimilarNotes for agent=${agentId}, date=${today}`);
3242
- try {
3243
- const merged = await mergeSimilarNotes(agentId, storageCtx);
3244
- if (merged > 0) {
3245
- logger.info(`openclaw-amem: merged ${merged} similar notes today (${today})`);
3246
- } else {
3247
- logger.info(`openclaw-amem: mergeSimilarNotes completed, 0 pairs merged (${today})`);
3248
- }
3249
- } catch (mergeErr) {
3250
- logger.error(
3251
- `openclaw-amem: mergeSimilarNotes FAILED \u2014 ${mergeErr.message}
3252
- ${mergeErr.stack}`
3253
- );
3254
- }
3255
3606
  } catch (e) {
3256
3607
  logger.warn(`openclaw-amem: agent_end CRUD hook failed \u2014 ${e.message}`);
3608
+ } finally {
3609
+ if (since) owe(resolveAgentId2(ctx), since);
3610
+ done();
3257
3611
  }
3258
3612
  },
3259
- { timeoutMs: 3e4 }
3613
+ { timeoutMs: HOOK_BUDGET_MS }
3260
3614
  );
3261
3615
  logger.info("openclaw-amem: agent_end CRUD decision hook registered");
3262
3616
  }
3263
- function scheduleNextRun() {
3264
- const now = /* @__PURE__ */ new Date();
3265
- const target = /* @__PURE__ */ new Date();
3266
- target.setHours(2, 30, 0, 0);
3267
- if (target.getTime() <= now.getTime()) {
3268
- target.setDate(target.getDate() + 1);
3269
- }
3270
- const delay = target.getTime() - now.getTime();
3271
- setTimeout(async () => {
3272
- try {
3273
- logger.info("openclaw-amem: Running scheduled daily consolidation...");
3274
- const merged = await consolidateMemories(defaultScope.agentId, logger, defaultScope.storageCtx);
3275
- if (merged > 0) {
3276
- logger.info(`openclaw-amem: Scheduled daily consolidation merged ${merged} pairs.`);
3617
+ const nightlyJob = () => runNightly({
3618
+ owedFile,
3619
+ defaultAgent: resolveAgentId2(),
3620
+ mergeDay: async (rawAgentId, day) => {
3621
+ const scope = buildScope2(rawAgentId);
3622
+ const merged = await mergeSimilarNotes(scope.agentId, scope.storageCtx, day);
3623
+ if (merged > 0) logger.info(`openclaw-amem: merged ${merged} similar notes (${scope.agentId}, ${day})`);
3624
+ },
3625
+ after: [
3626
+ {
3627
+ name: "consolidation",
3628
+ run: async () => {
3629
+ logger.info("openclaw-amem: Running scheduled daily consolidation...");
3630
+ const merged = await consolidateMemories(defaultScope.agentId, logger, defaultScope.storageCtx);
3631
+ if (merged > 0) {
3632
+ logger.info(`openclaw-amem: Scheduled daily consolidation merged ${merged} pairs.`);
3633
+ }
3277
3634
  }
3278
- } catch (err) {
3279
- logger.warn(`openclaw-amem: Scheduled daily consolidation failed \u2014 ${err.message}`);
3280
- }
3281
- if (conflictSweepEnabled) {
3282
- try {
3283
- const res = await conflictSweep(defaultScope.agentId, {
3284
- storageCtx: defaultScope.storageCtx,
3285
- logger
3286
- });
3287
- if (res.pairsFound > 0) {
3288
- logger.info(
3289
- `openclaw-amem: Contradiction sweep flagged ${res.pairsFound} pair(s)` + (res.retired > 0 ? `, retired ${res.retired}` : "") + ` (${res.batchesScanned} batch(es) read, ${res.batchesSkipped} unchanged).`
3290
- );
3635
+ },
3636
+ // Story 43: the cold half of the tiering split. The per-turn CRUD decision
3637
+ // runs on the fast model, which is safe but misses contradictions; this is
3638
+ // what catches them. Runs AFTER consolidation, as its own step, so neither
3639
+ // task can take the other down.
3640
+ //
3641
+ // Only batches that gained a note since the last run are re-read, so a
3642
+ // steady-state night costs a call or two rather than a full re-read.
3643
+ ...conflictSweepEnabled ? [
3644
+ {
3645
+ name: "contradiction sweep",
3646
+ run: async () => {
3647
+ const res = await conflictSweep(defaultScope.agentId, {
3648
+ storageCtx: defaultScope.storageCtx,
3649
+ logger
3650
+ });
3651
+ if (res.pairsFound > 0) {
3652
+ logger.info(
3653
+ `openclaw-amem: Contradiction sweep flagged ${res.pairsFound} pair(s)` + (res.retired > 0 ? `, retired ${res.retired}` : "") + ` (${res.batchesScanned} batch(es) read, ${res.batchesSkipped} unchanged).`
3654
+ );
3655
+ }
3291
3656
  }
3292
- } catch (err) {
3293
- logger.warn(`openclaw-amem: Scheduled contradiction sweep failed \u2014 ${err.message}`);
3294
3657
  }
3295
- }
3296
- scheduleNextRun();
3297
- }, delay);
3298
- }
3299
- scheduleNextRun();
3658
+ ] : []
3659
+ ],
3660
+ inBackground: (work) => runInBackground(busySinceNow(), work),
3661
+ preempted: (err) => err instanceof BackgroundPreempted,
3662
+ logger
3663
+ });
3300
3664
  if (typeof api.registerService === "function") {
3301
3665
  api.registerService({
3302
3666
  id: "amem-plugin",
3303
3667
  start() {
3668
+ const events = api.runtime?.events;
3669
+ if (typeof events?.onAgentEvent === "function") {
3670
+ watchAgentEvents((listener) => events.onAgentEvent(listener));
3671
+ } else {
3672
+ logger.warn(
3673
+ "openclaw-amem: this OpenClaw does not report runs to plugins, so the nightly job cannot wait for them"
3674
+ );
3675
+ }
3676
+ scheduleNightly(
3677
+ nightlyJob,
3678
+ (err) => logger.warn(`openclaw-amem: nightly job failed \u2014 ${err.message}`)
3679
+ );
3680
+ void checkCollection();
3304
3681
  logger.info(`openclaw-amem: started (backend: amem-qdrant, default agentId: ${defaultScope.agentId})`);
3305
3682
  },
3306
3683
  stop() {
3684
+ cancelNightly();
3685
+ unwatchAgentEvents();
3307
3686
  logger.info("openclaw-amem: stopped");
3308
3687
  }
3309
3688
  });
@@ -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.2",
6
+ "version": "2.1.4",
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.2",
3
+ "version": "2.1.4",
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.122.0",
19
+ "@anthropic-ai/sdk": "^0.127.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",