mr-memory 2.0.0 → 2.3.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/index.ts +356 -206
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -1,88 +1,127 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* MemoryRouter Plugin for OpenClaw
|
|
2
|
+
* MemoryRouter Plugin for OpenClaw
|
|
3
3
|
*
|
|
4
4
|
* Persistent AI memory via MemoryRouter (memoryrouter.ai).
|
|
5
|
-
* Uses
|
|
6
|
-
*
|
|
5
|
+
* Uses before_agent_start + agent_end hooks to inject/store memories
|
|
6
|
+
* via the MemoryRouter relay API. No proxy interception needed.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
8
|
+
* BYOK — provider API keys never touch MemoryRouter.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { readFile, readdir, stat } from "node:fs/promises";
|
|
12
|
+
import { join } from "node:path";
|
|
11
13
|
import { spawn } from "node:child_process";
|
|
12
14
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
13
15
|
|
|
14
16
|
const DEFAULT_ENDPOINT = "https://api.memoryrouter.ai";
|
|
15
17
|
|
|
18
|
+
// Workspace files OpenClaw loads into the system prompt
|
|
19
|
+
const WORKSPACE_FILES = [
|
|
20
|
+
"IDENTITY.md", "USER.md", "MEMORY.md", "HEARTBEAT.md",
|
|
21
|
+
"TOOLS.md", "AGENTS.md", "SOUL.md", "BOOTSTRAP.md",
|
|
22
|
+
];
|
|
23
|
+
|
|
16
24
|
type MemoryRouterConfig = {
|
|
17
25
|
key: string;
|
|
18
26
|
endpoint?: string;
|
|
19
|
-
density?:
|
|
27
|
+
density?: "low" | "high" | "xhigh";
|
|
28
|
+
mode?: "relay" | "proxy";
|
|
20
29
|
};
|
|
21
30
|
|
|
31
|
+
// ──────────────────────────────────────────────────────
|
|
32
|
+
// Helpers
|
|
33
|
+
// ──────────────────────────────────────────────────────
|
|
34
|
+
|
|
22
35
|
function resolveOpenClawInvocation(): { command: string; args: string[] } {
|
|
23
36
|
const entry = process.argv[1];
|
|
24
|
-
if (entry) {
|
|
25
|
-
|
|
26
|
-
command: process.execPath,
|
|
27
|
-
args: [entry],
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return {
|
|
32
|
-
command: "openclaw",
|
|
33
|
-
args: [],
|
|
34
|
-
};
|
|
37
|
+
if (entry) return { command: process.execPath, args: [entry] };
|
|
38
|
+
return { command: "openclaw", args: [] };
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
async function runOpenClawConfigSet(path: string, value: string, json = false): Promise<void> {
|
|
38
42
|
const base = resolveOpenClawInvocation();
|
|
39
43
|
const args = [...base.args, "config", "set", path, value];
|
|
40
|
-
if (json)
|
|
41
|
-
args.push("--json");
|
|
42
|
-
}
|
|
44
|
+
if (json) args.push("--json");
|
|
43
45
|
|
|
44
46
|
await new Promise<void>((resolve, reject) => {
|
|
45
47
|
const child = spawn(base.command, args, {
|
|
46
48
|
stdio: ["ignore", "ignore", "pipe"],
|
|
47
49
|
env: process.env,
|
|
48
50
|
});
|
|
49
|
-
|
|
50
51
|
let stderr = "";
|
|
51
|
-
child.stderr.on("data", (chunk) => {
|
|
52
|
-
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
child.on("error", (err) => reject(err));
|
|
52
|
+
child.stderr.on("data", (chunk) => { stderr += String(chunk); });
|
|
53
|
+
child.on("error", reject);
|
|
56
54
|
child.on("close", (code) => {
|
|
57
|
-
if (code === 0)
|
|
58
|
-
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
reject(new Error(`openclaw config set failed (exit ${code}): ${stderr.trim()}`));
|
|
55
|
+
if (code === 0) resolve();
|
|
56
|
+
else reject(new Error(`openclaw config set failed (exit ${code}): ${stderr.trim()}`));
|
|
62
57
|
});
|
|
63
58
|
});
|
|
64
59
|
}
|
|
65
60
|
|
|
61
|
+
type CompatApi = OpenClawPluginApi & {
|
|
62
|
+
updatePluginConfig?: (config: Record<string, unknown>) => Promise<void>;
|
|
63
|
+
updatePluginEnabled?: (enabled: boolean) => Promise<void>;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
66
|
async function setPluginConfig(api: OpenClawPluginApi, config: Record<string, unknown>): Promise<void> {
|
|
67
|
-
const
|
|
68
|
-
if (typeof
|
|
69
|
-
await
|
|
67
|
+
const compat = api as CompatApi;
|
|
68
|
+
if (typeof compat.updatePluginConfig === "function") {
|
|
69
|
+
await compat.updatePluginConfig(config);
|
|
70
70
|
return;
|
|
71
71
|
}
|
|
72
|
-
|
|
73
72
|
await runOpenClawConfigSet(`plugins.entries.${api.id}.config`, JSON.stringify(config), true);
|
|
74
73
|
}
|
|
75
74
|
|
|
76
75
|
async function setPluginEnabled(api: OpenClawPluginApi, enabled: boolean): Promise<void> {
|
|
77
|
-
const
|
|
78
|
-
if (typeof
|
|
79
|
-
await
|
|
76
|
+
const compat = api as CompatApi;
|
|
77
|
+
if (typeof compat.updatePluginEnabled === "function") {
|
|
78
|
+
await compat.updatePluginEnabled(enabled);
|
|
80
79
|
return;
|
|
81
80
|
}
|
|
82
|
-
|
|
83
81
|
await runOpenClawConfigSet(`plugins.entries.${api.id}.enabled`, enabled ? "true" : "false", true);
|
|
84
82
|
}
|
|
85
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Read all workspace files and return as a single text blob for token counting.
|
|
86
|
+
*/
|
|
87
|
+
async function readWorkspaceFiles(workspaceDir: string): Promise<string> {
|
|
88
|
+
const parts: string[] = [];
|
|
89
|
+
for (const file of WORKSPACE_FILES) {
|
|
90
|
+
try {
|
|
91
|
+
const content = await readFile(join(workspaceDir, file), "utf8");
|
|
92
|
+
if (content.trim()) parts.push(`## ${file}\n${content}`);
|
|
93
|
+
} catch { /* file doesn't exist — skip */ }
|
|
94
|
+
}
|
|
95
|
+
return parts.join("\n\n");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Build a text representation of tools config for token counting.
|
|
100
|
+
*/
|
|
101
|
+
function serializeToolsConfig(config: Record<string, unknown>): string {
|
|
102
|
+
const tools = config.tools;
|
|
103
|
+
if (!tools) return "";
|
|
104
|
+
try {
|
|
105
|
+
return `## Tools Config\n${JSON.stringify(tools, null, 2)}`;
|
|
106
|
+
} catch { return ""; }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Build a text representation of skills for token counting.
|
|
111
|
+
*/
|
|
112
|
+
function serializeSkillsConfig(config: Record<string, unknown>): string {
|
|
113
|
+
const skills = (config as any).skills?.entries;
|
|
114
|
+
if (!skills || typeof skills !== "object") return "";
|
|
115
|
+
try {
|
|
116
|
+
const names = Object.keys(skills);
|
|
117
|
+
return `## Skills (${names.length})\n${names.join(", ")}`;
|
|
118
|
+
} catch { return ""; }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ──────────────────────────────────────────────────────
|
|
122
|
+
// Plugin Definition
|
|
123
|
+
// ──────────────────────────────────────────────────────
|
|
124
|
+
|
|
86
125
|
const memoryRouterPlugin = {
|
|
87
126
|
id: "mr-memory",
|
|
88
127
|
name: "MemoryRouter",
|
|
@@ -92,34 +131,171 @@ const memoryRouterPlugin = {
|
|
|
92
131
|
const cfg = api.pluginConfig as MemoryRouterConfig | undefined;
|
|
93
132
|
const endpoint = cfg?.endpoint?.replace(/\/v1\/?$/, "") || DEFAULT_ENDPOINT;
|
|
94
133
|
const memoryKey = cfg?.key;
|
|
95
|
-
const density = cfg?.density ||
|
|
96
|
-
|
|
97
|
-
// ==================================================================
|
|
98
|
-
// Core: Relay architecture — memory via API hooks (no patching needed)
|
|
99
|
-
// ==================================================================
|
|
134
|
+
const density = cfg?.density || "high";
|
|
135
|
+
const mode = cfg?.mode || "relay";
|
|
100
136
|
|
|
101
137
|
if (memoryKey) {
|
|
102
|
-
api.logger.info?.(`memoryrouter:
|
|
138
|
+
api.logger.info?.(`memoryrouter: active (key: ${memoryKey.slice(0, 6)}..., mode: ${mode})`);
|
|
103
139
|
} else {
|
|
104
140
|
api.logger.info?.("memoryrouter: no key configured — run: openclaw mr <key>");
|
|
105
141
|
}
|
|
106
142
|
|
|
143
|
+
// ==================================================================
|
|
144
|
+
// Core: before_agent_start — search memories, inject context
|
|
145
|
+
// ==================================================================
|
|
146
|
+
|
|
107
147
|
if (memoryKey) {
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
148
|
+
// Track whether we've already fired for this prompt (dedup double-fire)
|
|
149
|
+
let lastPreparedPrompt = "";
|
|
150
|
+
// Track whether before_prompt_build already handled the first call in this run
|
|
151
|
+
let promptBuildFiredThisRun = false;
|
|
152
|
+
|
|
153
|
+
// ── llm_input: fires on EVERY LLM call (tool iterations, cron, sub-agents)
|
|
154
|
+
// On stock OpenClaw, the return value is ignored (fire-and-forget).
|
|
155
|
+
// When PR #24122 merges, OpenClaw will use the returned prependContext.
|
|
156
|
+
// This gives forward compatibility — no plugin update needed.
|
|
157
|
+
api.on("llm_input", async (event, ctx) => {
|
|
158
|
+
// Skip the first call — before_prompt_build already handled it
|
|
159
|
+
// (before_prompt_build includes workspace+tools+skills for accurate billing)
|
|
160
|
+
if (promptBuildFiredThisRun) {
|
|
161
|
+
promptBuildFiredThisRun = false; // reset so subsequent calls go through
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
112
164
|
|
|
113
165
|
try {
|
|
166
|
+
const prompt = event.prompt;
|
|
167
|
+
if (prompt === lastPreparedPrompt && lastPreparedPrompt !== "") return;
|
|
168
|
+
lastPreparedPrompt = prompt;
|
|
169
|
+
|
|
170
|
+
// Build lightweight context (no workspace/tools — just history + prompt)
|
|
171
|
+
const contextPayload: Array<{ role: string; content: string }> = [];
|
|
172
|
+
if (event.historyMessages && Array.isArray(event.historyMessages)) {
|
|
173
|
+
for (const msg of event.historyMessages) {
|
|
174
|
+
const m = msg as { role?: string; content?: unknown };
|
|
175
|
+
if (!m.role) continue;
|
|
176
|
+
let text = "";
|
|
177
|
+
if (typeof m.content === "string") text = m.content;
|
|
178
|
+
else if (Array.isArray(m.content)) {
|
|
179
|
+
text = (m.content as Array<{ type?: string; text?: string }>)
|
|
180
|
+
.filter(b => b.type === "text" && b.text)
|
|
181
|
+
.map(b => b.text!)
|
|
182
|
+
.join("\n");
|
|
183
|
+
}
|
|
184
|
+
if (text) contextPayload.push({ role: m.role, content: text });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
contextPayload.push({ role: "user", content: prompt });
|
|
188
|
+
|
|
189
|
+
const densityMap: Record<string, number> = { low: 40, high: 80, xhigh: 160 };
|
|
190
|
+
const contextLimit = densityMap[density] || 80;
|
|
191
|
+
|
|
192
|
+
const res = await fetch(`${endpoint}/v1/memory/prepare`, {
|
|
193
|
+
method: "POST",
|
|
194
|
+
headers: {
|
|
195
|
+
"Content-Type": "application/json",
|
|
196
|
+
Authorization: `Bearer ${memoryKey}`,
|
|
197
|
+
},
|
|
198
|
+
body: JSON.stringify({
|
|
199
|
+
messages: contextPayload,
|
|
200
|
+
density,
|
|
201
|
+
context_limit: contextLimit,
|
|
202
|
+
}),
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
if (!res.ok) return;
|
|
206
|
+
|
|
207
|
+
const data = (await res.json()) as {
|
|
208
|
+
context?: string;
|
|
209
|
+
memories_found?: number;
|
|
210
|
+
tokens_billed?: number;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
if (data.context) {
|
|
214
|
+
api.logger.info?.(
|
|
215
|
+
`memoryrouter: injected ${data.memories_found || 0} memories on tool iteration (${data.tokens_billed || 0} tokens billed)`,
|
|
216
|
+
);
|
|
217
|
+
return { prependContext: data.context };
|
|
218
|
+
}
|
|
219
|
+
} catch {
|
|
220
|
+
// Silent fail on tool iterations — don't block the agent
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// ── before_prompt_build: fires once per run (primary, includes full billing context)
|
|
225
|
+
api.on("before_prompt_build", async (event, ctx) => {
|
|
226
|
+
promptBuildFiredThisRun = true;
|
|
227
|
+
try {
|
|
228
|
+
const prompt = event.prompt;
|
|
229
|
+
|
|
230
|
+
// Deduplicate — if we already prepared this exact prompt, skip
|
|
231
|
+
if (prompt === lastPreparedPrompt && lastPreparedPrompt !== "") {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
lastPreparedPrompt = prompt;
|
|
235
|
+
|
|
236
|
+
// 1. Read workspace files for full token count
|
|
237
|
+
const workspaceDir = ctx.workspaceDir || "";
|
|
238
|
+
let workspaceText = "";
|
|
239
|
+
if (workspaceDir) {
|
|
240
|
+
workspaceText = await readWorkspaceFiles(workspaceDir);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// 2. Serialize tools + skills from config
|
|
244
|
+
const toolsText = serializeToolsConfig(api.config as unknown as Record<string, unknown>);
|
|
245
|
+
const skillsText = serializeSkillsConfig(api.config as unknown as Record<string, unknown>);
|
|
246
|
+
|
|
247
|
+
// 3. Build full context payload (messages + workspace + tools + skills)
|
|
248
|
+
const contextPayload: Array<{ role: string; content: string }> = [];
|
|
249
|
+
|
|
250
|
+
// Add workspace context as a system-level entry
|
|
251
|
+
const fullContext = [workspaceText, toolsText, skillsText].filter(Boolean).join("\n\n");
|
|
252
|
+
if (fullContext) {
|
|
253
|
+
contextPayload.push({ role: "system", content: fullContext });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Add conversation history
|
|
257
|
+
if (event.messages && Array.isArray(event.messages)) {
|
|
258
|
+
let skipped = 0;
|
|
259
|
+
for (const msg of event.messages) {
|
|
260
|
+
const m = msg as { role?: string; content?: unknown };
|
|
261
|
+
if (!m.role) continue;
|
|
262
|
+
|
|
263
|
+
let text = "";
|
|
264
|
+
if (typeof m.content === "string") {
|
|
265
|
+
text = m.content;
|
|
266
|
+
} else if (Array.isArray(m.content)) {
|
|
267
|
+
// Handle Anthropic-style content blocks [{type:"text", text:"..."}, ...]
|
|
268
|
+
text = (m.content as Array<{ type?: string; text?: string }>)
|
|
269
|
+
.filter(b => b.type === "text" && b.text)
|
|
270
|
+
.map(b => b.text!)
|
|
271
|
+
.join("\n");
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (text) {
|
|
275
|
+
contextPayload.push({ role: m.role, content: text });
|
|
276
|
+
} else {
|
|
277
|
+
skipped++;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Add current user prompt
|
|
283
|
+
contextPayload.push({ role: "user", content: prompt });
|
|
284
|
+
|
|
285
|
+
// 4. Call /v1/memory/prepare
|
|
286
|
+
const densityMap: Record<string, number> = { low: 40, high: 80, xhigh: 160 };
|
|
287
|
+
const contextLimit = densityMap[density] || 80;
|
|
288
|
+
|
|
114
289
|
const res = await fetch(`${endpoint}/v1/memory/prepare`, {
|
|
115
290
|
method: "POST",
|
|
116
291
|
headers: {
|
|
117
|
-
"Authorization": `Bearer ${memoryKey}`,
|
|
118
292
|
"Content-Type": "application/json",
|
|
293
|
+
Authorization: `Bearer ${memoryKey}`,
|
|
119
294
|
},
|
|
120
295
|
body: JSON.stringify({
|
|
121
|
-
messages:
|
|
296
|
+
messages: contextPayload,
|
|
122
297
|
density,
|
|
298
|
+
context_limit: contextLimit,
|
|
123
299
|
}),
|
|
124
300
|
});
|
|
125
301
|
|
|
@@ -128,94 +304,112 @@ const memoryRouterPlugin = {
|
|
|
128
304
|
return;
|
|
129
305
|
}
|
|
130
306
|
|
|
131
|
-
const data = await res.json() as {
|
|
307
|
+
const data = (await res.json()) as {
|
|
308
|
+
context?: string;
|
|
309
|
+
memories_found?: number;
|
|
310
|
+
tokens_billed?: number;
|
|
311
|
+
};
|
|
132
312
|
|
|
133
313
|
if (data.context) {
|
|
134
|
-
api.logger.info?.(
|
|
314
|
+
api.logger.info?.(
|
|
315
|
+
`memoryrouter: injected ${data.memories_found || 0} memories (${data.tokens_billed || 0} tokens billed)`,
|
|
316
|
+
);
|
|
135
317
|
return { prependContext: data.context };
|
|
136
318
|
}
|
|
137
319
|
} catch (err) {
|
|
138
|
-
api.logger.warn?.(
|
|
320
|
+
api.logger.warn?.(
|
|
321
|
+
`memoryrouter: prepare error — ${err instanceof Error ? err.message : String(err)}`,
|
|
322
|
+
);
|
|
139
323
|
}
|
|
140
324
|
});
|
|
141
325
|
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
|
|
145
|
-
if (!event.success || !event.messages) return;
|
|
146
|
-
|
|
147
|
-
const msgs = event.messages as Array<Record<string, unknown>>;
|
|
148
|
-
if (!msgs.length) return;
|
|
326
|
+
// ==================================================================
|
|
327
|
+
// Core: agent_end — store this turn's conversation
|
|
328
|
+
// ==================================================================
|
|
149
329
|
|
|
330
|
+
api.on("agent_end", async (event, ctx) => {
|
|
150
331
|
try {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
if (
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (typeof content === "string") {
|
|
163
|
-
text = content;
|
|
164
|
-
} else if (Array.isArray(content)) {
|
|
165
|
-
for (const block of content as Array<Record<string, unknown>>) {
|
|
166
|
-
if (typeof block.text === "string") {
|
|
167
|
-
text += (text ? "\n" : "") + block.text;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
332
|
+
const msgs = event.messages;
|
|
333
|
+
if (!msgs || !Array.isArray(msgs) || msgs.length === 0) return;
|
|
334
|
+
|
|
335
|
+
// Extract text from a message (handles string + content block arrays)
|
|
336
|
+
function extractText(content: unknown): string {
|
|
337
|
+
if (typeof content === "string") return content;
|
|
338
|
+
if (Array.isArray(content)) {
|
|
339
|
+
return (content as Array<{ type?: string; text?: string }>)
|
|
340
|
+
.filter(b => b.type === "text" && b.text)
|
|
341
|
+
.map(b => b.text!)
|
|
342
|
+
.join("\n");
|
|
170
343
|
}
|
|
344
|
+
return "";
|
|
345
|
+
}
|
|
171
346
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
if (cleanText.includes("[Memory Context]")) {
|
|
181
|
-
cleanText = cleanText.replace(/\[Memory Context\][\s\S]*?\n\n/g, "").trim();
|
|
182
|
-
}
|
|
183
|
-
if (cleanText.length >= 10) {
|
|
184
|
-
userMsg = { role, content: cleanText };
|
|
185
|
-
}
|
|
347
|
+
// Find the last user message, then collect ALL assistant messages after it
|
|
348
|
+
// This captures the full response even if she sent multiple messages
|
|
349
|
+
let lastUserIdx = -1;
|
|
350
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
351
|
+
const msg = msgs[i] as { role?: string; content?: unknown };
|
|
352
|
+
const text = extractText(msg.content);
|
|
353
|
+
if (msg.role === "user" && text) {
|
|
354
|
+
lastUserIdx = i;
|
|
186
355
|
break;
|
|
187
356
|
}
|
|
188
357
|
}
|
|
189
358
|
|
|
190
|
-
// Only store this turn's pair — no duplication
|
|
191
359
|
const toStore: Array<{ role: string; content: string }> = [];
|
|
192
|
-
|
|
193
|
-
|
|
360
|
+
|
|
361
|
+
// Add the user message
|
|
362
|
+
if (lastUserIdx >= 0) {
|
|
363
|
+
const userMsg = msgs[lastUserIdx] as { content?: unknown };
|
|
364
|
+
const userText = extractText(userMsg.content);
|
|
365
|
+
if (userText) toStore.push({ role: "user", content: userText });
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Collect ALL assistant messages after the last user message
|
|
369
|
+
const assistantParts: string[] = [];
|
|
370
|
+
for (let i = (lastUserIdx >= 0 ? lastUserIdx + 1 : 0); i < msgs.length; i++) {
|
|
371
|
+
const msg = msgs[i] as { role?: string; content?: unknown };
|
|
372
|
+
if (msg.role === "assistant") {
|
|
373
|
+
const text = extractText(msg.content);
|
|
374
|
+
if (text) assistantParts.push(text);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (assistantParts.length > 0) {
|
|
378
|
+
toStore.push({ role: "assistant", content: assistantParts.join("\n\n") });
|
|
379
|
+
}
|
|
194
380
|
|
|
195
381
|
if (toStore.length === 0) return;
|
|
196
382
|
|
|
197
|
-
|
|
383
|
+
// Fire and forget — don't block the response
|
|
384
|
+
fetch(`${endpoint}/v1/memory/ingest`, {
|
|
198
385
|
method: "POST",
|
|
199
386
|
headers: {
|
|
200
|
-
"Authorization": `Bearer ${memoryKey}`,
|
|
201
387
|
"Content-Type": "application/json",
|
|
388
|
+
Authorization: `Bearer ${memoryKey}`,
|
|
202
389
|
},
|
|
203
390
|
body: JSON.stringify({
|
|
204
391
|
messages: toStore,
|
|
392
|
+
session_id: ctx.sessionKey,
|
|
205
393
|
}),
|
|
394
|
+
}).then(async (res) => {
|
|
395
|
+
if (!res.ok) {
|
|
396
|
+
api.logger.warn?.(`memoryrouter: ingest failed (${res.status})`);
|
|
397
|
+
}
|
|
398
|
+
}).catch((err) => {
|
|
399
|
+
api.logger.warn?.(
|
|
400
|
+
`memoryrouter: ingest error — ${err instanceof Error ? err.message : String(err)}`,
|
|
401
|
+
);
|
|
206
402
|
});
|
|
207
|
-
|
|
208
|
-
if (!res.ok) {
|
|
209
|
-
api.logger.warn?.(`memoryrouter: ingest failed (${res.status})`);
|
|
210
|
-
}
|
|
211
403
|
} catch (err) {
|
|
212
|
-
api.logger.warn?.(
|
|
404
|
+
api.logger.warn?.(
|
|
405
|
+
`memoryrouter: agent_end error — ${err instanceof Error ? err.message : String(err)}`,
|
|
406
|
+
);
|
|
213
407
|
}
|
|
214
408
|
});
|
|
215
|
-
} // end if (memoryKey)
|
|
409
|
+
} // end if (memoryKey)
|
|
216
410
|
|
|
217
411
|
// ==================================================================
|
|
218
|
-
// CLI Commands
|
|
412
|
+
// CLI Commands
|
|
219
413
|
// ==================================================================
|
|
220
414
|
|
|
221
415
|
api.registerCli(
|
|
@@ -225,7 +419,6 @@ const memoryRouterPlugin = {
|
|
|
225
419
|
console.error("Invalid key format. Keys start with 'mk' (e.g. mk_xxx)");
|
|
226
420
|
return;
|
|
227
421
|
}
|
|
228
|
-
|
|
229
422
|
try {
|
|
230
423
|
await setPluginConfig(api, { key });
|
|
231
424
|
await setPluginEnabled(api, true);
|
|
@@ -234,30 +427,22 @@ const memoryRouterPlugin = {
|
|
|
234
427
|
} catch (err) {
|
|
235
428
|
const message = err instanceof Error ? err.message : String(err);
|
|
236
429
|
console.error(`Failed to enable MemoryRouter: ${message}`);
|
|
237
|
-
console.error("Fallback: openclaw config set plugins.entries.mr-memory.config.key <key>");
|
|
238
|
-
console.error("Then: openclaw config set plugins.entries.mr-memory.enabled true --json");
|
|
239
430
|
}
|
|
240
431
|
};
|
|
241
432
|
|
|
242
|
-
const mr = program
|
|
433
|
+
const mr = program
|
|
434
|
+
.command("mr")
|
|
243
435
|
.description("MemoryRouter memory commands")
|
|
244
436
|
.argument("[key]", "Your MemoryRouter memory key (mk_xxx)")
|
|
245
437
|
.action(async (key: string | undefined) => {
|
|
246
|
-
if (!key) {
|
|
247
|
-
// No key provided — show help
|
|
248
|
-
mr.help();
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
438
|
+
if (!key) { mr.help(); return; }
|
|
251
439
|
await applyKey(key);
|
|
252
440
|
});
|
|
253
441
|
|
|
254
|
-
// Backward compat: `openclaw mr enable <key>` still works
|
|
255
442
|
mr.command("enable")
|
|
256
443
|
.description("Enable MemoryRouter with a memory key (alias)")
|
|
257
444
|
.argument("<key>", "Your MemoryRouter memory key (mk_xxx)")
|
|
258
|
-
.action(async (key: string) => {
|
|
259
|
-
await applyKey(key);
|
|
260
|
-
});
|
|
445
|
+
.action(async (key: string) => { await applyKey(key); });
|
|
261
446
|
|
|
262
447
|
mr.command("off")
|
|
263
448
|
.description("Disable MemoryRouter (removes key)")
|
|
@@ -265,143 +450,110 @@ const memoryRouterPlugin = {
|
|
|
265
450
|
try {
|
|
266
451
|
await setPluginConfig(api, {});
|
|
267
452
|
await setPluginEnabled(api, false);
|
|
268
|
-
console.log("✓ MemoryRouter disabled.
|
|
269
|
-
console.log(" Key removed. Re-enable with: openclaw mr <key>");
|
|
453
|
+
console.log("✓ MemoryRouter disabled.");
|
|
270
454
|
} catch (err) {
|
|
271
|
-
console.error(`Failed to disable
|
|
455
|
+
console.error(`Failed to disable: ${err instanceof Error ? err.message : String(err)}`);
|
|
272
456
|
}
|
|
273
457
|
});
|
|
274
458
|
|
|
275
459
|
// Density commands
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
.description("Set memory density to high (80 results, ~24k tokens) [default]")
|
|
294
|
-
.action(async () => {
|
|
295
|
-
if (!memoryKey) {
|
|
296
|
-
console.error("MemoryRouter not configured. Run: openclaw mr <key>");
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
try {
|
|
300
|
-
await setPluginConfig(api, { key: memoryKey, endpoint: cfg?.endpoint, density: 'high' });
|
|
301
|
-
console.log("✓ Memory density set to high (80 results, ~24k tokens)");
|
|
302
|
-
} catch (err) {
|
|
303
|
-
console.error(`Failed to set density: ${err instanceof Error ? err.message : String(err)}`);
|
|
304
|
-
}
|
|
305
|
-
});
|
|
460
|
+
for (const [name, desc] of [
|
|
461
|
+
["xhigh", "Set density to xhigh (160 results, ~50k tokens)"],
|
|
462
|
+
["high", "Set density to high (80 results, ~24k tokens) [default]"],
|
|
463
|
+
["low", "Set density to low (40 results, ~12k tokens)"],
|
|
464
|
+
] as const) {
|
|
465
|
+
mr.command(name)
|
|
466
|
+
.description(desc)
|
|
467
|
+
.action(async () => {
|
|
468
|
+
if (!memoryKey) { console.error("Not configured. Run: openclaw mr <key>"); return; }
|
|
469
|
+
try {
|
|
470
|
+
await setPluginConfig(api, { key: memoryKey, endpoint: cfg?.endpoint, density: name });
|
|
471
|
+
console.log(`✓ Memory density set to ${name}`);
|
|
472
|
+
} catch (err) {
|
|
473
|
+
console.error(`Failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
}
|
|
306
477
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
478
|
+
// Mode commands
|
|
479
|
+
for (const [modeName, modeDesc] of [
|
|
480
|
+
["relay", "Relay mode — hooks only, works on stock OpenClaw [default]"],
|
|
481
|
+
["proxy", "Proxy mode — memory on every LLM call (requires registerStreamFnWrapper)"],
|
|
482
|
+
] as const) {
|
|
483
|
+
mr.command(modeName)
|
|
484
|
+
.description(modeDesc)
|
|
485
|
+
.action(async () => {
|
|
486
|
+
if (!memoryKey) { console.error("Not configured. Run: openclaw mr <key>"); return; }
|
|
487
|
+
try {
|
|
488
|
+
await setPluginConfig(api, { key: memoryKey, endpoint: cfg?.endpoint, density, mode: modeName });
|
|
489
|
+
console.log(`✓ Mode set to ${modeName}`);
|
|
490
|
+
} catch (err) {
|
|
491
|
+
console.error(`Failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
}
|
|
322
495
|
|
|
323
496
|
mr.command("status")
|
|
324
497
|
.description("Show MemoryRouter vault stats")
|
|
325
498
|
.option("--json", "JSON output")
|
|
326
499
|
.action(async (opts) => {
|
|
327
|
-
if (!memoryKey) {
|
|
328
|
-
console.error("MemoryRouter not configured. Run: openclaw mr <key>");
|
|
329
|
-
return;
|
|
330
|
-
}
|
|
500
|
+
if (!memoryKey) { console.error("Not configured. Run: openclaw mr <key>"); return; }
|
|
331
501
|
try {
|
|
332
502
|
const res = await fetch(`${endpoint}/v1/memory/stats`, {
|
|
333
503
|
headers: { Authorization: `Bearer ${memoryKey}` },
|
|
334
504
|
});
|
|
335
|
-
const data = await res.json() as
|
|
336
|
-
|
|
505
|
+
const data = (await res.json()) as { totalVectors?: number; totalTokens?: number };
|
|
337
506
|
if (opts.json) {
|
|
338
|
-
console.log(JSON.stringify({ enabled: true, key: memoryKey, density, stats: data }, null, 2));
|
|
507
|
+
console.log(JSON.stringify({ enabled: true, key: memoryKey, density, mode, stats: data }, null, 2));
|
|
339
508
|
} else {
|
|
340
509
|
console.log("MemoryRouter Status");
|
|
341
510
|
console.log("───────────────────────────");
|
|
342
511
|
console.log(`Enabled: ✓ Yes`);
|
|
343
512
|
console.log(`Key: ${memoryKey.slice(0, 6)}...${memoryKey.slice(-3)}`);
|
|
344
|
-
console.log(`
|
|
513
|
+
console.log(`Mode: ${mode}`);
|
|
514
|
+
console.log(`Density: ${density}`);
|
|
345
515
|
console.log(`Endpoint: ${endpoint}`);
|
|
346
|
-
console.log(
|
|
347
|
-
console.log(
|
|
348
|
-
const stats = data as { totalVectors?: number; totalTokens?: number };
|
|
349
|
-
console.log(` Memories: ${stats.totalVectors ?? 0}`);
|
|
350
|
-
console.log(` Tokens: ${stats.totalTokens ?? 0}`);
|
|
516
|
+
console.log(`Memories: ${data.totalVectors ?? 0}`);
|
|
517
|
+
console.log(`Tokens: ${data.totalTokens ?? 0}`);
|
|
351
518
|
}
|
|
352
519
|
} catch (err) {
|
|
353
|
-
console.error(`Failed
|
|
520
|
+
console.error(`Failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
354
521
|
}
|
|
355
522
|
});
|
|
356
523
|
|
|
357
524
|
mr.command("upload")
|
|
358
525
|
.description("Upload workspace + session history to vault")
|
|
359
526
|
.argument("[path]", "Specific file or directory to upload")
|
|
360
|
-
.option("--workspace <dir>", "Workspace directory
|
|
361
|
-
.option("--brain <dir>", "State directory with sessions
|
|
527
|
+
.option("--workspace <dir>", "Workspace directory")
|
|
528
|
+
.option("--brain <dir>", "State directory with sessions")
|
|
362
529
|
.action(async (targetPath: string | undefined, opts: { workspace?: string; brain?: string }) => {
|
|
363
|
-
if (!memoryKey) {
|
|
364
|
-
console.error("MemoryRouter not configured. Run: openclaw mr <key>");
|
|
365
|
-
return;
|
|
366
|
-
}
|
|
530
|
+
if (!memoryKey) { console.error("Not configured. Run: openclaw mr <key>"); return; }
|
|
367
531
|
const os = await import("node:os");
|
|
368
532
|
const path = await import("node:path");
|
|
369
533
|
const stateDir = opts.brain ? path.resolve(opts.brain) : path.join(os.homedir(), ".openclaw");
|
|
370
|
-
|
|
371
|
-
const configWorkspace = api.config.workspace || api.config.agents?.defaults?.workspace;
|
|
534
|
+
const configWorkspace = (api.config as any).workspace || (api.config as any).agents?.defaults?.workspace;
|
|
372
535
|
const workspacePath = opts.workspace
|
|
373
536
|
? path.resolve(opts.workspace)
|
|
374
537
|
: configWorkspace
|
|
375
538
|
? path.resolve(configWorkspace.replace(/^~/, os.homedir()))
|
|
376
539
|
: path.join(os.homedir(), ".openclaw", "workspace");
|
|
377
540
|
const { runUpload } = await import("./upload.js");
|
|
378
|
-
await runUpload({
|
|
379
|
-
memoryKey,
|
|
380
|
-
endpoint,
|
|
381
|
-
targetPath,
|
|
382
|
-
stateDir,
|
|
383
|
-
workspacePath,
|
|
384
|
-
hasWorkspaceFlag: !!opts.workspace,
|
|
385
|
-
hasBrainFlag: !!opts.brain,
|
|
386
|
-
});
|
|
541
|
+
await runUpload({ memoryKey, endpoint, targetPath, stateDir, workspacePath, hasWorkspaceFlag: !!opts.workspace, hasBrainFlag: !!opts.brain });
|
|
387
542
|
});
|
|
388
543
|
|
|
389
544
|
mr.command("delete")
|
|
390
545
|
.description("Clear all memories from vault")
|
|
391
546
|
.action(async () => {
|
|
392
|
-
if (!memoryKey) {
|
|
393
|
-
console.error("MemoryRouter not configured. Run: openclaw mr <key>");
|
|
394
|
-
return;
|
|
395
|
-
}
|
|
547
|
+
if (!memoryKey) { console.error("Not configured. Run: openclaw mr <key>"); return; }
|
|
396
548
|
try {
|
|
397
549
|
const res = await fetch(`${endpoint}/v1/memory`, {
|
|
398
550
|
method: "DELETE",
|
|
399
551
|
headers: { Authorization: `Bearer ${memoryKey}` },
|
|
400
552
|
});
|
|
401
|
-
const data = await res.json() as { message?: string };
|
|
553
|
+
const data = (await res.json()) as { message?: string };
|
|
402
554
|
console.log(`✓ ${data.message || "Vault cleared"}`);
|
|
403
555
|
} catch (err) {
|
|
404
|
-
console.error(`Failed
|
|
556
|
+
console.error(`Failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
405
557
|
}
|
|
406
558
|
});
|
|
407
559
|
},
|
|
@@ -415,9 +567,7 @@ const memoryRouterPlugin = {
|
|
|
415
567
|
api.registerService({
|
|
416
568
|
id: "mr-memory",
|
|
417
569
|
start: () => {
|
|
418
|
-
if (memoryKey) {
|
|
419
|
-
api.logger.info?.(`memoryrouter: active (key: ${memoryKey.slice(0, 6)}...)`);
|
|
420
|
-
}
|
|
570
|
+
if (memoryKey) api.logger.info?.(`memoryrouter: active (key: ${memoryKey.slice(0, 6)}...)`);
|
|
421
571
|
},
|
|
422
572
|
stop: () => {
|
|
423
573
|
api.logger.info?.("memoryrouter: stopped");
|