mr-memory 3.7.6 → 3.7.7

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 } from "./memoryrouter-api.js";
11
+ import { deleteMemories, getMemoryStats, ingestMemory, 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";
@@ -267,7 +267,7 @@ const WORKSPACE_FILES = [
267
267
  "TOOLS.md", "AGENTS.md", "SOUL.md", "BOOTSTRAP.md",
268
268
  ];
269
269
  function openClawConfigPath() {
270
- return process.env.OPENCLAW_CONFIG || join(os.homedir(), ".openclaw", "openclaw.json");
270
+ return process.env.OPENCLAW_CONFIG_PATH || process.env.OPENCLAW_CONFIG || join(os.homedir(), ".openclaw", "openclaw.json");
271
271
  }
272
272
  async function readOpenClawConfigFile() {
273
273
  try {
@@ -357,6 +357,47 @@ async function setPluginEnabled(api, enabled) {
357
357
  }
358
358
  await updateOpenClawPluginEntry(api, { enabled });
359
359
  }
360
+ async function mergePluginConfig(api, patch) {
361
+ const current = await readOpenClawConfigFile();
362
+ const existing = current.plugins?.entries?.[api.id]?.config;
363
+ await setPluginConfig(api, {
364
+ ...(existing && typeof existing === "object" && !Array.isArray(existing) ? existing : {}),
365
+ ...patch,
366
+ });
367
+ }
368
+ // Keep mr-memory as a first-class plugin config entry without clobbering user settings.
369
+ // OpenClaw doctor validates plugin entries against discoverable manifests, so the entry
370
+ // must be present, enabled, and allowlisted when an allowlist exists.
371
+ async function ensureDoctorSafePluginEntry(api) {
372
+ try {
373
+ const config = await readOpenClawConfigFile();
374
+ const plugins = config.plugins ?? {};
375
+ const entries = plugins.entries ?? {};
376
+ const existingEntry = entries[api.id] && typeof entries[api.id] === "object" && !Array.isArray(entries[api.id])
377
+ ? entries[api.id]
378
+ : {};
379
+ const nextPlugins = {
380
+ ...plugins,
381
+ entries: {
382
+ ...entries,
383
+ [api.id]: {
384
+ ...existingEntry,
385
+ enabled: true,
386
+ },
387
+ },
388
+ };
389
+ if (Array.isArray(plugins.allow) && !plugins.allow.includes(api.id)) {
390
+ nextPlugins.allow = [...plugins.allow, api.id];
391
+ }
392
+ const next = { ...config, plugins: nextPlugins };
393
+ if (JSON.stringify(next.plugins) === JSON.stringify(config.plugins))
394
+ return;
395
+ await writeOpenClawConfigFile(next);
396
+ }
397
+ catch (err) {
398
+ api.logger.info?.(`memoryrouter: doctor-safe registration skipped — ${err instanceof Error ? err.message : String(err)}`);
399
+ }
400
+ }
360
401
  /**
361
402
  * Read all workspace files and return as a single text blob for token counting.
362
403
  */
@@ -434,13 +475,16 @@ const memoryRouterPlugin = {
434
475
  return memoryKey;
435
476
  }
436
477
  const hasAnyKey = !!(memoryKey || (agentKeys && Object.keys(agentKeys).length > 0));
478
+ void (async () => {
479
+ await ensureDoctorSafePluginEntry(api);
480
+ if (hasAnyKey)
481
+ await selfHealNativeMemorySearch(api);
482
+ })();
437
483
  if (hasAnyKey) {
438
484
  if (memoryKey)
439
485
  api.logger.info?.(`memoryrouter: active (key: ${memoryKey.slice(0, 6)}...)`);
440
486
  if (agentKeys)
441
487
  api.logger.info?.(`memoryrouter: ${Object.keys(agentKeys).length} agent key(s) configured`);
442
- // Self-heal already-configured installs: disable native search if still on.
443
- void selfHealNativeMemorySearch(api);
444
488
  }
445
489
  else {
446
490
  api.logger.info?.("memoryrouter: no key configured — run: openclaw mr <key>");
@@ -603,6 +647,8 @@ If setting up MemoryRouter for another agent:
603
647
  if (data.context) {
604
648
  const elapsed = Date.now() - startMs;
605
649
  api.logger.info?.(`memoryrouter: injected ${data.memories_found || 0} memories (${data.memory_tokens || 0} tokens, ${elapsed}ms) [llm_input]`);
650
+ if (elapsed > 2000 && data.metrics)
651
+ log(`memoryrouter: prepare metrics ${JSON.stringify(data.metrics)}`);
606
652
  const wrapped = wrapForInjection(data.context);
607
653
  return { appendSystemContext: wrapped };
608
654
  }
@@ -704,6 +750,8 @@ If setting up MemoryRouter for another agent:
704
750
  if (data.context) {
705
751
  const elapsed = Date.now() - startMs;
706
752
  api.logger.info?.(`memoryrouter: injected ${data.memories_found || 0} memories (${data.memory_tokens || 0} tokens, ${elapsed}ms)`);
753
+ if (elapsed > 2000 && data.metrics)
754
+ log(`memoryrouter: prepare metrics ${JSON.stringify(data.metrics)}`);
707
755
  const wrapped = wrapForInjection(data.context);
708
756
  return { appendSystemContext: wrapped };
709
757
  }
@@ -919,7 +967,7 @@ If setting up MemoryRouter for another agent:
919
967
  return;
920
968
  }
921
969
  try {
922
- await setPluginConfig(api, { key });
970
+ await mergePluginConfig(api, { key });
923
971
  await setPluginEnabled(api, true);
924
972
  try {
925
973
  const changed = await setNativeMemorySearchEnabled(false);
@@ -995,7 +1043,7 @@ If setting up MemoryRouter for another agent:
995
1043
  return;
996
1044
  }
997
1045
  try {
998
- await setPluginConfig(api, { key: memoryKey, endpoint: cfg?.endpoint, density: name });
1046
+ await mergePluginConfig(api, { key: memoryKey, density: name });
999
1047
  console.log(`✓ Memory density set to ${name}`);
1000
1048
  }
1001
1049
  catch (err) {
@@ -1013,11 +1061,8 @@ If setting up MemoryRouter for another agent:
1013
1061
  }
1014
1062
  const newLogging = !logging;
1015
1063
  try {
1016
- await setPluginConfig(api, {
1064
+ await mergePluginConfig(api, {
1017
1065
  key: memoryKey,
1018
- endpoint: cfg?.endpoint,
1019
- density,
1020
- mode,
1021
1066
  logging: newLogging,
1022
1067
  });
1023
1068
  console.log(`✓ Logging ${newLogging ? "ON" : "OFF"} (restart gateway to apply)`);
@@ -1170,8 +1215,17 @@ If setting up MemoryRouter for another agent:
1170
1215
  start: () => {
1171
1216
  if (memoryKey)
1172
1217
  api.logger.info?.(`memoryrouter: active (key: ${memoryKey.slice(0, 6)}...)`);
1218
+ if (hasAnyKey && !warmupTimer) {
1219
+ pingWarmup("startup");
1220
+ warmupTimer = setInterval(() => pingWarmup("keepalive"), 4 * 60 * 1000);
1221
+ warmupTimer.unref?.();
1222
+ }
1173
1223
  },
1174
1224
  stop: () => {
1225
+ if (warmupTimer) {
1226
+ clearInterval(warmupTimer);
1227
+ warmupTimer = undefined;
1228
+ }
1175
1229
  api.logger.info?.("memoryrouter: stopped");
1176
1230
  },
1177
1231
  });
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 } from "./memoryrouter-api.js";
12
+ import { deleteMemories, getMemoryStats, ingestMemory, 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";
@@ -296,7 +296,7 @@ type CompatApi = OpenClawPluginApi & {
296
296
  type JsonObject = Record<string, any>;
297
297
 
298
298
  function openClawConfigPath(): string {
299
- return process.env.OPENCLAW_CONFIG || join(os.homedir(), ".openclaw", "openclaw.json");
299
+ return process.env.OPENCLAW_CONFIG_PATH || process.env.OPENCLAW_CONFIG || join(os.homedir(), ".openclaw", "openclaw.json");
300
300
  }
301
301
 
302
302
  async function readOpenClawConfigFile(): Promise<JsonObject> {
@@ -388,6 +388,47 @@ async function setPluginEnabled(api: OpenClawPluginApi, enabled: boolean): Promi
388
388
  await updateOpenClawPluginEntry(api, { enabled });
389
389
  }
390
390
 
391
+ async function mergePluginConfig(api: OpenClawPluginApi, patch: Record<string, unknown>): Promise<void> {
392
+ const current = await readOpenClawConfigFile();
393
+ const existing = current.plugins?.entries?.[api.id]?.config;
394
+ await setPluginConfig(api, {
395
+ ...(existing && typeof existing === "object" && !Array.isArray(existing) ? existing : {}),
396
+ ...patch,
397
+ });
398
+ }
399
+
400
+ // Keep mr-memory as a first-class plugin config entry without clobbering user settings.
401
+ // OpenClaw doctor validates plugin entries against discoverable manifests, so the entry
402
+ // must be present, enabled, and allowlisted when an allowlist exists.
403
+ async function ensureDoctorSafePluginEntry(api: OpenClawPluginApi): Promise<void> {
404
+ try {
405
+ const config = await readOpenClawConfigFile();
406
+ const plugins = config.plugins ?? {};
407
+ const entries = plugins.entries ?? {};
408
+ const existingEntry = entries[api.id] && typeof entries[api.id] === "object" && !Array.isArray(entries[api.id])
409
+ ? entries[api.id]
410
+ : {};
411
+ const nextPlugins: JsonObject = {
412
+ ...plugins,
413
+ entries: {
414
+ ...entries,
415
+ [api.id]: {
416
+ ...existingEntry,
417
+ enabled: true,
418
+ },
419
+ },
420
+ };
421
+ if (Array.isArray(plugins.allow) && !plugins.allow.includes(api.id)) {
422
+ nextPlugins.allow = [...plugins.allow, api.id];
423
+ }
424
+ const next = { ...config, plugins: nextPlugins };
425
+ if (JSON.stringify(next.plugins) === JSON.stringify(config.plugins)) return;
426
+ await writeOpenClawConfigFile(next);
427
+ } catch (err) {
428
+ api.logger.info?.(`memoryrouter: doctor-safe registration skipped — ${err instanceof Error ? err.message : String(err)}`);
429
+ }
430
+ }
431
+
391
432
  /**
392
433
  * Read all workspace files and return as a single text blob for token counting.
393
434
  */
@@ -460,12 +501,44 @@ const memoryRouterPlugin = {
460
501
  }
461
502
 
462
503
  const hasAnyKey = !!(memoryKey || (agentKeys && Object.keys(agentKeys).length > 0));
504
+ let warmupTimer: ReturnType<typeof setInterval> | undefined;
505
+
506
+ function configuredKeys(): string[] {
507
+ const keys = new Set<string>();
508
+ if (memoryKey) keys.add(memoryKey);
509
+ if (agentKeys) {
510
+ for (const key of Object.values(agentKeys)) {
511
+ if (key) keys.add(key);
512
+ }
513
+ }
514
+ return Array.from(keys);
515
+ }
516
+
517
+ function pingWarmup(reason: string): void {
518
+ for (const key of configuredKeys()) {
519
+ warmupMemory(endpoint, key, embeddings)
520
+ .then(async (res) => {
521
+ if (!res.ok) {
522
+ log(`memoryrouter: warmup ${reason} failed (${res.status})`);
523
+ return;
524
+ }
525
+ const data = await res.json().catch(() => null) as any;
526
+ log(`memoryrouter: warmup ${reason} ok (${data?.totalVectors ?? 0} vectors, ${data?.warmupTimeMs ?? "?"}ms)`);
527
+ })
528
+ .catch((err) => {
529
+ log(`memoryrouter: warmup ${reason} error — ${err instanceof Error ? err.message : String(err)}`);
530
+ });
531
+ }
532
+ }
533
+
534
+ void (async () => {
535
+ await ensureDoctorSafePluginEntry(api);
536
+ if (hasAnyKey) await selfHealNativeMemorySearch(api);
537
+ })();
463
538
 
464
539
  if (hasAnyKey) {
465
540
  if (memoryKey) api.logger.info?.(`memoryrouter: active (key: ${memoryKey.slice(0, 6)}...)`);
466
541
  if (agentKeys) api.logger.info?.(`memoryrouter: ${Object.keys(agentKeys).length} agent key(s) configured`);
467
- // Self-heal already-configured installs: disable native search if still on.
468
- void selfHealNativeMemorySearch(api);
469
542
  } else {
470
543
  api.logger.info?.("memoryrouter: no key configured — run: openclaw mr <key>");
471
544
  api.logger.info?.("memoryrouter: get your free API key at https://memoryrouter.ai");
@@ -632,11 +705,13 @@ If setting up MemoryRouter for another agent:
632
705
  memories_found?: number;
633
706
  tokens_billed?: number;
634
707
  memory_tokens?: number;
708
+ metrics?: Record<string, unknown>;
635
709
  };
636
710
 
637
711
  if (data.context) {
638
712
  const elapsed = Date.now() - startMs;
639
713
  api.logger.info?.(`memoryrouter: injected ${data.memories_found || 0} memories (${data.memory_tokens || 0} tokens, ${elapsed}ms) [llm_input]`);
714
+ if (elapsed > 2000 && data.metrics) log(`memoryrouter: prepare metrics ${JSON.stringify(data.metrics)}`);
640
715
  const wrapped = wrapForInjection(data.context);
641
716
  return { appendSystemContext: wrapped };
642
717
  }
@@ -744,11 +819,13 @@ If setting up MemoryRouter for another agent:
744
819
  memories_found?: number;
745
820
  tokens_billed?: number;
746
821
  memory_tokens?: number;
822
+ metrics?: Record<string, unknown>;
747
823
  };
748
824
 
749
825
  if (data.context) {
750
826
  const elapsed = Date.now() - startMs;
751
827
  api.logger.info?.(`memoryrouter: injected ${data.memories_found || 0} memories (${data.memory_tokens || 0} tokens, ${elapsed}ms)`);
828
+ if (elapsed > 2000 && data.metrics) log(`memoryrouter: prepare metrics ${JSON.stringify(data.metrics)}`);
752
829
  const wrapped = wrapForInjection(data.context);
753
830
  return { appendSystemContext: wrapped };
754
831
  }
@@ -973,7 +1050,7 @@ If setting up MemoryRouter for another agent:
973
1050
  return;
974
1051
  }
975
1052
  try {
976
- await setPluginConfig(api, { key });
1053
+ await mergePluginConfig(api, { key });
977
1054
  await setPluginEnabled(api, true);
978
1055
  try {
979
1056
  const changed = await setNativeMemorySearchEnabled(false);
@@ -1046,7 +1123,7 @@ If setting up MemoryRouter for another agent:
1046
1123
  .action(async () => {
1047
1124
  if (!memoryKey) { console.error("Not configured. Run: openclaw mr <key>"); return; }
1048
1125
  try {
1049
- await setPluginConfig(api, { key: memoryKey, endpoint: cfg?.endpoint, density: name });
1126
+ await mergePluginConfig(api, { key: memoryKey, density: name });
1050
1127
  console.log(`✓ Memory density set to ${name}`);
1051
1128
  } catch (err) {
1052
1129
  console.error(`Failed: ${err instanceof Error ? err.message : String(err)}`);
@@ -1064,11 +1141,8 @@ If setting up MemoryRouter for another agent:
1064
1141
  }
1065
1142
  const newLogging = !logging;
1066
1143
  try {
1067
- await setPluginConfig(api, {
1144
+ await mergePluginConfig(api, {
1068
1145
  key: memoryKey,
1069
- endpoint: cfg?.endpoint,
1070
- density,
1071
- mode,
1072
1146
  logging: newLogging,
1073
1147
  });
1074
1148
  console.log(`✓ Logging ${newLogging ? "ON" : "OFF"} (restart gateway to apply)`);
@@ -1237,8 +1311,17 @@ If setting up MemoryRouter for another agent:
1237
1311
  id: "mr-memory",
1238
1312
  start: () => {
1239
1313
  if (memoryKey) api.logger.info?.(`memoryrouter: active (key: ${memoryKey.slice(0, 6)}...)`);
1314
+ if (hasAnyKey && !warmupTimer) {
1315
+ pingWarmup("startup");
1316
+ warmupTimer = setInterval(() => pingWarmup("keepalive"), 4 * 60 * 1000);
1317
+ warmupTimer.unref?.();
1318
+ }
1240
1319
  },
1241
1320
  stop: () => {
1321
+ if (warmupTimer) {
1322
+ clearInterval(warmupTimer);
1323
+ warmupTimer = undefined;
1324
+ }
1242
1325
  api.logger.info?.("memoryrouter: stopped");
1243
1326
  },
1244
1327
  });
@@ -47,3 +47,14 @@ export async function deleteMemories(endpoint, key, embeddings) {
47
47
  headers: { Authorization: `Bearer ${key}` },
48
48
  });
49
49
  }
50
+
51
+ export async function warmupMemory(endpoint, key, embeddings) {
52
+ return fetch(`${endpoint}/v1/memory/warmup`, {
53
+ method: "POST",
54
+ headers: {
55
+ Authorization: `Bearer ${key}`,
56
+ ...(embeddings && { "X-Embedding-Model": embeddings }),
57
+ },
58
+ signal: AbortSignal.timeout(15000),
59
+ });
60
+ }
@@ -58,3 +58,14 @@ export async function deleteMemories(endpoint: string, key: string, embeddings?:
58
58
  headers: { Authorization: `Bearer ${key}` },
59
59
  });
60
60
  }
61
+
62
+ export async function warmupMemory(endpoint: string, key: string, embeddings?: string): Promise<Response> {
63
+ return fetch(`${endpoint}/v1/memory/warmup`, {
64
+ method: "POST",
65
+ headers: {
66
+ Authorization: `Bearer ${key}`,
67
+ ...(embeddings && { "X-Embedding-Model": embeddings }),
68
+ },
69
+ signal: AbortSignal.timeout(15000),
70
+ });
71
+ }
@@ -46,6 +46,14 @@
46
46
  "type": "string",
47
47
  "enum": ["bge", "qwen"],
48
48
  "description": "Embedding model: bge (1024d, default) or qwen (4096d)"
49
+ },
50
+ "agentKeys": {
51
+ "type": "object",
52
+ "additionalProperties": {
53
+ "type": "string",
54
+ "pattern": "^mk[_-]"
55
+ },
56
+ "description": "Optional per-agent MemoryRouter keys, keyed by OpenClaw agent id"
49
57
  }
50
58
  },
51
59
  "required": []
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mr-memory",
3
- "version": "3.7.6",
3
+ "version": "3.7.7",
4
4
  "description": "mr-memory is the MemoryRouter plugin for OpenClaw — persistent memory across every conversation",
5
5
  "type": "module",
6
6
  "files": [