@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 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: `6000`)
105
- - `MEMOS_RECALL_FILTER_RETRIES` (default: `0`)
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": 6000,
150
- "recallFilterRetries": 0,
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`(默认 `6000`)
107
- - `MEMOS_RECALL_FILTER_RETRIES`(默认 `0`)
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": 6000,
150
- "recallFilterRetries": 0,
149
+ "recallFilterTimeoutMs": 30000,
150
+ "recallFilterRetries": 1,
151
151
  "recallFilterCandidateLimit": 30,
152
152
  "recallFilterMaxItemChars": 500,
153
153
  "recallFilterFailOpen": true,
@@ -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-beta.0",
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) : 0;
330
- const timeoutMs = Number.isFinite(cfg.recallFilterTimeoutMs) ? Math.max(1000, cfg.recallFilterTimeoutMs) : 6000;
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
- const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
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
- lastError = err;
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
- return applyRecallDecision(data, decision, lists);
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: [] };
@@ -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
- 6000,
249
+ 30000,
250
250
  ),
251
- recallFilterRetries: parseNumber(cfg.recallFilterRetries ?? loadEnvVar("MEMOS_RECALL_FILTER_RETRIES"), 0),
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?.create_time);
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?.create_time);
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}`;
@@ -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-beta.0",
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",
@@ -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-beta.0",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memtensor/memos-cloud-openclaw-plugin",
3
- "version": "0.1.10-beta.4",
3
+ "version": "0.1.10",
4
4
  "description": "OpenClaw lifecycle plugin for MemOS Cloud (add + recall memory)",
5
5
  "scripts": {
6
6
  "sync-version": "node scripts/sync-version.js",
@@ -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
+ });