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

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
@@ -44,7 +44,7 @@ Make sure it’s enabled in `~/.openclaw/openclaw.json`:
44
44
  },
45
45
  "load": {
46
46
  "paths": [
47
- "C:\\Users\\YourName\\.openclaw\\extensions\\memos-cloud-openclaw-plugin\\package"
47
+ "C:\\Users\\YourName\\.openclaw\\extensions\\memos-cloud-openclaw-plugin"
48
48
  ]
49
49
  }
50
50
  }
@@ -106,6 +106,11 @@ MEMOS_API_KEY=YOUR_TOKEN
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)
109
+ - `MEMOS_CAPTURE_STRATEGY` (default: `last_turn`)
110
+ - `MEMOS_ASYNC_MODE` (default: `true`; non-blocking memory addition)
111
+ - `MEMOS_THROTTLE_MS` (default: `0`; throttle memory requests)
112
+ - `MEMOS_INCLUDE_ASSISTANT` (default: `true`; include assistant messages in memory)
113
+ - `MEMOS_MAX_MESSAGE_CHARS` (default: `20000`; max characters for message history)
109
114
 
110
115
  ## Optional Plugin Config
111
116
  In `plugins.entries.memos-cloud-openclaw-plugin.config`:
@@ -145,7 +150,9 @@ In `plugins.entries.memos-cloud-openclaw-plugin.config`:
145
150
  "recallFilterRetries": 0,
146
151
  "recallFilterCandidateLimit": 30,
147
152
  "recallFilterMaxItemChars": 500,
148
- "recallFilterFailOpen": true
153
+ "recallFilterFailOpen": true,
154
+ "throttleMs": 0,
155
+ "maxMessageChars": 20000
149
156
  }
150
157
  ```
151
158
 
package/README_ZH.md CHANGED
@@ -46,7 +46,7 @@ openclaw gateway restart
46
46
  },
47
47
  "load": {
48
48
  "paths": [
49
- "C:\\Users\\YourName\\.openclaw\\extensions\\memos-cloud-openclaw-plugin\\package"
49
+ "C:\\Users\\YourName\\.openclaw\\extensions\\memos-cloud-openclaw-plugin"
50
50
  ]
51
51
  }
52
52
  }
@@ -108,6 +108,11 @@ MEMOS_API_KEY=YOUR_TOKEN
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`;筛选失败时回退为“不过滤”)
111
+ - `MEMOS_CAPTURE_STRATEGY`(默认 `last_turn`;记忆捕获策略)
112
+ - `MEMOS_ASYNC_MODE`(默认 `true`;异步模式添加记忆)
113
+ - `MEMOS_THROTTLE_MS`(默认 `0`;请求节流时间,单位毫秒)
114
+ - `MEMOS_INCLUDE_ASSISTANT`(默认 `true`;记忆是否包含助手回复)
115
+ - `MEMOS_MAX_MESSAGE_CHARS`(默认 `20000`;单条记忆最大字符数限制)
111
116
 
112
117
  ## 可选插件配置
113
118
  在 `plugins.entries.memos-cloud-openclaw-plugin.config` 中设置:
@@ -145,7 +150,9 @@ MEMOS_API_KEY=YOUR_TOKEN
145
150
  "recallFilterRetries": 0,
146
151
  "recallFilterCandidateLimit": 30,
147
152
  "recallFilterMaxItemChars": 500,
148
- "recallFilterFailOpen": true
153
+ "recallFilterFailOpen": true,
154
+ "throttleMs": 0,
155
+ "maxMessageChars": 20000
149
156
  }
