mr-memory 3.13.1 → 3.14.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,14 @@ 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.12.0
12
+ openclaw plugins install --force --accept-capabilities npm:mr-memory
13
13
  ```
14
14
 
15
- The explicit `npm:` source and version make the audited install deterministic. `mr-memory@3.12.0` is published on npm. A ClawHub listing has not been verified and is not required for this path.
15
+ For OpenClaw 2026.9.x and later, use a supported Node version (24.16+ on Node 24 LTS, or 26.1+ on Node 26; Node 25 is not supported). The flags explicitly approve the npm trust and capability prompts.
16
+
17
+ The explicit `npm:` source installs the latest published `mr-memory` release from npm. A ClawHub listing has not been verified and is not required for this path.
18
+
19
+ OpenClaw prints `OpenClaw can't verify where this plugin came from ... reason=provenance-invalid` for every plugin that is not an official OpenClaw package. That line is informational: the plugin is installed and loads.
16
20
 
17
21
  ## Setup
18
22
 
@@ -24,14 +28,34 @@ openclaw mr upload # Upload workspace + session history
24
28
  ## Commands
25
29
 
26
30
  ```bash
31
+ openclaw mr doctor # Repair plugin registration + native memory-search settings
27
32
  openclaw mr status # Vault stats
28
33
  openclaw mr upload # Upload workspace + sessions
29
34
  openclaw mr upload --brain ~/.notopenclaw # Upload from a different agent
30
35
  openclaw mr off # Disable
31
36
  openclaw mr delete # Clear vault
32
37
  openclaw mr <key> # Enable with key
38
+ openclaw mr low # Up to 21 memories (12 raw + 6 tier 2 + 3 tier 3)
39
+ openclaw mr medium # Up to 43 memories (25 + 12 + 6), balanced default
40
+ openclaw mr high # Up to 87 memories (50 + 25 + 12)
33
41
  ```
34
42
 
43
+ Loading the plugin never writes your OpenClaw config. Setup (`openclaw mr <key>`) and `openclaw mr doctor` explicitly repair its enabled entry and allowlist membership, disable native memory search when a memory key is configured, and on OpenClaw 2026.10+ grant `plugins.entries.mr-memory.hooks.allowConversationAccess` (without that grant OpenClaw silently drops the `before_prompt_build`, `llm_input` and `agent_end` hooks of every non-bundled plugin, so no memory would be injected or captured). Existing users who see a config-repair hint should run `openclaw mr doctor`, then restart their gateway. On current OpenClaw this uses the host’s locked config mutation API and `memory.search.enabled`; older OpenClaw and John’s Harness use `agents.defaults.memorySearch.enabled`. Repairs never restart the gateway automatically. Native memory flush is left untouched.
44
+
45
+ The plugin declares no OpenClaw version floor on purpose. OpenClaw evaluates `openclaw.compat.pluginApi` and `openclaw.install.minHostVersion` against its own version string, and when that string is not semver (for example `OPENCLAW_VERSION=latest` exported in the shell, as the official installer documents) a declared floor makes OpenClaw skip the plugin and the install fails with `package owner "mr-memory" has no authoritative runtime child list`. mr-memory detects host capabilities at runtime instead.
46
+
47
+ On OpenClaw 2026.10 and later, non-bundled plugins only receive conversation hooks (`before_prompt_build`, `llm_input`, `agent_end`) when `plugins.entries.mr-memory.hooks.allowConversationAccess` is `true`. Setup and `doctor` grant it on those hosts; without it the plugin loads but never injects or captures memory. Run `openclaw plugins doctor` to confirm no hooks are blocked. The `provenance-invalid` warning from `openclaw plugins doctor` is informational: it applies to every npm plugin that is not in OpenClaw’s official catalog.
48
+
49
+ ### Compatibility (3.14.0)
50
+
51
+ `mr-memory` no longer declares a host version floor (`openclaw.compat.pluginApi` / `install.minHostVersion`). OpenClaw 2026.10 reads the host version from `OPENCLAW_VERSION` when it is exported, and the official install instructions export `OPENCLAW_VERSION=latest`; a non-semver string made the host skip the plugin and fail the install with `no authoritative runtime child list`. The plugin detects host capabilities at runtime instead. The manifest now declares its six tools under `contracts.tools` (2026.10 rejects undeclared tool registrations) and asks for gateway startup activation.
52
+
53
+ ## Memory Density
54
+
55
+ The current density choices are `low`, `medium`, and `high`. New installs and configs without an explicit density use `medium`, the balanced default. Existing explicit `low` or `high` settings are unchanged. Legacy `xhigh` configs remain valid so upgrades do not break the gateway, but the plugin normalizes `xhigh` to `high` and no longer advertises it as a current option.
56
+
57
+ The totals are tier-aware retrieval budgets, not raw-memory-only limits. Actual results can be lower when fewer relevant memories match.
58
+
35
59
  ## Upload Options
36
60
 
37
61
  ```bash
@@ -102,19 +126,24 @@ After `openclaw mr <key>`, config is stored at:
102
126
  ```json
103
127
  {
104
128
  "plugins": {
129
+ "allow": ["mr-memory"],
105
130
  "entries": {
106
- "memoryrouter": {
131
+ "mr-memory": {
107
132
  "enabled": true,
133
+ "hooks": { "allowConversationAccess": true },
108
134
  "config": {
109
135
  "key": "mk_xxx",
110
136
  "endpoint": "https://api.memoryrouter.ai"
111
137
  }
112
138
  }
113
139
  }
114
- }
140
+ },
141
+ "memory": { "search": { "enabled": false } }
115
142
  }
116
143
  ```
117
144
 
145
+ `plugins.allow` is only extended when it already exists, and the `hooks` grant is only written on OpenClaw builds whose schema accepts it.
146
+
118
147
  ## Cross-AI and MCP Memory
119
148
 
120
149
  The OpenClaw plugin uses relay hooks. Separately, the same MemoryRouter vault can connect to supported ChatGPT, Claude, Claude Code, Codex, and compatible MCP clients. Each host follows its own integration contract. Hosted ChatGPT and Claude connectors are model-directed, do not passively capture every conversation, and do not import old conversations just by connecting.
package/index.js CHANGED
@@ -290,10 +290,55 @@ const WORKSPACE_FILES = [
290
290
  "IDENTITY.md", "USER.md", "MEMORY.md", "HEARTBEAT.md",
291
291
  "TOOLS.md", "AGENTS.md", "SOUL.md", "BOOTSTRAP.md",
292
292
  ];
293
+ // ──────────────────────────────────────────────────────
294
+ // Helpers
295
+ // ──────────────────────────────────────────────────────
296
+ /** Current densities are low, medium, and high. Legacy xhigh remains valid in
297
+ * existing configs but is deliberately hidden from current choices and sent
298
+ * to MemoryRouter as high. Missing or invalid values use the balanced medium
299
+ * default. */
300
+ function normalizeMemoryDensity(value) {
301
+ switch (value) {
302
+ case "low":
303
+ case "medium":
304
+ case "high":
305
+ return value;
306
+ case "xhigh":
307
+ return "high";
308
+ default:
309
+ return "medium";
310
+ }
311
+ }
293
312
  function openClawConfigPath() {
294
313
  return process.env.OPENCLAW_CONFIG_PATH || process.env.OPENCLAW_CONFIG || join(os.homedir(), ".openclaw", "openclaw.json");
295
314
  }
296
- async function readOpenClawConfigFile() {
315
+ function hostConfigApi(api) {
316
+ return api.runtime?.config;
317
+ }
318
+ // OpenClaw's current()/mutateConfigFile API accompanies the canonical memory
319
+ // owner schema (memory.search). Older OpenClaw and John's Harness retain
320
+ // agents.defaults.memorySearch. Do not use the host version: forks version
321
+ // independently of their upstream config schema.
322
+ function usesCanonicalMemorySearch(api) {
323
+ return typeof hostConfigApi(api)?.current === "function";
324
+ }
325
+ function nativeMemorySearchDisabled(api) {
326
+ const config = api.config;
327
+ // Metadata-only registration may deliberately throw on api.runtime access.
328
+ // Read the provided config only; runtime capability detection belongs in CLI actions.
329
+ return config?.memory?.search?.enabled === false
330
+ || config?.agents?.defaults?.memorySearch?.enabled === false;
331
+ }
332
+ // Only explicit CLI actions call these helpers. Loading/registering the plugin
333
+ // must never mutate the host config (including fire-and-forget writes).
334
+ async function readOpenClawConfigFile(api) {
335
+ const runtimeConfig = hostConfigApi(api);
336
+ if (typeof runtimeConfig?.current === "function") {
337
+ return structuredClone(runtimeConfig.current());
338
+ }
339
+ if (typeof runtimeConfig?.loadConfig === "function") {
340
+ return structuredClone(runtimeConfig.loadConfig());
341
+ }
297
342
  try {
298
343
  return JSON.parse(await readFile(openClawConfigPath(), "utf8"));
299
344
  }
@@ -303,7 +348,13 @@ async function readOpenClawConfigFile() {
303
348
  throw err;
304
349
  }
305
350
  }
306
- async function writeOpenClawConfigFile(config) {
351
+ async function writeOpenClawConfigFile(api, config) {
352
+ const runtimeConfig = hostConfigApi(api);
353
+ if (typeof runtimeConfig?.writeConfigFile === "function") {
354
+ await runtimeConfig.writeConfigFile(config);
355
+ return;
356
+ }
357
+ // Legacy hosts only: no native config writer is exposed.
307
358
  const configPath = openClawConfigPath();
308
359
  await mkdir(dirname(configPath), { recursive: true });
309
360
  try {
@@ -316,51 +367,74 @@ async function writeOpenClawConfigFile(config) {
316
367
  await writeFile(configPath, `${JSON.stringify(config, null, 2)}
