mr-memory 3.9.1 → 3.11.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.ts CHANGED
@@ -10,6 +10,7 @@
10
10
 
11
11
  import { readFile, readdir, stat, lstat, writeFile, copyFile, mkdir } from "node:fs/promises";
12
12
  import { commitReflections, consolidateCheckout, dateSearch, deleteMemories, deleteMemoryIds, getMemoryStats, ingestMemory, inspectMemory, prepareMemory, searchMemory, warmupMemory } from "./memoryrouter-api.js";
13
+ import { isAutoConsolidationSession, maybeTriggerAutoConsolidate } from "./auto-consolidate.js";
13
14
  import { join, resolve, relative, isAbsolute, sep, dirname } from "node:path";
14
15
  import path from "node:path";
15
16
  import os from "node:os";
@@ -84,6 +85,15 @@ function buildDebtNudgeLine(reflection: ReflectionDebt | undefined): string | nu
84
85
  return `[MemoryRouter] Consolidation suggested: ${kTokens}k unconsolidated tokens. Use the memory_reflect tool to consolidate them now.`;
85
86
  }
86
87
 
88
+ // ── Historical import nudge (server-owned import.suggested block; additive,
89
+ // no rate limit, clears server-side on import completion or dismiss)
90
+ type ImportSuggestedState = { suggested?: boolean; platform_hint?: string | null; suggestion?: string };
91
+
92
+ function buildImportNudgeLine(importState: ImportSuggestedState | undefined): string | null {
93
+ if (!importState || !importState.suggested) return null;
94
+ return `[MemoryRouter] Historical import suggested: this machine has local conversation history not yet in your vault. Run: openclaw mr import --instructions`;
95
+ }
96
+
87
97
  // ── memory_get helpers (copied from OpenClaw source — pure filesystem, no embeddings)
