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 +24 -4
- package/index.js +194 -92
- package/index.ts +194 -88
- package/openclaw.plugin.json +77 -32
- package/package.json +2 -6
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
|
|
12
|
+
openclaw plugins install --force --accept-capabilities npm:mr-memory
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
409
|
-
//
|
|
410
|
-
//
|
|
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
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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
|
|
1459
|
-
|
|
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
|
-
|
|
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
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
409
|
-
|
|
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
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
...config.
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
465
|
-
//
|
|
466
|
-
//
|
|
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
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
-
|
|
664
|
-
|
|
665
|
-
|
|
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
|
|
1553
|
-
|
|
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)}`);
|
package/openclaw.plugin.json
CHANGED
|
@@ -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
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
"
|
|
26
|
-
"
|
|
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
|
-
"
|
|
32
|
-
"
|
|
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": [
|
|
66
|
+
"enum": [
|
|
67
|
+
"relay",
|
|
68
|
+
"proxy"
|
|
69
|
+
]
|
|
61
70
|
},
|
|
62
71
|
"embeddings": {
|
|
63
72
|
"type": "string",
|
|
64
|
-
"enum": [
|
|
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.
|
|
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",
|