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 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
- const wrapped = wrapForInjection(data.context);
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
- const wrapped = wrapForInjection(data.context);
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, { query, limit, embeddings });
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
- const wrapped = wrapForInjection(data.context);
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
- const wrapped = wrapForInjection(data.context);
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, { query, limit, embeddings });
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
  );
@@ -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({ query: payload.query, limit: payload.limit }),
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",
@@ -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({ query: payload.query, limit: payload.limit }),
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mr-memory",
3
- "version": "3.8.0",
3
+ "version": "3.9.0",
4
4
  "description": "Persistent memory for OpenClaw, powered by MemoryRouter. Relay-based AI agent memory across sessions, compaction, and model changes.",
5
5
  "type": "module",
6
6
  "files": [