mr-memory 3.7.10 → 3.9.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 +12 -2
- package/index.js +474 -41
- package/index.ts +435 -15
- package/memoryrouter-api.js +69 -2
- package/memoryrouter-api.ts +75 -2
- package/openclaw.plugin.json +11 -0
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { readFile, readdir, stat, lstat, writeFile, copyFile, mkdir } from "node:fs/promises";
|
|
12
|
-
import { deleteMemories, getMemoryStats, ingestMemory, prepareMemory, searchMemory, warmupMemory } from "./memoryrouter-api.js";
|
|
12
|
+
import { commitReflections, consolidateCheckout, dateSearch, deleteMemories, deleteMemoryIds, getMemoryStats, ingestMemory, inspectMemory, prepareMemory, searchMemory, warmupMemory } from "./memoryrouter-api.js";
|
|
13
13
|
import { join, resolve, relative, isAbsolute, sep, dirname } from "node:path";
|
|
14
14
|
import path from "node:path";
|
|
15
15
|
import os from "node:os";
|
|
@@ -17,6 +17,16 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
|
17
17
|
|
|
18
18
|
const DEFAULT_ENDPOINT = "https://api.memoryrouter.ai";
|
|
19
19
|
|
|
20
|
+
/** Keep-warm defaults.
|
|
21
|
+
*
|
|
22
|
+
* Warmth only matters in the minutes right after real activity, so warmup
|
|
23
|
+
* pings are activity-scoped instead of always-on: every message opens a
|
|
24
|
+
* KEEP_WARM window, pings fire at KEEP_WARM_PING_INTERVAL_MS inside it, and
|
|
25
|
+
* the timer is cleared completely once the window closes. An idle gateway
|
|
26
|
+
* sends zero warmup traffic. */
|
|
27
|
+
const DEFAULT_KEEP_WARM_MS = 3 * 60 * 1000; // 3 minutes after the last message
|
|
28
|
+
const KEEP_WARM_PING_INTERVAL_MS = 60 * 1000; // cadence inside the window
|
|
29
|
+
|
|
20
30
|
/** Strip media-attached references and OpenClaw media instruction text so
|
|
21
31
|
* old screenshots/photos/audio don't get stored and re-injected forever. */
|
|
22
32
|
const MEDIA_ATTACHED_RE = /\[media attached[^\]]*\]/gi;
|
|
@@ -55,6 +65,27 @@ function jsonToolResult(payload: unknown) {
|
|
|
55
65
|
return { content: [{ type: "text" as const, text: JSON.stringify(payload, null, 2) }], details: payload };
|
|
56
66
|
}
|
|
57
67
|
|
|
68
|
+
// ── Reflection debt nudge (rate-limited, one line, appended to prepare injections)
|
|
69
|
+
type ReflectionDebt = {
|
|
70
|
+
suggestion?: string | null;
|
|
71
|
+
unconsolidated?: {
|
|
72
|
+
raw?: { units?: number; est_tokens?: number };
|
|
73
|
+
tier2?: { count?: number; importance?: number; est_tokens?: number };
|
|
74
|
+
};
|
|
75
|
+
threshold_tokens?: number;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const NUDGE_INTERVAL_MS = 24 * 60 * 60 * 1000; // at most one nudge per 24h
|
|
79
|
+
|
|
80
|
+
function buildDebtNudgeLine(reflection: ReflectionDebt | undefined): string | null {
|
|
81
|
+
if (!reflection || !reflection.suggestion) return null;
|
|
82
|
+
const rawTokens = reflection.unconsolidated?.raw?.est_tokens ?? 0;
|
|
83
|
+
const tier2Tokens = reflection.unconsolidated?.tier2?.est_tokens ?? 0;
|
|
84
|
+
const totalTokens = rawTokens + tier2Tokens;
|
|
85
|
+
const kTokens = Math.max(1, Math.round(totalTokens / 1000));
|
|
86
|
+
return `[MemoryRouter] Consolidation available: ${kTokens}k unconsolidated tokens. Use the memory_reflect tool when convenient.`;
|
|
87
|
+
}
|
|
88
|
+
|
|
58
89
|
// ── memory_get helpers (copied from OpenClaw source — pure filesystem, no embeddings)
|
|
59
90
|
function isFileMissingError(err: unknown): boolean {
|
|
60
91
|
return Boolean(err && typeof err === "object" && "code" in err && (err as any).code === "ENOENT");
|
|
@@ -282,6 +313,9 @@ type MemoryRouterConfig = {
|
|
|
282
313
|
logging?: boolean;
|
|
283
314
|
embeddings?: string;
|
|
284
315
|
agentKeys?: Record<string, string>;
|
|
316
|
+
/** How long to keep the vault warm after the last message, in ms.
|
|
317
|
+
* Default 180000 (3 minutes). Set 0 to disable warmup pings entirely. */
|
|
318
|
+
keepWarmMs?: number;
|
|
285
319
|
};
|
|
286
320
|
|
|
287
321
|
// ──────────────────────────────────────────────────────
|
|
@@ -320,6 +354,36 @@ async function writeOpenClawConfigFile(config: JsonObject): Promise<void> {
|
|
|
320
354
|
`, "utf8");
|
|
321
355
|
}
|
|
322
356
|
|
|
357
|
+
// Nudge rate-limit state lives beside the OpenClaw config so it survives restarts
|
|
358
|
+
// without churning the main config file.
|
|
359
|
+
function nudgeStatePath(): string {
|
|
360
|
+
return join(dirname(openClawConfigPath()), "mr-memory-state.json");
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async function consumeNudgeBudget(): Promise<boolean> {
|
|
364
|
+
const now = Date.now();
|
|
365
|
+
let state: { lastNudgeAt?: number } = {};
|
|
366
|
+
try {
|
|
367
|
+
state = JSON.parse(await readFile(nudgeStatePath(), "utf8"));
|
|
368
|
+
} catch { /* missing or unreadable state: treat as never nudged */ }
|
|
369
|
+
if (typeof state.lastNudgeAt === "number" && now - state.lastNudgeAt < NUDGE_INTERVAL_MS) {
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
try {
|
|
373
|
+
await writeFile(nudgeStatePath(), `${JSON.stringify({ ...state, lastNudgeAt: now }, null, 2)}\n`, "utf8");
|
|
374
|
+
} catch { /* state write failure must never break prepare */ }
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** One-line, rate-limited debt nudge for prepare injections. Returns null when
|
|
379
|
+
* there is no suggestion or the 24h budget is spent. */
|
|
380
|
+
async function maybeDebtNudge(reflection: ReflectionDebt | undefined): Promise<string | null> {
|
|
381
|
+
const line = buildDebtNudgeLine(reflection);
|
|
382
|
+
if (!line) return null;
|
|
383
|
+
if (!(await consumeNudgeBudget())) return null;
|
|
384
|
+
return line;
|
|
385
|
+
}
|
|
386
|
+
|
|
323
387
|
// ──────────────────────────────────────────────────────
|
|
324
388
|
// Full-replace: disable OpenClaw's native memory search so MemoryRouter
|
|
325
389
|
// owns the memory_search tool (no name conflict, no native embeddings 429).
|
|
@@ -501,7 +565,18 @@ const memoryRouterPlugin = {
|
|
|
501
565
|
}
|
|
502
566
|
|
|
503
567
|
const hasAnyKey = !!(memoryKey || (agentKeys && Object.keys(agentKeys).length > 0));
|
|
568
|
+
|
|
569
|
+
// Keep-warm window: 0 (or negative) disables warmup pings entirely.
|
|
570
|
+
const keepWarmMs = typeof cfg?.keepWarmMs === "number" && cfg.keepWarmMs >= 0
|
|
571
|
+
? cfg.keepWarmMs
|
|
572
|
+
: DEFAULT_KEEP_WARM_MS;
|
|
504
573
|
let warmupTimer: ReturnType<typeof setInterval> | undefined;
|
|
574
|
+
// undefined = no activity yet (never compare against 0; a 0 timestamp is a
|
|
575
|
+
// valid clock value and must not read as "no activity").
|
|
576
|
+
let lastActivityAt: number | undefined;
|
|
577
|
+
// Starts true so keep-warm works even if the host never calls service.start();
|
|
578
|
+
// service.stop() flips it off so no timer can outlive the plugin.
|
|
579
|
+
let serviceRunning = true;
|
|
505
580
|
|
|
506
581
|
function configuredKeys(): string[] {
|
|
507
582
|
const keys = new Set<string>();
|
|
@@ -531,6 +606,41 @@ const memoryRouterPlugin = {
|
|
|
531
606
|
}
|
|
532
607
|
}
|
|
533
608
|
|
|
609
|
+
function stopWarmLoop(): void {
|
|
610
|
+
if (warmupTimer) {
|
|
611
|
+
clearInterval(warmupTimer);
|
|
612
|
+
warmupTimer = undefined;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/** True while we're still inside the post-message keep-warm window. */
|
|
617
|
+
function withinWarmWindow(): boolean {
|
|
618
|
+
if (keepWarmMs <= 0 || lastActivityAt === undefined) return false;
|
|
619
|
+
return Date.now() - lastActivityAt <= keepWarmMs;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/** Called on every message-driven activity (prepare / ingest).
|
|
623
|
+
* Opens (or extends) the keep-warm window and makes sure the loop runs.
|
|
624
|
+
* Once the window lapses with no new activity the interval is cleared, so
|
|
625
|
+
* an idle gateway sends no warmup traffic at all. */
|
|
626
|
+
function noteActivity(): void {
|
|
627
|
+
if (!hasAnyKey || keepWarmMs <= 0 || !serviceRunning) return;
|
|
628
|
+
lastActivityAt = Date.now();
|
|
629
|
+
if (warmupTimer) return; // window extended; existing loop keeps running
|
|
630
|
+
// No immediate ping: the prepare/ingest request that triggered this call
|
|
631
|
+
// already warmed the vault server-side. Pings only cover the idle gaps
|
|
632
|
+
// inside the window.
|
|
633
|
+
warmupTimer = setInterval(() => {
|
|
634
|
+
if (!withinWarmWindow()) {
|
|
635
|
+
log("memoryrouter: keep-warm window elapsed — pings stopped until next message");
|
|
636
|
+
stopWarmLoop();
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
pingWarmup("keepalive");
|
|
640
|
+
}, KEEP_WARM_PING_INTERVAL_MS);
|
|
641
|
+
warmupTimer.unref?.();
|
|
642
|
+
}
|
|
643
|
+
|
|
534
644
|
void (async () => {
|
|
535
645
|
await ensureDoctorSafePluginEntry(api);
|
|
536
646
|
if (hasAnyKey) await selfHealNativeMemorySearch(api);
|
|
@@ -684,6 +794,9 @@ If setting up MemoryRouter for another agent:
|
|
|
684
794
|
}
|
|
685
795
|
|
|
686
796
|
|
|
797
|
+
// Message activity — open/extend the keep-warm window.
|
|
798
|
+
noteActivity();
|
|
799
|
+
|
|
687
800
|
const res = await prepareMemory(endpoint, activeKey, {
|
|
688
801
|
messages: contextPayload,
|
|
689
802
|
density,
|
|
@@ -706,13 +819,17 @@ If setting up MemoryRouter for another agent:
|
|
|
706
819
|
tokens_billed?: number;
|
|
707
820
|
memory_tokens?: number;
|
|
708
821
|
metrics?: Record<string, unknown>;
|
|
822
|
+
reflection?: ReflectionDebt;
|
|
709
823
|
};
|
|
710
824
|
|
|
711
825
|
if (data.context) {
|
|
712
826
|
const elapsed = Date.now() - startMs;
|
|
713
827
|
api.logger.info?.(`memoryrouter: injected ${data.memories_found || 0} memories (${data.memory_tokens || 0} tokens, ${elapsed}ms) [llm_input]`);
|
|
714
828
|
if (elapsed > 2000 && data.metrics) log(`memoryrouter: prepare metrics ${JSON.stringify(data.metrics)}`);
|
|
715
|
-
|
|
829
|
+
let contextText = data.context;
|
|
830
|
+
const nudge = await maybeDebtNudge(data.reflection);
|
|
831
|
+
if (nudge) contextText = `${contextText}\n\n${nudge}`;
|
|
832
|
+
const wrapped = wrapForInjection(contextText);
|
|
716
833
|
return { appendSystemContext: wrapped };
|
|
717
834
|
}
|
|
718
835
|
} catch {
|
|
@@ -795,6 +912,9 @@ If setting up MemoryRouter for another agent:
|
|
|
795
912
|
|
|
796
913
|
// 4. Call /v1/memory/prepare (no session_id — always search core vault)
|
|
797
914
|
|
|
915
|
+
// Message activity — open/extend the keep-warm window.
|
|
916
|
+
noteActivity();
|
|
917
|
+
|
|
798
918
|
const res = await prepareMemory(endpoint, activeKey, {
|
|
799
919
|
messages: contextPayload,
|
|
800
920
|
density,
|
|
@@ -820,13 +940,17 @@ If setting up MemoryRouter for another agent:
|
|
|
820
940
|
tokens_billed?: number;
|
|
821
941
|
memory_tokens?: number;
|
|
822
942
|
metrics?: Record<string, unknown>;
|
|
943
|
+
reflection?: ReflectionDebt;
|
|
823
944
|
};
|
|
824
945
|
|
|
825
946
|
if (data.context) {
|
|
826
947
|
const elapsed = Date.now() - startMs;
|
|
827
948
|
api.logger.info?.(`memoryrouter: injected ${data.memories_found || 0} memories (${data.memory_tokens || 0} tokens, ${elapsed}ms)`);
|
|
828
949
|
if (elapsed > 2000 && data.metrics) log(`memoryrouter: prepare metrics ${JSON.stringify(data.metrics)}`);
|
|
829
|
-
|
|
950
|
+
let contextText = data.context;
|
|
951
|
+
const nudge = await maybeDebtNudge(data.reflection);
|
|
952
|
+
if (nudge) contextText = `${contextText}\n\n${nudge}`;
|
|
953
|
+
const wrapped = wrapForInjection(contextText);
|
|
830
954
|
return { appendSystemContext: wrapped };
|
|
831
955
|
}
|
|
832
956
|
} catch (err) {
|
|
@@ -914,6 +1038,9 @@ If setting up MemoryRouter for another agent:
|
|
|
914
1038
|
return;
|
|
915
1039
|
}
|
|
916
1040
|
|
|
1041
|
+
// Message activity — open/extend the keep-warm window.
|
|
1042
|
+
noteActivity();
|
|
1043
|
+
|
|
917
1044
|
// Fire and forget — don't block message delivery waiting for ingest
|
|
918
1045
|
ingestMemory(endpoint, activeKey, {
|
|
919
1046
|
messages: toStore,
|
|
@@ -957,6 +1084,17 @@ If setting up MemoryRouter for another agent:
|
|
|
957
1084
|
query: { type: "string" },
|
|
958
1085
|
maxResults: { type: "number" },
|
|
959
1086
|
minScore: { type: "number" },
|
|
1087
|
+
tiers: {
|
|
1088
|
+
type: "array",
|
|
1089
|
+
items: { type: "integer", enum: [1, 2, 3] },
|
|
1090
|
+
description: "Restrict results to these memory tiers. 1 raw memories, 2 reflections, 3 high-level reflections. Omit to search all tiers blended.",
|
|
1091
|
+
},
|
|
1092
|
+
importance: {
|
|
1093
|
+
type: "integer",
|
|
1094
|
+
minimum: 1,
|
|
1095
|
+
maximum: 10,
|
|
1096
|
+
description: "Only return memories rated at least this important (1 to 10). Only reflections carry ratings, so this filters to tier 2 and 3 content.",
|
|
1097
|
+
},
|
|
960
1098
|
},
|
|
961
1099
|
required: ["query"],
|
|
962
1100
|
} as any,
|
|
@@ -965,14 +1103,26 @@ If setting up MemoryRouter for another agent:
|
|
|
965
1103
|
const query = typeof params.query === "string" ? params.query.trim() : "";
|
|
966
1104
|
if (!query) return jsonToolResult({ results: [], error: "query required" });
|
|
967
1105
|
const limit = typeof params.maxResults === "number" ? params.maxResults : 50;
|
|
1106
|
+
const tiers = Array.isArray(params.tiers)
|
|
1107
|
+
? (params.tiers as unknown[]).filter((t): t is number => typeof t === "number" && [1, 2, 3].includes(t))
|
|
1108
|
+
: undefined;
|
|
1109
|
+
const importance = typeof params.importance === "number" && params.importance >= 1 && params.importance <= 10
|
|
1110
|
+
? Math.floor(params.importance)
|
|
1111
|
+
: undefined;
|
|
968
1112
|
try {
|
|
969
|
-
const res = await searchMemory(endpoint, toolKey, {
|
|
1113
|
+
const res = await searchMemory(endpoint, toolKey, {
|
|
1114
|
+
query,
|
|
1115
|
+
limit,
|
|
1116
|
+
...(tiers && tiers.length > 0 && { tiers }),
|
|
1117
|
+
...(importance !== undefined && { importance }),
|
|
1118
|
+
embeddings,
|
|
1119
|
+
});
|
|
970
1120
|
if (!res.ok) {
|
|
971
1121
|
const errBody = await res.text().catch(() => "");
|
|
972
1122
|
return jsonToolResult({ results: [], error: `Search failed: HTTP ${res.status}`, details: errBody.slice(0, 200) });
|
|
973
1123
|
}
|
|
974
1124
|
const data = await res.json() as {
|
|
975
|
-
memories?: Array<{ content: string; score: number; role: string; window: string; timestampHuman: string }>;
|
|
1125
|
+
memories?: Array<{ content: string; score: number; role: string; window: string; timestampHuman: string; tier?: number; importance?: number }>;
|
|
976
1126
|
totalMemories?: number;
|
|
977
1127
|
tokenCount?: number;
|
|
978
1128
|
};
|
|
@@ -984,6 +1134,8 @@ If setting up MemoryRouter for another agent:
|
|
|
984
1134
|
role: m.role,
|
|
985
1135
|
window: m.window,
|
|
986
1136
|
date: m.timestampHuman,
|
|
1137
|
+
...(m.tier !== undefined && { tier: m.tier }),
|
|
1138
|
+
...(m.importance !== undefined && m.importance !== null && { importance: m.importance }),
|
|
987
1139
|
}));
|
|
988
1140
|
return jsonToolResult({
|
|
989
1141
|
results,
|
|
@@ -999,6 +1151,211 @@ If setting up MemoryRouter for another agent:
|
|
|
999
1151
|
};
|
|
1000
1152
|
});
|
|
1001
1153
|
|
|
1154
|
+
// ==================================================================
|
|
1155
|
+
// Tools: memory_reflect + memory_commit (consolidation, two-tool shape)
|
|
1156
|
+
// Mirrors the MCP consolidate_memories / commit_reflections pair.
|
|
1157
|
+
// Invariants: source_ids are stripped, the model never sees row IDs,
|
|
1158
|
+
// and contract instructions are transmitted verbatim.
|
|
1159
|
+
// ==================================================================
|
|
1160
|
+
|
|
1161
|
+
// memory_reflect: checkout wrapper (nothing is modified until memory_commit)
|
|
1162
|
+
api.registerTool((ctx) => {
|
|
1163
|
+
const toolKey = resolveKey(ctx);
|
|
1164
|
+
return {
|
|
1165
|
+
label: "Memory Reflect",
|
|
1166
|
+
name: "memory_reflect",
|
|
1167
|
+
description: "Consolidate unconsolidated memories into reflections. Use when memory responses indicate consolidation is available, or when the user asks to organize or reflect on memory. Checks out a batch of memories under a 15 minute lease and returns their texts plus server instructions for writing reflection entries. Follow the instructions, then call memory_commit with the batch_id. Nothing is modified until commit.",
|
|
1168
|
+
parameters: {
|
|
1169
|
+
type: "object",
|
|
1170
|
+
properties: {
|
|
1171
|
+
tier: {
|
|
1172
|
+
type: "integer",
|
|
1173
|
+
enum: [1, 2],
|
|
1174
|
+
description: "1 consolidates raw memories into reflections; 2 consolidates reflections into high-level reflections. Default 1.",
|
|
1175
|
+
},
|
|
1176
|
+
max_tokens: {
|
|
1177
|
+
type: "integer",
|
|
1178
|
+
minimum: 1000,
|
|
1179
|
+
description: "Token budget for the checked-out batch. Defaults to 50000. No connector-side cap.",
|
|
1180
|
+
},
|
|
1181
|
+
},
|
|
1182
|
+
required: [],
|
|
1183
|
+
} as any,
|
|
1184
|
+
execute: async (_toolCallId: string, params: Record<string, unknown>) => {
|
|
1185
|
+
if (!toolKey) return jsonToolResult({ error: "No memory key configured for this agent" });
|
|
1186
|
+
const tier = params.tier === 2 ? 2 : 1;
|
|
1187
|
+
const maxTokens = typeof params.max_tokens === "number" && params.max_tokens >= 1000
|
|
1188
|
+
? Math.floor(params.max_tokens)
|
|
1189
|
+
: 50000;
|
|
1190
|
+
try {
|
|
1191
|
+
const res = await consolidateCheckout(endpoint, toolKey, {
|
|
1192
|
+
tier: tier as 1 | 2,
|
|
1193
|
+
max_tokens: maxTokens,
|
|
1194
|
+
...(embeddings && { embeddings }),
|
|
1195
|
+
});
|
|
1196
|
+
if (!res.ok) {
|
|
1197
|
+
const errBody = await res.text().catch(() => "");
|
|
1198
|
+
return jsonToolResult({ error: `Checkout failed: HTTP ${res.status}`, details: errBody.slice(0, 200) });
|
|
1199
|
+
}
|
|
1200
|
+
const data = await res.json() as {
|
|
1201
|
+
batch_id?: string | null;
|
|
1202
|
+
tier?: number;
|
|
1203
|
+
count?: number;
|
|
1204
|
+
est_tokens?: number;
|
|
1205
|
+
lease_expires_at?: string | null;
|
|
1206
|
+
memories?: Array<{ text: string; date: string }>;
|
|
1207
|
+
reflection_contract?: { version?: number; instructions?: string };
|
|
1208
|
+
tier_definitions?: { target_tier?: number; instruction?: string };
|
|
1209
|
+
};
|
|
1210
|
+
if (!data.batch_id) {
|
|
1211
|
+
return jsonToolResult({ batch_id: null, count: 0, message: "Nothing to consolidate at this tier right now." });
|
|
1212
|
+
}
|
|
1213
|
+
// Contract instructions are server-authored and passed through verbatim.
|
|
1214
|
+
// If neither contract form is present, refuse; never author a local prompt.
|
|
1215
|
+
const instructions = data.reflection_contract?.instructions ?? data.tier_definitions?.instruction;
|
|
1216
|
+
if (!instructions) {
|
|
1217
|
+
return jsonToolResult({ error: "Checkout response carried no reflection contract. Cannot consolidate without server instructions." });
|
|
1218
|
+
}
|
|
1219
|
+
// source_ids stripped: the model never sees memory row IDs.
|
|
1220
|
+
return jsonToolResult({
|
|
1221
|
+
batch_id: data.batch_id,
|
|
1222
|
+
lease_expires_at: data.lease_expires_at ?? null,
|
|
1223
|
+
count: data.count ?? 0,
|
|
1224
|
+
est_tokens: data.est_tokens ?? 0,
|
|
1225
|
+
instructions,
|
|
1226
|
+
memories: (data.memories || []).map((m) => ({ text: m.text, date: m.date })),
|
|
1227
|
+
message: `When your entries are ready, call memory_commit with batch_id ${data.batch_id} before ${data.lease_expires_at ?? "the lease expires"}.`,
|
|
1228
|
+
});
|
|
1229
|
+
} catch (err) {
|
|
1230
|
+
return jsonToolResult({ error: err instanceof Error ? err.message : String(err) });
|
|
1231
|
+
}
|
|
1232
|
+
},
|
|
1233
|
+
};
|
|
1234
|
+
});
|
|
1235
|
+
|
|
1236
|
+
// memory_commit: commit wrapper (idempotent; 409 means the lease expired)
|
|
1237
|
+
api.registerTool((ctx) => {
|
|
1238
|
+
const toolKey = resolveKey(ctx);
|
|
1239
|
+
return {
|
|
1240
|
+
label: "Memory Commit",
|
|
1241
|
+
name: "memory_commit",
|
|
1242
|
+
description: "Commit reflection entries for a batch checked out with memory_reflect. Each entry is standalone text plus an importance rating from 1 to 10. Safe to retry: committing the same batch twice reports success without duplicating.",
|
|
1243
|
+
parameters: {
|
|
1244
|
+
type: "object",
|
|
1245
|
+
properties: {
|
|
1246
|
+
batch_id: { type: "string", description: "The batch_id returned by memory_reflect." },
|
|
1247
|
+
entries: {
|
|
1248
|
+
type: "array",
|
|
1249
|
+
items: {
|
|
1250
|
+
type: "object",
|
|
1251
|
+
properties: {
|
|
1252
|
+
text: { type: "string", description: "Standalone reflection prose, 1 to 8000 chars. No IDs or scores inline." },
|
|
1253
|
+
importance: { type: "integer", minimum: 1, maximum: 10 },
|
|
1254
|
+
},
|
|
1255
|
+
required: ["text", "importance"],
|
|
1256
|
+
additionalProperties: false,
|
|
1257
|
+
},
|
|
1258
|
+
minItems: 1,
|
|
1259
|
+
maxItems: 100,
|
|
1260
|
+
},
|
|
1261
|
+
},
|
|
1262
|
+
required: ["batch_id", "entries"],
|
|
1263
|
+
} as any,
|
|
1264
|
+
execute: async (_toolCallId: string, params: Record<string, unknown>) => {
|
|
1265
|
+
if (!toolKey) return jsonToolResult({ error: "No memory key configured for this agent" });
|
|
1266
|
+
const batchId = typeof params.batch_id === "string" ? params.batch_id.trim() : "";
|
|
1267
|
+
if (!batchId) return jsonToolResult({ error: "batch_id required" });
|
|
1268
|
+
const rawEntries = Array.isArray(params.entries) ? params.entries as Array<Record<string, unknown>> : [];
|
|
1269
|
+
if (rawEntries.length < 1 || rawEntries.length > 100) {
|
|
1270
|
+
return jsonToolResult({ error: "entries must contain between 1 and 100 items" });
|
|
1271
|
+
}
|
|
1272
|
+
const entries: Array<{ text: string; importance: number }> = [];
|
|
1273
|
+
for (let i = 0; i < rawEntries.length; i++) {
|
|
1274
|
+
const e = rawEntries[i];
|
|
1275
|
+
if (!e || typeof e !== "object") return jsonToolResult({ error: `entry ${i + 1} is not an object` });
|
|
1276
|
+
const extraKeys = Object.keys(e).filter((k) => k !== "text" && k !== "importance");
|
|
1277
|
+
if (extraKeys.length > 0) {
|
|
1278
|
+
// Position-reference impossibility: entries are text plus importance only.
|
|
1279
|
+
return jsonToolResult({ error: `entry ${i + 1} has unsupported fields: ${extraKeys.join(", ")}. Entries are text plus importance only; coverage is always the whole batch.` });
|
|
1280
|
+
}
|
|
1281
|
+
const text = typeof e.text === "string" ? e.text : "";
|
|
1282
|
+
if (!text || text.length > 8000) return jsonToolResult({ error: `entry ${i + 1} text must be 1 to 8000 chars` });
|
|
1283
|
+
const importance = typeof e.importance === "number" && Number.isInteger(e.importance) && e.importance >= 1 && e.importance <= 10
|
|
1284
|
+
? e.importance
|
|
1285
|
+
: null;
|
|
1286
|
+
if (importance === null) return jsonToolResult({ error: `entry ${i + 1} importance must be an integer 1 to 10` });
|
|
1287
|
+
entries.push({ text, importance });
|
|
1288
|
+
}
|
|
1289
|
+
try {
|
|
1290
|
+
const res = await commitReflections(endpoint, toolKey, {
|
|
1291
|
+
batch_id: batchId,
|
|
1292
|
+
entries,
|
|
1293
|
+
...(embeddings && { embeddings }),
|
|
1294
|
+
});
|
|
1295
|
+
if (res.status === 409) {
|
|
1296
|
+
return jsonToolResult({ error: "lease expired, call memory_reflect again" });
|
|
1297
|
+
}
|
|
1298
|
+
if (!res.ok) {
|
|
1299
|
+
const errBody = await res.text().catch(() => "");
|
|
1300
|
+
return jsonToolResult({ error: `Commit failed: HTTP ${res.status}`, details: errBody.slice(0, 200) });
|
|
1301
|
+
}
|
|
1302
|
+
const data = await res.json() as {
|
|
1303
|
+
committed?: boolean;
|
|
1304
|
+
already_committed?: boolean;
|
|
1305
|
+
created?: Array<{ id: string; importance: number }>;
|
|
1306
|
+
sources_consolidated?: number;
|
|
1307
|
+
reflection?: ReflectionDebt;
|
|
1308
|
+
};
|
|
1309
|
+
// Created UUIDs dropped: the model never sees memory row IDs.
|
|
1310
|
+
return jsonToolResult({
|
|
1311
|
+
committed: data.committed ?? false,
|
|
1312
|
+
already_committed: data.already_committed ?? false,
|
|
1313
|
+
entries_created: data.created?.length ?? 0,
|
|
1314
|
+
sources_consolidated: data.sources_consolidated ?? 0,
|
|
1315
|
+
debt: data.reflection?.suggestion ?? null,
|
|
1316
|
+
});
|
|
1317
|
+
} catch (err) {
|
|
1318
|
+
return jsonToolResult({ error: err instanceof Error ? err.message : String(err) });
|
|
1319
|
+
}
|
|
1320
|
+
},
|
|
1321
|
+
};
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1324
|
+
// memory_inspect: read-only lineage drill-down (curation surface, ids allowed)
|
|
1325
|
+
api.registerTool((ctx) => {
|
|
1326
|
+
const toolKey = resolveKey(ctx);
|
|
1327
|
+
return {
|
|
1328
|
+
label: "Memory Inspect",
|
|
1329
|
+
name: "memory_inspect",
|
|
1330
|
+
description: "Inspect a memory's consolidation lineage by memory_id: its tier, the batch it came from, the sources it consolidated, and what it was consolidated into. Read-only.",
|
|
1331
|
+
parameters: {
|
|
1332
|
+
type: "object",
|
|
1333
|
+
properties: {
|
|
1334
|
+
memory_id: { type: "string", description: "The memory id to inspect." },
|
|
1335
|
+
},
|
|
1336
|
+
required: ["memory_id"],
|
|
1337
|
+
} as any,
|
|
1338
|
+
execute: async (_toolCallId: string, params: Record<string, unknown>) => {
|
|
1339
|
+
if (!toolKey) return jsonToolResult({ error: "No memory key configured for this agent" });
|
|
1340
|
+
const memoryId = typeof params.memory_id === "string" ? params.memory_id.trim() : "";
|
|
1341
|
+
if (!memoryId) return jsonToolResult({ error: "memory_id required" });
|
|
1342
|
+
try {
|
|
1343
|
+
const res = await inspectMemory(endpoint, toolKey, {
|
|
1344
|
+
memory_id: memoryId,
|
|
1345
|
+
...(embeddings && { embeddings }),
|
|
1346
|
+
});
|
|
1347
|
+
if (!res.ok) {
|
|
1348
|
+
const errBody = await res.text().catch(() => "");
|
|
1349
|
+
return jsonToolResult({ error: `Inspect failed: HTTP ${res.status}`, details: errBody.slice(0, 200) });
|
|
1350
|
+
}
|
|
1351
|
+
return jsonToolResult(await res.json());
|
|
1352
|
+
} catch (err) {
|
|
1353
|
+
return jsonToolResult({ error: err instanceof Error ? err.message : String(err) });
|
|
1354
|
+
}
|
|
1355
|
+
},
|
|
1356
|
+
};
|
|
1357
|
+
});
|
|
1358
|
+
|
|
1002
1359
|
// memory_get — exact copy of OpenClaw's implementation (pure local file read)
|
|
1003
1360
|
api.registerTool((ctx) => {
|
|
1004
1361
|
const workspaceDir = ctx.workspaceDir || "";
|
|
@@ -1285,11 +1642,36 @@ If setting up MemoryRouter for another agent:
|
|
|
1285
1642
|
});
|
|
1286
1643
|
|
|
1287
1644
|
mr.command("delete")
|
|
1288
|
-
.description("Clear all memories from vault")
|
|
1645
|
+
.description("Clear all memories from vault, or delete specific memories with --ids")
|
|
1646
|
+
.option("--ids <ids>", "Comma-separated memory ids to delete (instead of the whole vault)")
|
|
1647
|
+
.option("--yes", "Confirm deletion (required with --ids)")
|
|
1289
1648
|
.option("--key <key>", "Override memory key (for subagent vaults)")
|
|
1290
|
-
.action(async (opts: { key?: string }) => {
|
|
1649
|
+
.action(async (opts: { ids?: string; yes?: boolean; key?: string }) => {
|
|
1291
1650
|
const effectiveKey = opts.key || memoryKey;
|
|
1292
1651
|
if (!effectiveKey) { console.error("Not configured. Run: openclaw mr <key> or pass --key <key>"); return; }
|
|
1652
|
+
if (opts.ids) {
|
|
1653
|
+
const ids = opts.ids.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1654
|
+
if (ids.length === 0) { console.error("No ids provided."); return; }
|
|
1655
|
+
console.log(`The following ${ids.length} memor${ids.length === 1 ? "y" : "ies"} will be PERMANENTLY deleted:`);
|
|
1656
|
+
for (const id of ids) console.log(` • ${id}`);
|
|
1657
|
+
if (!opts.yes) {
|
|
1658
|
+
console.error("\nRefusing to delete without --yes. Re-run with --yes to confirm.");
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
try {
|
|
1662
|
+
const res = await deleteMemoryIds(endpoint, effectiveKey, { ids, ...(embeddings && { embeddings }) });
|
|
1663
|
+
if (!res.ok) {
|
|
1664
|
+
const errBody = await res.text().catch(() => "");
|
|
1665
|
+
console.error(`Delete failed: HTTP ${res.status}${errBody ? `: ${errBody.slice(0, 200)}` : ""}`);
|
|
1666
|
+
return;
|
|
1667
|
+
}
|
|
1668
|
+
const data = (await res.json()) as { requested?: number; deleted?: number; not_found?: number };
|
|
1669
|
+
console.log(`✓ Deleted ${data.deleted ?? 0} of ${data.requested ?? ids.length} (not found: ${data.not_found ?? 0})`);
|
|
1670
|
+
} catch (err) {
|
|
1671
|
+
console.error(`Failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1672
|
+
}
|
|
1673
|
+
return;
|
|
1674
|
+
}
|
|
1293
1675
|
try {
|
|
1294
1676
|
const res = await deleteMemories(endpoint, effectiveKey, embeddings);
|
|
1295
1677
|
const data = (await res.json()) as { message?: string };
|
|
@@ -1299,6 +1681,42 @@ If setting up MemoryRouter for another agent:
|
|
|
1299
1681
|
console.error(`Failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1300
1682
|
}
|
|
1301
1683
|
});
|
|
1684
|
+
|
|
1685
|
+
mr.command("reflect")
|
|
1686
|
+
.description("Show reflection debt status (consolidation runs via the agent's memory_reflect tool)")
|
|
1687
|
+
.option("--key <key>", "Override memory key (for subagent vaults)")
|
|
1688
|
+
.action(async (opts: { key?: string }) => {
|
|
1689
|
+
const effectiveKey = opts.key || memoryKey;
|
|
1690
|
+
if (!effectiveKey) { console.error("Not configured. Run: openclaw mr <key> or pass --key <key>"); return; }
|
|
1691
|
+
try {
|
|
1692
|
+
// Debt probe: an empty-window date-search returns the reflection
|
|
1693
|
+
// debt block with zero results and zero retrieval billing.
|
|
1694
|
+
const from = new Date(Date.now() - 1000).toISOString();
|
|
1695
|
+
const res = await dateSearch(endpoint, effectiveKey, { from, max_tokens: 1000, ...(embeddings && { embeddings }) });
|
|
1696
|
+
if (!res.ok) {
|
|
1697
|
+
const errBody = await res.text().catch(() => "");
|
|
1698
|
+
console.error(`Debt check failed: HTTP ${res.status}${errBody ? `: ${errBody.slice(0, 200)}` : ""}`);
|
|
1699
|
+
return;
|
|
1700
|
+
}
|
|
1701
|
+
const data = (await res.json()) as { reflection?: ReflectionDebt };
|
|
1702
|
+
const debt = data.reflection;
|
|
1703
|
+
console.log("Reflection Debt");
|
|
1704
|
+
console.log("───────────────────────────");
|
|
1705
|
+
const raw = debt?.unconsolidated?.raw;
|
|
1706
|
+
const tier2 = debt?.unconsolidated?.tier2;
|
|
1707
|
+
console.log(`Raw unconsolidated: ${raw?.units ?? 0} memories (~${raw?.est_tokens ?? 0} tokens)`);
|
|
1708
|
+
console.log(`Tier 2 unconsolidated: ${tier2?.count ?? 0} reflections (~${tier2?.est_tokens ?? 0} tokens)`);
|
|
1709
|
+
console.log(`Threshold: ${debt?.threshold_tokens ?? 0} tokens`);
|
|
1710
|
+
if (debt?.suggestion) {
|
|
1711
|
+
console.log(`\nStatus: ${debt.suggestion}`);
|
|
1712
|
+
console.log("Consolidation runs through the agent. Ask your agent to consolidate memories (it uses the memory_reflect and memory_commit tools).");
|
|
1713
|
+
} else {
|
|
1714
|
+
console.log("\nStatus: nothing to consolidate right now.");
|
|
1715
|
+
}
|
|
1716
|
+
} catch (err) {
|
|
1717
|
+
console.error(`Failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1718
|
+
}
|
|
1719
|
+
});
|
|
1302
1720
|
},
|
|
1303
1721
|
{ commands: ["mr"] },
|
|
1304
1722
|
);
|
|
@@ -1311,17 +1729,19 @@ If setting up MemoryRouter for another agent:
|
|
|
1311
1729
|
id: "mr-memory",
|
|
1312
1730
|
start: () => {
|
|
1313
1731
|
if (memoryKey) api.logger.info?.(`memoryrouter: active (key: ${memoryKey.slice(0, 6)}...)`);
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1732
|
+
serviceRunning = true;
|
|
1733
|
+
// No startup ping and no always-on keepalive: warmth is driven by real
|
|
1734
|
+
// activity. The first message opens the keep-warm window.
|
|
1735
|
+
if (hasAnyKey) {
|
|
1736
|
+
log(keepWarmMs > 0
|
|
1737
|
+
? `memoryrouter: keep-warm armed (${Math.round(keepWarmMs / 1000)}s after each message)`
|
|
1738
|
+
: "memoryrouter: keep-warm disabled (keepWarmMs: 0)");
|
|
1318
1739
|
}
|
|
1319
1740
|
},
|
|
1320
1741
|
stop: () => {
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
}
|
|
1742
|
+
serviceRunning = false;
|
|
1743
|
+
lastActivityAt = undefined;
|
|
1744
|
+
stopWarmLoop();
|
|
1325
1745
|
api.logger.info?.("memoryrouter: stopped");
|
|
1326
1746
|
},
|
|
1327
1747
|
});
|