neuralmemory 1.11.2 → 1.13.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/dist/index.js CHANGED
@@ -41,14 +41,14 @@ function buildToolInstructions(tools) {
41
41
  const toolList = tools
42
42
  .map((t) => `- ${t.name}: ${t.description.slice(0, 100)}`)
43
43
  .join("\n");
44
- return `You have NeuralMemory tools for persistent memory across sessions. Call these as TOOL CALLS (not CLI commands):
45
-
46
- ${toolList}
47
-
48
- NeuralMemory (nmem_*) is your primary memory system. memory_search and memory_get are legacy aliases that redirect to nmem_recall — prefer nmem_* tools directly for full functionality.
49
-
50
- These are tool calls, NOT shell commands. Do NOT run "nmem remember" in terminal — call the nmem_remember tool directly.
51
-
44
+ return `You have NeuralMemory tools for persistent memory across sessions. Call these as TOOL CALLS (not CLI commands):
45
+
46
+ ${toolList}
47
+
48
+ NeuralMemory (nmem_*) is your primary memory system. memory_search and memory_get are legacy aliases that redirect to nmem_recall — prefer nmem_* tools directly for full functionality.
49
+
50
+ These are tool calls, NOT shell commands. Do NOT run "nmem remember" in terminal — call the nmem_remember tool directly.
51
+
52
52
  PROACTIVE MEMORY: Use nmem_remember after decisions, errors, and insights. Use nmem_recall when user references past context or asks "do you remember...". Use nmem_remember_batch to store multiple memories at once.`;
53
53
  }
54
54
  const DEFAULT_CONFIG = {
@@ -130,7 +130,7 @@ const plugin = {
130
130
  id: "neuralmemory",
131
131
  name: "NeuralMemory",
132
132
  description: "Brain-inspired persistent memory for AI agents — neurons, synapses, and fibers",
133
- version: "1.11.2",
133
+ version: "1.13.0",
134
134
  kind: "memory",
135
135
  register(api) {
136
136
  const cfg = resolveConfig(api.pluginConfig);
@@ -1,93 +1,93 @@
1
- {
2
- "id": "neuralmemory",
3
- "kind": "memory",
4
- "name": "NeuralMemory",
5
- "description": "Brain-inspired persistent memory for AI agents — neurons, synapses, and fibers. REQUIRED: set plugins.slots.memory = \"neuralmemory\" in openclaw.json to disable the default memory-core plugin and activate NeuralMemory as the exclusive memory provider.",
6
- "version": "1.11.2",
7
- "configSchema": {
8
- "jsonSchema": {
9
- "type": "object",
10
- "additionalProperties": false,
11
- "properties": {
12
- "pythonPath": {
13
- "type": "string",
14
- "description": "Path to Python executable with neural-memory installed",
15
- "default": "python"
16
- },
17
- "brain": {
18
- "type": "string",
19
- "pattern": "^[a-zA-Z0-9_\\-.]{1,64}$",
20
- "description": "Brain name to use for this workspace",
21
- "default": "default"
22
- },
23
- "autoContext": {
24
- "type": "boolean",
25
- "description": "Inject relevant memory context before each agent run",
26
- "default": true
27
- },
28
- "autoCapture": {
29
- "type": "boolean",
30
- "description": "Auto-extract and store memories after each agent run",
31
- "default": true
32
- },
33
- "contextDepth": {
34
- "type": "integer",
35
- "minimum": 0,
36
- "maximum": 3,
37
- "description": "Recall depth for auto-context: 0=instant, 1=context, 2=habit, 3=deep",
38
- "default": 1
39
- },
40
- "maxContextTokens": {
41
- "type": "integer",
42
- "minimum": 100,
43
- "maximum": 10000,
44
- "description": "Maximum tokens for auto-context injection",
45
- "default": 500
46
- },
47
- "timeout": {
48
- "type": "integer",
49
- "minimum": 5000,
50
- "maximum": 120000,
51
- "description": "MCP request timeout in milliseconds",
52
- "default": 30000
53
- }
54
- }
55
- },
56
- "uiHints": {
57
- "_setup": {
58
- "label": "Required Setup",
59
- "help": "Add to openclaw.json: { \"plugins\": { \"slots\": { \"memory\": \"neuralmemory\" } } } — this disables the default memory-core plugin so agents use NeuralMemory exclusively. Without this, agents may still call memory_search (memory-core) even with AGENTS.MD rules."
60
- },
61
- "pythonPath": {
62
- "label": "Python Path",
63
- "placeholder": "python",
64
- "help": "Path to Python executable that has neural-memory installed (pip install neural-memory)"
65
- },
66
- "brain": {
67
- "label": "Brain Name",
68
- "placeholder": "default",
69
- "help": "Which brain to use — each workspace can have its own brain"
70
- },
71
- "autoContext": {
72
- "label": "Auto-inject Context",
73
- "help": "Automatically query relevant memories and inject them before each agent run"
74
- },
75
- "autoCapture": {
76
- "label": "Auto-capture Memories",
77
- "help": "Automatically extract facts, decisions, and insights after each agent run"
78
- },
79
- "contextDepth": {
80
- "label": "Context Depth",
81
- "help": "How deep to search: 0=instant lookup, 1=contextual, 2=habit patterns, 3=full graph traversal"
82
- },
83
- "maxContextTokens": {
84
- "label": "Max Context Tokens",
85
- "help": "Maximum tokens to inject as context (higher = more context but uses more prompt space)"
86
- },
87
- "timeout": {
88
- "label": "MCP Timeout (ms)",
89
- "help": "How long to wait for MCP server responses. Increase if you see timeout errors on slow machines (default: 30000)"
90
- }
91
- }
92
- }
93
- }
1
+ {
2
+ "id": "neuralmemory",
3
+ "kind": "memory",
4
+ "name": "NeuralMemory",
5
+ "description": "Brain-inspired persistent memory for AI agents — neurons, synapses, and fibers. REQUIRED: set plugins.slots.memory = \"neuralmemory\" in openclaw.json to disable the default memory-core plugin and activate NeuralMemory as the exclusive memory provider.",
6
+ "version": "1.13.0",
7
+ "configSchema": {
8
+ "jsonSchema": {
9
+ "type": "object",
10
+ "additionalProperties": false,
11
+ "properties": {
12
+ "pythonPath": {
13
+ "type": "string",
14
+ "description": "Path to Python executable with neural-memory installed",
15
+ "default": "python"
16
+ },
17
+ "brain": {
18
+ "type": "string",
19
+ "pattern": "^[a-zA-Z0-9_\\-.]{1,64}$",
20
+ "description": "Brain name to use for this workspace",
21
+ "default": "default"
22
+ },
23
+ "autoContext": {
24
+ "type": "boolean",
25
+ "description": "Inject relevant memory context before each agent run",
26
+ "default": true
27
+ },
28
+ "autoCapture": {
29
+ "type": "boolean",
30
+ "description": "Auto-extract and store memories after each agent run",
31
+ "default": true
32
+ },
33
+ "contextDepth": {
34
+ "type": "integer",
35
+ "minimum": 0,
36
+ "maximum": 3,
37
+ "description": "Recall depth for auto-context: 0=instant, 1=context, 2=habit, 3=deep",
38
+ "default": 1
39
+ },
40
+ "maxContextTokens": {
41
+ "type": "integer",
42
+ "minimum": 100,
43
+ "maximum": 10000,
44
+ "description": "Maximum tokens for auto-context injection",
45
+ "default": 500
46
+ },
47
+ "timeout": {
48
+ "type": "integer",
49
+ "minimum": 5000,
50
+ "maximum": 120000,
51
+ "description": "MCP request timeout in milliseconds",
52
+ "default": 30000
53
+ }
54
+ }
55
+ },
56
+ "uiHints": {
57
+ "_setup": {
58
+ "label": "Required Setup",
59
+ "help": "Add to openclaw.json: { \"plugins\": { \"slots\": { \"memory\": \"neuralmemory\" } } } — this disables the default memory-core plugin so agents use NeuralMemory exclusively. Without this, agents may still call memory_search (memory-core) even with AGENTS.MD rules."
60
+ },
61
+ "pythonPath": {
62
+ "label": "Python Path",
63
+ "placeholder": "python",
64
+ "help": "Path to Python executable that has neural-memory installed (pip install neural-memory)"
65
+ },
66
+ "brain": {
67
+ "label": "Brain Name",
68
+ "placeholder": "default",
69
+ "help": "Which brain to use — each workspace can have its own brain"
70
+ },
71
+ "autoContext": {
72
+ "label": "Auto-inject Context",
73
+ "help": "Automatically query relevant memories and inject them before each agent run"
74
+ },
75
+ "autoCapture": {
76
+ "label": "Auto-capture Memories",
77
+ "help": "Automatically extract facts, decisions, and insights after each agent run"
78
+ },
79
+ "contextDepth": {
80
+ "label": "Context Depth",
81
+ "help": "How deep to search: 0=instant lookup, 1=contextual, 2=habit patterns, 3=full graph traversal"
82
+ },
83
+ "maxContextTokens": {
84
+ "label": "Max Context Tokens",
85
+ "help": "Maximum tokens to inject as context (higher = more context but uses more prompt space)"
86
+ },
87
+ "timeout": {
88
+ "label": "MCP Timeout (ms)",
89
+ "help": "How long to wait for MCP server responses. Increase if you see timeout errors on slow machines (default: 30000)"
90
+ }
91
+ }
92
+ }
93
+ }
package/package.json CHANGED
@@ -1,53 +1,53 @@
1
- {
2
- "name": "neuralmemory",
3
- "version": "1.11.2",
4
- "description": "NeuralMemory plugin for OpenClaw — brain-inspired persistent memory for AI agents",
5
- "type": "module",
6
- "main": "dist/index.js",
7
- "types": "dist/index.d.ts",
8
- "openclaw": {
9
- "id": "neuralmemory",
10
- "extensions": [
11
- "./dist/index.js"
12
- ],
13
- "source": "./src/index.ts"
14
- },
15
- "scripts": {
16
- "build": "tsc",
17
- "typecheck": "tsc --noEmit",
18
- "prepublishOnly": "npm run build",
19
- "test": "vitest run",
20
- "test:watch": "vitest",
21
- "test:coverage": "vitest run --coverage"
22
- },
23
- "peerDependencies": {},
24
- "keywords": [
25
- "openclaw",
26
- "openclaw-plugin",
27
- "memory",
28
- "neuralmemory",
29
- "ai-agent",
30
- "persistent-memory"
31
- ],
32
- "author": "NeuralMemory Contributors",
33
- "license": "MIT",
34
- "homepage": "https://github.com/nhadaututtheky/neural-memory#readme",
35
- "repository": {
36
- "type": "git",
37
- "url": "https://github.com/nhadaututtheky/neural-memory"
38
- },
39
- "files": [
40
- "src/",
41
- "dist/",
42
- "openclaw.plugin.json"
43
- ],
44
- "engines": {
45
- "node": ">=18.0.0"
46
- },
47
- "devDependencies": {
48
- "@types/node": "^25.2.2",
49
- "@vitest/coverage-v8": "^3.2.4",
50
- "typescript": "^5.9.3",
51
- "vitest": "^3.0.0"
52
- }
53
- }
1
+ {
2
+ "name": "neuralmemory",
3
+ "version": "1.13.0",
4
+ "description": "NeuralMemory plugin for OpenClaw — brain-inspired persistent memory for AI agents",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "openclaw": {
9
+ "id": "neuralmemory",
10
+ "extensions": [
11
+ "./dist/index.js"
12
+ ],
13
+ "source": "./src/index.ts"
14
+ },
15
+ "scripts": {
16
+ "build": "tsc",
17
+ "typecheck": "tsc --noEmit",
18
+ "prepublishOnly": "npm run build",
19
+ "test": "vitest run",
20
+ "test:watch": "vitest",
21
+ "test:coverage": "vitest run --coverage"
22
+ },
23
+ "peerDependencies": {},
24
+ "keywords": [
25
+ "openclaw",
26
+ "openclaw-plugin",
27
+ "memory",
28
+ "neuralmemory",
29
+ "ai-agent",
30
+ "persistent-memory"
31
+ ],
32
+ "author": "NeuralMemory Contributors",
33
+ "license": "MIT",
34
+ "homepage": "https://github.com/nhadaututtheky/neural-memory#readme",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "https://github.com/nhadaututtheky/neural-memory"
38
+ },
39
+ "files": [
40
+ "src/",
41
+ "dist/",
42
+ "openclaw.plugin.json"
43
+ ],
44
+ "engines": {
45
+ "node": ">=18.0.0"
46
+ },
47
+ "devDependencies": {
48
+ "@types/node": "^25.2.2",
49
+ "@vitest/coverage-v8": "^3.2.4",
50
+ "typescript": "^5.9.3",
51
+ "vitest": "^3.0.0"
52
+ }
53
+ }
package/src/index.ts CHANGED
@@ -1,342 +1,342 @@
1
- /**
2
- * NeuralMemory — OpenClaw Memory Plugin
3
- *
4
- * Brain-inspired persistent memory for AI agents.
5
- * Occupies the exclusive "memory" plugin slot.
6
- *
7
- * Architecture:
8
- * OpenClaw ←→ Plugin (TypeScript) ←→ MCP stdio ←→ NeuralMemory (Python)
9
- *
10
- * v1.7.0: Dynamic tool proxy — fetches all tools from MCP `tools/list`
11
- * instead of hardcoding 6 tools. Automatically exposes every tool the
12
- * MCP server provides (39+ tools in NM v2.28.0).
13
- *
14
- * v1.8.0: Compatible with NM v2.29.0 — RRF score fusion, graph-based
15
- * query expansion, and Personalized PageRank activation.
16
- *
17
- * v1.8.1: Fix async register() — OpenClaw requires synchronous registration.
18
- * Fallback tools registered sync; MCP connection deferred to service.start().
19
- *
20
- * v1.9.0: Backward-compat shim tools (memory_search, memory_get) to prevent
21
- * "allowList contains unknown entries" warnings when NM replaces memory-core.
22
- *
23
- * v1.10.0: Singleton MCP client — multiple workspaces (multi-agent) share
24
- * the same connected client instance, keyed by (pythonPath, brain). Fixes
25
- * "NeuralMemory service not running" when OpenClaw registers the plugin
26
- * for a second workspace after gateway startup.
27
- *
28
- * Registers:
29
- * N tools — dynamically from MCP server (fallback: 5 core + 2 compat)
30
- * 1 service — MCP process lifecycle (start/stop)
31
- * 2 hooks — before_agent_start (auto-context), agent_end (auto-capture)
32
- */
33
-
34
- import type {
35
- OpenClawPluginDefinition,
36
- OpenClawPluginApi,
37
- BeforeAgentStartEvent,
38
- BeforeAgentStartResult,
39
- AgentEndEvent,
40
- } from "./types.js";
41
- import { NeuralMemoryMcpClient } from "./mcp-client.js";
42
- import type { PluginLogger } from "./types.js";
43
- import { createToolsFromMcp, createFallbackTools, createCompatibilityTools } from "./tools.js";
44
- import type { ToolDefinition } from "./tools.js";
45
-
46
- // ── System prompt for tool awareness ──────────────────────
47
-
48
- /**
49
- * Build a system prompt listing all registered tool names.
50
- * This makes the agent aware of which nmem_* tools are available.
51
- */
52
- function buildToolInstructions(tools: ToolDefinition[]): string {
53
- const toolList = tools
54
- .map((t) => `- ${t.name}: ${t.description.slice(0, 100)}`)
55
- .join("\n");
56
-
57
- return `You have NeuralMemory tools for persistent memory across sessions. Call these as TOOL CALLS (not CLI commands):
58
-
59
- ${toolList}
60
-
61
- NeuralMemory (nmem_*) is your primary memory system. memory_search and memory_get are legacy aliases that redirect to nmem_recall — prefer nmem_* tools directly for full functionality.
62
-
63
- These are tool calls, NOT shell commands. Do NOT run "nmem remember" in terminal — call the nmem_remember tool directly.
64
-
65
- PROACTIVE MEMORY: Use nmem_remember after decisions, errors, and insights. Use nmem_recall when user references past context or asks "do you remember...". Use nmem_remember_batch to store multiple memories at once.`;
66
- }
67
-
68
- // ── Config ─────────────────────────────────────────────────
69
-
70
- type PluginConfig = {
71
- pythonPath: string;
72
- brain: string;
73
- autoContext: boolean;
74
- autoCapture: boolean;
75
- contextDepth: number;
76
- maxContextTokens: number;
77
- timeout: number;
78
- initTimeout: number;
79
- };
80
-
81
- const DEFAULT_CONFIG: Readonly<PluginConfig> = {
82
- pythonPath: "python",
83
- brain: "default",
84
- autoContext: true,
85
- autoCapture: true,
86
- contextDepth: 1,
87
- maxContextTokens: 500,
88
- timeout: 30_000,
89
- initTimeout: 90_000,
90
- };
91
-
92
- export const BRAIN_NAME_RE = /^[a-zA-Z0-9_\-.]{1,64}$/;
93
- export const MAX_AUTO_CAPTURE_CHARS = 50_000;
94
-
95
- export function resolveConfig(raw?: Record<string, unknown>): PluginConfig {
96
- const merged = { ...DEFAULT_CONFIG, ...(raw ?? {}) };
97
-
98
- return {
99
- pythonPath:
100
- typeof merged.pythonPath === "string" && merged.pythonPath.length > 0
101
- ? merged.pythonPath
102
- : DEFAULT_CONFIG.pythonPath,
103
- brain:
104
- typeof merged.brain === "string" && BRAIN_NAME_RE.test(merged.brain)
105
- ? merged.brain
106
- : DEFAULT_CONFIG.brain,
107
- autoContext:
108
- typeof merged.autoContext === "boolean"
109
- ? merged.autoContext
110
- : DEFAULT_CONFIG.autoContext,
111
- autoCapture:
112
- typeof merged.autoCapture === "boolean"
113
- ? merged.autoCapture
114
- : DEFAULT_CONFIG.autoCapture,
115
- contextDepth:
116
- typeof merged.contextDepth === "number" &&
117
- Number.isInteger(merged.contextDepth) &&
118
- merged.contextDepth >= 0 &&
119
- merged.contextDepth <= 3
120
- ? merged.contextDepth
121
- : DEFAULT_CONFIG.contextDepth,
122
- maxContextTokens:
123
- typeof merged.maxContextTokens === "number" &&
124
- Number.isInteger(merged.maxContextTokens) &&
125
- merged.maxContextTokens >= 100 &&
126
- merged.maxContextTokens <= 10_000
127
- ? merged.maxContextTokens
128
- : DEFAULT_CONFIG.maxContextTokens,
129
- timeout:
130
- typeof merged.timeout === "number" &&
131
- Number.isFinite(merged.timeout) &&
132
- merged.timeout >= 5_000 &&
133
- merged.timeout <= 120_000
134
- ? merged.timeout
135
- : DEFAULT_CONFIG.timeout,
136
- initTimeout:
137
- typeof merged.initTimeout === "number" &&
138
- Number.isFinite(merged.initTimeout) &&
139
- merged.initTimeout >= 10_000 &&
140
- merged.initTimeout <= 300_000
141
- ? merged.initTimeout
142
- : DEFAULT_CONFIG.initTimeout,
143
- };
144
- }
145
-
146
- // ── Singleton MCP client pool ────────────────────────────────
147
- // Multiple workspaces may call register() independently, but all
148
- // should share the same MCP process per (pythonPath, brain) combo.
149
-
150
- const mcpClients = new Map<string, NeuralMemoryMcpClient>();
151
-
152
- function getOrCreateMcpClient(
153
- cfg: PluginConfig,
154
- logger: PluginLogger,
155
- ): NeuralMemoryMcpClient {
156
- const key = `${cfg.pythonPath}::${cfg.brain}`;
157
-
158
- const existing = mcpClients.get(key);
159
- if (existing) {
160
- logger.info(`Reusing existing MCP client for brain "${cfg.brain}"`);
161
- return existing;
162
- }
163
-
164
- const mcp = new NeuralMemoryMcpClient({
165
- pythonPath: cfg.pythonPath,
166
- brain: cfg.brain,
167
- logger,
168
- timeout: cfg.timeout,
169
- initTimeout: cfg.initTimeout,
170
- });
171
-
172
- mcpClients.set(key, mcp);
173
- return mcp;
174
- }
175
-
176
- // ── Plugin definition ──────────────────────────────────────
177
-
178
- const plugin: OpenClawPluginDefinition = {
179
- id: "neuralmemory",
180
- name: "NeuralMemory",
181
- description:
182
- "Brain-inspired persistent memory for AI agents — neurons, synapses, and fibers",
183
- version: "1.11.2",
184
- kind: "memory",
185
-
186
- register(api: OpenClawPluginApi): void {
187
- const cfg = resolveConfig(api.pluginConfig);
188
-
189
- const mcp = getOrCreateMcpClient(cfg, api.logger);
190
-
191
- // ── Register fallback tools synchronously ────────────
192
- // OpenClaw requires register() to be synchronous.
193
- // Register stable fallback tools immediately; MCP connection
194
- // and dynamic tool discovery happen in service.start().
195
- // Fallback tools auto-reconnect MCP on first call.
196
-
197
- const registeredTools = createFallbackTools(mcp);
198
- const compatTools = createCompatibilityTools(mcp);
199
-
200
- for (const t of [...registeredTools, ...compatTools]) {
201
- api.registerTool(t, { name: t.name });
202
- }
203
-
204
- api.logger.info(
205
- `Registered ${registeredTools.length} NeuralMemory tools + ${compatTools.length} compat shims (sync)`,
206
- );
207
-
208
- // ── Service: MCP process lifecycle ───────────────────
209
-
210
- api.registerService({
211
- id: "neuralmemory-mcp",
212
-
213
- async start(): Promise<void> {
214
- if (!mcp.connected) {
215
- try {
216
- await mcp.connect();
217
- api.logger.info("NeuralMemory MCP connected in service.start()");
218
-
219
- // Log discovered tools for diagnostics (cannot re-register
220
- // after register() — OpenClaw freezes the tool list).
221
- try {
222
- const dynamicTools = await createToolsFromMcp(mcp);
223
- api.logger.info(
224
- `NeuralMemory MCP discovered ${dynamicTools.length} tools`,
225
- );
226
- } catch (err) {
227
- api.logger.warn(
228
- `Tool discovery failed: ${(err as Error).message}`,
229
- );
230
- }
231
- } catch (err) {
232
- api.logger.error(
233
- `Failed to start NeuralMemory MCP: ${(err as Error).message}`,
234
- );
235
- throw err;
236
- }
237
- }
238
- },
239
-
240
- async stop(): Promise<void> {
241
- // Remove from singleton pool so next register() creates fresh client
242
- const key = `${cfg.pythonPath}::${cfg.brain}`;
243
- mcpClients.delete(key);
244
- await mcp.close();
245
- api.logger.info("NeuralMemory MCP service stopped");
246
- },
247
- });
248
-
249
- // ── Hook: tool awareness + auto-context before agent start ───
250
-
251
- api.on(
252
- "before_agent_start",
253
- async (
254
- event: unknown,
255
- _ctx: unknown,
256
- ): Promise<BeforeAgentStartResult | void> => {
257
- const result: BeforeAgentStartResult = {
258
- systemPrompt: buildToolInstructions(registeredTools),
259
- };
260
-
261
- if (cfg.autoContext && mcp.connected) {
262
- const ev = event as BeforeAgentStartEvent;
263
-
264
- try {
265
- const raw = await mcp.callTool("nmem_recall", {
266
- query: ev.prompt,
267
- depth: cfg.contextDepth,
268
- max_tokens: cfg.maxContextTokens,
269
- });
270
-
271
- const data = JSON.parse(raw) as {
272
- answer?: string;
273
- confidence?: number;
274
- };
275
-
276
- if (data.answer && (data.confidence ?? 0) > 0.1) {
277
- result.prependContext = `[NeuralMemory — relevant context]\n${data.answer}`;
278
- }
279
- } catch (err) {
280
- api.logger.warn(
281
- `Auto-context failed: ${(err as Error).message}`,
282
- );
283
- }
284
- }
285
-
286
- return result;
287
- },
288
- { priority: 10 },
289
- );
290
-
291
- // ── Hook: auto-capture after agent completes ────────
292
-
293
- if (cfg.autoCapture) {
294
- api.on(
295
- "agent_end",
296
- async (event: unknown, _ctx: unknown): Promise<void> => {
297
- if (!mcp.connected) return;
298
-
299
- const ev = event as AgentEndEvent;
300
- if (!ev.success) return;
301
-
302
- try {
303
- const messages = ev.messages?.slice(-5) ?? [];
304
- const text = messages
305
- .filter(
306
- (m: unknown): m is { role: string; content: string } =>
307
- typeof m === "object" &&
308
- m !== null &&
309
- (m as { role?: string }).role === "assistant" &&
310
- typeof (m as { content?: unknown }).content === "string",
311
- )
312
- .map((m) => m.content)
313
- .join("\n")
314
- .slice(0, MAX_AUTO_CAPTURE_CHARS);
315
-
316
- if (text.length > 50) {
317
- await mcp.callTool("nmem_auto", {
318
- action: "process",
319
- text,
320
- });
321
- }
322
- } catch (err) {
323
- api.logger.warn(
324
- `Auto-capture failed: ${(err as Error).message}`,
325
- );
326
- }
327
- },
328
- { priority: 90 },
329
- );
330
- }
331
-
332
- // ── Done ────────────────────────────────────────────
333
-
334
- api.logger.info(
335
- `NeuralMemory registered (brain: ${cfg.brain}, ` +
336
- `autoContext: ${cfg.autoContext}, autoCapture: ${cfg.autoCapture}) — ` +
337
- `tools will be loaded dynamically from MCP on service start`,
338
- );
339
- },
340
- };
341
-
342
- export default plugin;
1
+ /**
2
+ * NeuralMemory — OpenClaw Memory Plugin
3
+ *
4
+ * Brain-inspired persistent memory for AI agents.
5
+ * Occupies the exclusive "memory" plugin slot.
6
+ *
7
+ * Architecture:
8
+ * OpenClaw ←→ Plugin (TypeScript) ←→ MCP stdio ←→ NeuralMemory (Python)
9
+ *
10
+ * v1.7.0: Dynamic tool proxy — fetches all tools from MCP `tools/list`
11
+ * instead of hardcoding 6 tools. Automatically exposes every tool the
12
+ * MCP server provides (39+ tools in NM v2.28.0).
13
+ *
14
+ * v1.8.0: Compatible with NM v2.29.0 — RRF score fusion, graph-based
15
+ * query expansion, and Personalized PageRank activation.
16
+ *
17
+ * v1.8.1: Fix async register() — OpenClaw requires synchronous registration.
18
+ * Fallback tools registered sync; MCP connection deferred to service.start().
19
+ *
20
+ * v1.9.0: Backward-compat shim tools (memory_search, memory_get) to prevent
21
+ * "allowList contains unknown entries" warnings when NM replaces memory-core.
22
+ *
23
+ * v1.10.0: Singleton MCP client — multiple workspaces (multi-agent) share
24
+ * the same connected client instance, keyed by (pythonPath, brain). Fixes
25
+ * "NeuralMemory service not running" when OpenClaw registers the plugin
26
+ * for a second workspace after gateway startup.
27
+ *
28
+ * Registers:
29
+ * N tools — dynamically from MCP server (fallback: 5 core + 2 compat)
30
+ * 1 service — MCP process lifecycle (start/stop)
31
+ * 2 hooks — before_agent_start (auto-context), agent_end (auto-capture)
32
+ */
33
+
34
+ import type {
35
+ OpenClawPluginDefinition,
36
+ OpenClawPluginApi,
37
+ BeforeAgentStartEvent,
38
+ BeforeAgentStartResult,
39
+ AgentEndEvent,
40
+ } from "./types.js";
41
+ import { NeuralMemoryMcpClient } from "./mcp-client.js";
42
+ import type { PluginLogger } from "./types.js";
43
+ import { createToolsFromMcp, createFallbackTools, createCompatibilityTools } from "./tools.js";
44
+ import type { ToolDefinition } from "./tools.js";
45
+
46
+ // ── System prompt for tool awareness ──────────────────────
47
+
48
+ /**
49
+ * Build a system prompt listing all registered tool names.
50
+ * This makes the agent aware of which nmem_* tools are available.
51
+ */
52
+ function buildToolInstructions(tools: ToolDefinition[]): string {
53
+ const toolList = tools
54
+ .map((t) => `- ${t.name}: ${t.description.slice(0, 100)}`)
55
+ .join("\n");
56
+
57
+ return `You have NeuralMemory tools for persistent memory across sessions. Call these as TOOL CALLS (not CLI commands):
58
+
59
+ ${toolList}
60
+
61
+ NeuralMemory (nmem_*) is your primary memory system. memory_search and memory_get are legacy aliases that redirect to nmem_recall — prefer nmem_* tools directly for full functionality.
62
+
63
+ These are tool calls, NOT shell commands. Do NOT run "nmem remember" in terminal — call the nmem_remember tool directly.
64
+
65
+ PROACTIVE MEMORY: Use nmem_remember after decisions, errors, and insights. Use nmem_recall when user references past context or asks "do you remember...". Use nmem_remember_batch to store multiple memories at once.`;
66
+ }
67
+
68
+ // ── Config ─────────────────────────────────────────────────
69
+
70
+ type PluginConfig = {
71
+ pythonPath: string;
72
+ brain: string;
73
+ autoContext: boolean;
74
+ autoCapture: boolean;
75
+ contextDepth: number;
76
+ maxContextTokens: number;
77
+ timeout: number;
78
+ initTimeout: number;
79
+ };
80
+
81
+ const DEFAULT_CONFIG: Readonly<PluginConfig> = {
82
+ pythonPath: "python",
83
+ brain: "default",
84
+ autoContext: true,
85
+ autoCapture: true,
86
+ contextDepth: 1,
87
+ maxContextTokens: 500,
88
+ timeout: 30_000,
89
+ initTimeout: 90_000,
90
+ };
91
+
92
+ export const BRAIN_NAME_RE = /^[a-zA-Z0-9_\-.]{1,64}$/;
93
+ export const MAX_AUTO_CAPTURE_CHARS = 50_000;
94
+
95
+ export function resolveConfig(raw?: Record<string, unknown>): PluginConfig {
96
+ const merged = { ...DEFAULT_CONFIG, ...(raw ?? {}) };
97
+
98
+ return {
99
+ pythonPath:
100
+ typeof merged.pythonPath === "string" && merged.pythonPath.length > 0
101
+ ? merged.pythonPath
102
+ : DEFAULT_CONFIG.pythonPath,
103
+ brain:
104
+ typeof merged.brain === "string" && BRAIN_NAME_RE.test(merged.brain)
105
+ ? merged.brain
106
+ : DEFAULT_CONFIG.brain,
107
+ autoContext:
108
+ typeof merged.autoContext === "boolean"
109
+ ? merged.autoContext
110
+ : DEFAULT_CONFIG.autoContext,
111
+ autoCapture:
112
+ typeof merged.autoCapture === "boolean"
113
+ ? merged.autoCapture
114
+ : DEFAULT_CONFIG.autoCapture,
115
+ contextDepth:
116
+ typeof merged.contextDepth === "number" &&
117
+ Number.isInteger(merged.contextDepth) &&
118
+ merged.contextDepth >= 0 &&
119
+ merged.contextDepth <= 3
120
+ ? merged.contextDepth
121
+ : DEFAULT_CONFIG.contextDepth,
122
+ maxContextTokens:
123
+ typeof merged.maxContextTokens === "number" &&
124
+ Number.isInteger(merged.maxContextTokens) &&
125
+ merged.maxContextTokens >= 100 &&
126
+ merged.maxContextTokens <= 10_000
127
+ ? merged.maxContextTokens
128
+ : DEFAULT_CONFIG.maxContextTokens,
129
+ timeout:
130
+ typeof merged.timeout === "number" &&
131
+ Number.isFinite(merged.timeout) &&
132
+ merged.timeout >= 5_000 &&
133
+ merged.timeout <= 120_000
134
+ ? merged.timeout
135
+ : DEFAULT_CONFIG.timeout,
136
+ initTimeout:
137
+ typeof merged.initTimeout === "number" &&
138
+ Number.isFinite(merged.initTimeout) &&
139
+ merged.initTimeout >= 10_000 &&
140
+ merged.initTimeout <= 300_000
141
+ ? merged.initTimeout
142
+ : DEFAULT_CONFIG.initTimeout,
143
+ };
144
+ }
145
+
146
+ // ── Singleton MCP client pool ────────────────────────────────
147
+ // Multiple workspaces may call register() independently, but all
148
+ // should share the same MCP process per (pythonPath, brain) combo.
149
+
150
+ const mcpClients = new Map<string, NeuralMemoryMcpClient>();
151
+
152
+ function getOrCreateMcpClient(
153
+ cfg: PluginConfig,
154
+ logger: PluginLogger,
155
+ ): NeuralMemoryMcpClient {
156
+ const key = `${cfg.pythonPath}::${cfg.brain}`;
157
+
158
+ const existing = mcpClients.get(key);
159
+ if (existing) {
160
+ logger.info(`Reusing existing MCP client for brain "${cfg.brain}"`);
161
+ return existing;
162
+ }
163
+
164
+ const mcp = new NeuralMemoryMcpClient({
165
+ pythonPath: cfg.pythonPath,
166
+ brain: cfg.brain,
167
+ logger,
168
+ timeout: cfg.timeout,
169
+ initTimeout: cfg.initTimeout,
170
+ });
171
+
172
+ mcpClients.set(key, mcp);
173
+ return mcp;
174
+ }
175
+
176
+ // ── Plugin definition ──────────────────────────────────────
177
+
178
+ const plugin: OpenClawPluginDefinition = {
179
+ id: "neuralmemory",
180
+ name: "NeuralMemory",
181
+ description:
182
+ "Brain-inspired persistent memory for AI agents — neurons, synapses, and fibers",
183
+ version: "1.13.0",
184
+ kind: "memory",
185
+
186
+ register(api: OpenClawPluginApi): void {
187
+ const cfg = resolveConfig(api.pluginConfig);
188
+
189
+ const mcp = getOrCreateMcpClient(cfg, api.logger);
190
+
191
+ // ── Register fallback tools synchronously ────────────
192
+ // OpenClaw requires register() to be synchronous.
193
+ // Register stable fallback tools immediately; MCP connection
194
+ // and dynamic tool discovery happen in service.start().
195
+ // Fallback tools auto-reconnect MCP on first call.
196
+
197
+ const registeredTools = createFallbackTools(mcp);
198
+ const compatTools = createCompatibilityTools(mcp);
199
+
200
+ for (const t of [...registeredTools, ...compatTools]) {
201
+ api.registerTool(t, { name: t.name });
202
+ }
203
+
204
+ api.logger.info(
205
+ `Registered ${registeredTools.length} NeuralMemory tools + ${compatTools.length} compat shims (sync)`,
206
+ );
207
+
208
+ // ── Service: MCP process lifecycle ───────────────────
209
+
210
+ api.registerService({
211
+ id: "neuralmemory-mcp",
212
+
213
+ async start(): Promise<void> {
214
+ if (!mcp.connected) {
215
+ try {
216
+ await mcp.connect();
217
+ api.logger.info("NeuralMemory MCP connected in service.start()");
218
+
219
+ // Log discovered tools for diagnostics (cannot re-register
220
+ // after register() — OpenClaw freezes the tool list).
221
+ try {
222
+ const dynamicTools = await createToolsFromMcp(mcp);
223
+ api.logger.info(
224
+ `NeuralMemory MCP discovered ${dynamicTools.length} tools`,
225
+ );
226
+ } catch (err) {
227
+ api.logger.warn(
228
+ `Tool discovery failed: ${(err as Error).message}`,
229
+ );
230
+ }
231
+ } catch (err) {
232
+ api.logger.error(
233
+ `Failed to start NeuralMemory MCP: ${(err as Error).message}`,
234
+ );
235
+ throw err;
236
+ }
237
+ }
238
+ },
239
+
240
+ async stop(): Promise<void> {
241
+ // Remove from singleton pool so next register() creates fresh client
242
+ const key = `${cfg.pythonPath}::${cfg.brain}`;
243
+ mcpClients.delete(key);
244
+ await mcp.close();
245
+ api.logger.info("NeuralMemory MCP service stopped");
246
+ },
247
+ });
248
+
249
+ // ── Hook: tool awareness + auto-context before agent start ───
250
+
251
+ api.on(
252
+ "before_agent_start",
253
+ async (
254
+ event: unknown,
255
+ _ctx: unknown,
256
+ ): Promise<BeforeAgentStartResult | void> => {
257
+ const result: BeforeAgentStartResult = {
258
+ systemPrompt: buildToolInstructions(registeredTools),
259
+ };
260
+
261
+ if (cfg.autoContext && mcp.connected) {
262
+ const ev = event as BeforeAgentStartEvent;
263
+
264
+ try {
265
+ const raw = await mcp.callTool("nmem_recall", {
266
+ query: ev.prompt,
267
+ depth: cfg.contextDepth,
268
+ max_tokens: cfg.maxContextTokens,
269
+ });
270
+
271
+ const data = JSON.parse(raw) as {
272
+ answer?: string;
273
+ confidence?: number;
274
+ };
275
+
276
+ if (data.answer && (data.confidence ?? 0) > 0.1) {
277
+ result.prependContext = `[NeuralMemory — relevant context]\n${data.answer}`;
278
+ }
279
+ } catch (err) {
280
+ api.logger.warn(
281
+ `Auto-context failed: ${(err as Error).message}`,
282
+ );
283
+ }
284
+ }
285
+
286
+ return result;
287
+ },
288
+ { priority: 10 },
289
+ );
290
+
291
+ // ── Hook: auto-capture after agent completes ────────
292
+
293
+ if (cfg.autoCapture) {
294
+ api.on(
295
+ "agent_end",
296
+ async (event: unknown, _ctx: unknown): Promise<void> => {
297
+ if (!mcp.connected) return;
298
+
299
+ const ev = event as AgentEndEvent;
300
+ if (!ev.success) return;
301
+
302
+ try {
303
+ const messages = ev.messages?.slice(-5) ?? [];
304
+ const text = messages
305
+ .filter(
306
+ (m: unknown): m is { role: string; content: string } =>
307
+ typeof m === "object" &&
308
+ m !== null &&
309
+ (m as { role?: string }).role === "assistant" &&
310
+ typeof (m as { content?: unknown }).content === "string",
311
+ )
312
+ .map((m) => m.content)
313
+ .join("\n")
314
+ .slice(0, MAX_AUTO_CAPTURE_CHARS);
315
+
316
+ if (text.length > 50) {
317
+ await mcp.callTool("nmem_auto", {
318
+ action: "process",
319
+ text,
320
+ });
321
+ }
322
+ } catch (err) {
323
+ api.logger.warn(
324
+ `Auto-capture failed: ${(err as Error).message}`,
325
+ );
326
+ }
327
+ },
328
+ { priority: 90 },
329
+ );
330
+ }
331
+
332
+ // ── Done ────────────────────────────────────────────
333
+
334
+ api.logger.info(
335
+ `NeuralMemory registered (brain: ${cfg.brain}, ` +
336
+ `autoContext: ${cfg.autoContext}, autoCapture: ${cfg.autoCapture}) — ` +
337
+ `tools will be loaded dynamically from MCP on service start`,
338
+ );
339
+ },
340
+ };
341
+
342
+ export default plugin;