317
368
  `, "utf8");
318
369
  }
370
+ async function mutateOpenClawConfigFile(api, mutate) {
371
+ const runtimeConfig = hostConfigApi(api);
372
+ if (typeof runtimeConfig?.mutateConfigFile === "function") {
373
+ // The host rewrites the file on every mutation call, even a no-op one.
374
+ // Probe a snapshot first so repeated `mr doctor` runs stay write-free.
375
+ if (typeof runtimeConfig.current === "function") {
376
+ const probe = structuredClone(runtimeConfig.current());
377
+ if (!mutate(probe))
378
+ return false;
379
+ }
380
+ // Let the host acquire its lock and supply the latest authored config.
381
+ // Never auto-restart a gateway as a side effect of a plugin CLI command.
382
+ const result = await runtimeConfig.mutateConfigFile({
383
+ base: "source",
384
+ afterWrite: { mode: "none", reason: "MemoryRouter explicit CLI configuration; restart manually to apply" },
385
+ mutate,
386
+ });
387
+ return result.result === true;
388
+ }
389
+ const config = await readOpenClawConfigFile(api);
390
+ if (!mutate(config))
391
+ return false;
392
+ await writeOpenClawConfigFile(api, config);
393
+ return true;
394
+ }
319
395
  // ──────────────────────────────────────────────────────
320
396
  // Full-replace: disable OpenClaw's native memory search so MemoryRouter
321
397
  // owns the memory_search tool (no name conflict, no native embeddings 429).
322
398
  // memoryFlush (durable MEMORY.md writes) is intentionally left untouched.
323
- // Global + subagents: writes agents.defaults.memorySearch.enabled.
324
- // ──────────────────────────────────────────────────────
325
- async function setNativeMemorySearchEnabled(enabled) {
326
- const config = await readOpenClawConfigFile();
327
- config.agents ??= {};
328
- config.agents.defaults ??= {};
329
- config.agents.defaults.memorySearch ??= {};
330
- if (config.agents.defaults.memorySearch.enabled === enabled)
331
- return false; // no-op
332
- config.agents.defaults.memorySearch.enabled = enabled;
333
- await writeOpenClawConfigFile(config);
334
- return true;
399
+ // Global defaults cover subagents unless the user explicitly overrides them.
400
+ async function setNativeMemorySearchEnabled(api, enabled) {
401
+ return mutateOpenClawConfigFile(api, (config) => {
402
+ let search;
403
+ if (usesCanonicalMemorySearch(api)) {
404
+ config.memory ??= {};
405
+ config.memory.search ??= {};
406
+ search = config.memory.search;
407
+ }
408
+ else {
409
+ config.agents ??= {};
410
+ config.agents.defaults ??= {};
411
+ config.agents.defaults.memorySearch ??= {};
412
+ search = config.agents.defaults.memorySearch;
413
+ }
414
+ if (search.enabled === enabled)
415
+ return false;
416
+ search.enabled = enabled;
417
+ return true;
418
+ });
335
419
  }
336
- // Boot-time self-heal: if a key is configured but native search is still on,
337
- // flip it off so already-installed users are fixed on restart after updating
338
- // (no need to re-run `openclaw mr <key>`).
420
+ // Explicit setup/doctor repair only. With no key, leave native search available.
339
421
  async function selfHealNativeMemorySearch(api) {
340
- try {
341
- const config = await readOpenClawConfigFile();
342
- const ms = config?.agents?.defaults?.memorySearch;
343
- if (ms?.enabled === false)
344
- return; // already disabled — nothing to do
345
- const changed = await setNativeMemorySearchEnabled(false);
346
- if (changed) {
347
- api.logger.info?.("memoryrouter: disabled native memory search (full-replace). Restart gateway to apply.");
348
- }
349
- }
350
- catch (err) {
351
- api.logger.info?.(`memoryrouter: self-heal skipped — ${err instanceof Error ? err.message : String(err)}`);
352
- }
422
+ const changed = await setNativeMemorySearchEnabled(api, false);
423
+ if (changed)
424
+ console.log(" • Native memory search disabled — MemoryRouter now owns memory_search");
353
425
  }
354
- async function updateOpenClawPluginEntry(api, patch) {
355
- const config = await readOpenClawConfigFile();
356
- config.plugins ??= {};
357
- config.plugins.entries ??= {};
358
- config.plugins.entries[api.id] ??= {};
359
- config.plugins.entries[api.id] = {
360
- ...config.plugins.entries[api.id],
361
- ...patch,
362
- };
363
- await writeOpenClawConfigFile(config);
426
+ async function updateOpenClawPluginEntry(api, patch, mergeConfig = false) {
427
+ await mutateOpenClawConfigFile(api, (config) => {
428
+ config.plugins ??= {};
429
+ config.plugins.entries ??= {};
430
+ config.plugins.entries[api.id] ??= { enabled: true };
431
+ const entry = config.plugins.entries[api.id];
432
+ if (patch.config)
433
+ entry.config = mergeConfig ? { ...(entry.config || {}), ...patch.config } : patch.config;
434
+ if (typeof patch.enabled === "boolean")
435
+ entry.enabled = patch.enabled;
436
+ return true;
437
+ });
364
438
  }
365
439
  async function setPluginConfig(api, config) {
366
440
  const compat = api;
@@ -379,45 +453,65 @@ async function setPluginEnabled(api, enabled) {
379
453
  await updateOpenClawPluginEntry(api, { enabled });
380
454
  }
381
455
  async function mergePluginConfig(api, patch) {
382
- const current = await readOpenClawConfigFile();
456
+ if (typeof api.updatePluginConfig !== "function") {
457
+ // Merge the focused patch inside the host's transaction, not into a stale
458
+ // runtime snapshot which could overwrite another command's latest changes.
459
+ await updateOpenClawPluginEntry(api, { config: patch }, true);
460
+ return;
461
+ }
462
+ const current = await readOpenClawConfigFile(api);
383
463
  const existing = current.plugins?.entries?.[api.id]?.config;
384
464
  await setPluginConfig(api, {
385
465
  ...(existing && typeof existing === "object" && !Array.isArray(existing) ? existing : {}),
386
466
  ...patch,
387
467
  });
388
468
  }
389
- // Keep mr-memory as a first-class plugin config entry without clobbering user settings.
390
- // OpenClaw doctor validates plugin entries against discoverable manifests, so the entry
391
- // must be present, enabled, and allowlisted when an allowlist exists.
469
+ // OpenClaw 2026.10+ drops every conversation hook (before_prompt_build,
470
+ // llm_input, agent_end) from a non-bundled plugin unless the user grants
471
+ // plugins.entries.<id>.hooks.allowConversationAccess. Without it mr-memory
472
+ // loads but never injects or captures a single memory. The grant is only
473
+ // written by the explicit CLI actions, on hosts whose schema accepts it
474
+ // (the same hosts that expose current()/mutateConfigFile). Older hosts and
475
+ // John's Harness reject unknown keys under hooks, so they are left alone.
476
+ function hostGatesConversationHooks(api) {
477
+ return usesCanonicalMemorySearch(api);
478
+ }
479
+ function conversationAccessGranted(config, pluginId) {
480
+ return config?.plugins?.entries?.[pluginId]?.hooks?.allowConversationAccess === true;
481
+ }
482
+ async function ensureConversationHookAccess(api) {
483
+ if (!hostGatesConversationHooks(api))
484
+ return false;
485
+ return mutateOpenClawConfigFile(api, (config) => {
486
+ config.plugins ??= {};
487
+ config.plugins.entries ??= {};
488
+ config.plugins.entries[api.id] ??= { enabled: true };
489
+ const entry = config.plugins.entries[api.id];
490
+ if (entry.hooks?.allowConversationAccess === true)
491
+ return false;
492
+ entry.hooks = { ...(entry.hooks && typeof entry.hooks === "object" ? entry.hooks : {}), allowConversationAccess: true };
493
+ return true;
494
+ });
495
+ }
496
+ // Explicit setup/doctor repair; preserve all config, use native plugin APIs first.
497
+ // Never call this from register(), a hook, or service.start().
392
498
  async function ensureDoctorSafePluginEntry(api) {
393
- try {
394
- const config = await readOpenClawConfigFile();
395
- const plugins = config.plugins ?? {};
396
- const entries = plugins.entries ?? {};
397
- const existingEntry = entries[api.id] && typeof entries[api.id] === "object" && !Array.isArray(entries[api.id])
398
- ? entries[api.id]
399
- : {};
400
- const nextPlugins = {
401
- ...plugins,
402
- entries: {
403
- ...entries,
404
- [api.id]: {
405
- ...existingEntry,
406
- enabled: true,
407
- },
408
- },
409
- };
410
- if (Array.isArray(plugins.allow) && !plugins.allow.includes(api.id)) {
411
- nextPlugins.allow = [...plugins.allow, api.id];
412
- }
413
- const next = { ...config, plugins: nextPlugins };
414
- if (JSON.stringify(next.plugins) === JSON.stringify(config.plugins))
415
- return;
416
- await writeOpenClawConfigFile(next);
417
- }
418
- catch (err) {
419
- api.logger.info?.(`memoryrouter: doctor-safe registration skipped — ${err instanceof Error ? err.message : String(err)}`);
499
+ const config = await readOpenClawConfigFile(api);
500
+ if (config.plugins?.entries?.[api.id]?.enabled !== true) {
501
+ await setPluginEnabled(api, true);
420
502
  }
503
+ // Plugin-enable APIs do not necessarily maintain plugins.allow. Re-read after
504
+ // enabling so a host API's config update cannot be overwritten by a stale copy.
505
+ await mutateOpenClawConfigFile(api, (current) => {
506
+ const allow = current.plugins?.allow;
507
+ if (!Array.isArray(allow) || allow.includes(api.id))
508
+ return false;
509
+ current.plugins.allow = [...allow, api.id];
510
+ return true;
511
+ });
512
+ const granted = await ensureConversationHookAccess(api);
513
+ if (granted)
514
+ console.log(" • Conversation hook access granted — MemoryRouter can inject and capture memory");
421
515
  }
422
516
  /**
423
517
  * Read all workspace files and return as a single text blob for token counting.
@@ -474,7 +568,7 @@ const memoryRouterPlugin = {
474
568
  const cfg = api.pluginConfig;
475
569
  const endpoint = cfg?.endpoint?.replace(/\/v1\/?$/, "") || DEFAULT_ENDPOINT;
476
570
  const memoryKey = cfg?.key;
477
- const density = cfg?.density || "high";
571
+ const density = normalizeMemoryDensity(cfg?.density);
478
572
  const mode = cfg?.mode || "relay";
479
573
  const logging = cfg?.logging ?? false;
480
574
  const embeddings = cfg?.embeddings; // undefined = bge (default), "qwen" = qwen3-8b
@@ -589,11 +683,24 @@ const memoryRouterPlugin = {
589
683
  }, KEEP_WARM_PING_INTERVAL_MS);
590
684
  warmupTimer.unref?.();
591
685
  }
592
- void (async () => {
593
- await ensureDoctorSafePluginEntry(api);
594
- if (hasAnyKey)
595
- await selfHealNativeMemorySearch(api);
596
- })();
686
+ // Host install/update/doctor may load plugins inside a guarded config
687
+ // transaction. Even an unawaited write here races its base-hash check.
688
+ // Configuration repair is owned exclusively by the explicit CLI actions.
689
+ // Hook gating is a runtime capability check; metadata-only registration may
690
+ // throw on api.runtime access, so read the plugin entry from api.config only.
691
+ const hookGrantMissing = hasAnyKey && !conversationAccessGranted(api.config, api.id)
692
+ && (() => { try {
693
+ return hostGatesConversationHooks(api);
694
+ }
695
+ catch {
696
+ return false;
697
+ } })();
698
+ if (hasAnyKey && (!nativeMemorySearchDisabled(api)
699
+ || api.config?.plugins?.entries?.[api.id]?.enabled !== true
700
+ || (Array.isArray(api.config?.plugins?.allow) && !api.config.plugins.allow.includes(api.id))
701
+ || hookGrantMissing)) {
702
+ api.logger.info?.("memoryrouter: config repair available — run: openclaw mr doctor");
703
+ }
597
704
  if (hasAnyKey) {
598
705
  if (memoryKey)
599
706
  api.logger.info?.(`memoryrouter: active (key: ${memoryKey.slice(0, 6)}...)`);
@@ -688,6 +795,8 @@ If setting up MemoryRouter for another agent:
688
795
  // On stock OpenClaw, the return value is ignored (fire-and-forget).
689
796
  // When PR #24122 merges, OpenClaw will use the returned prependContext.
690
797
  // This gives forward compatibility — no plugin update needed.
798
+ // Current stock SDK typings still mark llm_input as observation-only;
799
+ // runtime builds that support returned context consume this value.
691
800
  api.on("llm_input", async (event, ctx) => {
692
801
  // Auto-consolidation runs are single-purpose tool sessions: no memory
693
802
  // injection (their prompt must stay exactly the consolidation
@@ -1087,7 +1196,7 @@ If setting up MemoryRouter for another agent:
1087
1196
  }
1088
1197
  },
1089
1198
  };
1090
- });
1199
+ }, { name: "memory_search" });
1091
1200
  // memory_date_search: calls MR /v1/memory/date-search (time-window retrieval)
1092
1201
  api.registerTool((ctx) => {
1093
1202
  const toolKey = resolveKey(ctx);
@@ -1185,7 +1294,7 @@ If setting up MemoryRouter for another agent:
1185
1294
  }
1186
1295
  },
1187
1296
  };
1188
- });
1297
+ }, { name: "memory_date_search" });
1189
1298
  // ==================================================================
1190
1299
  // Tools: memory_reflect + memory_commit (consolidation, two-tool shape)
1191
1300
  // Mirrors the MCP consolidate_memories / commit_reflections pair.
@@ -1258,7 +1367,7 @@ If setting up MemoryRouter for another agent:
1258
1367
  }
1259
1368
  },
1260
1369
  };
1261
- });
1370
+ }, { name: "memory_reflect" });
1262
1371
  // memory_commit: commit wrapper (idempotent; 409 means the lease expired)
1263
1372
  api.registerTool((ctx) => {
1264
1373
  const toolKey = resolveKey(ctx);
@@ -1345,7 +1454,7 @@ If setting up MemoryRouter for another agent:
1345
1454
  }
1346
1455
  },
1347
1456
  };
1348
- });
1457
+ }, { name: "memory_commit" });
1349
1458
  // memory_inspect: read-only lineage drill-down (curation surface, ids allowed)
1350
1459
  api.registerTool((ctx) => {
1351
1460
  const toolKey = resolveKey(ctx);
@@ -1382,7 +1491,7 @@ If setting up MemoryRouter for another agent:
1382
1491
  }
1383
1492
  },
1384
1493
  };
1385
- });
1494
+ }, { name: "memory_inspect" });
1386
1495
  // memory_get — exact copy of OpenClaw's implementation (pure local file read)
1387
1496
  api.registerTool((ctx) => {
1388
1497
  const workspaceDir = ctx.workspaceDir || "";
@@ -1421,7 +1530,7 @@ If setting up MemoryRouter for another agent:
1421
1530
  }
1422
1531
  },
1423
1532
  };
1424
- });
1533
+ }, { name: "memory_get" });
1425
1534
  } // end if (hasAnyKey)
1426
1535
  // ==================================================================
1427
1536
  // CLI Commands
@@ -1434,22 +1543,17 @@ If setting up MemoryRouter for another agent:
1434
1543
  }
1435
1544
  try {
1436
1545
  await mergePluginConfig(api, { key });
1437
- await setPluginEnabled(api, true);
1438
- try {
1439
- const changed = await setNativeMemorySearchEnabled(false);
1440
- if (changed)
1441
- console.log(` • Native memory search disabled — MemoryRouter now owns memory_search`);
1442
- }
1443
- catch (err) {
1444
- console.error(` ⚠ Could not disable native memory search: ${err instanceof Error ? err.message : String(err)}`);
1445
- }
1546
+ await ensureDoctorSafePluginEntry(api);
1547
+ await selfHealNativeMemorySearch(api);
1446
1548
  console.log(`✓ MemoryRouter enabled. Key: ${key.slice(0, 6)}...${key.slice(-3)}`);
1447
1549
  console.log(` • memory_search + memory_date_search + memory_get tools registered via plugin`);
1550
+ console.log(` • Restart the gateway to apply: openclaw gateway restart`);
1448
1551
  console.log(`\nRun: openclaw mr upload to upload your memories`);
1449
1552
  }
1450
1553
  catch (err) {
1451
1554
  const message = err instanceof Error ? err.message : String(err);
1452
1555
  console.error(`Failed to enable MemoryRouter: ${message}`);
1556
+ process.exitCode = 1;
1453
1557
  }
1454
1558
  };
1455
1559
  const mr = program
@@ -1467,6 +1571,25 @@ If setting up MemoryRouter for another agent:
1467
1571
  .description("Enable MemoryRouter with a memory key (alias)")
1468
1572
  .argument("<key>", "Your MemoryRouter memory key (mk_xxx)")
1469
1573
  .action(async (key) => { await applyKey(key); });
1574
+ mr.command("doctor")
1575
+ .description("Repair MemoryRouter plugin registration and native memory-search settings")
1576
+ .action(async () => {
1577
+ try {
1578
+ await ensureDoctorSafePluginEntry(api);
1579
+ const current = await readOpenClawConfigFile(api);
1580
+ const saved = current.plugins?.entries?.[api.id]?.config;
1581
+ const hasSavedKey = !!(saved?.key || Object.values(saved?.agentKeys ?? {}).some(Boolean));
1582
+ if (hasSavedKey)
1583
+ await selfHealNativeMemorySearch(api);
1584
+ console.log("✓ MemoryRouter config repaired. Restart gateway to apply changed settings.");
1585
+ if (!hasSavedKey)
1586
+ console.log(" • No key configured — run: openclaw mr <key>");
1587
+ }
1588
+ catch (err) {
1589
+ console.error(`MemoryRouter doctor failed: ${err instanceof Error ? err.message : String(err)}`);
1590
+ process.exitCode = 1;
1591
+ }
1592
+ });
1470
1593
  mr.command("off")
1471
1594
  .description("Disable MemoryRouter (removes key, keeps CLI available)")
1472
1595
  .action(async () => {
@@ -1477,7 +1600,7 @@ If setting up MemoryRouter for another agent:
1477
1600
  // Note: we intentionally do NOT call setPluginEnabled(api, false)
1478
1601
  // Restore native memory search so the user isn't left with NO memory search.
1479
1602
  try {
1480
- const changed = await setNativeMemorySearchEnabled(true);
1603
+ const changed = await setNativeMemorySearchEnabled(api, true);
1481
1604
  if (changed)
1482
1605
  console.log(" • Native memory search restored");
1483
1606
  }
@@ -1499,11 +1622,12 @@ If setting up MemoryRouter for another agent:
1499
1622
  console.log("Then restart gateway:");
1500
1623
  console.log(" openclaw gateway restart");
1501
1624
  });
1502
- // Density commands
1625
+ // Current density commands. Legacy xhigh configs still load as high,
1626
+ // but xhigh is intentionally not advertised as a command.
1503
1627
  for (const [name, desc] of [
1504
- ["xhigh", "Set density to xhigh (160 results, ~50k tokens)"],
1505
- ["high", "Set density to high (80 results, ~24k tokens) [default]"],
1506
- ["low", "Set density to low (40 results, ~12k tokens)"],
1628
+ ["low", "Set density to low: up to 21 memories (12 raw + 6 tier 2 + 3 tier 3)"],
1629
+ ["medium", "Set density to medium: up to 43 memories (25 raw + 12 tier 2 + 6 tier 3) [default, balanced]"],
1630
+ ["high", "Set density to high: up to 87 memories (50 raw + 25 tier 2 + 12 tier 3)"],
1507
1631
  ]) {
1508
1632
  mr.command(name)
1509
1633
  .description(desc)
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 { commitReflections, consolidateCheckout, dateSearch, deleteMemories, deleteMemoryIds, getMemoryStats, ingestMemory, inspectMemory, prepareMemory, searchMemory, warmupMemory } from "./memoryrouter-api.js";
12
+ import { commitReflections, consolidateCheckout, dateSearch, deleteMemories, deleteMemoryIds, getMemoryStats, ingestMemory, inspectMemory, prepareMemory, searchMemory, warmupMemory, type MemoryDensity } from "./memoryrouter-api.js";
13
13
  import { isAutoConsolidationSession, maybeTriggerAutoConsolidate } from "./auto-consolidate.js";
14
14
  import { join, resolve, relative, isAbsolute, sep, dirname } from "node:path";
15
15
  import path from "node:path";
@@ -316,7 +316,8 @@ const WORKSPACE_FILES = [
316
316
  type MemoryRouterConfig = {
317
317
  key: string;
318
318
  endpoint?: string;
319
- density?: "low" | "high" | "xhigh";
319
+ /** xhigh is accepted only for backward compatibility and normalizes to high. */
320
+ density?: MemoryDensity | "xhigh";
320
321
  mode?: "relay" | "proxy";
321
322
  logging?: boolean;
322
323
  embeddings?: string;
@@ -334,6 +335,23 @@ type MemoryRouterConfig = {
334
335
  // Helpers
335
336
  // ──────────────────────────────────────────────────────
336
337
 
338
+ /** Current densities are low, medium, and high. Legacy xhigh remains valid in
339
+ * existing configs but is deliberately hidden from current choices and sent
340
+ * to MemoryRouter as high. Missing or invalid values use the balanced medium
341
+ * default. */
342
+ function normalizeMemoryDensity(value?: MemoryRouterConfig["density"]): MemoryDensity {
343
+ switch (value) {
344
+ case "low":
345
+ case "medium":
346
+ case "high":
347
+ return value;
348
+ case "xhigh":
349
+ return "high";
350
+ default:
351
+ return "medium";
352
+ }
353
+ }
354
+
337
355
  type CompatApi = OpenClawPluginApi & {
338
356
  updatePluginConfig?: (config: Record<string, unknown>) => Promise<void>;
339
357
  updatePluginEnabled?: (enabled: boolean) => Promise<void>;
@@ -345,7 +363,47 @@ function openClawConfigPath(): string {
345
363
  return process.env.OPENCLAW_CONFIG_PATH || process.env.OPENCLAW_CONFIG || join(os.homedir(), ".openclaw", "openclaw.json");
346
364
  }
347
365
 
348
- async function readOpenClawConfigFile(): Promise<JsonObject> {
366
+ type HostConfigApi = {
367
+ loadConfig?: () => JsonObject;
368
+ writeConfigFile?: (config: JsonObject) => Promise<unknown>;
369
+ current?: () => JsonObject;
370
+ mutateConfigFile?: (params: {
371
+ base: "source";
372
+ afterWrite: { mode: "none"; reason: string };
373
+ mutate: (draft: JsonObject) => boolean;
374
+ }) => Promise<{ result?: boolean }>;
375
+ };
376
+
377
+ function hostConfigApi(api: OpenClawPluginApi): HostConfigApi | undefined {
378
+ return api.runtime?.config as unknown as HostConfigApi | undefined;
379
+ }
380
+
381
+ // OpenClaw's current()/mutateConfigFile API accompanies the canonical memory
382
+ // owner schema (memory.search). Older OpenClaw and John's Harness retain
383
+ // agents.defaults.memorySearch. Do not use the host version: forks version
384
+ // independently of their upstream config schema.
385
+ function usesCanonicalMemorySearch(api: OpenClawPluginApi): boolean {
386
+ return typeof hostConfigApi(api)?.current === "function";
387
+ }
388
+
389
+ function nativeMemorySearchDisabled(api: OpenClawPluginApi): boolean {
390
+ const config = api.config as JsonObject;
391
+ // Metadata-only registration may deliberately throw on api.runtime access.
392
+ // Read the provided config only; runtime capability detection belongs in CLI actions.
393
+ return config?.memory?.search?.enabled === false
394
+ || config?.agents?.defaults?.memorySearch?.enabled === false;
395
+ }
396
+
397
+ // Only explicit CLI actions call these helpers. Loading/registering the plugin
398
+ // must never mutate the host config (including fire-and-forget writes).
399
+ async function readOpenClawConfigFile(api: OpenClawPluginApi): Promise<JsonObject> {
400
+ const runtimeConfig = hostConfigApi(api);
401
+ if (typeof runtimeConfig?.current === "function") {
402
+ return structuredClone(runtimeConfig.current());
403
+ }
404
+ if (typeof runtimeConfig?.loadConfig === "function") {
405
+ return structuredClone(runtimeConfig.loadConfig());
406
+ }
349
407
  try {
350
408
  return JSON.parse(await readFile(openClawConfigPath(), "utf8"));
351
409
  } catch (err: any) {
@@ -354,7 +412,13 @@ async function readOpenClawConfigFile(): Promise<JsonObject> {
354
412
  }
355
413
  }
356
414
 
357
- async function writeOpenClawConfigFile(config: JsonObject): Promise<void> {
415
+ async function writeOpenClawConfigFile(api: OpenClawPluginApi, config: JsonObject): Promise<void> {
416
+ const runtimeConfig = hostConfigApi(api);
417
+ if (typeof runtimeConfig?.writeConfigFile === "function") {
418
+ await runtimeConfig.writeConfigFile(config);
419
+ return;
420
+ }
421
+ // Legacy hosts only: no native config writer is exposed.
358
422
  const configPath = openClawConfigPath();
359
423
  await mkdir(dirname(configPath), { recursive: true });
360
424
  try {
@@ -366,54 +430,70 @@ async function writeOpenClawConfigFile(config: JsonObject): Promise<void> {
366
430
  `, "utf8");
