mr-memory 3.13.2 → 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.13.2
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.13.2` 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,6 +28,7 @@ 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
@@ -35,6 +40,16 @@ openclaw mr medium # Up to 43 memories (25 + 12 + 6), balanced default
35
40
  openclaw mr high # Up to 87 memories (50 + 25 + 12)
36
41
  ```
37
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
+
38
53
  ## Memory Density
39
54
 
40
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.
@@ -111,19 +126,24 @@ After `openclaw mr <key>`, config is stored at:
111
126
  ```json
112
127
  {
113
128
  "plugins": {
129
+ "allow": ["mr-memory"],
114
130
  "entries": {
115
- "memoryrouter": {
131
+ "mr-memory": {
116
132
  "enabled": true,
133
+ "hooks": { "allowConversationAccess": true },
117
134
  "config": {
118
135
  "key": "mk_xxx",
119
136
  "endpoint": "https://api.memoryrouter.ai"
120
137
  }
121
138
  }
122
139
  }
123
- }
140
+ },
141
+ "memory": { "search": { "enabled": false } }
124
142
  }
125
143
  ```
126
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
+
127
147
  ## Cross-AI and MCP Memory
128
148
 
129
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
@@ -312,7 +312,33 @@ function normalizeMemoryDensity(value) {
312
312
  function openClawConfigPath() {
313
313
  return process.env.OPENCLAW_CONFIG_PATH || process.env.OPENCLAW_CONFIG || join(os.homedir(), ".openclaw", "openclaw.json");
314
314
  }
315
- 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
+ }
316
342
  try {
317
343
  return JSON.parse(await readFile(openClawConfigPath(), "utf8"));
318
344
  }
@@ -322,7 +348,13 @@ async function readOpenClawConfigFile() {
322
348
  throw err;
323
349
  }
324
350
  }
325
- 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.
326
358
  const configPath = openClawConfigPath();
327
359
  await mkdir(dirname(configPath), { recursive: true });
328
360
  try {
@@ -335,51 +367,74 @@ async function writeOpenClawConfigFile(config) {
335
367
  await writeFile(configPath, `${JSON.stringify(config, null, 2)}
336
368
  `, "utf8");
337
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
+ }
338
395
  // ──────────────────────────────────────────────────────
339
396
  // Full-replace: disable OpenClaw's native memory search so MemoryRouter
340
397
  // owns the memory_search tool (no name conflict, no native embeddings 429).
341
398
  // memoryFlush (durable MEMORY.md writes) is intentionally left untouched.
342
- // Global + subagents: writes agents.defaults.memorySearch.enabled.
343
- // ──────────────────────────────────────────────────────
344
- async function setNativeMemorySearchEnabled(enabled) {
345
- const config = await readOpenClawConfigFile();
346
- config.agents ??= {};
347
- config.agents.defaults ??= {};
348
- config.agents.defaults.memorySearch ??= {};
349
- if (config.agents.defaults.memorySearch.enabled === enabled)
350
- return false; // no-op
351
- config.agents.defaults.memorySearch.enabled = enabled;
352
- await writeOpenClawConfigFile(config);
353
- 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
+ });
354
419
  }
355
- // Boot-time self-heal: if a key is configured but native search is still on,
356
- // flip it off so already-installed users are fixed on restart after updating
357
- // (no need to re-run `openclaw mr <key>`).
420
+ // Explicit setup/doctor repair only. With no key, leave native search available.
358
421
  async function selfHealNativeMemorySearch(api) {
359
- try {
360
- const config = await readOpenClawConfigFile();
361
- const ms = config?.agents?.defaults?.memorySearch;
362
- if (ms?.enabled === false)
363
- return; // already disabled — nothing to do
364
- const changed = await setNativeMemorySearchEnabled(false);
365
- if (changed) {
366
- api.logger.info?.("memoryrouter: disabled native memory search (full-replace). Restart gateway to apply.");
367
- }
368
- }
369
- catch (err) {
370
- api.logger.info?.(`memoryrouter: self-heal skipped — ${err instanceof Error ? err.message : String(err)}`);
371
- }
422
+ const changed = await setNativeMemorySearchEnabled(api, false);
423
+ if (changed)
424
+ console.log(" • Native memory search disabled — MemoryRouter now owns memory_search");
372
425
  }
