@memtensor/memos-cloud-openclaw-plugin 0.1.8-beta.9 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -2
- package/README_ZH.md +21 -2
- package/clawdbot.plugin.json +1 -1
- package/index.js +200 -7
- package/lib/check-update.js +5 -64
- package/lib/memos-cloud-api.js +108 -58
- package/moltbot.plugin.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -97,6 +97,15 @@ MEMOS_API_KEY=YOUR_TOKEN
|
|
|
97
97
|
- `MEMOS_CONVERSATION_PREFIX` / `MEMOS_CONVERSATION_SUFFIX` (optional)
|
|
98
98
|
- `MEMOS_CONVERSATION_SUFFIX_MODE` (`none` | `counter`, default: `none`)
|
|
99
99
|
- `MEMOS_CONVERSATION_RESET_ON_NEW` (default: `true`, requires hooks.internal.enabled)
|
|
100
|
+
- `MEMOS_RECALL_FILTER_ENABLED` (default: `false`; run model-based memory filtering before injection)
|
|
101
|
+
- `MEMOS_RECALL_FILTER_BASE_URL` (OpenAI-compatible base URL, e.g. `http://127.0.0.1:11434/v1`)
|
|
102
|
+
- `MEMOS_RECALL_FILTER_API_KEY` (optional; required if your endpoint needs auth)
|
|
103
|
+
- `MEMOS_RECALL_FILTER_MODEL` (model name used to filter recall candidates)
|
|
104
|
+
- `MEMOS_RECALL_FILTER_TIMEOUT_MS` (default: `6000`)
|
|
105
|
+
- `MEMOS_RECALL_FILTER_RETRIES` (default: `0`)
|
|
106
|
+
- `MEMOS_RECALL_FILTER_CANDIDATE_LIMIT` (default: `30` per category)
|
|
107
|
+
- `MEMOS_RECALL_FILTER_MAX_ITEM_CHARS` (default: `500`)
|
|
108
|
+
- `MEMOS_RECALL_FILTER_FAIL_OPEN` (default: `true`; fallback to unfiltered recall on failure)
|
|
100
109
|
|
|
101
110
|
## Optional Plugin Config
|
|
102
111
|
In `plugins.entries.memos-cloud-openclaw-plugin.config`:
|
|
@@ -127,7 +136,16 @@ In `plugins.entries.memos-cloud-openclaw-plugin.config`:
|
|
|
127
136
|
"tags": ["openclaw"],
|
|
128
137
|
"agentId": "",
|
|
129
138
|
"multiAgentMode": false,
|
|
130
|
-
"asyncMode": true
|
|
139
|
+
"asyncMode": true,
|
|
140
|
+
"recallFilterEnabled": false,
|
|
141
|
+
"recallFilterBaseUrl": "http://127.0.0.1:11434/v1",
|
|
142
|
+
"recallFilterApiKey": "",
|
|
143
|
+
"recallFilterModel": "qwen2.5:7b",
|
|
144
|
+
"recallFilterTimeoutMs": 6000,
|
|
145
|
+
"recallFilterRetries": 0,
|
|
146
|
+
"recallFilterCandidateLimit": 30,
|
|
147
|
+
"recallFilterMaxItemChars": 500,
|
|
148
|
+
"recallFilterFailOpen": true
|
|
131
149
|
}
|
|
132
150
|
```
|
|
133
151
|
|
|
@@ -135,7 +153,8 @@ In `plugins.entries.memos-cloud-openclaw-plugin.config`:
|
|
|
135
153
|
- **Recall** (`before_agent_start`)
|
|
136
154
|
- Builds a `/search/memory` request using `user_id`, `query` (= prompt + optional prefix), and optional filters.
|
|
137
155
|
- Default **global recall**: when `recallGlobal=true`, it does **not** pass `conversation_id`.
|
|
138
|
-
-
|
|
156
|
+
- Optional second-pass filtering: if `recallFilterEnabled=true`, candidates are sent to your configured model and only returned `keep` items are injected.
|
|
157
|
+
- Injects a stable MemOS recall protocol via `appendSystemContext`, while the retrieved `<memories>` block remains in `prependContext`.
|
|
139
158
|
|
|
140
159
|
- **Add** (`agent_end`)
|
|
141
160
|
- Builds a `/add/message` request with the **last turn** by default (user + assistant).
|
package/README_ZH.md
CHANGED
|
@@ -99,6 +99,15 @@ MEMOS_API_KEY=YOUR_TOKEN
|
|
|
99
99
|
- `MEMOS_CONVERSATION_PREFIX` / `MEMOS_CONVERSATION_SUFFIX`(可选)
|
|
100
100
|
- `MEMOS_CONVERSATION_SUFFIX_MODE`(`none` | `counter`,默认 `none`)
|
|
101
101
|
- `MEMOS_CONVERSATION_RESET_ON_NEW`(默认 `true`,需 hooks.internal.enabled)
|
|
102
|
+
- `MEMOS_RECALL_FILTER_ENABLED`(默认 `false`;开启后先用你指定的模型过滤召回记忆再注入)
|
|
103
|
+
- `MEMOS_RECALL_FILTER_BASE_URL`(OpenAI 兼容接口,例如 `http://127.0.0.1:11434/v1`)
|
|
104
|
+
- `MEMOS_RECALL_FILTER_API_KEY`(可选,若你的接口需要鉴权)
|
|
105
|
+
- `MEMOS_RECALL_FILTER_MODEL`(用于筛选记忆的模型名)
|
|
106
|
+
- `MEMOS_RECALL_FILTER_TIMEOUT_MS`(默认 `6000`)
|
|
107
|
+
- `MEMOS_RECALL_FILTER_RETRIES`(默认 `0`)
|
|
108
|
+
- `MEMOS_RECALL_FILTER_CANDIDATE_LIMIT`(默认每类 `30` 条)
|
|
109
|
+
- `MEMOS_RECALL_FILTER_MAX_ITEM_CHARS`(默认 `500`)
|
|
110
|
+
- `MEMOS_RECALL_FILTER_FAIL_OPEN`(默认 `true`;筛选失败时回退为“不过滤”)
|
|
102
111
|
|
|
103
112
|
## 可选插件配置
|
|
104
113
|
在 `plugins.entries.memos-cloud-openclaw-plugin.config` 中设置:
|
|
@@ -127,7 +136,16 @@ MEMOS_API_KEY=YOUR_TOKEN
|
|
|
127
136
|
"tags": ["openclaw"],
|
|
128
137
|
"agentId": "",
|
|
129
138
|
"multiAgentMode": false,
|
|
130
|
-
"asyncMode": true
|
|
139
|
+
"asyncMode": true,
|
|
140
|
+
"recallFilterEnabled": false,
|
|
141
|
+
"recallFilterBaseUrl": "http://127.0.0.1:11434/v1",
|
|
142
|
+
"recallFilterApiKey": "",
|
|
143
|
+
"recallFilterModel": "qwen2.5:7b",
|
|
144
|
+
"recallFilterTimeoutMs": 6000,
|
|
145
|
+
"recallFilterRetries": 0,
|
|
146
|
+
"recallFilterCandidateLimit": 30,
|
|
147
|
+
"recallFilterMaxItemChars": 500,
|
|
148
|
+
"recallFilterFailOpen": true
|
|
131
149
|
}
|
|
132
150
|
```
|
|
133
151
|
|
|
@@ -137,7 +155,8 @@ MEMOS_API_KEY=YOUR_TOKEN
|
|
|
137
155
|
- `user_id`、`query`(= prompt + 可选前缀)
|
|
138
156
|
- 默认**全局召回**:`recallGlobal=true` 时不传 `conversation_id`
|
|
139
157
|
- 可选 `filter` / `knowledgebase_ids`
|
|
140
|
-
-
|
|
158
|
+
- (可选)若开启 `recallFilterEnabled`,会先把 `memory/preference/tool_memory` 候选发给你配置的模型做二次筛选,只保留 `keep` 的条目
|
|
159
|
+
- 将稳定的 MemOS 召回协议通过 `appendSystemContext` 注入,而检索到的 `<memories>` 数据块继续通过 `prependContext` 注入
|
|
141
160
|
|
|
142
161
|
### 2) 添加(agent_end)
|
|
143
162
|
- 默认只写**最后一轮**(user + assistant)
|
package/clawdbot.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "memos-cloud-openclaw-plugin",
|
|
3
3
|
"name": "MemOS Cloud OpenClaw Plugin",
|
|
4
4
|
"description": "MemOS Cloud recall + add memory via lifecycle hooks",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.9",
|
|
6
6
|
"kind": "lifecycle",
|
|
7
7
|
"main": "./index.js",
|
|
8
8
|
"configSchema": {
|
package/index.js
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
import {
|
|
3
3
|
addMessage,
|
|
4
4
|
buildConfig,
|
|
5
|
+
extractResultData,
|
|
5
6
|
extractText,
|
|
6
|
-
|
|
7
|
+
formatRecallHookResult,
|
|
7
8
|
USER_QUERY_MARKER,
|
|
8
9
|
searchMemory,
|
|
9
10
|
} from "./lib/memos-cloud-api.js";
|
|
@@ -196,6 +197,197 @@ function truncate(text, maxLen) {
|
|
|
196
197
|
return text.length > maxLen ? `${text.slice(0, maxLen)}...` : text;
|
|
197
198
|
}
|
|
198
199
|
|
|
200
|
+
function sleep(ms) {
|
|
201
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function parseModelJson(text) {
|
|
205
|
+
if (!text || typeof text !== "string") return null;
|
|
206
|
+
const trimmed = text.trim();
|
|
207
|
+
if (!trimmed) return null;
|
|
208
|
+
try {
|
|
209
|
+
return JSON.parse(trimmed);
|
|
210
|
+
} catch {
|
|
211
|
+
// Some models wrap JSON in markdown code fences.
|
|
212
|
+
}
|
|
213
|
+
const fenceMatch = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
|
|
214
|
+
if (fenceMatch?.[1]) {
|
|
215
|
+
try {
|
|
216
|
+
return JSON.parse(fenceMatch[1].trim());
|
|
217
|
+
} catch {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const first = trimmed.indexOf("{");
|
|
222
|
+
const last = trimmed.lastIndexOf("}");
|
|
223
|
+
if (first >= 0 && last > first) {
|
|
224
|
+
try {
|
|
225
|
+
return JSON.parse(trimmed.slice(first, last + 1));
|
|
226
|
+
} catch {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function normalizeIndexList(value, maxLen) {
|
|
234
|
+
if (!Array.isArray(value)) return [];
|
|
235
|
+
const seen = new Set();
|
|
236
|
+
const out = [];
|
|
237
|
+
for (const v of value) {
|
|
238
|
+
if (!Number.isInteger(v)) continue;
|
|
239
|
+
if (v < 0 || v >= maxLen) continue;
|
|
240
|
+
if (seen.has(v)) continue;
|
|
241
|
+
seen.add(v);
|
|
242
|
+
out.push(v);
|
|
243
|
+
}
|
|
244
|
+
return out;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function buildRecallCandidates(data, cfg) {
|
|
248
|
+
const limit = Number.isFinite(cfg.recallFilterCandidateLimit) ? Math.max(0, cfg.recallFilterCandidateLimit) : 30;
|
|
249
|
+
const maxChars = Number.isFinite(cfg.recallFilterMaxItemChars) ? Math.max(80, cfg.recallFilterMaxItemChars) : 500;
|
|
250
|
+
const memoryList = Array.isArray(data?.memory_detail_list) ? data.memory_detail_list : [];
|
|
251
|
+
const preferenceList = Array.isArray(data?.preference_detail_list) ? data.preference_detail_list : [];
|
|
252
|
+
const toolList = Array.isArray(data?.tool_memory_detail_list) ? data.tool_memory_detail_list : [];
|
|
253
|
+
|
|
254
|
+
const memoryCandidates = memoryList.slice(0, limit).map((item, idx) => ({
|
|
255
|
+
idx,
|
|
256
|
+
text: truncate(item?.memory_value || item?.memory_key || "", maxChars),
|
|
257
|
+
relativity: item?.relativity,
|
|
258
|
+
}));
|
|
259
|
+
const preferenceCandidates = preferenceList.slice(0, limit).map((item, idx) => ({
|
|
260
|
+
idx,
|
|
261
|
+
text: truncate(item?.preference || "", maxChars),
|
|
262
|
+
relativity: item?.relativity,
|
|
263
|
+
preference_type: item?.preference_type || "",
|
|
264
|
+
}));
|
|
265
|
+
const toolCandidates = toolList.slice(0, limit).map((item, idx) => ({
|
|
266
|
+
idx,
|
|
267
|
+
text: truncate(item?.tool_value || "", maxChars),
|
|
268
|
+
relativity: item?.relativity,
|
|
269
|
+
}));
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
memoryList,
|
|
273
|
+
preferenceList,
|
|
274
|
+
toolList,
|
|
275
|
+
candidatePayload: {
|
|
276
|
+
memory: memoryCandidates,
|
|
277
|
+
preference: preferenceCandidates,
|
|
278
|
+
tool_memory: toolCandidates,
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function applyRecallDecision(data, decision, lists) {
|
|
284
|
+
const keep = decision?.keep || {};
|
|
285
|
+
const memoryIdx = normalizeIndexList(keep.memory, lists.memoryList.length);
|
|
286
|
+
const preferenceIdx = normalizeIndexList(keep.preference, lists.preferenceList.length);
|
|
287
|
+
const toolIdx = normalizeIndexList(keep.tool_memory, lists.toolList.length);
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
...data,
|
|
291
|
+
memory_detail_list: memoryIdx.map((idx) => lists.memoryList[idx]),
|
|
292
|
+
preference_detail_list: preferenceIdx.map((idx) => lists.preferenceList[idx]),
|
|
293
|
+
tool_memory_detail_list: toolIdx.map((idx) => lists.toolList[idx]),
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function callRecallFilterModel(cfg, userPrompt, candidatePayload) {
|
|
298
|
+
const headers = {
|
|
299
|
+
"Content-Type": "application/json",
|
|
300
|
+
};
|
|
301
|
+
if (cfg.recallFilterApiKey) {
|
|
302
|
+
headers.Authorization = `Bearer ${cfg.recallFilterApiKey}`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const modelInput = {
|
|
306
|
+
user_query: userPrompt,
|
|
307
|
+
candidate_memories: candidatePayload,
|
|
308
|
+
output_schema: {
|
|
309
|
+
keep: {
|
|
310
|
+
memory: ["number index"],
|
|
311
|
+
preference: ["number index"],
|
|
312
|
+
tool_memory: ["number index"],
|
|
313
|
+
},
|
|
314
|
+
reason: "optional short string",
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const body = {
|
|
319
|
+
model: cfg.recallFilterModel,
|
|
320
|
+
temperature: 0,
|
|
321
|
+
messages: [
|
|
322
|
+
{
|
|
323
|
+
role: "system",
|
|
324
|
+
content:
|
|
325
|
+
"You are a strict memory relevance judge. Return JSON only. Keep only items directly useful for answering current user query. If unsure, do not keep.",
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
role: "user",
|
|
329
|
+
content: JSON.stringify(modelInput),
|
|
330
|
+
},
|
|
331
|
+
],
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
let lastError;
|
|
335
|
+
const retries = Number.isFinite(cfg.recallFilterRetries) ? Math.max(0, cfg.recallFilterRetries) : 0;
|
|
336
|
+
const timeoutMs = Number.isFinite(cfg.recallFilterTimeoutMs) ? Math.max(1000, cfg.recallFilterTimeoutMs) : 6000;
|
|
337
|
+
|
|
338
|
+
for (let attempt = 0; attempt <= retries; attempt += 1) {
|
|
339
|
+
try {
|
|
340
|
+
const controller = new AbortController();
|
|
341
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
342
|
+
const res = await fetch(`${cfg.recallFilterBaseUrl}/chat/completions`, {
|
|
343
|
+
method: "POST",
|
|
344
|
+
headers,
|
|
345
|
+
body: JSON.stringify(body),
|
|
346
|
+
signal: controller.signal,
|
|
347
|
+
});
|
|
348
|
+
clearTimeout(timeoutId);
|
|
349
|
+
if (!res.ok) {
|
|
350
|
+
throw new Error(`HTTP ${res.status}`);
|
|
351
|
+
}
|
|
352
|
+
const json = await res.json();
|
|
353
|
+
const text = json?.choices?.[0]?.message?.content || "";
|
|
354
|
+
const parsed = parseModelJson(text);
|
|
355
|
+
if (!parsed || typeof parsed !== "object") {
|
|
356
|
+
throw new Error("invalid JSON output from recall filter model");
|
|
357
|
+
}
|
|
358
|
+
return parsed;
|
|
359
|
+
} catch (err) {
|
|
360
|
+
lastError = err;
|
|
361
|
+
if (attempt < retries) {
|
|
362
|
+
await sleep(120 * (attempt + 1));
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
throw lastError;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async function maybeFilterRecallData(cfg, data, userPrompt, log) {
|
|
370
|
+
if (!cfg.recallFilterEnabled) return data;
|
|
371
|
+
if (!cfg.recallFilterBaseUrl || !cfg.recallFilterModel) {
|
|
372
|
+
log.warn?.("[memos-cloud] recall filter enabled but missing recallFilterBaseUrl/recallFilterModel; skip filter");
|
|
373
|
+
return data;
|
|
374
|
+
}
|
|
375
|
+
const lists = buildRecallCandidates(data, cfg);
|
|
376
|
+
const hasCandidates =
|
|
377
|
+
lists.candidatePayload.memory.length > 0 ||
|
|
378
|
+
lists.candidatePayload.preference.length > 0 ||
|
|
379
|
+
lists.candidatePayload.tool_memory.length > 0;
|
|
380
|
+
if (!hasCandidates) return data;
|
|
381
|
+
|
|
382
|
+
try {
|
|
383
|
+
const decision = await callRecallFilterModel(cfg, userPrompt, lists.candidatePayload);
|
|
384
|
+
return applyRecallDecision(data, decision, lists);
|
|
385
|
+
} catch (err) {
|
|
386
|
+
log.warn?.(`[memos-cloud] recall filter failed: ${String(err)}`);
|
|
387
|
+
return cfg.recallFilterFailOpen ? data : { ...data, memory_detail_list: [], preference_detail_list: [], tool_memory_detail_list: [] };
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
199
391
|
export default {
|
|
200
392
|
id: "memos-cloud-openclaw-plugin",
|
|
201
393
|
name: "MemOS Cloud OpenClaw Plugin",
|
|
@@ -243,16 +435,17 @@ export default {
|
|
|
243
435
|
try {
|
|
244
436
|
const payload = buildSearchPayload(cfg, event.prompt, ctx);
|
|
245
437
|
const result = await searchMemory(cfg, payload);
|
|
246
|
-
const
|
|
438
|
+
const resultData = extractResultData(result);
|
|
439
|
+
if (!resultData) return;
|
|
440
|
+
const filteredData = await maybeFilterRecallData(cfg, resultData, event.prompt, log);
|
|
441
|
+
const hookResult = formatRecallHookResult({ data: filteredData }, {
|
|
247
442
|
wrapTagBlocks: true,
|
|
248
443
|
relativity: payload.relativity,
|
|
249
|
-
maxItemChars: cfg.maxItemChars
|
|
444
|
+
maxItemChars: cfg.maxItemChars,
|
|
250
445
|
});
|
|
251
|
-
if (!
|
|
446
|
+
if (!hookResult.appendSystemContext && !hookResult.prependContext) return;
|
|
252
447
|
|
|
253
|
-
return
|
|
254
|
-
prependContext: promptBlock,
|
|
255
|
-
};
|
|
448
|
+
return hookResult;
|
|
256
449
|
} catch (err) {
|
|
257
450
|
log.warn?.(`[memos-cloud] recall failed: ${String(err)}`);
|
|
258
451
|
}
|
package/lib/check-update.js
CHANGED
|
@@ -39,36 +39,6 @@ const ANSI = {
|
|
|
39
39
|
RED: "\x1b[31m"
|
|
40
40
|
};
|
|
41
41
|
|
|
42
|
-
function canExecuteCli(cliName, timeoutMs = 8000) {
|
|
43
|
-
return new Promise((resolve) => {
|
|
44
|
-
let done = false;
|
|
45
|
-
const child = spawn(cliName, ["--version"], { shell: true });
|
|
46
|
-
const timer = setTimeout(() => {
|
|
47
|
-
if (done) return;
|
|
48
|
-
done = true;
|
|
49
|
-
killProcessTree(child);
|
|
50
|
-
resolve({ ok: false, reason: `timeout after ${timeoutMs}ms` });
|
|
51
|
-
}, timeoutMs);
|
|
52
|
-
|
|
53
|
-
child.on("error", (err) => {
|
|
54
|
-
if (done) return;
|
|
55
|
-
done = true;
|
|
56
|
-
clearTimeout(timer);
|
|
57
|
-
resolve({ ok: false, reason: err?.message || String(err) });
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
child.on("close", (code) => {
|
|
61
|
-
if (done) return;
|
|
62
|
-
done = true;
|
|
63
|
-
clearTimeout(timer);
|
|
64
|
-
resolve({
|
|
65
|
-
ok: code === 0,
|
|
66
|
-
reason: code === 0 ? "" : `exit code ${code}`,
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
|
|
72
42
|
|
|
73
43
|
function getPackageVersion() {
|
|
74
44
|
try {
|
|
@@ -188,15 +158,6 @@ export function startUpdateChecker(log) {
|
|
|
188
158
|
return;
|
|
189
159
|
}
|
|
190
160
|
|
|
191
|
-
// Check if we have write permission to the plugin directory before attempting update
|
|
192
|
-
const pluginDir = path.join(__dirname, "..");
|
|
193
|
-
try {
|
|
194
|
-
fs.accessSync(pluginDir, fs.constants.W_OK);
|
|
195
|
-
} catch (err) {
|
|
196
|
-
log.warn?.(`${ANSI.YELLOW}[memos-cloud] Update available (${latestVersion}), but skipping auto-update due to missing write permissions in ${pluginDir}. Please run manually with sudo.${ANSI.RESET}`);
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
161
|
log.info?.(`${ANSI.YELLOW}[memos-cloud] Update available: ${currentVersion} -> ${latestVersion}. Updating in background...${ANSI.RESET}`);
|
|
201
162
|
|
|
202
163
|
let dotCount = 0;
|
|
@@ -216,14 +177,6 @@ export function startUpdateChecker(log) {
|
|
|
216
177
|
return "openclaw";
|
|
217
178
|
})();
|
|
218
179
|
|
|
219
|
-
const preflight = await canExecuteCli(cliName);
|
|
220
|
-
if (!preflight.ok) {
|
|
221
|
-
log.warn?.(
|
|
222
|
-
`${ANSI.YELLOW}[memos-cloud] Update available (${latestVersion}), but cannot execute CLI '${cliName}' (${preflight.reason}). Please update manually: ${cliName} plugins update memos-cloud-openclaw-plugin${ANSI.RESET}`,
|
|
223
|
-
);
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
180
|
isUpdating = true;
|
|
228
181
|
const spawnOpts = { shell: true };
|
|
229
182
|
// On Unix, detach the process so we can kill the entire process group on timeout
|
|
@@ -231,14 +184,6 @@ export function startUpdateChecker(log) {
|
|
|
231
184
|
spawnOpts.detached = true;
|
|
232
185
|
}
|
|
233
186
|
const child = spawn(cliName, ["plugins", "update", "memos-cloud-openclaw-plugin"], spawnOpts);
|
|
234
|
-
let finished = false;
|
|
235
|
-
const finishUpdateSequence = () => {
|
|
236
|
-
if (finished) return;
|
|
237
|
-
finished = true;
|
|
238
|
-
clearTimeout(updateTimeout);
|
|
239
|
-
clearInterval(progressInterval);
|
|
240
|
-
isUpdating = false;
|
|
241
|
-
};
|
|
242
187
|
|
|
243
188
|
// Timeout mechanism: forcefully kill the update process if it hangs for more than the configured timeout
|
|
244
189
|
const updateTimeout = setTimeout(() => {
|
|
@@ -248,7 +193,8 @@ export function startUpdateChecker(log) {
|
|
|
248
193
|
// Fallback: if kill failed and the close event never fires, forcefully release the lock after 5 seconds
|
|
249
194
|
setTimeout(() => {
|
|
250
195
|
if (isUpdating) {
|
|
251
|
-
|
|
196
|
+
clearInterval(progressInterval);
|
|
197
|
+
isUpdating = false;
|
|
252
198
|
}
|
|
253
199
|
}, 5000);
|
|
254
200
|
}, UPDATE_TIMEOUT);
|
|
@@ -274,7 +220,9 @@ export function startUpdateChecker(log) {
|
|
|
274
220
|
});
|
|
275
221
|
|
|
276
222
|
child.on("close", (code) => {
|
|
277
|
-
|
|
223
|
+
clearTimeout(updateTimeout);
|
|
224
|
+
clearInterval(progressInterval);
|
|
225
|
+
isUpdating = false;
|
|
278
226
|
|
|
279
227
|
// Wait for a brief moment to let file system sync if needed
|
|
280
228
|
setTimeout(() => {
|
|
@@ -289,13 +237,6 @@ export function startUpdateChecker(log) {
|
|
|
289
237
|
}, 1000); // Small 1-second buffer for file systems
|
|
290
238
|
});
|
|
291
239
|
|
|
292
|
-
child.on("error", (err) => {
|
|
293
|
-
finishUpdateSequence();
|
|
294
|
-
log.warn?.(
|
|
295
|
-
`${ANSI.RED}[memos-cloud] Failed to start auto-update process: ${err?.message || String(err)}. Please run manually: ${cliName} plugins update memos-cloud-openclaw-plugin${ANSI.RESET}`,
|
|
296
|
-
);
|
|
297
|
-
});
|
|
298
|
-
|
|
299
240
|
} catch (error) {
|
|
300
241
|
log.warn?.(`${ANSI.RED}[memos-cloud] Update check failed entirely: ${error.message}${ANSI.RESET}`);
|
|
301
242
|
}
|
package/lib/memos-cloud-api.js
CHANGED
|
@@ -27,7 +27,7 @@ function stripQuotes(value) {
|
|
|
27
27
|
return trimmed;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
function extractResultData(result) {
|
|
30
|
+
export function extractResultData(result) {
|
|
31
31
|
if (!result || typeof result !== "object") return null;
|
|
32
32
|
return result.data ?? result.data?.data ?? result.data?.result ?? null;
|
|
33
33
|
}
|
|
@@ -120,6 +120,12 @@ function parseBool(value, fallback) {
|
|
|
120
120
|
return fallback;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
function parseNumber(value, fallback) {
|
|
124
|
+
if (value === undefined || value === null || value === "") return fallback;
|
|
125
|
+
const n = Number(value);
|
|
126
|
+
return Number.isFinite(n) ? n : fallback;
|
|
127
|
+
}
|
|
128
|
+
|
|
123
129
|
export function buildConfig(pluginConfig = {}) {
|
|
124
130
|
const cfg = pluginConfig ?? {};
|
|
125
131
|
|
|
@@ -147,6 +153,15 @@ export function buildConfig(pluginConfig = {}) {
|
|
|
147
153
|
parseBool(loadEnvVar("MEMOS_MULTI_AGENT_MODE"), false),
|
|
148
154
|
);
|
|
149
155
|
|
|
156
|
+
const recallFilterEnabled = parseBool(
|
|
157
|
+
cfg.recallFilterEnabled,
|
|
158
|
+
parseBool(loadEnvVar("MEMOS_RECALL_FILTER_ENABLED"), false),
|
|
159
|
+
);
|
|
160
|
+
const recallFilterFailOpen = parseBool(
|
|
161
|
+
cfg.recallFilterFailOpen,
|
|
162
|
+
parseBool(loadEnvVar("MEMOS_RECALL_FILTER_FAIL_OPEN"), true),
|
|
163
|
+
);
|
|
164
|
+
|
|
150
165
|
return {
|
|
151
166
|
baseUrl: baseUrl.replace(/\/+$/, ""),
|
|
152
167
|
apiKey,
|
|
@@ -185,6 +200,21 @@ export function buildConfig(pluginConfig = {}) {
|
|
|
185
200
|
allowKnowledgebaseIds: cfg.allowKnowledgebaseIds ?? [],
|
|
186
201
|
asyncMode: cfg.asyncMode ?? true,
|
|
187
202
|
multiAgentMode,
|
|
203
|
+
recallFilterEnabled,
|
|
204
|
+
recallFilterBaseUrl:
|
|
205
|
+
(cfg.recallFilterBaseUrl ?? loadEnvVar("MEMOS_RECALL_FILTER_BASE_URL") ?? "").replace(/\/+$/, ""),
|
|
206
|
+
recallFilterApiKey: cfg.recallFilterApiKey ?? loadEnvVar("MEMOS_RECALL_FILTER_API_KEY") ?? "",
|
|
207
|
+
recallFilterModel: cfg.recallFilterModel ?? loadEnvVar("MEMOS_RECALL_FILTER_MODEL") ?? "",
|
|
208
|
+
recallFilterTimeoutMs: parseNumber(
|
|
209
|
+
cfg.recallFilterTimeoutMs ?? loadEnvVar("MEMOS_RECALL_FILTER_TIMEOUT_MS"),
|
|
210
|
+
6000,
|
|
211
|
+
),
|
|
212
|
+
recallFilterRetries: parseNumber(cfg.recallFilterRetries ?? loadEnvVar("MEMOS_RECALL_FILTER_RETRIES"), 0),
|
|
213
|
+
recallFilterCandidateLimit:
|
|
214
|
+
parseNumber(cfg.recallFilterCandidateLimit ?? loadEnvVar("MEMOS_RECALL_FILTER_CANDIDATE_LIMIT"), 30),
|
|
215
|
+
recallFilterMaxItemChars:
|
|
216
|
+
parseNumber(cfg.recallFilterMaxItemChars ?? loadEnvVar("MEMOS_RECALL_FILTER_MAX_ITEM_CHARS"), 500),
|
|
217
|
+
recallFilterFailOpen,
|
|
188
218
|
timeoutMs: cfg.timeoutMs ?? 5000,
|
|
189
219
|
retries: cfg.retries ?? 1,
|
|
190
220
|
throttleMs: cfg.throttleMs ?? 0,
|
|
@@ -295,9 +325,7 @@ function wrapCodeBlock(lines, options = {}) {
|
|
|
295
325
|
return ["```text", ...lines, "```"];
|
|
296
326
|
}
|
|
297
327
|
|
|
298
|
-
function
|
|
299
|
-
const now = options.currentTime ?? Date.now();
|
|
300
|
-
const nowText = formatTime(now) || formatTime(Date.now()) || "";
|
|
328
|
+
function buildMemorySections(data, options = {}) {
|
|
301
329
|
const memoryList = data?.memory_detail_list ?? [];
|
|
302
330
|
const preferenceList = data?.preference_detail_list ?? [];
|
|
303
331
|
|
|
@@ -325,8 +353,60 @@ function buildPromptFromData(data, options = {}) {
|
|
|
325
353
|
})
|
|
326
354
|
.filter(Boolean);
|
|
327
355
|
|
|
328
|
-
|
|
356
|
+
return { memoryLines, preferenceLines };
|
|
357
|
+
}
|
|
329
358
|
|
|
359
|
+
const STATIC_RECALL_SYSTEM_PROMPT = [
|
|
360
|
+
"# Role",
|
|
361
|
+
"",
|
|
362
|
+
"You are an intelligent assistant with long-term memory capabilities (MemOS Assistant). Your goal is to combine retrieved memory fragments to provide highly personalized, accurate, and logically rigorous responses.",
|
|
363
|
+
"",
|
|
364
|
+
"# System Context",
|
|
365
|
+
"",
|
|
366
|
+
"* Current Time: Use the runtime-provided current time as the baseline for freshness checks.",
|
|
367
|
+
"* Additional memory context for the current turn may be prepended before the original user query as a structured `<memories>` block.",
|
|
368
|
+
"",
|
|
369
|
+
"# Memory Data",
|
|
370
|
+
"",
|
|
371
|
+
'Below is the information retrieved by MemOS, categorized into "Facts" and "Preferences".',
|
|
372
|
+
"* **Facts**: May include user attributes, historical conversations, or third-party details.",
|
|
373
|
+
"* **Special Note**: Content tagged with '[assistant观点]' or '[模型总结]' represents **past AI inference**, **not** direct user statements.",
|
|
374
|
+
"* **Preferences**: The user's explicit or implicit requirements on response style, format, or reasoning.",
|
|
375
|
+
"",
|
|
376
|
+
"# Critical Protocol: Memory Safety",
|
|
377
|
+
"",
|
|
378
|
+
"Retrieved memories may contain **AI speculation**, **irrelevant noise**, or **wrong subject attribution**. You must strictly apply the **Four-Step Verdict**. If any step fails, **discard the memory**:",
|
|
379
|
+
"",
|
|
380
|
+
"1. **Source Verification**:",
|
|
381
|
+
"* **Core**: Distinguish direct user statements from AI inference.",
|
|
382
|
+
"* If a memory has tags like '[assistant观点]' or '[模型总结]', treat it as a **hypothesis**, not a user-grounded fact.",
|
|
383
|
+
"* *Counterexample*: If memory says '[assistant观点] User loves mangoes' but the user never said that, do not assume it as fact.",
|
|
384
|
+
"* **Principle: AI summaries are reference-only and have much lower authority than direct user statements.**",
|
|
385
|
+
"",
|
|
386
|
+
"2. **Attribution Check**:",
|
|
387
|
+
"* Is the subject in memory definitely the user?",
|
|
388
|
+
"* If the memory describes a **third party** (e.g., candidate, interviewee, fictional character, case data), never attribute it to the user.",
|
|
389
|
+
"",
|
|
390
|
+
"3. **Strong Relevance Check**:",
|
|
391
|
+
"* Does the memory directly help answer the current 'Original Query'?",
|
|
392
|
+
"* If it is only a keyword overlap with different context, ignore it.",
|
|
393
|
+
"",
|
|
394
|
+
"4. **Freshness Check**:",
|
|
395
|
+
"* If memory conflicts with the user's latest intent, prioritize the current 'Original Query' as the highest source of truth.",
|
|
396
|
+
"",
|
|
397
|
+
"# Instructions",
|
|
398
|
+
"",
|
|
399
|
+
"1. **Review**: Read '<facts>' first and apply the Four-Step Verdict to remove noise and unreliable AI inference.",
|
|
400
|
+
"2. **Execute**:",
|
|
401
|
+
" - Use only memories that pass filtering as context.",
|
|
402
|
+
" - Strictly follow style requirements from '<preferences>'.",
|
|
403
|
+
"3. **Output**: Answer directly. Never mention internal terms such as \"memory store\", \"retrieval\", or \"AI opinions\".",
|
|
404
|
+
"4. **Attention**: Additional memory context may already be provided before the original user query. Do not read from or write to local `MEMORY.md` or `memory/*` files for reference, as they may be outdated or irrelevant to the current query.",
|
|
405
|
+
].join("\n");
|
|
406
|
+
|
|
407
|
+
function buildMemoryPrependBlock(data, options = {}) {
|
|
408
|
+
const { memoryLines, preferenceLines } = buildMemorySections(data, options);
|
|
409
|
+
const hasContent = memoryLines.length > 0 || preferenceLines.length > 0;
|
|
330
410
|
if (!hasContent) return "";
|
|
331
411
|
|
|
332
412
|
const memoriesBlock = [
|
|
@@ -340,57 +420,17 @@ function buildPromptFromData(data, options = {}) {
|
|
|
340
420
|
"</memories>",
|
|
341
421
|
];
|
|
342
422
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
"",
|
|
346
|
-
"You are an intelligent assistant with long-term memory capabilities (MemOS Assistant). Your goal is to combine retrieved memory fragments to provide highly personalized, accurate, and logically rigorous responses.",
|
|
347
|
-
"",
|
|
348
|
-
"# System Context",
|
|
349
|
-
"",
|
|
350
|
-
`* Current Time: ${nowText} (Use this as the baseline for freshness checks)`,
|
|
351
|
-
"",
|
|
352
|
-
"# Memory Data",
|
|
353
|
-
"",
|
|
354
|
-
'Below is the information retrieved by MemOS, categorized into "Facts" and "Preferences".',
|
|
355
|
-
"* **Facts**: May include user attributes, historical conversations, or third-party details.",
|
|
356
|
-
"* **Special Note**: Content tagged with '[assistant观点]' or '[模型总结]' represents **past AI inference**, **not** direct user statements.",
|
|
357
|
-
"* **Preferences**: The user's explicit or implicit requirements on response style, format, or reasoning.",
|
|
358
|
-
"",
|
|
359
|
-
...wrapCodeBlock(memoriesBlock, options),
|
|
360
|
-
"",
|
|
361
|
-
"# Critical Protocol: Memory Safety",
|
|
362
|
-
"",
|
|
363
|
-
"Retrieved memories may contain **AI speculation**, **irrelevant noise**, or **wrong subject attribution**. You must strictly apply the **Four-Step Verdict**. If any step fails, **discard the memory**:",
|
|
364
|
-
"",
|
|
365
|
-
"1. **Source Verification**:",
|
|
366
|
-
"* **Core**: Distinguish direct user statements from AI inference.",
|
|
367
|
-
"* If a memory has tags like '[assistant观点]' or '[模型总结]', treat it as a **hypothesis**, not a user-grounded fact.",
|
|
368
|
-
"* *Counterexample*: If memory says '[assistant观点] User loves mangoes' but the user never said that, do not assume it as fact.",
|
|
369
|
-
"* **Principle: AI summaries are reference-only and have much lower authority than direct user statements.**",
|
|
370
|
-
"",
|
|
371
|
-
"2. **Attribution Check**:",
|
|
372
|
-
"* Is the subject in memory definitely the user?",
|
|
373
|
-
"* If the memory describes a **third party** (e.g., candidate, interviewee, fictional character, case data), never attribute it to the user.",
|
|
374
|
-
"",
|
|
375
|
-
"3. **Strong Relevance Check**:",
|
|
376
|
-
"* Does the memory directly help answer the current 'Original Query'?",
|
|
377
|
-
"* If it is only a keyword overlap with different context, ignore it.",
|
|
378
|
-
"",
|
|
379
|
-
"4. **Freshness Check**:",
|
|
380
|
-
"* If memory conflicts with the user's latest intent, prioritize the current 'Original Query' as the highest source of truth.",
|
|
381
|
-
"",
|
|
382
|
-
"# Instructions",
|
|
383
|
-
"",
|
|
384
|
-
"1. **Review**: Read '<facts>' first and apply the Four-Step Verdict to remove noise and unreliable AI inference.",
|
|
385
|
-
"2. **Execute**:",
|
|
386
|
-
" - Use only memories that pass filtering as context.",
|
|
387
|
-
" - Strictly follow style requirements from '<preferences>'.",
|
|
388
|
-
"3. **Output**: Answer directly. Never mention internal terms such as \"memory store\", \"retrieval\", or \"AI opinions\".",
|
|
389
|
-
"4. **Attention**: Additional memory context is already provided. Do not read from or write to local `MEMORY.md` or `memory/*` files for reference, as they may be outdated or irrelevant to the current query.",
|
|
390
|
-
USER_QUERY_MARKER,
|
|
391
|
-
];
|
|
423
|
+
return [...wrapCodeBlock(memoriesBlock, options), "", USER_QUERY_MARKER].join("\n");
|
|
424
|
+
}
|
|
392
425
|
|
|
393
|
-
|
|
426
|
+
export function formatPromptBlockFromData(data, options = {}) {
|
|
427
|
+
if (!data || typeof data !== "object") return "";
|
|
428
|
+
return buildMemoryPrependBlock(data, options);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export function formatPromptBlock(result, options = {}) {
|
|
432
|
+
const data = extractResultData(result);
|
|
433
|
+
return formatPromptBlockFromData(data, options);
|
|
394
434
|
}
|
|
395
435
|
|
|
396
436
|
export function formatContextBlock(result, options = {}) {
|
|
@@ -438,10 +478,20 @@ export function formatContextBlock(result, options = {}) {
|
|
|
438
478
|
return lines.length > 0 ? lines.join("\n") : "";
|
|
439
479
|
}
|
|
440
480
|
|
|
441
|
-
export function
|
|
481
|
+
export function formatRecallHookResult(result, options = {}) {
|
|
442
482
|
const data = extractResultData(result);
|
|
443
|
-
if (!data)
|
|
444
|
-
|
|
483
|
+
if (!data) {
|
|
484
|
+
return {
|
|
485
|
+
appendSystemContext: "",
|
|
486
|
+
prependContext: "",
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return {
|
|
491
|
+
// Keep this system addendum byte-stable across turns so provider-side prefix caching can hit.
|
|
492
|
+
appendSystemContext: STATIC_RECALL_SYSTEM_PROMPT,
|
|
493
|
+
prependContext: buildMemoryPrependBlock(data, options),
|
|
494
|
+
};
|
|
445
495
|
}
|
|
446
496
|
|
|
447
497
|
function truncate(text, maxLen) {
|
package/moltbot.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "memos-cloud-openclaw-plugin",
|
|
3
3
|
"name": "MemOS Cloud OpenClaw Plugin",
|
|
4
4
|
"description": "MemOS Cloud recall + add memory via lifecycle hooks",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.9",
|
|
6
6
|
"kind": "lifecycle",
|
|
7
7
|
"main": "./index.js",
|
|
8
8
|
"configSchema": {
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "memos-cloud-openclaw-plugin",
|
|
3
3
|
"name": "MemOS Cloud OpenClaw Plugin",
|
|
4
4
|
"description": "MemOS Cloud recall + add memory via lifecycle hooks",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.9",
|
|
6
6
|
"kind": "lifecycle",
|
|
7
7
|
"main": "./index.js",
|
|
8
8
|
"configSchema": {
|
package/package.json
CHANGED