mr-memory 3.7.10 → 3.8.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 CHANGED
@@ -9,10 +9,10 @@ Use it when you want OpenClaw memory that does not restart from zero every sessi
9
9
  ## Install
10
10
 
11
11
  ```bash
12
- openclaw plugins install npm:mr-memory@3.7.10
12
+ openclaw plugins install npm:mr-memory@3.8.0
13
13
  ```
14
14
 
15
- The explicit `npm:` source and version make the audited install deterministic. `mr-memory@3.7.10` is published on npm. A ClawHub listing has not been verified and is not required for this path.
15
+ The explicit `npm:` source and version make the audited install deterministic. `mr-memory@3.8.0` is published on npm. A ClawHub listing has not been verified and is not required for this path.
16
16
 
17
17
  ## Setup
18
18
 
@@ -46,6 +46,16 @@ When enabled, relay hooks retrieve memory before agent/model input and capture t
46
46
 
47
47
  Only direct user-to-AI conversation is stored. Tool use, subagent work, and internal processing are excluded. Historical workspace/session upload is a separate explicit `openclaw mr upload` operation; installing or enabling the relay does not imply an import already happened.
48
48
 
49
+ ### Keep-warm
50
+
51
+ Retrieval is fastest against a warm vault, so the plugin keeps yours warm for **3 minutes after each message**. Every message re-opens that window; once it lapses with no new activity the warmup timer is cleared entirely, so an idle agent sends **no** background warmup traffic. There is no startup ping and no always-on keepalive.
52
+
53
+ Tune it with `keepWarmMs` (milliseconds, default `180000`; `0` disables warmup pings):
54
+
55
+ ```json
56
+ { "plugins": { "entries": { "mr-memory": { "config": { "keepWarmMs": 180000 } } } } }
57
+ ```
58
+
49
59
  ## Config
50
60
 
51
61
  After `openclaw mr <key>`, config is stored at:
package/index.js CHANGED
@@ -12,6 +12,15 @@ import { deleteMemories, getMemoryStats, ingestMemory, prepareMemory, searchMemo
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";
15
+ /** Keep-warm defaults.
16
+ *
17
+ * Warmth only matters in the minutes right after real activity, so warmup
18
+ * pings are activity-scoped instead of always-on: every message opens a
19
+ * KEEP_WARM window, pings fire at KEEP_WARM_PING_INTERVAL_MS inside it, and
20
+ * the timer is cleared completely once the window closes. An idle gateway
21
+ * sends zero warmup traffic. */
22
+ const DEFAULT_KEEP_WARM_MS = 3 * 60 * 1000; // 3 minutes after the last message
23
+ const KEEP_WARM_PING_INTERVAL_MS = 60 * 1000; // cadence inside the window
15
24
  /** Strip media-attached references and OpenClaw media instruction text so
16
25
  * old screenshots/photos/audio don't get stored and re-injected forever. */
17
26
  const MEDIA_ATTACHED_RE = /\[media attached[^\]]*\]/gi;
