mr-memory 3.8.0 → 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/index.js +346 -5
- package/index.ts +357 -7
- package/memoryrouter-api.js +69 -2
- package/memoryrouter-api.ts +75 -2
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* BYOK — provider API keys never touch MemoryRouter.
|
|
9
9
|
*/
|
|
10
10
|
import { readFile, lstat, writeFile, copyFile, mkdir } from "node:fs/promises";
|
|
11
|
-
import { deleteMemories, getMemoryStats, ingestMemory, prepareMemory, searchMemory, warmupMemory } from "./memoryrouter-api.js";
|
|
11
|
+
import { commitReflections, consolidateCheckout, dateSearch, deleteMemories, deleteMemoryIds, getMemoryStats, ingestMemory, inspectMemory, prepareMemory, searchMemory, warmupMemory } from "./memoryrouter-api.js";
|
|
12
12
|
import { join, resolve, relative, isAbsolute, sep, dirname } from "node:path";
|
|
13
13
|
import os from "node:os";
|
|
14
14
|
const DEFAULT_ENDPOINT = "https://api.memoryrouter.ai";
|
|
@@ -55,6 +55,16 @@ function stripOldMemory(text) {
|
|
|
55
55
|
function jsonToolResult(payload) {
|
|
56
56
|
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }], details: payload };
|
|
57
57
|
}
|
|
58
|
+
const NUDGE_INTERVAL_MS = 24 * 60 * 60 * 1000; // at most one nudge per 24h
|
|
59
|
+
function buildDebtNudgeLine(reflection) {
|
|
60
|
+
if (!reflection || !reflection.suggestion)
|
|
61
|
+
return null;
|
|
62
|
+
const rawTokens = reflection.unconsolidated?.raw?.est_tokens ?? 0;
|
|
63
|
+
const tier2Tokens = reflection.unconsolidated?.tier2?.est_tokens ?? 0;
|
|
64
|
+
const totalTokens = rawTokens + tier2Tokens;
|
|
65
|
+
const kTokens = Math.max(1, Math.round(totalTokens / 1000));
|
|
66
|
+
return `[MemoryRouter] Consolidation available: ${kTokens}k unconsolidated tokens. Use the memory_reflect tool when convenient.`;
|
|
67
|
+
}
|
|
58
68
|
// ── memory_get helpers (copied from OpenClaw source — pure filesystem, no embeddings)
|
|
59
69
|
function isFileMissingError(err) {
|
|
60
70
|
return Boolean(err && typeof err === "object" && "code" in err && err.code === "ENOENT");
|
|
@@ -301,6 +311,37 @@ async function writeOpenClawConfigFile(config) {
|
|
|
301
311
|
await writeFile(configPath, `${JSON.stringify(config, null, 2)}
|
|
302
312
|
`, "utf8");
|
|
303
313
|
}
|
|
314
|
+
// Nudge rate-limit state lives beside the OpenClaw config so it survives restarts
|
|
315
|
+
// without churning the main config file.
|
|
316
|
+
function nudgeStatePath() {
|
|
317
|
+
return join(dirname(openClawConfigPath()), "mr-memory-state.json");
|
|
318
|
+
}
|
|
319
|
+
async function consumeNudgeBudget() {
|
|
320
|
+
const now = Date.now();
|
|
321
|
+
let state = {};
|
|
322
|
+
try {
|
|
323
|
+
state = JSON.parse(await readFile(nudgeStatePath(), "utf8"));
|
|
324
|
+
}
|
|
325
|
+
catch { /* missing or unreadable state: treat as never nudged */ }
|
|
326
|
+
if (typeof state.lastNudgeAt === "number" && now - state.lastNudgeAt < NUDGE_INTERVAL_MS) {
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
try {
|
|
330
|
+
await writeFile(nudgeStatePath(), `${JSON.stringify({ ...state, lastNudgeAt: now }, null, 2)}\n`, "utf8");
|
|
331
|
+
}
|
|
332
|
+
catch { /* state write failure must never break prepare */ }
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
/** One-line, rate-limited debt nudge for prepare injections. Returns null when
|
|
336
|
+
* there is no suggestion or the 24h budget is spent. */
|
|
337
|
+
async function maybeDebtNudge(reflection) {
|
|
338
|
+
const line = buildDebtNudgeLine(reflection);
|
|
339
|
+
if (!line)
|
|
340
|
+
return null;
|
|
341
|
+
if (!(await consumeNudgeBudget()))
|
|
342
|
+
return null;
|
|
343
|
+
return line;
|
|
344
|
+
}
|
|
304
345
|
// ──────────────────────────────────────────────────────
|
|
305
346
|
// Full-replace: disable OpenClaw's native memory search so MemoryRouter
|
|
306
347
|
// owns the memory_search tool (no name conflict, no native embeddings 429).
|
|
@@ -731,7 +772,11 @@ If setting up MemoryRouter for another agent:
|
|
|
731
772
|
api.logger.info?.(`memoryrouter: injected ${data.memories_found || 0} memories (${data.memory_tokens || 0} tokens, ${elapsed}ms) [llm_input]`);
|
|
732
773
|
if (elapsed > 2000 && data.metrics)
|
|
733
774
|
log(`memoryrouter: prepare metrics ${JSON.stringify(data.metrics)}`);
|
|
734
|
-
|
|
775
|
+
let contextText = data.context;
|
|
776
|
+
const nudge = await maybeDebtNudge(data.reflection);
|
|
777
|
+
if (nudge)
|
|
778
|
+
contextText = `${contextText}\n\n${nudge}`;
|
|
779
|
+
const wrapped = wrapForInjection(contextText);
|
|
735
780
|
return { appendSystemContext: wrapped };
|
|
736
781
|
}
|
|
737
782
|
}
|
|
@@ -836,7 +881,11 @@ If setting up MemoryRouter for another agent:
|
|
|
836
881
|
api.logger.info?.(`memoryrouter: injected ${data.memories_found || 0} memories (${data.memory_tokens || 0} tokens, ${elapsed}ms)`);
|
|
837
882
|
if (elapsed > 2000 && data.metrics)
|
|
838
883
|
log(`memoryrouter: prepare metrics ${JSON.stringify(data.metrics)}`);
|
|
839
|
-
|
|
884
|
+
let contextText = data.context;
|
|
885
|
+
const nudge = await maybeDebtNudge(data.reflection);
|
|
886
|
+
if (nudge)
|
|
887
|
+
contextText = `${contextText}\n\n${nudge}`;
|
|
888
|
+
const wrapped = wrapForInjection(contextText);
|
|
840
889
|
return { appendSystemContext: wrapped };
|
|
841
890
|
}
|
|
842
891
|
}
|
|
@@ -963,6 +1012,17 @@ If setting up MemoryRouter for another agent:
|
|
|
963
1012
|
query: { type: "string" },
|
|
964
1013
|
maxResults: { type: "number" },
|
|
965
1014
|
minScore: { type: "number" },
|
|
1015
|
+
tiers: {
|
|
1016
|
+
type: "array",
|
|
1017
|
+
items: { type: "integer", enum: [1, 2, 3] },
|
|
1018
|
+
description: "Restrict results to these memory tiers. 1 raw memories, 2 reflections, 3 high-level reflections. Omit to search all tiers blended.",
|
|
1019
|
+
},
|
|
1020
|
+
importance: {
|
|
1021
|
+
type: "integer",
|
|
1022
|
+
minimum: 1,
|
|
1023
|
+
maximum: 10,
|
|
1024
|
+
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.",
|
|
1025
|
+
},
|
|
966
1026
|
},
|
|
967
1027
|
required: ["query"],
|
|
968
1028
|
},
|
|
@@ -973,8 +1033,20 @@ If setting up MemoryRouter for another agent:
|
|
|
973
1033
|
if (!query)
|
|
974
1034
|
return jsonToolResult({ results: [], error: "query required" });
|
|
975
1035
|
const limit = typeof params.maxResults === "number" ? params.maxResults : 50;
|
|
1036
|
+
const tiers = Array.isArray(params.tiers)
|
|
1037
|
+
? params.tiers.filter((t) => typeof t === "number" && [1, 2, 3].includes(t))
|
|
1038
|
+
: undefined;
|
|
1039
|
+
const importance = typeof params.importance === "number" && params.importance >= 1 && params.importance <= 10
|
|
1040
|
+
? Math.floor(params.importance)
|
|
1041
|
+
: undefined;
|
|
976
1042
|
try {
|
|
977
|
-
const res = await searchMemory(endpoint, toolKey, {
|
|
1043
|
+
const res = await searchMemory(endpoint, toolKey, {
|
|
1044
|
+
query,
|
|
1045
|
+
limit,
|
|
1046
|
+
...(tiers && tiers.length > 0 && { tiers }),
|
|
1047
|
+
...(importance !== undefined && { importance }),
|
|
1048
|
+
embeddings,
|
|
1049
|
+
});
|
|
978
1050
|
if (!res.ok) {
|
|
979
1051
|
const errBody = await res.text().catch(() => "");
|
|
980
1052
|
return jsonToolResult({ results: [], error: `Search failed: HTTP ${res.status}`, details: errBody.slice(0, 200) });
|
|
@@ -988,6 +1060,8 @@ If setting up MemoryRouter for another agent:
|
|
|
988
1060
|
role: m.role,
|
|
989
1061
|
window: m.window,
|
|
990
1062
|
date: m.timestampHuman,
|
|
1063
|
+
...(m.tier !== undefined && { tier: m.tier }),
|
|
1064
|
+
...(m.importance !== undefined && m.importance !== null && { importance: m.importance }),
|
|
991
1065
|
}));
|
|
992
1066
|
return jsonToolResult({
|
|
993
1067
|
results,
|
|
@@ -1003,6 +1077,203 @@ If setting up MemoryRouter for another agent:
|
|
|
1003
1077
|
},
|
|
1004
1078
|
};
|
|
1005
1079
|
});
|
|
1080
|
+
// ==================================================================
|
|
1081
|
+
// Tools: memory_reflect + memory_commit (consolidation, two-tool shape)
|
|
1082
|
+
// Mirrors the MCP consolidate_memories / commit_reflections pair.
|
|
1083
|
+
// Invariants: source_ids are stripped, the model never sees row IDs,
|
|
1084
|
+
// and contract instructions are transmitted verbatim.
|
|
1085
|
+
// ==================================================================
|
|
1086
|
+
// memory_reflect: checkout wrapper (nothing is modified until memory_commit)
|
|
1087
|
+
api.registerTool((ctx) => {
|
|
1088
|
+
const toolKey = resolveKey(ctx);
|
|
1089
|
+
return {
|
|
1090
|
+
label: "Memory Reflect",
|
|
1091
|
+
name: "memory_reflect",
|
|
1092
|
+
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.",
|
|
1093
|
+
parameters: {
|
|
1094
|
+
type: "object",
|
|
1095
|
+
properties: {
|
|
1096
|
+
tier: {
|
|
1097
|
+
type: "integer",
|
|
1098
|
+
enum: [1, 2],
|
|
1099
|
+
description: "1 consolidates raw memories into reflections; 2 consolidates reflections into high-level reflections. Default 1.",
|
|
1100
|
+
},
|
|
1101
|
+
max_tokens: {
|
|
1102
|
+
type: "integer",
|
|
1103
|
+
minimum: 1000,
|
|
1104
|
+
description: "Token budget for the checked-out batch. Defaults to 50000. No connector-side cap.",
|
|
1105
|
+
},
|
|
1106
|
+
},
|
|
1107
|
+
required: [],
|
|
1108
|
+
},
|
|
1109
|
+
execute: async (_toolCallId, params) => {
|
|
1110
|
+
if (!toolKey)
|
|
1111
|
+
return jsonToolResult({ error: "No memory key configured for this agent" });
|
|
1112
|
+
const tier = params.tier === 2 ? 2 : 1;
|
|
1113
|
+
const maxTokens = typeof params.max_tokens === "number" && params.max_tokens >= 1000
|
|
1114
|
+
? Math.floor(params.max_tokens)
|
|
1115
|
+
: 50000;
|
|
1116
|
+
try {
|
|
1117
|
+
const res = await consolidateCheckout(endpoint, toolKey, {
|
|
1118
|
+
tier: tier,
|
|
1119
|
+
max_tokens: maxTokens,
|
|
1120
|
+
...(embeddings && { embeddings }),
|
|
1121
|
+
});
|
|
1122
|
+
if (!res.ok) {
|
|
1123
|
+
const errBody = await res.text().catch(() => "");
|
|
1124
|
+
return jsonToolResult({ error: `Checkout failed: HTTP ${res.status}`, details: errBody.slice(0, 200) });
|
|
1125
|
+
}
|
|
1126
|
+
const data = await res.json();
|
|
1127
|
+
if (!data.batch_id) {
|
|
1128
|
+
return jsonToolResult({ batch_id: null, count: 0, message: "Nothing to consolidate at this tier right now." });
|
|
1129
|
+
}
|
|
1130
|
+
// Contract instructions are server-authored and passed through verbatim.
|
|
1131
|
+
// If neither contract form is present, refuse; never author a local prompt.
|
|
1132
|
+
const instructions = data.reflection_contract?.instructions ?? data.tier_definitions?.instruction;
|
|
1133
|
+
if (!instructions) {
|
|
1134
|
+
return jsonToolResult({ error: "Checkout response carried no reflection contract. Cannot consolidate without server instructions." });
|
|
1135
|
+
}
|
|
1136
|
+
// source_ids stripped: the model never sees memory row IDs.
|
|
1137
|
+
return jsonToolResult({
|
|
1138
|
+
batch_id: data.batch_id,
|
|
1139
|
+
lease_expires_at: data.lease_expires_at ?? null,
|
|
1140
|
+
count: data.count ?? 0,
|
|
1141
|
+
est_tokens: data.est_tokens ?? 0,
|
|
1142
|
+
instructions,
|
|
1143
|
+
memories: (data.memories || []).map((m) => ({ text: m.text, date: m.date })),
|
|
1144
|
+
message: `When your entries are ready, call memory_commit with batch_id ${data.batch_id} before ${data.lease_expires_at ?? "the lease expires"}.`,
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
catch (err) {
|
|
1148
|
+
return jsonToolResult({ error: err instanceof Error ? err.message : String(err) });
|
|
1149
|
+
}
|
|
1150
|
+
},
|
|
1151
|
+
};
|
|
1152
|
+
});
|
|
1153
|
+
// memory_commit: commit wrapper (idempotent; 409 means the lease expired)
|
|
1154
|
+
api.registerTool((ctx) => {
|
|
1155
|
+
const toolKey = resolveKey(ctx);
|
|
1156
|
+
return {
|
|
1157
|
+
label: "Memory Commit",
|
|
1158
|
+
name: "memory_commit",
|
|
1159
|
+
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.",
|
|
1160
|
+
parameters: {
|
|
1161
|
+
type: "object",
|
|
1162
|
+
properties: {
|
|
1163
|
+
batch_id: { type: "string", description: "The batch_id returned by memory_reflect." },
|
|
1164
|
+
entries: {
|
|
1165
|
+
type: "array",
|
|
1166
|
+
items: {
|
|
1167
|
+
type: "object",
|
|
1168
|
+
properties: {
|
|
1169
|
+
text: { type: "string", description: "Standalone reflection prose, 1 to 8000 chars. No IDs or scores inline." },
|
|
1170
|
+
importance: { type: "integer", minimum: 1, maximum: 10 },
|
|
1171
|
+
},
|
|
1172
|
+
required: ["text", "importance"],
|
|
1173
|
+
additionalProperties: false,
|
|
1174
|
+
},
|
|
1175
|
+
minItems: 1,
|
|
1176
|
+
maxItems: 100,
|
|
1177
|
+
},
|
|
1178
|
+
},
|
|
1179
|
+
required: ["batch_id", "entries"],
|
|
1180
|
+
},
|
|
1181
|
+
execute: async (_toolCallId, params) => {
|
|
1182
|
+
if (!toolKey)
|
|
1183
|
+
return jsonToolResult({ error: "No memory key configured for this agent" });
|
|
1184
|
+
const batchId = typeof params.batch_id === "string" ? params.batch_id.trim() : "";
|
|
1185
|
+
if (!batchId)
|
|
1186
|
+
return jsonToolResult({ error: "batch_id required" });
|
|
1187
|
+
const rawEntries = Array.isArray(params.entries) ? params.entries : [];
|
|
1188
|
+
if (rawEntries.length < 1 || rawEntries.length > 100) {
|
|
1189
|
+
return jsonToolResult({ error: "entries must contain between 1 and 100 items" });
|
|
1190
|
+
}
|
|
1191
|
+
const entries = [];
|
|
1192
|
+
for (let i = 0; i < rawEntries.length; i++) {
|
|
1193
|
+
const e = rawEntries[i];
|
|
1194
|
+
if (!e || typeof e !== "object")
|
|
1195
|
+
return jsonToolResult({ error: `entry ${i + 1} is not an object` });
|
|
1196
|
+
const extraKeys = Object.keys(e).filter((k) => k !== "text" && k !== "importance");
|
|
1197
|
+
if (extraKeys.length > 0) {
|
|
1198
|
+
// Position-reference impossibility: entries are text plus importance only.
|
|
1199
|
+
return jsonToolResult({ error: `entry ${i + 1} has unsupported fields: ${extraKeys.join(", ")}. Entries are text plus importance only; coverage is always the whole batch.` });
|
|
1200
|
+
}
|
|
1201
|
+
const text = typeof e.text === "string" ? e.text : "";
|
|
1202
|
+
if (!text || text.length > 8000)
|
|
1203
|
+
return jsonToolResult({ error: `entry ${i + 1} text must be 1 to 8000 chars` });
|
|
1204
|
+
const importance = typeof e.importance === "number" && Number.isInteger(e.importance) && e.importance >= 1 && e.importance <= 10
|
|
1205
|
+
? e.importance
|
|
1206
|
+
: null;
|
|
1207
|
+
if (importance === null)
|
|
1208
|
+
return jsonToolResult({ error: `entry ${i + 1} importance must be an integer 1 to 10` });
|
|
1209
|
+
entries.push({ text, importance });
|
|
1210
|
+
}
|
|
1211
|
+
try {
|
|
1212
|
+
const res = await commitReflections(endpoint, toolKey, {
|
|
1213
|
+
batch_id: batchId,
|
|
1214
|
+
entries,
|
|
1215
|
+
...(embeddings && { embeddings }),
|
|
1216
|
+
});
|
|
1217
|
+
if (res.status === 409) {
|
|
1218
|
+
return jsonToolResult({ error: "lease expired, call memory_reflect again" });
|
|
1219
|
+
}
|
|
1220
|
+
if (!res.ok) {
|
|
1221
|
+
const errBody = await res.text().catch(() => "");
|
|
1222
|
+
return jsonToolResult({ error: `Commit failed: HTTP ${res.status}`, details: errBody.slice(0, 200) });
|
|
1223
|
+
}
|
|
1224
|
+
const data = await res.json();
|
|
1225
|
+
// Created UUIDs dropped: the model never sees memory row IDs.
|
|
1226
|
+
return jsonToolResult({
|
|
1227
|
+
committed: data.committed ?? false,
|
|
1228
|
+
already_committed: data.already_committed ?? false,
|
|
1229
|
+
entries_created: data.created?.length ?? 0,
|
|
1230
|
+
sources_consolidated: data.sources_consolidated ?? 0,
|
|
1231
|
+
debt: data.reflection?.suggestion ?? null,
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
catch (err) {
|
|
1235
|
+
return jsonToolResult({ error: err instanceof Error ? err.message : String(err) });
|
|
1236
|
+
}
|
|
1237
|
+
},
|
|
1238
|
+
};
|
|
1239
|
+
});
|
|
1240
|
+
// memory_inspect: read-only lineage drill-down (curation surface, ids allowed)
|
|
1241
|
+
api.registerTool((ctx) => {
|
|
1242
|
+
const toolKey = resolveKey(ctx);
|
|
1243
|
+
return {
|
|
1244
|
+
label: "Memory Inspect",
|
|
1245
|
+
name: "memory_inspect",
|
|
1246
|
+
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.",
|
|
1247
|
+
parameters: {
|
|
1248
|
+
type: "object",
|
|
1249
|
+
properties: {
|
|
1250
|
+
memory_id: { type: "string", description: "The memory id to inspect." },
|
|
1251
|
+
},
|
|
1252
|
+
required: ["memory_id"],
|
|
1253
|
+
},
|
|
1254
|
+
execute: async (_toolCallId, params) => {
|
|
1255
|
+
if (!toolKey)
|
|
1256
|
+
return jsonToolResult({ error: "No memory key configured for this agent" });
|
|
1257
|
+
const memoryId = typeof params.memory_id === "string" ? params.memory_id.trim() : "";
|
|
1258
|
+
if (!memoryId)
|
|
1259
|
+
return jsonToolResult({ error: "memory_id required" });
|
|
1260
|
+
try {
|
|
1261
|
+
const res = await inspectMemory(endpoint, toolKey, {
|
|
1262
|
+
memory_id: memoryId,
|
|
1263
|
+
...(embeddings && { embeddings }),
|
|
1264
|
+
});
|
|
1265
|
+
if (!res.ok) {
|
|
1266
|
+
const errBody = await res.text().catch(() => "");
|
|
1267
|
+
return jsonToolResult({ error: `Inspect failed: HTTP ${res.status}`, details: errBody.slice(0, 200) });
|
|
1268
|
+
}
|
|
1269
|
+
return jsonToolResult(await res.json());
|
|
1270
|
+
}
|
|
1271
|
+
catch (err) {
|
|
1272
|
+
return jsonToolResult({ error: err instanceof Error ? err.message : String(err) });
|
|
1273
|
+
}
|
|
1274
|
+
},
|
|
1275
|
+
};
|
|
1276
|
+
});
|
|
1006
1277
|
// memory_get — exact copy of OpenClaw's implementation (pure local file read)
|
|
1007
1278
|
api.registerTool((ctx) => {
|
|
1008
1279
|
const workspaceDir = ctx.workspaceDir || "";
|
|
@@ -1278,7 +1549,9 @@ If setting up MemoryRouter for another agent:
|
|
|
1278
1549
|
}
|
|
1279
1550
|
});
|
|
1280
1551
|
mr.command("delete")
|
|
1281
|
-
.description("Clear all memories from vault")
|
|
1552
|
+
.description("Clear all memories from vault, or delete specific memories with --ids")
|
|
1553
|
+
.option("--ids <ids>", "Comma-separated memory ids to delete (instead of the whole vault)")
|
|
1554
|
+
.option("--yes", "Confirm deletion (required with --ids)")
|
|
1282
1555
|
.option("--key <key>", "Override memory key (for subagent vaults)")
|
|
1283
1556
|
.action(async (opts) => {
|
|
1284
1557
|
const effectiveKey = opts.key || memoryKey;
|
|
@@ -1286,6 +1559,34 @@ If setting up MemoryRouter for another agent:
|
|
|
1286
1559
|
console.error("Not configured. Run: openclaw mr <key> or pass --key <key>");
|
|
1287
1560
|
return;
|
|
1288
1561
|
}
|
|
1562
|
+
if (opts.ids) {
|
|
1563
|
+
const ids = opts.ids.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1564
|
+
if (ids.length === 0) {
|
|
1565
|
+
console.error("No ids provided.");
|
|
1566
|
+
return;
|
|
1567
|
+
}
|
|
1568
|
+
console.log(`The following ${ids.length} memor${ids.length === 1 ? "y" : "ies"} will be PERMANENTLY deleted:`);
|
|
1569
|
+
for (const id of ids)
|
|
1570
|
+
console.log(` • ${id}`);
|
|
1571
|
+
if (!opts.yes) {
|
|
1572
|
+
console.error("\nRefusing to delete without --yes. Re-run with --yes to confirm.");
|
|
1573
|
+
return;
|
|
1574
|
+
}
|
|
1575
|
+
try {
|
|
1576
|
+
const res = await deleteMemoryIds(endpoint, effectiveKey, { ids, ...(embeddings && { embeddings }) });
|
|
1577
|
+
if (!res.ok) {
|
|
1578
|
+
const errBody = await res.text().catch(() => "");
|
|
1579
|
+
console.error(`Delete failed: HTTP ${res.status}${errBody ? `: ${errBody.slice(0, 200)}` : ""}`);
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
const data = (await res.json());
|
|
1583
|
+
console.log(`✓ Deleted ${data.deleted ?? 0} of ${data.requested ?? ids.length} (not found: ${data.not_found ?? 0})`);
|
|
1584
|
+
}
|
|
1585
|
+
catch (err) {
|
|
1586
|
+
console.error(`Failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1587
|
+
}
|
|
1588
|
+
return;
|
|
1589
|
+
}
|
|
1289
1590
|
try {
|
|
1290
1591
|
const res = await deleteMemories(endpoint, effectiveKey, embeddings);
|
|
1291
1592
|
const data = (await res.json());
|
|
@@ -1296,6 +1597,46 @@ If setting up MemoryRouter for another agent:
|
|
|
1296
1597
|
console.error(`Failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1297
1598
|
}
|
|
1298
1599
|
});
|
|
1600
|
+
mr.command("reflect")
|
|
1601
|
+
.description("Show reflection debt status (consolidation runs via the agent's memory_reflect tool)")
|
|
1602
|
+
.option("--key <key>", "Override memory key (for subagent vaults)")
|
|
1603
|
+
.action(async (opts) => {
|
|
1604
|
+
const effectiveKey = opts.key || memoryKey;
|
|
1605
|
+
if (!effectiveKey) {
|
|
1606
|
+
console.error("Not configured. Run: openclaw mr <key> or pass --key <key>");
|
|
1607
|
+
return;
|
|
1608
|
+
}
|
|
1609
|
+
try {
|
|
1610
|
+
// Debt probe: an empty-window date-search returns the reflection
|
|
1611
|
+
// debt block with zero results and zero retrieval billing.
|
|
1612
|
+
const from = new Date(Date.now() - 1000).toISOString();
|
|
1613
|
+
const res = await dateSearch(endpoint, effectiveKey, { from, max_tokens: 1000, ...(embeddings && { embeddings }) });
|
|
1614
|
+
if (!res.ok) {
|
|
1615
|
+
const errBody = await res.text().catch(() => "");
|
|
1616
|
+
console.error(`Debt check failed: HTTP ${res.status}${errBody ? `: ${errBody.slice(0, 200)}` : ""}`);
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1619
|
+
const data = (await res.json());
|
|
1620
|
+
const debt = data.reflection;
|
|
1621
|
+
console.log("Reflection Debt");
|
|
1622
|
+
console.log("───────────────────────────");
|
|
1623
|
+
const raw = debt?.unconsolidated?.raw;
|
|
1624
|
+
const tier2 = debt?.unconsolidated?.tier2;
|
|
1625
|
+
console.log(`Raw unconsolidated: ${raw?.units ?? 0} memories (~${raw?.est_tokens ?? 0} tokens)`);
|
|
1626
|
+
console.log(`Tier 2 unconsolidated: ${tier2?.count ?? 0} reflections (~${tier2?.est_tokens ?? 0} tokens)`);
|
|
1627
|
+
console.log(`Threshold: ${debt?.threshold_tokens ?? 0} tokens`);
|
|
1628
|
+
if (debt?.suggestion) {
|
|
1629
|
+
console.log(`\nStatus: ${debt.suggestion}`);
|
|
1630
|
+
console.log("Consolidation runs through the agent. Ask your agent to consolidate memories (it uses the memory_reflect and memory_commit tools).");
|
|
1631
|
+
}
|
|
1632
|
+
else {
|
|
1633
|
+
console.log("\nStatus: nothing to consolidate right now.");
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
catch (err) {
|
|
1637
|
+
console.error(`Failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1638
|
+
}
|
|
1639
|
+
});
|
|
1299
1640
|
}, { commands: ["mr"] });
|
|
1300
1641
|
// ==================================================================
|
|
1301
1642
|
// Service
|
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";
|
|
@@ -65,6 +65,27 @@ function jsonToolResult(payload: unknown) {
|
|
|
65
65
|
return { content: [{ type: "text" as const, text: JSON.stringify(payload, null, 2) }], details: payload };
|
|
66
66
|
}
|
|
67
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
|
+
|
|
68
89
|
// ── memory_get helpers (copied from OpenClaw source — pure filesystem, no embeddings)
|
|
69
90
|
function isFileMissingError(err: unknown): boolean {
|
|
70
91
|
return Boolean(err && typeof err === "object" && "code" in err && (err as any).code === "ENOENT");
|
|
@@ -333,6 +354,36 @@ async function writeOpenClawConfigFile(config: JsonObject): Promise<void> {
|
|
|
333
354
|
`, "utf8");
|
|
334
355
|
}
|
|
335
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
|
+
|
|
336
387
|
// ──────────────────────────────────────────────────────
|
|
337
388
|
// Full-replace: disable OpenClaw's native memory search so MemoryRouter
|
|
338
389
|
// owns the memory_search tool (no name conflict, no native embeddings 429).
|
|
@@ -768,13 +819,17 @@ If setting up MemoryRouter for another agent:
|
|
|
768
819
|
tokens_billed?: number;
|
|
769
820
|
memory_tokens?: number;
|
|
770
821
|
metrics?: Record<string, unknown>;
|
|
822
|
+
reflection?: ReflectionDebt;
|
|
771
823
|
};
|
|
772
824
|
|
|
773
825
|
if (data.context) {
|
|
774
826
|
const elapsed = Date.now() - startMs;
|
|
775
827
|
api.logger.info?.(`memoryrouter: injected ${data.memories_found || 0} memories (${data.memory_tokens || 0} tokens, ${elapsed}ms) [llm_input]`);
|
|
776
828
|
if (elapsed > 2000 && data.metrics) log(`memoryrouter: prepare metrics ${JSON.stringify(data.metrics)}`);
|
|
777
|
-
|
|
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);
|
|
778
833
|
return { appendSystemContext: wrapped };
|
|
779
834
|
}
|
|
780
835
|
} catch {
|
|
@@ -885,13 +940,17 @@ If setting up MemoryRouter for another agent:
|
|
|
885
940
|
tokens_billed?: number;
|
|
886
941
|
memory_tokens?: number;
|
|
887
942
|
metrics?: Record<string, unknown>;
|
|
943
|
+
reflection?: ReflectionDebt;
|
|
888
944
|
};
|
|
889
945
|
|
|
890
946
|
if (data.context) {
|
|
891
947
|
const elapsed = Date.now() - startMs;
|
|
892
948
|
api.logger.info?.(`memoryrouter: injected ${data.memories_found || 0} memories (${data.memory_tokens || 0} tokens, ${elapsed}ms)`);
|
|
893
949
|
if (elapsed > 2000 && data.metrics) log(`memoryrouter: prepare metrics ${JSON.stringify(data.metrics)}`);
|
|
894
|
-
|
|
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);
|
|
895
954
|
return { appendSystemContext: wrapped };
|
|
896
955
|
}
|
|
897
956
|
} catch (err) {
|
|
@@ -1025,6 +1084,17 @@ If setting up MemoryRouter for another agent:
|
|
|
1025
1084
|
query: { type: "string" },
|
|
1026
1085
|
maxResults: { type: "number" },
|
|
1027
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
|
+
},
|
|
1028
1098
|
},
|
|
1029
1099
|
required: ["query"],
|
|
1030
1100
|
} as any,
|
|
@@ -1033,14 +1103,26 @@ If setting up MemoryRouter for another agent:
|
|
|
1033
1103
|
const query = typeof params.query === "string" ? params.query.trim() : "";
|
|
1034
1104
|
if (!query) return jsonToolResult({ results: [], error: "query required" });
|
|
1035
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;
|
|
1036
1112
|
try {
|
|
1037
|
-
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
|
+
});
|
|
1038
1120
|
if (!res.ok) {
|
|
1039
1121
|
const errBody = await res.text().catch(() => "");
|
|
1040
1122
|
return jsonToolResult({ results: [], error: `Search failed: HTTP ${res.status}`, details: errBody.slice(0, 200) });
|
|
1041
1123
|
}
|
|
1042
1124
|
const data = await res.json() as {
|
|
1043
|
-
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 }>;
|
|
1044
1126
|
totalMemories?: number;
|
|
1045
1127
|
tokenCount?: number;
|
|
1046
1128
|
};
|
|
@@ -1052,6 +1134,8 @@ If setting up MemoryRouter for another agent:
|
|
|
1052
1134
|
role: m.role,
|
|
1053
1135
|
window: m.window,
|
|
1054
1136
|
date: m.timestampHuman,
|
|
1137
|
+
...(m.tier !== undefined && { tier: m.tier }),
|
|
1138
|
+
...(m.importance !== undefined && m.importance !== null && { importance: m.importance }),
|
|
1055
1139
|
}));
|
|
1056
1140
|
return jsonToolResult({
|
|
1057
1141
|
results,
|
|
@@ -1067,6 +1151,211 @@ If setting up MemoryRouter for another agent:
|
|
|
1067
1151
|
};
|
|
1068
1152
|
});
|
|
1069
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
|
+
|
|
1070
1359
|
// memory_get — exact copy of OpenClaw's implementation (pure local file read)
|
|
1071
1360
|
api.registerTool((ctx) => {
|
|
1072
1361
|
const workspaceDir = ctx.workspaceDir || "";
|
|
@@ -1353,11 +1642,36 @@ If setting up MemoryRouter for another agent:
|
|
|
1353
1642
|
});
|
|
1354
1643
|
|
|
1355
1644
|
mr.command("delete")
|
|
1356
|
-
.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)")
|
|
1357
1648
|
.option("--key <key>", "Override memory key (for subagent vaults)")
|
|
1358
|
-
.action(async (opts: { key?: string }) => {
|
|
1649
|
+
.action(async (opts: { ids?: string; yes?: boolean; key?: string }) => {
|
|
1359
1650
|
const effectiveKey = opts.key || memoryKey;
|
|
1360
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
|
+
}
|
|
1361
1675
|
try {
|
|
1362
1676
|
const res = await deleteMemories(endpoint, effectiveKey, embeddings);
|
|
1363
1677
|
const data = (await res.json()) as { message?: string };
|
|
@@ -1367,6 +1681,42 @@ If setting up MemoryRouter for another agent:
|
|
|
1367
1681
|
console.error(`Failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1368
1682
|
}
|
|
1369
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
|
+
});
|
|
1370
1720
|
},
|
|
1371
1721
|
{ commands: ["mr"] },
|
|
1372
1722
|
);
|
package/memoryrouter-api.js
CHANGED
|
@@ -27,7 +27,75 @@ export async function searchMemory(endpoint, key, payload) {
|
|
|
27
27
|
"Content-Type": "application/json",
|
|
28
28
|
...(payload.embeddings && { "X-Embedding-Model": payload.embeddings }),
|
|
29
29
|
},
|
|
30
|
-
body: JSON.stringify({
|
|
30
|
+
body: JSON.stringify({
|
|
31
|
+
query: payload.query,
|
|
32
|
+
limit: payload.limit,
|
|
33
|
+
...(payload.tiers !== undefined && { tiers: payload.tiers }),
|
|
34
|
+
...(payload.importance !== undefined && { importance: payload.importance }),
|
|
35
|
+
}),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
export async function consolidateCheckout(endpoint, key, payload) {
|
|
39
|
+
return fetch(`${endpoint}/v1/memory/consolidate/checkout`, {
|
|
40
|
+
method: "POST",
|
|
41
|
+
headers: {
|
|
42
|
+
...JSON_HEADERS,
|
|
43
|
+
Authorization: `Bearer ${key}`,
|
|
44
|
+
...(payload.embeddings && { "X-Embedding-Model": payload.embeddings }),
|
|
45
|
+
},
|
|
46
|
+
body: JSON.stringify({
|
|
47
|
+
tier: payload.tier,
|
|
48
|
+
max_tokens: payload.max_tokens ?? 50000,
|
|
49
|
+
}),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
export async function commitReflections(endpoint, key, payload) {
|
|
53
|
+
return fetch(`${endpoint}/v1/memory/consolidate/commit`, {
|
|
54
|
+
method: "POST",
|
|
55
|
+
headers: {
|
|
56
|
+
...JSON_HEADERS,
|
|
57
|
+
Authorization: `Bearer ${key}`,
|
|
58
|
+
...(payload.embeddings && { "X-Embedding-Model": payload.embeddings }),
|
|
59
|
+
},
|
|
60
|
+
body: JSON.stringify({ batch_id: payload.batch_id, entries: payload.entries }),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
export async function inspectMemory(endpoint, key, payload) {
|
|
64
|
+
return fetch(`${endpoint}/v1/memory/inspect`, {
|
|
65
|
+
method: "POST",
|
|
66
|
+
headers: {
|
|
67
|
+
...JSON_HEADERS,
|
|
68
|
+
Authorization: `Bearer ${key}`,
|
|
69
|
+
...(payload.embeddings && { "X-Embedding-Model": payload.embeddings }),
|
|
70
|
+
},
|
|
71
|
+
body: JSON.stringify({ memory_id: payload.memory_id }),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
export async function deleteMemoryIds(endpoint, key, payload) {
|
|
75
|
+
return fetch(`${endpoint}/v1/memory/delete-ids`, {
|
|
76
|
+
method: "POST",
|
|
77
|
+
headers: {
|
|
78
|
+
...JSON_HEADERS,
|
|
79
|
+
Authorization: `Bearer ${key}`,
|
|
80
|
+
...(payload.embeddings && { "X-Embedding-Model": payload.embeddings }),
|
|
81
|
+
},
|
|
82
|
+
body: JSON.stringify({ ids: payload.ids }),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
export async function dateSearch(endpoint, key, payload) {
|
|
86
|
+
return fetch(`${endpoint}/v1/memory/date-search`, {
|
|
87
|
+
method: "POST",
|
|
88
|
+
headers: {
|
|
89
|
+
...JSON_HEADERS,
|
|
90
|
+
Authorization: `Bearer ${key}`,
|
|
91
|
+
...(payload.embeddings && { "X-Embedding-Model": payload.embeddings }),
|
|
92
|
+
},
|
|
93
|
+
body: JSON.stringify({
|
|
94
|
+
from: payload.from,
|
|
95
|
+
...(payload.to !== undefined && { to: payload.to }),
|
|
96
|
+
...(payload.query !== undefined && { query: payload.query }),
|
|
97
|
+
...(payload.max_tokens !== undefined && { max_tokens: payload.max_tokens }),
|
|
98
|
+
}),
|
|
31
99
|
});
|
|
32
100
|
}
|
|
33
101
|
export async function getMemoryStats(endpoint, key, embeddings) {
|
|
@@ -47,7 +115,6 @@ export async function deleteMemories(endpoint, key, embeddings) {
|
|
|
47
115
|
headers: { Authorization: `Bearer ${key}` },
|
|
48
116
|
});
|
|
49
117
|
}
|
|
50
|
-
|
|
51
118
|
export async function warmupMemory(endpoint, key, embeddings) {
|
|
52
119
|
return fetch(`${endpoint}/v1/memory/warmup`, {
|
|
53
120
|
method: "POST",
|
package/memoryrouter-api.ts
CHANGED
|
@@ -28,7 +28,7 @@ export function ingestMemory(endpoint: string, key: string, payload: { messages:
|
|
|
28
28
|
});
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
export async function searchMemory(endpoint: string, key: string, payload: { query: string; limit: number; embeddings?: string }): Promise<Response> {
|
|
31
|
+
export async function searchMemory(endpoint: string, key: string, payload: { query: string; limit: number; tiers?: number[]; importance?: number; embeddings?: string }): Promise<Response> {
|
|
32
32
|
return fetch(`${endpoint}/v1/memory/search`, {
|
|
33
33
|
method: "POST",
|
|
34
34
|
headers: {
|
|
@@ -36,7 +36,80 @@ export async function searchMemory(endpoint: string, key: string, payload: { que
|
|
|
36
36
|
"Content-Type": "application/json",
|
|
37
37
|
...(payload.embeddings && { "X-Embedding-Model": payload.embeddings }),
|
|
38
38
|
},
|
|
39
|
-
body: JSON.stringify({
|
|
39
|
+
body: JSON.stringify({
|
|
40
|
+
query: payload.query,
|
|
41
|
+
limit: payload.limit,
|
|
42
|
+
...(payload.tiers !== undefined && { tiers: payload.tiers }),
|
|
43
|
+
...(payload.importance !== undefined && { importance: payload.importance }),
|
|
44
|
+
}),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function consolidateCheckout(endpoint: string, key: string, payload: { tier: 1 | 2; max_tokens?: number; embeddings?: string }): Promise<Response> {
|
|
49
|
+
return fetch(`${endpoint}/v1/memory/consolidate/checkout`, {
|
|
50
|
+
method: "POST",
|
|
51
|
+
headers: {
|
|
52
|
+
...JSON_HEADERS,
|
|
53
|
+
Authorization: `Bearer ${key}`,
|
|
54
|
+
...(payload.embeddings && { "X-Embedding-Model": payload.embeddings }),
|
|
55
|
+
},
|
|
56
|
+
body: JSON.stringify({
|
|
57
|
+
tier: payload.tier,
|
|
58
|
+
max_tokens: payload.max_tokens ?? 50000,
|
|
59
|
+
}),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function commitReflections(endpoint: string, key: string, payload: { batch_id: string; entries: Array<{ text: string; importance: number }>; embeddings?: string }): Promise<Response> {
|
|
64
|
+
return fetch(`${endpoint}/v1/memory/consolidate/commit`, {
|
|
65
|
+
method: "POST",
|
|
66
|
+
headers: {
|
|
67
|
+
...JSON_HEADERS,
|
|
68
|
+
Authorization: `Bearer ${key}`,
|
|
69
|
+
...(payload.embeddings && { "X-Embedding-Model": payload.embeddings }),
|
|
70
|
+
},
|
|
71
|
+
body: JSON.stringify({ batch_id: payload.batch_id, entries: payload.entries }),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function inspectMemory(endpoint: string, key: string, payload: { memory_id: string; embeddings?: string }): Promise<Response> {
|
|
76
|
+
return fetch(`${endpoint}/v1/memory/inspect`, {
|
|
77
|
+
method: "POST",
|
|
78
|
+
headers: {
|
|
79
|
+
...JSON_HEADERS,
|
|
80
|
+
Authorization: `Bearer ${key}`,
|
|
81
|
+
...(payload.embeddings && { "X-Embedding-Model": payload.embeddings }),
|
|
82
|
+
},
|
|
83
|
+
body: JSON.stringify({ memory_id: payload.memory_id }),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function deleteMemoryIds(endpoint: string, key: string, payload: { ids: string[]; embeddings?: string }): Promise<Response> {
|
|
88
|
+
return fetch(`${endpoint}/v1/memory/delete-ids`, {
|
|
89
|
+
method: "POST",
|
|
90
|
+
headers: {
|
|
91
|
+
...JSON_HEADERS,
|
|
92
|
+
Authorization: `Bearer ${key}`,
|
|
93
|
+
...(payload.embeddings && { "X-Embedding-Model": payload.embeddings }),
|
|
94
|
+
},
|
|
95
|
+
body: JSON.stringify({ ids: payload.ids }),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function dateSearch(endpoint: string, key: string, payload: { from: string; to?: string; query?: string; max_tokens?: number; embeddings?: string }): Promise<Response> {
|
|
100
|
+
return fetch(`${endpoint}/v1/memory/date-search`, {
|
|
101
|
+
method: "POST",
|
|
102
|
+
headers: {
|
|
103
|
+
...JSON_HEADERS,
|
|
104
|
+
Authorization: `Bearer ${key}`,
|
|
105
|
+
...(payload.embeddings && { "X-Embedding-Model": payload.embeddings }),
|
|
106
|
+
},
|
|
107
|
+
body: JSON.stringify({
|
|
108
|
+
from: payload.from,
|
|
109
|
+
...(payload.to !== undefined && { to: payload.to }),
|
|
110
|
+
...(payload.query !== undefined && { query: payload.query }),
|
|
111
|
+
...(payload.max_tokens !== undefined && { max_tokens: payload.max_tokens }),
|
|
112
|
+
}),
|
|
40
113
|
});
|
|
41
114
|
}
|
|
42
115
|
|
package/package.json
CHANGED