@memtensor/memos-cloud-openclaw-plugin 0.1.10-beta.4 → 0.1.10
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 +4 -4
- package/README_ZH.md +4 -4
- package/clawdbot.plugin.json +42 -2
- package/index.js +19 -6
- package/lib/memos-cloud-api.js +8 -4
- package/moltbot.plugin.json +42 -2
- package/openclaw.plugin.json +42 -2
- package/package.json +1 -1
- package/test/query-strip.test.mjs +41 -0
package/README.md
CHANGED
|
@@ -101,8 +101,8 @@ MEMOS_API_KEY=YOUR_TOKEN
|
|
|
101
101
|
- `MEMOS_RECALL_FILTER_BASE_URL` (OpenAI-compatible base URL, e.g. `http://127.0.0.1:11434/v1`)
|
|
102
102
|
- `MEMOS_RECALL_FILTER_API_KEY` (optional; required if your endpoint needs auth)
|
|
103
103
|
- `MEMOS_RECALL_FILTER_MODEL` (model name used to filter recall candidates)
|
|
104
|
-
- `MEMOS_RECALL_FILTER_TIMEOUT_MS` (default: `
|
|
105
|
-
- `MEMOS_RECALL_FILTER_RETRIES` (default: `
|
|
104
|
+
- `MEMOS_RECALL_FILTER_TIMEOUT_MS` (default: `30000`)
|
|
105
|
+
- `MEMOS_RECALL_FILTER_RETRIES` (default: `1`)
|
|
106
106
|
- `MEMOS_RECALL_FILTER_CANDIDATE_LIMIT` (default: `30` per category)
|
|
107
107
|
- `MEMOS_RECALL_FILTER_MAX_ITEM_CHARS` (default: `500`)
|
|
108
108
|
- `MEMOS_RECALL_FILTER_FAIL_OPEN` (default: `true`; fallback to unfiltered recall on failure)
|
|
@@ -146,8 +146,8 @@ In `plugins.entries.memos-cloud-openclaw-plugin.config`:
|
|
|
146
146
|
"recallFilterBaseUrl": "http://127.0.0.1:11434/v1",
|
|
147
147
|
"recallFilterApiKey": "",
|
|
148
148
|
"recallFilterModel": "qwen2.5:7b",
|
|
149
|
-
"recallFilterTimeoutMs":
|
|
150
|
-
"recallFilterRetries":
|
|
149
|
+
"recallFilterTimeoutMs": 30000,
|
|
150
|
+
"recallFilterRetries": 1,
|
|
151
151
|
"recallFilterCandidateLimit": 30,
|
|
152
152
|
"recallFilterMaxItemChars": 500,
|
|
153
153
|
"recallFilterFailOpen": true,
|
package/README_ZH.md
CHANGED
|
@@ -103,8 +103,8 @@ MEMOS_API_KEY=YOUR_TOKEN
|
|
|
103
103
|
- `MEMOS_RECALL_FILTER_BASE_URL`(OpenAI 兼容接口,例如 `http://127.0.0.1:11434/v1`)
|
|
104
104
|
- `MEMOS_RECALL_FILTER_API_KEY`(可选,若你的接口需要鉴权)
|
|
105
105
|
- `MEMOS_RECALL_FILTER_MODEL`(用于筛选记忆的模型名)
|
|
106
|
-
- `MEMOS_RECALL_FILTER_TIMEOUT_MS`(默认 `
|
|
107
|
-
- `MEMOS_RECALL_FILTER_RETRIES`(默认 `
|
|
106
|
+
- `MEMOS_RECALL_FILTER_TIMEOUT_MS`(默认 `30000`)
|
|
107
|
+
- `MEMOS_RECALL_FILTER_RETRIES`(默认 `1`)
|
|
108
108
|
- `MEMOS_RECALL_FILTER_CANDIDATE_LIMIT`(默认每类 `30` 条)
|
|
109
109
|
- `MEMOS_RECALL_FILTER_MAX_ITEM_CHARS`(默认 `500`)
|
|
110
110
|
- `MEMOS_RECALL_FILTER_FAIL_OPEN`(默认 `true`;筛选失败时回退为“不过滤”)
|
|
@@ -146,8 +146,8 @@ MEMOS_API_KEY=YOUR_TOKEN
|
|
|
146
146
|
"recallFilterBaseUrl": "http://127.0.0.1:11434/v1",
|
|
147
147
|
"recallFilterApiKey": "",
|
|
148
148
|
"recallFilterModel": "qwen2.5:7b",
|
|
149
|
-
"recallFilterTimeoutMs":
|
|
150
|
-
"recallFilterRetries":
|
|
149
|
+
"recallFilterTimeoutMs": 30000,
|
|
150
|
+
"recallFilterRetries": 1,
|
|
151
151
|
"recallFilterCandidateLimit": 30,
|
|
152
152
|
"recallFilterMaxItemChars": 500,
|
|
153
153
|
"recallFilterFailOpen": true,
|
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.10
|
|
5
|
+
"version": "0.1.10",
|
|
6
6
|
"kind": "lifecycle",
|
|
7
7
|
"main": "./index.js",
|
|
8
8
|
"configSchema": {
|
|
@@ -109,7 +109,47 @@
|
|
|
109
109
|
},
|
|
110
110
|
"filter": {
|
|
111
111
|
"type": "object",
|
|
112
|
-
"description": "MemOS search filter"
|
|
112
|
+
"description": "MemOS search filter",
|
|
113
|
+
"additionalProperties": true
|
|
114
|
+
},
|
|
115
|
+
"relativity": {
|
|
116
|
+
"type": "number",
|
|
117
|
+
"description": "Search relativity threshold",
|
|
118
|
+
"default": 0.45
|
|
119
|
+
},
|
|
120
|
+
"recallFilterEnabled": {
|
|
121
|
+
"type": "boolean",
|
|
122
|
+
"default": false
|
|
123
|
+
},
|
|
124
|
+
"recallFilterBaseUrl": {
|
|
125
|
+
"type": "string",
|
|
126
|
+
"description": "OpenAI-compatible API base URL for recall filtering"
|
|
127
|
+
},
|
|
128
|
+
"recallFilterApiKey": {
|
|
129
|
+
"type": "string"
|
|
130
|
+
},
|
|
131
|
+
"recallFilterModel": {
|
|
132
|
+
"type": "string"
|
|
133
|
+
},
|
|
134
|
+
"recallFilterTimeoutMs": {
|
|
135
|
+
"type": "integer",
|
|
136
|
+
"default": 30000
|
|
137
|
+
},
|
|
138
|
+
"recallFilterRetries": {
|
|
139
|
+
"type": "integer",
|
|
140
|
+
"default": 1
|
|
141
|
+
},
|
|
142
|
+
"recallFilterCandidateLimit": {
|
|
143
|
+
"type": "integer",
|
|
144
|
+
"default": 30
|
|
145
|
+
},
|
|
146
|
+
"recallFilterMaxItemChars": {
|
|
147
|
+
"type": "integer",
|
|
148
|
+
"default": 500
|
|
149
|
+
},
|
|
150
|
+
"recallFilterFailOpen": {
|
|
151
|
+
"type": "boolean",
|
|
152
|
+
"default": true
|
|
113
153
|
},
|
|
114
154
|
"knowledgebaseIds": {
|
|
115
155
|
"type": "array",
|
package/index.js
CHANGED
|
@@ -326,20 +326,20 @@ async function callRecallFilterModel(cfg, userPrompt, candidatePayload) {
|
|
|
326
326
|
};
|
|
327
327
|
|
|
328
328
|
let lastError;
|
|
329
|
-
const retries = Number.isFinite(cfg.recallFilterRetries) ? Math.max(0, cfg.recallFilterRetries) :
|
|
330
|
-
const timeoutMs = Number.isFinite(cfg.recallFilterTimeoutMs) ? Math.max(1000, cfg.recallFilterTimeoutMs) :
|
|
329
|
+
const retries = Number.isFinite(cfg.recallFilterRetries) ? Math.max(0, cfg.recallFilterRetries) : 1;
|
|
330
|
+
const timeoutMs = Number.isFinite(cfg.recallFilterTimeoutMs) ? Math.max(1000, cfg.recallFilterTimeoutMs) : 30000;
|
|
331
331
|
|
|
332
332
|
for (let attempt = 0; attempt <= retries; attempt += 1) {
|
|
333
|
+
let timeoutId;
|
|
333
334
|
try {
|
|
334
335
|
const controller = new AbortController();
|
|
335
|
-
|
|
336
|
+
timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
336
337
|
const res = await fetch(`${cfg.recallFilterBaseUrl}/chat/completions`, {
|
|
337
338
|
method: "POST",
|
|
338
339
|
headers,
|
|
339
340
|
body: JSON.stringify(body),
|
|
340
341
|
signal: controller.signal,
|
|
341
342
|
});
|
|
342
|
-
clearTimeout(timeoutId);
|
|
343
343
|
if (!res.ok) {
|
|
344
344
|
throw new Error(`HTTP ${res.status}`);
|
|
345
345
|
}
|
|
@@ -351,10 +351,17 @@ async function callRecallFilterModel(cfg, userPrompt, candidatePayload) {
|
|
|
351
351
|
}
|
|
352
352
|
return parsed;
|
|
353
353
|
} catch (err) {
|
|
354
|
-
|
|
354
|
+
const isAbort = err?.name === "AbortError" || /aborted/i.test(String(err?.message ?? err));
|
|
355
|
+
lastError = isAbort
|
|
356
|
+
? new Error(
|
|
357
|
+
`timed out after ${timeoutMs}ms (raise recallFilterTimeoutMs; local LLMs often need 30s+ on cold start)`,
|
|
358
|
+
)
|
|
359
|
+
: err;
|
|
355
360
|
if (attempt < retries) {
|
|
356
361
|
await sleep(120 * (attempt + 1));
|
|
357
362
|
}
|
|
363
|
+
} finally {
|
|
364
|
+
if (timeoutId !== undefined) clearTimeout(timeoutId);
|
|
358
365
|
}
|
|
359
366
|
}
|
|
360
367
|
throw lastError;
|
|
@@ -375,7 +382,13 @@ async function maybeFilterRecallData(cfg, data, userPrompt, log) {
|
|
|
375
382
|
|
|
376
383
|
try {
|
|
377
384
|
const decision = await callRecallFilterModel(cfg, userPrompt, lists.candidatePayload);
|
|
378
|
-
|
|
385
|
+
const filtered = applyRecallDecision(data, decision, lists);
|
|
386
|
+
log.info?.(
|
|
387
|
+
`[memos-cloud] recall filter applied: memory ${lists.memoryList.length}->${filtered.memory_detail_list?.length ?? 0}, ` +
|
|
388
|
+
`preference ${lists.preferenceList.length}->${filtered.preference_detail_list?.length ?? 0}, ` +
|
|
389
|
+
`tool_memory ${lists.toolList.length}->${filtered.tool_memory_detail_list?.length ?? 0}`,
|
|
390
|
+
);
|
|
391
|
+
return filtered;
|
|
379
392
|
} catch (err) {
|
|
380
393
|
log.warn?.(`[memos-cloud] recall filter failed: ${String(err)}`);
|
|
381
394
|
return cfg.recallFilterFailOpen ? data : { ...data, memory_detail_list: [], preference_detail_list: [], tool_memory_detail_list: [] };
|
package/lib/memos-cloud-api.js
CHANGED
|
@@ -246,9 +246,9 @@ export function buildConfig(pluginConfig = {}) {
|
|
|
246
246
|
recallFilterModel: cfg.recallFilterModel ?? loadEnvVar("MEMOS_RECALL_FILTER_MODEL") ?? "",
|
|
247
247
|
recallFilterTimeoutMs: parseNumber(
|
|
248
248
|
cfg.recallFilterTimeoutMs ?? loadEnvVar("MEMOS_RECALL_FILTER_TIMEOUT_MS"),
|
|
249
|
-
|
|
249
|
+
30000,
|
|
250
250
|
),
|
|
251
|
-
recallFilterRetries: parseNumber(cfg.recallFilterRetries ?? loadEnvVar("MEMOS_RECALL_FILTER_RETRIES"),
|
|
251
|
+
recallFilterRetries: parseNumber(cfg.recallFilterRetries ?? loadEnvVar("MEMOS_RECALL_FILTER_RETRIES"), 1),
|
|
252
252
|
recallFilterCandidateLimit:
|
|
253
253
|
parseNumber(cfg.recallFilterCandidateLimit ?? loadEnvVar("MEMOS_RECALL_FILTER_CANDIDATE_LIMIT"), 30),
|
|
254
254
|
recallFilterMaxItemChars:
|
|
@@ -518,12 +518,16 @@ function sanitizeInlineText(text) {
|
|
|
518
518
|
return String(text).replace(/\r?\n+/g, " ").trim();
|
|
519
519
|
}
|
|
520
520
|
|
|
521
|
+
function resolveDisplayTime(item) {
|
|
522
|
+
return item?.update_time ?? item?.create_time;
|
|
523
|
+
}
|
|
524
|
+
|
|
521
525
|
function formatMemoryLine(item, text, options = {}) {
|
|
522
526
|
const cleaned = sanitizeInlineText(text);
|
|
523
527
|
if (!cleaned) return "";
|
|
524
528
|
const maxChars = options.maxItemChars;
|
|
525
529
|
const truncated = truncate(cleaned, maxChars);
|
|
526
|
-
const time = formatTime(item
|
|
530
|
+
const time = formatTime(resolveDisplayTime(item));
|
|
527
531
|
if (time) return ` -[${time}] ${truncated}`;
|
|
528
532
|
return ` - ${truncated}`;
|
|
529
533
|
}
|
|
@@ -533,7 +537,7 @@ function formatPreferenceLine(item, text, options = {}) {
|
|
|
533
537
|
if (!cleaned) return "";
|
|
534
538
|
const maxChars = options.maxItemChars;
|
|
535
539
|
const truncated = truncate(cleaned, maxChars);
|
|
536
|
-
const time = formatTime(item
|
|
540
|
+
const time = formatTime(resolveDisplayTime(item));
|
|
537
541
|
const type = normalizePreferenceType(item?.preference_type);
|
|
538
542
|
const typeLabel = type ? ` [${type}]` : "";
|
|
539
543
|
if (time) return ` -[${time}]${typeLabel} ${truncated}`;
|
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.10
|
|
5
|
+
"version": "0.1.10",
|
|
6
6
|
"kind": "lifecycle",
|
|
7
7
|
"main": "./index.js",
|
|
8
8
|
"configSchema": {
|
|
@@ -109,7 +109,47 @@
|
|
|
109
109
|
},
|
|
110
110
|
"filter": {
|
|
111
111
|
"type": "object",
|
|
112
|
-
"description": "MemOS search filter"
|
|
112
|
+
"description": "MemOS search filter",
|
|
113
|
+
"additionalProperties": true
|
|
114
|
+
},
|
|
115
|
+
"relativity": {
|
|
116
|
+
"type": "number",
|
|
117
|
+
"description": "Search relativity threshold",
|
|
118
|
+
"default": 0.45
|
|
119
|
+
},
|
|
120
|
+
"recallFilterEnabled": {
|
|
121
|
+
"type": "boolean",
|
|
122
|
+
"default": false
|
|
123
|
+
},
|
|
124
|
+
"recallFilterBaseUrl": {
|
|
125
|
+
"type": "string",
|
|
126
|
+
"description": "OpenAI-compatible API base URL for recall filtering"
|
|
127
|
+
},
|
|
128
|
+
"recallFilterApiKey": {
|
|
129
|
+
"type": "string"
|
|
130
|
+
},
|
|
131
|
+
"recallFilterModel": {
|
|
132
|
+
"type": "string"
|
|
133
|
+
},
|
|
134
|
+
"recallFilterTimeoutMs": {
|
|
135
|
+
"type": "integer",
|
|
136
|
+
"default": 30000
|
|
137
|
+
},
|
|
138
|
+
"recallFilterRetries": {
|
|
139
|
+
"type": "integer",
|
|
140
|
+
"default": 1
|
|
141
|
+
},
|
|
142
|
+
"recallFilterCandidateLimit": {
|
|
143
|
+
"type": "integer",
|
|
144
|
+
"default": 30
|
|
145
|
+
},
|
|
146
|
+
"recallFilterMaxItemChars": {
|
|
147
|
+
"type": "integer",
|
|
148
|
+
"default": 500
|
|
149
|
+
},
|
|
150
|
+
"recallFilterFailOpen": {
|
|
151
|
+
"type": "boolean",
|
|
152
|
+
"default": true
|
|
113
153
|
},
|
|
114
154
|
"knowledgebaseIds": {
|
|
115
155
|
"type": "array",
|
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.10
|
|
5
|
+
"version": "0.1.10",
|
|
6
6
|
"kind": "lifecycle",
|
|
7
7
|
"main": "./index.js",
|
|
8
8
|
"configSchema": {
|
|
@@ -109,7 +109,47 @@
|
|
|
109
109
|
},
|
|
110
110
|
"filter": {
|
|
111
111
|
"type": "object",
|
|
112
|
-
"description": "MemOS search filter"
|
|
112
|
+
"description": "MemOS search filter",
|
|
113
|
+
"additionalProperties": true
|
|
114
|
+
},
|
|
115
|
+
"relativity": {
|
|
116
|
+
"type": "number",
|
|
117
|
+
"description": "Search relativity threshold",
|
|
118
|
+
"default": 0.45
|
|
119
|
+
},
|
|
120
|
+
"recallFilterEnabled": {
|
|
121
|
+
"type": "boolean",
|
|
122
|
+
"default": false
|
|
123
|
+
},
|
|
124
|
+
"recallFilterBaseUrl": {
|
|
125
|
+
"type": "string",
|
|
126
|
+
"description": "OpenAI-compatible API base URL for recall filtering"
|
|
127
|
+
},
|
|
128
|
+
"recallFilterApiKey": {
|
|
129
|
+
"type": "string"
|
|
130
|
+
},
|
|
131
|
+
"recallFilterModel": {
|
|
132
|
+
"type": "string"
|
|
133
|
+
},
|
|
134
|
+
"recallFilterTimeoutMs": {
|
|
135
|
+
"type": "integer",
|
|
136
|
+
"default": 30000
|
|
137
|
+
},
|
|
138
|
+
"recallFilterRetries": {
|
|
139
|
+
"type": "integer",
|
|
140
|
+
"default": 1
|
|
141
|
+
},
|
|
142
|
+
"recallFilterCandidateLimit": {
|
|
143
|
+
"type": "integer",
|
|
144
|
+
"default": 30
|
|
145
|
+
},
|
|
146
|
+
"recallFilterMaxItemChars": {
|
|
147
|
+
"type": "integer",
|
|
148
|
+
"default": 500
|
|
149
|
+
},
|
|
150
|
+
"recallFilterFailOpen": {
|
|
151
|
+
"type": "boolean",
|
|
152
|
+
"default": true
|
|
113
153
|
},
|
|
114
154
|
"knowledgebaseIds": {
|
|
115
155
|
"type": "array",
|
package/package.json
CHANGED
|
@@ -3,6 +3,7 @@ import assert from "node:assert/strict";
|
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
USER_QUERY_MARKER,
|
|
6
|
+
formatPromptBlockFromData,
|
|
6
7
|
sanitizeAddMessagePayload,
|
|
7
8
|
sanitizeSearchPayload,
|
|
8
9
|
stripOpenClawInjectedPrefix,
|
|
@@ -338,3 +339,43 @@ test("sanitizes only user messages in add payload", () => {
|
|
|
338
339
|
],
|
|
339
340
|
});
|
|
340
341
|
});
|
|
342
|
+
|
|
343
|
+
test("formatPromptBlockFromData prefers update_time over create_time", () => {
|
|
344
|
+
const block = formatPromptBlockFromData({
|
|
345
|
+
memory_detail_list: [
|
|
346
|
+
{
|
|
347
|
+
memory_value: "更新后的记忆",
|
|
348
|
+
create_time: "2026-03-17 10:00",
|
|
349
|
+
update_time: "2026-03-18 16:20",
|
|
350
|
+
relativity: 0.9,
|
|
351
|
+
},
|
|
352
|
+
],
|
|
353
|
+
preference_detail_list: [
|
|
354
|
+
{
|
|
355
|
+
preference: "更新后的偏好",
|
|
356
|
+
preference_type: "explicit",
|
|
357
|
+
create_time: "2026-03-17 11:00",
|
|
358
|
+
update_time: "2026-03-18 16:21",
|
|
359
|
+
relativity: 0.9,
|
|
360
|
+
},
|
|
361
|
+
],
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
assert.match(block, /\-\[2026-03-18 16:20\] 更新后的记忆/);
|
|
365
|
+
assert.match(block, /\-\[2026-03-18 16:21\] \[Explicit Preference\] 更新后的偏好/);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
test("formatPromptBlockFromData falls back to create_time when update_time is missing", () => {
|
|
369
|
+
const block = formatPromptBlockFromData({
|
|
370
|
+
memory_detail_list: [
|
|
371
|
+
{
|
|
372
|
+
memory_value: "只有创建时间",
|
|
373
|
+
create_time: "2026-03-18 09:30",
|
|
374
|
+
relativity: 0.9,
|
|
375
|
+
},
|
|
376
|
+
],
|
|
377
|
+
preference_detail_list: [],
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
assert.match(block, /\-\[2026-03-18 09:30\] 只有创建时间/);
|
|
381
|
+
});
|