@@ -299,37 +308,34 @@ async function writeOpenClawConfigFile(config) {
299
308
  // Global + subagents: writes agents.defaults.memorySearch.enabled.
300
309
  // ──────────────────────────────────────────────────────
301
310
  async function setNativeMemorySearchEnabled(enabled) {
302
- const config = await readOpenClawConfigFile();
303
- config.agents ??= {};
304
- config.agents.defaults ??= {};
305
- config.agents.defaults.memorySearch ??= {};
306
- if (config.agents.defaults.memorySearch.enabled === enabled) return false; // no-op
307
- config.agents.defaults.memorySearch.enabled = enabled;
308
- await writeOpenClawConfigFile(config);
309
- return true;
311
+ const config = await readOpenClawConfigFile();
312
+ config.agents ??= {};
313
+ config.agents.defaults ??= {};
314
+ config.agents.defaults.memorySearch ??= {};
315
+ if (config.agents.defaults.memorySearch.enabled === enabled)
316
+ return false; // no-op
317
+ config.agents.defaults.memorySearch.enabled = enabled;
318
+ await writeOpenClawConfigFile(config);
319
+ return true;
310
320
  }
311
-
312
321
  // Boot-time self-heal: if a key is configured but native search is still on,
313
322
  // flip it off so already-installed users are fixed on restart after updating
314
323
  // (no need to re-run `openclaw mr <key>`).
315
324
  async function selfHealNativeMemorySearch(api) {
316
- try {
317
- const config = await readOpenClawConfigFile();
318
- const ms = config?.agents?.defaults?.memorySearch;
319
- if (ms?.enabled === false) return; // already disabled — nothing to do
320
- const changed = await setNativeMemorySearchEnabled(false);
321
- if (changed) {
322
- api.logger.info?.(
323
- "memoryrouter: disabled native memory search (full-replace). Restart gateway to apply.",
324
- );
325
+ try {
326
+ const config = await readOpenClawConfigFile();
327
+ const ms = config?.agents?.defaults?.memorySearch;
328
+ if (ms?.enabled === false)
329
+ return; // already disabled — nothing to do
330
+ const changed = await setNativeMemorySearchEnabled(false);
331
+ if (changed) {
332
+ api.logger.info?.("memoryrouter: disabled native memory search (full-replace). Restart gateway to apply.");
333
+ }
334
+ }
335
+ catch (err) {
336
+ api.logger.info?.(`memoryrouter: self-heal skipped — ${err instanceof Error ? err.message : String(err)}`);
325
337
  }
326
- } catch (err) {
327
- api.logger.info?.(
328
- `memoryrouter: self-heal skipped — ${err instanceof Error ? err.message : String(err)}`,
329
- );
330
- }
331
338
  }
332
-
333
339
  async function updateOpenClawPluginEntry(api, patch) {
334
340
  const config = await readOpenClawConfigFile();
335
341
  config.plugins ??= {};
@@ -475,6 +481,80 @@ const memoryRouterPlugin = {
475
481
  return memoryKey;
476
482
  }
477
483
  const hasAnyKey = !!(memoryKey || (agentKeys && Object.keys(agentKeys).length > 0));
484
+ // Keep-warm window: 0 (or negative) disables warmup pings entirely.
485
+ const keepWarmMs = typeof cfg?.keepWarmMs === "number" && cfg.keepWarmMs >= 0
486
+ ? cfg.keepWarmMs
487
+ : DEFAULT_KEEP_WARM_MS;
488
+ let warmupTimer;
489
+ // undefined = no activity yet (never compare against 0; a 0 timestamp is a
490
+ // valid clock value and must not read as "no activity").
491
+ let lastActivityAt;
492
+ // Starts true so keep-warm works even if the host never calls service.start();
493
+ // service.stop() flips it off so no timer can outlive the plugin.
494
+ let serviceRunning = true;
495
+ function configuredKeys() {
496
+ const keys = new Set();
497
+ if (memoryKey)
498
+ keys.add(memoryKey);
499
+ if (agentKeys) {
500
+ for (const key of Object.values(agentKeys)) {
501
+ if (key)
502
+ keys.add(key);
503
+ }
504
+ }
505
+ return Array.from(keys);
506
+ }
507
+ function pingWarmup(reason) {
508
+ for (const key of configuredKeys()) {
509
+ warmupMemory(endpoint, key, embeddings)
510
+ .then(async (res) => {
511
+ if (!res.ok) {
512
+ log(`memoryrouter: warmup ${reason} failed (${res.status})`);
513
+ return;
514
+ }
515
+ const data = await res.json().catch(() => null);
516
+ log(`memoryrouter: warmup ${reason} ok (${data?.totalVectors ?? 0} vectors, ${data?.warmupTimeMs ?? "?"}ms)`);
517
+ })
518
+ .catch((err) => {
519
+ log(`memoryrouter: warmup ${reason} error — ${err instanceof Error ? err.message : String(err)}`);
520
+ });
521
+ }
522
+ }
523
+ function stopWarmLoop() {
524
+ if (warmupTimer) {
525
+ clearInterval(warmupTimer);
526
+ warmupTimer = undefined;
527
+ }
528
+ }
529
+ /** True while we're still inside the post-message keep-warm window. */
530
+ function withinWarmWindow() {
531
+ if (keepWarmMs <= 0 || lastActivityAt === undefined)
532
+ return false;
533
+ return Date.now() - lastActivityAt <= keepWarmMs;
534
+ }
535
+ /** Called on every message-driven activity (prepare / ingest).
536
+ * Opens (or extends) the keep-warm window and makes sure the loop runs.
537
+ * Once the window lapses with no new activity the interval is cleared, so
538
+ * an idle gateway sends no warmup traffic at all. */
539
+ function noteActivity() {
540
+ if (!hasAnyKey || keepWarmMs <= 0 || !serviceRunning)
541
+ return;
542
+ lastActivityAt = Date.now();
543
+ if (warmupTimer)
544
+ return; // window extended; existing loop keeps running
545
+ // No immediate ping: the prepare/ingest request that triggered this call
546
+ // already warmed the vault server-side. Pings only cover the idle gaps
547
+ // inside the window.
548
+ warmupTimer = setInterval(() => {
549
+ if (!withinWarmWindow()) {
550
+ log("memoryrouter: keep-warm window elapsed — pings stopped until next message");
551
+ stopWarmLoop();
552
+ return;
553
+ }
554
+ pingWarmup("keepalive");
555
+ }, KEEP_WARM_PING_INTERVAL_MS);
556
+ warmupTimer.unref?.();
557
+ }
478
558
  void (async () => {
479
559
  await ensureDoctorSafePluginEntry(api);
480
560
  if (hasAnyKey)
@@ -628,6 +708,8 @@ If setting up MemoryRouter for another agent:
628
708
  contextPayload.push({ role: "user", content: taskPrompt });
629
709
  }
630
710
  }
711
+ // Message activity — open/extend the keep-warm window.
712
+ noteActivity();
631
713
  const res = await prepareMemory(endpoint, activeKey, {
632
714
  messages: contextPayload,
633
715
  density,
@@ -728,6 +810,8 @@ If setting up MemoryRouter for another agent:
728
810
  }
729
811
  }
730
812
  // 4. Call /v1/memory/prepare (no session_id — always search core vault)
813
+ // Message activity — open/extend the keep-warm window.
814
+ noteActivity();
731
815
  const res = await prepareMemory(endpoint, activeKey, {
732
816
  messages: contextPayload,
733
817
  density,
@@ -834,6 +918,8 @@ If setting up MemoryRouter for another agent:
834
918
  log("memoryrouter: skipped ingest — system noise filtered out");
835
919
  return;
836
920
  }
921
+ // Message activity — open/extend the keep-warm window.
922
+ noteActivity();
837
923
  // Fire and forget — don't block message delivery waiting for ingest
838
924
  ingestMemory(endpoint, activeKey, {
839
925
  messages: toStore,
@@ -971,8 +1057,10 @@ If setting up MemoryRouter for another agent:
971
1057
  await setPluginEnabled(api, true);
972
1058
  try {
973
1059
  const changed = await setNativeMemorySearchEnabled(false);
974
- if (changed) console.log(` • Native memory search disabled — MemoryRouter now owns memory_search`);
975
- } catch (err) {
1060
+ if (changed)
1061
+ console.log(` • Native memory search disabled — MemoryRouter now owns memory_search`);
1062
+ }
1063
+ catch (err) {
976
1064
  console.error(` ⚠ Could not disable native memory search: ${err instanceof Error ? err.message : String(err)}`);
977
1065
  }
978
1066
  console.log(`✓ MemoryRouter enabled. Key: ${key.slice(0, 6)}...${key.slice(-3)}`);
@@ -1010,8 +1098,10 @@ If setting up MemoryRouter for another agent:
1010
1098
  // Restore native memory search so the user isn't left with NO memory search.
1011
1099
  try {
1012
1100
  const changed = await setNativeMemorySearchEnabled(true);
1013
- if (changed) console.log(" • Native memory search restored");
1014
- } catch (err) {
1101
+ if (changed)
1102
+ console.log(" • Native memory search restored");
1103
+ }
1104
+ catch (err) {
1015
1105
  console.error(` ⚠ Could not restore native memory search: ${err instanceof Error ? err.message : String(err)}`);
1016
1106
  }
1017
1107
  console.log("✓ MemoryRouter disabled (key cleared).");
@@ -1215,17 +1305,19 @@ If setting up MemoryRouter for another agent:
1215
1305
  start: () => {
1216
1306
  if (memoryKey)
1217
1307
  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?.();
1308
+ serviceRunning = true;
1309
+ // No startup ping and no always-on keepalive: warmth is driven by real
1310
+ // activity. The first message opens the keep-warm window.
1311
+ if (hasAnyKey) {
1312
+ log(keepWarmMs > 0
1313
+ ? `memoryrouter: keep-warm armed (${Math.round(keepWarmMs / 1000)}s after each message)`
1314
+ : "memoryrouter: keep-warm disabled (keepWarmMs: 0)");
1222
1315
  }
1223
1316
  },
1224
1317
  stop: () => {
1225
- if (warmupTimer) {
1226
- clearInterval(warmupTimer);
1227
- warmupTimer = undefined;
1228
- }
1318
+ serviceRunning = false;
1319
+ lastActivityAt = undefined;
1320
+ stopWarmLoop();
1229
1321
  api.logger.info?.("memoryrouter: stopped");
1230
1322
  },
1231
1323
  });
package/index.ts CHANGED
@@ -17,6 +17,16 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
17
17
 
18
18
  const DEFAULT_ENDPOINT = "https://api.memoryrouter.ai";
19
19
 
20
+ /** Keep-warm defaults.
21
+ *
22
+ * Warmth only matters in the minutes right after real activity, so warmup
23
+ * pings are activity-scoped instead of always-on: every message opens a
24
+ * KEEP_WARM window, pings fire at KEEP_WARM_PING_INTERVAL_MS inside it, and
25
+ * the timer is cleared completely once the window closes. An idle gateway
26
+ * sends zero warmup traffic. */
27
+ const DEFAULT_KEEP_WARM_MS = 3 * 60 * 1000; // 3 minutes after the last message
28
+ const KEEP_WARM_PING_INTERVAL_MS = 60 * 1000; // cadence inside the window
29
+
20
30
  /** Strip media-attached references and OpenClaw media instruction text so
21
31
  * old screenshots/photos/audio don't get stored and re-injected forever. */
22
32
  const MEDIA_ATTACHED_RE = /\[media attached[^\]]*\]/gi;
@@ -282,6 +292,9 @@ type MemoryRouterConfig = {
282
292
  logging?: boolean;
283
293
  embeddings?: string;
284
294
  agentKeys?: Record<string, string>;
295
+ /** How long to keep the vault warm after the last message, in ms.
296
+ * Default 180000 (3 minutes). Set 0 to disable warmup pings entirely. */
297
+ keepWarmMs?: number;
285
298
  };
286
299
 
287
300
  // ──────────────────────────────────────────────────────
@@ -501,7 +514,18 @@ const memoryRouterPlugin = {
501
514
  }
502
515
 
503
516
  const hasAnyKey = !!(memoryKey || (agentKeys && Object.keys(agentKeys).length > 0));
517
+
518
+ // Keep-warm window: 0 (or negative) disables warmup pings entirely.
519
+ const keepWarmMs = typeof cfg?.keepWarmMs === "number" && cfg.keepWarmMs >= 0
520
+ ? cfg.keepWarmMs
521
+ : DEFAULT_KEEP_WARM_MS;
504
522
  let warmupTimer: ReturnType<typeof setInterval> | undefined;
523
+ // undefined = no activity yet (never compare against 0; a 0 timestamp is a
524
+ // valid clock value and must not read as "no activity").
525
+ let lastActivityAt: number | undefined;
526
+ // Starts true so keep-warm works even if the host never calls service.start();
527
+ // service.stop() flips it off so no timer can outlive the plugin.
528
+ let serviceRunning = true;
505
529
 
506
530
  function configuredKeys(): string[] {
507
531
  const keys = new Set<string>();
@@ -531,6 +555,41 @@ const memoryRouterPlugin = {
531
555
  }
532
556
  }
533
557
 
558
+ function stopWarmLoop(): void {
559
+ if (warmupTimer) {
560
+ clearInterval(warmupTimer);
561
+ warmupTimer = undefined;
562
+ }
563
+ }
564
+
565
+ /** True while we're still inside the post-message keep-warm window. */
566
+ function withinWarmWindow(): boolean {
567
+ if (keepWarmMs <= 0 || lastActivityAt === undefined) return false;
568
+ return Date.now() - lastActivityAt <= keepWarmMs;
569
+ }
570
+
571
+ /** Called on every message-driven activity (prepare / ingest).
572
+ * Opens (or extends) the keep-warm window and makes sure the loop runs.
573
+ * Once the window lapses with no new activity the interval is cleared, so
574
+ * an idle gateway sends no warmup traffic at all. */
575
+ function noteActivity(): void {
576
+ if (!hasAnyKey || keepWarmMs <= 0 || !serviceRunning) return;
577
+ lastActivityAt = Date.now();
578
+ if (warmupTimer) return; // window extended; existing loop keeps running
579
+ // No immediate ping: the prepare/ingest request that triggered this call
580
+ // already warmed the vault server-side. Pings only cover the idle gaps
581
+ // inside the window.
582
+ warmupTimer = setInterval(() => {
583
+ if (!withinWarmWindow()) {
584
+ log("memoryrouter: keep-warm window elapsed — pings stopped until next message");
585
+ stopWarmLoop();
586
+ return;
587
+ }
588
+ pingWarmup("keepalive");
589
+ }, KEEP_WARM_PING_INTERVAL_MS);
590
+ warmupTimer.unref?.();
591
+ }
592
+
534
593
  void (async () => {
535
594
  await ensureDoctorSafePluginEntry(api);
536
595
  if (hasAnyKey) await selfHealNativeMemorySearch(api);
@@ -684,6 +743,9 @@ If setting up MemoryRouter for another agent:
684
743
  }
685
744
 
686
745
 
746
+ // Message activity — open/extend the keep-warm window.
747
+ noteActivity();
748
+
687
749
  const res = await prepareMemory(endpoint, activeKey, {
688
750
  messages: contextPayload,
689
751
  density,
@@ -795,6 +857,9 @@ If setting up MemoryRouter for another agent:
795
857
 
796
858
  // 4. Call /v1/memory/prepare (no session_id — always search core vault)
797
859
 
860
+ // Message activity — open/extend the keep-warm window.
861
+ noteActivity();
862
+
798
863
  const res = await prepareMemory(endpoint, activeKey, {
799
864
  messages: contextPayload,
800
865
  density,
@@ -914,6 +979,9 @@ If setting up MemoryRouter for another agent:
914
979
  return;
915
980
  }
916
981
 
982
+ // Message activity — open/extend the keep-warm window.
983
+ noteActivity();
984
+
917
985
  // Fire and forget — don't block message delivery waiting for ingest
918
986
  ingestMemory(endpoint, activeKey, {
919
987
  messages: toStore,
@@ -1311,17 +1379,19 @@ If setting up MemoryRouter for another agent:
1311
1379
  id: "mr-memory",
1312
1380
  start: () => {
1313
1381
  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?.();
1382
+ serviceRunning = true;
1383
+ // No startup ping and no always-on keepalive: warmth is driven by real
1384
+ // activity. The first message opens the keep-warm window.
1385
+ if (hasAnyKey) {
1386
+ log(keepWarmMs > 0
1387
+ ? `memoryrouter: keep-warm armed (${Math.round(keepWarmMs / 1000)}s after each message)`
1388
+ : "memoryrouter: keep-warm disabled (keepWarmMs: 0)");
1318
1389
  }
1319
1390
  },
1320
1391
  stop: () => {
1321
- if (warmupTimer) {
1322
- clearInterval(warmupTimer);
1323
- warmupTimer = undefined;
1324
- }
1392
+ serviceRunning = false;
1393
+ lastActivityAt = undefined;
1394
+ stopWarmLoop();
1325
1395
  api.logger.info?.("memoryrouter: stopped");
1326
1396
  },
1327
1397
  });
@@ -21,6 +21,12 @@
21
21
  "placeholder": "high",
22
22
  "advanced": true,
23
23
  "help": "Amount of memory to inject: low (40), high (80), xhigh (160)"
24
+ },
25
+ "keepWarmMs": {
26
+ "label": "Keep Warm Window (ms)",
27
+ "placeholder": "180000",
28
+ "advanced": true,
29
+ "help": "How long to keep your vault warm after each message. Default 180000 (3 min). Set 0 to disable warmup pings."
24
30
  }
25
31
  },
26
32
  "configSchema": {
@@ -50,6 +56,11 @@
50
56
  "enum": ["bge", "qwen"],
51
57
  "description": "Embedding model: bge (1024d, default) or qwen (4096d)"
52
58
  },
59
+ "keepWarmMs": {
60
+ "type": "integer",
61
+ "minimum": 0,
62
+ "description": "Keep the vault warm for this many ms after each message. Default 180000 (3 minutes). 0 disables warmup pings entirely."
63
+ },
53
64
  "agentKeys": {
54
65
  "type": "object",
55
66
  "additionalProperties": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mr-memory",
3
- "version": "3.7.10",
3
+ "version": "3.8.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": [