@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.
- package/clawdbot.plugin.json +1 -1
- package/index.js +9 -14
- package/lib/memos-cloud-api.js +72 -0
- package/moltbot.plugin.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/test/query-strip.test.mjs +152 -0
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.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
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
package/lib/memos-cloud-api.js
CHANGED
|
@@ -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;
|
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.10-beta.0",
|
|
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.10-beta.0",
|
|
6
6
|
"kind": "lifecycle",
|
|
7
7
|
"main": "./index.js",
|
|
8
8
|
"configSchema": {
|
package/package.json
CHANGED
|
@@ -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
|
+
});
|