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/README.md +29 -0
- package/auto-consolidate.js +292 -0
- package/auto-consolidate.ts +279 -0
- package/import-transport.js +457 -0
- package/import-transport.ts +511 -0
- package/index.js +81 -0
- package/index.ts +82 -0
- package/memoryrouter-api.js +51 -0
- package/memoryrouter-api.ts +59 -0
- package/openclaw.plugin.json +10 -0
- package/package.json +5 -1
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)")
|
package/memoryrouter-api.js
CHANGED
|
@@ -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",
|
package/memoryrouter-api.ts
CHANGED
|
@@ -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/openclaw.plugin.json
CHANGED
|
@@ -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.
|
|
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"
|