88
98
  function isFileMissingError(err: unknown): boolean {
89
99
  return Boolean(err && typeof err === "object" && "code" in err && (err as any).code === "ENOENT");
@@ -314,6 +324,10 @@ type MemoryRouterConfig = {
314
324
  /** How long to keep the vault warm after the last message, in ms.
315
325
  * Default 180000 (3 minutes). Set 0 to disable warmup pings entirely. */
316
326
  keepWarmMs?: number;
327
+ /** Automatic consolidation when the server reports debt above its
328
+ * threshold. Default true. Set false to opt out (the injected suggestion
329
+ * nudge remains). */
330
+ autoConsolidate?: boolean;
317
331
  };
318
332
 
319
333
  // ──────────────────────────────────────────────────────
@@ -516,8 +530,27 @@ const memoryRouterPlugin = {
516
530
  const logging = cfg?.logging ?? false;
517
531
  const embeddings = cfg?.embeddings; // undefined = bge (default), "qwen" = qwen3-8b
518
532
  const agentKeys = cfg?.agentKeys;
533
+ const autoConsolidate = cfg?.autoConsolidate !== false; // default true
519
534
  const log = (msg: string) => { if (logging) api.logger.info?.(msg); };
520
535
 
536
+ // Automatic consolidation trigger: deterministic plugin-side automation
537
+ // driving an isolated agent turn via the gateway's subagent runtime.
538
+ // Fire-and-forget; all rails (lock, cooldown, 3-batch bound) live in
539
+ // auto-consolidate.ts. Failures never propagate into the hook path.
540
+ function triggerAutoConsolidation(reflection: ReflectionDebt | undefined, ctx: any, activeKey: string): void {
541
+ try {
542
+ maybeTriggerAutoConsolidate(reflection ?? null, {
543
+ autoConsolidate,
544
+ sessionKey: ctx?.sessionKey,
545
+ runtime: (api as any).runtime?.subagent,
546
+ agentId: ctx?.agentId || "main",
547
+ endpoint,
548
+ key: activeKey,
549
+ embeddings,
550
+ });
551
+ } catch { /* never blocks or crashes the host */ }
552
+ }
553
+
521
554
  // Resolve the correct memory key for a given agent context.
522
555
  // Multi-agent gateways: each agent gets its own key via agentKeys map.
523
556
  // Single-agent: falls back to the global key.
@@ -712,6 +745,10 @@ If setting up MemoryRouter for another agent:
712
745
  // When PR #24122 merges, OpenClaw will use the returned prependContext.
713
746
  // This gives forward compatibility — no plugin update needed.
714
747
  api.on("llm_input", async (event, ctx) => {
748
+ // Auto-consolidation runs are single-purpose tool sessions: no memory
749
+ // injection (their prompt must stay exactly the consolidation
750
+ // mission) and no re-trigger loops.
751
+ if (isAutoConsolidationSession(ctx?.sessionKey)) return;
715
752
  // Skip the first call — before_prompt_build already handled it
716
753
  // (before_prompt_build includes workspace+tools+skills for accurate billing)
717
754
  if (promptBuildFiredThisRun) {
@@ -788,8 +825,11 @@ If setting up MemoryRouter for another agent:
788
825
  memory_tokens?: number;
789
826
  metrics?: Record<string, unknown>;
790
827
  reflection?: ReflectionDebt;
828
+ import?: ImportSuggestedState;
791
829
  };
792
830
 
831
+ triggerAutoConsolidation(data.reflection, ctx, activeKey);
832
+
793
833
  if (data.context) {
794
834
  const elapsed = Date.now() - startMs;
795
835
  api.logger.info?.(`memoryrouter: injected ${data.memories_found || 0} memories (${data.memory_tokens || 0} tokens, ${elapsed}ms) [llm_input]`);
@@ -797,6 +837,8 @@ If setting up MemoryRouter for another agent:
797
837
  let contextText = data.context;
798
838
  const nudge = buildDebtNudgeLine(data.reflection);
799
839
  if (nudge) contextText = `${contextText}\n\n${nudge}`;
840
+ const importNudge = buildImportNudgeLine(data.import);
841
+ if (importNudge) contextText = `${contextText}\n\n${importNudge}`;
800
842
  const wrapped = wrapForInjection(contextText);
801
843
  return { appendSystemContext: wrapped };
802
844
  }
@@ -807,6 +849,7 @@ If setting up MemoryRouter for another agent:
807
849
 
808
850
  // ── before_prompt_build: fires once per run (primary, includes full billing context)
809
851
  api.on("before_prompt_build", async (event, ctx) => {
852
+ if (isAutoConsolidationSession(ctx?.sessionKey)) return;
810
853
  promptBuildFiredThisRun = true;
811
854
  const activeKey = resolveKey(ctx);
812
855
  if (!activeKey) return; // No key for this agent
@@ -909,8 +952,11 @@ If setting up MemoryRouter for another agent:
909
952
  memory_tokens?: number;
910
953
  metrics?: Record<string, unknown>;
911
954
  reflection?: ReflectionDebt;
955
+ import?: ImportSuggestedState;
912
956
  };
913
957
 
958
+ triggerAutoConsolidation(data.reflection, ctx, activeKey);
959
+
914
960
  if (data.context) {
915
961
  const elapsed = Date.now() - startMs;
916
962
  api.logger.info?.(`memoryrouter: injected ${data.memories_found || 0} memories (${data.memory_tokens || 0} tokens, ${elapsed}ms)`);
@@ -918,6 +964,8 @@ If setting up MemoryRouter for another agent:
918
964
  let contextText = data.context;
919
965
  const nudge = buildDebtNudgeLine(data.reflection);
920
966
  if (nudge) contextText = `${contextText}\n\n${nudge}`;
967
+ const importNudge = buildImportNudgeLine(data.import);
968
+ if (importNudge) contextText = `${contextText}\n\n${importNudge}`;
921
969
  const wrapped = wrapForInjection(contextText);
922
970
  return { appendSystemContext: wrapped };
923
971
  }
@@ -933,6 +981,8 @@ If setting up MemoryRouter for another agent:
933
981
  // ==================================================================
934
982
 
935
983
  api.on("agent_end", async (event, ctx) => {
984
+ // Never store the auto-consolidation session's own chatter as memory.
985
+ if (isAutoConsolidationSession(ctx?.sessionKey)) return;
936
986
  const activeKey = resolveKey(ctx);
937
987
  if (!activeKey) return; // No key for this agent
938
988
  try {
@@ -1650,6 +1700,38 @@ If setting up MemoryRouter for another agent:
1650
1700
  }
1651
1701
  });
1652
1702
 
1703
+ mr.command("import")
1704
+ .description("Import historical OpenClaw conversations into your vault (agent-driven extraction + CLI transport)")
1705
+ .option("--instructions", "Fetch and print the server's extraction prompt for the host agent to execute")
1706
+ .option("--file <path>", "Upload a JSONL extract: preview, price quote, explicit approval, idempotent batches, resume")
1707
+ .option("--dismiss", "Permanently silence the import suggestion for this vault")
1708
+ .option("--yes", "Skip the interactive approval prompt (required for non-interactive --file runs)")
1709
+ .option("--key <key>", "Override memory key (for subagent vaults)")
1710
+ .action(async (opts: { instructions?: boolean; file?: string; dismiss?: boolean; yes?: boolean; key?: string }) => {
1711
+ const effectiveKey = opts.key || process.env.MEMORYROUTER_API_KEY || memoryKey;
1712
+ if (!effectiveKey) { console.error("Not configured. Run: openclaw mr <key> or pass --key <key>"); return; }
1713
+ const modes = [Boolean(opts.instructions), opts.file !== undefined, Boolean(opts.dismiss)].filter(Boolean).length;
1714
+ if (modes > 1) { console.error("Choose one of --instructions, --file <path>, or --dismiss"); return; }
1715
+ try {
1716
+ const transport = await import("./import-transport.js");
1717
+ if (opts.dismiss) {
1718
+ await transport.runDismiss({ endpoint, key: effectiveKey });
1719
+ return;
1720
+ }
1721
+ if (opts.file) {
1722
+ await transport.runImportFile({ endpoint, key: effectiveKey, file: opts.file, yes: Boolean(opts.yes) });
1723
+ return;
1724
+ }
1725
+ await transport.runInstructions({ endpoint, key: effectiveKey });
1726
+ console.log("");
1727
+ console.log("Execute the prompt above with your agent, then upload the extract:");
1728
+ console.log(" openclaw mr import --file <path>");
1729
+ } catch (err) {
1730
+ console.error(`Import failed: ${err instanceof Error ? err.message : String(err)}`);
1731
+ process.exitCode = 1;
1732
+ }
1733
+ });
1734
+
1653
1735
  mr.command("reflect")
1654
1736
  .description("Show reflection debt status (consolidation runs via the agent's memory_reflect tool)")
1655
1737
  .option("--key <key>", "Override memory key (for subagent vaults)")
@@ -115,6 +115,57 @@ export async function deleteMemories(endpoint, key, embeddings) {
115
115
  headers: { Authorization: `Bearer ${key}` },
116
116
  });
117
117
  }
118
+ // ── Historical import wrappers (agent-driven import) ────────────────────────
119
+ // The server owns the extraction prompt; the CLI owns transport. The imports
120
+ // API is fully idempotent: create/approve/batches are safe to re-run.
121
+ export async function importInstructions(endpoint, key, platform, outputPath) {
122
+ const query = `platform=${encodeURIComponent(platform)}` + (outputPath ? `&output_path=${encodeURIComponent(outputPath)}` : "");
123
+ return fetch(`${endpoint}/v1/memory/imports/instructions?${query}`, {
124
+ headers: { Authorization: `Bearer ${key}` },
125
+ });
126
+ }
127
+ export async function importCreate(endpoint, key, body) {
128
+ return fetch(`${endpoint}/v1/memory/imports`, {
129
+ method: "POST",
130
+ headers: { ...JSON_HEADERS, Authorization: `Bearer ${key}` },
131
+ body: JSON.stringify(body),
132
+ });
133
+ }
134
+ export async function importApprove(endpoint, key, importId, approvalDigest) {
135
+ return fetch(`${endpoint}/v1/memory/imports/${encodeURIComponent(importId)}/approve`, {
136
+ method: "POST",
137
+ headers: { ...JSON_HEADERS, Authorization: `Bearer ${key}` },
138
+ body: JSON.stringify({ approval_digest: approvalDigest }),
139
+ });
140
+ }
141
+ export async function importBatch(endpoint, key, importId, batchIndex, jsonl, idempotencyKey) {
142
+ return fetch(`${endpoint}/v1/memory/imports/${encodeURIComponent(importId)}/batches/${batchIndex}`, {
143
+ method: "POST",
144
+ headers: {
145
+ Authorization: `Bearer ${key}`,
146
+ "Content-Type": "application/x-ndjson",
147
+ "Idempotency-Key": idempotencyKey,
148
+ },
149
+ body: jsonl,
150
+ });
151
+ }
152
+ export async function importStatus(endpoint, key, importId) {
153
+ return fetch(`${endpoint}/v1/memory/imports/${encodeURIComponent(importId)}`, {
154
+ headers: { Authorization: `Bearer ${key}` },
155
+ });
156
+ }
157
+ export async function importReceipt(endpoint, key, importId) {
158
+ return fetch(`${endpoint}/v1/memory/imports/${encodeURIComponent(importId)}/receipt`, {
159
+ headers: { Authorization: `Bearer ${key}` },
160
+ });
161
+ }
162
+ export async function importDismiss(endpoint, key, platform) {
163
+ return fetch(`${endpoint}/v1/memory/imports/dismiss-suggestion`, {
164
+ method: "POST",
165
+ headers: { ...JSON_HEADERS, Authorization: `Bearer ${key}` },
166
+ body: JSON.stringify({ platform }),
167
+ });
168
+ }
118
169
  export async function warmupMemory(endpoint, key, embeddings) {
119
170
  return fetch(`${endpoint}/v1/memory/warmup`, {
120
171
  method: "POST",
@@ -132,6 +132,65 @@ export async function deleteMemories(endpoint: string, key: string, embeddings?:
132
132
  });
133
133
  }
134
134
 
135
+ // ── Historical import wrappers (agent-driven import) ────────────────────────
136
+ // The server owns the extraction prompt; the CLI owns transport. The imports
137
+ // API is fully idempotent: create/approve/batches are safe to re-run.
138
+
139
+ export async function importInstructions(endpoint: string, key: string, platform: string, outputPath?: string): Promise<Response> {
140
+ const query = `platform=${encodeURIComponent(platform)}` + (outputPath ? `&output_path=${encodeURIComponent(outputPath)}` : "");
141
+ return fetch(`${endpoint}/v1/memory/imports/instructions?${query}`, {
142
+ headers: { Authorization: `Bearer ${key}` },
143
+ });
144
+ }
145
+
146
+ export async function importCreate(endpoint: string, key: string, body: Record<string, unknown>): Promise<Response> {
147
+ return fetch(`${endpoint}/v1/memory/imports`, {
148
+ method: "POST",
149
+ headers: { ...JSON_HEADERS, Authorization: `Bearer ${key}` },
150
+ body: JSON.stringify(body),
151
+ });
152
+ }
153
+
154
+ export async function importApprove(endpoint: string, key: string, importId: string, approvalDigest: string): Promise<Response> {
155
+ return fetch(`${endpoint}/v1/memory/imports/${encodeURIComponent(importId)}/approve`, {
156
+ method: "POST",
157
+ headers: { ...JSON_HEADERS, Authorization: `Bearer ${key}` },
158
+ body: JSON.stringify({ approval_digest: approvalDigest }),
159
+ });
160
+ }
161
+
162
+ export async function importBatch(endpoint: string, key: string, importId: string, batchIndex: number, jsonl: string, idempotencyKey: string): Promise<Response> {
163
+ return fetch(`${endpoint}/v1/memory/imports/${encodeURIComponent(importId)}/batches/${batchIndex}`, {
164
+ method: "POST",
165
+ headers: {
166
+ Authorization: `Bearer ${key}`,
167
+ "Content-Type": "application/x-ndjson",
168
+ "Idempotency-Key": idempotencyKey,
169
+ },
170
+ body: jsonl,
171
+ });
172
+ }
173
+
174
+ export async function importStatus(endpoint: string, key: string, importId: string): Promise<Response> {
175
+ return fetch(`${endpoint}/v1/memory/imports/${encodeURIComponent(importId)}`, {
176
+ headers: { Authorization: `Bearer ${key}` },
177
+ });
178
+ }
179
+
180
+ export async function importReceipt(endpoint: string, key: string, importId: string): Promise<Response> {
181
+ return fetch(`${endpoint}/v1/memory/imports/${encodeURIComponent(importId)}/receipt`, {
182
+ headers: { Authorization: `Bearer ${key}` },
183
+ });
184
+ }
185
+
186
+ export async function importDismiss(endpoint: string, key: string, platform: string): Promise<Response> {
187
+ return fetch(`${endpoint}/v1/memory/imports/dismiss-suggestion`, {
188
+ method: "POST",
189
+ headers: { ...JSON_HEADERS, Authorization: `Bearer ${key}` },
190
+ body: JSON.stringify({ platform }),
191
+ });
192
+ }
193
+
135
194
  export async function warmupMemory(endpoint: string, key: string, embeddings?: string): Promise<Response> {
136
195
  return fetch(`${endpoint}/v1/memory/warmup`, {
137
196
  method: "POST",
@@ -27,6 +27,12 @@
27
27
  "placeholder": "180000",
28
28
  "advanced": true,
29
29
  "help": "How long to keep your vault warm after each message. Default 180000 (3 min). Set 0 to disable warmup pings."
30
+ },
31
+ "autoConsolidate": {
32
+ "label": "Automatic Consolidation",
33
+ "placeholder": "true",
34
+ "advanced": true,
35
+ "help": "Automatically consolidate memories in the background when debt crosses the server threshold. Default true."
30
36
  }
31
37
  },
32
38
  "configSchema": {
@@ -61,6 +67,10 @@
61
67
  "minimum": 0,
62
68
  "description": "Keep the vault warm for this many ms after each message. Default 180000 (3 minutes). 0 disables warmup pings entirely."
63
69
  },
70
+ "autoConsolidate": {
71
+ "type": "boolean",
72
+ "description": "Automatically consolidate memories in an isolated background agent turn when the server reports debt above its threshold. Default true. Set false to opt out (the in-context suggestion nudge remains)."
73
+ },
64
74
  "agentKeys": {
65
75
  "type": "object",
66
76
  "additionalProperties": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mr-memory",
3
- "version": "3.9.1",
3
+ "version": "3.11.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": [
@@ -11,6 +11,10 @@
11
11
  "upload.ts",
12
12
  "memoryrouter-api.js",
13
13
  "memoryrouter-api.ts",
14
+ "auto-consolidate.js",
15
+ "auto-consolidate.ts",
16
+ "import-transport.js",
17
+ "import-transport.ts",
14
18
  "upload-api.js",
15
19
  "upload-api.ts",
16
20
  "openclaw.plugin.json"