367
431
  }
368
432
 
433
+ async function mutateOpenClawConfigFile(api: OpenClawPluginApi, mutate: (draft: JsonObject) => boolean): Promise<boolean> {
434
+ const runtimeConfig = hostConfigApi(api);
435
+ if (typeof runtimeConfig?.mutateConfigFile === "function") {
436
+ // The host rewrites the file on every mutation call, even a no-op one.
437
+ // Probe a snapshot first so repeated `mr doctor` runs stay write-free.
438
+ if (typeof runtimeConfig.current === "function") {
439
+ const probe = structuredClone(runtimeConfig.current());
440
+ if (!mutate(probe)) return false;
441
+ }
442
+ // Let the host acquire its lock and supply the latest authored config.
443
+ // Never auto-restart a gateway as a side effect of a plugin CLI command.
444
+ const result = await runtimeConfig.mutateConfigFile({
445
+ base: "source",
446
+ afterWrite: { mode: "none", reason: "MemoryRouter explicit CLI configuration; restart manually to apply" },
447
+ mutate,
448
+ });
449
+ return result.result === true;
450
+ }
451
+ const config = await readOpenClawConfigFile(api);
452
+ if (!mutate(config)) return false;
453
+ await writeOpenClawConfigFile(api, config);
454
+ return true;
455
+ }
456
+
369
457
  // ──────────────────────────────────────────────────────