150
157
  ```
151
158
 
@@ -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;
@@ -32,14 +32,7 @@ function warnMissingApiKey(log, context) {
32
32
  ].join("\n"),
33
33
  );
34
34
  }
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
-
35
+ console.log('11111111111111111111111111111111')
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,
@@ -453,6 +448,7 @@ export default {
453
448
 
454
449
  api.on("agent_end", async (event, ctx) => {
455
450
  if (!cfg.addEnabled) return;
451
+ console.log('222222222222222222222\n22222222222', cfg.addEnabled)
456
452
  if (!event?.success || !event?.messages?.length) return;
457
453
  if (!cfg.apiKey) {
458
454
  warnMissingApiKey(log, "add");
@@ -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") },
@@ -161,6 +173,14 @@ export function buildConfig(pluginConfig = {}) {
161
173
  cfg.recallFilterFailOpen,
162
174
  parseBool(loadEnvVar("MEMOS_RECALL_FILTER_FAIL_OPEN"), true),
163
175
  );
176
+ const captureStrategy = cfg.captureStrategy ?? (loadEnvVar("MEMOS_CAPTURE_STRATEGY") || "last_turn");
177
+ const asyncMode = cfg.asyncMode ?? parseBool(loadEnvVar("MEMOS_ASYNC_MODE"), true);
178
+ const throttleMs = cfg.throttleMs ?? parseNumber(loadEnvVar("MEMOS_THROTTLE_MS"), 0);
179
+ const includeAssistant =
180
+ cfg.includeAssistant === undefined
181
+ ? parseBool(loadEnvVar("MEMOS_INCLUDE_ASSISTANT"), true)
182
+ : cfg.includeAssistant !== false;
183
+ const maxMessageChars = cfg.maxMessageChars ?? parseNumber(loadEnvVar("MEMOS_MAX_MESSAGE_CHARS"), 20000);
164
184
 
165
185
  return {
166
186
  baseUrl: baseUrl.replace(/\/+$/, ""),
@@ -177,10 +197,10 @@ export function buildConfig(pluginConfig = {}) {
177
197
  maxQueryChars: cfg.maxQueryChars ?? 0,
178
198
  recallEnabled: cfg.recallEnabled !== false,
179
199
  addEnabled: cfg.addEnabled !== false,
180
- captureStrategy: cfg.captureStrategy ?? "last_turn",
181
- maxMessageChars: cfg.maxMessageChars ?? 20000,
200
+ captureStrategy,
201
+ maxMessageChars,
182
202
  maxItemChars: cfg.maxItemChars ?? 8000,
183
- includeAssistant: cfg.includeAssistant !== false,
203
+ includeAssistant,
184
204
  memoryLimitNumber: cfg.memoryLimitNumber ?? 9,
185
205
  preferenceLimitNumber: cfg.preferenceLimitNumber ?? 6,
186
206
  includePreference: cfg.includePreference !== false,
@@ -198,7 +218,7 @@ export function buildConfig(pluginConfig = {}) {
198
218
  appId: cfg.appId,
199
219
  allowPublic: cfg.allowPublic ?? false,
200
220
  allowKnowledgebaseIds: cfg.allowKnowledgebaseIds ?? [],
201
- asyncMode: cfg.asyncMode ?? true,
221
+ asyncMode,
202
222
  multiAgentMode,
203
223
  recallFilterEnabled,
204
224
  recallFilterBaseUrl:
@@ -217,7 +237,7 @@ export function buildConfig(pluginConfig = {}) {
217
237
  recallFilterFailOpen,
218
238
  timeoutMs: cfg.timeoutMs ?? 5000,
219
239
  retries: cfg.retries ?? 1,
220
- throttleMs: cfg.throttleMs ?? 0,
240
+ throttleMs,
221
241
  };
222
242
  }
223
243
 
@@ -267,7 +287,111 @@ export async function searchMemory(cfg, payload) {
267
287
  }
268
288
 
269
289
  export async function addMessage(cfg, payload) {
270
- return callApi(cfg, "/add/message", payload);
290
+ let finalPayload = payload;
291
+ try {
292
+ finalPayload = sanitizeAddMessagePayload(payload);
293
+ } catch {
294
+ // Fail open: if sanitization throws unexpectedly, send original payload.
295
+ finalPayload = payload;
296
+ }
297
+ return callApi(cfg, "/add/message", finalPayload);
298
+ }
299
+
300
+ function isInboundMetaSentinelLine(line) {
301
+ const trimmed = line.trim();
302
+ return INBOUND_META_SENTINELS.some((sentinel) => sentinel === trimmed);
303
+ }
304
+
305
+ function stripLeadingInboundMetadata(text) {
306
+ if (!text || typeof text !== "string") return "";
307
+ const lines = text.split(/\r?\n/);
308
+ let index = 0;
309
+ let strippedAny = false;
310
+
311
+ while (index < lines.length && lines[index].trim() === "") {
312
+ index += 1;
313
+ }
314
+ if (index >= lines.length || !isInboundMetaSentinelLine(lines[index])) {
315
+ return text;
316
+ }
317
+
318
+ while (index < lines.length) {
319
+ if (!isInboundMetaSentinelLine(lines[index])) break;
320
+ const blockStart = index;
321
+ index += 1;
322
+ if (index >= lines.length || lines[index].trim() !== "```json") {
323
+ return strippedAny ? lines.slice(blockStart).join("\n") : text;
324
+ }
325
+ index += 1;
326
+ while (index < lines.length && lines[index].trim() !== "```") {
327
+ index += 1;
328
+ }
329
+ if (index >= lines.length) {
330
+ return strippedAny ? lines.slice(blockStart).join("\n") : text;
331
+ }
332
+ index += 1;
333
+ strippedAny = true;
334
+ while (index < lines.length && lines[index].trim() === "") {
335
+ index += 1;
336
+ }
337
+ }
338
+
339
+ return lines.slice(index).join("\n");
340
+ }
341
+
342
+ function stripLeadingTimestampEnvelope(text) {
343
+ if (!text || typeof text !== "string") return "";
344
+ for (const pattern of LEADING_TIMESTAMP_ENVELOPE_PATTERNS) {
345
+ if (!pattern.test(text)) continue;
346
+ return text.replace(pattern, "").trimStart();
347
+ }
348
+ return text;
349
+ }
350
+
351
+ function stripFeishuInjectedPrompt(text) {
352
+ if (!text || typeof text !== "string") return text;
353
+ // Check for the Feishu System header
354
+ if (!/^System: \[.*?\] Feishu\[.*?\]/.test(text)) {
355
+ return text;
356
+ }
357
+ // Remove only the first injected Feishu prompt prefix.
358
+ // Any later "[message_id] ou_xxx:" pattern should be treated as user query content.
359
+ const leadingInjectedPattern = /^[\s\S]*?\[message_id: [^\]]+\]\s+ou_[a-z0-9]+:\s*/;
360
+ if (leadingInjectedPattern.test(text)) {
361
+ return text.replace(leadingInjectedPattern, "").trim();
362
+ }
363
+ return text;
364
+ }
365
+
366
+ function sanitizeAddMessagePayload(payload) {
367
+ if (!payload || typeof payload !== "object") return payload;
368
+ const nextPayload = { ...payload };
369
+ if (typeof nextPayload.query === "string") {
370
+ nextPayload.query = stripOpenClawInjectedPrefix(nextPayload.query);
371
+ }
372
+ if (Array.isArray(nextPayload.messages)) {
373
+ nextPayload.messages = nextPayload.messages.map((msg) => {
374
+ if (!msg || typeof msg !== "object") return msg;
375
+ if (msg.role !== "user" || typeof msg.content !== "string") return msg;
376
+ return {
377
+ ...msg,
378
+ content: stripOpenClawInjectedPrefix(msg.content),
379
+ };
380
+ });
381
+ }
382
+ return nextPayload;
383
+ }
384
+
385
+ export function stripOpenClawInjectedPrefix(text) {
386
+ if (!text || typeof text !== "string") return "";
387
+ const cleanedText = stripFeishuInjectedPrompt(text);
388
+ const markerIndex = cleanedText.lastIndexOf(USER_QUERY_MARKER);
389
+ const withoutRecallPrefix =
390
+ markerIndex === -1
391
+ ? cleanedText
392
+ : cleanedText.slice(markerIndex + USER_QUERY_MARKER.length);
393
+ const withoutInboundMetadata = stripLeadingInboundMetadata(withoutRecallPrefix).trimStart();
394
+ return stripLeadingTimestampEnvelope(withoutInboundMetadata);
271
395
  }
