@memtensor/memos-cloud-openclaw-plugin 0.1.19-beta.0 → 0.1.20-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/README.md +316 -316
- package/clawdbot.plugin.json +1 -1
- package/index.js +879 -809
- package/lib/arms-reporter.js +143 -116
- package/lib/check-update.js +174 -202
- package/lib/config-resolution-schema.js +50 -50
- package/lib/config-ui/app.css +1022 -1022
- package/lib/config-ui/app.js +1454 -1454
- package/lib/config-ui/index.html +113 -113
- package/lib/memos-cloud-api.js +869 -868
- package/lib/semver.js +91 -0
- package/moltbot.plugin.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +56 -46
- package/README_ZH.md +0 -321
- package/scripts/sync-version.js +0 -45
- package/test/direct-session-user-id.test.mjs +0 -125
- package/test/query-strip.test.mjs +0 -454
- package/test/recall-hook-registration.test.mjs +0 -56
package/lib/memos-cloud-api.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
|
-
import { setTimeout as delay } from "node:timers/promises";
|
|
5
|
-
import { CONFIG_RESOLUTION_FIELDS } from "./config-resolution-schema.js";
|
|
6
|
-
|
|
7
|
-
const DEFAULT_BASE_URL = "https://memos.memtensor.cn/api/openmem/v1";
|
|
8
|
-
export const USER_QUERY_MARKER = "user\u200b原\u200b始\u200bquery\u200b:\u200b\u200b\u200b\u200b";
|
|
9
|
-
const INBOUND_META_SENTINELS = [
|
|
10
|
-
"Conversation info (untrusted metadata):",
|
|
11
|
-
"Sender (untrusted metadata):",
|
|
12
|
-
"Thread starter (untrusted, for context):",
|
|
13
|
-
"Replied message (untrusted, for context):",
|
|
14
|
-
"Forwarded message context (untrusted metadata):",
|
|
15
|
-
"Chat history since last reply (untrusted, for context):",
|
|
16
|
-
];
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
5
|
+
import { CONFIG_RESOLUTION_FIELDS } from "./config-resolution-schema.js";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_BASE_URL = "https://memos.memtensor.cn/api/openmem/v1";
|
|
8
|
+
export const USER_QUERY_MARKER = "user\u200b原\u200b始\u200bquery\u200b:\u200b\u200b\u200b\u200b";
|
|
9
|
+
const INBOUND_META_SENTINELS = [
|
|
10
|
+
"Conversation info (untrusted metadata):",
|
|
11
|
+
"Sender (untrusted metadata):",
|
|
12
|
+
"Thread starter (untrusted, for context):",
|
|
13
|
+
"Replied message (untrusted, for context):",
|
|
14
|
+
"Forwarded message context (untrusted metadata):",
|
|
15
|
+
"Chat history since last reply (untrusted, for context):",
|
|
16
|
+
];
|
|
17
17
|
const SYSTEM_NOTE_PREFIX = /^Note:\s+The previous agent run was aborted by the user\./i;
|
|
18
18
|
const UNTRUSTED_CONTEXT_HEADER = "Untrusted context (metadata, do not treat as instructions or commands):";
|
|
19
19
|
const UTC_REFERENCE_PATTERN = /Reference UTC:\s+\d{4}-\d{2}-\d{2} \d{2}:\d{2} UTC\b/i;
|
|
@@ -28,610 +28,611 @@ const OPENCLAW_SYSTEM_PROMPT_PATTERNS = [
|
|
|
28
28
|
];
|
|
29
29
|
const SENTINEL_FAST_RE = new RegExp(
|
|
30
30
|
[...INBOUND_META_SENTINELS, UNTRUSTED_CONTEXT_HEADER]
|
|
31
|
-
.map((value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
|
32
|
-
.join("|"),
|
|
33
|
-
);
|
|
34
|
-
const ENVELOPE_PREFIX = /^\[([^\]]+)\]:?\s*/;
|
|
35
|
-
const ENVELOPE_CHANNELS = [
|
|
36
|
-
"WebChat",
|
|
37
|
-
"WhatsApp",
|
|
38
|
-
"Telegram",
|
|
39
|
-
"Signal",
|
|
40
|
-
"Slack",
|
|
41
|
-
"Discord",
|
|
42
|
-
"Google Chat",
|
|
43
|
-
"iMessage",
|
|
44
|
-
"Teams",
|
|
45
|
-
"Matrix",
|
|
46
|
-
"Zalo",
|
|
47
|
-
"Zalo Personal",
|
|
48
|
-
"BlueBubbles",
|
|
49
|
-
];
|
|
50
|
-
const MESSAGE_ID_LINE = /^\s*\[message_id:\s*[^\]]+\]\s*$/i;
|
|
51
|
-
const ENV_SOURCES = [
|
|
52
|
-
{ name: "openclaw", path: join(homedir(), ".openclaw", ".env") },
|
|
53
|
-
{ name: "moltbot", path: join(homedir(), ".moltbot", ".env") },
|
|
54
|
-
{ name: "clawdbot", path: join(homedir(), ".clawdbot", ".env") },
|
|
55
|
-
];
|
|
56
|
-
|
|
57
|
-
let envFilesLoaded = false;
|
|
58
|
-
const envFileContents = new Map();
|
|
59
|
-
const envFileValues = new Map();
|
|
60
|
-
|
|
61
|
-
function stripQuotes(value) {
|
|
62
|
-
if (!value) return value;
|
|
63
|
-
const trimmed = value.trim();
|
|
64
|
-
if (
|
|
65
|
-
(trimmed.startsWith("\"") && trimmed.endsWith("\"")) ||
|
|
66
|
-
(trimmed.startsWith("'") && trimmed.endsWith("'"))
|
|
67
|
-
) {
|
|
68
|
-
return trimmed.slice(1, -1);
|
|
69
|
-
}
|
|
70
|
-
return trimmed;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function extractResultData(result) {
|
|
74
|
-
if (!result || typeof result !== "object") return null;
|
|
75
|
-
return result.data ?? result.data?.data ?? result.data?.result ?? null;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function pad2(value) {
|
|
79
|
-
return String(value).padStart(2, "0");
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function formatTime(value) {
|
|
83
|
-
if (value === undefined || value === null || value === "") return "";
|
|
84
|
-
if (typeof value === "number") {
|
|
85
|
-
const date = new Date(value);
|
|
86
|
-
if (Number.isNaN(date.getTime())) return "";
|
|
87
|
-
return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())} ${pad2(
|
|
88
|
-
date.getHours(),
|
|
89
|
-
)}:${pad2(date.getMinutes())}`;
|
|
90
|
-
}
|
|
91
|
-
if (typeof value === "string") {
|
|
92
|
-
const trimmed = value.trim();
|
|
93
|
-
if (!trimmed) return "";
|
|
94
|
-
if (/^\d+$/.test(trimmed)) return formatTime(Number(trimmed));
|
|
95
|
-
return trimmed;
|
|
96
|
-
}
|
|
97
|
-
return "";
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function parseEnvFile(content) {
|
|
101
|
-
const values = new Map();
|
|
102
|
-
for (const line of content.split(/\r?\n/)) {
|
|
103
|
-
const trimmed = line.trim();
|
|
104
|
-
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
105
|
-
const idx = trimmed.indexOf("=");
|
|
106
|
-
if (idx <= 0) continue;
|
|
107
|
-
const key = trimmed.slice(0, idx).trim();
|
|
108
|
-
const rawValue = trimmed.slice(idx + 1);
|
|
109
|
-
if (!key) continue;
|
|
110
|
-
values.set(key, stripQuotes(rawValue));
|
|
111
|
-
}
|
|
112
|
-
return values;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function loadEnvFiles() {
|
|
116
|
-
if (envFilesLoaded) return;
|
|
117
|
-
envFilesLoaded = true;
|
|
118
|
-
for (const source of ENV_SOURCES) {
|
|
119
|
-
try {
|
|
120
|
-
const content = readFileSync(source.path, "utf-8");
|
|
121
|
-
envFileContents.set(source.name, content);
|
|
122
|
-
envFileValues.set(source.name, parseEnvFile(content));
|
|
123
|
-
} catch {
|
|
124
|
-
// ignore missing files
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function loadEnvFromFiles(name) {
|
|
130
|
-
for (const source of ENV_SOURCES) {
|
|
131
|
-
const values = envFileValues.get(source.name);
|
|
132
|
-
if (!values) continue;
|
|
133
|
-
if (values.has(name)) return values.get(name);
|
|
134
|
-
}
|
|
135
|
-
return undefined;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function loadEnvVar(name) {
|
|
139
|
-
loadEnvFiles();
|
|
140
|
-
const fromFiles = loadEnvFromFiles(name);
|
|
141
|
-
if (fromFiles !== undefined) return fromFiles;
|
|
142
|
-
return undefined;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export function getEnvFileStatus() {
|
|
146
|
-
loadEnvFiles();
|
|
147
|
-
const sources = ENV_SOURCES.filter((source) => envFileContents.has(source.name));
|
|
148
|
-
return {
|
|
149
|
-
found: sources.length > 0,
|
|
150
|
-
sources: sources.map((source) => source.name),
|
|
151
|
-
paths: sources.map((source) => source.path),
|
|
152
|
-
searchPaths: ENV_SOURCES.map((source) => source.path),
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function parseBool(value, fallback) {
|
|
157
|
-
if (value === undefined || value === null || value === "") return fallback;
|
|
158
|
-
if (typeof value === "boolean") return value;
|
|
159
|
-
const normalized = String(value).trim().toLowerCase();
|
|
160
|
-
if (["1", "true", "yes", "y", "on"].includes(normalized)) return true;
|
|
161
|
-
if (["0", "false", "no", "n", "off"].includes(normalized)) return false;
|
|
162
|
-
return fallback;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function parseNumber(value, fallback) {
|
|
166
|
-
if (value === undefined || value === null || value === "") return fallback;
|
|
167
|
-
const n = Number(value);
|
|
168
|
-
return Number.isFinite(n) ? n : fallback;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function parseStringArray(value) {
|
|
172
|
-
if (!value) return [];
|
|
173
|
-
if (Array.isArray(value)) return value.map((v) => String(v).trim()).filter(Boolean);
|
|
174
|
-
return String(value)
|
|
175
|
-
.split(",")
|
|
176
|
-
.map((s) => s.trim().replace(/^["']|["']$/g, ""))
|
|
177
|
-
.filter(Boolean);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function parseJsonObject(value) {
|
|
181
|
-
if (!value || typeof value !== "string") return null;
|
|
182
|
-
try {
|
|
183
|
-
const parsed = JSON.parse(value);
|
|
184
|
-
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
185
|
-
return parsed;
|
|
186
|
-
}
|
|
187
|
-
} catch {
|
|
188
|
-
// ignore parse error
|
|
189
|
-
}
|
|
190
|
-
return null;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
export function buildConfig(pluginConfig = {}) {
|
|
194
|
-
const cfg = pluginConfig ?? {};
|
|
195
|
-
|
|
196
|
-
const baseUrl = cfg.baseUrl || loadEnvVar("MEMOS_BASE_URL") || DEFAULT_BASE_URL;
|
|
197
|
-
const apiKey = cfg.apiKey || loadEnvVar("MEMOS_API_KEY") || "";
|
|
198
|
-
const userId = cfg.userId || loadEnvVar("MEMOS_USER_ID") || "openclaw-user";
|
|
199
|
-
const conversationId = cfg.conversationId || loadEnvVar("MEMOS_CONVERSATION_ID") || "";
|
|
200
|
-
|
|
201
|
-
const recallGlobal = parseBool(
|
|
202
|
-
cfg.recallGlobal,
|
|
203
|
-
parseBool(loadEnvVar("MEMOS_RECALL_GLOBAL"), true),
|
|
204
|
-
);
|
|
205
|
-
|
|
206
|
-
const conversationIdPrefix = cfg.conversationIdPrefix ?? loadEnvVar("MEMOS_CONVERSATION_PREFIX") ?? "";
|
|
207
|
-
const conversationIdSuffix = cfg.conversationIdSuffix ?? loadEnvVar("MEMOS_CONVERSATION_SUFFIX") ?? "";
|
|
208
|
-
const conversationSuffixMode =
|
|
209
|
-
cfg.conversationSuffixMode ?? loadEnvVar("MEMOS_CONVERSATION_SUFFIX_MODE") ?? "none";
|
|
210
|
-
const resetOnNew = parseBool(
|
|
211
|
-
cfg.resetOnNew,
|
|
212
|
-
parseBool(loadEnvVar("MEMOS_CONVERSATION_RESET_ON_NEW"), true),
|
|
213
|
-
);
|
|
214
|
-
|
|
215
|
-
const multiAgentMode = parseBool(
|
|
216
|
-
cfg.multiAgentMode,
|
|
217
|
-
parseBool(loadEnvVar("MEMOS_MULTI_AGENT_MODE"), false),
|
|
218
|
-
);
|
|
219
|
-
|
|
220
|
-
const allowedAgents = parseStringArray(
|
|
221
|
-
cfg.allowedAgents ?? loadEnvVar("MEMOS_ALLOWED_AGENTS"),
|
|
222
|
-
);
|
|
223
|
-
|
|
224
|
-
const recallFilterEnabled = parseBool(
|
|
225
|
-
cfg.recallFilterEnabled,
|
|
226
|
-
parseBool(loadEnvVar("MEMOS_RECALL_FILTER_ENABLED"), false),
|
|
227
|
-
);
|
|
228
|
-
const recallFilterFailOpen = parseBool(
|
|
229
|
-
cfg.recallFilterFailOpen,
|
|
230
|
-
parseBool(loadEnvVar("MEMOS_RECALL_FILTER_FAIL_OPEN"), true),
|
|
231
|
-
);
|
|
232
|
-
const captureStrategy = cfg.captureStrategy ?? (loadEnvVar("MEMOS_CAPTURE_STRATEGY") || "last_turn");
|
|
233
|
-
const asyncMode = cfg.asyncMode ?? parseBool(loadEnvVar("MEMOS_ASYNC_MODE"), true);
|
|
234
|
-
const throttleMs = cfg.throttleMs ?? parseNumber(loadEnvVar("MEMOS_THROTTLE_MS"), 0);
|
|
235
|
-
const includeAssistant =
|
|
236
|
-
cfg.includeAssistant === undefined
|
|
237
|
-
? parseBool(loadEnvVar("MEMOS_INCLUDE_ASSISTANT"), true)
|
|
238
|
-
: cfg.includeAssistant !== false;
|
|
239
|
-
const maxMessageChars = cfg.maxMessageChars ?? parseNumber(loadEnvVar("MEMOS_MAX_MESSAGE_CHARS"), 20000);
|
|
240
|
-
const rumEnabled = parseBool(
|
|
241
|
-
cfg.rumEnabled,
|
|
242
|
-
parseBool(loadEnvVar("MEMOS_RUM_ENABLED"), true),
|
|
243
|
-
);
|
|
244
|
-
const useDirectSessionUserId = parseBool(
|
|
245
|
-
cfg.useDirectSessionUserId,
|
|
246
|
-
parseBool(loadEnvVar("MEMOS_USE_DIRECT_SESSION_USER_ID"), false),
|
|
247
|
-
);
|
|
248
|
-
|
|
249
|
-
return {
|
|
250
|
-
baseUrl: baseUrl.replace(/\/+$/, ""),
|
|
251
|
-
apiKey,
|
|
252
|
-
userId,
|
|
253
|
-
conversationId,
|
|
254
|
-
conversationIdPrefix,
|
|
255
|
-
conversationIdSuffix,
|
|
256
|
-
conversationSuffixMode,
|
|
257
|
-
useDirectSessionUserId,
|
|
258
|
-
recallGlobal,
|
|
259
|
-
resetOnNew,
|
|
260
|
-
envFileStatus: getEnvFileStatus(),
|
|
261
|
-
queryPrefix: cfg.queryPrefix ?? "",
|
|
262
|
-
maxQueryChars: cfg.maxQueryChars ?? 0,
|
|
263
|
-
recallEnabled: cfg.recallEnabled !== false,
|
|
264
|
-
addEnabled: cfg.addEnabled !== false,
|
|
265
|
-
captureStrategy,
|
|
266
|
-
maxMessageChars,
|
|
267
|
-
maxItemChars: cfg.maxItemChars ?? 8000,
|
|
268
|
-
includeAssistant,
|
|
269
|
-
memoryLimitNumber: cfg.memoryLimitNumber ?? 9,
|
|
270
|
-
preferenceLimitNumber: cfg.preferenceLimitNumber ?? 6,
|
|
271
|
-
includePreference: cfg.includePreference !== false,
|
|
272
|
-
includeToolMemory: cfg.includeToolMemory === true,
|
|
273
|
-
toolMemoryLimitNumber: cfg.toolMemoryLimitNumber ?? 6,
|
|
274
|
-
relativity: cfg.relativity ?? ((() => {
|
|
275
|
-
const v = loadEnvVar("MEMOS_RELATIVITY");
|
|
276
|
-
return v ? parseFloat(v) : 0.45;
|
|
277
|
-
})()),
|
|
278
|
-
filter: cfg.filter,
|
|
279
|
-
knowledgebaseIds: cfg.knowledgebaseIds ?? (loadEnvVar("MEMOS_KNOWLEDGEBASE_IDS") ? parseStringArray(loadEnvVar("MEMOS_KNOWLEDGEBASE_IDS")) : []),
|
|
280
|
-
tags: cfg.tags ?? (loadEnvVar("MEMOS_TAGS") ? parseStringArray(loadEnvVar("MEMOS_TAGS")) : ["openclaw"]),
|
|
281
|
-
info: cfg.info ?? {},
|
|
282
|
-
agentId: cfg.agentId,
|
|
283
|
-
appId: cfg.appId,
|
|
284
|
-
allowPublic: cfg.allowPublic ?? false,
|
|
285
|
-
allowKnowledgebaseIds: cfg.allowKnowledgebaseIds ?? (loadEnvVar("MEMOS_ALLOW_KNOWLEDGEBASE_IDS") ? parseStringArray(loadEnvVar("MEMOS_ALLOW_KNOWLEDGEBASE_IDS")) : []),
|
|
286
|
-
asyncMode,
|
|
287
|
-
multiAgentMode,
|
|
288
|
-
allowedAgents,
|
|
289
|
-
recallFilterEnabled,
|
|
290
|
-
recallFilterBaseUrl:
|
|
291
|
-
(cfg.recallFilterBaseUrl ?? loadEnvVar("MEMOS_RECALL_FILTER_BASE_URL") ?? "").replace(/\/+$/, ""),
|
|
292
|
-
recallFilterApiKey: cfg.recallFilterApiKey ?? loadEnvVar("MEMOS_RECALL_FILTER_API_KEY") ?? "",
|
|
293
|
-
recallFilterModel: cfg.recallFilterModel ?? loadEnvVar("MEMOS_RECALL_FILTER_MODEL") ?? "",
|
|
294
|
-
recallFilterTimeoutMs: parseNumber(
|
|
295
|
-
cfg.recallFilterTimeoutMs ?? loadEnvVar("MEMOS_RECALL_FILTER_TIMEOUT_MS"),
|
|
296
|
-
30000,
|
|
297
|
-
),
|
|
298
|
-
recallFilterRetries: parseNumber(cfg.recallFilterRetries ?? loadEnvVar("MEMOS_RECALL_FILTER_RETRIES"), 1),
|
|
299
|
-
recallFilterCandidateLimit:
|
|
300
|
-
parseNumber(cfg.recallFilterCandidateLimit ?? loadEnvVar("MEMOS_RECALL_FILTER_CANDIDATE_LIMIT"), 30),
|
|
301
|
-
recallFilterMaxItemChars:
|
|
302
|
-
parseNumber(cfg.recallFilterMaxItemChars ?? loadEnvVar("MEMOS_RECALL_FILTER_MAX_ITEM_CHARS"), 500),
|
|
303
|
-
recallFilterFailOpen,
|
|
304
|
-
timeoutMs: cfg.timeoutMs ?? 5000,
|
|
305
|
-
retries: cfg.retries ?? 1,
|
|
306
|
-
throttleMs,
|
|
307
|
-
rumEnabled,
|
|
308
|
-
_agentOverrides: cfg.agentOverrides ?? parseJsonObject(loadEnvVar("MEMOS_AGENT_OVERRIDES")) ?? {},
|
|
309
|
-
};
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
function hasConfigValue(cfg, field) {
|
|
313
|
-
const configKey = field.configKey ?? field.key;
|
|
314
|
-
if (field.configMode === "truthy") return Boolean(cfg[configKey]);
|
|
315
|
-
return cfg[configKey] !== undefined && cfg[configKey] !== null;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function hasEnvValue(envValue, envMode) {
|
|
319
|
-
if (envMode === "truthy") return Boolean(envValue);
|
|
320
|
-
if (envMode === "defined") return envValue !== undefined;
|
|
321
|
-
return false;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
function resolveInheritedValue(field, resolved, envRaw) {
|
|
325
|
-
if (Object.prototype.hasOwnProperty.call(field, "inheritedValue")) {
|
|
326
|
-
return field.inheritedValue;
|
|
327
|
-
}
|
|
328
|
-
if (field.inheritedFrom === "env") {
|
|
329
|
-
const envValue = field.envVar ? envRaw[field.envVar] : undefined;
|
|
330
|
-
return envValue ?? field.inheritedFallback;
|
|
331
|
-
}
|
|
332
|
-
const resolvedKey = field.resolvedKey ?? field.key;
|
|
333
|
-
return resolved[resolvedKey];
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
export function getConfigResolution(pluginConfig = {}) {
|
|
337
|
-
const cfg = pluginConfig ?? {};
|
|
338
|
-
const resolved = buildConfig(cfg);
|
|
339
|
-
const envRaw = {};
|
|
340
|
-
|
|
341
|
-
for (const field of CONFIG_RESOLUTION_FIELDS) {
|
|
342
|
-
if (!field.envVar) continue;
|
|
343
|
-
if (Object.prototype.hasOwnProperty.call(envRaw, field.envVar)) continue;
|
|
344
|
-
envRaw[field.envVar] = loadEnvVar(field.envVar);
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
const fieldMeta = {};
|
|
348
|
-
for (const field of CONFIG_RESOLUTION_FIELDS) {
|
|
349
|
-
const envValue = field.envVar ? envRaw[field.envVar] : undefined;
|
|
350
|
-
const source = hasConfigValue(cfg, field)
|
|
351
|
-
? "config"
|
|
352
|
-
: hasEnvValue(envValue, field.envMode)
|
|
353
|
-
? "env"
|
|
354
|
-
: field.fallbackSource;
|
|
355
|
-
fieldMeta[field.key] = {
|
|
356
|
-
source,
|
|
357
|
-
inheritedValue: resolveInheritedValue(field, resolved, envRaw),
|
|
358
|
-
uiDefaultValue: field.uiDefaultValue,
|
|
359
|
-
};
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
return { resolved, fieldMeta };
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
const AGENT_OVERRIDABLE_KEYS = [
|
|
366
|
-
"knowledgebaseIds", "memoryLimitNumber", "preferenceLimitNumber",
|
|
367
|
-
"includePreference", "includeToolMemory", "toolMemoryLimitNumber",
|
|
368
|
-
"relativity",
|
|
369
|
-
"recallEnabled", "addEnabled", "captureStrategy", "queryPrefix",
|
|
370
|
-
"maxItemChars", "maxMessageChars", "includeAssistant",
|
|
371
|
-
"recallGlobal", "recallFilterEnabled", "recallFilterModel",
|
|
372
|
-
"recallFilterBaseUrl", "recallFilterApiKey",
|
|
373
|
-
"allowKnowledgebaseIds", "tags", "throttleMs",
|
|
374
|
-
];
|
|
375
|
-
|
|
376
|
-
export function resolveAgentConfig(baseCfg, agentId) {
|
|
377
|
-
if (!agentId || !baseCfg._agentOverrides) return baseCfg;
|
|
378
|
-
const overrides = baseCfg._agentOverrides[agentId];
|
|
379
|
-
if (!overrides || typeof overrides !== "object") return baseCfg;
|
|
380
|
-
|
|
381
|
-
const merged = { ...baseCfg };
|
|
382
|
-
for (const key of AGENT_OVERRIDABLE_KEYS) {
|
|
383
|
-
if (key in overrides) {
|
|
384
|
-
merged[key] = overrides[key];
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
return merged;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
export async function callApi({ baseUrl, apiKey, timeoutMs = 5000, retries = 1 }, path, body) {
|
|
391
|
-
if (!apiKey) {
|
|
392
|
-
throw new Error("Missing MEMOS API key (Token auth)");
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
const headers = {
|
|
396
|
-
"Content-Type": "application/json",
|
|
397
|
-
Authorization: `Token ${apiKey}`,
|
|
398
|
-
};
|
|
399
|
-
|
|
400
|
-
let lastError;
|
|
401
|
-
for (let attempt = 0; attempt <= retries; attempt += 1) {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
if (typeof payload
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
if (entry
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
if (!cfg.
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
if (!
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
let
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
if (
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
if (/\d{4}-\d{2}-\d{2}\
|
|
539
|
-
if (/\d{
|
|
540
|
-
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
if (!
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
if
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
const
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
//
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
//
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
31
|
+
.map((value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
|
32
|
+
.join("|"),
|
|
33
|
+
);
|
|
34
|
+
const ENVELOPE_PREFIX = /^\[([^\]]+)\]:?\s*/;
|
|
35
|
+
const ENVELOPE_CHANNELS = [
|
|
36
|
+
"WebChat",
|
|
37
|
+
"WhatsApp",
|
|
38
|
+
"Telegram",
|
|
39
|
+
"Signal",
|
|
40
|
+
"Slack",
|
|
41
|
+
"Discord",
|
|
42
|
+
"Google Chat",
|
|
43
|
+
"iMessage",
|
|
44
|
+
"Teams",
|
|
45
|
+
"Matrix",
|
|
46
|
+
"Zalo",
|
|
47
|
+
"Zalo Personal",
|
|
48
|
+
"BlueBubbles",
|
|
49
|
+
];
|
|
50
|
+
const MESSAGE_ID_LINE = /^\s*\[message_id:\s*[^\]]+\]\s*$/i;
|
|
51
|
+
const ENV_SOURCES = [
|
|
52
|
+
{ name: "openclaw", path: join(homedir(), ".openclaw", ".env") },
|
|
53
|
+
{ name: "moltbot", path: join(homedir(), ".moltbot", ".env") },
|
|
54
|
+
{ name: "clawdbot", path: join(homedir(), ".clawdbot", ".env") },
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
let envFilesLoaded = false;
|
|
58
|
+
const envFileContents = new Map();
|
|
59
|
+
const envFileValues = new Map();
|
|
60
|
+
|
|
61
|
+
function stripQuotes(value) {
|
|
62
|
+
if (!value) return value;
|
|
63
|
+
const trimmed = value.trim();
|
|
64
|
+
if (
|
|
65
|
+
(trimmed.startsWith("\"") && trimmed.endsWith("\"")) ||
|
|
66
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'"))
|
|
67
|
+
) {
|
|
68
|
+
return trimmed.slice(1, -1);
|
|
69
|
+
}
|
|
70
|
+
return trimmed;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function extractResultData(result) {
|
|
74
|
+
if (!result || typeof result !== "object") return null;
|
|
75
|
+
return result.data ?? result.data?.data ?? result.data?.result ?? null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function pad2(value) {
|
|
79
|
+
return String(value).padStart(2, "0");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function formatTime(value) {
|
|
83
|
+
if (value === undefined || value === null || value === "") return "";
|
|
84
|
+
if (typeof value === "number") {
|
|
85
|
+
const date = new Date(value);
|
|
86
|
+
if (Number.isNaN(date.getTime())) return "";
|
|
87
|
+
return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())} ${pad2(
|
|
88
|
+
date.getHours(),
|
|
89
|
+
)}:${pad2(date.getMinutes())}`;
|
|
90
|
+
}
|
|
91
|
+
if (typeof value === "string") {
|
|
92
|
+
const trimmed = value.trim();
|
|
93
|
+
if (!trimmed) return "";
|
|
94
|
+
if (/^\d+$/.test(trimmed)) return formatTime(Number(trimmed));
|
|
95
|
+
return trimmed;
|
|
96
|
+
}
|
|
97
|
+
return "";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function parseEnvFile(content) {
|
|
101
|
+
const values = new Map();
|
|
102
|
+
for (const line of content.split(/\r?\n/)) {
|
|
103
|
+
const trimmed = line.trim();
|
|
104
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
105
|
+
const idx = trimmed.indexOf("=");
|
|
106
|
+
if (idx <= 0) continue;
|
|
107
|
+
const key = trimmed.slice(0, idx).trim();
|
|
108
|
+
const rawValue = trimmed.slice(idx + 1);
|
|
109
|
+
if (!key) continue;
|
|
110
|
+
values.set(key, stripQuotes(rawValue));
|
|
111
|
+
}
|
|
112
|
+
return values;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function loadEnvFiles() {
|
|
116
|
+
if (envFilesLoaded) return;
|
|
117
|
+
envFilesLoaded = true;
|
|
118
|
+
for (const source of ENV_SOURCES) {
|
|
119
|
+
try {
|
|
120
|
+
const content = readFileSync(source.path, "utf-8");
|
|
121
|
+
envFileContents.set(source.name, content);
|
|
122
|
+
envFileValues.set(source.name, parseEnvFile(content));
|
|
123
|
+
} catch {
|
|
124
|
+
// ignore missing files
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function loadEnvFromFiles(name) {
|
|
130
|
+
for (const source of ENV_SOURCES) {
|
|
131
|
+
const values = envFileValues.get(source.name);
|
|
132
|
+
if (!values) continue;
|
|
133
|
+
if (values.has(name)) return values.get(name);
|
|
134
|
+
}
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function loadEnvVar(name) {
|
|
139
|
+
loadEnvFiles();
|
|
140
|
+
const fromFiles = loadEnvFromFiles(name);
|
|
141
|
+
if (fromFiles !== undefined) return fromFiles;
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function getEnvFileStatus() {
|
|
146
|
+
loadEnvFiles();
|
|
147
|
+
const sources = ENV_SOURCES.filter((source) => envFileContents.has(source.name));
|
|
148
|
+
return {
|
|
149
|
+
found: sources.length > 0,
|
|
150
|
+
sources: sources.map((source) => source.name),
|
|
151
|
+
paths: sources.map((source) => source.path),
|
|
152
|
+
searchPaths: ENV_SOURCES.map((source) => source.path),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function parseBool(value, fallback) {
|
|
157
|
+
if (value === undefined || value === null || value === "") return fallback;
|
|
158
|
+
if (typeof value === "boolean") return value;
|
|
159
|
+
const normalized = String(value).trim().toLowerCase();
|
|
160
|
+
if (["1", "true", "yes", "y", "on"].includes(normalized)) return true;
|
|
161
|
+
if (["0", "false", "no", "n", "off"].includes(normalized)) return false;
|
|
162
|
+
return fallback;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function parseNumber(value, fallback) {
|
|
166
|
+
if (value === undefined || value === null || value === "") return fallback;
|
|
167
|
+
const n = Number(value);
|
|
168
|
+
return Number.isFinite(n) ? n : fallback;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function parseStringArray(value) {
|
|
172
|
+
if (!value) return [];
|
|
173
|
+
if (Array.isArray(value)) return value.map((v) => String(v).trim()).filter(Boolean);
|
|
174
|
+
return String(value)
|
|
175
|
+
.split(",")
|
|
176
|
+
.map((s) => s.trim().replace(/^["']|["']$/g, ""))
|
|
177
|
+
.filter(Boolean);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function parseJsonObject(value) {
|
|
181
|
+
if (!value || typeof value !== "string") return null;
|
|
182
|
+
try {
|
|
183
|
+
const parsed = JSON.parse(value);
|
|
184
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
185
|
+
return parsed;
|
|
186
|
+
}
|
|
187
|
+
} catch {
|
|
188
|
+
// ignore parse error
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function buildConfig(pluginConfig = {}) {
|
|
194
|
+
const cfg = pluginConfig ?? {};
|
|
195
|
+
|
|
196
|
+
const baseUrl = cfg.baseUrl || loadEnvVar("MEMOS_BASE_URL") || DEFAULT_BASE_URL;
|
|
197
|
+
const apiKey = cfg.apiKey || loadEnvVar("MEMOS_API_KEY") || "";
|
|
198
|
+
const userId = cfg.userId || loadEnvVar("MEMOS_USER_ID") || "openclaw-user";
|
|
199
|
+
const conversationId = cfg.conversationId || loadEnvVar("MEMOS_CONVERSATION_ID") || "";
|
|
200
|
+
|
|
201
|
+
const recallGlobal = parseBool(
|
|
202
|
+
cfg.recallGlobal,
|
|
203
|
+
parseBool(loadEnvVar("MEMOS_RECALL_GLOBAL"), true),
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
const conversationIdPrefix = cfg.conversationIdPrefix ?? loadEnvVar("MEMOS_CONVERSATION_PREFIX") ?? "";
|
|
207
|
+
const conversationIdSuffix = cfg.conversationIdSuffix ?? loadEnvVar("MEMOS_CONVERSATION_SUFFIX") ?? "";
|
|
208
|
+
const conversationSuffixMode =
|
|
209
|
+
cfg.conversationSuffixMode ?? loadEnvVar("MEMOS_CONVERSATION_SUFFIX_MODE") ?? "none";
|
|
210
|
+
const resetOnNew = parseBool(
|
|
211
|
+
cfg.resetOnNew,
|
|
212
|
+
parseBool(loadEnvVar("MEMOS_CONVERSATION_RESET_ON_NEW"), true),
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const multiAgentMode = parseBool(
|
|
216
|
+
cfg.multiAgentMode,
|
|
217
|
+
parseBool(loadEnvVar("MEMOS_MULTI_AGENT_MODE"), false),
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
const allowedAgents = parseStringArray(
|
|
221
|
+
cfg.allowedAgents ?? loadEnvVar("MEMOS_ALLOWED_AGENTS"),
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
const recallFilterEnabled = parseBool(
|
|
225
|
+
cfg.recallFilterEnabled,
|
|
226
|
+
parseBool(loadEnvVar("MEMOS_RECALL_FILTER_ENABLED"), false),
|
|
227
|
+
);
|
|
228
|
+
const recallFilterFailOpen = parseBool(
|
|
229
|
+
cfg.recallFilterFailOpen,
|
|
230
|
+
parseBool(loadEnvVar("MEMOS_RECALL_FILTER_FAIL_OPEN"), true),
|
|
231
|
+
);
|
|
232
|
+
const captureStrategy = cfg.captureStrategy ?? (loadEnvVar("MEMOS_CAPTURE_STRATEGY") || "last_turn");
|
|
233
|
+
const asyncMode = cfg.asyncMode ?? parseBool(loadEnvVar("MEMOS_ASYNC_MODE"), true);
|
|
234
|
+
const throttleMs = cfg.throttleMs ?? parseNumber(loadEnvVar("MEMOS_THROTTLE_MS"), 0);
|
|
235
|
+
const includeAssistant =
|
|
236
|
+
cfg.includeAssistant === undefined
|
|
237
|
+
? parseBool(loadEnvVar("MEMOS_INCLUDE_ASSISTANT"), true)
|
|
238
|
+
: cfg.includeAssistant !== false;
|
|
239
|
+
const maxMessageChars = cfg.maxMessageChars ?? parseNumber(loadEnvVar("MEMOS_MAX_MESSAGE_CHARS"), 20000);
|
|
240
|
+
const rumEnabled = parseBool(
|
|
241
|
+
cfg.rumEnabled,
|
|
242
|
+
parseBool(loadEnvVar("MEMOS_RUM_ENABLED"), true),
|
|
243
|
+
);
|
|
244
|
+
const useDirectSessionUserId = parseBool(
|
|
245
|
+
cfg.useDirectSessionUserId,
|
|
246
|
+
parseBool(loadEnvVar("MEMOS_USE_DIRECT_SESSION_USER_ID"), false),
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
baseUrl: baseUrl.replace(/\/+$/, ""),
|
|
251
|
+
apiKey,
|
|
252
|
+
userId,
|
|
253
|
+
conversationId,
|
|
254
|
+
conversationIdPrefix,
|
|
255
|
+
conversationIdSuffix,
|
|
256
|
+
conversationSuffixMode,
|
|
257
|
+
useDirectSessionUserId,
|
|
258
|
+
recallGlobal,
|
|
259
|
+
resetOnNew,
|
|
260
|
+
envFileStatus: getEnvFileStatus(),
|
|
261
|
+
queryPrefix: cfg.queryPrefix ?? "",
|
|
262
|
+
maxQueryChars: cfg.maxQueryChars ?? 0,
|
|
263
|
+
recallEnabled: cfg.recallEnabled !== false,
|
|
264
|
+
addEnabled: cfg.addEnabled !== false,
|
|
265
|
+
captureStrategy,
|
|
266
|
+
maxMessageChars,
|
|
267
|
+
maxItemChars: cfg.maxItemChars ?? 8000,
|
|
268
|
+
includeAssistant,
|
|
269
|
+
memoryLimitNumber: cfg.memoryLimitNumber ?? 9,
|
|
270
|
+
preferenceLimitNumber: cfg.preferenceLimitNumber ?? 6,
|
|
271
|
+
includePreference: cfg.includePreference !== false,
|
|
272
|
+
includeToolMemory: cfg.includeToolMemory === true,
|
|
273
|
+
toolMemoryLimitNumber: cfg.toolMemoryLimitNumber ?? 6,
|
|
274
|
+
relativity: cfg.relativity ?? ((() => {
|
|
275
|
+
const v = loadEnvVar("MEMOS_RELATIVITY");
|
|
276
|
+
return v ? parseFloat(v) : 0.45;
|
|
277
|
+
})()),
|
|
278
|
+
filter: cfg.filter,
|
|
279
|
+
knowledgebaseIds: cfg.knowledgebaseIds ?? (loadEnvVar("MEMOS_KNOWLEDGEBASE_IDS") ? parseStringArray(loadEnvVar("MEMOS_KNOWLEDGEBASE_IDS")) : []),
|
|
280
|
+
tags: cfg.tags ?? (loadEnvVar("MEMOS_TAGS") ? parseStringArray(loadEnvVar("MEMOS_TAGS")) : ["openclaw"]),
|
|
281
|
+
info: cfg.info ?? {},
|
|
282
|
+
agentId: cfg.agentId,
|
|
283
|
+
appId: cfg.appId,
|
|
284
|
+
allowPublic: cfg.allowPublic ?? false,
|
|
285
|
+
allowKnowledgebaseIds: cfg.allowKnowledgebaseIds ?? (loadEnvVar("MEMOS_ALLOW_KNOWLEDGEBASE_IDS") ? parseStringArray(loadEnvVar("MEMOS_ALLOW_KNOWLEDGEBASE_IDS")) : []),
|
|
286
|
+
asyncMode,
|
|
287
|
+
multiAgentMode,
|
|
288
|
+
allowedAgents,
|
|
289
|
+
recallFilterEnabled,
|
|
290
|
+
recallFilterBaseUrl:
|
|
291
|
+
(cfg.recallFilterBaseUrl ?? loadEnvVar("MEMOS_RECALL_FILTER_BASE_URL") ?? "").replace(/\/+$/, ""),
|
|
292
|
+
recallFilterApiKey: cfg.recallFilterApiKey ?? loadEnvVar("MEMOS_RECALL_FILTER_API_KEY") ?? "",
|
|
293
|
+
recallFilterModel: cfg.recallFilterModel ?? loadEnvVar("MEMOS_RECALL_FILTER_MODEL") ?? "",
|
|
294
|
+
recallFilterTimeoutMs: parseNumber(
|
|
295
|
+
cfg.recallFilterTimeoutMs ?? loadEnvVar("MEMOS_RECALL_FILTER_TIMEOUT_MS"),
|
|
296
|
+
30000,
|
|
297
|
+
),
|
|
298
|
+
recallFilterRetries: parseNumber(cfg.recallFilterRetries ?? loadEnvVar("MEMOS_RECALL_FILTER_RETRIES"), 1),
|
|
299
|
+
recallFilterCandidateLimit:
|
|
300
|
+
parseNumber(cfg.recallFilterCandidateLimit ?? loadEnvVar("MEMOS_RECALL_FILTER_CANDIDATE_LIMIT"), 30),
|
|
301
|
+
recallFilterMaxItemChars:
|
|
302
|
+
parseNumber(cfg.recallFilterMaxItemChars ?? loadEnvVar("MEMOS_RECALL_FILTER_MAX_ITEM_CHARS"), 500),
|
|
303
|
+
recallFilterFailOpen,
|
|
304
|
+
timeoutMs: cfg.timeoutMs ?? 5000,
|
|
305
|
+
retries: cfg.retries ?? 1,
|
|
306
|
+
throttleMs,
|
|
307
|
+
rumEnabled,
|
|
308
|
+
_agentOverrides: cfg.agentOverrides ?? parseJsonObject(loadEnvVar("MEMOS_AGENT_OVERRIDES")) ?? {},
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function hasConfigValue(cfg, field) {
|
|
313
|
+
const configKey = field.configKey ?? field.key;
|
|
314
|
+
if (field.configMode === "truthy") return Boolean(cfg[configKey]);
|
|
315
|
+
return cfg[configKey] !== undefined && cfg[configKey] !== null;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function hasEnvValue(envValue, envMode) {
|
|
319
|
+
if (envMode === "truthy") return Boolean(envValue);
|
|
320
|
+
if (envMode === "defined") return envValue !== undefined;
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function resolveInheritedValue(field, resolved, envRaw) {
|
|
325
|
+
if (Object.prototype.hasOwnProperty.call(field, "inheritedValue")) {
|
|
326
|
+
return field.inheritedValue;
|
|
327
|
+
}
|
|
328
|
+
if (field.inheritedFrom === "env") {
|
|
329
|
+
const envValue = field.envVar ? envRaw[field.envVar] : undefined;
|
|
330
|
+
return envValue ?? field.inheritedFallback;
|
|
331
|
+
}
|
|
332
|
+
const resolvedKey = field.resolvedKey ?? field.key;
|
|
333
|
+
return resolved[resolvedKey];
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function getConfigResolution(pluginConfig = {}) {
|
|
337
|
+
const cfg = pluginConfig ?? {};
|
|
338
|
+
const resolved = buildConfig(cfg);
|
|
339
|
+
const envRaw = {};
|
|
340
|
+
|
|
341
|
+
for (const field of CONFIG_RESOLUTION_FIELDS) {
|
|
342
|
+
if (!field.envVar) continue;
|
|
343
|
+
if (Object.prototype.hasOwnProperty.call(envRaw, field.envVar)) continue;
|
|
344
|
+
envRaw[field.envVar] = loadEnvVar(field.envVar);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const fieldMeta = {};
|
|
348
|
+
for (const field of CONFIG_RESOLUTION_FIELDS) {
|
|
349
|
+
const envValue = field.envVar ? envRaw[field.envVar] : undefined;
|
|
350
|
+
const source = hasConfigValue(cfg, field)
|
|
351
|
+
? "config"
|
|
352
|
+
: hasEnvValue(envValue, field.envMode)
|
|
353
|
+
? "env"
|
|
354
|
+
: field.fallbackSource;
|
|
355
|
+
fieldMeta[field.key] = {
|
|
356
|
+
source,
|
|
357
|
+
inheritedValue: resolveInheritedValue(field, resolved, envRaw),
|
|
358
|
+
uiDefaultValue: field.uiDefaultValue,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return { resolved, fieldMeta };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const AGENT_OVERRIDABLE_KEYS = [
|
|
366
|
+
"knowledgebaseIds", "memoryLimitNumber", "preferenceLimitNumber",
|
|
367
|
+
"includePreference", "includeToolMemory", "toolMemoryLimitNumber",
|
|
368
|
+
"relativity",
|
|
369
|
+
"recallEnabled", "addEnabled", "captureStrategy", "queryPrefix",
|
|
370
|
+
"maxItemChars", "maxMessageChars", "includeAssistant",
|
|
371
|
+
"recallGlobal", "recallFilterEnabled", "recallFilterModel",
|
|
372
|
+
"recallFilterBaseUrl", "recallFilterApiKey",
|
|
373
|
+
"allowKnowledgebaseIds", "tags", "throttleMs",
|
|
374
|
+
];
|
|
375
|
+
|
|
376
|
+
export function resolveAgentConfig(baseCfg, agentId) {
|
|
377
|
+
if (!agentId || !baseCfg._agentOverrides) return baseCfg;
|
|
378
|
+
const overrides = baseCfg._agentOverrides[agentId];
|
|
379
|
+
if (!overrides || typeof overrides !== "object") return baseCfg;
|
|
380
|
+
|
|
381
|
+
const merged = { ...baseCfg };
|
|
382
|
+
for (const key of AGENT_OVERRIDABLE_KEYS) {
|
|
383
|
+
if (key in overrides) {
|
|
384
|
+
merged[key] = overrides[key];
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return merged;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export async function callApi({ baseUrl, apiKey, timeoutMs = 5000, retries = 1 }, path, body) {
|
|
391
|
+
if (!apiKey) {
|
|
392
|
+
throw new Error("Missing MEMOS API key (Token auth)");
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const headers = {
|
|
396
|
+
"Content-Type": "application/json",
|
|
397
|
+
Authorization: `Token ${apiKey}`,
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
let lastError;
|
|
401
|
+
for (let attempt = 0; attempt <= retries; attempt += 1) {
|
|
402
|
+
let timeoutId;
|
|
403
|
+
try {
|
|
404
|
+
const controller = new AbortController();
|
|
405
|
+
timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
406
|
+
|
|
407
|
+
const res = await fetch(`${baseUrl}${path}`, {
|
|
408
|
+
method: "POST",
|
|
409
|
+
headers,
|
|
410
|
+
body: JSON.stringify(body),
|
|
411
|
+
signal: controller.signal,
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
if (!res.ok) {
|
|
415
|
+
throw new Error(`HTTP ${res.status}`);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return await res.json();
|
|
419
|
+
} catch (err) {
|
|
420
|
+
lastError = err;
|
|
421
|
+
if (attempt < retries) {
|
|
422
|
+
await delay(100 * (attempt + 1));
|
|
423
|
+
}
|
|
424
|
+
} finally {
|
|
425
|
+
if (timeoutId !== undefined) clearTimeout(timeoutId);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
throw lastError;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export function sanitizeSearchPayload(payload) {
|
|
433
|
+
if (!payload || typeof payload !== "object") return payload;
|
|
434
|
+
if (typeof payload.query !== "string") return payload;
|
|
435
|
+
const query = stripOpenClawInjectedPrefix(payload.query);
|
|
436
|
+
if (query === payload.query) return payload;
|
|
437
|
+
return { ...payload, query };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function sanitizeAddMessageEntry(entry) {
|
|
441
|
+
if (!entry || typeof entry !== "object") return entry;
|
|
442
|
+
if (entry.role !== "user" || typeof entry.content !== "string") return entry;
|
|
443
|
+
const content = stripOpenClawInjectedPrefix(entry.content);
|
|
444
|
+
if (content === entry.content) return entry;
|
|
445
|
+
return { ...entry, content };
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export function isAgentAllowed(cfg, ctx) {
|
|
449
|
+
if (!cfg.multiAgentMode) return true;
|
|
450
|
+
if (!cfg.allowedAgents || cfg.allowedAgents.length === 0) return true;
|
|
451
|
+
const agentId = ctx?.agentId || cfg.agentId || "main";
|
|
452
|
+
return cfg.allowedAgents.includes(agentId);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
export async function searchMemory(cfg, payload) {
|
|
456
|
+
return callApi(cfg, "/search/memory", sanitizeSearchPayload(payload));
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export async function addMessage(cfg, payload) {
|
|
460
|
+
let finalPayload = payload;
|
|
461
|
+
try {
|
|
462
|
+
finalPayload = sanitizeAddMessagePayload(payload);
|
|
463
|
+
} catch {
|
|
464
|
+
// Fail open: if sanitization throws unexpectedly, send original payload.
|
|
465
|
+
finalPayload = payload;
|
|
466
|
+
}
|
|
467
|
+
return callApi(cfg, "/add/message", finalPayload);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function isInboundMetaSentinelLine(line) {
|
|
471
|
+
const trimmed = line.trim();
|
|
472
|
+
return INBOUND_META_SENTINELS.some((sentinel) => sentinel === trimmed);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function shouldStripTrailingUntrustedContext(lines, index) {
|
|
476
|
+
if (lines[index]?.trim() !== UNTRUSTED_CONTEXT_HEADER) return false;
|
|
477
|
+
const probe = lines.slice(index + 1, Math.min(lines.length, index + 8)).join("\n");
|
|
478
|
+
return /<<<EXTERNAL_UNTRUSTED_CONTENT|UNTRUSTED channel metadata \(|Source:\s+/.test(probe);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function stripTrailingUntrustedContextSuffix(lines) {
|
|
482
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
483
|
+
if (!shouldStripTrailingUntrustedContext(lines, index)) continue;
|
|
484
|
+
let end = index;
|
|
485
|
+
while (end > 0 && lines[end - 1]?.trim() === "") {
|
|
486
|
+
end -= 1;
|
|
487
|
+
}
|
|
488
|
+
return lines.slice(0, end);
|
|
489
|
+
}
|
|
490
|
+
return lines;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function stripLeadingInboundMetadata(text) {
|
|
494
|
+
if (!text || typeof text !== "string") return "";
|
|
495
|
+
if (!SENTINEL_FAST_RE.test(text)) return text;
|
|
496
|
+
|
|
497
|
+
const lines = text.split(/\r?\n/);
|
|
498
|
+
let index = 0;
|
|
499
|
+
let strippedAny = false;
|
|
500
|
+
|
|
501
|
+
while (index < lines.length && lines[index].trim() === "") {
|
|
502
|
+
index += 1;
|
|
503
|
+
}
|
|
504
|
+
if (index >= lines.length) return "";
|
|
505
|
+
if (!isInboundMetaSentinelLine(lines[index])) {
|
|
506
|
+
return stripTrailingUntrustedContextSuffix(lines).join("\n");
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
while (index < lines.length) {
|
|
510
|
+
if (!isInboundMetaSentinelLine(lines[index])) break;
|
|
511
|
+
const blockStart = index;
|
|
512
|
+
index += 1;
|
|
513
|
+
if (index >= lines.length || lines[index].trim() !== "```json") {
|
|
514
|
+
return strippedAny
|
|
515
|
+
? stripTrailingUntrustedContextSuffix(lines.slice(blockStart)).join("\n")
|
|
516
|
+
: text;
|
|
517
|
+
}
|
|
518
|
+
index += 1;
|
|
519
|
+
while (index < lines.length && lines[index].trim() !== "```") {
|
|
520
|
+
index += 1;
|
|
521
|
+
}
|
|
522
|
+
if (index >= lines.length) {
|
|
523
|
+
return strippedAny
|
|
524
|
+
? stripTrailingUntrustedContextSuffix(lines.slice(blockStart)).join("\n")
|
|
525
|
+
: text;
|
|
526
|
+
}
|
|
527
|
+
index += 1;
|
|
528
|
+
strippedAny = true;
|
|
529
|
+
while (index < lines.length && lines[index].trim() === "") {
|
|
530
|
+
index += 1;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return stripTrailingUntrustedContextSuffix(lines.slice(index)).join("\n");
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function looksLikeEnvelopeHeader(header) {
|
|
538
|
+
if (/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}Z\b/.test(header)) return true;
|
|
539
|
+
if (/\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}\b/.test(header)) return true;
|
|
540
|
+
if (/\d{1,2}:\d{2}\s*(?:AM|PM)\s+on\s+\d{1,2}\s+[A-Za-z]+,\s+\d{4}\b/i.test(header)) return true;
|
|
541
|
+
return ENVELOPE_CHANNELS.some((label) => header.startsWith(`${label} `));
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function stripLeadingEnvelope(text) {
|
|
545
|
+
if (!text || typeof text !== "string") return "";
|
|
546
|
+
const match = text.match(ENVELOPE_PREFIX);
|
|
547
|
+
if (!match) return text;
|
|
548
|
+
if (!looksLikeEnvelopeHeader(match[1] ?? "")) return text;
|
|
549
|
+
return text.slice(match[0].length);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function stripLeadingMessageIdHints(text) {
|
|
553
|
+
if (!text || typeof text !== "string" || !text.includes("[message_id:")) return text;
|
|
554
|
+
const lines = text.split(/\r?\n/);
|
|
555
|
+
let index = 0;
|
|
556
|
+
while (index < lines.length && MESSAGE_ID_LINE.test(lines[index])) {
|
|
557
|
+
index += 1;
|
|
558
|
+
while (index < lines.length && lines[index].trim() === "") {
|
|
559
|
+
index += 1;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
return index === 0 ? text : lines.slice(index).join("\n");
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function stripTrailingFeishuSystemHints(text) {
|
|
566
|
+
if (!text || typeof text !== "string") return text;
|
|
567
|
+
const pattern = /(?:\s*\[System:\s[^\]]*\])+\s*$/;
|
|
568
|
+
if (!pattern.test(text)) return text;
|
|
569
|
+
const stripped = text.replace(pattern, "").trim();
|
|
570
|
+
return stripped || text;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function stripLeadingSystemNote(text) {
|
|
574
|
+
if (!text || typeof text !== "string") return text;
|
|
575
|
+
const lines = text.split(/\r?\n/);
|
|
576
|
+
let index = 0;
|
|
577
|
+
|
|
578
|
+
// Skip leading empty lines
|
|
579
|
+
while (index < lines.length && lines[index].trim() === "") {
|
|
580
|
+
index += 1;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (index >= lines.length) return "";
|
|
584
|
+
|
|
585
|
+
// Check if first non-empty line matches system note pattern
|
|
586
|
+
if (!SYSTEM_NOTE_PREFIX.test(lines[index])) return text;
|
|
587
|
+
|
|
588
|
+
// Skip the system note line
|
|
589
|
+
index += 1;
|
|
590
|
+
|
|
591
|
+
// Skip trailing empty lines after the note
|
|
592
|
+
while (index < lines.length && lines[index].trim() === "") {
|
|
593
|
+
index += 1;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
return index === 0 ? text : lines.slice(index).join("\n");
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function stripLeadingFeishuSenderPrefix(text) {
|
|
600
|
+
if (!text || typeof text !== "string") return text;
|
|
601
|
+
// Feishu user IDs are typically "ou_<id>". Strip only if it is the leading line prefix.
|
|
602
|
+
const match = text.match(/^(\s*)ou_[a-z0-9_-]+:\s*/i);
|
|
603
|
+
if (!match) return text;
|
|
604
|
+
const stripped = text.slice(match[0].length);
|
|
605
|
+
return stripped || text;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function stripFeishuInjectedPrompt(text) {
|
|
609
|
+
if (!text || typeof text !== "string") return text;
|
|
610
|
+
const hasFeishuSystemHeader = /^System: \[.*?\] Feishu\[.*?\]/.test(text);
|
|
611
|
+
const hasLeadingMessageIdAndSender =
|
|
612
|
+
/^\s*\[message_id: [^\]]+\]\s*(?:\r?\n\s*)?ou_[a-z0-9_-]+:\s*/i.test(text);
|
|
613
|
+
// Keep legacy Feishu header path and support newer payloads that directly start with
|
|
614
|
+
// "[message_id] + ou_xxx:".
|
|
615
|
+
if (!hasFeishuSystemHeader && !hasLeadingMessageIdAndSender) {
|
|
616
|
+
return text;
|
|
617
|
+
}
|
|
618
|
+
// Remove only the first injected Feishu prompt prefix.
|
|
619
|
+
// Any later "[message_id] ou_xxx:" pattern should be treated as user query content.
|
|
620
|
+
const leadingInjectedPattern = /^[\s\S]*?\[message_id: [^\]]+\]\s*(?:\r?\n\s*)?ou_[a-z0-9_-]+:\s*/i;
|
|
621
|
+
if (leadingInjectedPattern.test(text)) {
|
|
622
|
+
return text.replace(leadingInjectedPattern, "").trim();
|
|
623
|
+
}
|
|
624
|
+
return text;
|
|
625
|
+
}
|
|
626
|
+
|
|
626
627
|
export function sanitizeAddMessagePayload(payload) {
|
|
627
628
|
if (!payload || typeof payload !== "object") return payload;
|
|
628
629
|
const nextPayload = { ...payload };
|
|
629
|
-
if (typeof nextPayload.query === "string") {
|
|
630
|
-
nextPayload.query = stripOpenClawInjectedPrefix(nextPayload.query);
|
|
631
|
-
}
|
|
632
|
-
if (Array.isArray(nextPayload.messages)) {
|
|
633
|
-
nextPayload.messages = nextPayload.messages.map((msg) => sanitizeAddMessageEntry(msg));
|
|
634
|
-
}
|
|
630
|
+
if (typeof nextPayload.query === "string") {
|
|
631
|
+
nextPayload.query = stripOpenClawInjectedPrefix(nextPayload.query);
|
|
632
|
+
}
|
|
633
|
+
if (Array.isArray(nextPayload.messages)) {
|
|
634
|
+
nextPayload.messages = nextPayload.messages.map((msg) => sanitizeAddMessageEntry(msg));
|
|
635
|
+
}
|
|
635
636
|
return nextPayload;
|
|
636
637
|
}
|
|
637
638
|
|
|
@@ -653,254 +654,254 @@ export function stripOpenClawInjectedPrefix(text) {
|
|
|
653
654
|
const cleanedText = stripFeishuInjectedPrompt(text);
|
|
654
655
|
if (isOpenClawSystemPrompt(cleanedText)) return "";
|
|
655
656
|
const markerIndex = cleanedText.lastIndexOf(USER_QUERY_MARKER);
|
|
656
|
-
const withoutRecallPrefix =
|
|
657
|
-
markerIndex === -1
|
|
658
|
-
? cleanedText
|
|
659
|
-
: cleanedText.slice(markerIndex + USER_QUERY_MARKER.length);
|
|
660
|
-
const withoutSystemNote = stripLeadingSystemNote(withoutRecallPrefix).trimStart();
|
|
661
|
-
const withoutInboundMetadata = stripLeadingInboundMetadata(withoutSystemNote).trimStart();
|
|
662
|
-
const withoutMessageIdHints = stripLeadingMessageIdHints(withoutInboundMetadata).trimStart();
|
|
663
|
-
const withoutEnvelope = stripLeadingEnvelope(withoutMessageIdHints).trimStart();
|
|
664
|
-
const withoutTrailingSystemHints = stripTrailingFeishuSystemHints(withoutEnvelope).trimStart();
|
|
665
|
-
return stripLeadingFeishuSenderPrefix(withoutTrailingSystemHints).trimStart();
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
export function extractText(content) {
|
|
669
|
-
if (!content) return "";
|
|
670
|
-
if (typeof content === "string") return content;
|
|
671
|
-
if (Array.isArray(content)) {
|
|
672
|
-
return content
|
|
673
|
-
.filter((block) => block && typeof block === "object" && block.type === "text")
|
|
674
|
-
.map((block) => block.text)
|
|
675
|
-
.join(" ");
|
|
676
|
-
}
|
|
677
|
-
return "";
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
function normalizePreferenceType(value) {
|
|
681
|
-
if (!value) return "";
|
|
682
|
-
const normalized = String(value).trim().toLowerCase();
|
|
683
|
-
if (!normalized) return "";
|
|
684
|
-
if (normalized.includes("explicit")) return "Explicit Preference";
|
|
685
|
-
if (normalized.includes("implicit")) return "Implicit Preference";
|
|
686
|
-
return String(value)
|
|
687
|
-
.replace(/[_-]+/g, " ")
|
|
688
|
-
.replace(/\b\w/g, (ch) => ch.toUpperCase());
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
function sanitizeInlineText(text) {
|
|
692
|
-
if (text === undefined || text === null) return "";
|
|
693
|
-
return String(text).replace(/\r?\n+/g, " ").trim();
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
function resolveDisplayTime(item) {
|
|
697
|
-
return item?.update_time ?? item?.create_time;
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
function formatMemoryLine(item, text, options = {}) {
|
|
701
|
-
const cleaned = sanitizeInlineText(text);
|
|
702
|
-
if (!cleaned) return "";
|
|
703
|
-
const maxChars = options.maxItemChars;
|
|
704
|
-
const truncated = truncate(cleaned, maxChars);
|
|
705
|
-
const time = formatTime(resolveDisplayTime(item));
|
|
706
|
-
if (time) return ` -[${time}] ${truncated}`;
|
|
707
|
-
return ` - ${truncated}`;
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
function formatPreferenceLine(item, text, options = {}) {
|
|
711
|
-
const cleaned = sanitizeInlineText(text);
|
|
712
|
-
if (!cleaned) return "";
|
|
713
|
-
const maxChars = options.maxItemChars;
|
|
714
|
-
const truncated = truncate(cleaned, maxChars);
|
|
715
|
-
const time = formatTime(resolveDisplayTime(item));
|
|
716
|
-
const type = normalizePreferenceType(item?.preference_type);
|
|
717
|
-
const typeLabel = type ? ` [${type}]` : "";
|
|
718
|
-
if (time) return ` -[${time}]${typeLabel} ${truncated}`;
|
|
719
|
-
return ` -${typeLabel} ${truncated}`;
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
function wrapCodeBlock(lines, options = {}) {
|
|
723
|
-
if (!options.wrapTagBlocks) return lines;
|
|
724
|
-
return ["```text", ...lines, "```"];
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
function buildMemorySections(data, options = {}) {
|
|
728
|
-
const memoryList = data?.memory_detail_list ?? [];
|
|
729
|
-
const preferenceList = data?.preference_detail_list ?? [];
|
|
730
|
-
const toolMemoryList = data?.tool_memory_detail_list ?? [];
|
|
731
|
-
|
|
732
|
-
const threshold = options.relativity ?? 0;
|
|
733
|
-
|
|
734
|
-
const memoryLines = memoryList
|
|
735
|
-
.filter((item) => (item?.relativity ?? 1) > threshold)
|
|
736
|
-
.map((item) => {
|
|
737
|
-
const text = item?.memory_value || item?.memory_key || "";
|
|
738
|
-
return formatMemoryLine(item, text, options);
|
|
739
|
-
})
|
|
740
|
-
.filter(Boolean);
|
|
741
|
-
|
|
742
|
-
const preferenceLines = preferenceList
|
|
743
|
-
.filter((item) => (item?.relativity ?? 1) > threshold)
|
|
744
|
-
.map((item) => {
|
|
745
|
-
const text = item?.preference || "";
|
|
746
|
-
return formatPreferenceLine(item, text, options);
|
|
747
|
-
})
|
|
748
|
-
.filter(Boolean);
|
|
749
|
-
|
|
750
|
-
const toolMemoryLines = toolMemoryList
|
|
751
|
-
.filter((item) => (item?.relativity ?? 1) > threshold)
|
|
752
|
-
.map((item) => {
|
|
753
|
-
const text = item?.tool_value || "";
|
|
754
|
-
return formatMemoryLine(item, text, options);
|
|
755
|
-
})
|
|
756
|
-
.filter(Boolean);
|
|
757
|
-
|
|
758
|
-
return { memoryLines, preferenceLines, toolMemoryLines };
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
const STATIC_RECALL_SYSTEM_PROMPT = [
|
|
762
|
-
"# Role",
|
|
763
|
-
"",
|
|
764
|
-
"You are an intelligent assistant with long-term memory capabilities (MemOS Assistant). Your goal is to combine retrieved memory fragments to provide highly personalized, accurate, and logically rigorous responses.",
|
|
765
|
-
"",
|
|
766
|
-
"# System Context",
|
|
767
|
-
"",
|
|
768
|
-
"* Current Time: Use the runtime-provided current time as the baseline for freshness checks.",
|
|
769
|
-
"* Additional memory context for the current turn may be prepended before the original user query as a structured `<memories>` block.",
|
|
770
|
-
"",
|
|
771
|
-
"# Memory Data",
|
|
772
|
-
"",
|
|
773
|
-
'Below is the information retrieved by MemOS, categorized into "Facts" and "Preferences".',
|
|
774
|
-
"* **Facts**: May include user attributes, historical conversations, or third-party details.",
|
|
775
|
-
"* **Special Note**: Content tagged with '[assistant观点]' or '[模型总结]' represents **past AI inference**, **not** direct user statements.",
|
|
776
|
-
"* **Preferences**: The user's explicit or implicit requirements on response style, format, or reasoning.",
|
|
777
|
-
"",
|
|
778
|
-
"# Critical Protocol: Memory Safety",
|
|
779
|
-
"",
|
|
780
|
-
"Retrieved memories may contain **AI speculation**, **irrelevant noise**, or **wrong subject attribution**. You must strictly apply the **Four-Step Verdict**. If any step fails, **discard the memory**:",
|
|
781
|
-
"",
|
|
782
|
-
"1. **Source Verification**:",
|
|
783
|
-
"* **Core**: Distinguish direct user statements from AI inference.",
|
|
784
|
-
"* If a memory has tags like '[assistant观点]' or '[模型总结]', treat it as a **hypothesis**, not a user-grounded fact.",
|
|
785
|
-
"* *Counterexample*: If memory says '[assistant观点] User loves mangoes' but the user never said that, do not assume it as fact.",
|
|
786
|
-
"* **Principle: AI summaries are reference-only and have much lower authority than direct user statements.**",
|
|
787
|
-
"",
|
|
788
|
-
"2. **Attribution Check**:",
|
|
789
|
-
"* Is the subject in memory definitely the user?",
|
|
790
|
-
"* If the memory describes a **third party** (e.g., candidate, interviewee, fictional character, case data), never attribute it to the user.",
|
|
791
|
-
"",
|
|
792
|
-
"3. **Strong Relevance Check**:",
|
|
793
|
-
"* Does the memory directly help answer the current 'Original Query'?",
|
|
794
|
-
"* If it is only a keyword overlap with different context, ignore it.",
|
|
795
|
-
"",
|
|
796
|
-
"4. **Freshness Check**:",
|
|
797
|
-
"* If memory conflicts with the user's latest intent, prioritize the current 'Original Query' as the highest source of truth.",
|
|
798
|
-
"",
|
|
799
|
-
"# Instructions",
|
|
800
|
-
"",
|
|
801
|
-
"1. **Review**: Read '<facts>' first and apply the Four-Step Verdict to remove noise and unreliable AI inference.",
|
|
802
|
-
"2. **Execute**:",
|
|
803
|
-
" - Use only memories that pass filtering as context.",
|
|
804
|
-
" - Strictly follow style requirements from '<preferences>'.",
|
|
805
|
-
"3. **Output**: Answer directly. Never mention internal terms such as \"memory store\", \"retrieval\", or \"AI opinions\".",
|
|
806
|
-
"4. **Attention**: Additional memory context may already be provided before the original user query. Do not read from or write to local `MEMORY.md` or `memory/*` files for reference, as they may be outdated or irrelevant to the current query.",
|
|
807
|
-
].join("\n");
|
|
808
|
-
|
|
809
|
-
function buildMemoryPrependBlock(data, options = {}) {
|
|
810
|
-
const { memoryLines, preferenceLines, toolMemoryLines } = buildMemorySections(data, options);
|
|
811
|
-
const hasContent = memoryLines.length > 0 || preferenceLines.length > 0 || toolMemoryLines.length > 0;
|
|
812
|
-
if (!hasContent) return "";
|
|
813
|
-
|
|
814
|
-
const memoriesBlock = [
|
|
815
|
-
"<memories>",
|
|
816
|
-
" <facts>",
|
|
817
|
-
...memoryLines,
|
|
818
|
-
" </facts>",
|
|
819
|
-
" <tool_memories>",
|
|
820
|
-
...toolMemoryLines,
|
|
821
|
-
" </tool_memories>",
|
|
822
|
-
" <preferences>",
|
|
823
|
-
...preferenceLines,
|
|
824
|
-
" </preferences>",
|
|
825
|
-
"</memories>",
|
|
826
|
-
];
|
|
827
|
-
|
|
828
|
-
return [...wrapCodeBlock(memoriesBlock, options), "", USER_QUERY_MARKER].join("\n");
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
export function formatPromptBlockFromData(data, options = {}) {
|
|
832
|
-
if (!data || typeof data !== "object") return "";
|
|
833
|
-
return buildMemoryPrependBlock(data, options);
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
export function formatPromptBlock(result, options = {}) {
|
|
837
|
-
const data = extractResultData(result);
|
|
838
|
-
return formatPromptBlockFromData(data, options);
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
export function formatContextBlock(result, options = {}) {
|
|
842
|
-
const data = extractResultData(result);
|
|
843
|
-
if (!data) return "";
|
|
844
|
-
|
|
845
|
-
const memoryList = data.memory_detail_list ?? [];
|
|
846
|
-
const prefList = data.preference_detail_list ?? [];
|
|
847
|
-
const toolList = data.tool_memory_detail_list ?? [];
|
|
848
|
-
const preferenceNote = data.preference_note;
|
|
849
|
-
|
|
850
|
-
const lines = [];
|
|
851
|
-
if (memoryList.length > 0) {
|
|
852
|
-
lines.push("Facts:");
|
|
853
|
-
for (const item of memoryList) {
|
|
854
|
-
const text = item?.memory_value || item?.memory_key || "";
|
|
855
|
-
if (!text) continue;
|
|
856
|
-
lines.push(`- ${truncate(text, options.maxItemChars)}`);
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
if (prefList.length > 0) {
|
|
861
|
-
lines.push("Preferences:");
|
|
862
|
-
for (const item of prefList) {
|
|
863
|
-
const pref = item?.preference || "";
|
|
864
|
-
const type = item?.preference_type ? `(${item.preference_type}) ` : "";
|
|
865
|
-
if (!pref) continue;
|
|
866
|
-
lines.push(`- ${type}${truncate(pref, options.maxItemChars)}`);
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
if (toolList.length > 0) {
|
|
871
|
-
lines.push("Tool Memories:");
|
|
872
|
-
for (const item of toolList) {
|
|
873
|
-
const value = item?.tool_value || "";
|
|
874
|
-
if (!value) continue;
|
|
875
|
-
lines.push(`- ${truncate(value, options.maxItemChars)}`);
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
if (preferenceNote) {
|
|
880
|
-
lines.push(`Preference Note: ${truncate(preferenceNote, options.maxItemChars)}`);
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
return lines.length > 0 ? lines.join("\n") : "";
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
export function formatRecallHookResult(result, options = {}) {
|
|
887
|
-
const data = extractResultData(result);
|
|
888
|
-
if (!data) {
|
|
889
|
-
return {
|
|
890
|
-
appendSystemContext: "",
|
|
891
|
-
prependContext: "",
|
|
892
|
-
};
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
return {
|
|
896
|
-
// Keep this system addendum byte-stable across turns so provider-side prefix caching can hit.
|
|
897
|
-
appendSystemContext: STATIC_RECALL_SYSTEM_PROMPT,
|
|
898
|
-
prependContext: buildMemoryPrependBlock(data, options),
|
|
899
|
-
};
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
function truncate(text, maxLen) {
|
|
903
|
-
if (!text) return "";
|
|
904
|
-
const limit = maxLen || 10000;
|
|
905
|
-
return text.length > limit ? `${text.slice(0, limit)}...` : text;
|
|
906
|
-
}
|
|
657
|
+
const withoutRecallPrefix =
|
|
658
|
+
markerIndex === -1
|
|
659
|
+
? cleanedText
|
|
660
|
+
: cleanedText.slice(markerIndex + USER_QUERY_MARKER.length);
|
|
661
|
+
const withoutSystemNote = stripLeadingSystemNote(withoutRecallPrefix).trimStart();
|
|
662
|
+
const withoutInboundMetadata = stripLeadingInboundMetadata(withoutSystemNote).trimStart();
|
|
663
|
+
const withoutMessageIdHints = stripLeadingMessageIdHints(withoutInboundMetadata).trimStart();
|
|
664
|
+
const withoutEnvelope = stripLeadingEnvelope(withoutMessageIdHints).trimStart();
|
|
665
|
+
const withoutTrailingSystemHints = stripTrailingFeishuSystemHints(withoutEnvelope).trimStart();
|
|
666
|
+
return stripLeadingFeishuSenderPrefix(withoutTrailingSystemHints).trimStart();
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
export function extractText(content) {
|
|
670
|
+
if (!content) return "";
|
|
671
|
+
if (typeof content === "string") return content;
|
|
672
|
+
if (Array.isArray(content)) {
|
|
673
|
+
return content
|
|
674
|
+
.filter((block) => block && typeof block === "object" && block.type === "text")
|
|
675
|
+
.map((block) => block.text)
|
|
676
|
+
.join(" ");
|
|
677
|
+
}
|
|
678
|
+
return "";
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function normalizePreferenceType(value) {
|
|
682
|
+
if (!value) return "";
|
|
683
|
+
const normalized = String(value).trim().toLowerCase();
|
|
684
|
+
if (!normalized) return "";
|
|
685
|
+
if (normalized.includes("explicit")) return "Explicit Preference";
|
|
686
|
+
if (normalized.includes("implicit")) return "Implicit Preference";
|
|
687
|
+
return String(value)
|
|
688
|
+
.replace(/[_-]+/g, " ")
|
|
689
|
+
.replace(/\b\w/g, (ch) => ch.toUpperCase());
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function sanitizeInlineText(text) {
|
|
693
|
+
if (text === undefined || text === null) return "";
|
|
694
|
+
return String(text).replace(/\r?\n+/g, " ").trim();
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function resolveDisplayTime(item) {
|
|
698
|
+
return item?.update_time ?? item?.create_time;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function formatMemoryLine(item, text, options = {}) {
|
|
702
|
+
const cleaned = sanitizeInlineText(text);
|
|
703
|
+
if (!cleaned) return "";
|
|
704
|
+
const maxChars = options.maxItemChars;
|
|
705
|
+
const truncated = truncate(cleaned, maxChars);
|
|
706
|
+
const time = formatTime(resolveDisplayTime(item));
|
|
707
|
+
if (time) return ` -[${time}] ${truncated}`;
|
|
708
|
+
return ` - ${truncated}`;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function formatPreferenceLine(item, text, options = {}) {
|
|
712
|
+
const cleaned = sanitizeInlineText(text);
|
|
713
|
+
if (!cleaned) return "";
|
|
714
|
+
const maxChars = options.maxItemChars;
|
|
715
|
+
const truncated = truncate(cleaned, maxChars);
|
|
716
|
+
const time = formatTime(resolveDisplayTime(item));
|
|
717
|
+
const type = normalizePreferenceType(item?.preference_type);
|
|
718
|
+
const typeLabel = type ? ` [${type}]` : "";
|
|
719
|
+
if (time) return ` -[${time}]${typeLabel} ${truncated}`;
|
|
720
|
+
return ` -${typeLabel} ${truncated}`;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function wrapCodeBlock(lines, options = {}) {
|
|
724
|
+
if (!options.wrapTagBlocks) return lines;
|
|
725
|
+
return ["```text", ...lines, "```"];
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function buildMemorySections(data, options = {}) {
|
|
729
|
+
const memoryList = data?.memory_detail_list ?? [];
|
|
730
|
+
const preferenceList = data?.preference_detail_list ?? [];
|
|
731
|
+
const toolMemoryList = data?.tool_memory_detail_list ?? [];
|
|
732
|
+
|
|
733
|
+
const threshold = options.relativity ?? 0;
|
|
734
|
+
|
|
735
|
+
const memoryLines = memoryList
|
|
736
|
+
.filter((item) => (item?.relativity ?? 1) > threshold)
|
|
737
|
+
.map((item) => {
|
|
738
|
+
const text = item?.memory_value || item?.memory_key || "";
|
|
739
|
+
return formatMemoryLine(item, text, options);
|
|
740
|
+
})
|
|
741
|
+
.filter(Boolean);
|
|
742
|
+
|
|
743
|
+
const preferenceLines = preferenceList
|
|
744
|
+
.filter((item) => (item?.relativity ?? 1) > threshold)
|
|
745
|
+
.map((item) => {
|
|
746
|
+
const text = item?.preference || "";
|
|
747
|
+
return formatPreferenceLine(item, text, options);
|
|
748
|
+
})
|
|
749
|
+
.filter(Boolean);
|
|
750
|
+
|
|
751
|
+
const toolMemoryLines = toolMemoryList
|
|
752
|
+
.filter((item) => (item?.relativity ?? 1) > threshold)
|
|
753
|
+
.map((item) => {
|
|
754
|
+
const text = item?.tool_value || "";
|
|
755
|
+
return formatMemoryLine(item, text, options);
|
|
756
|
+
})
|
|
757
|
+
.filter(Boolean);
|
|
758
|
+
|
|
759
|
+
return { memoryLines, preferenceLines, toolMemoryLines };
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const STATIC_RECALL_SYSTEM_PROMPT = [
|
|
763
|
+
"# Role",
|
|
764
|
+
"",
|
|
765
|
+
"You are an intelligent assistant with long-term memory capabilities (MemOS Assistant). Your goal is to combine retrieved memory fragments to provide highly personalized, accurate, and logically rigorous responses.",
|
|
766
|
+
"",
|
|
767
|
+
"# System Context",
|
|
768
|
+
"",
|
|
769
|
+
"* Current Time: Use the runtime-provided current time as the baseline for freshness checks.",
|
|
770
|
+
"* Additional memory context for the current turn may be prepended before the original user query as a structured `<memories>` block.",
|
|
771
|
+
"",
|
|
772
|
+
"# Memory Data",
|
|
773
|
+
"",
|
|
774
|
+
'Below is the information retrieved by MemOS, categorized into "Facts" and "Preferences".',
|
|
775
|
+
"* **Facts**: May include user attributes, historical conversations, or third-party details.",
|
|
776
|
+
"* **Special Note**: Content tagged with '[assistant观点]' or '[模型总结]' represents **past AI inference**, **not** direct user statements.",
|
|
777
|
+
"* **Preferences**: The user's explicit or implicit requirements on response style, format, or reasoning.",
|
|
778
|
+
"",
|
|
779
|
+
"# Critical Protocol: Memory Safety",
|
|
780
|
+
"",
|
|
781
|
+
"Retrieved memories may contain **AI speculation**, **irrelevant noise**, or **wrong subject attribution**. You must strictly apply the **Four-Step Verdict**. If any step fails, **discard the memory**:",
|
|
782
|
+
"",
|
|
783
|
+
"1. **Source Verification**:",
|
|
784
|
+
"* **Core**: Distinguish direct user statements from AI inference.",
|
|
785
|
+
"* If a memory has tags like '[assistant观点]' or '[模型总结]', treat it as a **hypothesis**, not a user-grounded fact.",
|
|
786
|
+
"* *Counterexample*: If memory says '[assistant观点] User loves mangoes' but the user never said that, do not assume it as fact.",
|
|
787
|
+
"* **Principle: AI summaries are reference-only and have much lower authority than direct user statements.**",
|
|
788
|
+
"",
|
|
789
|
+
"2. **Attribution Check**:",
|
|
790
|
+
"* Is the subject in memory definitely the user?",
|
|
791
|
+
"* If the memory describes a **third party** (e.g., candidate, interviewee, fictional character, case data), never attribute it to the user.",
|
|
792
|
+
"",
|
|
793
|
+
"3. **Strong Relevance Check**:",
|
|
794
|
+
"* Does the memory directly help answer the current 'Original Query'?",
|
|
795
|
+
"* If it is only a keyword overlap with different context, ignore it.",
|
|
796
|
+
"",
|
|
797
|
+
"4. **Freshness Check**:",
|
|
798
|
+
"* If memory conflicts with the user's latest intent, prioritize the current 'Original Query' as the highest source of truth.",
|
|
799
|
+
"",
|
|
800
|
+
"# Instructions",
|
|
801
|
+
"",
|
|
802
|
+
"1. **Review**: Read '<facts>' first and apply the Four-Step Verdict to remove noise and unreliable AI inference.",
|
|
803
|
+
"2. **Execute**:",
|
|
804
|
+
" - Use only memories that pass filtering as context.",
|
|
805
|
+
" - Strictly follow style requirements from '<preferences>'.",
|
|
806
|
+
"3. **Output**: Answer directly. Never mention internal terms such as \"memory store\", \"retrieval\", or \"AI opinions\".",
|
|
807
|
+
"4. **Attention**: Additional memory context may already be provided before the original user query. Do not read from or write to local `MEMORY.md` or `memory/*` files for reference, as they may be outdated or irrelevant to the current query.",
|
|
808
|
+
].join("\n");
|
|
809
|
+
|
|
810
|
+
function buildMemoryPrependBlock(data, options = {}) {
|
|
811
|
+
const { memoryLines, preferenceLines, toolMemoryLines } = buildMemorySections(data, options);
|
|
812
|
+
const hasContent = memoryLines.length > 0 || preferenceLines.length > 0 || toolMemoryLines.length > 0;
|
|
813
|
+
if (!hasContent) return "";
|
|
814
|
+
|
|
815
|
+
const memoriesBlock = [
|
|
816
|
+
"<memories>",
|
|
817
|
+
" <facts>",
|
|
818
|
+
...memoryLines,
|
|
819
|
+
" </facts>",
|
|
820
|
+
" <tool_memories>",
|
|
821
|
+
...toolMemoryLines,
|
|
822
|
+
" </tool_memories>",
|
|
823
|
+
" <preferences>",
|
|
824
|
+
...preferenceLines,
|
|
825
|
+
" </preferences>",
|
|
826
|
+
"</memories>",
|
|
827
|
+
];
|
|
828
|
+
|
|
829
|
+
return [...wrapCodeBlock(memoriesBlock, options), "", USER_QUERY_MARKER].join("\n");
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
export function formatPromptBlockFromData(data, options = {}) {
|
|
833
|
+
if (!data || typeof data !== "object") return "";
|
|
834
|
+
return buildMemoryPrependBlock(data, options);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
export function formatPromptBlock(result, options = {}) {
|
|
838
|
+
const data = extractResultData(result);
|
|
839
|
+
return formatPromptBlockFromData(data, options);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
export function formatContextBlock(result, options = {}) {
|
|
843
|
+
const data = extractResultData(result);
|
|
844
|
+
if (!data) return "";
|
|
845
|
+
|
|
846
|
+
const memoryList = data.memory_detail_list ?? [];
|
|
847
|
+
const prefList = data.preference_detail_list ?? [];
|
|
848
|
+
const toolList = data.tool_memory_detail_list ?? [];
|
|
849
|
+
const preferenceNote = data.preference_note;
|
|
850
|
+
|
|
851
|
+
const lines = [];
|
|
852
|
+
if (memoryList.length > 0) {
|
|
853
|
+
lines.push("Facts:");
|
|
854
|
+
for (const item of memoryList) {
|
|
855
|
+
const text = item?.memory_value || item?.memory_key || "";
|
|
856
|
+
if (!text) continue;
|
|
857
|
+
lines.push(`- ${truncate(text, options.maxItemChars)}`);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
if (prefList.length > 0) {
|
|
862
|
+
lines.push("Preferences:");
|
|
863
|
+
for (const item of prefList) {
|
|
864
|
+
const pref = item?.preference || "";
|
|
865
|
+
const type = item?.preference_type ? `(${item.preference_type}) ` : "";
|
|
866
|
+
if (!pref) continue;
|
|
867
|
+
lines.push(`- ${type}${truncate(pref, options.maxItemChars)}`);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
if (toolList.length > 0) {
|
|
872
|
+
lines.push("Tool Memories:");
|
|
873
|
+
for (const item of toolList) {
|
|
874
|
+
const value = item?.tool_value || "";
|
|
875
|
+
if (!value) continue;
|
|
876
|
+
lines.push(`- ${truncate(value, options.maxItemChars)}`);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
if (preferenceNote) {
|
|
881
|
+
lines.push(`Preference Note: ${truncate(preferenceNote, options.maxItemChars)}`);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
return lines.length > 0 ? lines.join("\n") : "";
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
export function formatRecallHookResult(result, options = {}) {
|
|
888
|
+
const data = extractResultData(result);
|
|
889
|
+
if (!data) {
|
|
890
|
+
return {
|
|
891
|
+
appendSystemContext: "",
|
|
892
|
+
prependContext: "",
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
return {
|
|
897
|
+
// Keep this system addendum byte-stable across turns so provider-side prefix caching can hit.
|
|
898
|
+
appendSystemContext: STATIC_RECALL_SYSTEM_PROMPT,
|
|
899
|
+
prependContext: buildMemoryPrependBlock(data, options),
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function truncate(text, maxLen) {
|
|
904
|
+
if (!text) return "";
|
|
905
|
+
const limit = maxLen || 10000;
|
|
906
|
+
return text.length > limit ? `${text.slice(0, limit)}...` : text;
|
|
907
|
+
}
|