@memtensor/memos-cloud-openclaw-plugin 0.1.9 → 0.1.10-beta.0

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.
@@ -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.9",
5
+ "version": "0.1.10-beta.0",
6
6
  "kind": "lifecycle",
7
7
  "main": "./index.js",
8
8
  "configSchema": {
package/index.js CHANGED
@@ -5,8 +5,8 @@ import {
5
5
  extractResultData,
6
6
  extractText,
7
7
  formatRecallHookResult,
8
- USER_QUERY_MARKER,
9
8
  searchMemory,
9
+ stripOpenClawInjectedPrefix,
10
10
  } from "./lib/memos-cloud-api.js";
11
11
  import { startUpdateChecker } from "./lib/check-update.js";
12
12
  let lastCaptureTime = 0;
@@ -33,13 +33,6 @@ function warnMissingApiKey(log, context) {
33
33
  );
34
34
  }
35
35
 
36
- function stripPrependedPrompt(content) {
37
- if (!content) return content;
38
- const idx = content.lastIndexOf(USER_QUERY_MARKER);
39
- if (idx === -1) return content;
40
- return content.slice(idx + USER_QUERY_MARKER.length).trimStart();
41
- }
42
-
43
36
  function getCounterSuffix(sessionKey) {
44
37
  if (!sessionKey) return "";
45
38
  const current = conversationCounters.get(sessionKey) ?? 0;
@@ -73,7 +66,8 @@ function resolveConversationId(cfg, ctx) {
73
66
  }
74
67
 
75
68
  function buildSearchPayload(cfg, prompt, ctx) {
76
- const queryRaw = `${cfg.queryPrefix || ""}${prompt}`;
69
+ const cleanPrompt = stripOpenClawInjectedPrefix(prompt);
70
+ const queryRaw = `${cfg.queryPrefix || ""}${cleanPrompt}`;
77
71
  const query =
78
72
  Number.isFinite(cfg.maxQueryChars) && cfg.maxQueryChars > 0
79
73
  ? queryRaw.slice(0, cfg.maxQueryChars)
@@ -162,7 +156,7 @@ function pickLastTurnMessages(messages, cfg) {
162
156
  for (const msg of slice) {
163
157
  if (!msg || !msg.role) continue;
164
158
  if (msg.role === "user") {
165
- const content = stripPrependedPrompt(extractText(msg.content));
159
+ const content = stripOpenClawInjectedPrefix(extractText(msg.content));
166
160
  if (content) results.push({ role: "user", content: truncate(content, cfg.maxMessageChars) });
167
161
  continue;
168
162
  }
@@ -180,7 +174,7 @@ function pickFullSessionMessages(messages, cfg) {
180
174
  for (const msg of messages) {
181
175
  if (!msg || !msg.role) continue;
182
176
  if (msg.role === "user") {
183
- const content = stripPrependedPrompt(extractText(msg.content));
177
+ const content = stripOpenClawInjectedPrefix(extractText(msg.content));
184
178
  if (content) results.push({ role: "user", content: truncate(content, cfg.maxMessageChars) });
185
179
  }
186
180
  if (msg.role === "assistant" && cfg.includeAssistant) {
@@ -426,18 +420,19 @@ export default {
426
420
 
427
421
  api.on("before_agent_start", async (event, ctx) => {
428
422
  if (!cfg.recallEnabled) return;
429
- if (!event?.prompt || event.prompt.length < 3) return;
423
+ const userPrompt = stripOpenClawInjectedPrefix(event?.prompt || "");
424
+ if (!userPrompt || userPrompt.length < 3) return;
430
425
  if (!cfg.apiKey) {
431
426
  warnMissingApiKey(log, "recall");
432
427
  return;
433
428
  }
434
429
 
435
430
  try {
436
- const payload = buildSearchPayload(cfg, event.prompt, ctx);
431
+ const payload = buildSearchPayload(cfg, userPrompt, ctx);
437
432
  const result = await searchMemory(cfg, payload);
438
433
  const resultData = extractResultData(result);
439
434
  if (!resultData) return;
440
- const filteredData = await maybeFilterRecallData(cfg, resultData, event.prompt, log);
435
+ const filteredData = await maybeFilterRecallData(cfg, resultData, userPrompt, log);
441
436
  const hookResult = formatRecallHookResult({ data: filteredData }, {
442
437
  wrapTagBlocks: true,
443
438
  relativity: payload.relativity,
@@ -5,6 +5,18 @@ import { setTimeout as delay } from "node:timers/promises";
5
5
 
6
6
  const DEFAULT_BASE_URL = "https://memos.memtensor.cn/api/openmem/v1";
7
7
  export const USER_QUERY_MARKER = "user\u200b原\u200b始\u200bquery\u200b:\u200b\u200b\u200b\u200b";
8
+ const INBOUND_META_SENTINELS = [
9
+ "Conversation info (untrusted metadata):",
10
+ "Sender (untrusted metadata):",
11
+ "Thread starter (untrusted, for context):",
12
+ "Replied message (untrusted, for context):",
13
+ "Forwarded message context (untrusted metadata):",
14
+ "Chat history since last reply (untrusted, for context):",
15
+ ];
16
+ const LEADING_TIMESTAMP_ENVELOPE_PATTERNS = [
17
+ /^\[(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s+\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}(?:\s+GMT[+-]\d{1,2})?\]\s*/,
18
+ /^\[\d{1,2}:\d{2}\s*(?:AM|PM)\s+on\s+\d{1,2}\s+[A-Za-z]+,\s+\d{4}\]:\s*/i,
19
+ ];
8
20
  const ENV_SOURCES = [
9
21
  { name: "openclaw", path: join(homedir(), ".openclaw", ".env") },
10
22
  { name: "moltbot", path: join(homedir(), ".moltbot", ".env") },
@@ -270,6 +282,66 @@ export async function addMessage(cfg, payload) {
270
282
  return callApi(cfg, "/add/message", payload);
271
283
  }
272
284
 
285
+ function isInboundMetaSentinelLine(line) {
286
+ const trimmed = line.trim();
287
+ return INBOUND_META_SENTINELS.some((sentinel) => sentinel === trimmed);
288
+ }
289
+
290
+ function stripLeadingInboundMetadata(text) {
291
+ if (!text || typeof text !== "string") return "";
292
+ const lines = text.split(/\r?\n/);
293
+ let index = 0;
294
+ let strippedAny = false;
295
+
296
+ while (index < lines.length && lines[index].trim() === "") {
297
+ index += 1;
298
+ }
299
+ if (index >= lines.length || !isInboundMetaSentinelLine(lines[index])) {
300
+ return text;
301
+ }
302
+
303
+ while (index < lines.length) {
304
+ if (!isInboundMetaSentinelLine(lines[index])) break;
305
+ const blockStart = index;
306
+ index += 1;
307
+ if (index >= lines.length || lines[index].trim() !== "```json") {
308
+ return strippedAny ? lines.slice(blockStart).join("\n") : text;
309
+ }
310
+ index += 1;
311
+ while (index < lines.length && lines[index].trim() !== "```") {
312
+ index += 1;
313
+ }
314
+ if (index >= lines.length) {
315
+ return strippedAny ? lines.slice(blockStart).join("\n") : text;
316
+ }
317
+ index += 1;
318
+ strippedAny = true;
319
+ while (index < lines.length && lines[index].trim() === "") {
320
+ index += 1;
321
+ }
322
+ }
323
+
324
+ return lines.slice(index).join("\n");
325
+ }
326
+
327
+ function stripLeadingTimestampEnvelope(text) {
328
+ if (!text || typeof text !== "string") return "";
329
+ for (const pattern of LEADING_TIMESTAMP_ENVELOPE_PATTERNS) {
330
+ if (!pattern.test(text)) continue;
331
+ return text.replace(pattern, "").trimStart();
332
+ }
333
+ return text;
334
+ }
335
+
336
+ export function stripOpenClawInjectedPrefix(text) {
337
+ if (!text || typeof text !== "string") return "";
338
+ const markerIndex = text.lastIndexOf(USER_QUERY_MARKER);
339
+ const withoutRecallPrefix =
340
+ markerIndex === -1 ? text : text.slice(markerIndex + USER_QUERY_MARKER.length);
341
+ const withoutInboundMetadata = stripLeadingInboundMetadata(withoutRecallPrefix).trimStart();
342
+ return stripLeadingTimestampEnvelope(withoutInboundMetadata);
343
+ }
344
+
273
345
  export function extractText(content) {
274
346
  if (!content) return "";
275
347
  if (typeof content === "string") return content;
@@ -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.9",
5
+ "version": "0.1.10-beta.0",
6
6
  "kind": "lifecycle",
7
7
  "main": "./index.js",
8
8
  "configSchema": {
@@ -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.9",
5
+ "version": "0.1.10-beta.0",
6
6
  "kind": "lifecycle",
7
7
  "main": "./index.js",
8
8
  "configSchema": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memtensor/memos-cloud-openclaw-plugin",
3
- "version": "0.1.9",
3
+ "version": "0.1.10-beta.0",
4
4
  "description": "OpenClaw lifecycle plugin for MemOS Cloud (add + recall memory)",
5
5
  "scripts": {
6
6
  "sync-version": "node scripts/sync-version.js",
@@ -0,0 +1,152 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+
4
+ import {
5
+ USER_QUERY_MARKER,
6
+ stripOpenClawInjectedPrefix,
7
+ } from "../lib/memos-cloud-api.js";
8
+
9
+ test("leaves plain user text unchanged", () => {
10
+ assert.equal(stripOpenClawInjectedPrefix("直接就是用户问题"), "直接就是用户问题");
11
+ });
12
+
13
+ test("strips MemOS recall marker and keeps original query", () => {
14
+ const input = `<memories>\n <facts>\n </facts>\n</memories>\n\n${USER_QUERY_MARKER}真正的问题`;
15
+ assert.equal(stripOpenClawInjectedPrefix(input), "真正的问题");
16
+ });
17
+
18
+ test("strips OpenClaw inbound metadata prefix blocks", () => {
19
+ const input = [
20
+ "Conversation info (untrusted metadata):",
21
+ "```json",
22
+ '{"message_id":"123"}',
23
+ "```",
24
+ "",
25
+ "Sender (untrusted metadata):",
26
+ "```json",
27
+ '{"label":"Aurora"}',
28
+ "```",
29
+ "",
30
+ "帮我看下这个问题",
31
+ ].join("\n");
32
+
33
+ assert.equal(stripOpenClawInjectedPrefix(input), "帮我看下这个问题");
34
+ });
35
+
36
+ test("strips recall marker and inbound metadata together", () => {
37
+ const input = [
38
+ "<memories>",
39
+ " <facts>",
40
+ " </facts>",
41
+ "</memories>",
42
+ "",
43
+ `${USER_QUERY_MARKER}Conversation info (untrusted metadata):`,
44
+ "```json",
45
+ '{"message_id":"123","history_count":1}',
46
+ "```",
47
+ "",
48
+ "Chat history since last reply (untrusted, for context):",
49
+ "```json",
50
+ '[{"sender":"Aurora","body":"上一条"}]',
51
+ "```",
52
+ "",
53
+ "继续",
54
+ ].join("\n");
55
+
56
+ assert.equal(stripOpenClawInjectedPrefix(input), "继续");
57
+ });
58
+
59
+ test("keeps content when metadata block is malformed", () => {
60
+ const input = [
61
+ "Conversation info (untrusted metadata):",
62
+ "not-a-json-fence",
63
+ "真正的问题",
64
+ ].join("\n");
65
+
66
+ assert.equal(stripOpenClawInjectedPrefix(input), input);
67
+ });
68
+
69
+ test("keeps content unchanged when sentinel appears in normal body", () => {
70
+ const input = [
71
+ "请原样解释下面这段文本:",
72
+ "Conversation info (untrusted metadata):",
73
+ "```json",
74
+ '{"message_id":"123"}',
75
+ "```",
76
+ ].join("\n");
77
+
78
+ assert.equal(stripOpenClawInjectedPrefix(input), input);
79
+ });
80
+
81
+ test("strips valid prefix even if body starts with a sentinel-like line", () => {
82
+ const input = [
83
+ "Sender (untrusted metadata):",
84
+ "```json",
85
+ '{"label":"Aurora"}',
86
+ "```",
87
+ "",
88
+ "Sender (untrusted metadata):",
89
+ "这行是正文,不是 OpenClaw 注入块",
90
+ ].join("\n");
91
+
92
+ assert.equal(
93
+ stripOpenClawInjectedPrefix(input),
94
+ ["Sender (untrusted metadata):", "这行是正文,不是 OpenClaw 注入块"].join("\n"),
95
+ );
96
+ });
97
+
98
+ test("supports leading blank lines before inbound metadata", () => {
99
+ const input = [
100
+ "",
101
+ "",
102
+ "Conversation info (untrusted metadata):",
103
+ "```json",
104
+ '{"message_id":"123"}',
105
+ "```",
106
+ "",
107
+ "真正的问题",
108
+ ].join("\n");
109
+
110
+ assert.equal(stripOpenClawInjectedPrefix(input), "真正的问题");
111
+ });
112
+
113
+ test("supports windows newlines", () => {
114
+ const input =
115
+ "Conversation info (untrusted metadata):\r\n```json\r\n{\"message_id\":\"123\"}\r\n```\r\n\r\n继续";
116
+
117
+ assert.equal(stripOpenClawInjectedPrefix(input), "继续");
118
+ });
119
+
120
+ test("strips gateway-client sender block and leading weekday timestamp envelope", () => {
121
+ const input = [
122
+ "Sender (untrusted metadata):",
123
+ "```json",
124
+ '{"label":"openclaw-tui (gateway-client)","id":"gateway-client"}',
125
+ "```",
126
+ "",
127
+ "[Mon 2026-03-16 14:27 GMT+8] What is Melanie's hand-painted bowl a reminder of?",
128
+ ].join("\n");
129
+
130
+ assert.equal(
131
+ stripOpenClawInjectedPrefix(input),
132
+ "What is Melanie's hand-painted bowl a reminder of?",
133
+ );
134
+ });
135
+
136
+ test("strips leading pm-on-date envelope after inbound metadata", () => {
137
+ const input = [
138
+ "Sender (untrusted metadata):",
139
+ "```json",
140
+ '{"label":"openclaw-tui (gateway-client)","id":"gateway-client"}',
141
+ "```",
142
+ "",
143
+ "[06:18 PM on 07 March, 2026]: 继续",
144
+ ].join("\n");
145
+
146
+ assert.equal(stripOpenClawInjectedPrefix(input), "继续");
147
+ });
148
+
149
+ test("keeps bracketed content when it is not a recognized timestamp envelope", () => {
150
+ const input = "[Important] What is Melanie's hand-painted bowl a reminder of?";
151
+ assert.equal(stripOpenClawInjectedPrefix(input), input);
152
+ });