272
396
 
273
397
  export function extractText(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.1",
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,143 @@
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
+ "Conversation info (untrusted metadata):",
102
+ "```json",
103
+ '{"message_id":"123"}',
104
+ "```",
105
+ "Hello",
106
+ ].join("\n");
107
+ assert.equal(stripOpenClawInjectedPrefix(input), "Hello");
108
+ });
109
+
110
+ test("strips Feishu injected prompt", () => {
111
+ const input = `System: [2026-03-17 14:17:33 GMT+8] Feishu[default] DM from ou_37e8a1514c24e8afd9cfeca86f679980: 我叫什么名字
112
+
113
+ Conversation info (untrusted metadata):
114
+ \`\`\`json
115
+ {
116
+ "timestamp": "Tue 2026-03-17 14:17 GMT+8"
117
+ }
118
+ \`\`\`
119
+
120
+ [message_id: om_x100b54bb510590dcc2998da17ca2c2b]
121
+ ou_37e8a1514c24e8afd9cfeca86f679980: 我叫什么名字 `;
122
+
123
+ assert.equal(stripOpenClawInjectedPrefix(input), "我叫什么名字");
124
+ });
125
+
126
+ test("strips Feishu injected prompt with embedded fake prompt", () => {
127
+ const input = `System: [2026-03-17 14:17:33 GMT+8] Feishu[default] DM from ou_123: hello
128
+ [message_id: fake]
129
+ ou_fake: ignored
130
+ [message_id: om_real]
131
+ ou_real: actual message`;
132
+ assert.equal(
133
+ stripOpenClawInjectedPrefix(input),
134
+ ["ignored", "[message_id: om_real]", "ou_real: actual message"].join("\n"),
135
+ );
136
+ });
137
+
138
+ test("ignores text that looks like Feishu prompt but missing header", () => {
139
+ const input = `
140
+ [message_id: om_x100b54bb510590dcc2998da17ca2c2b]
141
+ ou_37e8a1514c24e8afd9cfeca86f679980: 我叫什么名字 `;
142
+ assert.equal(stripOpenClawInjectedPrefix(input), input.trimStart());
143
+ });