mr-memory 3.9.0 → 3.10.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
@@ -65,7 +65,7 @@ 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)
68
+ // ── Reflection debt nudge (one line, appended to prepare injections whenever the server suggests it)
69
69
  type ReflectionDebt = {
70
70
  suggestion?: string | null;
71
71
  unconsolidated?: {
@@ -75,15 +75,22 @@ type ReflectionDebt = {
75
75
  threshold_tokens?: number;
76
76
  };
77
77
 
78
- const NUDGE_INTERVAL_MS = 24 * 60 * 60 * 1000; // at most one nudge per 24h
79
-
80
78
  function buildDebtNudgeLine(reflection: ReflectionDebt | undefined): string | null {
81
79
  if (!reflection || !reflection.suggestion) return null;
82
80
  const rawTokens = reflection.unconsolidated?.raw?.est_tokens ?? 0;
83
81
  const tier2Tokens = reflection.unconsolidated?.tier2?.est_tokens ?? 0;
84
82
  const totalTokens = rawTokens + tier2Tokens;
85
83
  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.`;
84
+ return `[MemoryRouter] Consolidation suggested: ${kTokens}k unconsolidated tokens. Use the memory_reflect tool to consolidate them now.`;
85
+ }
86
+
87
+ // ── Historical import nudge (server-owned import.suggested block; additive,
88
+ // no rate limit, clears server-side on import completion or dismiss)
89
+ type ImportSuggestedState = { suggested?: boolean; platform_hint?: string | null; suggestion?: string };
90
+
91
+ function buildImportNudgeLine(importState: ImportSuggestedState | undefined): string | null {
92
+ if (!importState || !importState.suggested) return null;
93
+ return `[MemoryRouter] Historical import suggested: this machine has local conversation history not yet in your vault. Run: openclaw mr import --instructions`;
87
94
  }
88
95
 
89
96
  // ── memory_get helpers (copied from OpenClaw source — pure filesystem, no embeddings)
@@ -354,36 +361,6 @@ async function writeOpenClawConfigFile(config: JsonObject): Promise<void> {
354
361
  `, "utf8");
355
362
  }
356
363
 
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
-
387
364
  // ──────────────────────────────────────────────────────
388
365
  // Full-replace: disable OpenClaw's native memory search so MemoryRouter
389
366
  // owns the memory_search tool (no name conflict, no native embeddings 429).
@@ -820,6 +797,7 @@ If setting up MemoryRouter for another agent:
820
797
  memory_tokens?: number;
821
798
  metrics?: Record<string, unknown>;
822
799
  reflection?: ReflectionDebt;
800
+ import?: ImportSuggestedState;
823
801
  };
824
802
 
825
803
  if (data.context) {
@@ -827,8 +805,10 @@ If setting up MemoryRouter for another agent:
827
805
  api.logger.info?.(`memoryrouter: injected ${data.memories_found || 0} memories (${data.memory_tokens || 0} tokens, ${elapsed}ms) [llm_input]`);
828
806
  if (elapsed > 2000 && data.metrics) log(`memoryrouter: prepare metrics ${JSON.stringify(data.metrics)}`);
829
807
  let contextText = data.context;
830
- const nudge = await maybeDebtNudge(data.reflection);
808
+ const nudge = buildDebtNudgeLine(data.reflection);
831
809
  if (nudge) contextText = `${contextText}\n\n${nudge}`;
810
+ const importNudge = buildImportNudgeLine(data.import);
811
+ if (importNudge) contextText = `${contextText}\n\n${importNudge}`;
832
812
  const wrapped = wrapForInjection(contextText);
833
813
  return { appendSystemContext: wrapped };
834
814
  }
@@ -941,6 +921,7 @@ If setting up MemoryRouter for another agent:
941
921
  memory_tokens?: number;
942
922
  metrics?: Record<string, unknown>;
943
923
  reflection?: ReflectionDebt;
924
+ import?: ImportSuggestedState;
944
925
  };
