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 +488 -109
- package/openclaw.plugin.json +11 -1
- package/package.json +2 -2
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,
|
|
261
|
-
const res = await fetch(`${QDRANT_URL}${
|
|
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} ${
|
|
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
|
|
322
|
-
if (typeof recorded === "string" && recorded !==
|
|
323
|
-
throw new EmbeddingModelMismatchError(col, recorded,
|
|
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,
|
|
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
|
|
1210
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
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
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
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;
|
|
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;
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
3613
|
+
{ timeoutMs: HOOK_BUDGET_MS }
|
|
3260
3614
|
);
|
|
3261
3615
|
logger.info("openclaw-amem: agent_end CRUD decision hook registered");
|
|
3262
3616
|
}
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
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
|
-
}
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
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
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
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
|
});
|
package/openclaw.plugin.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "amem",
|
|
4
4
|
"description": "Catches memories that contradict each other. Notes rewrite themselves as new ones arrive, link into a graph, and stay separated per agent and per person. Runs on a small model. Requires a local Qdrant. 中文走 jieba 分词,提示词有中文版。",
|
|
5
5
|
"icon": "https://amem.owo.lc/logo.png",
|
|
6
|
-
"version": "2.1.
|
|
6
|
+
"version": "2.1.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.
|
|
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.
|
|
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",
|