@matrixorigin/thememoria 0.4.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 +368 -0
- package/openclaw/client.ts +1030 -0
- package/openclaw/config.ts +629 -0
- package/openclaw/format.ts +55 -0
- package/openclaw/index.ts +2360 -0
- package/openclaw.plugin.json +254 -0
- package/package.json +49 -0
- package/scripts/connect_openclaw_memoria.mjs +172 -0
- package/scripts/install-openclaw-memoria.sh +727 -0
- package/scripts/uninstall-openclaw-memoria.sh +362 -0
- package/scripts/verify_plugin_install.mjs +149 -0
- package/skills/memoria-memory/SKILL.md +43 -0
- package/skills/memoria-recovery/SKILL.md +29 -0
|
@@ -0,0 +1,2360 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
5
|
+
import {
|
|
6
|
+
MEMORIA_MEMORY_TYPES,
|
|
7
|
+
MEMORIA_TRUST_TIERS,
|
|
8
|
+
memoriaPluginConfigSchema,
|
|
9
|
+
parseMemoriaPluginConfig,
|
|
10
|
+
type MemoriaMemoryType,
|
|
11
|
+
type MemoriaPluginConfig,
|
|
12
|
+
type MemoriaTrustTier,
|
|
13
|
+
} from "./config.js";
|
|
14
|
+
import {
|
|
15
|
+
MemoriaClient,
|
|
16
|
+
type MemoriaMemoryRecord,
|
|
17
|
+
type MemoriaStatsResponse,
|
|
18
|
+
} from "./client.js";
|
|
19
|
+
import { formatMemoryList, formatRelevantMemoriesContext } from "./format.js";
|
|
20
|
+
|
|
21
|
+
type ToolResult = {
|
|
22
|
+
content: Array<{ type: "text"; text: string }>;
|
|
23
|
+
details: Record<string, unknown>;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type PluginIdentityContext = {
|
|
27
|
+
agentId?: string;
|
|
28
|
+
sessionKey?: string;
|
|
29
|
+
sessionId?: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const EMPTY_OBJECT_SCHEMA = {
|
|
33
|
+
type: "object",
|
|
34
|
+
additionalProperties: false,
|
|
35
|
+
properties: {},
|
|
36
|
+
} as const;
|
|
37
|
+
|
|
38
|
+
const MEMORIA_AGENT_GUIDANCE = [
|
|
39
|
+
"Memoria is the durable external memory system for this runtime.",
|
|
40
|
+
"When the user asks you to remember, update, forget, correct, snapshot, or restore memory, prefer the Memoria tools over editing local workspace memory files.",
|
|
41
|
+
"Use memory_store for new durable facts or preferences, memory_correct or memory_forget for repairs, and memory_snapshot plus memory_rollback when you need a recoverable checkpoint.",
|
|
42
|
+
"Workspace files like MEMORY.md and memory/YYYY-MM-DD.md are separate local notes. Only edit them when the user explicitly asks for file-based memory or workspace-local notes.",
|
|
43
|
+
"Do not claim that only memory_search or memory_get are available when other memory_* tools are present in the tool list.",
|
|
44
|
+
].join("\n");
|
|
45
|
+
|
|
46
|
+
const PLUGIN_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
47
|
+
const INSTALLER_SCRIPT = path.join(PLUGIN_ROOT, "scripts", "install-openclaw-memoria.sh");
|
|
48
|
+
const VERIFY_SCRIPT = path.join(PLUGIN_ROOT, "scripts", "verify_plugin_install.mjs");
|
|
49
|
+
const CONNECT_SCRIPT = path.join(PLUGIN_ROOT, "scripts", "connect_openclaw_memoria.mjs");
|
|
50
|
+
|
|
51
|
+
function resolveOpenClawBinFromProcess(): string {
|
|
52
|
+
return typeof process.argv[1] === "string" && process.argv[1].trim()
|
|
53
|
+
? process.argv[1]
|
|
54
|
+
: "openclaw";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function stripAnsi(value: string): string {
|
|
58
|
+
return value.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, "");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function extractConfigPathFromCliOutput(rawOutput: string): string | undefined {
|
|
62
|
+
const cleanedLines = stripAnsi(rawOutput)
|
|
63
|
+
.split(/\r?\n/)
|
|
64
|
+
.map((line) => line.trim())
|
|
65
|
+
.filter(Boolean);
|
|
66
|
+
|
|
67
|
+
for (let index = cleanedLines.length - 1; index >= 0; index -= 1) {
|
|
68
|
+
const line = cleanedLines[index];
|
|
69
|
+
const directPathMatch = line.match(/(~?\/[^\s]*openclaw\.json)$/);
|
|
70
|
+
if (directPathMatch?.[1]) {
|
|
71
|
+
return directPathMatch[1];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const embeddedPathMatch = line.match(/((?:~|\/)[^\s]*openclaw\.json)/);
|
|
75
|
+
if (embeddedPathMatch?.[1]) {
|
|
76
|
+
return embeddedPathMatch[1];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function resolveOpenClawConfigFile(): string {
|
|
84
|
+
const explicitConfigPath =
|
|
85
|
+
typeof process.env.OPENCLAW_CONFIG_PATH === "string" ? process.env.OPENCLAW_CONFIG_PATH.trim() : "";
|
|
86
|
+
if (explicitConfigPath) {
|
|
87
|
+
return explicitConfigPath.replace(/^~(?=$|\/|\\)/, process.env.HOME ?? "~");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const openclawBin = resolveOpenClawBinFromProcess();
|
|
91
|
+
const fromCli = spawnSync(openclawBin, ["config", "file"], {
|
|
92
|
+
cwd: PLUGIN_ROOT,
|
|
93
|
+
env: process.env,
|
|
94
|
+
encoding: "utf8",
|
|
95
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
96
|
+
});
|
|
97
|
+
const candidate = extractConfigPathFromCliOutput(
|
|
98
|
+
typeof fromCli.stdout === "string" ? fromCli.stdout : "",
|
|
99
|
+
);
|
|
100
|
+
if (fromCli.status === 0 && candidate) {
|
|
101
|
+
return candidate.replace(/^~(?=$|\/|\\)/, process.env.HOME ?? "~");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const openclawHome = typeof process.env.OPENCLAW_HOME === "string" ? process.env.OPENCLAW_HOME : "";
|
|
105
|
+
return openclawHome
|
|
106
|
+
? path.join(openclawHome, ".openclaw", "openclaw.json")
|
|
107
|
+
: path.join(process.env.HOME ?? "", ".openclaw", "openclaw.json");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function runLocalCommand(
|
|
111
|
+
command: string,
|
|
112
|
+
args: string[],
|
|
113
|
+
options: { env?: NodeJS.ProcessEnv } = {},
|
|
114
|
+
) {
|
|
115
|
+
const env = options.env ? { ...process.env, ...options.env } : process.env;
|
|
116
|
+
const result = spawnSync(command, args, {
|
|
117
|
+
cwd: PLUGIN_ROOT,
|
|
118
|
+
env,
|
|
119
|
+
stdio: "inherit",
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
if (result.error) {
|
|
123
|
+
throw result.error;
|
|
124
|
+
}
|
|
125
|
+
if (typeof result.status === "number" && result.status !== 0) {
|
|
126
|
+
throw new Error(`${path.basename(command)} exited with status ${result.status}`);
|
|
127
|
+
}
|
|
128
|
+
if (result.signal) {
|
|
129
|
+
throw new Error(`${path.basename(command)} terminated by signal ${result.signal}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function isExecutableAvailable(command: string): boolean {
|
|
134
|
+
const probe = spawnSync(command, ["--version"], {
|
|
135
|
+
cwd: PLUGIN_ROOT,
|
|
136
|
+
env: process.env,
|
|
137
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
if (probe.error && "code" in probe.error && probe.error.code === "ENOENT") {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function assertMemoriaExecutableAvailable(command: string, mode: "cloud" | "local") {
|
|
147
|
+
if (isExecutableAvailable(command)) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const installHint =
|
|
151
|
+
"curl -sSL https://raw.githubusercontent.com/matrixorigin/Memoria/main/scripts/install.sh | bash -s -- -y -d ~/.local/bin";
|
|
152
|
+
throw new Error(
|
|
153
|
+
`Memoria executable '${command}' was not found. Install Memoria CLI first (${installHint}), rerun setup with --install-memoria, or rerun with --memoria-bin <path>. This plugin uses local memoria CLI as the MCP bridge even in mode=${mode}.`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function objectSchema(
|
|
158
|
+
properties: Record<string, unknown>,
|
|
159
|
+
required: string[] = [],
|
|
160
|
+
): Record<string, unknown> {
|
|
161
|
+
return {
|
|
162
|
+
type: "object",
|
|
163
|
+
additionalProperties: false,
|
|
164
|
+
properties,
|
|
165
|
+
...(required.length > 0 ? { required } : {}),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function jsonResult(payload: Record<string, unknown>): ToolResult {
|
|
170
|
+
return {
|
|
171
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
|
|
172
|
+
details: payload,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function textResult(text: string, details: Record<string, unknown> = {}): ToolResult {
|
|
177
|
+
return {
|
|
178
|
+
content: [{ type: "text", text }],
|
|
179
|
+
details,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
184
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
185
|
+
? (value as Record<string, unknown>)
|
|
186
|
+
: null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function extractTextContent(content: unknown): string {
|
|
190
|
+
if (typeof content === "string") {
|
|
191
|
+
return content.trim();
|
|
192
|
+
}
|
|
193
|
+
if (!Array.isArray(content)) {
|
|
194
|
+
return "";
|
|
195
|
+
}
|
|
196
|
+
const parts: string[] = [];
|
|
197
|
+
for (const item of content) {
|
|
198
|
+
const block = asRecord(item);
|
|
199
|
+
if (!block || block.type !== "text" || typeof block.text !== "string") {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const text = block.text.trim();
|
|
203
|
+
if (text) {
|
|
204
|
+
parts.push(text);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return parts.join("\n").trim();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function collectRecentConversationMessages(
|
|
211
|
+
messages: unknown[],
|
|
212
|
+
options: { tailMessages: number; maxChars: number },
|
|
213
|
+
): Array<{ role: string; content: string }> {
|
|
214
|
+
const normalized: Array<{ role: string; content: string }> = [];
|
|
215
|
+
|
|
216
|
+
for (const entry of messages) {
|
|
217
|
+
const message = asRecord(entry);
|
|
218
|
+
if (!message) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
const role = typeof message.role === "string" ? message.role.trim() : "";
|
|
222
|
+
if (role !== "user" && role !== "assistant") {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
const text = extractTextContent(message.content);
|
|
226
|
+
if (!text) {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
normalized.push({ role, content: text });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const tail = normalized.slice(-options.tailMessages);
|
|
233
|
+
const output: Array<{ role: string; content: string }> = [];
|
|
234
|
+
let usedChars = 0;
|
|
235
|
+
|
|
236
|
+
for (let index = tail.length - 1; index >= 0; index -= 1) {
|
|
237
|
+
const current = tail[index];
|
|
238
|
+
if (usedChars >= options.maxChars) {
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
const remaining = options.maxChars - usedChars;
|
|
242
|
+
const content =
|
|
243
|
+
current.content.length > remaining ? current.content.slice(-remaining) : current.content;
|
|
244
|
+
usedChars += content.length;
|
|
245
|
+
output.unshift({ role: current.role, content });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return output;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function readString(
|
|
252
|
+
params: Record<string, unknown>,
|
|
253
|
+
key: string,
|
|
254
|
+
options: { required?: boolean; label?: string } = {},
|
|
255
|
+
): string | undefined {
|
|
256
|
+
const { required = false, label = key } = options;
|
|
257
|
+
const raw = params[key];
|
|
258
|
+
if (typeof raw !== "string" || !raw.trim()) {
|
|
259
|
+
if (required) {
|
|
260
|
+
throw new Error(`${label} required`);
|
|
261
|
+
}
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
264
|
+
return raw.trim();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function readNumber(params: Record<string, unknown>, key: string): number | undefined {
|
|
268
|
+
const raw = params[key];
|
|
269
|
+
return typeof raw === "number" && Number.isFinite(raw) ? raw : undefined;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function readBoolean(params: Record<string, unknown>, key: string): boolean | undefined {
|
|
273
|
+
const raw = params[key];
|
|
274
|
+
return typeof raw === "boolean" ? raw : undefined;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function clampInt(value: number | undefined, min: number, max: number, fallback: number): number {
|
|
278
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
279
|
+
return fallback;
|
|
280
|
+
}
|
|
281
|
+
return Math.min(max, Math.max(min, Math.trunc(value)));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function readMemoryType(
|
|
285
|
+
params: Record<string, unknown>,
|
|
286
|
+
key: string,
|
|
287
|
+
): MemoriaMemoryType | undefined {
|
|
288
|
+
const raw = readString(params, key);
|
|
289
|
+
if (!raw) {
|
|
290
|
+
return undefined;
|
|
291
|
+
}
|
|
292
|
+
if (!MEMORIA_MEMORY_TYPES.includes(raw as MemoriaMemoryType)) {
|
|
293
|
+
throw new Error(`${key} must be one of ${MEMORIA_MEMORY_TYPES.join(", ")}`);
|
|
294
|
+
}
|
|
295
|
+
return raw as MemoriaMemoryType;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function readTrustTier(
|
|
299
|
+
params: Record<string, unknown>,
|
|
300
|
+
key: string,
|
|
301
|
+
): MemoriaTrustTier | undefined {
|
|
302
|
+
const raw = readString(params, key);
|
|
303
|
+
if (!raw) {
|
|
304
|
+
return undefined;
|
|
305
|
+
}
|
|
306
|
+
if (!MEMORIA_TRUST_TIERS.includes(raw as MemoriaTrustTier)) {
|
|
307
|
+
throw new Error(`${key} must be one of ${MEMORIA_TRUST_TIERS.join(", ")}`);
|
|
308
|
+
}
|
|
309
|
+
return raw as MemoriaTrustTier;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function readToolTopK(params: Record<string, unknown>, fallback: number): number {
|
|
313
|
+
return clampInt(readNumber(params, "topK") ?? readNumber(params, "maxResults"), 1, 20, fallback);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function readObjectArray(raw: unknown, label: string): Array<Record<string, unknown>> {
|
|
317
|
+
if (!Array.isArray(raw)) {
|
|
318
|
+
throw new Error(`${label} must be an array`);
|
|
319
|
+
}
|
|
320
|
+
return raw.map((entry) => {
|
|
321
|
+
const record = asRecord(entry);
|
|
322
|
+
if (!record) {
|
|
323
|
+
throw new Error(`${label} must be an array of objects`);
|
|
324
|
+
}
|
|
325
|
+
return record;
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function readEntityPayload(
|
|
330
|
+
params: Record<string, unknown>,
|
|
331
|
+
key: string,
|
|
332
|
+
): Array<Record<string, unknown>> {
|
|
333
|
+
const raw = params[key];
|
|
334
|
+
if (Array.isArray(raw)) {
|
|
335
|
+
return readObjectArray(raw, key);
|
|
336
|
+
}
|
|
337
|
+
if (typeof raw === "string" && raw.trim()) {
|
|
338
|
+
let parsed: unknown;
|
|
339
|
+
try {
|
|
340
|
+
parsed = JSON.parse(raw);
|
|
341
|
+
} catch {
|
|
342
|
+
throw new Error(`${key} must be valid JSON or an array`);
|
|
343
|
+
}
|
|
344
|
+
return readObjectArray(parsed, key);
|
|
345
|
+
}
|
|
346
|
+
throw new Error(`${key} required`);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function readObserveMessages(
|
|
350
|
+
params: Record<string, unknown>,
|
|
351
|
+
key: string,
|
|
352
|
+
): Array<{ role: string; content: string }> {
|
|
353
|
+
const records = readObjectArray(params[key], key);
|
|
354
|
+
return records.map((message) => {
|
|
355
|
+
const role = readString(message, "role", { required: true, label: "messages[].role" })!;
|
|
356
|
+
const content = readString(message, "content", {
|
|
357
|
+
required: true,
|
|
358
|
+
label: "messages[].content",
|
|
359
|
+
})!;
|
|
360
|
+
return { role, content };
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function readStringList(
|
|
365
|
+
params: Record<string, unknown>,
|
|
366
|
+
key: string,
|
|
367
|
+
): string[] | undefined {
|
|
368
|
+
const raw = params[key];
|
|
369
|
+
if (raw === undefined) {
|
|
370
|
+
return undefined;
|
|
371
|
+
}
|
|
372
|
+
if (!Array.isArray(raw)) {
|
|
373
|
+
throw new Error(`${key} must be an array of strings`);
|
|
374
|
+
}
|
|
375
|
+
const items = raw.map((entry) => {
|
|
376
|
+
if (typeof entry !== "string" || !entry.trim()) {
|
|
377
|
+
throw new Error(`${key} must be an array of strings`);
|
|
378
|
+
}
|
|
379
|
+
return entry.trim();
|
|
380
|
+
});
|
|
381
|
+
return items.length > 0 ? items : undefined;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function buildMemoryPath(memoryId: string): string {
|
|
385
|
+
return `memoria://${memoryId}`;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const EMBEDDED_ONLY_TOOL_NAMES: string[] = [];
|
|
389
|
+
|
|
390
|
+
const CLI_COMMAND_NAMES = ["memoria", "ltm"] as const;
|
|
391
|
+
|
|
392
|
+
const MEMORY_TOOL_ALIASES: Record<string, string> = {
|
|
393
|
+
memory_recall: "memory_retrieve",
|
|
394
|
+
"ltm list": "memory_list",
|
|
395
|
+
"ltm search": "memory_recall",
|
|
396
|
+
"ltm stats": "memory_stats",
|
|
397
|
+
"ltm health": "memory_health",
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
function buildMemoryStatsPayload(
|
|
401
|
+
config: MemoriaPluginConfig,
|
|
402
|
+
userId: string,
|
|
403
|
+
stats: MemoriaStatsResponse,
|
|
404
|
+
): Record<string, unknown> {
|
|
405
|
+
return {
|
|
406
|
+
backend: config.backend,
|
|
407
|
+
userId,
|
|
408
|
+
activeMemoryCount: stats.activeMemoryCount,
|
|
409
|
+
inactiveMemoryCount: stats.inactiveMemoryCount,
|
|
410
|
+
byType: stats.byType,
|
|
411
|
+
entityCount: stats.entityCount,
|
|
412
|
+
snapshotCount: stats.snapshotCount,
|
|
413
|
+
branchCount: stats.branchCount,
|
|
414
|
+
healthWarnings: stats.healthWarnings,
|
|
415
|
+
autoRecall: config.autoRecall,
|
|
416
|
+
autoObserve: config.autoObserve,
|
|
417
|
+
supportsRollback: true,
|
|
418
|
+
supportsBranches: true,
|
|
419
|
+
partial: stats.partial ?? false,
|
|
420
|
+
limitations: stats.limitations ?? [],
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function buildCapabilitiesPayload(config: MemoriaPluginConfig): Record<string, unknown> {
|
|
425
|
+
const limitations = [
|
|
426
|
+
"OpenClaw reserves `openclaw memory` for built-in file-memory commands; compatibility CLI is exposed as `openclaw ltm`.",
|
|
427
|
+
"memory_get is resolved from recent tool results plus a bounded Rust MCP scan; if an older memory is missing, rerun memory_search or memory_list first.",
|
|
428
|
+
];
|
|
429
|
+
|
|
430
|
+
return {
|
|
431
|
+
backend: config.backend,
|
|
432
|
+
userIdStrategy: config.userIdStrategy,
|
|
433
|
+
autoRecall: config.autoRecall,
|
|
434
|
+
autoObserve: config.autoObserve,
|
|
435
|
+
llmConfigured: Boolean(config.llmApiKey || config.backend === "http"),
|
|
436
|
+
tools: supportedToolNames(),
|
|
437
|
+
embeddedOnly: [...EMBEDDED_ONLY_TOOL_NAMES],
|
|
438
|
+
cliCommands: [...CLI_COMMAND_NAMES],
|
|
439
|
+
aliases: MEMORY_TOOL_ALIASES,
|
|
440
|
+
backendFeatures: {
|
|
441
|
+
rollback: true,
|
|
442
|
+
snapshots: true,
|
|
443
|
+
branches: true,
|
|
444
|
+
governance: true,
|
|
445
|
+
reflect: true,
|
|
446
|
+
entities: true,
|
|
447
|
+
rebuildIndex: true,
|
|
448
|
+
},
|
|
449
|
+
limitations,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function normalizeScore(confidence?: number | null): number {
|
|
454
|
+
if (typeof confidence !== "number" || !Number.isFinite(confidence)) {
|
|
455
|
+
return 0.5;
|
|
456
|
+
}
|
|
457
|
+
if (confidence < 0) {
|
|
458
|
+
return 0;
|
|
459
|
+
}
|
|
460
|
+
if (confidence > 1) {
|
|
461
|
+
return 1;
|
|
462
|
+
}
|
|
463
|
+
return confidence;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function sliceContent(content: string, from?: number, lines?: number): string {
|
|
467
|
+
const allLines = content.split(/\r?\n/);
|
|
468
|
+
const start = Math.max(0, (from ?? 1) - 1);
|
|
469
|
+
const end = typeof lines === "number" && lines > 0 ? start + lines : allLines.length;
|
|
470
|
+
return allLines.slice(start, end).join("\n");
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function resolveUserId(
|
|
474
|
+
config: MemoriaPluginConfig,
|
|
475
|
+
ctx: PluginIdentityContext,
|
|
476
|
+
explicitUserId?: string,
|
|
477
|
+
): string {
|
|
478
|
+
if (explicitUserId?.trim()) {
|
|
479
|
+
return explicitUserId.trim();
|
|
480
|
+
}
|
|
481
|
+
if (config.userIdStrategy === "sessionKey") {
|
|
482
|
+
return ctx.sessionKey?.trim() || ctx.sessionId?.trim() || config.defaultUserId;
|
|
483
|
+
}
|
|
484
|
+
if (config.userIdStrategy === "agentId") {
|
|
485
|
+
return ctx.agentId?.trim() || ctx.sessionKey?.trim() || config.defaultUserId;
|
|
486
|
+
}
|
|
487
|
+
return config.defaultUserId;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function toMemorySearchPayload(memories: MemoriaMemoryRecord[]) {
|
|
491
|
+
return memories.map((memory) => ({
|
|
492
|
+
path: buildMemoryPath(memory.memory_id),
|
|
493
|
+
startLine: 1,
|
|
494
|
+
endLine: Math.max(1, memory.content.split(/\r?\n/).length),
|
|
495
|
+
score: normalizeScore(memory.confidence),
|
|
496
|
+
snippet: memory.content,
|
|
497
|
+
source: "memory",
|
|
498
|
+
}));
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function supportedToolNames(): string[] {
|
|
502
|
+
return [
|
|
503
|
+
"memory_search",
|
|
504
|
+
"memory_get",
|
|
505
|
+
"memory_store",
|
|
506
|
+
"memory_retrieve",
|
|
507
|
+
"memory_recall",
|
|
508
|
+
"memory_list",
|
|
509
|
+
"memory_stats",
|
|
510
|
+
"memory_profile",
|
|
511
|
+
"memory_correct",
|
|
512
|
+
"memory_purge",
|
|
513
|
+
"memory_forget",
|
|
514
|
+
"memory_health",
|
|
515
|
+
"memory_observe",
|
|
516
|
+
"memory_governance",
|
|
517
|
+
"memory_consolidate",
|
|
518
|
+
"memory_reflect",
|
|
519
|
+
"memory_extract_entities",
|
|
520
|
+
"memory_link_entities",
|
|
521
|
+
"memory_rebuild_index",
|
|
522
|
+
"memory_capabilities",
|
|
523
|
+
"memory_snapshot",
|
|
524
|
+
"memory_snapshots",
|
|
525
|
+
"memory_rollback",
|
|
526
|
+
"memory_branch",
|
|
527
|
+
"memory_branches",
|
|
528
|
+
"memory_checkout",
|
|
529
|
+
"memory_branch_delete",
|
|
530
|
+
"memory_merge",
|
|
531
|
+
"memory_diff",
|
|
532
|
+
];
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function hasMessageError(value: unknown): value is Record<string, unknown> & { message: string } {
|
|
536
|
+
const record = asRecord(value);
|
|
537
|
+
return Boolean(record && "error" in record && typeof record.message === "string");
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function hasNonEmptyString(value: unknown): boolean {
|
|
541
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function shouldShowOnboardingHint(rawPluginConfig: unknown): boolean {
|
|
545
|
+
const raw = asRecord(rawPluginConfig);
|
|
546
|
+
if (!raw) {
|
|
547
|
+
return true;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const backend = hasNonEmptyString(raw.backend) ? String(raw.backend).trim().toLowerCase() : "";
|
|
551
|
+
const hasCloudConfig = hasNonEmptyString(raw.apiUrl) || hasNonEmptyString(raw.apiKey);
|
|
552
|
+
const hasLocalConfig = hasNonEmptyString(raw.dbUrl);
|
|
553
|
+
|
|
554
|
+
return !(backend === "http" || hasCloudConfig || hasLocalConfig);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const ONBOARDING_HINT_ONCE_KEY = "__memory_memoria_onboarding_hint_logged__";
|
|
558
|
+
|
|
559
|
+
function shouldLogOnboardingHintOnce(): boolean {
|
|
560
|
+
const state = globalThis as Record<string, unknown>;
|
|
561
|
+
if (state[ONBOARDING_HINT_ONCE_KEY] === true) {
|
|
562
|
+
return false;
|
|
563
|
+
}
|
|
564
|
+
state[ONBOARDING_HINT_ONCE_KEY] = true;
|
|
565
|
+
return true;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const plugin = {
|
|
569
|
+
id: "memory-memoria",
|
|
570
|
+
name: "Memory (Memoria)",
|
|
571
|
+
description: "Memoria-backed long-term memory plugin for OpenClaw powered by the Rust memoria CLI and API.",
|
|
572
|
+
kind: "memory" as const,
|
|
573
|
+
configSchema: memoriaPluginConfigSchema,
|
|
574
|
+
|
|
575
|
+
register(api: OpenClawPluginApi) {
|
|
576
|
+
const config = parseMemoriaPluginConfig(api.pluginConfig);
|
|
577
|
+
const client = new MemoriaClient(config);
|
|
578
|
+
|
|
579
|
+
const needsSetup = shouldShowOnboardingHint(api.pluginConfig);
|
|
580
|
+
const isFirstRegister = shouldLogOnboardingHintOnce();
|
|
581
|
+
|
|
582
|
+
if (isFirstRegister) {
|
|
583
|
+
api.logger.info(
|
|
584
|
+
`memory-memoria: registered (${needsSetup ? "pending setup" : config.backend})`,
|
|
585
|
+
);
|
|
586
|
+
|
|
587
|
+
const isEnableCommand =
|
|
588
|
+
process.argv.some((arg) => arg === "enable") &&
|
|
589
|
+
process.argv.some((arg) => arg === "plugins");
|
|
590
|
+
if (needsSetup && isEnableCommand) {
|
|
591
|
+
api.logger.info(
|
|
592
|
+
"🧠 Memoria next step (Cloud, recommended): openclaw memoria setup --mode cloud --api-url <MEMORIA_API_URL> --api-key <MEMORIA_API_KEY> --install-memoria",
|
|
593
|
+
);
|
|
594
|
+
api.logger.info(
|
|
595
|
+
"🧩 Local quick start: openclaw memoria setup --mode local --install-memoria --embedding-api-key <EMBEDDING_API_KEY>",
|
|
596
|
+
);
|
|
597
|
+
api.logger.info(
|
|
598
|
+
"📘 More options: openclaw memoria setup --help",
|
|
599
|
+
);
|
|
600
|
+
api.logger.info(
|
|
601
|
+
"🧪 Verify with: openclaw memoria health",
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
api.on("before_prompt_build", async () => ({
|
|
607
|
+
appendSystemContext: MEMORIA_AGENT_GUIDANCE,
|
|
608
|
+
}));
|
|
609
|
+
|
|
610
|
+
api.registerTool(
|
|
611
|
+
(ctx) => {
|
|
612
|
+
const userIdProperty = {
|
|
613
|
+
type: "string",
|
|
614
|
+
description: "Optional explicit Memoria user_id override",
|
|
615
|
+
} as const;
|
|
616
|
+
const forceProperty = {
|
|
617
|
+
type: "boolean",
|
|
618
|
+
description: "Skip cooldown when the backend supports it",
|
|
619
|
+
} as const;
|
|
620
|
+
const modeProperty = {
|
|
621
|
+
type: "string",
|
|
622
|
+
description: "auto uses internal LLM when configured, otherwise falls back to candidates",
|
|
623
|
+
enum: ["auto", "internal", "candidates"],
|
|
624
|
+
} as const;
|
|
625
|
+
|
|
626
|
+
const memorySearchTool = {
|
|
627
|
+
label: "Memory Search",
|
|
628
|
+
name: "memory_search",
|
|
629
|
+
description:
|
|
630
|
+
"Search Memoria for prior work, preferences, facts, decisions, or todos before answering questions that depend on earlier context.",
|
|
631
|
+
parameters: objectSchema(
|
|
632
|
+
{
|
|
633
|
+
query: { type: "string", description: "Natural-language memory query" },
|
|
634
|
+
topK: {
|
|
635
|
+
type: "integer",
|
|
636
|
+
description: "Maximum number of results to return",
|
|
637
|
+
minimum: 1,
|
|
638
|
+
maximum: 20,
|
|
639
|
+
},
|
|
640
|
+
maxResults: {
|
|
641
|
+
type: "integer",
|
|
642
|
+
description: "Alias for topK",
|
|
643
|
+
minimum: 1,
|
|
644
|
+
maximum: 20,
|
|
645
|
+
},
|
|
646
|
+
userId: userIdProperty,
|
|
647
|
+
},
|
|
648
|
+
["query"],
|
|
649
|
+
),
|
|
650
|
+
execute: async (_toolCallId: string, rawParams: unknown) => {
|
|
651
|
+
const params = asRecord(rawParams) ?? {};
|
|
652
|
+
const query = readString(params, "query", { required: true, label: "query" })!;
|
|
653
|
+
const userId = resolveUserId(config, ctx, readString(params, "userId"));
|
|
654
|
+
const topK = readToolTopK(params, config.retrieveTopK);
|
|
655
|
+
|
|
656
|
+
try {
|
|
657
|
+
const memories = await client.search({
|
|
658
|
+
userId,
|
|
659
|
+
query,
|
|
660
|
+
topK,
|
|
661
|
+
});
|
|
662
|
+
return jsonResult({
|
|
663
|
+
provider: "memoria",
|
|
664
|
+
backend: config.backend,
|
|
665
|
+
userId,
|
|
666
|
+
results: toMemorySearchPayload(memories),
|
|
667
|
+
memories,
|
|
668
|
+
});
|
|
669
|
+
} catch (error) {
|
|
670
|
+
return jsonResult({
|
|
671
|
+
results: [],
|
|
672
|
+
memories: [],
|
|
673
|
+
disabled: true,
|
|
674
|
+
unavailable: true,
|
|
675
|
+
error: error instanceof Error ? error.message : String(error),
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
},
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
const memoryGetTool = {
|
|
682
|
+
label: "Memory Get",
|
|
683
|
+
name: "memory_get",
|
|
684
|
+
description: "Read a specific Memoria memory returned by memory_search.",
|
|
685
|
+
parameters: objectSchema(
|
|
686
|
+
{
|
|
687
|
+
path: { type: "string", description: "memoria://<memory_id>" },
|
|
688
|
+
from: { type: "integer", description: "Start line (1-based)", minimum: 1 },
|
|
689
|
+
lines: { type: "integer", description: "Number of lines", minimum: 1 },
|
|
690
|
+
userId: userIdProperty,
|
|
691
|
+
},
|
|
692
|
+
["path"],
|
|
693
|
+
),
|
|
694
|
+
execute: async (_toolCallId: string, rawParams: unknown) => {
|
|
695
|
+
const params = asRecord(rawParams) ?? {};
|
|
696
|
+
const rawPath = readString(params, "path", { required: true, label: "path" })!;
|
|
697
|
+
const memoryId = rawPath.startsWith("memoria://")
|
|
698
|
+
? rawPath.slice("memoria://".length)
|
|
699
|
+
: "";
|
|
700
|
+
if (!memoryId) {
|
|
701
|
+
return jsonResult({
|
|
702
|
+
path: rawPath,
|
|
703
|
+
text: "",
|
|
704
|
+
disabled: true,
|
|
705
|
+
error: "invalid memoria path",
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
const userId = resolveUserId(config, ctx, readString(params, "userId"));
|
|
710
|
+
const from = clampInt(readNumber(params, "from"), 1, Number.MAX_SAFE_INTEGER, 1);
|
|
711
|
+
const lines =
|
|
712
|
+
readNumber(params, "lines") === undefined
|
|
713
|
+
? undefined
|
|
714
|
+
: clampInt(readNumber(params, "lines"), 1, Number.MAX_SAFE_INTEGER, 1);
|
|
715
|
+
|
|
716
|
+
try {
|
|
717
|
+
const memory = await client.getMemory({ userId, memoryId });
|
|
718
|
+
if (!memory) {
|
|
719
|
+
return jsonResult({
|
|
720
|
+
path: rawPath,
|
|
721
|
+
text: "",
|
|
722
|
+
disabled: true,
|
|
723
|
+
error: "memory not found",
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
return jsonResult({
|
|
727
|
+
path: rawPath,
|
|
728
|
+
text: sliceContent(memory.content, from, lines),
|
|
729
|
+
memory,
|
|
730
|
+
});
|
|
731
|
+
} catch (error) {
|
|
732
|
+
return jsonResult({
|
|
733
|
+
path: rawPath,
|
|
734
|
+
text: "",
|
|
735
|
+
disabled: true,
|
|
736
|
+
error: error instanceof Error ? error.message : String(error),
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
},
|
|
740
|
+
};
|
|
741
|
+
|
|
742
|
+
const memoryHealthTool = {
|
|
743
|
+
label: "Memory Health",
|
|
744
|
+
name: "memory_health",
|
|
745
|
+
description: "Check Memoria connectivity and health warnings for the current user.",
|
|
746
|
+
parameters: objectSchema({
|
|
747
|
+
userId: userIdProperty,
|
|
748
|
+
}),
|
|
749
|
+
execute: async (_toolCallId: string, rawParams: unknown) => {
|
|
750
|
+
const params = asRecord(rawParams) ?? {};
|
|
751
|
+
const userId = resolveUserId(config, ctx, readString(params, "userId"));
|
|
752
|
+
const health = await client.health(userId);
|
|
753
|
+
return jsonResult({
|
|
754
|
+
userId,
|
|
755
|
+
backend: config.backend,
|
|
756
|
+
...(asRecord(health) ?? {}),
|
|
757
|
+
});
|
|
758
|
+
},
|
|
759
|
+
};
|
|
760
|
+
|
|
761
|
+
const memoryStoreTool = {
|
|
762
|
+
label: "Memory Store",
|
|
763
|
+
name: "memory_store",
|
|
764
|
+
description: "Store a durable memory in Memoria.",
|
|
765
|
+
parameters: objectSchema(
|
|
766
|
+
{
|
|
767
|
+
content: { type: "string", description: "Memory content to store" },
|
|
768
|
+
memoryType: {
|
|
769
|
+
type: "string",
|
|
770
|
+
description: `One of: ${MEMORIA_MEMORY_TYPES.join(", ")}`,
|
|
771
|
+
enum: [...MEMORIA_MEMORY_TYPES],
|
|
772
|
+
},
|
|
773
|
+
trustTier: {
|
|
774
|
+
type: "string",
|
|
775
|
+
description: `Optional trust tier: ${MEMORIA_TRUST_TIERS.join(", ")}`,
|
|
776
|
+
enum: [...MEMORIA_TRUST_TIERS],
|
|
777
|
+
},
|
|
778
|
+
sessionId: {
|
|
779
|
+
type: "string",
|
|
780
|
+
description: "Optional session scope for the memory",
|
|
781
|
+
},
|
|
782
|
+
source: {
|
|
783
|
+
type: "string",
|
|
784
|
+
description: "Optional source label",
|
|
785
|
+
},
|
|
786
|
+
userId: userIdProperty,
|
|
787
|
+
},
|
|
788
|
+
["content"],
|
|
789
|
+
),
|
|
790
|
+
execute: async (_toolCallId: string, rawParams: unknown) => {
|
|
791
|
+
const params = asRecord(rawParams) ?? {};
|
|
792
|
+
const content = readString(params, "content", {
|
|
793
|
+
required: true,
|
|
794
|
+
label: "content",
|
|
795
|
+
})!;
|
|
796
|
+
const memoryType = readMemoryType(params, "memoryType") ?? "semantic";
|
|
797
|
+
const trustTier = readTrustTier(params, "trustTier");
|
|
798
|
+
const userId = resolveUserId(config, ctx, readString(params, "userId"));
|
|
799
|
+
const stored = await client.storeMemory({
|
|
800
|
+
userId,
|
|
801
|
+
content,
|
|
802
|
+
memoryType,
|
|
803
|
+
trustTier,
|
|
804
|
+
sessionId: readString(params, "sessionId") ?? ctx.sessionId,
|
|
805
|
+
source: readString(params, "source") ?? "openclaw:memory_store",
|
|
806
|
+
});
|
|
807
|
+
return textResult(`Stored memory ${stored.memory_id}.`, {
|
|
808
|
+
ok: true,
|
|
809
|
+
userId,
|
|
810
|
+
path: buildMemoryPath(stored.memory_id),
|
|
811
|
+
memory: stored,
|
|
812
|
+
});
|
|
813
|
+
},
|
|
814
|
+
};
|
|
815
|
+
|
|
816
|
+
const executeMemoryRetrieve = async (_toolCallId: string, rawParams: unknown) => {
|
|
817
|
+
const params = asRecord(rawParams) ?? {};
|
|
818
|
+
const query = readString(params, "query", { required: true, label: "query" })!;
|
|
819
|
+
const userId = resolveUserId(config, ctx, readString(params, "userId"));
|
|
820
|
+
const topK = readToolTopK(params, config.retrieveTopK);
|
|
821
|
+
const sessionId = readString(params, "sessionId") ?? ctx.sessionId;
|
|
822
|
+
|
|
823
|
+
const [memories, health] = await Promise.all([
|
|
824
|
+
client.retrieve({
|
|
825
|
+
userId,
|
|
826
|
+
query,
|
|
827
|
+
topK,
|
|
828
|
+
memoryTypes: config.retrieveMemoryTypes,
|
|
829
|
+
sessionId,
|
|
830
|
+
includeCrossSession: config.includeCrossSession,
|
|
831
|
+
}),
|
|
832
|
+
client.health(userId).catch(() => null),
|
|
833
|
+
]);
|
|
834
|
+
|
|
835
|
+
const warnings = Array.isArray(asRecord(health)?.warnings)
|
|
836
|
+
? (asRecord(health)?.warnings as unknown[]).filter(
|
|
837
|
+
(entry): entry is string => typeof entry === "string" && entry.trim().length > 0,
|
|
838
|
+
)
|
|
839
|
+
: [];
|
|
840
|
+
|
|
841
|
+
return jsonResult({
|
|
842
|
+
backend: config.backend,
|
|
843
|
+
userId,
|
|
844
|
+
count: memories.length,
|
|
845
|
+
warnings,
|
|
846
|
+
memories,
|
|
847
|
+
});
|
|
848
|
+
};
|
|
849
|
+
|
|
850
|
+
const memoryRetrieveParameters = objectSchema(
|
|
851
|
+
{
|
|
852
|
+
query: { type: "string", description: "Retrieval query" },
|
|
853
|
+
topK: {
|
|
854
|
+
type: "integer",
|
|
855
|
+
description: "Maximum number of memories to retrieve",
|
|
856
|
+
minimum: 1,
|
|
857
|
+
maximum: 20,
|
|
858
|
+
},
|
|
859
|
+
maxResults: {
|
|
860
|
+
type: "integer",
|
|
861
|
+
description: "Alias for topK",
|
|
862
|
+
minimum: 1,
|
|
863
|
+
maximum: 20,
|
|
864
|
+
},
|
|
865
|
+
sessionId: {
|
|
866
|
+
type: "string",
|
|
867
|
+
description: "Optional session scope hint",
|
|
868
|
+
},
|
|
869
|
+
userId: userIdProperty,
|
|
870
|
+
},
|
|
871
|
+
["query"],
|
|
872
|
+
);
|
|
873
|
+
|
|
874
|
+
const memoryRetrieveTool = {
|
|
875
|
+
label: "Memory Retrieve",
|
|
876
|
+
name: "memory_retrieve",
|
|
877
|
+
description: "Retrieve the most relevant memories for a natural-language query.",
|
|
878
|
+
parameters: memoryRetrieveParameters,
|
|
879
|
+
execute: executeMemoryRetrieve,
|
|
880
|
+
};
|
|
881
|
+
|
|
882
|
+
const memoryRecallTool = {
|
|
883
|
+
label: "Memory Recall",
|
|
884
|
+
name: "memory_recall",
|
|
885
|
+
description:
|
|
886
|
+
"Compatibility alias for memory_retrieve, matching memory-lancedb-pro's recall tool.",
|
|
887
|
+
parameters: memoryRetrieveParameters,
|
|
888
|
+
execute: executeMemoryRetrieve,
|
|
889
|
+
};
|
|
890
|
+
|
|
891
|
+
const memoryListTool = {
|
|
892
|
+
label: "Memory List",
|
|
893
|
+
name: "memory_list",
|
|
894
|
+
description: "List recent memories for the current user.",
|
|
895
|
+
parameters: objectSchema({
|
|
896
|
+
memoryType: {
|
|
897
|
+
type: "string",
|
|
898
|
+
description: `Optional memory type filter: ${MEMORIA_MEMORY_TYPES.join(", ")}`,
|
|
899
|
+
enum: [...MEMORIA_MEMORY_TYPES],
|
|
900
|
+
},
|
|
901
|
+
limit: {
|
|
902
|
+
type: "integer",
|
|
903
|
+
description: "Maximum number of memories to return",
|
|
904
|
+
minimum: 1,
|
|
905
|
+
maximum: 200,
|
|
906
|
+
},
|
|
907
|
+
sessionId: {
|
|
908
|
+
type: "string",
|
|
909
|
+
description: "Optional session filter",
|
|
910
|
+
},
|
|
911
|
+
includeInactive: {
|
|
912
|
+
type: "boolean",
|
|
913
|
+
description: "Include inactive memories when the backend supports it",
|
|
914
|
+
},
|
|
915
|
+
userId: userIdProperty,
|
|
916
|
+
}),
|
|
917
|
+
execute: async (_toolCallId: string, rawParams: unknown) => {
|
|
918
|
+
const params = asRecord(rawParams) ?? {};
|
|
919
|
+
const userId = resolveUserId(config, ctx, readString(params, "userId"));
|
|
920
|
+
const result = await client.listMemories({
|
|
921
|
+
userId,
|
|
922
|
+
memoryType: readMemoryType(params, "memoryType"),
|
|
923
|
+
limit: clampInt(readNumber(params, "limit"), 1, 200, 20),
|
|
924
|
+
sessionId: readString(params, "sessionId"),
|
|
925
|
+
includeInactive: readBoolean(params, "includeInactive") ?? false,
|
|
926
|
+
});
|
|
927
|
+
return jsonResult({
|
|
928
|
+
backend: config.backend,
|
|
929
|
+
userId,
|
|
930
|
+
count: result.count,
|
|
931
|
+
items: result.items,
|
|
932
|
+
includeInactive: result.include_inactive ?? false,
|
|
933
|
+
partial: result.partial ?? false,
|
|
934
|
+
limitations: result.limitations ?? [],
|
|
935
|
+
});
|
|
936
|
+
},
|
|
937
|
+
};
|
|
938
|
+
|
|
939
|
+
const memoryStatsTool = {
|
|
940
|
+
label: "Memory Stats",
|
|
941
|
+
name: "memory_stats",
|
|
942
|
+
description: "Return aggregate memory statistics for the current user.",
|
|
943
|
+
parameters: objectSchema({
|
|
944
|
+
userId: userIdProperty,
|
|
945
|
+
}),
|
|
946
|
+
execute: async (_toolCallId: string, rawParams: unknown) => {
|
|
947
|
+
const params = asRecord(rawParams) ?? {};
|
|
948
|
+
const userId = resolveUserId(config, ctx, readString(params, "userId"));
|
|
949
|
+
const stats = await client.stats(userId);
|
|
950
|
+
return jsonResult(buildMemoryStatsPayload(config, userId, stats));
|
|
951
|
+
},
|
|
952
|
+
};
|
|
953
|
+
|
|
954
|
+
const memoryProfileTool = {
|
|
955
|
+
label: "Memory Profile",
|
|
956
|
+
name: "memory_profile",
|
|
957
|
+
description: "Read the Memoria profile summary for the current user.",
|
|
958
|
+
parameters: objectSchema({
|
|
959
|
+
userId: userIdProperty,
|
|
960
|
+
}),
|
|
961
|
+
execute: async (_toolCallId: string, rawParams: unknown) => {
|
|
962
|
+
const params = asRecord(rawParams) ?? {};
|
|
963
|
+
const userId = resolveUserId(config, ctx, readString(params, "userId"));
|
|
964
|
+
const profile = await client.profile(userId);
|
|
965
|
+
const summary = profile.profile?.trim() || "No profile available yet.";
|
|
966
|
+
return textResult(summary, {
|
|
967
|
+
profile,
|
|
968
|
+
});
|
|
969
|
+
},
|
|
970
|
+
};
|
|
971
|
+
|
|
972
|
+
const memoryCorrectTool = {
|
|
973
|
+
label: "Memory Correct",
|
|
974
|
+
name: "memory_correct",
|
|
975
|
+
description: "Correct an existing memory by id or by semantic query.",
|
|
976
|
+
parameters: objectSchema(
|
|
977
|
+
{
|
|
978
|
+
memoryId: { type: "string", description: "Specific memory id to correct" },
|
|
979
|
+
query: { type: "string", description: "Semantic query used to locate the memory" },
|
|
980
|
+
newContent: { type: "string", description: "Corrected memory content" },
|
|
981
|
+
reason: { type: "string", description: "Optional correction reason" },
|
|
982
|
+
userId: userIdProperty,
|
|
983
|
+
},
|
|
984
|
+
["newContent"],
|
|
985
|
+
),
|
|
986
|
+
execute: async (_toolCallId: string, rawParams: unknown) => {
|
|
987
|
+
const params = asRecord(rawParams) ?? {};
|
|
988
|
+
const memoryId = readString(params, "memoryId");
|
|
989
|
+
const query = readString(params, "query");
|
|
990
|
+
const newContent = readString(params, "newContent", {
|
|
991
|
+
required: true,
|
|
992
|
+
label: "newContent",
|
|
993
|
+
})!;
|
|
994
|
+
const reason = readString(params, "reason") ?? "";
|
|
995
|
+
const userId = resolveUserId(config, ctx, readString(params, "userId"));
|
|
996
|
+
|
|
997
|
+
if (!memoryId && !query) {
|
|
998
|
+
throw new Error("memoryId or query required");
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
const updated = memoryId
|
|
1002
|
+
? await client.correctById({ userId, memoryId, newContent, reason })
|
|
1003
|
+
: await client.correctByQuery({ userId, query: query!, newContent, reason });
|
|
1004
|
+
|
|
1005
|
+
if (hasMessageError(updated)) {
|
|
1006
|
+
return textResult(updated.message, {
|
|
1007
|
+
ok: false,
|
|
1008
|
+
userId,
|
|
1009
|
+
result: updated,
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
return textResult(`Corrected memory ${updated.memory_id}.`, {
|
|
1014
|
+
ok: true,
|
|
1015
|
+
userId,
|
|
1016
|
+
memory: updated,
|
|
1017
|
+
});
|
|
1018
|
+
},
|
|
1019
|
+
};
|
|
1020
|
+
|
|
1021
|
+
const memoryPurgeTool = {
|
|
1022
|
+
label: "Memory Purge",
|
|
1023
|
+
name: "memory_purge",
|
|
1024
|
+
description: "Delete memories by id or by keyword topic.",
|
|
1025
|
+
parameters: objectSchema({
|
|
1026
|
+
memoryId: { type: "string", description: "Specific memory id to delete" },
|
|
1027
|
+
topic: { type: "string", description: "Keyword/topic for bulk deletion" },
|
|
1028
|
+
reason: { type: "string", description: "Optional deletion reason" },
|
|
1029
|
+
userId: userIdProperty,
|
|
1030
|
+
}),
|
|
1031
|
+
execute: async (_toolCallId: string, rawParams: unknown) => {
|
|
1032
|
+
const params = asRecord(rawParams) ?? {};
|
|
1033
|
+
const memoryId = readString(params, "memoryId");
|
|
1034
|
+
const topic = readString(params, "topic");
|
|
1035
|
+
const reason = readString(params, "reason") ?? "";
|
|
1036
|
+
const userId = resolveUserId(config, ctx, readString(params, "userId"));
|
|
1037
|
+
|
|
1038
|
+
if (!memoryId && !topic) {
|
|
1039
|
+
throw new Error("memoryId or topic required");
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
const result = await client.purgeMemory({
|
|
1043
|
+
userId,
|
|
1044
|
+
memoryId,
|
|
1045
|
+
topic,
|
|
1046
|
+
reason,
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
return textResult(`Purged ${String(result.purged ?? 0)} memories.`, {
|
|
1050
|
+
ok: true,
|
|
1051
|
+
userId,
|
|
1052
|
+
result,
|
|
1053
|
+
});
|
|
1054
|
+
},
|
|
1055
|
+
};
|
|
1056
|
+
|
|
1057
|
+
const memoryForgetTool = {
|
|
1058
|
+
label: "Memory Forget",
|
|
1059
|
+
name: "memory_forget",
|
|
1060
|
+
description: "Delete a memory by id or find one by query and delete it.",
|
|
1061
|
+
parameters: objectSchema({
|
|
1062
|
+
memoryId: { type: "string", description: "Specific memory id to delete" },
|
|
1063
|
+
query: { type: "string", description: "Semantic query used to locate a memory" },
|
|
1064
|
+
reason: { type: "string", description: "Optional deletion reason" },
|
|
1065
|
+
userId: userIdProperty,
|
|
1066
|
+
}),
|
|
1067
|
+
execute: async (_toolCallId: string, rawParams: unknown) => {
|
|
1068
|
+
const params = asRecord(rawParams) ?? {};
|
|
1069
|
+
const memoryId = readString(params, "memoryId");
|
|
1070
|
+
const query = readString(params, "query");
|
|
1071
|
+
const reason = readString(params, "reason") ?? "";
|
|
1072
|
+
const userId = resolveUserId(config, ctx, readString(params, "userId"));
|
|
1073
|
+
|
|
1074
|
+
if (!memoryId && !query) {
|
|
1075
|
+
throw new Error("memoryId or query required");
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
if (memoryId) {
|
|
1079
|
+
const result = await client.deleteMemory({ userId, memoryId, reason });
|
|
1080
|
+
return textResult(`Forgot memory ${memoryId}.`, {
|
|
1081
|
+
ok: true,
|
|
1082
|
+
userId,
|
|
1083
|
+
result,
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
const candidates = await client.search({
|
|
1088
|
+
userId,
|
|
1089
|
+
query: query!,
|
|
1090
|
+
topK: 5,
|
|
1091
|
+
});
|
|
1092
|
+
|
|
1093
|
+
if (candidates.length === 0) {
|
|
1094
|
+
return textResult("No matching memories found.", {
|
|
1095
|
+
ok: false,
|
|
1096
|
+
userId,
|
|
1097
|
+
candidates: [],
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
if (candidates.length > 1) {
|
|
1102
|
+
return textResult(
|
|
1103
|
+
`Found ${candidates.length} candidates. Re-run with memoryId.\n${formatMemoryList(candidates)}`,
|
|
1104
|
+
{
|
|
1105
|
+
ok: false,
|
|
1106
|
+
userId,
|
|
1107
|
+
candidates,
|
|
1108
|
+
},
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
const result = await client.deleteMemory({
|
|
1113
|
+
userId,
|
|
1114
|
+
memoryId: candidates[0].memory_id,
|
|
1115
|
+
reason,
|
|
1116
|
+
});
|
|
1117
|
+
return textResult(`Forgot memory ${candidates[0].memory_id}.`, {
|
|
1118
|
+
ok: true,
|
|
1119
|
+
userId,
|
|
1120
|
+
result,
|
|
1121
|
+
memory: candidates[0],
|
|
1122
|
+
});
|
|
1123
|
+
},
|
|
1124
|
+
};
|
|
1125
|
+
|
|
1126
|
+
const memoryObserveTool = {
|
|
1127
|
+
label: "Memory Observe",
|
|
1128
|
+
name: "memory_observe",
|
|
1129
|
+
description: "Run Memoria's observe pipeline over explicit conversation messages.",
|
|
1130
|
+
parameters: objectSchema(
|
|
1131
|
+
{
|
|
1132
|
+
messages: {
|
|
1133
|
+
type: "array",
|
|
1134
|
+
description: "Conversation messages as { role, content } objects",
|
|
1135
|
+
items: {
|
|
1136
|
+
type: "object",
|
|
1137
|
+
additionalProperties: false,
|
|
1138
|
+
properties: {
|
|
1139
|
+
role: { type: "string" },
|
|
1140
|
+
content: { type: "string" },
|
|
1141
|
+
},
|
|
1142
|
+
required: ["role", "content"],
|
|
1143
|
+
},
|
|
1144
|
+
},
|
|
1145
|
+
sourceEventIds: {
|
|
1146
|
+
type: "array",
|
|
1147
|
+
description: "Optional upstream event identifiers",
|
|
1148
|
+
items: { type: "string" },
|
|
1149
|
+
},
|
|
1150
|
+
sessionId: {
|
|
1151
|
+
type: "string",
|
|
1152
|
+
description: "Optional session scope passed through to Memoria observe",
|
|
1153
|
+
},
|
|
1154
|
+
userId: userIdProperty,
|
|
1155
|
+
},
|
|
1156
|
+
["messages"],
|
|
1157
|
+
),
|
|
1158
|
+
execute: async (_toolCallId: string, rawParams: unknown) => {
|
|
1159
|
+
const params = asRecord(rawParams) ?? {};
|
|
1160
|
+
const userId = resolveUserId(config, ctx, readString(params, "userId"));
|
|
1161
|
+
const messages = readObserveMessages(params, "messages");
|
|
1162
|
+
const sourceEventIds = readStringList(params, "sourceEventIds");
|
|
1163
|
+
const created = await client.observe({
|
|
1164
|
+
userId,
|
|
1165
|
+
messages,
|
|
1166
|
+
sourceEventIds,
|
|
1167
|
+
sessionId: readString(params, "sessionId") ?? ctx.sessionId,
|
|
1168
|
+
});
|
|
1169
|
+
return jsonResult({
|
|
1170
|
+
ok: true,
|
|
1171
|
+
userId,
|
|
1172
|
+
count: created.length,
|
|
1173
|
+
memories: created,
|
|
1174
|
+
});
|
|
1175
|
+
},
|
|
1176
|
+
};
|
|
1177
|
+
|
|
1178
|
+
const memoryGovernanceTool = {
|
|
1179
|
+
label: "Memory Governance",
|
|
1180
|
+
name: "memory_governance",
|
|
1181
|
+
description: "Run Memoria governance for the current user.",
|
|
1182
|
+
parameters: objectSchema({
|
|
1183
|
+
force: forceProperty,
|
|
1184
|
+
userId: userIdProperty,
|
|
1185
|
+
}),
|
|
1186
|
+
execute: async (_toolCallId: string, rawParams: unknown) => {
|
|
1187
|
+
const params = asRecord(rawParams) ?? {};
|
|
1188
|
+
const userId = resolveUserId(config, ctx, readString(params, "userId"));
|
|
1189
|
+
const result = await client.governance({
|
|
1190
|
+
userId,
|
|
1191
|
+
force: readBoolean(params, "force") ?? false,
|
|
1192
|
+
});
|
|
1193
|
+
return jsonResult({
|
|
1194
|
+
userId,
|
|
1195
|
+
result,
|
|
1196
|
+
});
|
|
1197
|
+
},
|
|
1198
|
+
};
|
|
1199
|
+
|
|
1200
|
+
const memoryConsolidateTool = {
|
|
1201
|
+
label: "Memory Consolidate",
|
|
1202
|
+
name: "memory_consolidate",
|
|
1203
|
+
description: "Run Memoria graph consolidation for the current user.",
|
|
1204
|
+
parameters: objectSchema({
|
|
1205
|
+
force: forceProperty,
|
|
1206
|
+
userId: userIdProperty,
|
|
1207
|
+
}),
|
|
1208
|
+
execute: async (_toolCallId: string, rawParams: unknown) => {
|
|
1209
|
+
const params = asRecord(rawParams) ?? {};
|
|
1210
|
+
const userId = resolveUserId(config, ctx, readString(params, "userId"));
|
|
1211
|
+
const result = await client.consolidate({
|
|
1212
|
+
userId,
|
|
1213
|
+
force: readBoolean(params, "force") ?? false,
|
|
1214
|
+
});
|
|
1215
|
+
return jsonResult({
|
|
1216
|
+
userId,
|
|
1217
|
+
result,
|
|
1218
|
+
});
|
|
1219
|
+
},
|
|
1220
|
+
};
|
|
1221
|
+
|
|
1222
|
+
const memoryReflectTool = {
|
|
1223
|
+
label: "Memory Reflect",
|
|
1224
|
+
name: "memory_reflect",
|
|
1225
|
+
description: "Run Memoria reflection or return reflection candidates.",
|
|
1226
|
+
parameters: objectSchema({
|
|
1227
|
+
mode: modeProperty,
|
|
1228
|
+
force: forceProperty,
|
|
1229
|
+
userId: userIdProperty,
|
|
1230
|
+
}),
|
|
1231
|
+
execute: async (_toolCallId: string, rawParams: unknown) => {
|
|
1232
|
+
const params = asRecord(rawParams) ?? {};
|
|
1233
|
+
const mode = readString(params, "mode") ?? "auto";
|
|
1234
|
+
if (!["auto", "internal", "candidates"].includes(mode)) {
|
|
1235
|
+
throw new Error("mode must be one of auto, internal, candidates");
|
|
1236
|
+
}
|
|
1237
|
+
const userId = resolveUserId(config, ctx, readString(params, "userId"));
|
|
1238
|
+
const force = readBoolean(params, "force") ?? false;
|
|
1239
|
+
const result = await client.reflect({ userId, force, mode });
|
|
1240
|
+
const payload = asRecord(result) ?? {};
|
|
1241
|
+
|
|
1242
|
+
return jsonResult({
|
|
1243
|
+
mode,
|
|
1244
|
+
userId,
|
|
1245
|
+
...payload,
|
|
1246
|
+
});
|
|
1247
|
+
},
|
|
1248
|
+
};
|
|
1249
|
+
|
|
1250
|
+
const memoryExtractEntitiesTool = {
|
|
1251
|
+
label: "Memory Extract Entities",
|
|
1252
|
+
name: "memory_extract_entities",
|
|
1253
|
+
description: "Run Memoria entity extraction or return extraction candidates.",
|
|
1254
|
+
parameters: objectSchema({
|
|
1255
|
+
mode: modeProperty,
|
|
1256
|
+
force: forceProperty,
|
|
1257
|
+
userId: userIdProperty,
|
|
1258
|
+
}),
|
|
1259
|
+
execute: async (_toolCallId: string, rawParams: unknown) => {
|
|
1260
|
+
const params = asRecord(rawParams) ?? {};
|
|
1261
|
+
const mode = readString(params, "mode") ?? "auto";
|
|
1262
|
+
if (!["auto", "internal", "candidates"].includes(mode)) {
|
|
1263
|
+
throw new Error("mode must be one of auto, internal, candidates");
|
|
1264
|
+
}
|
|
1265
|
+
const userId = resolveUserId(config, ctx, readString(params, "userId"));
|
|
1266
|
+
const force = readBoolean(params, "force") ?? false;
|
|
1267
|
+
const result = await client.extractEntities({ userId, force, mode });
|
|
1268
|
+
const payload = asRecord(result) ?? {};
|
|
1269
|
+
|
|
1270
|
+
return jsonResult({
|
|
1271
|
+
mode,
|
|
1272
|
+
userId,
|
|
1273
|
+
...payload,
|
|
1274
|
+
});
|
|
1275
|
+
},
|
|
1276
|
+
};
|
|
1277
|
+
|
|
1278
|
+
const memoryLinkEntitiesTool = {
|
|
1279
|
+
label: "Memory Link Entities",
|
|
1280
|
+
name: "memory_link_entities",
|
|
1281
|
+
description: "Write entity links from candidate extraction results.",
|
|
1282
|
+
parameters: objectSchema(
|
|
1283
|
+
{
|
|
1284
|
+
entities: {
|
|
1285
|
+
description: "Array or JSON string of [{ memory_id, entities: [{ name, type }] }]",
|
|
1286
|
+
},
|
|
1287
|
+
userId: userIdProperty,
|
|
1288
|
+
},
|
|
1289
|
+
["entities"],
|
|
1290
|
+
),
|
|
1291
|
+
execute: async (_toolCallId: string, rawParams: unknown) => {
|
|
1292
|
+
const params = asRecord(rawParams) ?? {};
|
|
1293
|
+
const userId = resolveUserId(config, ctx, readString(params, "userId"));
|
|
1294
|
+
const entities = readEntityPayload(params, "entities");
|
|
1295
|
+
const result = await client.linkEntities({ userId, entities });
|
|
1296
|
+
return jsonResult({
|
|
1297
|
+
userId,
|
|
1298
|
+
result,
|
|
1299
|
+
});
|
|
1300
|
+
},
|
|
1301
|
+
};
|
|
1302
|
+
|
|
1303
|
+
const memoryRebuildIndexTool = {
|
|
1304
|
+
label: "Memory Rebuild Index",
|
|
1305
|
+
name: "memory_rebuild_index",
|
|
1306
|
+
description: "Rebuild a Memoria IVF vector index.",
|
|
1307
|
+
parameters: objectSchema({
|
|
1308
|
+
table: {
|
|
1309
|
+
type: "string",
|
|
1310
|
+
description: "Target table",
|
|
1311
|
+
enum: ["mem_memories", "memory_graph_nodes"],
|
|
1312
|
+
},
|
|
1313
|
+
}),
|
|
1314
|
+
execute: async (_toolCallId: string, rawParams: unknown) => {
|
|
1315
|
+
const params = asRecord(rawParams) ?? {};
|
|
1316
|
+
const table = readString(params, "table") ?? "mem_memories";
|
|
1317
|
+
if (!["mem_memories", "memory_graph_nodes"].includes(table)) {
|
|
1318
|
+
throw new Error("table must be one of mem_memories, memory_graph_nodes");
|
|
1319
|
+
}
|
|
1320
|
+
const result = await client.rebuildIndex(table);
|
|
1321
|
+
return jsonResult({
|
|
1322
|
+
table,
|
|
1323
|
+
...((asRecord(result) ?? {}) as Record<string, unknown>),
|
|
1324
|
+
});
|
|
1325
|
+
},
|
|
1326
|
+
};
|
|
1327
|
+
|
|
1328
|
+
const memoryCapabilitiesTool = {
|
|
1329
|
+
label: "Memory Capabilities",
|
|
1330
|
+
name: "memory_capabilities",
|
|
1331
|
+
description: "List tool coverage and backend-specific limitations for this plugin.",
|
|
1332
|
+
parameters: EMPTY_OBJECT_SCHEMA,
|
|
1333
|
+
execute: async () => {
|
|
1334
|
+
return jsonResult(buildCapabilitiesPayload(config));
|
|
1335
|
+
},
|
|
1336
|
+
};
|
|
1337
|
+
|
|
1338
|
+
const memorySnapshotTool = {
|
|
1339
|
+
label: "Memory Snapshot",
|
|
1340
|
+
name: "memory_snapshot",
|
|
1341
|
+
description: "Create a named snapshot of current memory state.",
|
|
1342
|
+
parameters: objectSchema(
|
|
1343
|
+
{
|
|
1344
|
+
name: { type: "string", description: "Snapshot name" },
|
|
1345
|
+
description: { type: "string", description: "Optional snapshot description" },
|
|
1346
|
+
userId: userIdProperty,
|
|
1347
|
+
},
|
|
1348
|
+
["name"],
|
|
1349
|
+
),
|
|
1350
|
+
execute: async (_toolCallId: string, rawParams: unknown) => {
|
|
1351
|
+
const params = asRecord(rawParams) ?? {};
|
|
1352
|
+
const userId = resolveUserId(config, ctx, readString(params, "userId"));
|
|
1353
|
+
const name = readString(params, "name", { required: true, label: "name" })!;
|
|
1354
|
+
const snapshot = await client.createSnapshot({
|
|
1355
|
+
userId,
|
|
1356
|
+
name,
|
|
1357
|
+
description: readString(params, "description") ?? "",
|
|
1358
|
+
});
|
|
1359
|
+
return jsonResult({
|
|
1360
|
+
userId,
|
|
1361
|
+
snapshot,
|
|
1362
|
+
});
|
|
1363
|
+
},
|
|
1364
|
+
};
|
|
1365
|
+
|
|
1366
|
+
const memorySnapshotsTool = {
|
|
1367
|
+
label: "Memory Snapshots",
|
|
1368
|
+
name: "memory_snapshots",
|
|
1369
|
+
description: "List all known memory snapshots.",
|
|
1370
|
+
parameters: objectSchema({
|
|
1371
|
+
userId: userIdProperty,
|
|
1372
|
+
}),
|
|
1373
|
+
execute: async (_toolCallId: string, rawParams: unknown) => {
|
|
1374
|
+
const params = asRecord(rawParams) ?? {};
|
|
1375
|
+
const userId = resolveUserId(config, ctx, readString(params, "userId"));
|
|
1376
|
+
const snapshots = await client.listSnapshots(userId);
|
|
1377
|
+
return jsonResult({
|
|
1378
|
+
userId,
|
|
1379
|
+
snapshots,
|
|
1380
|
+
});
|
|
1381
|
+
},
|
|
1382
|
+
};
|
|
1383
|
+
|
|
1384
|
+
const memoryRollbackTool = {
|
|
1385
|
+
label: "Memory Rollback",
|
|
1386
|
+
name: "memory_rollback",
|
|
1387
|
+
description: "Rollback memory state to a named snapshot.",
|
|
1388
|
+
parameters: objectSchema(
|
|
1389
|
+
{
|
|
1390
|
+
name: { type: "string", description: "Snapshot name" },
|
|
1391
|
+
userId: userIdProperty,
|
|
1392
|
+
},
|
|
1393
|
+
["name"],
|
|
1394
|
+
),
|
|
1395
|
+
execute: async (_toolCallId: string, rawParams: unknown) => {
|
|
1396
|
+
const params = asRecord(rawParams) ?? {};
|
|
1397
|
+
const userId = resolveUserId(config, ctx, readString(params, "userId"));
|
|
1398
|
+
const name = readString(params, "name", { required: true, label: "name" })!;
|
|
1399
|
+
const result = await client.rollbackSnapshot({ userId, name });
|
|
1400
|
+
return jsonResult({
|
|
1401
|
+
userId,
|
|
1402
|
+
result,
|
|
1403
|
+
});
|
|
1404
|
+
},
|
|
1405
|
+
};
|
|
1406
|
+
|
|
1407
|
+
const memoryBranchTool = {
|
|
1408
|
+
label: "Memory Branch",
|
|
1409
|
+
name: "memory_branch",
|
|
1410
|
+
description: "Create a new memory branch for isolated experimentation.",
|
|
1411
|
+
parameters: objectSchema(
|
|
1412
|
+
{
|
|
1413
|
+
name: { type: "string", description: "Branch name" },
|
|
1414
|
+
fromSnapshot: { type: "string", description: "Optional source snapshot name" },
|
|
1415
|
+
fromTimestamp: {
|
|
1416
|
+
type: "string",
|
|
1417
|
+
description: "Optional source timestamp in YYYY-MM-DD HH:MM:SS",
|
|
1418
|
+
},
|
|
1419
|
+
userId: userIdProperty,
|
|
1420
|
+
},
|
|
1421
|
+
["name"],
|
|
1422
|
+
),
|
|
1423
|
+
execute: async (_toolCallId: string, rawParams: unknown) => {
|
|
1424
|
+
const params = asRecord(rawParams) ?? {};
|
|
1425
|
+
const userId = resolveUserId(config, ctx, readString(params, "userId"));
|
|
1426
|
+
const name = readString(params, "name", { required: true, label: "name" })!;
|
|
1427
|
+
const fromSnapshot = readString(params, "fromSnapshot");
|
|
1428
|
+
const fromTimestamp = readString(params, "fromTimestamp");
|
|
1429
|
+
if (fromSnapshot && fromTimestamp) {
|
|
1430
|
+
throw new Error("fromSnapshot and fromTimestamp are mutually exclusive");
|
|
1431
|
+
}
|
|
1432
|
+
const result = await client.branchCreate({
|
|
1433
|
+
userId,
|
|
1434
|
+
name,
|
|
1435
|
+
fromSnapshot,
|
|
1436
|
+
fromTimestamp,
|
|
1437
|
+
});
|
|
1438
|
+
return jsonResult({
|
|
1439
|
+
userId,
|
|
1440
|
+
result,
|
|
1441
|
+
});
|
|
1442
|
+
},
|
|
1443
|
+
};
|
|
1444
|
+
|
|
1445
|
+
const memoryBranchesTool = {
|
|
1446
|
+
label: "Memory Branches",
|
|
1447
|
+
name: "memory_branches",
|
|
1448
|
+
description: "List all memory branches for the current user.",
|
|
1449
|
+
parameters: objectSchema({
|
|
1450
|
+
userId: userIdProperty,
|
|
1451
|
+
}),
|
|
1452
|
+
execute: async (_toolCallId: string, rawParams: unknown) => {
|
|
1453
|
+
const params = asRecord(rawParams) ?? {};
|
|
1454
|
+
const userId = resolveUserId(config, ctx, readString(params, "userId"));
|
|
1455
|
+
const branches = await client.branchList(userId);
|
|
1456
|
+
return jsonResult({
|
|
1457
|
+
userId,
|
|
1458
|
+
branches,
|
|
1459
|
+
});
|
|
1460
|
+
},
|
|
1461
|
+
};
|
|
1462
|
+
|
|
1463
|
+
const memoryCheckoutTool = {
|
|
1464
|
+
label: "Memory Checkout",
|
|
1465
|
+
name: "memory_checkout",
|
|
1466
|
+
description: "Switch the active memory branch.",
|
|
1467
|
+
parameters: objectSchema(
|
|
1468
|
+
{
|
|
1469
|
+
name: { type: "string", description: "Branch name or main" },
|
|
1470
|
+
userId: userIdProperty,
|
|
1471
|
+
},
|
|
1472
|
+
["name"],
|
|
1473
|
+
),
|
|
1474
|
+
execute: async (_toolCallId: string, rawParams: unknown) => {
|
|
1475
|
+
const params = asRecord(rawParams) ?? {};
|
|
1476
|
+
const userId = resolveUserId(config, ctx, readString(params, "userId"));
|
|
1477
|
+
const name = readString(params, "name", { required: true, label: "name" })!;
|
|
1478
|
+
const result = await client.branchCheckout({ userId, name });
|
|
1479
|
+
return jsonResult({
|
|
1480
|
+
userId,
|
|
1481
|
+
result,
|
|
1482
|
+
});
|
|
1483
|
+
},
|
|
1484
|
+
};
|
|
1485
|
+
|
|
1486
|
+
const memoryBranchDeleteTool = {
|
|
1487
|
+
label: "Memory Branch Delete",
|
|
1488
|
+
name: "memory_branch_delete",
|
|
1489
|
+
description: "Delete a memory branch.",
|
|
1490
|
+
parameters: objectSchema(
|
|
1491
|
+
{
|
|
1492
|
+
name: { type: "string", description: "Branch name" },
|
|
1493
|
+
userId: userIdProperty,
|
|
1494
|
+
},
|
|
1495
|
+
["name"],
|
|
1496
|
+
),
|
|
1497
|
+
execute: async (_toolCallId: string, rawParams: unknown) => {
|
|
1498
|
+
const params = asRecord(rawParams) ?? {};
|
|
1499
|
+
const userId = resolveUserId(config, ctx, readString(params, "userId"));
|
|
1500
|
+
const name = readString(params, "name", { required: true, label: "name" })!;
|
|
1501
|
+
const result = await client.branchDelete({ userId, name });
|
|
1502
|
+
return jsonResult({
|
|
1503
|
+
userId,
|
|
1504
|
+
result,
|
|
1505
|
+
});
|
|
1506
|
+
},
|
|
1507
|
+
};
|
|
1508
|
+
|
|
1509
|
+
const memoryMergeTool = {
|
|
1510
|
+
label: "Memory Merge",
|
|
1511
|
+
name: "memory_merge",
|
|
1512
|
+
description: "Merge a branch back into main.",
|
|
1513
|
+
parameters: objectSchema(
|
|
1514
|
+
{
|
|
1515
|
+
source: { type: "string", description: "Branch name to merge from" },
|
|
1516
|
+
strategy: {
|
|
1517
|
+
type: "string",
|
|
1518
|
+
description: "append skips conflicting duplicates; replace overwrites them",
|
|
1519
|
+
enum: ["append", "replace"],
|
|
1520
|
+
},
|
|
1521
|
+
userId: userIdProperty,
|
|
1522
|
+
},
|
|
1523
|
+
["source"],
|
|
1524
|
+
),
|
|
1525
|
+
execute: async (_toolCallId: string, rawParams: unknown) => {
|
|
1526
|
+
const params = asRecord(rawParams) ?? {};
|
|
1527
|
+
const userId = resolveUserId(config, ctx, readString(params, "userId"));
|
|
1528
|
+
const source = readString(params, "source", { required: true, label: "source" })!;
|
|
1529
|
+
const strategy = readString(params, "strategy") ?? "append";
|
|
1530
|
+
if (!["append", "replace"].includes(strategy)) {
|
|
1531
|
+
throw new Error("strategy must be one of append, replace");
|
|
1532
|
+
}
|
|
1533
|
+
const result = await client.branchMerge({ userId, source, strategy });
|
|
1534
|
+
return jsonResult({
|
|
1535
|
+
userId,
|
|
1536
|
+
result,
|
|
1537
|
+
});
|
|
1538
|
+
},
|
|
1539
|
+
};
|
|
1540
|
+
|
|
1541
|
+
const memoryDiffTool = {
|
|
1542
|
+
label: "Memory Diff",
|
|
1543
|
+
name: "memory_diff",
|
|
1544
|
+
description: "Show what would change if a branch were merged into main.",
|
|
1545
|
+
parameters: objectSchema(
|
|
1546
|
+
{
|
|
1547
|
+
source: { type: "string", description: "Branch name to diff" },
|
|
1548
|
+
limit: {
|
|
1549
|
+
type: "integer",
|
|
1550
|
+
description: "Maximum number of changes to return",
|
|
1551
|
+
minimum: 1,
|
|
1552
|
+
maximum: 500,
|
|
1553
|
+
},
|
|
1554
|
+
userId: userIdProperty,
|
|
1555
|
+
},
|
|
1556
|
+
["source"],
|
|
1557
|
+
),
|
|
1558
|
+
execute: async (_toolCallId: string, rawParams: unknown) => {
|
|
1559
|
+
const params = asRecord(rawParams) ?? {};
|
|
1560
|
+
const userId = resolveUserId(config, ctx, readString(params, "userId"));
|
|
1561
|
+
const source = readString(params, "source", { required: true, label: "source" })!;
|
|
1562
|
+
const limit = clampInt(readNumber(params, "limit"), 1, 500, 50);
|
|
1563
|
+
const result = await client.branchDiff({ userId, source, limit });
|
|
1564
|
+
return jsonResult({
|
|
1565
|
+
userId,
|
|
1566
|
+
result,
|
|
1567
|
+
});
|
|
1568
|
+
},
|
|
1569
|
+
};
|
|
1570
|
+
|
|
1571
|
+
return [
|
|
1572
|
+
memorySearchTool,
|
|
1573
|
+
memoryGetTool,
|
|
1574
|
+
memoryHealthTool,
|
|
1575
|
+
memoryStoreTool,
|
|
1576
|
+
memoryRetrieveTool,
|
|
1577
|
+
memoryRecallTool,
|
|
1578
|
+
memoryListTool,
|
|
1579
|
+
memoryStatsTool,
|
|
1580
|
+
memoryProfileTool,
|
|
1581
|
+
memoryCorrectTool,
|
|
1582
|
+
memoryPurgeTool,
|
|
1583
|
+
memoryForgetTool,
|
|
1584
|
+
memoryObserveTool,
|
|
1585
|
+
memoryGovernanceTool,
|
|
1586
|
+
memoryConsolidateTool,
|
|
1587
|
+
memoryReflectTool,
|
|
1588
|
+
memoryExtractEntitiesTool,
|
|
1589
|
+
memoryLinkEntitiesTool,
|
|
1590
|
+
memoryRebuildIndexTool,
|
|
1591
|
+
memoryCapabilitiesTool,
|
|
1592
|
+
memorySnapshotTool,
|
|
1593
|
+
memorySnapshotsTool,
|
|
1594
|
+
memoryRollbackTool,
|
|
1595
|
+
memoryBranchTool,
|
|
1596
|
+
memoryBranchesTool,
|
|
1597
|
+
memoryCheckoutTool,
|
|
1598
|
+
memoryBranchDeleteTool,
|
|
1599
|
+
memoryMergeTool,
|
|
1600
|
+
memoryDiffTool,
|
|
1601
|
+
];
|
|
1602
|
+
},
|
|
1603
|
+
{ names: supportedToolNames() },
|
|
1604
|
+
);
|
|
1605
|
+
|
|
1606
|
+
api.registerCli(
|
|
1607
|
+
({ program }) => {
|
|
1608
|
+
const memoria = program.command("memoria").description("Memoria plugin commands");
|
|
1609
|
+
const ltm = program
|
|
1610
|
+
.command("ltm")
|
|
1611
|
+
.description("Compatibility commands for memory-lancedb-pro style workflows");
|
|
1612
|
+
|
|
1613
|
+
const printJson = (value: unknown) => {
|
|
1614
|
+
console.log(JSON.stringify(value, null, 2));
|
|
1615
|
+
};
|
|
1616
|
+
|
|
1617
|
+
const resolveCliUserId = (raw: unknown, fallback = config.defaultUserId) => {
|
|
1618
|
+
return typeof raw === "string" && raw.trim() ? raw.trim() : fallback;
|
|
1619
|
+
};
|
|
1620
|
+
|
|
1621
|
+
const withCliClient = <Args extends unknown[]>(
|
|
1622
|
+
handler: (...args: Args) => Promise<void> | void,
|
|
1623
|
+
) => {
|
|
1624
|
+
return async (...args: Args) => {
|
|
1625
|
+
try {
|
|
1626
|
+
await handler(...args);
|
|
1627
|
+
} finally {
|
|
1628
|
+
client.close();
|
|
1629
|
+
}
|
|
1630
|
+
};
|
|
1631
|
+
};
|
|
1632
|
+
|
|
1633
|
+
const runMemoriaInstaller = (opts: {
|
|
1634
|
+
memoriaBin?: string;
|
|
1635
|
+
memoriaVersion?: string;
|
|
1636
|
+
memoriaInstallDir?: string;
|
|
1637
|
+
skipMemoriaInstall?: boolean;
|
|
1638
|
+
binaryOnly?: boolean;
|
|
1639
|
+
verify?: boolean;
|
|
1640
|
+
}) => {
|
|
1641
|
+
const args = [
|
|
1642
|
+
INSTALLER_SCRIPT,
|
|
1643
|
+
"--source-dir",
|
|
1644
|
+
PLUGIN_ROOT,
|
|
1645
|
+
"--openclaw-bin",
|
|
1646
|
+
resolveOpenClawBinFromProcess(),
|
|
1647
|
+
"--skip-plugin-install",
|
|
1648
|
+
];
|
|
1649
|
+
if (opts.memoriaBin) {
|
|
1650
|
+
args.push("--memoria-bin", opts.memoriaBin);
|
|
1651
|
+
}
|
|
1652
|
+
if (opts.memoriaVersion) {
|
|
1653
|
+
args.push("--memoria-version", opts.memoriaVersion);
|
|
1654
|
+
}
|
|
1655
|
+
if (opts.memoriaInstallDir) {
|
|
1656
|
+
args.push("--memoria-install-dir", opts.memoriaInstallDir);
|
|
1657
|
+
}
|
|
1658
|
+
if (opts.skipMemoriaInstall) {
|
|
1659
|
+
args.push("--skip-memoria-install");
|
|
1660
|
+
}
|
|
1661
|
+
if (opts.binaryOnly) {
|
|
1662
|
+
args.push("--binary-only");
|
|
1663
|
+
}
|
|
1664
|
+
if (opts.verify !== false) {
|
|
1665
|
+
args.push("--verify");
|
|
1666
|
+
}
|
|
1667
|
+
runLocalCommand("bash", args);
|
|
1668
|
+
};
|
|
1669
|
+
|
|
1670
|
+
const runMemoriaVerifier = (opts: { memoriaBin?: string }) => {
|
|
1671
|
+
const args = [
|
|
1672
|
+
VERIFY_SCRIPT,
|
|
1673
|
+
"--openclaw-bin",
|
|
1674
|
+
resolveOpenClawBinFromProcess(),
|
|
1675
|
+
"--config-file",
|
|
1676
|
+
resolveOpenClawConfigFile(),
|
|
1677
|
+
];
|
|
1678
|
+
if (opts.memoriaBin) {
|
|
1679
|
+
args.push("--memoria-bin", opts.memoriaBin);
|
|
1680
|
+
}
|
|
1681
|
+
runLocalCommand("node", args);
|
|
1682
|
+
};
|
|
1683
|
+
|
|
1684
|
+
const runMemoriaConnector = (opts: {
|
|
1685
|
+
configFile: string;
|
|
1686
|
+
mode: "cloud" | "local";
|
|
1687
|
+
apiUrl?: string;
|
|
1688
|
+
apiKey?: string;
|
|
1689
|
+
dbUrl?: string;
|
|
1690
|
+
memoriaBin?: string;
|
|
1691
|
+
userId?: string;
|
|
1692
|
+
embeddingProvider?: string;
|
|
1693
|
+
embeddingModel?: string;
|
|
1694
|
+
embeddingApiKey?: string;
|
|
1695
|
+
embeddingBaseUrl?: string;
|
|
1696
|
+
embeddingDim?: number;
|
|
1697
|
+
}) => {
|
|
1698
|
+
const args = [
|
|
1699
|
+
CONNECT_SCRIPT,
|
|
1700
|
+
"--config-file",
|
|
1701
|
+
opts.configFile,
|
|
1702
|
+
"--mode",
|
|
1703
|
+
opts.mode,
|
|
1704
|
+
];
|
|
1705
|
+
|
|
1706
|
+
if (opts.apiUrl) {
|
|
1707
|
+
args.push("--api-url", opts.apiUrl);
|
|
1708
|
+
}
|
|
1709
|
+
if (opts.apiKey) {
|
|
1710
|
+
args.push("--api-key", opts.apiKey);
|
|
1711
|
+
}
|
|
1712
|
+
if (opts.dbUrl) {
|
|
1713
|
+
args.push("--db-url", opts.dbUrl);
|
|
1714
|
+
}
|
|
1715
|
+
if (opts.memoriaBin) {
|
|
1716
|
+
args.push("--memoria-executable", opts.memoriaBin);
|
|
1717
|
+
}
|
|
1718
|
+
if (opts.userId) {
|
|
1719
|
+
args.push("--default-user-id", opts.userId);
|
|
1720
|
+
}
|
|
1721
|
+
if (opts.embeddingProvider) {
|
|
1722
|
+
args.push("--embedding-provider", opts.embeddingProvider);
|
|
1723
|
+
}
|
|
1724
|
+
if (opts.embeddingModel) {
|
|
1725
|
+
args.push("--embedding-model", opts.embeddingModel);
|
|
1726
|
+
}
|
|
1727
|
+
if (opts.embeddingApiKey) {
|
|
1728
|
+
args.push("--embedding-api-key", opts.embeddingApiKey);
|
|
1729
|
+
}
|
|
1730
|
+
if (opts.embeddingBaseUrl) {
|
|
1731
|
+
args.push("--embedding-base-url", opts.embeddingBaseUrl);
|
|
1732
|
+
}
|
|
1733
|
+
if (typeof opts.embeddingDim === "number" && Number.isFinite(opts.embeddingDim)) {
|
|
1734
|
+
args.push("--embedding-dim", String(Math.trunc(opts.embeddingDim)));
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
runLocalCommand("node", args);
|
|
1738
|
+
};
|
|
1739
|
+
|
|
1740
|
+
type RawConnectCliOptions = {
|
|
1741
|
+
mode?: unknown;
|
|
1742
|
+
apiUrl?: unknown;
|
|
1743
|
+
apiKey?: unknown;
|
|
1744
|
+
dbUrl?: unknown;
|
|
1745
|
+
memoriaBin?: unknown;
|
|
1746
|
+
memoriaVersion?: unknown;
|
|
1747
|
+
memoriaInstallDir?: unknown;
|
|
1748
|
+
installMemoria?: unknown;
|
|
1749
|
+
userId?: unknown;
|
|
1750
|
+
embeddingProvider?: unknown;
|
|
1751
|
+
embeddingModel?: unknown;
|
|
1752
|
+
embeddingApiKey?: unknown;
|
|
1753
|
+
embeddingBaseUrl?: unknown;
|
|
1754
|
+
embeddingDim?: unknown;
|
|
1755
|
+
skipValidate?: unknown;
|
|
1756
|
+
skipHealthCheck?: unknown;
|
|
1757
|
+
};
|
|
1758
|
+
|
|
1759
|
+
type NormalizedConnectOptions = {
|
|
1760
|
+
mode: "cloud" | "local";
|
|
1761
|
+
apiUrl?: string;
|
|
1762
|
+
apiKey?: string;
|
|
1763
|
+
dbUrl?: string;
|
|
1764
|
+
memoriaBin?: string;
|
|
1765
|
+
memoriaVersion?: string;
|
|
1766
|
+
memoriaInstallDir?: string;
|
|
1767
|
+
installMemoria: boolean;
|
|
1768
|
+
userId?: string;
|
|
1769
|
+
embeddingProvider?: string;
|
|
1770
|
+
embeddingModel?: string;
|
|
1771
|
+
embeddingApiKey?: string;
|
|
1772
|
+
embeddingBaseUrl?: string;
|
|
1773
|
+
embeddingDim?: number;
|
|
1774
|
+
validateConfig: boolean;
|
|
1775
|
+
healthCheck: boolean;
|
|
1776
|
+
};
|
|
1777
|
+
|
|
1778
|
+
const readOptionalCliString = (
|
|
1779
|
+
raw: unknown,
|
|
1780
|
+
opts: { trimTrailingSlashes?: boolean } = {},
|
|
1781
|
+
): string | undefined => {
|
|
1782
|
+
if (typeof raw !== "string") {
|
|
1783
|
+
return undefined;
|
|
1784
|
+
}
|
|
1785
|
+
const normalized = raw.trim();
|
|
1786
|
+
if (!normalized) {
|
|
1787
|
+
return undefined;
|
|
1788
|
+
}
|
|
1789
|
+
return opts.trimTrailingSlashes ? normalized.replace(/\/+$/, "") : normalized;
|
|
1790
|
+
};
|
|
1791
|
+
|
|
1792
|
+
const readOptionalEnvString = (
|
|
1793
|
+
envName: string,
|
|
1794
|
+
opts: { trimTrailingSlashes?: boolean } = {},
|
|
1795
|
+
): string | undefined => {
|
|
1796
|
+
const value = readOptionalCliString(process.env[envName], opts);
|
|
1797
|
+
return value;
|
|
1798
|
+
};
|
|
1799
|
+
|
|
1800
|
+
const normalizeConnectOptions = (
|
|
1801
|
+
raw: RawConnectCliOptions,
|
|
1802
|
+
defaultMode: "cloud" | "local" = "cloud",
|
|
1803
|
+
): NormalizedConnectOptions => {
|
|
1804
|
+
const modeRaw =
|
|
1805
|
+
typeof raw.mode === "string" && raw.mode.trim()
|
|
1806
|
+
? raw.mode.trim().toLowerCase()
|
|
1807
|
+
: defaultMode;
|
|
1808
|
+
if (modeRaw !== "cloud" && modeRaw !== "local") {
|
|
1809
|
+
throw new Error("mode must be one of: cloud, local");
|
|
1810
|
+
}
|
|
1811
|
+
const mode = modeRaw as "cloud" | "local";
|
|
1812
|
+
|
|
1813
|
+
const apiUrl =
|
|
1814
|
+
readOptionalCliString(raw.apiUrl, { trimTrailingSlashes: true }) ??
|
|
1815
|
+
readOptionalEnvString("MEMORIA_API_URL", { trimTrailingSlashes: true });
|
|
1816
|
+
const apiKey =
|
|
1817
|
+
readOptionalCliString(raw.apiKey) ?? readOptionalEnvString("MEMORIA_API_KEY");
|
|
1818
|
+
const dbUrl =
|
|
1819
|
+
readOptionalCliString(raw.dbUrl) ??
|
|
1820
|
+
readOptionalEnvString("MEMORIA_DB_URL") ??
|
|
1821
|
+
config.dbUrl;
|
|
1822
|
+
const memoriaBin =
|
|
1823
|
+
readOptionalCliString(raw.memoriaBin) ?? readOptionalEnvString("MEMORIA_EXECUTABLE");
|
|
1824
|
+
const memoriaVersion =
|
|
1825
|
+
readOptionalCliString(raw.memoriaVersion) ?? readOptionalEnvString("MEMORIA_RELEASE_TAG");
|
|
1826
|
+
const memoriaInstallDir =
|
|
1827
|
+
readOptionalCliString(raw.memoriaInstallDir) ??
|
|
1828
|
+
readOptionalEnvString("MEMORIA_BINARY_INSTALL_DIR");
|
|
1829
|
+
const installMemoria = raw.installMemoria === true;
|
|
1830
|
+
const userId =
|
|
1831
|
+
readOptionalCliString(raw.userId) ?? readOptionalEnvString("MEMORIA_DEFAULT_USER_ID");
|
|
1832
|
+
const embeddingProvider =
|
|
1833
|
+
readOptionalCliString(raw.embeddingProvider) ??
|
|
1834
|
+
readOptionalEnvString("MEMORIA_EMBEDDING_PROVIDER") ??
|
|
1835
|
+
config.embeddingProvider;
|
|
1836
|
+
const embeddingModel =
|
|
1837
|
+
readOptionalCliString(raw.embeddingModel) ??
|
|
1838
|
+
readOptionalEnvString("MEMORIA_EMBEDDING_MODEL") ??
|
|
1839
|
+
config.embeddingModel;
|
|
1840
|
+
const embeddingApiKey =
|
|
1841
|
+
readOptionalCliString(raw.embeddingApiKey) ??
|
|
1842
|
+
readOptionalEnvString("MEMORIA_EMBEDDING_API_KEY") ??
|
|
1843
|
+
config.embeddingApiKey;
|
|
1844
|
+
const embeddingBaseUrl =
|
|
1845
|
+
readOptionalCliString(raw.embeddingBaseUrl, { trimTrailingSlashes: true }) ??
|
|
1846
|
+
readOptionalEnvString("MEMORIA_EMBEDDING_BASE_URL", { trimTrailingSlashes: true }) ??
|
|
1847
|
+
config.embeddingBaseUrl;
|
|
1848
|
+
|
|
1849
|
+
const embeddingDimRaw = String(raw.embeddingDim ?? "").trim();
|
|
1850
|
+
const parsedEmbeddingDim = Number.parseInt(embeddingDimRaw, 10);
|
|
1851
|
+
if (embeddingDimRaw && !Number.isFinite(parsedEmbeddingDim)) {
|
|
1852
|
+
throw new Error("--embedding-dim must be a valid positive integer");
|
|
1853
|
+
}
|
|
1854
|
+
const embeddingDim = Number.isFinite(parsedEmbeddingDim)
|
|
1855
|
+
? parsedEmbeddingDim
|
|
1856
|
+
: config.embeddingDim;
|
|
1857
|
+
|
|
1858
|
+
if (mode === "cloud") {
|
|
1859
|
+
if (!apiUrl || !apiKey) {
|
|
1860
|
+
throw new Error(
|
|
1861
|
+
"cloud mode requires api-url and api-key. Example: openclaw memoria setup --mode cloud --api-url <MEMORIA_API_URL> --api-key <MEMORIA_API_KEY>",
|
|
1862
|
+
);
|
|
1863
|
+
}
|
|
1864
|
+
} else if (!dbUrl) {
|
|
1865
|
+
throw new Error(
|
|
1866
|
+
"local mode requires db-url. Example: openclaw memoria setup --mode local --db-url <MATRIXONE_DSN>",
|
|
1867
|
+
);
|
|
1868
|
+
} else if (embeddingProvider !== "local" && !embeddingApiKey) {
|
|
1869
|
+
throw new Error(
|
|
1870
|
+
"local mode requires embedding API key when embedding-provider is not 'local'. Quick start: openclaw memoria setup --mode local --install-memoria --embedding-api-key <EMBEDDING_API_KEY>",
|
|
1871
|
+
);
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
return {
|
|
1875
|
+
mode,
|
|
1876
|
+
apiUrl,
|
|
1877
|
+
apiKey,
|
|
1878
|
+
dbUrl,
|
|
1879
|
+
memoriaBin,
|
|
1880
|
+
memoriaVersion,
|
|
1881
|
+
memoriaInstallDir,
|
|
1882
|
+
installMemoria,
|
|
1883
|
+
userId,
|
|
1884
|
+
embeddingProvider,
|
|
1885
|
+
embeddingModel,
|
|
1886
|
+
embeddingApiKey,
|
|
1887
|
+
embeddingBaseUrl,
|
|
1888
|
+
embeddingDim,
|
|
1889
|
+
validateConfig: raw.skipValidate !== true,
|
|
1890
|
+
healthCheck: raw.skipHealthCheck !== true,
|
|
1891
|
+
};
|
|
1892
|
+
};
|
|
1893
|
+
|
|
1894
|
+
const applyConnectOptions = (normalized: NormalizedConnectOptions) => {
|
|
1895
|
+
const resolvedConfigFile = resolveOpenClawConfigFile();
|
|
1896
|
+
let memoriaBinForConfig = normalized.memoriaBin;
|
|
1897
|
+
const installDirFallback =
|
|
1898
|
+
normalized.memoriaInstallDir ??
|
|
1899
|
+
(memoriaBinForConfig && memoriaBinForConfig.includes("/")
|
|
1900
|
+
? path.dirname(memoriaBinForConfig)
|
|
1901
|
+
: path.join(process.env.HOME ?? "", ".local", "bin"));
|
|
1902
|
+
let effectiveMemoriaExecutable = memoriaBinForConfig ?? config.memoriaExecutable;
|
|
1903
|
+
|
|
1904
|
+
if (normalized.installMemoria && !isExecutableAvailable(effectiveMemoriaExecutable)) {
|
|
1905
|
+
runMemoriaInstaller({
|
|
1906
|
+
memoriaVersion: normalized.memoriaVersion,
|
|
1907
|
+
memoriaInstallDir: installDirFallback,
|
|
1908
|
+
binaryOnly: true,
|
|
1909
|
+
verify: false,
|
|
1910
|
+
});
|
|
1911
|
+
const installedPath =
|
|
1912
|
+
memoriaBinForConfig && memoriaBinForConfig.includes("/")
|
|
1913
|
+
? memoriaBinForConfig
|
|
1914
|
+
: path.join(installDirFallback, "memoria");
|
|
1915
|
+
if (isExecutableAvailable(installedPath)) {
|
|
1916
|
+
memoriaBinForConfig = installedPath;
|
|
1917
|
+
effectiveMemoriaExecutable = installedPath;
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
runMemoriaConnector({
|
|
1922
|
+
configFile: resolvedConfigFile,
|
|
1923
|
+
mode: normalized.mode,
|
|
1924
|
+
apiUrl: normalized.apiUrl,
|
|
1925
|
+
apiKey: normalized.apiKey,
|
|
1926
|
+
dbUrl: normalized.dbUrl,
|
|
1927
|
+
memoriaBin: memoriaBinForConfig,
|
|
1928
|
+
userId: normalized.userId,
|
|
1929
|
+
embeddingProvider: normalized.embeddingProvider,
|
|
1930
|
+
embeddingModel: normalized.embeddingModel,
|
|
1931
|
+
embeddingApiKey: normalized.embeddingApiKey,
|
|
1932
|
+
embeddingBaseUrl: normalized.embeddingBaseUrl,
|
|
1933
|
+
embeddingDim: normalized.embeddingDim,
|
|
1934
|
+
});
|
|
1935
|
+
|
|
1936
|
+
const openclawBin = resolveOpenClawBinFromProcess();
|
|
1937
|
+
const openclawEnv = { OPENCLAW_CONFIG_PATH: resolvedConfigFile };
|
|
1938
|
+
|
|
1939
|
+
if (normalized.validateConfig) {
|
|
1940
|
+
runLocalCommand(openclawBin, ["config", "validate"], { env: openclawEnv });
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
if (normalized.healthCheck) {
|
|
1944
|
+
assertMemoriaExecutableAvailable(effectiveMemoriaExecutable, normalized.mode);
|
|
1945
|
+
const healthArgs = ["memoria", "health"];
|
|
1946
|
+
if (normalized.userId) {
|
|
1947
|
+
healthArgs.push("--user-id", normalized.userId);
|
|
1948
|
+
}
|
|
1949
|
+
runLocalCommand(openclawBin, healthArgs, { env: openclawEnv });
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
printJson({
|
|
1953
|
+
ok: true,
|
|
1954
|
+
mode: normalized.mode,
|
|
1955
|
+
configFile: resolvedConfigFile,
|
|
1956
|
+
validated: normalized.validateConfig,
|
|
1957
|
+
healthChecked: normalized.healthCheck,
|
|
1958
|
+
next: normalized.healthCheck
|
|
1959
|
+
? "Connected and health-checked."
|
|
1960
|
+
: "Config updated. Run `openclaw memoria health` to verify.",
|
|
1961
|
+
});
|
|
1962
|
+
};
|
|
1963
|
+
|
|
1964
|
+
memoria
|
|
1965
|
+
.command("health")
|
|
1966
|
+
.description("Check Memoria connectivity")
|
|
1967
|
+
.option("--user-id <user>", "Explicit Memoria user_id", config.defaultUserId)
|
|
1968
|
+
.action(withCliClient(async (opts) => {
|
|
1969
|
+
const userId = resolveCliUserId(opts.userId);
|
|
1970
|
+
const result = await client.health(userId);
|
|
1971
|
+
printJson({
|
|
1972
|
+
userId,
|
|
1973
|
+
backend: config.backend,
|
|
1974
|
+
...(asRecord(result) ?? {}),
|
|
1975
|
+
});
|
|
1976
|
+
}));
|
|
1977
|
+
|
|
1978
|
+
memoria
|
|
1979
|
+
.command("search")
|
|
1980
|
+
.description("Search Memoria memories")
|
|
1981
|
+
.argument("<query>", "Search query")
|
|
1982
|
+
.option("--top-k <n>", "Maximum result count", String(config.retrieveTopK))
|
|
1983
|
+
.option("--user-id <user>", "Explicit Memoria user_id", config.defaultUserId)
|
|
1984
|
+
.action(withCliClient(async (query, opts) => {
|
|
1985
|
+
const topK = clampInt(Number.parseInt(String(opts.topK), 10), 1, 20, config.retrieveTopK);
|
|
1986
|
+
const userId = resolveCliUserId(opts.userId);
|
|
1987
|
+
const result = await client.retrieve({
|
|
1988
|
+
userId,
|
|
1989
|
+
query: String(query),
|
|
1990
|
+
topK,
|
|
1991
|
+
includeCrossSession: config.includeCrossSession,
|
|
1992
|
+
});
|
|
1993
|
+
printJson({
|
|
1994
|
+
backend: config.backend,
|
|
1995
|
+
userId,
|
|
1996
|
+
count: result.length,
|
|
1997
|
+
memories: result,
|
|
1998
|
+
});
|
|
1999
|
+
}));
|
|
2000
|
+
|
|
2001
|
+
memoria
|
|
2002
|
+
.command("list")
|
|
2003
|
+
.description("List recent Memoria memories")
|
|
2004
|
+
.option("--limit <n>", "Maximum result count", "20")
|
|
2005
|
+
.option("--type <memoryType>", "Optional memory type filter")
|
|
2006
|
+
.option("--session-id <id>", "Optional session filter")
|
|
2007
|
+
.option("--include-inactive", "Include inactive memories when supported", false)
|
|
2008
|
+
.option("--user-id <user>", "Explicit Memoria user_id", config.defaultUserId)
|
|
2009
|
+
.action(withCliClient(async (opts) => {
|
|
2010
|
+
const userId = resolveCliUserId(opts.userId);
|
|
2011
|
+
const result = await client.listMemories({
|
|
2012
|
+
userId,
|
|
2013
|
+
memoryType:
|
|
2014
|
+
typeof opts.type === "string" && opts.type.trim()
|
|
2015
|
+
? readMemoryType({ memoryType: opts.type }, "memoryType")
|
|
2016
|
+
: undefined,
|
|
2017
|
+
limit: clampInt(Number.parseInt(String(opts.limit), 10), 1, 200, 20),
|
|
2018
|
+
sessionId:
|
|
2019
|
+
typeof opts.sessionId === "string" && opts.sessionId.trim()
|
|
2020
|
+
? opts.sessionId.trim()
|
|
2021
|
+
: undefined,
|
|
2022
|
+
includeInactive: Boolean(opts.includeInactive),
|
|
2023
|
+
});
|
|
2024
|
+
printJson({
|
|
2025
|
+
backend: config.backend,
|
|
2026
|
+
userId,
|
|
2027
|
+
count: result.count,
|
|
2028
|
+
items: result.items,
|
|
2029
|
+
includeInactive: result.include_inactive ?? false,
|
|
2030
|
+
partial: result.partial ?? false,
|
|
2031
|
+
limitations: result.limitations ?? [],
|
|
2032
|
+
});
|
|
2033
|
+
}));
|
|
2034
|
+
|
|
2035
|
+
memoria
|
|
2036
|
+
.command("stats")
|
|
2037
|
+
.description("Show aggregate Memoria statistics")
|
|
2038
|
+
.option("--user-id <user>", "Explicit Memoria user_id", config.defaultUserId)
|
|
2039
|
+
.action(withCliClient(async (opts) => {
|
|
2040
|
+
const userId = resolveCliUserId(opts.userId);
|
|
2041
|
+
const result = await client.stats(userId);
|
|
2042
|
+
printJson(buildMemoryStatsPayload(config, userId, result));
|
|
2043
|
+
}));
|
|
2044
|
+
|
|
2045
|
+
memoria
|
|
2046
|
+
.command("profile")
|
|
2047
|
+
.description("Show the current Memoria profile")
|
|
2048
|
+
.option(
|
|
2049
|
+
"--user-id <user>",
|
|
2050
|
+
"Explicit Memoria user_id",
|
|
2051
|
+
config.defaultUserId,
|
|
2052
|
+
)
|
|
2053
|
+
.action(withCliClient(async (opts) => {
|
|
2054
|
+
const userId = resolveCliUserId(opts.userId, config.defaultUserId);
|
|
2055
|
+
const result = await client.profile(userId);
|
|
2056
|
+
printJson(result);
|
|
2057
|
+
}));
|
|
2058
|
+
|
|
2059
|
+
memoria
|
|
2060
|
+
.command("capabilities")
|
|
2061
|
+
.description("Show plugin capabilities and compatibility mappings")
|
|
2062
|
+
.action(withCliClient(async () => {
|
|
2063
|
+
printJson(buildCapabilitiesPayload(config));
|
|
2064
|
+
}));
|
|
2065
|
+
|
|
2066
|
+
memoria
|
|
2067
|
+
.command("install")
|
|
2068
|
+
.description("Install or repair the local Memoria runtime and plugin config")
|
|
2069
|
+
.option("--memoria-bin <path>", "Use an existing memoria executable")
|
|
2070
|
+
.option("--memoria-version <tag>", "Rust Memoria release tag to install")
|
|
2071
|
+
.option("--memoria-install-dir <path>", "Where to install memoria if it is missing")
|
|
2072
|
+
.option("--skip-memoria-install", "Require an existing memoria executable", false)
|
|
2073
|
+
.option("--no-verify", "Skip post-install verification")
|
|
2074
|
+
.action(withCliClient(async (opts) => {
|
|
2075
|
+
runMemoriaInstaller({
|
|
2076
|
+
memoriaBin:
|
|
2077
|
+
typeof opts.memoriaBin === "string" && opts.memoriaBin.trim()
|
|
2078
|
+
? opts.memoriaBin.trim()
|
|
2079
|
+
: undefined,
|
|
2080
|
+
memoriaVersion:
|
|
2081
|
+
typeof opts.memoriaVersion === "string" && opts.memoriaVersion.trim()
|
|
2082
|
+
? opts.memoriaVersion.trim()
|
|
2083
|
+
: undefined,
|
|
2084
|
+
memoriaInstallDir:
|
|
2085
|
+
typeof opts.memoriaInstallDir === "string" && opts.memoriaInstallDir.trim()
|
|
2086
|
+
? opts.memoriaInstallDir.trim()
|
|
2087
|
+
: undefined,
|
|
2088
|
+
skipMemoriaInstall: Boolean(opts.skipMemoriaInstall),
|
|
2089
|
+
verify: opts.verify !== false,
|
|
2090
|
+
});
|
|
2091
|
+
}));
|
|
2092
|
+
|
|
2093
|
+
memoria
|
|
2094
|
+
.command("verify")
|
|
2095
|
+
.description("Validate the current Memoria plugin install and backend status")
|
|
2096
|
+
.option("--memoria-bin <path>", "Use an explicit memoria executable for verification")
|
|
2097
|
+
.action(withCliClient(async (opts) => {
|
|
2098
|
+
runMemoriaVerifier({
|
|
2099
|
+
memoriaBin:
|
|
2100
|
+
typeof opts.memoriaBin === "string" && opts.memoriaBin.trim()
|
|
2101
|
+
? opts.memoriaBin.trim()
|
|
2102
|
+
: undefined,
|
|
2103
|
+
});
|
|
2104
|
+
}));
|
|
2105
|
+
|
|
2106
|
+
memoria
|
|
2107
|
+
.command("setup")
|
|
2108
|
+
.description("Recommended onboarding entrypoint: configure cloud/local backend then validate")
|
|
2109
|
+
.option("--mode <cloud|local>", "Backend mode to configure", "cloud")
|
|
2110
|
+
.option("--api-url <url>", "Memoria API URL (required for mode=cloud)")
|
|
2111
|
+
.option("--api-key <token>", "Memoria API token (required for mode=cloud)")
|
|
2112
|
+
.option("--db-url <dsn>", "MatrixOne DSN (required for mode=local)")
|
|
2113
|
+
.option("--memoria-bin <path>", "Path to memoria executable to pin in plugin config")
|
|
2114
|
+
.option("--install-memoria", "Install memoria binary automatically if it is missing", false)
|
|
2115
|
+
.option("--memoria-version <tag>", "Rust Memoria release tag to install when --install-memoria")
|
|
2116
|
+
.option("--memoria-install-dir <path>", "Install directory for memoria when --install-memoria")
|
|
2117
|
+
.option("--user-id <user>", "Default Memoria user id")
|
|
2118
|
+
.option("--embedding-provider <provider>", "Embedding provider for mode=local")
|
|
2119
|
+
.option("--embedding-model <model>", "Embedding model for mode=local")
|
|
2120
|
+
.option("--embedding-api-key <key>", "Embedding API key for mode=local")
|
|
2121
|
+
.option("--embedding-base-url <url>", "Embedding API base URL for mode=local")
|
|
2122
|
+
.option("--embedding-dim <n>", "Embedding dimensions for mode=local")
|
|
2123
|
+
.option("--skip-validate", "Skip `openclaw config validate`", false)
|
|
2124
|
+
.option("--skip-health-check", "Skip `openclaw memoria health`", false)
|
|
2125
|
+
.action(withCliClient(async (opts) => {
|
|
2126
|
+
const normalized = normalizeConnectOptions(opts as RawConnectCliOptions, "cloud");
|
|
2127
|
+
applyConnectOptions(normalized);
|
|
2128
|
+
}));
|
|
2129
|
+
|
|
2130
|
+
memoria
|
|
2131
|
+
.command("connect")
|
|
2132
|
+
.description("Configure cloud/local Memoria backend in OpenClaw config")
|
|
2133
|
+
.option("--mode <cloud|local>", "Backend mode to configure", "cloud")
|
|
2134
|
+
.option("--api-url <url>", "Memoria API URL (required for mode=cloud)")
|
|
2135
|
+
.option("--api-key <token>", "Memoria API token (required for mode=cloud)")
|
|
2136
|
+
.option("--db-url <dsn>", "MatrixOne DSN (required for mode=local)")
|
|
2137
|
+
.option("--memoria-bin <path>", "Path to memoria executable to pin in plugin config")
|
|
2138
|
+
.option("--user-id <user>", "Default Memoria user id")
|
|
2139
|
+
.option("--embedding-provider <provider>", "Embedding provider for mode=local")
|
|
2140
|
+
.option("--embedding-model <model>", "Embedding model for mode=local")
|
|
2141
|
+
.option("--embedding-api-key <key>", "Embedding API key for mode=local")
|
|
2142
|
+
.option("--embedding-base-url <url>", "Embedding API base URL for mode=local")
|
|
2143
|
+
.option("--embedding-dim <n>", "Embedding dimensions for mode=local")
|
|
2144
|
+
.option("--skip-validate", "Skip `openclaw config validate`", false)
|
|
2145
|
+
.option("--skip-health-check", "Skip `openclaw memoria health`", false)
|
|
2146
|
+
.action(withCliClient(async (opts) => {
|
|
2147
|
+
const normalized = normalizeConnectOptions(opts as RawConnectCliOptions, "cloud");
|
|
2148
|
+
applyConnectOptions(normalized);
|
|
2149
|
+
}));
|
|
2150
|
+
|
|
2151
|
+
ltm
|
|
2152
|
+
.command("list")
|
|
2153
|
+
.description("Compatibility alias for memoria list")
|
|
2154
|
+
.option("--limit <n>", "Maximum result count", "20")
|
|
2155
|
+
.option("--type <memoryType>", "Optional memory type filter")
|
|
2156
|
+
.option("--user-id <user>", "Explicit Memoria user_id", config.defaultUserId)
|
|
2157
|
+
.option("--json", "Ignored compatibility flag; output is already JSON", true)
|
|
2158
|
+
.action(withCliClient(async (opts) => {
|
|
2159
|
+
const userId = resolveCliUserId(opts.userId);
|
|
2160
|
+
const result = await client.listMemories({
|
|
2161
|
+
userId,
|
|
2162
|
+
memoryType:
|
|
2163
|
+
typeof opts.type === "string" && opts.type.trim()
|
|
2164
|
+
? readMemoryType({ memoryType: opts.type }, "memoryType")
|
|
2165
|
+
: undefined,
|
|
2166
|
+
limit: clampInt(Number.parseInt(String(opts.limit), 10), 1, 200, 20),
|
|
2167
|
+
});
|
|
2168
|
+
printJson({
|
|
2169
|
+
backend: config.backend,
|
|
2170
|
+
userId,
|
|
2171
|
+
count: result.count,
|
|
2172
|
+
items: result.items,
|
|
2173
|
+
partial: result.partial ?? false,
|
|
2174
|
+
limitations: result.limitations ?? [],
|
|
2175
|
+
});
|
|
2176
|
+
}));
|
|
2177
|
+
|
|
2178
|
+
ltm
|
|
2179
|
+
.command("search")
|
|
2180
|
+
.description("Compatibility alias for memory_recall")
|
|
2181
|
+
.argument("<query>", "Recall query")
|
|
2182
|
+
.option("--limit <n>", "Maximum result count", String(config.retrieveTopK))
|
|
2183
|
+
.option("--user-id <user>", "Explicit Memoria user_id", config.defaultUserId)
|
|
2184
|
+
.option("--json", "Ignored compatibility flag; output is already JSON", true)
|
|
2185
|
+
.action(withCliClient(async (query, opts) => {
|
|
2186
|
+
const topK = clampInt(Number.parseInt(String(opts.limit), 10), 1, 20, config.retrieveTopK);
|
|
2187
|
+
const userId = resolveCliUserId(opts.userId);
|
|
2188
|
+
const result = await client.retrieve({
|
|
2189
|
+
userId,
|
|
2190
|
+
query: String(query),
|
|
2191
|
+
topK,
|
|
2192
|
+
includeCrossSession: config.includeCrossSession,
|
|
2193
|
+
});
|
|
2194
|
+
printJson({
|
|
2195
|
+
backend: config.backend,
|
|
2196
|
+
userId,
|
|
2197
|
+
count: result.length,
|
|
2198
|
+
memories: result,
|
|
2199
|
+
});
|
|
2200
|
+
}));
|
|
2201
|
+
|
|
2202
|
+
ltm
|
|
2203
|
+
.command("stats")
|
|
2204
|
+
.description("Compatibility alias for memory_stats")
|
|
2205
|
+
.option("--user-id <user>", "Explicit Memoria user_id", config.defaultUserId)
|
|
2206
|
+
.option("--json", "Ignored compatibility flag; output is already JSON", true)
|
|
2207
|
+
.action(withCliClient(async (opts) => {
|
|
2208
|
+
const userId = resolveCliUserId(opts.userId);
|
|
2209
|
+
const result = await client.stats(userId);
|
|
2210
|
+
printJson(buildMemoryStatsPayload(config, userId, result));
|
|
2211
|
+
}));
|
|
2212
|
+
|
|
2213
|
+
ltm
|
|
2214
|
+
.command("health")
|
|
2215
|
+
.description("Check Memoria connectivity through the compatibility CLI")
|
|
2216
|
+
.option("--user-id <user>", "Explicit Memoria user_id", config.defaultUserId)
|
|
2217
|
+
.option("--json", "Ignored compatibility flag; output is already JSON", true)
|
|
2218
|
+
.action(withCliClient(async (opts) => {
|
|
2219
|
+
const userId = resolveCliUserId(opts.userId);
|
|
2220
|
+
const result = await client.health(userId);
|
|
2221
|
+
printJson({
|
|
2222
|
+
userId,
|
|
2223
|
+
backend: config.backend,
|
|
2224
|
+
...(asRecord(result) ?? {}),
|
|
2225
|
+
});
|
|
2226
|
+
}));
|
|
2227
|
+
},
|
|
2228
|
+
{ commands: [...CLI_COMMAND_NAMES] },
|
|
2229
|
+
);
|
|
2230
|
+
|
|
2231
|
+
const handleAutoRecall = async (
|
|
2232
|
+
prompt: string,
|
|
2233
|
+
ctx: PluginIdentityContext,
|
|
2234
|
+
): Promise<{ prependContext?: string } | void> => {
|
|
2235
|
+
const trimmed = prompt.trim();
|
|
2236
|
+
if (trimmed.length < config.recallMinPromptLength) {
|
|
2237
|
+
return;
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
const userId = resolveUserId(config, ctx);
|
|
2241
|
+
|
|
2242
|
+
try {
|
|
2243
|
+
const memories = await client.retrieve({
|
|
2244
|
+
userId,
|
|
2245
|
+
query: trimmed,
|
|
2246
|
+
topK: config.retrieveTopK,
|
|
2247
|
+
memoryTypes: config.retrieveMemoryTypes,
|
|
2248
|
+
sessionId: ctx.sessionId,
|
|
2249
|
+
includeCrossSession: config.includeCrossSession,
|
|
2250
|
+
});
|
|
2251
|
+
if (memories.length === 0) {
|
|
2252
|
+
return;
|
|
2253
|
+
}
|
|
2254
|
+
api.logger.info(`memory-memoria: recalled ${memories.length} memories`);
|
|
2255
|
+
return {
|
|
2256
|
+
prependContext: formatRelevantMemoriesContext(memories),
|
|
2257
|
+
};
|
|
2258
|
+
} catch (error) {
|
|
2259
|
+
api.logger.warn(`memory-memoria: auto-recall failed: ${String(error)}`);
|
|
2260
|
+
}
|
|
2261
|
+
};
|
|
2262
|
+
|
|
2263
|
+
if (config.autoRecall) {
|
|
2264
|
+
api.on("before_prompt_build", async (event, ctx) => {
|
|
2265
|
+
return await handleAutoRecall(event.prompt, ctx);
|
|
2266
|
+
});
|
|
2267
|
+
|
|
2268
|
+
api.on("before_agent_start", async (event, ctx) => {
|
|
2269
|
+
return await handleAutoRecall(event.prompt, ctx);
|
|
2270
|
+
});
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
if (config.autoObserve) {
|
|
2274
|
+
api.on("agent_end", async (event, ctx) => {
|
|
2275
|
+
if (!event.success || !Array.isArray(event.messages) || event.messages.length === 0) {
|
|
2276
|
+
return;
|
|
2277
|
+
}
|
|
2278
|
+
const messages = collectRecentConversationMessages(event.messages, {
|
|
2279
|
+
tailMessages: config.observeTailMessages,
|
|
2280
|
+
maxChars: config.observeMaxChars,
|
|
2281
|
+
});
|
|
2282
|
+
if (messages.length === 0) {
|
|
2283
|
+
return;
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
const userId = resolveUserId(config, ctx);
|
|
2287
|
+
|
|
2288
|
+
try {
|
|
2289
|
+
const created = await client.observe({
|
|
2290
|
+
userId,
|
|
2291
|
+
messages,
|
|
2292
|
+
sourceEventIds: ctx.sessionId ? [`openclaw:${ctx.sessionId}`] : undefined,
|
|
2293
|
+
sessionId: ctx.sessionId,
|
|
2294
|
+
});
|
|
2295
|
+
if (created.length > 0) {
|
|
2296
|
+
api.logger.info(`memory-memoria: observed ${created.length} new memories`);
|
|
2297
|
+
}
|
|
2298
|
+
} catch (error) {
|
|
2299
|
+
api.logger.warn(`memory-memoria: auto-observe failed: ${String(error)}`);
|
|
2300
|
+
}
|
|
2301
|
+
});
|
|
2302
|
+
|
|
2303
|
+
api.on("before_reset", async (event, ctx) => {
|
|
2304
|
+
if (!Array.isArray(event.messages) || event.messages.length === 0) {
|
|
2305
|
+
return;
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
const messages = collectRecentConversationMessages(event.messages, {
|
|
2309
|
+
tailMessages: config.observeTailMessages,
|
|
2310
|
+
maxChars: config.observeMaxChars,
|
|
2311
|
+
});
|
|
2312
|
+
if (messages.length === 0) {
|
|
2313
|
+
return;
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
const userId = resolveUserId(config, ctx);
|
|
2317
|
+
|
|
2318
|
+
try {
|
|
2319
|
+
const created = await client.observe({
|
|
2320
|
+
userId,
|
|
2321
|
+
messages,
|
|
2322
|
+
sourceEventIds: ctx.sessionId ? [`openclaw:before_reset:${ctx.sessionId}`] : undefined,
|
|
2323
|
+
sessionId: ctx.sessionId,
|
|
2324
|
+
});
|
|
2325
|
+
if (created.length > 0) {
|
|
2326
|
+
api.logger.info(
|
|
2327
|
+
`memory-memoria: observed ${created.length} new memories before reset`,
|
|
2328
|
+
);
|
|
2329
|
+
}
|
|
2330
|
+
} catch (error) {
|
|
2331
|
+
api.logger.warn(`memory-memoria: before_reset observe failed: ${String(error)}`);
|
|
2332
|
+
}
|
|
2333
|
+
});
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
api.on("after_compaction", async () => {
|
|
2337
|
+
api.logger.info(
|
|
2338
|
+
"memory-memoria: compaction finished; next prompt will use live Memoria recall",
|
|
2339
|
+
);
|
|
2340
|
+
});
|
|
2341
|
+
|
|
2342
|
+
api.registerService({
|
|
2343
|
+
id: "memory-memoria",
|
|
2344
|
+
async start() {
|
|
2345
|
+
try {
|
|
2346
|
+
const result = await client.health(config.defaultUserId);
|
|
2347
|
+
api.logger.info(`memory-memoria: connected (${String(result.status ?? "ok")})`);
|
|
2348
|
+
} catch (error) {
|
|
2349
|
+
api.logger.warn(`memory-memoria: health check failed: ${String(error)}`);
|
|
2350
|
+
}
|
|
2351
|
+
},
|
|
2352
|
+
stop() {
|
|
2353
|
+
client.close();
|
|
2354
|
+
api.logger.info("memory-memoria: stopped");
|
|
2355
|
+
},
|
|
2356
|
+
});
|
|
2357
|
+
},
|
|
2358
|
+
};
|
|
2359
|
+
|
|
2360
|
+
export default plugin;
|