945
926
 
946
927
  if (data.context) {
@@ -948,8 +929,10 @@ If setting up MemoryRouter for another agent:
948
929
  api.logger.info?.(`memoryrouter: injected ${data.memories_found || 0} memories (${data.memory_tokens || 0} tokens, ${elapsed}ms)`);
949
930
  if (elapsed > 2000 && data.metrics) log(`memoryrouter: prepare metrics ${JSON.stringify(data.metrics)}`);
950
931
  let contextText = data.context;
951
- const nudge = await maybeDebtNudge(data.reflection);
932
+ const nudge = buildDebtNudgeLine(data.reflection);
952
933
  if (nudge) contextText = `${contextText}\n\n${nudge}`;
934
+ const importNudge = buildImportNudgeLine(data.import);
935
+ if (importNudge) contextText = `${contextText}\n\n${importNudge}`;
953
936
  const wrapped = wrapForInjection(contextText);
954
937
  return { appendSystemContext: wrapped };
955
938
  }
@@ -1682,6 +1665,38 @@ If setting up MemoryRouter for another agent:
1682
1665
  }
1683
1666
  });
1684
1667
 
1668
+ mr.command("import")
1669
+ .description("Import historical OpenClaw conversations into your vault (agent-driven extraction + CLI transport)")
1670
+ .option("--instructions", "Fetch and print the server's extraction prompt for the host agent to execute")
1671
+ .option("--file <path>", "Upload a JSONL extract: preview, price quote, explicit approval, idempotent batches, resume")
1672
+ .option("--dismiss", "Permanently silence the import suggestion for this vault")
1673
+ .option("--yes", "Skip the interactive approval prompt (required for non-interactive --file runs)")
1674
+ .option("--key <key>", "Override memory key (for subagent vaults)")
1675
+ .action(async (opts: { instructions?: boolean; file?: string; dismiss?: boolean; yes?: boolean; key?: string }) => {
1676
+ const effectiveKey = opts.key || process.env.MEMORYROUTER_API_KEY || memoryKey;
1677
+ if (!effectiveKey) { console.error("Not configured. Run: openclaw mr <key> or pass --key <key>"); return; }
1678
+ const modes = [Boolean(opts.instructions), opts.file !== undefined, Boolean(opts.dismiss)].filter(Boolean).length;
1679
+ if (modes > 1) { console.error("Choose one of --instructions, --file <path>, or --dismiss"); return; }
1680
+ try {
1681
+ const transport = await import("./import-transport.js");
1682
+ if (opts.dismiss) {
1683
+ await transport.runDismiss({ endpoint, key: effectiveKey });
1684
+ return;
1685
+ }
1686
+ if (opts.file) {
1687
+ await transport.runImportFile({ endpoint, key: effectiveKey, file: opts.file, yes: Boolean(opts.yes) });
1688
+ return;
1689
+ }
1690
+ await transport.runInstructions({ endpoint, key: effectiveKey });
1691
+ console.log("");
1692
+ console.log("Execute the prompt above with your agent, then upload the extract:");
1693
+ console.log(" openclaw mr import --file <path>");
1694
+ } catch (err) {
1695
+ console.error(`Import failed: ${err instanceof Error ? err.message : String(err)}`);
1696
+ process.exitCode = 1;
1697
+ }
1698
+ });
1699
+
1685
1700
  mr.command("reflect")
1686
1701
  .description("Show reflection debt status (consolidation runs via the agent's memory_reflect tool)")
1687
1702
  .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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mr-memory",
3
- "version": "3.9.0",
3
+ "version": "3.10.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,8 @@
11
11
  "upload.ts",
12
12
  "memoryrouter-api.js",
13
13
  "memoryrouter-api.ts",
14
+ "import-transport.js",
15
+ "import-transport.ts",
14
16
  "upload-api.js",
15
17
  "upload-api.ts",
16
18
  "openclaw.plugin.json"