mr-memory 3.15.0 → 3.16.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.js CHANGED
@@ -12,6 +12,7 @@ import { commitReflections, consolidateCheckout, dateSearch, deleteMemories, del
12
12
  import { isAutoConsolidationSession, maybeTriggerAutoConsolidate } from "./auto-consolidate.js";
13
13
  import { withMemoryInstructions } from "./memory-instructions.js";
14
14
  import { join, resolve, relative, isAbsolute, sep, dirname } from "node:path";
15
+ import path from "node:path";
15
16
  import os from "node:os";
16
17
  const DEFAULT_ENDPOINT = "https://api.memoryrouter.ai";
17
18
  /** Keep-warm defaults.
@@ -291,6 +292,14 @@ const WORKSPACE_FILES = [
291
292
  "IDENTITY.md", "USER.md", "MEMORY.md", "HEARTBEAT.md",
292
293
  "TOOLS.md", "AGENTS.md", "SOUL.md", "BOOTSTRAP.md",
293
294
  ];
295
+ /** Explicit host agent id wins; older hosts fall back to workspace basename. */
296
+ export function isReadOnlyAgent(readOnlyAgents, ctx) {
297
+ const explicit = typeof ctx?.agentId === "string" ? ctx.agentId.trim() : "";
298
+ const dir = typeof ctx?.workspaceDir === "string"
299
+ ? path.basename(ctx.workspaceDir.replace(/[\\/]+$/, "")) : "";
300
+ const agentId = explicit || (dir === "workspace" ? "main" : dir.replace(/^workspace-/, ""));
301
+ return !!agentId && (readOnlyAgents ?? []).includes(agentId);
302
+ }
294
303
  // ──────────────────────────────────────────────────────
295
304
  // Helpers
296
305
  // ──────────────────────────────────────────────────────
@@ -574,6 +583,7 @@ const memoryRouterPlugin = {
574
583
  const logging = cfg?.logging ?? false;
575
584
  const embeddings = cfg?.embeddings; // undefined = bge (default), "qwen" = qwen3-8b
576
585
  const agentKeys = cfg?.agentKeys;
586
+ const readOnlyAgents = cfg?.readOnlyAgents;
577
587
  const autoConsolidate = cfg?.autoConsolidate !== false; // default true
578
588
  const log = (msg) => { if (logging)
579
589
  api.logger.info?.(msg); };
@@ -582,6 +592,8 @@ const memoryRouterPlugin = {
582
592
  // Fire-and-forget; all rails (lock, cooldown, 3-batch bound) live in
583
593
  // auto-consolidate.ts. Failures never propagate into the hook path.
584
594
  function triggerAutoConsolidation(reflection, ctx, activeKey) {
595
+ if (isReadOnlyAgent(readOnlyAgents, ctx))
596
+ return;
585
597
  try {
586
598
  maybeTriggerAutoConsolidate(reflection ?? null, {
587
599
  autoConsolidate,
@@ -879,10 +891,10 @@ If setting up MemoryRouter for another agent:
879
891
  if (data.context) {
880
892
  const elapsed = Date.now() - startMs;
881
893
  api.logger.info?.(`memoryrouter: injected ${data.memories_found || 0} memories (${data.memory_tokens || 0} tokens, ${elapsed}ms) [llm_input]`);
882
- if (elapsed > 2000 && data.metrics)
894
+ if (data.metrics)
883
895
  log(`memoryrouter: prepare metrics ${JSON.stringify(data.metrics)}`);
884
896
  let contextText = data.context;
885
- const nudge = buildDebtNudgeLine(data.reflection);
897
+ const nudge = isReadOnlyAgent(readOnlyAgents, ctx) ? null : buildDebtNudgeLine(data.reflection);
886
898
  if (nudge)
887
899
  contextText = `${contextText}\n\n${nudge}`;
888
900
  const importNudge = buildImportNudgeLine(data.import);
@@ -993,11 +1005,11 @@ If setting up MemoryRouter for another agent:
993
1005
  triggerAutoConsolidation(data.reflection, ctx, activeKey);
994
1006
  if (data.context) {
995
1007
  const elapsed = Date.now() - startMs;
996
- api.logger.info?.(`memoryrouter: injected ${data.memories_found || 0} memories (${data.memory_tokens || 0} tokens, ${elapsed}ms)`);
997
- if (elapsed > 2000 && data.metrics)
1008
+ api.logger.info?.(`memoryrouter: injected ${data.memories_found || 0} memories (${data.memory_tokens || 0} tokens, ${elapsed}ms) agent=${ctx?.agentId || "unknown"} session=${ctx?.sessionKey || "unknown"}`);
1009
+ if (data.metrics)
998
1010
  log(`memoryrouter: prepare metrics ${JSON.stringify(data.metrics)}`);
999
1011
  let contextText = data.context;
1000
- const nudge = buildDebtNudgeLine(data.reflection);
1012
+ const nudge = isReadOnlyAgent(readOnlyAgents, ctx) ? null : buildDebtNudgeLine(data.reflection);
1001
1013
  if (nudge)
1002
1014
  contextText = `${contextText}\n\n${nudge}`;
1003
1015
  const importNudge = buildImportNudgeLine(data.import);
@@ -1026,6 +1038,10 @@ If setting up MemoryRouter for another agent:
1026
1038
  // Core: agent_end — store this turn's conversation
1027
1039
  // ==================================================================
1028
1040
  api.on("agent_end", async (event, ctx) => {
1041
+ if (isReadOnlyAgent(readOnlyAgents, ctx)) {
1042
+ api.logger.info?.(`memoryrouter: ingest skipped read-only agent=${ctx?.agentId || path.basename(ctx?.workspaceDir || "")} session=${ctx?.sessionKey || "unknown"}`);
1043
+ return;
1044
+ }
1029
1045
  // Never store the auto-consolidation session's own chatter as memory.
1030
1046
  if (isAutoConsolidationSession(ctx?.sessionKey))
1031
1047
  return;
package/index.ts CHANGED
@@ -314,7 +314,17 @@ const WORKSPACE_FILES = [
314
314
  "TOOLS.md", "AGENTS.md", "SOUL.md", "BOOTSTRAP.md",
315
315
  ];
316
316
 
317
+ /** Explicit host agent id wins; older hosts fall back to workspace basename. */
318
+ export function isReadOnlyAgent(readOnlyAgents: string[] | undefined, ctx: any): boolean {
319
+ const explicit = typeof ctx?.agentId === "string" ? ctx.agentId.trim() : "";
320
+ const dir = typeof ctx?.workspaceDir === "string"
321
+ ? path.basename(ctx.workspaceDir.replace(/[\\/]+$/, "")) : "";
322
+ const agentId = explicit || (dir === "workspace" ? "main" : dir.replace(/^workspace-/, ""));
323
+ return !!agentId && (readOnlyAgents ?? []).includes(agentId);
324
+ }
325
+
317
326
  type MemoryRouterConfig = {
327
+ readOnlyAgents?: string[];
318
328
  key: string;
319
329
  endpoint?: string;
320
330
  /** xhigh is accepted only for backward compatibility and normalizes to high. */
@@ -632,6 +642,7 @@ const memoryRouterPlugin = {
632
642
  const logging = cfg?.logging ?? false;
633
643
  const embeddings = cfg?.embeddings; // undefined = bge (default), "qwen" = qwen3-8b
634
644
  const agentKeys = cfg?.agentKeys;
645
+ const readOnlyAgents = cfg?.readOnlyAgents;
635
646
  const autoConsolidate = cfg?.autoConsolidate !== false; // default true
636
647
  const log = (msg: string) => { if (logging) api.logger.info?.(msg); };
637
648
 
@@ -640,6 +651,7 @@ const memoryRouterPlugin = {
640
651
  // Fire-and-forget; all rails (lock, cooldown, 3-batch bound) live in
641
652
  // auto-consolidate.ts. Failures never propagate into the hook path.
642
653
  function triggerAutoConsolidation(reflection: ReflectionDebt | undefined, ctx: any, activeKey: string): void {
654
+ if (isReadOnlyAgent(readOnlyAgents, ctx)) return;
643
655
  try {
644
656
  maybeTriggerAutoConsolidate(reflection ?? null, {
645
657
  autoConsolidate,
@@ -946,9 +958,9 @@ If setting up MemoryRouter for another agent:
946
958
  if (data.context) {
947
959
  const elapsed = Date.now() - startMs;
948
960
  api.logger.info?.(`memoryrouter: injected ${data.memories_found || 0} memories (${data.memory_tokens || 0} tokens, ${elapsed}ms) [llm_input]`);
949
- if (elapsed > 2000 && data.metrics) log(`memoryrouter: prepare metrics ${JSON.stringify(data.metrics)}`);
961
+ if (data.metrics) log(`memoryrouter: prepare metrics ${JSON.stringify(data.metrics)}`);
950
962
  let contextText = data.context;
951
- const nudge = buildDebtNudgeLine(data.reflection);
963
+ const nudge = isReadOnlyAgent(readOnlyAgents, ctx) ? null : buildDebtNudgeLine(data.reflection);
952
964
  if (nudge) contextText = `${contextText}\n\n${nudge}`;
953
965
  const importNudge = buildImportNudgeLine(data.import);
954
966
  if (importNudge) contextText = `${contextText}\n\n${importNudge}`;
@@ -1072,10 +1084,10 @@ If setting up MemoryRouter for another agent:
1072
1084
 
1073
1085
  if (data.context) {
1074
1086
  const elapsed = Date.now() - startMs;
1075
- api.logger.info?.(`memoryrouter: injected ${data.memories_found || 0} memories (${data.memory_tokens || 0} tokens, ${elapsed}ms)`);
1076
- if (elapsed > 2000 && data.metrics) log(`memoryrouter: prepare metrics ${JSON.stringify(data.metrics)}`);
1087
+ api.logger.info?.(`memoryrouter: injected ${data.memories_found || 0} memories (${data.memory_tokens || 0} tokens, ${elapsed}ms) agent=${ctx?.agentId || "unknown"} session=${ctx?.sessionKey || "unknown"}`);
1088
+ if (data.metrics) log(`memoryrouter: prepare metrics ${JSON.stringify(data.metrics)}`);
1077
1089
  let contextText = data.context;
1078
- const nudge = buildDebtNudgeLine(data.reflection);
1090
+ const nudge = isReadOnlyAgent(readOnlyAgents, ctx) ? null : buildDebtNudgeLine(data.reflection);
1079
1091
  if (nudge) contextText = `${contextText}\n\n${nudge}`;
1080
1092
  const importNudge = buildImportNudgeLine(data.import);
1081
1093
  if (importNudge) contextText = `${contextText}\n\n${importNudge}`;
@@ -1104,6 +1116,10 @@ If setting up MemoryRouter for another agent:
1104
1116
  // ==================================================================
1105
1117
 
1106
1118
  api.on("agent_end", async (event, ctx) => {
1119
+ if (isReadOnlyAgent(readOnlyAgents, ctx)) {
1120
+ api.logger.info?.(`memoryrouter: ingest skipped read-only agent=${ctx?.agentId || path.basename(ctx?.workspaceDir || "")} session=${ctx?.sessionKey || "unknown"}`);
1121
+ return;
1122
+ }
1107
1123
  // Never store the auto-consolidation session's own chatter as memory.
1108
1124
  if (isAutoConsolidationSession(ctx?.sessionKey)) return;
1109
1125
  const activeKey = resolveKey(ctx);
@@ -13,7 +13,7 @@ export const MEMORY_INSTRUCTIONS = "MemoryRouter is the user’s external, persi
13
13
  */
14
14
  export const OPENCLAW_MAPPING = [
15
15
  "How this maps to OpenClaw: before each prompt the MemoryRouter plugin already runs the RETRIEVE search and adds the results as <memory_context>, so that search already ran; call memory_search only for more detail (tiers and importance are its filters), memory_inspect for the sources behind a reflection, and memory_date_search for time windows.",
16
- "After each turn the plugin saves the user's message and your final reply automatically. There is no separate save tool, so useful details are saved when they appear in the conversation; to supersede outdated information, state the correction plainly so the saved turn records it.",
16
+ "After each turn the plugin saves the user's message and your final reply automatically. There is no separate save tool, so the STORE rule is met by stating useful details plainly in your reply: when the user makes a decision, states a preference, gives a fact about a person, project, or tool, sets a plan or commitment, makes a correction, or reports an outcome, restate it with the exact wording, numbers, and identifiers so the saved turn records it. To supersede outdated information, state the correction and what it replaces.",
17
17
  "If the user asks what was searched or saved, answer truthfully.",
18
18
  ].join(" ");
19
19
  export const OPENCLAW_MEMORY_INSTRUCTIONS = `MemoryRouter instructions\n${MEMORY_INSTRUCTIONS}\n\n${OPENCLAW_MAPPING}`;
@@ -14,7 +14,7 @@ export const MEMORY_INSTRUCTIONS = "MemoryRouter is the user’s external, persi
14
14
  */
15
15
  export const OPENCLAW_MAPPING = [
16
16
  "How this maps to OpenClaw: before each prompt the MemoryRouter plugin already runs the RETRIEVE search and adds the results as <memory_context>, so that search already ran; call memory_search only for more detail (tiers and importance are its filters), memory_inspect for the sources behind a reflection, and memory_date_search for time windows.",
17
- "After each turn the plugin saves the user's message and your final reply automatically. There is no separate save tool, so useful details are saved when they appear in the conversation; to supersede outdated information, state the correction plainly so the saved turn records it.",
17
+ "After each turn the plugin saves the user's message and your final reply automatically. There is no separate save tool, so the STORE rule is met by stating useful details plainly in your reply: when the user makes a decision, states a preference, gives a fact about a person, project, or tool, sets a plan or commitment, makes a correction, or reports an outcome, restate it with the exact wording, numbers, and identifiers so the saved turn records it. To supersede outdated information, state the correction and what it replaces.",
18
18
  "If the user asks what was searched or saved, answer truthfully.",
19
19
  ].join(" ");
20
20
 
@@ -2,7 +2,7 @@
2
2
  "id": "mr-memory",
3
3
  "name": "MemoryRouter Memory",
4
4
  "description": "Persistent memory for OpenClaw, powered by MemoryRouter. Memory that survives sessions, compaction, and model changes.",
5
- "version": "3.15.0",
5
+ "version": "3.16.0",
6
6
  "activation": {
7
7
  "onStartup": true,
8
8
  "onCommands": [
@@ -92,6 +92,13 @@
92
92
  "pattern": "^mk[_-]"
93
93
  },
94
94
  "description": "Optional per-agent MemoryRouter keys, keyed by OpenClaw agent id"
95
+ },
96
+ "readOnlyAgents": {
97
+ "type": "array",
98
+ "items": {
99
+ "type": "string"
100
+ },
101
+ "description": "Agent IDs that receive memory retrieval but skip automatic ingest and automatic consolidation. Explicit tool writes must be separately restricted."
95
102
  }
96
103
  },
97
104
  "required": []
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mr-memory",
3
- "version": "3.15.0",
3
+ "version": "3.16.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": [