373
- async function updateOpenClawPluginEntry(api, patch) {
374
- const config = await readOpenClawConfigFile();
375
- config.plugins ??= {};
376
- config.plugins.entries ??= {};
377
- config.plugins.entries[api.id] ??= {};
378
- config.plugins.entries[api.id] = {
379
- ...config.plugins.entries[api.id],
380
- ...patch,
381
- };
382
- 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
+ });
383
438
  }
384
439
  async function setPluginConfig(api, config) {
385
440
  const compat = api;
@@ -398,45 +453,65 @@ async function setPluginEnabled(api, enabled) {
398
453
  await updateOpenClawPluginEntry(api, { enabled });
399
454
  }
400
455
  async function mergePluginConfig(api, patch) {
401
- 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);
402
463
  const existing = current.plugins?.entries?.[api.id]?.config;
403
464
  await setPluginConfig(api, {
404
465
  ...(existing && typeof existing === "object" && !Array.isArray(existing) ? existing : {}),
405
466
  ...patch,
406
467
  });
407
468
  }
408
- // Keep mr-memory as a first-class plugin config entry without clobbering user settings.
409
- // OpenClaw doctor validates plugin entries against discoverable manifests, so the entry
410
- // 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().
411
498
  async function ensureDoctorSafePluginEntry(api) {
412
- try {
413
- const config = await readOpenClawConfigFile();
414
- const plugins = config.plugins ?? {};
415
- const entries = plugins.entries ?? {};
416
- const existingEntry = entries[api.id] && typeof entries[api.id] === "object" && !Array.isArray(entries[api.id])
417
- ? entries[api.id]
418
- : {};
419
- const nextPlugins = {
420
- ...plugins,
421
- entries: {
422
- ...entries,
423
- [api.id]: {
424
- ...existingEntry,
425
- enabled: true,
426
- },
427
- },
428
- };
429
- if (Array.isArray(plugins.allow) && !plugins.allow.includes(api.id)) {
430
- nextPlugins.allow = [...plugins.allow, api.id];
431
- }
432
- const next = { ...config, plugins: nextPlugins };
433
- if (JSON.stringify(next.plugins) === JSON.stringify(config.plugins))
434
- return;
435
- await writeOpenClawConfigFile(next);
436
- }
437
- catch (err) {
438
- 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);
439
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");
440
515
  }