370
458
  // Full-replace: disable OpenClaw's native memory search so MemoryRouter
371
459
  // owns the memory_search tool (no name conflict, no native embeddings 429).
372
460
  // memoryFlush (durable MEMORY.md writes) is intentionally left untouched.
373
- // Global + subagents: writes agents.defaults.memorySearch.enabled.
374
- // ──────────────────────────────────────────────────────
375
- async function setNativeMemorySearchEnabled(enabled: boolean): Promise<boolean> {
376
- const config = await readOpenClawConfigFile();
377
- config.agents ??= {};
378
- config.agents.defaults ??= {};
379
- config.agents.defaults.memorySearch ??= {};
380
- if (config.agents.defaults.memorySearch.enabled === enabled) return false; // no-op
381
- config.agents.defaults.memorySearch.enabled = enabled;
382
- await writeOpenClawConfigFile(config);
383
- return true;
461
+ // Global defaults cover subagents unless the user explicitly overrides them.
462
+ async function setNativeMemorySearchEnabled(api: OpenClawPluginApi, enabled: boolean): Promise<boolean> {
463
+ return mutateOpenClawConfigFile(api, (config) => {
464
+ let search: JsonObject;
465
+ if (usesCanonicalMemorySearch(api)) {
466
+ config.memory ??= {};
467
+ config.memory.search ??= {};
468
+ search = config.memory.search;
469
+ } else {
470
+ config.agents ??= {};
471
+ config.agents.defaults ??= {};
472
+ config.agents.defaults.memorySearch ??= {};
473
+ search = config.agents.defaults.memorySearch;
474
+ }
475
+ if (search.enabled === enabled) return false;
476
+ search.enabled = enabled;
477
+ return true;
478
+ });
384
479
  }
385
480
 
386
- // Boot-time self-heal: if a key is configured but native search is still on,
387
- // flip it off so already-installed users are fixed on restart after updating
388
- // (no need to re-run `openclaw mr <key>`).
481
+ // Explicit setup/doctor repair only. With no key, leave native search available.
389
482
  async function selfHealNativeMemorySearch(api: OpenClawPluginApi): Promise<void> {
390
- try {
391
- const config = await readOpenClawConfigFile();
392
- const ms = config?.agents?.defaults?.memorySearch;
393
- if (ms?.enabled === false) return; // already disabled — nothing to do
394
- const changed = await setNativeMemorySearchEnabled(false);
395
- if (changed) {
396
- api.logger.info?.(
397
- "memoryrouter: disabled native memory search (full-replace). Restart gateway to apply.",
398
- );
399
- }
400
- } catch (err) {
401
- api.logger.info?.(
402
- `memoryrouter: self-heal skipped — ${err instanceof Error ? err.message : String(err)}`,
403
- );
404
- }
483
+ const changed = await setNativeMemorySearchEnabled(api, false);
484
+ if (changed) console.log(" • Native memory search disabled — MemoryRouter now owns memory_search");
405
485
  }
406
486
 
407
- async function updateOpenClawPluginEntry(api: OpenClawPluginApi, patch: JsonObject): Promise<void> {
408
- const config = await readOpenClawConfigFile();
409
- config.plugins ??= {};
410
- config.plugins.entries ??= {};
411
- config.plugins.entries[api.id] ??= {};
412
- config.plugins.entries[api.id] = {
413
- ...config.plugins.entries[api.id],
414
- ...patch,
415
- };
416
- await writeOpenClawConfigFile(config);
487
+ async function updateOpenClawPluginEntry(api: OpenClawPluginApi, patch: JsonObject, mergeConfig = false): Promise<void> {
488
+ await mutateOpenClawConfigFile(api, (config) => {
489
+ config.plugins ??= {};
490
+ config.plugins.entries ??= {};
491
+ config.plugins.entries[api.id] ??= { enabled: true };
492
+ const entry = config.plugins.entries[api.id];
493
+ if (patch.config) entry.config = mergeConfig ? { ...(entry.config || {}), ...patch.config } : patch.config;
494
+ if (typeof patch.enabled === "boolean") entry.enabled = patch.enabled;
495
+ return true;
496
+ });
417
497
  }
418
498
 
419
499
  async function setPluginConfig(api: OpenClawPluginApi, config: Record<string, unknown>): Promise<void> {
@@ -435,7 +515,13 @@ async function setPluginEnabled(api: OpenClawPluginApi, enabled: boolean): Promi
435
515
  }
436
516
 
437
517
  async function mergePluginConfig(api: OpenClawPluginApi, patch: Record<string, unknown>): Promise<void> {
438
- const current = await readOpenClawConfigFile();
518
+ if (typeof (api as CompatApi).updatePluginConfig !== "function") {
519
+ // Merge the focused patch inside the host's transaction, not into a stale
520
+ // runtime snapshot which could overwrite another command's latest changes.
521
+ await updateOpenClawPluginEntry(api, { config: patch }, true);
522
+ return;
523
+ }
524
+ const current = await readOpenClawConfigFile(api);
439
525
  const existing = current.plugins?.entries?.[api.id]?.config;
440
526
  await setPluginConfig(api, {
441
527
  ...(existing && typeof existing === "object" && !Array.isArray(existing) ? existing : {}),
@@ -443,36 +529,51 @@ async function mergePluginConfig(api: OpenClawPluginApi, patch: Record<string, u
443
529
  });
444
530
  }
445
531
 
446
- // Keep mr-memory as a first-class plugin config entry without clobbering user settings.
447
- // OpenClaw doctor validates plugin entries against discoverable manifests, so the entry
448
- // must be present, enabled, and allowlisted when an allowlist exists.
532
+ // OpenClaw 2026.10+ drops every conversation hook (before_prompt_build,
533
+ // llm_input, agent_end) from a non-bundled plugin unless the user grants
534
+ // plugins.entries.<id>.hooks.allowConversationAccess. Without it mr-memory
535
+ // loads but never injects or captures a single memory. The grant is only
536
+ // written by the explicit CLI actions, on hosts whose schema accepts it
537
+ // (the same hosts that expose current()/mutateConfigFile). Older hosts and
538
+ // John's Harness reject unknown keys under hooks, so they are left alone.
539
+ function hostGatesConversationHooks(api: OpenClawPluginApi): boolean {
540
+ return usesCanonicalMemorySearch(api);
541
+ }
542
+
543
+ function conversationAccessGranted(config: JsonObject, pluginId: string): boolean {
544
+ return config?.plugins?.entries?.[pluginId]?.hooks?.allowConversationAccess === true;
545
+ }
546
+
547
+ async function ensureConversationHookAccess(api: OpenClawPluginApi): Promise<boolean> {
548
+ if (!hostGatesConversationHooks(api)) return false;
549
+ return mutateOpenClawConfigFile(api, (config) => {
550
+ config.plugins ??= {};
551
+ config.plugins.entries ??= {};
552
+ config.plugins.entries[api.id] ??= { enabled: true };
553
+ const entry = config.plugins.entries[api.id];
554
+ if (entry.hooks?.allowConversationAccess === true) return false;
555
+ entry.hooks = { ...(entry.hooks && typeof entry.hooks === "object" ? entry.hooks : {}), allowConversationAccess: true };
556
+ return true;
557
+ });
558
+ }
559
+
560
+ // Explicit setup/doctor repair; preserve all config, use native plugin APIs first.
561
+ // Never call this from register(), a hook, or service.start().
449
562
  async function ensureDoctorSafePluginEntry(api: OpenClawPluginApi): Promise<void> {
450
- try {
451
- const config = await readOpenClawConfigFile();
452
- const plugins = config.plugins ?? {};
453
- const entries = plugins.entries ?? {};
454
- const existingEntry = entries[api.id] && typeof entries[api.id] === "object" && !Array.isArray(entries[api.id])
455
- ? entries[api.id]
456
- : {};
457
- const nextPlugins: JsonObject = {
458
- ...plugins,
459
- entries: {
460
- ...entries,
461
- [api.id]: {
462
- ...existingEntry,
463
- enabled: true,
464
- },
465
- },
466
- };
467
- if (Array.isArray(plugins.allow) && !plugins.allow.includes(api.id)) {
468
- nextPlugins.allow = [...plugins.allow, api.id];
469
- }
470
- const next = { ...config, plugins: nextPlugins };
471
- if (JSON.stringify(next.plugins) === JSON.stringify(config.plugins)) return;
472
- await writeOpenClawConfigFile(next);
473
- } catch (err) {
474
- api.logger.info?.(`memoryrouter: doctor-safe registration skipped — ${err instanceof Error ? err.message : String(err)}`);
563
+ const config = await readOpenClawConfigFile(api);
564
+ if (config.plugins?.entries?.[api.id]?.enabled !== true) {
565
+ await setPluginEnabled(api, true);
475
566
  }
567
+ // Plugin-enable APIs do not necessarily maintain plugins.allow. Re-read after
568
+ // enabling so a host API's config update cannot be overwritten by a stale copy.
569
+ await mutateOpenClawConfigFile(api, (current) => {
570
+ const allow = current.plugins?.allow;
571
+ if (!Array.isArray(allow) || allow.includes(api.id)) return false;
572
+ current.plugins.allow = [...allow, api.id];
573
+ return true;
574
+ });
575
+ const granted = await ensureConversationHookAccess(api);
576
+ if (granted) console.log(" • Conversation hook access granted — MemoryRouter can inject and capture memory");
476
577
  }
477
578
 
478
579
  /**
@@ -525,7 +626,7 @@ const memoryRouterPlugin = {
525
626
  const cfg = api.pluginConfig as MemoryRouterConfig | undefined;
526
627
  const endpoint = cfg?.endpoint?.replace(/\/v1\/?$/, "") || DEFAULT_ENDPOINT;
527
628
  const memoryKey = cfg?.key;
528
- const density = cfg?.density || "high";
629
+ const density = normalizeMemoryDensity(cfg?.density);
529
630
  const mode = cfg?.mode || "relay";
530
631
  const logging = cfg?.logging ?? false;
531
632
  const embeddings = cfg?.embeddings; // undefined = bge (default), "qwen" = qwen3-8b
@@ -642,10 +743,19 @@ const memoryRouterPlugin = {
642
743
  warmupTimer.unref?.();
643
744
  }
644
745
 
645
- void (async () => {
646
- await ensureDoctorSafePluginEntry(api);
647
- if (hasAnyKey) await selfHealNativeMemorySearch(api);
648
- })();
746
+ // Host install/update/doctor may load plugins inside a guarded config
747
+ // transaction. Even an unawaited write here races its base-hash check.
748
+ // Configuration repair is owned exclusively by the explicit CLI actions.
749
+ // Hook gating is a runtime capability check; metadata-only registration may
750
+ // throw on api.runtime access, so read the plugin entry from api.config only.
751
+ const hookGrantMissing = hasAnyKey && !conversationAccessGranted(api.config as JsonObject, api.id)
752
+ && (() => { try { return hostGatesConversationHooks(api); } catch { return false; } })();
753
+ if (hasAnyKey && (!nativeMemorySearchDisabled(api)
754
+ || api.config?.plugins?.entries?.[api.id]?.enabled !== true
755
+ || (Array.isArray(api.config?.plugins?.allow) && !api.config.plugins.allow.includes(api.id))
756
+ || hookGrantMissing)) {
757
+ api.logger.info?.("memoryrouter: config repair available — run: openclaw mr doctor");
758
+ }
649
759
 
650
760
  if (hasAnyKey) {
651
761
  if (memoryKey) api.logger.info?.(`memoryrouter: active (key: ${memoryKey.slice(0, 6)}...)`);
@@ -744,7 +854,9 @@ If setting up MemoryRouter for another agent:
744
854
  // On stock OpenClaw, the return value is ignored (fire-and-forget).
745
855
  // When PR #24122 merges, OpenClaw will use the returned prependContext.
746
856
  // This gives forward compatibility — no plugin update needed.
747
- api.on("llm_input", async (event, ctx) => {
857
+ // Current stock SDK typings still mark llm_input as observation-only;
858
+ // runtime builds that support returned context consume this value.
859
+ (api as any).on("llm_input", async (event: any, ctx: any) => {
748
860
  // Auto-consolidation runs are single-purpose tool sessions: no memory
749
861
  // injection (their prompt must stay exactly the consolidation
750
862
  // mission) and no re-trigger loops.
@@ -1167,7 +1279,7 @@ If setting up MemoryRouter for another agent:
1167
1279
  }
1168
1280
  },
1169
1281
  };
1170
- });
1282
+ }, { name: "memory_search" });
1171
1283
 
1172
1284
  // memory_date_search: calls MR /v1/memory/date-search (time-window retrieval)
1173
1285
  api.registerTool((ctx) => {
@@ -1270,7 +1382,7 @@ If setting up MemoryRouter for another agent:
1270
1382
  }
1271
1383
  },
1272
1384
  };
1273
- });
1385
+ }, { name: "memory_date_search" });
1274
1386
 
1275
1387
  // ==================================================================
1276
1388
  // Tools: memory_reflect + memory_commit (consolidation, two-tool shape)
@@ -1352,7 +1464,7 @@ If setting up MemoryRouter for another agent:
1352
1464
  }
1353
1465
  },
1354
1466
  };
1355
- });
1467
+ }, { name: "memory_reflect" });
1356
1468
 
1357
1469
  // memory_commit: commit wrapper (idempotent; 409 means the lease expired)
1358
1470
  api.registerTool((ctx) => {
@@ -1440,7 +1552,7 @@ If setting up MemoryRouter for another agent:
1440
1552
  }
1441
1553
  },
1442
1554
  };
1443
- });
1555
+ }, { name: "memory_commit" });
1444
1556
 
1445
1557
  // memory_inspect: read-only lineage drill-down (curation surface, ids allowed)
1446
1558
  api.registerTool((ctx) => {
@@ -1475,7 +1587,7 @@ If setting up MemoryRouter for another agent:
1475
1587
  }
1476
1588
  },
1477
1589
  };
1478
- });
1590
+ }, { name: "memory_inspect" });
1479
1591
 
1480
1592
  // memory_get — exact copy of OpenClaw's implementation (pure local file read)
1481
1593
  api.registerTool((ctx) => {
@@ -1513,7 +1625,7 @@ If setting up MemoryRouter for another agent:
1513
1625
  }
1514
1626
  },
1515
1627
  };
1516
- });
1628
+ }, { name: "memory_get" });
1517
1629
  } // end if (hasAnyKey)
1518
1630
 
1519
1631
  // ==================================================================
@@ -1529,19 +1641,16 @@ If setting up MemoryRouter for another agent:
1529
1641
  }
1530
1642
  try {
1531
1643
  await mergePluginConfig(api, { key });
1532
- await setPluginEnabled(api, true);
1533
- try {
1534
- const changed = await setNativeMemorySearchEnabled(false);
1535
- if (changed) console.log(` • Native memory search disabled — MemoryRouter now owns memory_search`);
1536
- } catch (err) {
1537
- console.error(` ⚠ Could not disable native memory search: ${err instanceof Error ? err.message : String(err)}`);
1538
- }
1644
+ await ensureDoctorSafePluginEntry(api);
1645
+ await selfHealNativeMemorySearch(api);
1539
1646
  console.log(`✓ MemoryRouter enabled. Key: ${key.slice(0, 6)}...${key.slice(-3)}`);
1540
1647
  console.log(` • memory_search + memory_date_search + memory_get tools registered via plugin`);
1648
+ console.log(` • Restart the gateway to apply: openclaw gateway restart`);
1541
1649
  console.log(`\nRun: openclaw mr upload to upload your memories`);
1542
1650
  } catch (err) {
1543
1651
  const message = err instanceof Error ? err.message : String(err);
1544
1652
  console.error(`Failed to enable MemoryRouter: ${message}`);
1653
+ process.exitCode = 1;
1545
1654
  }
1546
1655
  };
1547
1656
 
@@ -1559,6 +1668,23 @@ If setting up MemoryRouter for another agent:
1559
1668
  .argument("<key>", "Your MemoryRouter memory key (mk_xxx)")
1560
1669
  .action(async (key: string) => { await applyKey(key); });
1561
1670
 
1671
+ mr.command("doctor")
1672
+ .description("Repair MemoryRouter plugin registration and native memory-search settings")
1673
+ .action(async () => {
1674
+ try {
1675
+ await ensureDoctorSafePluginEntry(api);
1676
+ const current = await readOpenClawConfigFile(api);
1677
+ const saved = current.plugins?.entries?.[api.id]?.config;
1678
+ const hasSavedKey = !!(saved?.key || Object.values(saved?.agentKeys ?? {}).some(Boolean));
1679
+ if (hasSavedKey) await selfHealNativeMemorySearch(api);
1680
+ console.log("✓ MemoryRouter config repaired. Restart gateway to apply changed settings.");
1681
+ if (!hasSavedKey) console.log(" • No key configured — run: openclaw mr <key>");
1682
+ } catch (err) {
1683
+ console.error(`MemoryRouter doctor failed: ${err instanceof Error ? err.message : String(err)}`);
1684
+ process.exitCode = 1;
1685
+ }
1686
+ });
1687
+
1562
1688
  mr.command("off")
1563
1689
  .description("Disable MemoryRouter (removes key, keeps CLI available)")
1564
1690
  .action(async () => {
@@ -1569,7 +1695,7 @@ If setting up MemoryRouter for another agent:
1569
1695
  // Note: we intentionally do NOT call setPluginEnabled(api, false)
1570
1696
  // Restore native memory search so the user isn't left with NO memory search.
1571
1697
  try {
1572
- const changed = await setNativeMemorySearchEnabled(true);
1698
+ const changed = await setNativeMemorySearchEnabled(api, true);
1573
1699
  if (changed) console.log(" • Native memory search restored");
1574
1700
  } catch (err) {
1575
1701
  console.error(` ⚠ Could not restore native memory search: ${err instanceof Error ? err.message : String(err)}`);
@@ -1590,11 +1716,12 @@ If setting up MemoryRouter for another agent:
1590
1716
  console.log(" openclaw gateway restart");
1591
1717
  });
1592
1718
 
1593
- // Density commands
1719
+ // Current density commands. Legacy xhigh configs still load as high,
1720
+ // but xhigh is intentionally not advertised as a command.
1594
1721
  for (const [name, desc] of [
1595
- ["xhigh", "Set density to xhigh (160 results, ~50k tokens)"],
1596
- ["high", "Set density to high (80 results, ~24k tokens) [default]"],
1597
- ["low", "Set density to low (40 results, ~12k tokens)"],
1722
+ ["low", "Set density to low: up to 21 memories (12 raw + 6 tier 2 + 3 tier 3)"],
1723
+ ["medium", "Set density to medium: up to 43 memories (25 raw + 12 tier 2 + 6 tier 3) [default, balanced]"],
1724
+ ["high", "Set density to high: up to 87 memories (50 raw + 25 tier 2 + 12 tier 3)"],
1598
1725
  ] as const) {
1599
1726
  mr.command(name)
1600
1727
  .description(desc)
@@ -1,8 +1,10 @@
1
1
  const JSON_HEADERS = { "Content-Type": "application/json" };
2
2
 
3
+ export type MemoryDensity = "low" | "medium" | "high";
4
+
3
5
  type PreparePayload = {
4
6
  messages: Array<{ role: string; content: string }>;
5
- density: string;
7
+ density: MemoryDensity;
6
8
  embeddings?: string;
7
9
  };
8
10
 
@@ -2,37 +2,43 @@
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
- "icon": "https://memoryrouter.ai/logo.png",
6
- "uiHints": {
7
- "key": {
8
- "label": "Memory Key",
9
- "sensitive": true,
10
- "placeholder": "mk_xxx",
11
- "help": "Your MemoryRouter API key (get one at memoryrouter.ai)"
12
- },
13
- "endpoint": {
14
- "label": "API Endpoint",
15
- "placeholder": "https://api.memoryrouter.ai/v1",
16
- "advanced": true,
17
- "help": "Override for self-hosted MemoryRouter"
18
- },
19
- "density": {
20
- "label": "Memory Density",
21
- "placeholder": "high",
22
- "advanced": true,
23
- "help": "Amount of memory to inject: low (40), high (80), xhigh (160)"
5
+ "version": "3.14.0",
6
+ "activation": {
7
+ "onStartup": true,
8
+ "onCommands": [
9
+ "mr"
10
+ ],
11
+ "onCapabilities": [
12
+ "hook",
13
+ "tool"
14
+ ]
15
+ },
16
+ "cliCommands": [
17
+ {
18
+ "name": "mr",
19
+ "description": "MemoryRouter memory commands",
20
+ "hasSubcommands": true
21
+ }
22
+ ],
23
+ "contracts": {
24
+ "tools": [
25
+ "memory_search",
26
+ "memory_date_search",
27
+ "memory_get",
28
+ "memory_reflect",
29
+ "memory_commit",
30
+ "memory_inspect"
31
+ ]
32
+ },
33
+ "toolMetadata": {
34
+ "memory_get": {
35
+ "replaySafe": true
24
36
  },
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."
37
+ "memory_inspect": {
38
+ "replaySafe": true
30
39
  },
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."
40
+ "memory_commit": {
41
+ "sideEffecting": true
36
42
  }
37
43
  },
38
44
  "configSchema": {
@@ -48,18 +54,26 @@
48
54
  },
49
55
  "density": {
50
56
  "type": "string",
51
- "enum": ["low", "high", "xhigh"]
57
+ "pattern": "^(low|medium|high|xhigh)$",
58
+ "default": "medium",
59
+ "description": "Current densities are low, medium, and high. Legacy xhigh configs remain valid and are treated as high."
52
60
  },
53
61
  "logging": {
54
62
  "type": "boolean"
55
63
  },
56
64
  "mode": {
57
65
  "type": "string",
58
- "enum": ["relay", "proxy"]
66
+ "enum": [
67
+ "relay",
68
+ "proxy"
69
+ ]
59
70
  },
60
71
  "embeddings": {
61
72
  "type": "string",
62
- "enum": ["bge", "qwen"],
73
+ "enum": [
74
+ "bge",
75
+ "qwen"
76
+ ],
63
77
  "description": "Embedding model: bge (1024d, default) or qwen (4096d)"
64
78
  },
65
79
  "keepWarmMs": {
@@ -81,5 +95,38 @@
81
95
  }
82
96
  },
83
97
  "required": []
84
- }
98
+ },
99
+ "uiHints": {
100
+ "key": {
101
+ "label": "Memory Key",
102
+ "sensitive": true,
103
+ "placeholder": "mk_xxx",
104
+ "help": "Your MemoryRouter API key (get one at memoryrouter.ai)"
105
+ },
106
+ "endpoint": {
107
+ "label": "API Endpoint",
108
+ "placeholder": "https://api.memoryrouter.ai/v1",
109
+ "advanced": true,
110
+ "help": "Override for self-hosted MemoryRouter"
111
+ },
112
+ "density": {
113
+ "label": "Memory Density",
114
+ "placeholder": "medium",
115
+ "advanced": true,
116
+ "help": "Current choices: low, up to 21 memories (12 raw + 6 tier 2 + 3 tier 3); medium, up to 43 (25 + 12 + 6), balanced default; high, up to 87 (50 + 25 + 12)."
117
+ },
118
+ "keepWarmMs": {
119
+ "label": "Keep Warm Window (ms)",
120
+ "placeholder": "180000",
121
+ "advanced": true,
122
+ "help": "How long to keep your vault warm after each message. Default 180000 (3 min). Set 0 to disable warmup pings."
123
+ },
124
+ "autoConsolidate": {
125
+ "label": "Automatic Consolidation",
126
+ "placeholder": "true",
127
+ "advanced": true,
128
+ "help": "Automatically consolidate memories in the background when debt crosses the server threshold. Default true."
129
+ }
130
+ },
131
+ "icon": "https://memoryrouter.ai/logo.png"
85
132
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mr-memory",
3
- "version": "3.13.1",
3
+ "version": "3.14.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": [
@@ -49,12 +49,8 @@
49
49
  "extensions": [
50
50
  "./index.js"
51
51
  ],
52
- "compat": {
53
- "pluginApi": ">=2026.3.7"
54
- },
55
52
  "install": {
56
- "npmSpec": "mr-memory",
57
- "minHostVersion": ">=2026.3.7"
53
+ "npmSpec": "mr-memory"
58
54
  }
59
55
  },
60
56
  "main": "./index.js",