mr-memory 3.8.0 → 3.9.1
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 +314 -5
- package/index.ts +325 -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,15 @@ 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
|
+
function buildDebtNudgeLine(reflection) {
|
|
59
|
+
if (!reflection || !reflection.suggestion)
|
|
60
|
+
return null;
|
|
61
|
+
const rawTokens = reflection.unconsolidated?.raw?.est_tokens ?? 0;
|
|
62
|
+
const tier2Tokens = reflection.unconsolidated?.tier2?.est_tokens ?? 0;
|
|
63
|
+
const totalTokens = rawTokens + tier2Tokens;
|
|
64
|
+
const kTokens = Math.max(1, Math.round(totalTokens / 1000));
|
|
65
|
+
return `[MemoryRouter] Consolidation suggested: ${kTokens}k unconsolidated tokens. Use the memory_reflect tool to consolidate them now.`;
|
|
66
|
+
}
|
|
58
67
|
// ── memory_get helpers (copied from OpenClaw source — pure filesystem, no embeddings)
|
|
59
68
|
function isFileMissingError(err) {
|
|
60
69
|
return Boolean(err && typeof err === "object" && "code" in err && err.code === "ENOENT");
|
|
@@ -731,7 +740,11 @@ If setting up MemoryRouter for another agent:
|
|
|
731
740
|
api.logger.info?.(`memoryrouter: injected ${data.memories_found || 0} memories (${data.memory_tokens || 0} tokens, ${elapsed}ms) [llm_input]`);
|
|
732
741
|
if (elapsed > 2000 && data.metrics)
|
|
733
742
|
log(`memoryrouter: prepare metrics ${JSON.stringify(data.metrics)}`);
|
|
734
|
-
|
|
743
|
+
let contextText = data.context;
|
|
744
|
+
const nudge = buildDebtNudgeLine(data.reflection);
|
|
745
|
+
if (nudge)
|
|
746
|
+
contextText = `${contextText}\n\n${nudge}`;
|
|
747
|
+
const wrapped = wrapForInjection(contextText);
|
|
735
748
|
return { appendSystemContext: wrapped };
|
|
736
749
|
}
|
|
737
750
|
}
|
|
@@ -836,7 +849,11 @@ If setting up MemoryRouter for another agent:
|
|
|
836
849
|
api.logger.info?.(`memoryrouter: injected ${data.memories_found || 0} memories (${data.memory_tokens || 0} tokens, ${elapsed}ms)`);
|
|
837
850
|
if (elapsed > 2000 && data.metrics)
|
|
838
851
|
log(`memoryrouter: prepare metrics ${JSON.stringify(data.metrics)}`);
|
|
839
|
-
|
|
852
|
+
let contextText = data.context;
|
|
853
|
+
const nudge = buildDebtNudgeLine(data.reflection);
|
|
854
|
+
if (nudge)
|
|
855
|
+
contextText = `${contextText}\n\n${nudge}`;
|
|
856
|
+
const wrapped = wrapForInjection(contextText);
|
|
840
857
|
return { appendSystemContext: wrapped };
|
|
841
858
|
}
|
|
842
859
|
}
|
|
@@ -963,6 +980,17 @@ If setting up MemoryRouter for another agent:
|
|
|
963
980
|
query: { type: "string" },
|
|
964
981
|
maxResults: { type: "number" },
|
|
965
982
|
minScore: { type: "number" },
|
|
983
|
+
tiers: {
|
|
984
|
+
type: "array",
|
|
985
|
+
items: { type: "integer", enum: [1, 2, 3] },
|
|
986
|
+
description: "Restrict results to these memory tiers. 1 raw memories, 2 reflections, 3 high-level reflections. Omit to search all tiers blended.",
|
|
987
|
+
},
|
|
988
|
+
importance: {
|
|
989
|
+
type: "integer",
|
|
990
|
+
minimum: 1,
|
|
991
|
+
maximum: 10,
|
|
992
|
+
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.",
|
|
993
|
+
},
|
|
966
994
|
},
|
|
967
995
|
required: ["query"],
|
|
968
996
|
},
|
|
@@ -973,8 +1001,20 @@ If setting up MemoryRouter for another agent:
|
|
|
973
1001
|
if (!query)
|
|
974
1002
|
return jsonToolResult({ results: [], error: "query required" });
|
|
975
1003
|
const limit = typeof params.maxResults === "number" ? params.maxResults : 50;
|
|
1004
|
+
const tiers = Array.isArray(params.tiers)
|
|
1005
|
+
? params.tiers.filter((t) => typeof t === "number" && [1, 2, 3].includes(t))
|
|
1006
|
+
: undefined;
|
|
1007
|
+
const importance = typeof params.importance === "number" && params.importance >= 1 && params.importance <= 10
|
|
1008
|
+
? Math.floor(params.importance)
|
|
1009
|
+
: undefined;
|
|
976
1010
|
try {
|
|
977
|
-
const res = await searchMemory(endpoint, toolKey, {
|
|
1011
|
+
const res = await searchMemory(endpoint, toolKey, {
|
|
1012
|
+
query,
|
|
1013
|
+
limit,
|
|
1014
|
+
...(tiers && tiers.length > 0 && { tiers }),
|
|
1015
|
+
...(importance !== undefined && { importance }),
|
|
1016
|
+
embeddings,
|
|
1017
|
+
});
|
|
978
1018
|
if (!res.ok) {
|
|
979
1019
|
const errBody = await res.text().catch(() => "");
|
|
980
1020
|
return jsonToolResult({ results: [], error: `Search failed: HTTP ${res.status}`, details: errBody.slice(0, 200) });
|
|
@@ -988,6 +1028,8 @@ If setting up MemoryRouter for another agent:
|
|
|
988
1028
|
role: m.role,
|
|
989
1029
|
window: m.window,
|
|
990
1030
|
date: m.timestampHuman,
|
|
1031
|
+
...(m.tier !== undefined && { tier: m.tier }),
|
|
1032
|
+
...(m.importance !== undefined && m.importance !== null && { importance: m.importance }),
|
|
991
1033
|
}));
|
|
992
1034
|
return jsonToolResult({
|
|
993
1035
|
results,
|
|
@@ -1003,6 +1045,203 @@ If setting up MemoryRouter for another agent:
|
|
|
1003
1045
|
},
|
|
1004
1046
|
};
|
|
1005
1047
|
});
|
|
1048
|
+
// ==================================================================
|
|
1049
|
+
// Tools: memory_reflect + memory_commit (consolidation, two-tool shape)
|
|
1050
|
+
// Mirrors the MCP consolidate_memories / commit_reflections pair.
|
|
1051
|
+
// Invariants: source_ids are stripped, the model never sees row IDs,
|
|
1052
|
+
// and contract instructions are transmitted verbatim.
|
|
1053
|
+
// ==================================================================
|
|
1054
|
+
// memory_reflect: checkout wrapper (nothing is modified until memory_commit)
|
|
1055
|
+
api.registerTool((ctx) => {
|
|
1056
|
+
const toolKey = resolveKey(ctx);
|
|
1057
|
+
return {
|
|
1058
|
+
label: "Memory Reflect",
|
|
1059
|
+
name: "memory_reflect",
|
|
1060
|
+
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.",
|
|
1061
|
+
parameters: {
|
|
1062
|
+
type: "object",
|
|
1063
|
+
properties: {
|
|
1064
|
+
tier: {
|
|
1065
|
+
type: "integer",
|
|
1066
|
+
enum: [1, 2],
|
|
1067
|
+
description: "1 consolidates raw memories into reflections; 2 consolidates reflections into high-level reflections. Default 1.",
|
|
1068
|
+
},
|
|
1069
|
+
max_tokens: {
|
|
1070
|
+
type: "integer",
|
|
1071
|
+
minimum: 1000,
|
|
1072
|
+
description: "Token budget for the checked-out batch. Defaults to 50000. No connector-side cap.",
|
|
1073
|
+
},
|
|
1074
|
+
},
|
|
1075
|
+
required: [],
|
|
1076
|
+
},
|
|
1077
|
+
execute: async (_toolCallId, params) => {
|
|
1078
|
+
if (!toolKey)
|
|
1079
|
+
return jsonToolResult({ error: "No memory key configured for this agent" });
|
|
1080
|
+
const tier = params.tier === 2 ? 2 : 1;
|
|
1081
|
+
const maxTokens = typeof params.max_tokens === "number" && params.max_tokens >= 1000
|
|
1082
|
+
? Math.floor(params.max_tokens)
|
|
1083
|
+
: 50000;
|
|
1084
|
+
try {
|
|
1085
|
+
const res = await consolidateCheckout(endpoint, toolKey, {
|
|
1086
|
+
tier: tier,
|
|
1087
|
+
max_tokens: maxTokens,
|
|
1088
|
+
...(embeddings && { embeddings }),
|
|
1089
|
+
});
|
|
1090
|
+
if (!res.ok) {
|
|
1091
|
+
const errBody = await res.text().catch(() => "");
|
|
1092
|
+
return jsonToolResult({ error: `Checkout failed: HTTP ${res.status}`, details: errBody.slice(0, 200) });
|
|
1093
|
+
}
|
|
1094
|
+
const data = await res.json();
|
|
1095
|
+
if (!data.batch_id) {
|
|
1096
|
+
return jsonToolResult({ batch_id: null, count: 0, message: "Nothing to consolidate at this tier right now." });
|
|
1097
|
+
}
|
|
1098
|
+
// Contract instructions are server-authored and passed through verbatim.
|
|
1099
|
+
// If neither contract form is present, refuse; never author a local prompt.
|
|
1100
|
+
const instructions = data.reflection_contract?.instructions ?? data.tier_definitions?.instruction;
|
|
1101
|
+
if (!instructions) {
|
|
1102
|
+
return jsonToolResult({ error: "Checkout response carried no reflection contract. Cannot consolidate without server instructions." });
|
|
1103
|
+
}
|
|
1104
|
+
// source_ids stripped: the model never sees memory row IDs.
|
|
1105
|
+
return jsonToolResult({
|
|
1106
|
+
batch_id: data.batch_id,
|
|
1107
|
+
lease_expires_at: data.lease_expires_at ?? null,
|
|
1108
|
+
count: data.count ?? 0,
|
|
1109
|
+
est_tokens: data.est_tokens ?? 0,
|
|
1110
|
+
instructions,
|
|
1111
|
+
memories: (data.memories || []).map((m) => ({ text: m.text, date: m.date })),
|
|
1112
|
+
message: `When your entries are ready, call memory_commit with batch_id ${data.batch_id} before ${data.lease_expires_at ?? "the lease expires"}.`,
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
catch (err) {
|
|
1116
|
+
return jsonToolResult({ error: err instanceof Error ? err.message : String(err) });
|
|
1117
|
+
}
|
|
1118
|
+
},
|
|
1119
|
+
};
|
|
1120
|
+
});
|
|
1121
|
+
// memory_commit: commit wrapper (idempotent; 409 means the lease expired)
|
|
1122
|
+
api.registerTool((ctx) => {
|
|
1123
|
+
const toolKey = resolveKey(ctx);
|
|
1124
|
+
return {
|
|
1125
|
+
label: "Memory Commit",
|
|
1126
|
+
name: "memory_commit",
|
|
1127
|
+
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.",
|
|
1128
|
+
parameters: {
|
|
1129
|
+
type: "object",
|
|
1130
|
+
properties: {
|
|
1131
|
+
batch_id: { type: "string", description: "The batch_id returned by memory_reflect." },
|
|
1132
|
+
entries: {
|
|
1133
|
+
type: "array",
|
|
1134
|
+
items: {
|
|
1135
|
+
type: "object",
|
|
1136
|
+
properties: {
|
|
1137
|
+
text: { type: "string", description: "Standalone reflection prose, 1 to 8000 chars. No IDs or scores inline." },
|
|
1138
|
+
importance: { type: "integer", minimum: 1, maximum: 10 },
|
|
1139
|
+
},
|
|
1140
|
+
required: ["text", "importance"],
|
|
1141
|
+
additionalProperties: false,
|
|
1142
|
+
},
|
|
1143
|
+
minItems: 1,
|
|
1144
|
+
maxItems: 100,
|
|
1145
|
+
},
|
|
1146
|
+
},
|
|
1147
|
+
required: ["batch_id", "entries"],
|
|
1148
|
+
},
|
|
1149
|
+
execute: async (_toolCallId, params) => {
|
|
1150
|
+
if (!toolKey)
|
|
1151
|
+
return jsonToolResult({ error: "No memory key configured for this agent" });
|
|
1152
|
+
const batchId = typeof params.batch_id === "string" ? params.batch_id.trim() : "";
|
|
1153
|
+
if (!batchId)
|
|
1154
|
+
return jsonToolResult({ error: "batch_id required" });
|
|
1155
|
+
const rawEntries = Array.isArray(params.entries) ? params.entries : [];
|
|
1156
|
+
if (rawEntries.length < 1 || rawEntries.length > 100) {
|
|
1157
|
+
return jsonToolResult({ error: "entries must contain between 1 and 100 items" });
|
|
1158
|
+
}
|
|
1159
|
+
const entries = [];
|
|
1160
|
+
for (let i = 0; i < rawEntries.length; i++) {
|
|
1161
|
+
const e = rawEntries[i];
|
|
1162
|
+
if (!e || typeof e !== "object")
|
|
1163
|
+
return jsonToolResult({ error: `entry ${i + 1} is not an object` });
|
|
1164
|
+
const extraKeys = Object.keys(e).filter((k) => k !== "text" && k !== "importance");
|
|
1165
|
+
if (extraKeys.length > 0) {
|
|
1166
|
+
// Position-reference impossibility: entries are text plus importance only.
|
|
1167
|
+
return jsonToolResult({ error: `entry ${i + 1} has unsupported fields: ${extraKeys.join(", ")}. Entries are text plus importance only; coverage is always the whole batch.` });
|
|
1168
|
+
}
|
|
1169
|
+
const text = typeof e.text === "string" ? e.text : "";
|
|
1170
|
+
if (!text || text.length > 8000)
|
|
1171
|
+
return jsonToolResult({ error: `entry ${i + 1} text must be 1 to 8000 chars` });
|
|
1172
|
+
const importance = typeof e.importance === "number" && Number.isInteger(e.importance) && e.importance >= 1 && e.importance <= 10
|
|
1173
|
+
? e.importance
|
|
1174
|
+
: null;
|
|
1175
|
+
if (importance === null)
|
|
1176
|
+
return jsonToolResult({ error: `entry ${i + 1} importance must be an integer 1 to 10` });
|
|
1177
|
+
entries.push({ text, importance });
|
|
1178
|
+
}
|
|
1179
|
+
try {
|
|
1180
|
+
const res = await commitReflections(endpoint, toolKey, {
|
|
1181
|
+
batch_id: batchId,
|
|
1182
|
+
entries,
|
|
1183
|
+
...(embeddings && { embeddings }),
|
|
1184
|
+
});
|
|
1185
|
+
if (res.status === 409) {
|
|
1186
|
+
return jsonToolResult({ error: "lease expired, call memory_reflect again" });
|
|
1187
|
+
}
|
|
1188
|
+
if (!res.ok) {
|
|
1189
|
+
const errBody = await res.text().catch(() => "");
|
|
1190
|
+
return jsonToolResult({ error: `Commit failed: HTTP ${res.status}`, details: errBody.slice(0, 200) });
|
|
1191
|
+
}
|
|
1192
|
+
const data = await res.json();
|
|
1193
|
+
// Created UUIDs dropped: the model never sees memory row IDs.
|
|
1194
|
+
return jsonToolResult({
|
|
1195
|
+
committed: data.committed ?? false,
|
|
1196
|
+
already_committed: data.already_committed ?? false,
|
|
1197
|
+
entries_created: data.created?.length ?? 0,
|
|
1198
|
+
sources_consolidated: data.sources_consolidated ?? 0,
|
|
1199
|
+
debt: data.reflection?.suggestion ?? null,
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
catch (err) {
|
|
1203
|
+
return jsonToolResult({ error: err instanceof Error ? err.message : String(err) });
|
|
1204
|
+
}
|
|
1205
|
+
},
|
|
1206
|
+
};
|
|
1207
|
+
});
|
|
1208
|
+
// memory_inspect: read-only lineage drill-down (curation surface, ids allowed)
|
|
1209
|
+
api.registerTool((ctx) => {
|
|
1210
|
+
const toolKey = resolveKey(ctx);
|
|
1211
|
+
return {
|
|
1212
|
+
label: "Memory Inspect",
|
|
1213
|
+
name: "memory_inspect",
|
|
1214
|
+
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.",
|
|
1215
|
+
parameters: {
|
|
1216
|
+
type: "object",
|
|
1217
|
+
properties: {
|
|
1218
|
+
memory_id: { type: "string", description: "The memory id to inspect." },
|
|
1219
|
+
},
|
|
1220
|
+
required: ["memory_id"],
|
|
1221
|
+
},
|
|
1222
|
+
execute: async (_toolCallId, params) => {
|
|
1223
|
+
if (!toolKey)
|
|
1224
|
+
return jsonToolResult({ error: "No memory key configured for this agent" });
|
|
1225
|
+
const memoryId = typeof params.memory_id === "string" ? params.memory_id.trim() : "";
|
|
1226
|
+
if (!memoryId)
|
|
1227
|
+
return jsonToolResult({ error: "memory_id required" });
|
|
1228
|
+
try {
|
|
1229
|
+
const res = await inspectMemory(endpoint, toolKey, {
|
|
1230
|
+
memory_id: memoryId,
|
|
1231
|
+
...(embeddings && { embeddings }),
|
|
1232
|
+
});
|
|
1233
|
+
if (!res.ok) {
|
|
1234
|
+
const errBody = await res.text().catch(() => "");
|
|
1235
|
+
return jsonToolResult({ error: `Inspect failed: HTTP ${res.status}`, details: errBody.slice(0, 200) });
|
|
1236
|
+
}
|
|
1237
|
+
return jsonToolResult(await res.json());
|
|
1238
|
+
}
|
|
1239
|
+
catch (err) {
|
|
1240
|
+
return jsonToolResult({ error: err instanceof Error ? err.message : String(err) });
|
|
1241
|
+
}
|
|
1242
|
+
},
|
|
1243
|
+
};
|
|
1244
|
+
});
|
|
1006
1245
|
// memory_get — exact copy of OpenClaw's implementation (pure local file read)
|
|
1007
1246
|
api.registerTool((ctx) => {
|
|
1008
1247
|
const workspaceDir = ctx.workspaceDir || "";
|
|
@@ -1278,7 +1517,9 @@ If setting up MemoryRouter for another agent:
|
|
|
1278
1517
|
}
|
|
1279
1518
|
});
|
|
1280
1519
|
mr.command("delete")
|
|
1281
|
-
.description("Clear all memories from vault")
|
|
1520
|
+
.description("Clear all memories from vault, or delete specific memories with --ids")
|
|
1521
|
+
.option("--ids <ids>", "Comma-separated memory ids to delete (instead of the whole vault)")
|
|
1522
|
+
.option("--yes", "Confirm deletion (required with --ids)")
|
|
1282
1523
|
.option("--key <key>", "Override memory key (for subagent vaults)")
|
|
1283
1524
|
.action(async (opts) => {
|
|
1284
1525
|
const effectiveKey = opts.key || memoryKey;
|
|
@@ -1286,6 +1527,34 @@ If setting up MemoryRouter for another agent:
|
|
|
1286
1527
|
console.error("Not configured. Run: openclaw mr <key> or pass --key <key>");
|
|
1287
1528
|
return;
|
|
1288
1529
|
}
|
|
1530
|
+
if (opts.ids) {
|
|
1531
|
+
const ids = opts.ids.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1532
|
+
if (ids.length === 0) {
|
|
1533
|
+
console.error("No ids provided.");
|
|
1534
|
+
return;
|
|
1535
|
+
}
|
|
1536
|
+
console.log(`The following ${ids.length} memor${ids.length === 1 ? "y" : "ies"} will be PERMANENTLY deleted:`);
|
|
1537
|
+
for (const id of ids)
|
|
1538
|
+
console.log(` • ${id}`);
|
|
1539
|
+
if (!opts.yes) {
|
|
1540
|
+
console.error("\nRefusing to delete without --yes. Re-run with --yes to confirm.");
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
try {
|
|
1544
|
+
const res = await deleteMemoryIds(endpoint, effectiveKey, { ids, ...(embeddings && { embeddings }) });
|
|
1545
|
+
if (!res.ok) {
|
|
1546
|
+
const errBody = await res.text().catch(() => "");
|
|
1547
|
+
console.error(`Delete failed: HTTP ${res.status}${errBody ? `: ${errBody.slice(0, 200)}` : ""}`);
|
|
1548
|
+
return;
|
|
1549
|
+
}
|
|
1550
|
+
const data = (await res.json());
|
|
1551
|
+
console.log(`✓ Deleted ${data.deleted ?? 0} of ${data.requested ?? ids.length} (not found: ${data.not_found ?? 0})`);
|
|
1552
|
+
}
|
|
1553
|
+
catch (err) {
|
|
1554
|
+
console.error(`Failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1555
|
+
}
|
|
1556
|
+
return;
|
|
1557
|
+
}
|
|
1289
1558
|
try {
|
|
1290
1559
|
const res = await deleteMemories(endpoint, effectiveKey, embeddings);
|
|
1291
1560
|
const data = (await res.json());
|
|
@@ -1296,6 +1565,46 @@ If setting up MemoryRouter for another agent:
|
|
|
1296
1565
|
console.error(`Failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1297
1566
|
}
|
|
1298
1567
|
});
|
|
1568
|
+
mr.command("reflect")
|
|
1569
|
+
.description("Show reflection debt status (consolidation runs via the agent's memory_reflect tool)")
|
|
1570
|
+
.option("--key <key>", "Override memory key (for subagent vaults)")
|
|
1571
|
+
.action(async (opts) => {
|
|
1572
|
+
const effectiveKey = opts.key || memoryKey;
|
|
1573
|
+
if (!effectiveKey) {
|
|
1574
|
+
console.error("Not configured. Run: openclaw mr <key> or pass --key <key>");
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1577
|
+
try {
|
|
1578
|
+
// Debt probe: an empty-window date-search returns the reflection
|
|
1579
|
+
// debt block with zero results and zero retrieval billing.
|
|
1580
|
+
const from = new Date(Date.now() - 1000).toISOString();
|
|
1581
|
+
const res = await dateSearch(endpoint, effectiveKey, { from, max_tokens: 1000, ...(embeddings && { embeddings }) });
|
|
1582
|
+
if (!res.ok) {
|
|
1583
|
+
const errBody = await res.text().catch(() => "");
|
|
1584
|
+
console.error(`Debt check failed: HTTP ${res.status}${errBody ? `: ${errBody.slice(0, 200)}` : ""}`);
|
|
1585
|
+
return;
|
|
1586
|
+
}
|
|
1587
|
+
const data = (await res.json());
|
|
1588
|
+
const debt = data.reflection;
|
|
1589
|
+
console.log("Reflection Debt");
|
|
1590
|
+
console.log("───────────────────────────");
|
|
1591
|
+
const raw = debt?.unconsolidated?.raw;
|
|
1592
|
+
const tier2 = debt?.unconsolidated?.tier2;
|
|
1593
|
+
console.log(`Raw unconsolidated: ${raw?.units ?? 0} memories (~${raw?.est_tokens ?? 0} tokens)`);
|
|
1594
|
+
console.log(`Tier 2 unconsolidated: ${tier2?.count ?? 0} reflections (~${tier2?.est_tokens ?? 0} tokens)`);
|
|
1595
|
+
console.log(`Threshold: ${debt?.threshold_tokens ?? 0} tokens`);
|
|
1596
|
+
if (debt?.suggestion) {
|
|
1597
|
+
console.log(`\nStatus: ${debt.suggestion}`);
|
|
1598
|
+
console.log("Consolidation runs through the agent. Ask your agent to consolidate memories (it uses the memory_reflect and memory_commit tools).");
|
|
1599
|
+
}
|
|
1600
|
+
else {
|
|
1601
|
+
console.log("\nStatus: nothing to consolidate right now.");
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
catch (err) {
|
|
1605
|
+
console.error(`Failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1606
|
+
}
|
|
1607
|
+
});
|
|
1299
1608
|
}, { commands: ["mr"] });
|
|
1300
1609
|
// ==================================================================
|
|
1301
1610
|
// 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,25 @@ 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 (one line, appended to prepare injections whenever the server suggests it)
|
|
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
|
+
function buildDebtNudgeLine(reflection: ReflectionDebt | undefined): string | null {
|
|
79
|
+
if (!reflection || !reflection.suggestion) return null;
|
|
80
|
+
const rawTokens = reflection.unconsolidated?.raw?.est_tokens ?? 0;
|
|
81
|
+
const tier2Tokens = reflection.unconsolidated?.tier2?.est_tokens ?? 0;
|
|
82
|
+
const totalTokens = rawTokens + tier2Tokens;
|
|
83
|
+
const kTokens = Math.max(1, Math.round(totalTokens / 1000));
|
|
84
|
+
return `[MemoryRouter] Consolidation suggested: ${kTokens}k unconsolidated tokens. Use the memory_reflect tool to consolidate them now.`;
|
|
85
|
+
}
|
|
86
|
+
|
|
68
87
|
// ── memory_get helpers (copied from OpenClaw source — pure filesystem, no embeddings)
|
|
69
88
|
function isFileMissingError(err: unknown): boolean {
|
|
70
89
|
return Boolean(err && typeof err === "object" && "code" in err && (err as any).code === "ENOENT");
|
|
@@ -768,13 +787,17 @@ If setting up MemoryRouter for another agent:
|
|
|
768
787
|
tokens_billed?: number;
|
|
769
788
|
memory_tokens?: number;
|
|
770
789
|
metrics?: Record<string, unknown>;
|
|
790
|
+
reflection?: ReflectionDebt;
|
|
771
791
|
};
|
|
772
792
|
|
|
773
793
|
if (data.context) {
|
|
774
794
|
const elapsed = Date.now() - startMs;
|
|
775
795
|
api.logger.info?.(`memoryrouter: injected ${data.memories_found || 0} memories (${data.memory_tokens || 0} tokens, ${elapsed}ms) [llm_input]`);
|
|
776
796
|
if (elapsed > 2000 && data.metrics) log(`memoryrouter: prepare metrics ${JSON.stringify(data.metrics)}`);
|
|
777
|
-
|
|
797
|
+
let contextText = data.context;
|
|
798
|
+
const nudge = buildDebtNudgeLine(data.reflection);
|
|
799
|
+
if (nudge) contextText = `${contextText}\n\n${nudge}`;
|
|
800
|
+
const wrapped = wrapForInjection(contextText);
|
|
778
801
|
return { appendSystemContext: wrapped };
|
|
779
802
|
}
|
|
780
803
|
} catch {
|
|
@@ -885,13 +908,17 @@ If setting up MemoryRouter for another agent:
|
|
|
885
908
|
tokens_billed?: number;
|
|
886
909
|
memory_tokens?: number;
|
|
887
910
|
metrics?: Record<string, unknown>;
|
|
911
|
+
reflection?: ReflectionDebt;
|
|
888
912
|
};
|
|
889
913
|
|
|
890
914
|
if (data.context) {
|
|
891
915
|
const elapsed = Date.now() - startMs;
|
|
892
916
|
api.logger.info?.(`memoryrouter: injected ${data.memories_found || 0} memories (${data.memory_tokens || 0} tokens, ${elapsed}ms)`);
|
|
893
917
|
if (elapsed > 2000 && data.metrics) log(`memoryrouter: prepare metrics ${JSON.stringify(data.metrics)}`);
|
|
894
|
-
|
|
918
|
+
let contextText = data.context;
|
|
919
|
+
const nudge = buildDebtNudgeLine(data.reflection);
|
|
920
|
+
if (nudge) contextText = `${contextText}\n\n${nudge}`;
|
|
921
|
+
const wrapped = wrapForInjection(contextText);
|
|
895
922
|
return { appendSystemContext: wrapped };
|
|
896
923
|
}
|
|
897
924
|
} catch (err) {
|
|
@@ -1025,6 +1052,17 @@ If setting up MemoryRouter for another agent:
|
|
|
1025
1052
|
query: { type: "string" },
|
|
1026
1053
|
maxResults: { type: "number" },
|
|
1027
1054
|
minScore: { type: "number" },
|
|
1055
|
+
tiers: {
|
|
1056
|
+
type: "array",
|
|
1057
|
+
items: { type: "integer", enum: [1, 2, 3] },
|
|
1058
|
+
description: "Restrict results to these memory tiers. 1 raw memories, 2 reflections, 3 high-level reflections. Omit to search all tiers blended.",
|
|
1059
|
+
},
|
|
1060
|
+
importance: {
|
|
1061
|
+
type: "integer",
|
|
1062
|
+
minimum: 1,
|
|
1063
|
+
maximum: 10,
|
|
1064
|
+
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.",
|
|
1065
|
+
},
|
|
1028
1066
|
},
|
|
1029
1067
|
required: ["query"],
|
|
1030
1068
|
} as any,
|
|
@@ -1033,14 +1071,26 @@ If setting up MemoryRouter for another agent:
|
|
|
1033
1071
|
const query = typeof params.query === "string" ? params.query.trim() : "";
|
|
1034
1072
|
if (!query) return jsonToolResult({ results: [], error: "query required" });
|
|
1035
1073
|
const limit = typeof params.maxResults === "number" ? params.maxResults : 50;
|
|
1074
|
+
const tiers = Array.isArray(params.tiers)
|
|
1075
|
+
? (params.tiers as unknown[]).filter((t): t is number => typeof t === "number" && [1, 2, 3].includes(t))
|
|
1076
|
+
: undefined;
|
|
1077
|
+
const importance = typeof params.importance === "number" && params.importance >= 1 && params.importance <= 10
|
|
1078
|
+
? Math.floor(params.importance)
|
|
1079
|
+
: undefined;
|
|
1036
1080
|
try {
|
|
1037
|
-
const res = await searchMemory(endpoint, toolKey, {
|
|
1081
|
+
const res = await searchMemory(endpoint, toolKey, {
|
|
1082
|
+
query,
|
|
1083
|
+
limit,
|
|
1084
|
+
...(tiers && tiers.length > 0 && { tiers }),
|
|
1085
|
+
...(importance !== undefined && { importance }),
|
|
1086
|
+
embeddings,
|
|
1087
|
+
});
|
|
1038
1088
|
if (!res.ok) {
|
|
1039
1089
|
const errBody = await res.text().catch(() => "");
|
|
1040
1090
|
return jsonToolResult({ results: [], error: `Search failed: HTTP ${res.status}`, details: errBody.slice(0, 200) });
|
|
1041
1091
|
}
|
|
1042
1092
|
const data = await res.json() as {
|
|
1043
|
-
memories?: Array<{ content: string; score: number; role: string; window: string; timestampHuman: string }>;
|
|
1093
|
+
memories?: Array<{ content: string; score: number; role: string; window: string; timestampHuman: string; tier?: number; importance?: number }>;
|
|
1044
1094
|
totalMemories?: number;
|
|
1045
1095
|
tokenCount?: number;
|
|
1046
1096
|
};
|
|
@@ -1052,6 +1102,8 @@ If setting up MemoryRouter for another agent:
|
|
|
1052
1102
|
role: m.role,
|
|
1053
1103
|
window: m.window,
|
|
1054
1104
|
date: m.timestampHuman,
|
|
1105
|
+
...(m.tier !== undefined && { tier: m.tier }),
|
|
1106
|
+
...(m.importance !== undefined && m.importance !== null && { importance: m.importance }),
|
|
1055
1107
|
}));
|
|
1056
1108
|
return jsonToolResult({
|
|
1057
1109
|
results,
|
|
@@ -1067,6 +1119,211 @@ If setting up MemoryRouter for another agent:
|
|
|
1067
1119
|
};
|
|
1068
1120
|
});
|
|
1069
1121
|
|
|
1122
|
+
// ==================================================================
|
|
1123
|
+
// Tools: memory_reflect + memory_commit (consolidation, two-tool shape)
|
|
1124
|
+
// Mirrors the MCP consolidate_memories / commit_reflections pair.
|
|
1125
|
+
// Invariants: source_ids are stripped, the model never sees row IDs,
|
|
1126
|
+
// and contract instructions are transmitted verbatim.
|
|
1127
|
+
// ==================================================================
|
|
1128
|
+
|
|
1129
|
+
// memory_reflect: checkout wrapper (nothing is modified until memory_commit)
|
|
1130
|
+
api.registerTool((ctx) => {
|
|
1131
|
+
const toolKey = resolveKey(ctx);
|
|
1132
|
+
return {
|
|
1133
|
+
label: "Memory Reflect",
|
|
1134
|
+
name: "memory_reflect",
|
|
1135
|
+
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.",
|
|
1136
|
+
parameters: {
|
|
1137
|
+
type: "object",
|
|
1138
|
+
properties: {
|
|
1139
|
+
tier: {
|
|
1140
|
+
type: "integer",
|
|
1141
|
+
enum: [1, 2],
|
|
1142
|
+
description: "1 consolidates raw memories into reflections; 2 consolidates reflections into high-level reflections. Default 1.",
|
|
1143
|
+
},
|
|
1144
|
+
max_tokens: {
|
|
1145
|
+
type: "integer",
|
|
1146
|
+
minimum: 1000,
|
|
1147
|
+
description: "Token budget for the checked-out batch. Defaults to 50000. No connector-side cap.",
|
|
1148
|
+
},
|
|
1149
|
+
},
|
|
1150
|
+
required: [],
|
|
1151
|
+
} as any,
|
|
1152
|
+
execute: async (_toolCallId: string, params: Record<string, unknown>) => {
|
|
1153
|
+
if (!toolKey) return jsonToolResult({ error: "No memory key configured for this agent" });
|
|
1154
|
+
const tier = params.tier === 2 ? 2 : 1;
|
|
1155
|
+
const maxTokens = typeof params.max_tokens === "number" && params.max_tokens >= 1000
|
|
1156
|
+
? Math.floor(params.max_tokens)
|
|
1157
|
+
: 50000;
|
|
1158
|
+
try {
|
|
1159
|
+
const res = await consolidateCheckout(endpoint, toolKey, {
|
|
1160
|
+
tier: tier as 1 | 2,
|
|
1161
|
+
max_tokens: maxTokens,
|
|
1162
|
+
...(embeddings && { embeddings }),
|
|
1163
|
+
});
|
|
1164
|
+
if (!res.ok) {
|
|
1165
|
+
const errBody = await res.text().catch(() => "");
|
|
1166
|
+
return jsonToolResult({ error: `Checkout failed: HTTP ${res.status}`, details: errBody.slice(0, 200) });
|
|
1167
|
+
}
|
|
1168
|
+
const data = await res.json() as {
|
|
1169
|
+
batch_id?: string | null;
|
|
1170
|
+
tier?: number;
|
|
1171
|
+
count?: number;
|
|
1172
|
+
est_tokens?: number;
|
|
1173
|
+
lease_expires_at?: string | null;
|
|
1174
|
+
memories?: Array<{ text: string; date: string }>;
|
|
1175
|
+
reflection_contract?: { version?: number; instructions?: string };
|
|
1176
|
+
tier_definitions?: { target_tier?: number; instruction?: string };
|
|
1177
|
+
};
|
|
1178
|
+
if (!data.batch_id) {
|
|
1179
|
+
return jsonToolResult({ batch_id: null, count: 0, message: "Nothing to consolidate at this tier right now." });
|
|
1180
|
+
}
|
|
1181
|
+
// Contract instructions are server-authored and passed through verbatim.
|
|
1182
|
+
// If neither contract form is present, refuse; never author a local prompt.
|
|
1183
|
+
const instructions = data.reflection_contract?.instructions ?? data.tier_definitions?.instruction;
|
|
1184
|
+
if (!instructions) {
|
|
1185
|
+
return jsonToolResult({ error: "Checkout response carried no reflection contract. Cannot consolidate without server instructions." });
|
|
1186
|
+
}
|
|
1187
|
+
// source_ids stripped: the model never sees memory row IDs.
|
|
1188
|
+
return jsonToolResult({
|
|
1189
|
+
batch_id: data.batch_id,
|
|
1190
|
+
lease_expires_at: data.lease_expires_at ?? null,
|
|
1191
|
+
count: data.count ?? 0,
|
|
1192
|
+
est_tokens: data.est_tokens ?? 0,
|
|
1193
|
+
instructions,
|
|
1194
|
+
memories: (data.memories || []).map((m) => ({ text: m.text, date: m.date })),
|
|
1195
|
+
message: `When your entries are ready, call memory_commit with batch_id ${data.batch_id} before ${data.lease_expires_at ?? "the lease expires"}.`,
|
|
1196
|
+
});
|
|
1197
|
+
} catch (err) {
|
|
1198
|
+
return jsonToolResult({ error: err instanceof Error ? err.message : String(err) });
|
|
1199
|
+
}
|
|
1200
|
+
},
|
|
1201
|
+
};
|
|
1202
|
+
});
|
|
1203
|
+
|
|
1204
|
+
// memory_commit: commit wrapper (idempotent; 409 means the lease expired)
|
|
1205
|
+
api.registerTool((ctx) => {
|
|
1206
|
+
const toolKey = resolveKey(ctx);
|
|
1207
|
+
return {
|
|
1208
|
+
label: "Memory Commit",
|
|
1209
|
+
name: "memory_commit",
|
|
1210
|
+
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.",
|
|
1211
|
+
parameters: {
|
|
1212
|
+
type: "object",
|
|
1213
|
+
properties: {
|
|
1214
|
+
batch_id: { type: "string", description: "The batch_id returned by memory_reflect." },
|
|
1215
|
+
entries: {
|
|
1216
|
+
type: "array",
|
|
1217
|
+
items: {
|
|
1218
|
+
type: "object",
|
|
1219
|
+
properties: {
|
|
1220
|
+
text: { type: "string", description: "Standalone reflection prose, 1 to 8000 chars. No IDs or scores inline." },
|
|
1221
|
+
importance: { type: "integer", minimum: 1, maximum: 10 },
|
|
1222
|
+
},
|
|
1223
|
+
required: ["text", "importance"],
|
|
1224
|
+
additionalProperties: false,
|
|
1225
|
+
},
|
|
1226
|
+
minItems: 1,
|
|
1227
|
+
maxItems: 100,
|
|
1228
|
+
},
|
|
1229
|
+
},
|
|
1230
|
+
required: ["batch_id", "entries"],
|
|
1231
|
+
} as any,
|
|
1232
|
+
execute: async (_toolCallId: string, params: Record<string, unknown>) => {
|
|
1233
|
+
if (!toolKey) return jsonToolResult({ error: "No memory key configured for this agent" });
|
|
1234
|
+
const batchId = typeof params.batch_id === "string" ? params.batch_id.trim() : "";
|
|
1235
|
+
if (!batchId) return jsonToolResult({ error: "batch_id required" });
|
|
1236
|
+
const rawEntries = Array.isArray(params.entries) ? params.entries as Array<Record<string, unknown>> : [];
|
|
1237
|
+
if (rawEntries.length < 1 || rawEntries.length > 100) {
|
|
1238
|
+
return jsonToolResult({ error: "entries must contain between 1 and 100 items" });
|
|
1239
|
+
}
|
|
1240
|
+
const entries: Array<{ text: string; importance: number }> = [];
|
|
1241
|
+
for (let i = 0; i < rawEntries.length; i++) {
|
|
1242
|
+
const e = rawEntries[i];
|
|
1243
|
+
if (!e || typeof e !== "object") return jsonToolResult({ error: `entry ${i + 1} is not an object` });
|
|
1244
|
+
const extraKeys = Object.keys(e).filter((k) => k !== "text" && k !== "importance");
|
|
1245
|
+
if (extraKeys.length > 0) {
|
|
1246
|
+
// Position-reference impossibility: entries are text plus importance only.
|
|
1247
|
+
return jsonToolResult({ error: `entry ${i + 1} has unsupported fields: ${extraKeys.join(", ")}. Entries are text plus importance only; coverage is always the whole batch.` });
|
|
1248
|
+
}
|
|
1249
|
+
const text = typeof e.text === "string" ? e.text : "";
|
|
1250
|
+
if (!text || text.length > 8000) return jsonToolResult({ error: `entry ${i + 1} text must be 1 to 8000 chars` });
|
|
1251
|
+
const importance = typeof e.importance === "number" && Number.isInteger(e.importance) && e.importance >= 1 && e.importance <= 10
|
|
1252
|
+
? e.importance
|
|
1253
|
+
: null;
|
|
1254
|
+
if (importance === null) return jsonToolResult({ error: `entry ${i + 1} importance must be an integer 1 to 10` });
|
|
1255
|
+
entries.push({ text, importance });
|
|
1256
|
+
}
|
|
1257
|
+
try {
|
|
1258
|
+
const res = await commitReflections(endpoint, toolKey, {
|
|
1259
|
+
batch_id: batchId,
|
|
1260
|
+
entries,
|
|
1261
|
+
...(embeddings && { embeddings }),
|
|
1262
|
+
});
|
|
1263
|
+
if (res.status === 409) {
|
|
1264
|
+
return jsonToolResult({ error: "lease expired, call memory_reflect again" });
|
|
1265
|
+
}
|
|
1266
|
+
if (!res.ok) {
|
|
1267
|
+
const errBody = await res.text().catch(() => "");
|
|
1268
|
+
return jsonToolResult({ error: `Commit failed: HTTP ${res.status}`, details: errBody.slice(0, 200) });
|
|
1269
|
+
}
|
|
1270
|
+
const data = await res.json() as {
|
|
1271
|
+
committed?: boolean;
|
|
1272
|
+
already_committed?: boolean;
|
|
1273
|
+
created?: Array<{ id: string; importance: number }>;
|
|
1274
|
+
sources_consolidated?: number;
|
|
1275
|
+
reflection?: ReflectionDebt;
|
|
1276
|
+
};
|
|
1277
|
+
// Created UUIDs dropped: the model never sees memory row IDs.
|
|
1278
|
+
return jsonToolResult({
|
|
1279
|
+
committed: data.committed ?? false,
|
|
1280
|
+
already_committed: data.already_committed ?? false,
|
|
1281
|
+
entries_created: data.created?.length ?? 0,
|
|
1282
|
+
sources_consolidated: data.sources_consolidated ?? 0,
|
|
1283
|
+
debt: data.reflection?.suggestion ?? null,
|
|
1284
|
+
});
|
|
1285
|
+
} catch (err) {
|
|
1286
|
+
return jsonToolResult({ error: err instanceof Error ? err.message : String(err) });
|
|
1287
|
+
}
|
|
1288
|
+
},
|
|
1289
|
+
};
|
|
1290
|
+
});
|
|
1291
|
+
|
|
1292
|
+
// memory_inspect: read-only lineage drill-down (curation surface, ids allowed)
|
|
1293
|
+
api.registerTool((ctx) => {
|
|
1294
|
+
const toolKey = resolveKey(ctx);
|
|
1295
|
+
return {
|
|
1296
|
+
label: "Memory Inspect",
|
|
1297
|
+
name: "memory_inspect",
|
|
1298
|
+
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.",
|
|
1299
|
+
parameters: {
|
|
1300
|
+
type: "object",
|
|
1301
|
+
properties: {
|
|
1302
|
+
memory_id: { type: "string", description: "The memory id to inspect." },
|
|
1303
|
+
},
|
|
1304
|
+
required: ["memory_id"],
|
|
1305
|
+
} as any,
|
|
1306
|
+
execute: async (_toolCallId: string, params: Record<string, unknown>) => {
|
|
1307
|
+
if (!toolKey) return jsonToolResult({ error: "No memory key configured for this agent" });
|
|
1308
|
+
const memoryId = typeof params.memory_id === "string" ? params.memory_id.trim() : "";
|
|
1309
|
+
if (!memoryId) return jsonToolResult({ error: "memory_id required" });
|
|
1310
|
+
try {
|
|
1311
|
+
const res = await inspectMemory(endpoint, toolKey, {
|
|
1312
|
+
memory_id: memoryId,
|
|
1313
|
+
...(embeddings && { embeddings }),
|
|
1314
|
+
});
|
|
1315
|
+
if (!res.ok) {
|
|
1316
|
+
const errBody = await res.text().catch(() => "");
|
|
1317
|
+
return jsonToolResult({ error: `Inspect failed: HTTP ${res.status}`, details: errBody.slice(0, 200) });
|
|
1318
|
+
}
|
|
1319
|
+
return jsonToolResult(await res.json());
|
|
1320
|
+
} catch (err) {
|
|
1321
|
+
return jsonToolResult({ error: err instanceof Error ? err.message : String(err) });
|
|
1322
|
+
}
|
|
1323
|
+
},
|
|
1324
|
+
};
|
|
1325
|
+
});
|
|
1326
|
+
|
|
1070
1327
|
// memory_get — exact copy of OpenClaw's implementation (pure local file read)
|
|
1071
1328
|
api.registerTool((ctx) => {
|
|
1072
1329
|
const workspaceDir = ctx.workspaceDir || "";
|
|
@@ -1353,11 +1610,36 @@ If setting up MemoryRouter for another agent:
|
|
|
1353
1610
|
});
|
|
1354
1611
|
|
|
1355
1612
|
mr.command("delete")
|
|
1356
|
-
.description("Clear all memories from vault")
|
|
1613
|
+
.description("Clear all memories from vault, or delete specific memories with --ids")
|
|
1614
|
+
.option("--ids <ids>", "Comma-separated memory ids to delete (instead of the whole vault)")
|
|
1615
|
+
.option("--yes", "Confirm deletion (required with --ids)")
|
|
1357
1616
|
.option("--key <key>", "Override memory key (for subagent vaults)")
|
|
1358
|
-
.action(async (opts: { key?: string }) => {
|
|
1617
|
+
.action(async (opts: { ids?: string; yes?: boolean; key?: string }) => {
|
|
1359
1618
|
const effectiveKey = opts.key || memoryKey;
|
|
1360
1619
|
if (!effectiveKey) { console.error("Not configured. Run: openclaw mr <key> or pass --key <key>"); return; }
|
|
1620
|
+
if (opts.ids) {
|
|
1621
|
+
const ids = opts.ids.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1622
|
+
if (ids.length === 0) { console.error("No ids provided."); return; }
|
|
1623
|
+
console.log(`The following ${ids.length} memor${ids.length === 1 ? "y" : "ies"} will be PERMANENTLY deleted:`);
|
|
1624
|
+
for (const id of ids) console.log(` • ${id}`);
|
|
1625
|
+
if (!opts.yes) {
|
|
1626
|
+
console.error("\nRefusing to delete without --yes. Re-run with --yes to confirm.");
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
try {
|
|
1630
|
+
const res = await deleteMemoryIds(endpoint, effectiveKey, { ids, ...(embeddings && { embeddings }) });
|
|
1631
|
+
if (!res.ok) {
|
|
1632
|
+
const errBody = await res.text().catch(() => "");
|
|
1633
|
+
console.error(`Delete failed: HTTP ${res.status}${errBody ? `: ${errBody.slice(0, 200)}` : ""}`);
|
|
1634
|
+
return;
|
|
1635
|
+
}
|
|
1636
|
+
const data = (await res.json()) as { requested?: number; deleted?: number; not_found?: number };
|
|
1637
|
+
console.log(`✓ Deleted ${data.deleted ?? 0} of ${data.requested ?? ids.length} (not found: ${data.not_found ?? 0})`);
|
|
1638
|
+
} catch (err) {
|
|
1639
|
+
console.error(`Failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1640
|
+
}
|
|
1641
|
+
return;
|
|
1642
|
+
}
|
|
1361
1643
|
try {
|
|
1362
1644
|
const res = await deleteMemories(endpoint, effectiveKey, embeddings);
|
|
1363
1645
|
const data = (await res.json()) as { message?: string };
|
|
@@ -1367,6 +1649,42 @@ If setting up MemoryRouter for another agent:
|
|
|
1367
1649
|
console.error(`Failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1368
1650
|
}
|
|
1369
1651
|
});
|
|
1652
|
+
|
|
1653
|
+
mr.command("reflect")
|
|
1654
|
+
.description("Show reflection debt status (consolidation runs via the agent's memory_reflect tool)")
|
|
1655
|
+
.option("--key <key>", "Override memory key (for subagent vaults)")
|
|
1656
|
+
.action(async (opts: { key?: string }) => {
|
|
1657
|
+
const effectiveKey = opts.key || memoryKey;
|
|
1658
|
+
if (!effectiveKey) { console.error("Not configured. Run: openclaw mr <key> or pass --key <key>"); return; }
|
|
1659
|
+
try {
|
|
1660
|
+
// Debt probe: an empty-window date-search returns the reflection
|
|
1661
|
+
// debt block with zero results and zero retrieval billing.
|
|
1662
|
+
const from = new Date(Date.now() - 1000).toISOString();
|
|
1663
|
+
const res = await dateSearch(endpoint, effectiveKey, { from, max_tokens: 1000, ...(embeddings && { embeddings }) });
|
|
1664
|
+
if (!res.ok) {
|
|
1665
|
+
const errBody = await res.text().catch(() => "");
|
|
1666
|
+
console.error(`Debt check failed: HTTP ${res.status}${errBody ? `: ${errBody.slice(0, 200)}` : ""}`);
|
|
1667
|
+
return;
|
|
1668
|
+
}
|
|
1669
|
+
const data = (await res.json()) as { reflection?: ReflectionDebt };
|
|
1670
|
+
const debt = data.reflection;
|
|
1671
|
+
console.log("Reflection Debt");
|
|
1672
|
+
console.log("───────────────────────────");
|
|
1673
|
+
const raw = debt?.unconsolidated?.raw;
|
|
1674
|
+
const tier2 = debt?.unconsolidated?.tier2;
|
|
1675
|
+
console.log(`Raw unconsolidated: ${raw?.units ?? 0} memories (~${raw?.est_tokens ?? 0} tokens)`);
|
|
1676
|
+
console.log(`Tier 2 unconsolidated: ${tier2?.count ?? 0} reflections (~${tier2?.est_tokens ?? 0} tokens)`);
|
|
1677
|
+
console.log(`Threshold: ${debt?.threshold_tokens ?? 0} tokens`);
|
|
1678
|
+
if (debt?.suggestion) {
|
|
1679
|
+
console.log(`\nStatus: ${debt.suggestion}`);
|
|
1680
|
+
console.log("Consolidation runs through the agent. Ask your agent to consolidate memories (it uses the memory_reflect and memory_commit tools).");
|
|
1681
|
+
} else {
|
|
1682
|
+
console.log("\nStatus: nothing to consolidate right now.");
|
|
1683
|
+
}
|
|
1684
|
+
} catch (err) {
|
|
1685
|
+
console.error(`Failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1686
|
+
}
|
|
1687
|
+
});
|
|
1370
1688
|
},
|
|
1371
1689
|
{ commands: ["mr"] },
|
|
1372
1690
|
);
|
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