441
516
  /**
442
517
  * Read all workspace files and return as a single text blob for token counting.
@@ -608,11 +683,24 @@ const memoryRouterPlugin = {
608
683
  }, KEEP_WARM_PING_INTERVAL_MS);
609
684
  warmupTimer.unref?.();
610
685
  }
611
- void (async () => {
612
- await ensureDoctorSafePluginEntry(api);
613
- if (hasAnyKey)
614
- await selfHealNativeMemorySearch(api);
615
- })();
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
+ }
616
704
  if (hasAnyKey) {
617
705
  if (memoryKey)
618
706
  api.logger.info?.(`memoryrouter: active (key: ${memoryKey.slice(0, 6)}...)`);
@@ -1108,7 +1196,7 @@ If setting up MemoryRouter for another agent:
1108
1196
  }
1109
1197
  },
1110
1198
  };
1111
- });
1199
+ }, { name: "memory_search" });
1112
1200
  // memory_date_search: calls MR /v1/memory/date-search (time-window retrieval)
1113
1201
  api.registerTool((ctx) => {
1114
1202
  const toolKey = resolveKey(ctx);
@@ -1206,7 +1294,7 @@ If setting up MemoryRouter for another agent:
1206
1294
  }
1207
1295
  },
1208
1296
  };
1209
- });
1297
+ }, { name: "memory_date_search" });
1210
1298
  // ==================================================================
1211
1299
  // Tools: memory_reflect + memory_commit (consolidation, two-tool shape)
1212
1300
  // Mirrors the MCP consolidate_memories / commit_reflections pair.
@@ -1279,7 +1367,7 @@ If setting up MemoryRouter for another agent:
1279
1367
  }
1280
1368
  },
1281
1369
  };
1282
- });
1370
+ }, { name: "memory_reflect" });
1283
1371
  // memory_commit: commit wrapper (idempotent; 409 means the lease expired)
1284
1372
  api.registerTool((ctx) => {
1285
1373
  const toolKey = resolveKey(ctx);
@@ -1366,7 +1454,7 @@ If setting up MemoryRouter for another agent:
1366
1454
  }
1367
1455
  },
1368
1456
  };
1369
- });
1457
+ }, { name: "memory_commit" });
1370
1458
  // memory_inspect: read-only lineage drill-down (curation surface, ids allowed)
1371
1459
  api.registerTool((ctx) => {
1372
1460
  const toolKey = resolveKey(ctx);
@@ -1403,7 +1491,7 @@ If setting up MemoryRouter for another agent:
1403
1491
  }
1404
1492
  },
1405
1493
  };
1406
- });
1494
+ }, { name: "memory_inspect" });
1407
1495
  // memory_get — exact copy of OpenClaw's implementation (pure local file read)
1408
1496
  api.registerTool((ctx) => {
1409
1497
  const workspaceDir = ctx.workspaceDir || "";
@@ -1442,7 +1530,7 @@ If setting up MemoryRouter for another agent:
1442
1530
  }
1443
1531
  },
1444
1532
  };
1445
- });
1533
+ }, { name: "memory_get" });
1446
1534
  } // end if (hasAnyKey)
1447
1535
  // ==================================================================
1448
1536
  // CLI Commands
@@ -1455,22 +1543,17 @@ If setting up MemoryRouter for another agent:
1455
1543
  }
1456
1544
  try {
1457
1545
  await mergePluginConfig(api, { key });
1458
- await setPluginEnabled(api, true);
1459
- try {
1460
- const changed = await setNativeMemorySearchEnabled(false);
1461
- if (changed)
1462
- console.log(` • Native memory search disabled — MemoryRouter now owns memory_search`);
1463
- }
1464
- catch (err) {
1465
- console.error(` ⚠ Could not disable native memory search: ${err instanceof Error ? err.message : String(err)}`);
1466
- }
1546
+ await ensureDoctorSafePluginEntry(api);
1547
+ await selfHealNativeMemorySearch(api);
1467
1548
  console.log(`✓ MemoryRouter enabled. Key: ${key.slice(0, 6)}...${key.slice(-3)}`);
1468
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`);
1469
1551
  console.log(`\nRun: openclaw mr upload to upload your memories`);
1470
1552
  }
1471
1553
  catch (err) {
1472
1554
  const message = err instanceof Error ? err.message : String(err);
1473
1555
  console.error(`Failed to enable MemoryRouter: ${message}`);
1556
+ process.exitCode = 1;
1474
1557
  }
1475
1558
  };
1476
1559
  const mr = program
@@ -1488,6 +1571,25 @@ If setting up MemoryRouter for another agent:
1488
1571
  .description("Enable MemoryRouter with a memory key (alias)")
1489
1572
  .argument("<key>", "Your MemoryRouter memory key (mk_xxx)")
1490
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
+ });
1491
1593
  mr.command("off")
1492
1594
  .description("Disable MemoryRouter (removes key, keeps CLI available)")
1493
1595
  .action(async () => {
@@ -1498,7 +1600,7 @@ If setting up MemoryRouter for another agent:
1498
1600
  // Note: we intentionally do NOT call setPluginEnabled(api, false)
1499
1601
  // Restore native memory search so the user isn't left with NO memory search.
1500
1602
  try {
1501
- const changed = await setNativeMemorySearchEnabled(true);
1603
+ const changed = await setNativeMemorySearchEnabled(api, true);
1502
1604
  if (changed)
1503
1605
  console.log(" • Native memory search restored");
1504
1606
  }
package/index.ts CHANGED
@@ -363,7 +363,47 @@ function openClawConfigPath(): string {
363
363
  return process.env.OPENCLAW_CONFIG_PATH || process.env.OPENCLAW_CONFIG || join(os.homedir(), ".openclaw", "openclaw.json");
364
364
  }
365
365
 
366
- 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
+ }
367
407
  try {
368
408
  return JSON.parse(await readFile(openClawConfigPath(), "utf8"));
369
409
  } catch (err: any) {
@@ -372,7 +412,13 @@ async function readOpenClawConfigFile(): Promise<JsonObject> {
372
412
  }
373
413
  }
374
414
 
375
- 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.
376
422
  const configPath = openClawConfigPath();
377
423
  await mkdir(dirname(configPath), { recursive: true });
378
424
  try {
@@ -384,54 +430,70 @@ async function writeOpenClawConfigFile(config: JsonObject): Promise<void> {
384
430
  `, "utf8");
385
431
  }
386
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
+
387
457
  // ──────────────────────────────────────────────────────
388
458
  // Full-replace: disable OpenClaw's native memory search so MemoryRouter
389
459
  // owns the memory_search tool (no name conflict, no native embeddings 429).
390
460
  // memoryFlush (durable MEMORY.md writes) is intentionally left untouched.
391
- // Global + subagents: writes agents.defaults.memorySearch.enabled.
392
- // ──────────────────────────────────────────────────────
393
- async function setNativeMemorySearchEnabled(enabled: boolean): Promise<boolean> {
394
- const config = await readOpenClawConfigFile();
395
- config.agents ??= {};
396
- config.agents.defaults ??= {};
397
- config.agents.defaults.memorySearch ??= {};
398
- if (config.agents.defaults.memorySearch.enabled === enabled) return false; // no-op
399
- config.agents.defaults.memorySearch.enabled = enabled;
400
- await writeOpenClawConfigFile(config);
401
- 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
+ });
402
479
  }
403
480
 
404
- // Boot-time self-heal: if a key is configured but native search is still on,
405
- // flip it off so already-installed users are fixed on restart after updating
406
- // (no need to re-run `openclaw mr <key>`).
481
+ // Explicit setup/doctor repair only. With no key, leave native search available.
407
482
  async function selfHealNativeMemorySearch(api: OpenClawPluginApi): Promise<void> {
408
- try {
409
- const config = await readOpenClawConfigFile();
410
- const ms = config?.agents?.defaults?.memorySearch;
411
- if (ms?.enabled === false) return; // already disabled — nothing to do
412
- const changed = await setNativeMemorySearchEnabled(false);
413
- if (changed) {
414
- api.logger.info?.(
415
- "memoryrouter: disabled native memory search (full-replace). Restart gateway to apply.",
416
- );
417
- }
418
- } catch (err) {
419
- api.logger.info?.(
420
- `memoryrouter: self-heal skipped — ${err instanceof Error ? err.message : String(err)}`,
421
- );
422
- }
483
+ const changed = await setNativeMemorySearchEnabled(api, false);
484
+ if (changed) console.log(" • Native memory search disabled — MemoryRouter now owns memory_search");
423
485
  }
424
486
 
425
- async function updateOpenClawPluginEntry(api: OpenClawPluginApi, patch: JsonObject): Promise<void> {
426
- const config = await readOpenClawConfigFile();
427
- config.plugins ??= {};
428
- config.plugins.entries ??= {};
429
- config.plugins.entries[api.id] ??= {};
430
- config.plugins.entries[api.id] = {
431
- ...config.plugins.entries[api.id],
432
- ...patch,
433
- };
434
- 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
+ });
435
497
  }
436
498
 
437
499
  async function setPluginConfig(api: OpenClawPluginApi, config: Record<string, unknown>): Promise<void> {
@@ -453,7 +515,13 @@ async function setPluginEnabled(api: OpenClawPluginApi, enabled: boolean): Promi
453
515
  }
454
516
 
455
517
  async function mergePluginConfig(api: OpenClawPluginApi, patch: Record<string, unknown>): Promise<void> {
456
- 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);
457
525
  const existing = current.plugins?.entries?.[api.id]?.config;
458
526
  await setPluginConfig(api, {
459
527
  ...(existing && typeof existing === "object" && !Array.isArray(existing) ? existing : {}),
@@ -461,36 +529,51 @@ async function mergePluginConfig(api: OpenClawPluginApi, patch: Record<string, u
461
529
  });
462
530
  }
463
531
 
464
- // Keep mr-memory as a first-class plugin config entry without clobbering user settings.
465
- // OpenClaw doctor validates plugin entries against discoverable manifests, so the entry
466
- // 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().
467
562
  async function ensureDoctorSafePluginEntry(api: OpenClawPluginApi): Promise<void> {
468
- try {
469
- const config = await readOpenClawConfigFile();
470
- const plugins = config.plugins ?? {};
471
- const entries = plugins.entries ?? {};
472
- const existingEntry = entries[api.id] && typeof entries[api.id] === "object" && !Array.isArray(entries[api.id])
473
- ? entries[api.id]
474
- : {};
475
- const nextPlugins: JsonObject = {
476
- ...plugins,
477
- entries: {
478
- ...entries,
479
- [api.id]: {
480
- ...existingEntry,
481
- enabled: true,
482
- },
483
- },
484
- };
485
- if (Array.isArray(plugins.allow) && !plugins.allow.includes(api.id)) {
486
- nextPlugins.allow = [...plugins.allow, api.id];
487
- }
488
- const next = { ...config, plugins: nextPlugins };
489
- if (JSON.stringify(next.plugins) === JSON.stringify(config.plugins)) return;
490
- await writeOpenClawConfigFile(next);
491
- } catch (err) {
492
- 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);
493
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");
494
577
  }
495
578
 
496
579
  /**
@@ -660,10 +743,19 @@ const memoryRouterPlugin = {
660
743
  warmupTimer.unref?.();
661
744
  }
662
745
 
663
- void (async () => {
664
- await ensureDoctorSafePluginEntry(api);
665
- if (hasAnyKey) await selfHealNativeMemorySearch(api);
666
- })();
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
+ }
667
759
 
668
760
  if (hasAnyKey) {
669
761
  if (memoryKey) api.logger.info?.(`memoryrouter: active (key: ${memoryKey.slice(0, 6)}...)`);
@@ -1187,7 +1279,7 @@ If setting up MemoryRouter for another agent:
1187
1279
  }
1188
1280
  },
1189
1281
  };
1190
- });
1282
+ }, { name: "memory_search" });
1191
1283
 
1192
1284
  // memory_date_search: calls MR /v1/memory/date-search (time-window retrieval)
1193
1285
  api.registerTool((ctx) => {
@@ -1290,7 +1382,7 @@ If setting up MemoryRouter for another agent:
1290
1382
  }
1291
1383
  },
1292
1384
  };
1293
- });
1385
+ }, { name: "memory_date_search" });
1294
1386
 
1295
1387
  // ==================================================================
1296
1388
  // Tools: memory_reflect + memory_commit (consolidation, two-tool shape)
@@ -1372,7 +1464,7 @@ If setting up MemoryRouter for another agent:
1372
1464
  }
1373
1465
  },
1374
1466
  };
1375
- });
1467
+ }, { name: "memory_reflect" });
1376
1468
 
1377
1469
  // memory_commit: commit wrapper (idempotent; 409 means the lease expired)
1378
1470
  api.registerTool((ctx) => {
@@ -1460,7 +1552,7 @@ If setting up MemoryRouter for another agent:
1460
1552
  }
1461
1553
  },
1462
1554
  };
1463
- });
1555
+ }, { name: "memory_commit" });
1464
1556
 
1465
1557
  // memory_inspect: read-only lineage drill-down (curation surface, ids allowed)
1466
1558
  api.registerTool((ctx) => {
@@ -1495,7 +1587,7 @@ If setting up MemoryRouter for another agent:
1495
1587
  }
1496
1588
  },
1497
1589
  };
1498
- });
1590
+ }, { name: "memory_inspect" });
1499
1591
 
1500
1592
  // memory_get — exact copy of OpenClaw's implementation (pure local file read)
1501
1593
  api.registerTool((ctx) => {
@@ -1533,7 +1625,7 @@ If setting up MemoryRouter for another agent:
1533
1625
  }
1534
1626
  },
1535
1627
  };
1536
- });
1628
+ }, { name: "memory_get" });
1537
1629
  } // end if (hasAnyKey)
1538
1630
 
1539
1631
  // ==================================================================
@@ -1549,19 +1641,16 @@ If setting up MemoryRouter for another agent:
1549
1641
  }
1550
1642
  try {
1551
1643
  await mergePluginConfig(api, { key });
1552
- await setPluginEnabled(api, true);
1553
- try {
1554
- const changed = await setNativeMemorySearchEnabled(false);
1555
- if (changed) console.log(` • Native memory search disabled — MemoryRouter now owns memory_search`);
1556
- } catch (err) {
1557
- console.error(` ⚠ Could not disable native memory search: ${err instanceof Error ? err.message : String(err)}`);
1558
- }
1644
+ await ensureDoctorSafePluginEntry(api);
1645
+ await selfHealNativeMemorySearch(api);
1559
1646
  console.log(`✓ MemoryRouter enabled. Key: ${key.slice(0, 6)}...${key.slice(-3)}`);
1560
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`);
1561
1649
  console.log(`\nRun: openclaw mr upload to upload your memories`);
1562
1650
  } catch (err) {
1563
1651
  const message = err instanceof Error ? err.message : String(err);
1564
1652
  console.error(`Failed to enable MemoryRouter: ${message}`);
1653
+ process.exitCode = 1;
1565
1654
  }
1566
1655
  };
1567
1656
 
@@ -1579,6 +1668,23 @@ If setting up MemoryRouter for another agent:
1579
1668
  .argument("<key>", "Your MemoryRouter memory key (mk_xxx)")
1580
1669
  .action(async (key: string) => { await applyKey(key); });
1581
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
+
1582
1688
  mr.command("off")
1583
1689
  .description("Disable MemoryRouter (removes key, keeps CLI available)")
1584
1690
  .action(async () => {
@@ -1589,7 +1695,7 @@ If setting up MemoryRouter for another agent:
1589
1695
  // Note: we intentionally do NOT call setPluginEnabled(api, false)
1590
1696
  // Restore native memory search so the user isn't left with NO memory search.
1591
1697
  try {
1592
- const changed = await setNativeMemorySearchEnabled(true);
1698
+ const changed = await setNativeMemorySearchEnabled(api, true);
1593
1699
  if (changed) console.log(" • Native memory search restored");
1594
1700
  } catch (err) {
1595
1701
  console.error(` ⚠ Could not restore native memory search: ${err instanceof Error ? err.message : String(err)}`);
@@ -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": "medium",
22
- "advanced": true,
23
- "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)."
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": {
@@ -57,11 +63,17 @@
57
63
  },
58
64
  "mode": {
59
65
  "type": "string",
60
- "enum": ["relay", "proxy"]
66
+ "enum": [
67
+ "relay",
68
+ "proxy"
69
+ ]
61
70
  },
62
71
  "embeddings": {
63
72
  "type": "string",
64
- "enum": ["bge", "qwen"],
73
+ "enum": [
74
+ "bge",
75
+ "qwen"
76
+ ],
65
77
  "description": "Embedding model: bge (1024d, default) or qwen (4096d)"
66
78
  },
67
79
  "keepWarmMs": {
@@ -83,5 +95,38 @@
83
95
  }
84
96
  },
85
97
  "required": []
86
- }
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"
87
132
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mr-memory",
3
- "version": "3.13.2",
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",