@nowledge/openclaw-nowledge-mem 0.6.6 → 0.6.9

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/CHANGELOG.md CHANGED
@@ -2,15 +2,48 @@
2
2
 
3
3
  All notable changes to the Nowledge Mem OpenClaw plugin will be documented in this file.
4
4
 
5
+ ## [0.6.9] - 2026-03-05
6
+
7
+ ### Fixed
8
+
9
+ - **Auto-recall searched with full prompt instead of user message**: `event.prompt` contains the entire conversation history (system prompt + all prior turns), making recalled memories irrelevant as conversation grows. Now uses a tiered query strategy: substantial messages (>=40 chars) search alone; short messages (3-39 chars, likely follow-ups like "explain that more") include the last 3 messages as context for topic grounding; tiny messages (<3 chars) skip recall. Falls back to truncated `event.prompt` (500 chars) only when `event.messages` is unavailable.
10
+
11
+ ### Added
12
+
13
+ - **`recallMinScore` config option** (0-100, default 0): Minimum relevance score threshold for auto-recalled memories. Set to e.g. 30 to filter out low-confidence results. Configurable via OpenClaw Config UI, config file, or `NMEM_RECALL_MIN_SCORE` env var.
14
+
15
+ ## [0.6.8] - 2026-02-27
16
+
17
+ ### Changed
18
+
19
+ - **Config priority flipped**: `~/.nowledge-mem/openclaw.json` now takes priority over OpenClaw plugin settings. Most users configure via OpenClaw UI (no file needed). Users who create the file get predictable overrides.
20
+ - **No auto-created config file**: The plugin no longer creates `~/.nowledge-mem/openclaw.json` on first startup. OpenClaw's settings UI works out of the box. Create the file manually when you need persistent or scripted config.
21
+ - **Status tool shows config sources**: `nowledge_mem_status` now shows where each setting comes from (`file`, `pluginConfig`, `env`, or `default`).
22
+
23
+ ### Fixed
24
+
25
+ - **configSchema broke OpenClaw form UI**: `additionalProperties: true` (added in 0.6.7) caused OpenClaw's config form to show "Unsupported schema mode. Use Raw mode." Removed the field entirely - JSON Schema defaults to allowing additional properties when omitted, so typos still don't crash the plugin, and the form renders normally.
26
+
27
+ ## [0.6.7] - 2026-02-27
28
+
29
+ ### Added
30
+
31
+ - `nowledge_mem_status` tool: verify effective config, backend connectivity, and version. Shows mode (local/remote), whether API key is set, backend health, and all config values. 10 tools total.
32
+
33
+ ### Fixed
34
+
35
+ - **configSchema too strict**: `additionalProperties` changed from `false` to `true`. Previously, a typo in openclaw.json config (e.g. `sesionContext`) caused the plugin to silently fail to load. Now OpenClaw passes validation and the plugin's own parser gives a descriptive error listing valid keys.
36
+ - **apiUrl port typo**: configSchema description had port 14142 instead of 14242.
37
+
5
38
  ## [0.6.6] - 2026-02-27
6
39
 
7
40
  ### Changed - Config file + env var cascade
8
41
 
9
42
  **Config file at `~/.nowledge-mem/openclaw.json`**
10
43
 
11
- The plugin now reads configuration from a dedicated config file, independent of OpenClaw's `openclaw.json`. The file is auto-created on first startup with sensible defaults. If you had settings in OpenClaw's plugin config, those values are automatically migrated into the file on first run. No manual steps needed.
44
+ The plugin now reads configuration from a dedicated config file, independent of OpenClaw's `openclaw.json`. The file is auto-created on first startup with sensible defaults. If you had settings in OpenClaw's plugin config, those values are automatically seeded into the file on first run. No manual steps needed.
12
45
 
13
- This ensures your settings survive OpenClaw version upgrades that may strip custom plugin config fields (observed in OpenClaw >= 2026.2.25).
46
+ This gives you a single, easy-to-find place for all Nowledge Mem settings. The file persists across plugin reinstalls and OpenClaw upgrades.
14
47
 
15
48
  **Config cascade** (4 layers, highest to lowest priority):
16
49
  1. `api.pluginConfig` - from OpenClaw's plugin config (when supported)
@@ -34,7 +67,7 @@ This ensures your settings survive OpenClaw version upgrades that may strip cust
34
67
 
35
68
  ### Added - Thread tools + sourceThreadId linkage
36
69
 
37
- **Two new tools for conversation retrieval.** The plugin now provides 9 tools total.
70
+ **Two new tools for conversation retrieval.**
38
71
 
39
72
  - `nowledge_mem_thread_search` - search past conversations by keyword. Returns matched threads with message snippets and relevance scores. Use when the user asks about a past discussion or wants to find a specific conversation.
40
73
  - `nowledge_mem_thread_fetch` - fetch full messages from a specific thread with pagination support. Use to progressively retrieve long conversations. Accepts `offset` and `limit` for paginated retrieval.
package/CLAUDE.md CHANGED
@@ -28,7 +28,7 @@ Reflects Nowledge Mem's genuine v0.6 strengths:
28
28
  src/
29
29
  index.js - plugin registration (tools, hooks, commands, CLI)
30
30
  client.js - CLI wrapper with API fallback; credential handling
31
- config.js - config cascade: pluginConfig > ~/.nowledge-mem/openclaw.json > env vars > defaults
31
+ config.js - config cascade: ~/.nowledge-mem/openclaw.json > pluginConfig > env vars > defaults
32
32
  hooks/
33
33
  behavioral.js - always-on behavioral guidance (~50 tokens/turn)
34
34
  recall.js - before_prompt_build: inject Working Memory + recalled memories
@@ -44,10 +44,10 @@ src/
44
44
  thread-search.js - search past conversations by keyword
45
45
  thread-fetch.js - fetch full messages from a thread with pagination
46
46
  openclaw.plugin.json - manifest + config schema (version, uiHints, configSchema)
47
- ~/.nowledge-mem/openclaw.json - user config file (auto-created on first run)
47
+ ~/.nowledge-mem/openclaw.json - optional user config file (overrides OpenClaw settings when present)
48
48
  ```
49
49
 
50
- ## Tool Surface (9 tools)
50
+ ## Tool Surface (10 tools)
51
51
 
52
52
  ### OpenClaw Memory Slot (required for system prompt activation)
53
53
  - `memory_search` - multi-signal: BM25 + embedding + label + graph + decay. Returns `matchedVia` ("Text Match 100% + Semantic 69%"), `importance`, bi-temporal filters (`event_date_from/to`, `recorded_date_from/to`). Also returns `relatedThreads` (past conversation snippets matching the query) and `sourceThreadId` (link to source conversation). Mode: `"multi-signal"`.
@@ -64,6 +64,9 @@ openclaw.plugin.json - manifest + config schema (version, uiHints, configSchema)
64
64
  - `nowledge_mem_thread_search` - search past conversations by keyword. Returns threads with matched message snippets, relevance scores, and message counts. Supports `source` filter.
65
65
  - `nowledge_mem_thread_fetch` - fetch full messages from a specific thread. Supports pagination via `offset` + `limit` for progressive retrieval of long conversations.
66
66
 
67
+ ### Diagnostics
68
+ - `nowledge_mem_status` - show effective config (mode, apiUrl, apiKey set, sessionContext, sessionDigest, etc.), backend connectivity, and version. No parameters.
69
+
67
70
  ## Hook Surface
68
71
 
69
72
  - `before_prompt_build` (always-on) - behavioral guidance: brief note telling agent to save/search proactively. Adjusts when sessionContext is on to avoid redundant searches.
@@ -80,7 +83,7 @@ openclaw.plugin.json - manifest + config schema (version, uiHints, configSchema)
80
83
 
81
84
  ## Config Keys
82
85
 
83
- Config file at `~/.nowledge-mem/openclaw.json` (auto-created on first run). Also supports env vars and pluginConfig.
86
+ Optional config file at `~/.nowledge-mem/openclaw.json`. Falls through to OpenClaw plugin settings when the file is absent.
84
87
 
85
88
  | Key | Type | Default | Env Var | Description |
86
89
  |-----|------|---------|---------|-------------|
@@ -88,10 +91,13 @@ Config file at `~/.nowledge-mem/openclaw.json` (auto-created on first run). Also
88
91
  | `sessionDigest` | boolean | `true` | `NMEM_SESSION_DIGEST` | Thread capture + LLM distillation at session end |
89
92
  | `digestMinInterval` | integer 0-86400 | `300` | `NMEM_DIGEST_MIN_INTERVAL` | Minimum seconds between session digests |
90
93
  | `maxContextResults` | integer 1-20 | `5` | `NMEM_MAX_CONTEXT_RESULTS` | How many memories to inject at prompt time |
94
+ | `recallMinScore` | integer 0-100 | `0` | `NMEM_RECALL_MIN_SCORE` | Min relevance score (%) to include in auto-recall |
91
95
  | `apiUrl` | string | `""` | `NMEM_API_URL` | Remote server URL. Empty = local (127.0.0.1:14242) |
92
96
  | `apiKey` | string | `""` | `NMEM_API_KEY` | API key. Never logged. |
93
97
 
94
- **Priority**: pluginConfig > config file > env var > default.
98
+ **Priority**: config file > pluginConfig > env var > default.
99
+
100
+ `parseConfig()` returns `_sources` object tracking where each value came from (`"file"`, `"pluginConfig"`, `"env"`, `"default"`). Used by `nowledge_mem_status` tool.
95
101
 
96
102
  Legacy aliases accepted silently in all sources for backward compat:
97
103
  `autoRecall`→`sessionContext`, `autoCapture`→`sessionDigest`, `captureMinInterval`→`digestMinInterval`, `maxRecallResults`→`maxContextResults`.
@@ -99,7 +105,6 @@ Legacy aliases accepted silently in all sources for backward compat:
99
105
  ### Credential Handling Rules
100
106
  - `apiKey` → ONLY via child process env (`NMEM_API_KEY`). Never CLI arg, never logged.
101
107
  - `apiUrl` → passed as `--api-url` flag to CLI (not a secret).
102
- - Config values win over environment variables.
103
108
  - `_spawnEnv()` builds per-spawn env; `_apiUrlArgs()` adds `--api-url` when non-default.
104
109
 
105
110
  ## CLI Surface (nmem commands used by plugin)
package/README.md CHANGED
@@ -32,13 +32,15 @@ Start Nowledge Mem desktop app or run `nmem serve`, then enable the plugin:
32
32
  }
33
33
  ```
34
34
 
35
- That's it. The agent gets 9 tools and calls them on demand. No extra tokens wasted.
35
+ That's it. The agent gets 10 tools and calls them on demand. No extra tokens wasted.
36
36
 
37
37
  ### Remote mode
38
38
 
39
39
  Connect to a Nowledge Mem server running elsewhere (a VPS, a home server, or a shared team instance). See [remote access guide](https://mem.nowledge.co/docs/remote-access) for server setup.
40
40
 
41
- Enable the plugin the same way as local mode, then set your remote server in `~/.nowledge-mem/openclaw.json`:
41
+ Enable the plugin the same way as local mode, then set `apiUrl` and `apiKey` in the OpenClaw plugin settings.
42
+
43
+ Or use a config file at `~/.nowledge-mem/openclaw.json`:
42
44
 
43
45
  ```json
44
46
  {
@@ -49,6 +51,11 @@ Enable the plugin the same way as local mode, then set your remote server in `~/
49
51
 
50
52
  The `apiKey` is injected as `NMEM_API_KEY` into the nmem CLI process. Never passed as a CLI argument, never logged.
51
53
 
54
+ ### Configure via WebUI of OpenClaw
55
+
56
+ <img width="1192" height="2007" alt="openclaw-config" src="https://github.com/user-attachments/assets/a4abd855-db56-4a1f-b0ac-0114909d0c20" />
57
+
58
+
52
59
  ## How It Works
53
60
 
54
61
  ### Per-Turn Flow
@@ -62,7 +69,7 @@ flowchart TD
62
69
  B --> C["Behavioral guidance(always, ~110 tokens)"]
63
70
  B -.->|"sessionContext: true"| D["Working Memory +relevant memories (~1-2 KB)"]
64
71
 
65
- C --> E["Agent processes message(9 tools available)"]
72
+ C --> E["Agent processes message(10 tools available)"]
66
73
  D --> E
67
74
 
68
75
  E --> F{"Needs past context?"}
@@ -91,6 +98,7 @@ The behavioral hook nudges the agent to **search before answering** and **save a
91
98
  | Memory has `sourceThreadId` | `nowledge_mem_thread_fetch` | Fetch full source conversation. Pagination via `offset` + `limit`. |
92
99
  | "Find our discussion about X" | `nowledge_mem_thread_search` | Search past conversations by keyword. Returns matched snippets. |
93
100
  | "Forget X" | `nowledge_mem_forget` | Delete by ID or search query. |
101
+ | "Is my setup working?" | `nowledge_mem_status` | Show effective config, backend connectivity, and version. |
94
102
 
95
103
  ### Session Lifecycle (Automatic Capture)
96
104
 
@@ -153,21 +161,21 @@ flowchart LR
153
161
  subgraph "default" ["Default (recommended)"]
154
162
  direction TB
155
163
  D1["Every turn: behavioral guidance (~50 tokens)"]
156
- D2["Agent calls 9 tools on demand"]
164
+ D2["Agent calls 10 tools on demand"]
157
165
  D3["Session end: thread capture + LLM distillation"]
158
166
  end
159
167
 
160
168
  subgraph context ["Session Context"]
161
169
  direction TB
162
170
  C1["Every turn: guidance + Working Memory+ recalled memories (~1-2 KB)"]
163
- C2["Agent calls 9 tools on demand"]
171
+ C2["Agent calls 10 tools on demand"]
164
172
  C3["Session end: thread capture + LLM distillation"]
165
173
  end
166
174
 
167
175
  subgraph minimal ["Minimal"]
168
176
  direction TB
169
177
  M1["Every turn: behavioral guidance (~50 tokens)"]
170
- M2["Agent calls 9 tools on demand"]
178
+ M2["Agent calls 10 tools on demand"]
171
179
  M3["No automatic capture"]
172
180
  end
173
181
  ```
@@ -254,8 +262,8 @@ The plugin supports three modes. Choose based on how much you want to guarantee
254
262
 
255
263
  | Mode | Config | Behavior | Tradeoff |
256
264
  |------|--------|----------|----------|
257
- | **Default** (recommended) | `sessionContext: false, sessionDigest: true` | Agent calls 9 tools on demand. Behavioral guidance every turn (~50 tokens). Conversations captured + distilled at session end. | Lowest cost. Agent decides when to search, usually smart, occasionally forgets. |
258
- | **Session context** | `sessionContext: true` | Working Memory + relevant memories injected at prompt time, plus all 9 tools still available. | ~1-2 KB/turn. Guarantees context is always present. Best for short sessions or critical workflows. |
265
+ | **Default** (recommended) | `sessionContext: false, sessionDigest: true` | Agent calls 10 tools on demand. Behavioral guidance every turn (~50 tokens). Conversations captured + distilled at session end. | Lowest cost. Agent decides when to search, usually smart, occasionally forgets. |
266
+ | **Session context** | `sessionContext: true` | Working Memory + relevant memories injected at prompt time, plus all 10 tools still available. | ~1-2 KB/turn. Guarantees context is always present. Best for short sessions or critical workflows. |
259
267
  | **Minimal** | `sessionDigest: false` | Tool-only, no automatic capture. | Zero overhead beyond guidance. Use when you handle memory manually. |
260
268
 
261
269
  **Which mode should you use?**
@@ -319,7 +327,23 @@ openclaw nowledge-mem status
319
327
 
320
328
  ## Configuration
321
329
 
322
- On first startup, the plugin creates a config file at `~/.nowledge-mem/openclaw.json` with defaults. Edit it to customize:
330
+ No config needed to get started. The defaults work for local mode.
331
+
332
+ To change settings, use the OpenClaw plugin settings UI. Changes take effect on restart.
333
+
334
+ | Key | Type | Default | Description |
335
+ |-----|------|---------|-------------|
336
+ | `sessionContext` | boolean | `false` | Inject Working Memory + relevant memories at prompt time |
337
+ | `sessionDigest` | boolean | `true` | Thread capture + LLM distillation at session end |
338
+ | `digestMinInterval` | integer | `300` | Minimum seconds between session digests for the same thread (0-86400) |
339
+ | `maxContextResults` | integer | `5` | Max memories to inject at prompt time (1-20, only used when sessionContext is enabled) |
340
+ | `recallMinScore` | integer | `0` | Min relevance score (0-100%) to include in auto-recall. 0 = include all |
341
+ | `apiUrl` | string | `""` | Remote server URL. Empty = local (`http://127.0.0.1:14242`) |
342
+ | `apiKey` | string | `""` | API key for remote access. Injected as `NMEM_API_KEY` env var, never logged |
343
+
344
+ ### Advanced: config file
345
+
346
+ For persistent or scripted config, create `~/.nowledge-mem/openclaw.json`:
323
347
 
324
348
  ```json
325
349
  {
@@ -327,21 +351,13 @@ On first startup, the plugin creates a config file at `~/.nowledge-mem/openclaw.
327
351
  "sessionDigest": true,
328
352
  "digestMinInterval": 300,
329
353
  "maxContextResults": 5,
354
+ "recallMinScore": 0,
330
355
  "apiUrl": "",
331
356
  "apiKey": ""
332
357
  }
333
358
  ```
334
359
 
335
- Edit the file and restart OpenClaw. The plugin reads it on every startup.
336
-
337
- | Key | Type | Default | Description |
338
- |-----|------|---------|-------------|
339
- | `sessionContext` | boolean | `false` | Inject Working Memory + relevant memories at prompt time |
340
- | `sessionDigest` | boolean | `true` | Thread capture + LLM distillation at session end |
341
- | `digestMinInterval` | integer | `300` | Minimum seconds between session digests for the same thread (0-86400) |
342
- | `maxContextResults` | integer | `5` | Max memories to inject at prompt time (1-20, only used when sessionContext is enabled) |
343
- | `apiUrl` | string | `""` | Remote server URL. Empty = local (`http://127.0.0.1:14242`) |
344
- | `apiKey` | string | `""` | API key for remote access. Injected as `NMEM_API_KEY` env var, never logged |
360
+ The config file takes priority over OpenClaw settings. Only the keys you include are overridden; missing keys fall through to OpenClaw settings, then env vars, then defaults.
345
361
 
346
362
  ### Advanced: environment variables
347
363
 
@@ -352,11 +368,18 @@ NMEM_SESSION_CONTEXT=true
352
368
  NMEM_SESSION_DIGEST=true
353
369
  NMEM_DIGEST_MIN_INTERVAL=300
354
370
  NMEM_MAX_CONTEXT_RESULTS=5
371
+ NMEM_RECALL_MIN_SCORE=0
355
372
  NMEM_API_URL=https://...
356
373
  NMEM_API_KEY=your-key
357
374
  ```
358
375
 
359
- **Priority**: OpenClaw plugin config > config file > env vars > defaults.
376
+ ### Priority
377
+
378
+ ```
379
+ config file > OpenClaw settings > env vars > defaults
380
+ ```
381
+
382
+ Use `nowledge_mem_status` (or `openclaw nowledge-mem status`) to see where each value comes from.
360
383
 
361
384
  ## What Makes This Different
362
385
 
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "id": "openclaw-nowledge-mem",
3
- "version": "0.6.6",
3
+ "version": "0.6.9",
4
4
  "kind": "memory",
5
5
  "uiHints": {
6
6
  "sessionContext": {
7
7
  "label": "Session context injection",
8
- "help": "Inject Working Memory and relevant memories at prompt time. Off by default \u2014 the agent can call memory_search on demand instead."
8
+ "help": "Inject Working Memory and relevant memories at prompt time. Off by default; the agent can call memory_search on demand instead."
9
9
  },
10
10
  "sessionDigest": {
11
11
  "label": "Session digest at end",
12
- "help": "Capture conversation threads and distill key memories via LLM at session end. On by default \u2014 negligible cost, one triage call per session."
12
+ "help": "Capture conversation threads and distill key memories via LLM at session end. On by default. Negligible cost, one triage call per session."
13
13
  },
14
14
  "digestMinInterval": {
15
15
  "label": "Minimum digest interval (seconds)",
@@ -17,7 +17,11 @@
17
17
  },
18
18
  "maxContextResults": {
19
19
  "label": "Max context results",
20
- "help": "How many memories to inject for each context cycle (1\u201320)"
20
+ "help": "How many memories to inject for each context cycle (1-20)"
21
+ },
22
+ "recallMinScore": {
23
+ "label": "Min recall score (%)",
24
+ "help": "Only inject memories with relevance score above this threshold (0-100). Set to 0 to include all results."
21
25
  },
22
26
  "apiUrl": {
23
27
  "label": "Server URL (remote mode)",
@@ -25,7 +29,7 @@
25
29
  },
26
30
  "apiKey": {
27
31
  "label": "API key (remote mode)",
28
- "help": "Required when connecting to a remote server. Never logged or passed as a CLI argument \u2014 injected as NMEM_API_KEY env var only.",
32
+ "help": "Required when connecting to a remote server. Never logged or passed as a CLI argument. Injected as NMEM_API_KEY env var only.",
29
33
  "secret": true
30
34
  }
31
35
  },
@@ -35,7 +39,7 @@
35
39
  "sessionContext": {
36
40
  "type": "boolean",
37
41
  "default": false,
38
- "description": "Inject Working Memory and recalled memories at prompt time. Off by default \u2014 the agent queries memory on demand via tools."
42
+ "description": "Inject Working Memory and recalled memories at prompt time. Off by default; the agent queries memory on demand via tools."
39
43
  },
40
44
  "sessionDigest": {
41
45
  "type": "boolean",
@@ -56,6 +60,13 @@
56
60
  "maximum": 20,
57
61
  "description": "Maximum memories to inject at prompt time (only used when sessionContext is enabled)"
58
62
  },
63
+ "recallMinScore": {
64
+ "type": "integer",
65
+ "default": 0,
66
+ "minimum": 0,
67
+ "maximum": 100,
68
+ "description": "Minimum relevance score (0-100%) to include a memory in auto-recall. Set to 0 to include all results."
69
+ },
59
70
  "apiUrl": {
60
71
  "type": "string",
61
72
  "default": "",
@@ -66,7 +77,6 @@
66
77
  "default": "",
67
78
  "description": "API key for remote access. Stored in plugin config, injected as NMEM_API_KEY env var. Never logged."
68
79
  }
69
- },
70
- "additionalProperties": false
80
+ }
71
81
  }
72
82
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowledge/openclaw-nowledge-mem",
3
- "version": "0.6.6",
3
+ "version": "0.6.9",
4
4
  "type": "module",
5
5
  "description": "Nowledge Mem memory plugin for OpenClaw, local-first personal knowledge base",
6
6
  "author": {
package/src/config.js CHANGED
@@ -1,4 +1,4 @@
1
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
1
+ import { existsSync, readFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
 
@@ -19,6 +19,7 @@ const ALLOWED_KEYS = new Set([
19
19
  "sessionDigest",
20
20
  "digestMinInterval",
21
21
  "maxContextResults",
22
+ "recallMinScore",
22
23
  "apiUrl",
23
24
  "apiKey",
24
25
  // Legacy aliases — accepted but not advertised
@@ -42,69 +43,14 @@ const ALIAS_KEYS = {
42
43
  const CONFIG_DIR = join(homedir(), ".nowledge-mem");
43
44
  const CONFIG_PATH = join(CONFIG_DIR, "openclaw.json");
44
45
 
45
- const DEFAULT_CONFIG = {
46
- sessionContext: false,
47
- sessionDigest: true,
48
- digestMinInterval: 300,
49
- maxContextResults: 5,
50
- apiUrl: "",
51
- apiKey: "",
52
- };
53
-
54
46
  /**
55
- * Read ~/.nowledge-mem/openclaw.json.
56
- * On first run, creates the file seeded with values from pluginConfig
57
- * (if any) — this migrates settings before OpenClaw strips them.
58
- * Returns {} on any error — never crashes.
47
+ * Read ~/.nowledge-mem/openclaw.json if it exists.
48
+ * Returns {} when the file is missing or unreadable.
59
49
  */
60
- function readConfigFile(logger, pluginCfg) {
50
+ function readConfigFile(logger) {
61
51
  try {
62
52
  if (!existsSync(CONFIG_PATH)) {
63
- // Seed from pluginConfig + env vars for migration.
64
- // Users on older OpenClaw get their values written to the file
65
- // BEFORE a future OpenClaw update can strip them.
66
- const initial = { ...DEFAULT_CONFIG };
67
- if (pluginCfg && typeof pluginCfg === "object") {
68
- // Resolve aliases in pluginConfig for seeding
69
- const resolved = { ...pluginCfg };
70
- for (const [oldKey, newKey] of Object.entries(ALIAS_KEYS)) {
71
- if (oldKey in resolved) {
72
- if (!(newKey in resolved)) {
73
- resolved[newKey] = resolved[oldKey];
74
- }
75
- delete resolved[oldKey];
76
- }
77
- }
78
-
79
- if (typeof resolved.sessionContext === "boolean")
80
- initial.sessionContext = resolved.sessionContext;
81
- if (typeof resolved.sessionDigest === "boolean")
82
- initial.sessionDigest = resolved.sessionDigest;
83
- if (
84
- typeof resolved.digestMinInterval === "number" &&
85
- Number.isFinite(resolved.digestMinInterval)
86
- )
87
- initial.digestMinInterval = resolved.digestMinInterval;
88
- if (
89
- typeof resolved.maxContextResults === "number" &&
90
- Number.isFinite(resolved.maxContextResults)
91
- )
92
- initial.maxContextResults = resolved.maxContextResults;
93
- if (typeof resolved.apiUrl === "string" && resolved.apiUrl.trim())
94
- initial.apiUrl = resolved.apiUrl.trim();
95
- // apiKey intentionally NOT written to disk — keep in env/pluginConfig only
96
- }
97
-
98
- mkdirSync(CONFIG_DIR, { recursive: true });
99
- writeFileSync(
100
- CONFIG_PATH,
101
- JSON.stringify(initial, null, 2) + "\n",
102
- "utf-8",
103
- );
104
- logger?.info?.(
105
- `nowledge-mem: created config at ${CONFIG_PATH} — edit to customize`,
106
- );
107
- return { ...initial };
53
+ return {};
108
54
  }
109
55
  const content = readFileSync(CONFIG_PATH, "utf-8").trim();
110
56
  if (!content) return {};
@@ -148,7 +94,8 @@ function pickBool(obj, key) {
148
94
  }
149
95
 
150
96
  function pickNum(obj, key) {
151
- if (typeof obj[key] === "number" && Number.isFinite(obj[key])) return obj[key];
97
+ if (typeof obj[key] === "number" && Number.isFinite(obj[key]))
98
+ return obj[key];
152
99
  return undefined;
153
100
  }
154
101
 
@@ -166,14 +113,30 @@ function resolveAliases(obj) {
166
113
  return resolved;
167
114
  }
168
115
 
116
+ /**
117
+ * Return the first option whose value is not undefined.
118
+ * Each option is { value, source } where source is a label like
119
+ * "file", "pluginConfig", "env", or "default".
120
+ */
121
+ function firstDefined(...options) {
122
+ for (const opt of options) {
123
+ if (opt.value !== undefined) return opt;
124
+ }
125
+ return options[options.length - 1];
126
+ }
127
+
169
128
  // -------------------------------------------------------------------------
170
129
 
171
130
  /**
172
131
  * Parse plugin config with cascade:
173
- * pluginConfig > ~/.nowledge-mem/openclaw.json > env vars > defaults
132
+ * ~/.nowledge-mem/openclaw.json > pluginConfig > env vars > defaults
133
+ *
134
+ * The config file (if it exists) takes highest priority so users who
135
+ * create it get predictable behavior. Most users won't have one and
136
+ * will configure via OpenClaw's plugin settings UI (pluginConfig).
174
137
  *
175
138
  * Canonical keys: sessionContext, sessionDigest, digestMinInterval,
176
- * maxContextResults, apiUrl, apiKey
139
+ * maxContextResults, recallMinScore, apiUrl, apiKey
177
140
  *
178
141
  * Legacy aliases (accepted from all sources; never shown in docs):
179
142
  * autoRecall → sessionContext
@@ -184,21 +147,21 @@ function resolveAliases(obj) {
184
147
  * Environment variables (all optional):
185
148
  * NMEM_SESSION_CONTEXT — true/1/yes to enable (alias: NMEM_AUTO_RECALL)
186
149
  * NMEM_SESSION_DIGEST — true/1/yes to enable (alias: NMEM_AUTO_CAPTURE)
187
- * NMEM_DIGEST_MIN_INTERVAL — seconds (0–86400)
188
- * NMEM_MAX_CONTEXT_RESULTS — integer (1–20)
150
+ * NMEM_DIGEST_MIN_INTERVAL — seconds (0-86400)
151
+ * NMEM_MAX_CONTEXT_RESULTS — integer (1-20)
152
+ * NMEM_RECALL_MIN_SCORE — integer (0-100)
189
153
  * NMEM_API_URL — remote server URL
190
154
  * NMEM_API_KEY — API key (never logged)
191
155
  */
192
156
  export function parseConfig(raw, logger) {
193
157
  const pluginCfg = raw && typeof raw === "object" ? raw : {};
194
- const fileCfg = readConfigFile(logger, pluginCfg);
158
+ const fileCfg = readConfigFile(logger);
195
159
 
196
160
  // Resolve aliases in both sources
197
161
  const resolvedPlugin = resolveAliases(pluginCfg);
198
162
  const resolvedFile = resolveAliases(fileCfg);
199
163
 
200
164
  // Strict: reject unknown keys in pluginConfig (typo catcher).
201
- // File config is our own — validated by shape, no strict check needed.
202
165
  const unknownKeys = Object.keys(resolvedPlugin).filter(
203
166
  (k) => !ALLOWED_KEYS.has(k),
204
167
  );
@@ -209,62 +172,120 @@ export function parseConfig(raw, logger) {
209
172
  );
210
173
  }
211
174
 
212
- // --- sessionContext ---
213
- const sessionContext =
214
- pickBool(resolvedPlugin, "sessionContext") ??
215
- pickBool(resolvedFile, "sessionContext") ??
216
- envBool("NMEM_SESSION_CONTEXT", "NMEM_AUTO_RECALL") ??
217
- false;
175
+ const _sources = {};
176
+
177
+ // --- sessionContext: file > pluginConfig > env > default ---
178
+ const sc = firstDefined(
179
+ { value: pickBool(resolvedFile, "sessionContext"), source: "file" },
180
+ {
181
+ value: pickBool(resolvedPlugin, "sessionContext"),
182
+ source: "pluginConfig",
183
+ },
184
+ {
185
+ value: envBool("NMEM_SESSION_CONTEXT", "NMEM_AUTO_RECALL"),
186
+ source: "env",
187
+ },
188
+ { value: false, source: "default" },
189
+ );
190
+ const sessionContext = sc.value;
191
+ _sources.sessionContext = sc.source;
192
+
193
+ // --- sessionDigest: file > pluginConfig > env > default ---
194
+ const sd = firstDefined(
195
+ { value: pickBool(resolvedFile, "sessionDigest"), source: "file" },
196
+ {
197
+ value: pickBool(resolvedPlugin, "sessionDigest"),
198
+ source: "pluginConfig",
199
+ },
200
+ {
201
+ value: envBool("NMEM_SESSION_DIGEST", "NMEM_AUTO_CAPTURE"),
202
+ source: "env",
203
+ },
204
+ { value: true, source: "default" },
205
+ );
206
+ const sessionDigest = sd.value;
207
+ _sources.sessionDigest = sd.source;
218
208
 
219
- // --- sessionDigest ---
220
- const sessionDigest =
221
- pickBool(resolvedPlugin, "sessionDigest") ??
222
- pickBool(resolvedFile, "sessionDigest") ??
223
- envBool("NMEM_SESSION_DIGEST", "NMEM_AUTO_CAPTURE") ??
224
- true;
209
+ // --- digestMinInterval: file > pluginConfig > env > default ---
210
+ const dmiEnv =
211
+ envInt("NMEM_DIGEST_MIN_INTERVAL") ?? envInt("NMEM_CAPTURE_MIN_INTERVAL");
212
+ const dmi = firstDefined(
213
+ { value: pickNum(resolvedFile, "digestMinInterval"), source: "file" },
214
+ {
215
+ value: pickNum(resolvedPlugin, "digestMinInterval"),
216
+ source: "pluginConfig",
217
+ },
218
+ { value: dmiEnv, source: "env" },
219
+ { value: 300, source: "default" },
220
+ );
221
+ const digestMinInterval = Math.min(86400, Math.max(0, Math.trunc(dmi.value)));
222
+ _sources.digestMinInterval = dmi.source;
225
223
 
226
- // --- digestMinInterval ---
227
- const rawInterval =
228
- pickNum(resolvedPlugin, "digestMinInterval") ??
229
- pickNum(resolvedFile, "digestMinInterval") ??
230
- envInt("NMEM_DIGEST_MIN_INTERVAL") ??
231
- envInt("NMEM_CAPTURE_MIN_INTERVAL");
232
- const digestMinInterval =
233
- rawInterval !== undefined
234
- ? Math.min(86400, Math.max(0, Math.trunc(rawInterval)))
235
- : 300;
224
+ // --- maxContextResults: file > pluginConfig > env > default ---
225
+ const mcrEnv =
226
+ envInt("NMEM_MAX_CONTEXT_RESULTS") ?? envInt("NMEM_MAX_RECALL_RESULTS");
227
+ const mcr = firstDefined(
228
+ { value: pickNum(resolvedFile, "maxContextResults"), source: "file" },
229
+ {
230
+ value: pickNum(resolvedPlugin, "maxContextResults"),
231
+ source: "pluginConfig",
232
+ },
233
+ { value: mcrEnv, source: "env" },
234
+ { value: 5, source: "default" },
235
+ );
236
+ const maxContextResults = Math.min(20, Math.max(1, Math.trunc(mcr.value)));
237
+ _sources.maxContextResults = mcr.source;
236
238
 
237
- // --- maxContextResults ---
238
- const rawMax =
239
- pickNum(resolvedPlugin, "maxContextResults") ??
240
- pickNum(resolvedFile, "maxContextResults") ??
241
- envInt("NMEM_MAX_CONTEXT_RESULTS") ??
242
- envInt("NMEM_MAX_RECALL_RESULTS");
243
- const maxContextResults =
244
- rawMax !== undefined
245
- ? Math.min(20, Math.max(1, Math.trunc(rawMax)))
246
- : 5;
239
+ // --- recallMinScore: file > pluginConfig > env > default ---
240
+ const rmsEnv = envInt("NMEM_RECALL_MIN_SCORE");
241
+ const rms = firstDefined(
242
+ { value: pickNum(resolvedFile, "recallMinScore"), source: "file" },
243
+ {
244
+ value: pickNum(resolvedPlugin, "recallMinScore"),
245
+ source: "pluginConfig",
246
+ },
247
+ { value: rmsEnv, source: "env" },
248
+ { value: 0, source: "default" },
249
+ );
250
+ const recallMinScore = Math.min(100, Math.max(0, Math.trunc(rms.value)));
251
+ _sources.recallMinScore = rms.source;
247
252
 
248
- // --- apiUrl: pluginConfig > file > env > "" ---
249
- const apiUrl =
250
- (typeof resolvedPlugin.apiUrl === "string" && resolvedPlugin.apiUrl.trim()) ||
251
- (typeof resolvedFile.apiUrl === "string" && resolvedFile.apiUrl.trim()) ||
252
- envStr("NMEM_API_URL") ||
253
- "";
253
+ // --- apiUrl: file > pluginConfig > env > "" ---
254
+ const fileUrl =
255
+ typeof resolvedFile.apiUrl === "string" && resolvedFile.apiUrl.trim();
256
+ const pluginUrl =
257
+ typeof resolvedPlugin.apiUrl === "string" && resolvedPlugin.apiUrl.trim();
258
+ const au = firstDefined(
259
+ { value: fileUrl || undefined, source: "file" },
260
+ { value: pluginUrl || undefined, source: "pluginConfig" },
261
+ { value: envStr("NMEM_API_URL"), source: "env" },
262
+ { value: "", source: "default" },
263
+ );
264
+ const apiUrl = au.value;
265
+ _sources.apiUrl = au.source;
254
266
 
255
- // --- apiKey: pluginConfig > file > env > "" ---
256
- const apiKey =
257
- (typeof resolvedPlugin.apiKey === "string" && resolvedPlugin.apiKey.trim()) ||
258
- (typeof resolvedFile.apiKey === "string" && resolvedFile.apiKey.trim()) ||
259
- envStr("NMEM_API_KEY") ||
260
- "";
267
+ // --- apiKey: file > pluginConfig > env > "" ---
268
+ const fileKey =
269
+ typeof resolvedFile.apiKey === "string" && resolvedFile.apiKey.trim();
270
+ const pluginKey =
271
+ typeof resolvedPlugin.apiKey === "string" && resolvedPlugin.apiKey.trim();
272
+ const ak = firstDefined(
273
+ { value: fileKey || undefined, source: "file" },
274
+ { value: pluginKey || undefined, source: "pluginConfig" },
275
+ { value: envStr("NMEM_API_KEY"), source: "env" },
276
+ { value: "", source: "default" },
277
+ );
278
+ const apiKey = ak.value;
279
+ _sources.apiKey = ak.source;
261
280
 
262
281
  return {
263
282
  sessionContext,
264
283
  sessionDigest,
265
284
  digestMinInterval,
266
285
  maxContextResults,
286
+ recallMinScore,
267
287
  apiUrl,
268
288
  apiKey,
289
+ _sources,
269
290
  };
270
291
  }
@@ -250,7 +250,10 @@ function buildConversationText(normalized) {
250
250
  const parts = [];
251
251
  let total = 0;
252
252
  for (const m of normalized) {
253
- const text = truncate(m.fullContent || m.content, MAX_DISTILL_MESSAGE_CHARS);
253
+ const text = truncate(
254
+ m.fullContent || m.content,
255
+ MAX_DISTILL_MESSAGE_CHARS,
256
+ );
254
257
  const line = `${m.role}: ${text}`;
255
258
  if (total + line.length > MAX_CONVERSATION_CHARS) break;
256
259
  parts.push(line);
@@ -297,7 +300,9 @@ export function buildAgentEndCaptureHandler(client, cfg, logger) {
297
300
 
298
301
  // Skip when no new messages were added (e.g. heartbeat re-sync).
299
302
  if (!result || result.messagesAdded === 0) {
300
- logger.debug?.("capture: no new messages since last sync, skipping triage");
303
+ logger.debug?.(
304
+ "capture: no new messages since last sync, skipping triage",
305
+ );
301
306
  return;
302
307
  }
303
308
 
@@ -13,20 +13,148 @@ function escapeForPrompt(text) {
13
13
  );
14
14
  }
15
15
 
16
+ /** Max query length sent to search — longer messages get truncated. */
17
+ const MAX_QUERY_LENGTH = 500;
18
+
19
+ /**
20
+ * Messages shorter than this get augmented with recent conversational
21
+ * context. Below this threshold, a message is likely referential
22
+ * ("explain that more", "what about the second point?") and needs
23
+ * surrounding context to produce useful search results.
24
+ *
25
+ * Messages at or above this threshold are substantial enough to
26
+ * search on their own ("openviking 不好用", "how do I deploy to k8s?").
27
+ */
28
+ const SHORT_QUERY_THRESHOLD = 40;
29
+
30
+ /** How many recent messages to include for short-query context. */
31
+ const CONTEXT_MESSAGES = 3;
32
+
33
+ /** Per-message cap when building context window. */
34
+ const CONTEXT_MSG_MAX_CHARS = 150;
35
+
36
+ /**
37
+ * Extract text from message content (string or structured blocks).
38
+ * Mirrors the pattern from capture.js.
39
+ */
40
+ function extractText(content) {
41
+ if (typeof content === "string") return content.trim();
42
+ if (!Array.isArray(content)) return "";
43
+ const parts = [];
44
+ for (const block of content) {
45
+ if (!block || typeof block !== "object") continue;
46
+ if (block.type === "text" && typeof block.text === "string") {
47
+ const text = block.text.trim();
48
+ if (text) parts.push(text);
49
+ }
50
+ }
51
+ return parts.join("\n").trim();
52
+ }
53
+
54
+ /**
55
+ * Normalize a raw message entry to { role, text }.
56
+ * Handles both flat { role, content } and nested { message: { role, content } }.
57
+ * Returns null for non-user/assistant messages, empty content, or slash commands.
58
+ */
59
+ function normalizeMessage(raw) {
60
+ if (!raw || typeof raw !== "object") return null;
61
+ const msg =
62
+ raw.message && typeof raw.message === "object" ? raw.message : raw;
63
+ const role = typeof msg.role === "string" ? msg.role : "";
64
+ if (role !== "user" && role !== "assistant") return null;
65
+ const text = extractText(msg.content);
66
+ if (!text) return null;
67
+ // Skip slash commands — they're plugin directives, not conversational
68
+ if (role === "user" && text.startsWith("/")) return null;
69
+ return { role, text };
70
+ }
71
+
72
+ /**
73
+ * Build the search query from the hook event.
74
+ *
75
+ * Three tiers:
76
+ * >= 40 chars : latest user message alone (self-contained)
77
+ * 3-39 chars : latest user message + recent context (likely referential)
78
+ * < 3 chars : skip recall (too short — "ok", emoji, etc.)
79
+ *
80
+ * The event object provides:
81
+ * - event.messages: structured array of {role, content} messages (preferred)
82
+ * - event.prompt: the full formatted prompt (fallback, truncated)
83
+ */
84
+ function buildSearchQuery(event) {
85
+ const messages = event?.messages;
86
+
87
+ if (Array.isArray(messages) && messages.length > 0) {
88
+ // Find the latest user message (walk backwards)
89
+ let latestUserIdx = -1;
90
+ let latestUserText = "";
91
+ for (let i = messages.length - 1; i >= 0; i--) {
92
+ const norm = normalizeMessage(messages[i]);
93
+ if (norm && norm.role === "user") {
94
+ latestUserIdx = i;
95
+ latestUserText = norm.text;
96
+ break;
97
+ }
98
+ }
99
+
100
+ if (!latestUserText || latestUserText.length < 3) return "";
101
+
102
+ // Substantial message — use it alone
103
+ if (latestUserText.length >= SHORT_QUERY_THRESHOLD) {
104
+ return latestUserText.slice(0, MAX_QUERY_LENGTH);
105
+ }
106
+
107
+ // Short message — likely a follow-up. Add recent context so
108
+ // the search engine sees what topic the conversation is about.
109
+ // Collect up to CONTEXT_MESSAGES messages preceding the latest user
110
+ // message (both user and assistant, for topic grounding).
111
+ const contextParts = [];
112
+ const scanFrom = Math.max(0, latestUserIdx - CONTEXT_MESSAGES);
113
+ for (let i = scanFrom; i < latestUserIdx; i++) {
114
+ const norm = normalizeMessage(messages[i]);
115
+ if (!norm) continue;
116
+ const truncated =
117
+ norm.text.length > CONTEXT_MSG_MAX_CHARS
118
+ ? `${norm.text.slice(0, CONTEXT_MSG_MAX_CHARS)}…`
119
+ : norm.text;
120
+ contextParts.push(truncated);
121
+ }
122
+
123
+ if (contextParts.length > 0) {
124
+ // Latest message first (primary intent), then context
125
+ const combined = `${latestUserText}\n\n${contextParts.join("\n")}`;
126
+ return combined.slice(0, MAX_QUERY_LENGTH);
127
+ }
128
+
129
+ // No prior context available (first message) — use as-is
130
+ return latestUserText;
131
+ }
132
+
133
+ // Fallback: use event.prompt but truncate aggressively.
134
+ // This path only fires when OpenClaw doesn't provide event.messages.
135
+ if (typeof event?.prompt === "string" && event.prompt.length >= 5) {
136
+ return event.prompt.slice(0, MAX_QUERY_LENGTH);
137
+ }
138
+
139
+ return "";
140
+ }
141
+
16
142
  /**
17
- * Builds the before_agent_start hook handler.
143
+ * Builds the before_prompt_build hook handler.
18
144
  *
19
- * Injects two layers of context at session start:
145
+ * Injects two layers of context at prompt time:
20
146
  * 1. Working Memory — today's focus, priorities, unresolved flags
21
- * 2. Relevant memories — with types, labels, and source provenance
147
+ * 2. Relevant memories — searched using the user's latest message
22
148
  *
23
149
  * Tool guidance is minimal — the agent already sees full tool descriptions
24
150
  * in its tool list. We only add a brief behavioral note.
25
151
  */
26
152
  export function buildRecallHandler(client, cfg, logger) {
153
+ const minScore = (cfg.recallMinScore ?? 0) / 100; // config is 0-100, API is 0-1
154
+
27
155
  return async (event) => {
28
- const prompt = event.prompt;
29
- if (!prompt || prompt.length < 5) return;
156
+ const searchQuery = buildSearchQuery(event);
157
+ if (!searchQuery) return;
30
158
 
31
159
  const sections = [];
32
160
 
@@ -44,9 +172,17 @@ export function buildRecallHandler(client, cfg, logger) {
44
172
 
45
173
  // 2. Relevant memories — enriched with scoring signals and labels
46
174
  try {
47
- const results = await client.searchRich(prompt, cfg.maxContextResults);
48
- if (results.length > 0) {
49
- const lines = results.map((r) => {
175
+ const results = await client.searchRich(
176
+ searchQuery,
177
+ cfg.maxContextResults,
178
+ );
179
+ // Filter by minimum score if configured
180
+ const filtered =
181
+ minScore > 0
182
+ ? results.filter((r) => (r.score ?? 0) >= minScore)
183
+ : results;
184
+ if (filtered.length > 0) {
185
+ const lines = filtered.map((r) => {
50
186
  const title = r.title || "(untitled)";
51
187
  const score = `${(r.score * 100).toFixed(0)}%`;
52
188
  const labels =
@@ -82,7 +218,9 @@ export function buildRecallHandler(client, cfg, logger) {
82
218
  "</nowledge-mem>",
83
219
  ].join("\n");
84
220
 
85
- logger.debug?.(`recall: injecting ${context.length} chars`);
221
+ logger.debug?.(
222
+ `recall: injecting ${context.length} chars (query: ${searchQuery.slice(0, 80)}…)`,
223
+ );
86
224
  return { prependContext: context };
87
225
  };
88
226
  }
package/src/index.js CHANGED
@@ -18,6 +18,7 @@ import { createForgetTool } from "./tools/forget.js";
18
18
  import { createMemoryGetTool } from "./tools/memory-get.js";
19
19
  import { createMemorySearchTool } from "./tools/memory-search.js";
20
20
  import { createSaveTool } from "./tools/save.js";
21
+ import { createStatusTool } from "./tools/status.js";
21
22
  import { createThreadFetchTool } from "./tools/thread-fetch.js";
22
23
  import { createThreadSearchTool } from "./tools/thread-search.js";
23
24
  import { createTimelineTool } from "./tools/timeline.js";
@@ -52,6 +53,9 @@ export default {
52
53
  api.registerTool(createThreadSearchTool(client, logger));
53
54
  api.registerTool(createThreadFetchTool(client, logger));
54
55
 
56
+ // Diagnostics
57
+ api.registerTool(createStatusTool(client, logger, cfg));
58
+
55
59
  // Always-on: behavioral guidance so the agent proactively saves and searches.
56
60
  // Fires every turn via before_prompt_build — ~50 tokens, negligible cost.
57
61
  // When sessionContext is on, guidance adjusts to avoid redundant searches.
@@ -293,9 +293,7 @@ export function createConnectionsTool(client, logger) {
293
293
  const otherTitle = isOlderNode
294
294
  ? edge.newer_title || "(untitled)"
295
295
  : edge.older_title || "(untitled)";
296
- const otherId = isOlderNode
297
- ? edge.newer_id
298
- : edge.older_id;
296
+ const otherId = isOlderNode ? edge.newer_id : edge.older_id;
299
297
  const direction = isOlderNode ? "→" : "←";
300
298
  const idLine = otherId ? `\n → id: ${otherId}` : "";
301
299
  return ` ${direction} ${otherTitle}${relLabel ? ` — ${relLabel}` : ""}${idLine}`;
@@ -0,0 +1,152 @@
1
+ /**
2
+ * nowledge_mem_status - verify config, connectivity, and backend health.
3
+ *
4
+ * Lets the user (or agent) confirm that apiUrl, apiKey, and other
5
+ * settings are taking effect, and that the backend is reachable.
6
+ * Shows where each config value comes from (file, pluginConfig, env, default).
7
+ */
8
+ import { isDefaultApiUrl } from "../config.js";
9
+
10
+ export function createStatusTool(client, _logger, cfg) {
11
+ return {
12
+ name: "nowledge_mem_status",
13
+ description:
14
+ "Check Nowledge Mem status: effective configuration, backend connectivity, and version. " +
15
+ "Call this when the user wants to verify their setup is working, " +
16
+ "especially after changing apiUrl or apiKey for remote mode.",
17
+ parameters: {
18
+ type: "object",
19
+ properties: {},
20
+ },
21
+ async execute() {
22
+ const lines = [];
23
+ const details = {};
24
+ const sources = cfg._sources || {};
25
+
26
+ // 1. Mode + connection target
27
+ const remote = !isDefaultApiUrl(cfg.apiUrl);
28
+ const mode = remote ? "remote" : "local";
29
+ details.mode = mode;
30
+ details.apiUrl = cfg.apiUrl || "http://127.0.0.1:14242";
31
+ details.apiKeySet = Boolean(cfg.apiKey);
32
+
33
+ lines.push(
34
+ `Mode: ${remote ? `Remote (${cfg.apiUrl})` : "Local (127.0.0.1:14242)"}`,
35
+ );
36
+ lines.push(`API key: ${cfg.apiKey ? "set" : "not set"}`);
37
+
38
+ // 2. CLI resolution
39
+ try {
40
+ const cmd = client.resolveCommand();
41
+ details.cli = cmd.join(" ");
42
+ lines.push(`CLI: ${cmd.join(" ")}`);
43
+ } catch {
44
+ details.cli = null;
45
+ lines.push("CLI: not found (install with: pip install nmem-cli)");
46
+ }
47
+
48
+ // 3. Backend health
49
+ let healthy = false;
50
+ try {
51
+ healthy = await client.checkHealth();
52
+ } catch {
53
+ // checkHealth already returns false on error
54
+ }
55
+ details.healthy = healthy;
56
+
57
+ if (healthy) {
58
+ lines.push("Backend: reachable");
59
+ // Fetch detailed health info
60
+ try {
61
+ const health = await client.apiJson(
62
+ "GET",
63
+ "/health",
64
+ undefined,
65
+ 5000,
66
+ );
67
+ if (health.version) {
68
+ details.version = health.version;
69
+ lines.push(`Version: ${health.version}`);
70
+ }
71
+ if (health.database_connected !== undefined) {
72
+ details.databaseConnected = health.database_connected;
73
+ lines.push(
74
+ `Database: ${health.database_connected ? "connected" : "disconnected"}`,
75
+ );
76
+ }
77
+ } catch {
78
+ // Health endpoint not available on older backends
79
+ }
80
+ } else {
81
+ lines.push("Backend: not reachable");
82
+ if (remote) {
83
+ lines.push(
84
+ " Check that apiUrl is correct and the server is running.",
85
+ );
86
+ if (!cfg.apiKey) {
87
+ lines.push(" Remote mode usually requires an API key.");
88
+ }
89
+ } else {
90
+ lines.push(" Ensure the Nowledge Mem desktop app is running.");
91
+ }
92
+ }
93
+
94
+ // 4. Effective config with sources
95
+ lines.push("");
96
+ lines.push("Config (source):");
97
+ lines.push(
98
+ ` sessionContext: ${cfg.sessionContext} (${sources.sessionContext || "?"})`,
99
+ );
100
+ lines.push(
101
+ ` sessionDigest: ${cfg.sessionDigest} (${sources.sessionDigest || "?"})`,
102
+ );
103
+ lines.push(
104
+ ` digestMinInterval: ${cfg.digestMinInterval}s (${sources.digestMinInterval || "?"})`,
105
+ );
106
+ lines.push(
107
+ ` maxContextResults: ${cfg.maxContextResults} (${sources.maxContextResults || "?"})`,
108
+ );
109
+ lines.push(
110
+ ` recallMinScore: ${cfg.recallMinScore}% (${sources.recallMinScore || "?"})`,
111
+ );
112
+
113
+ details.config = {
114
+ sessionContext: {
115
+ value: cfg.sessionContext,
116
+ source: sources.sessionContext,
117
+ },
118
+ sessionDigest: {
119
+ value: cfg.sessionDigest,
120
+ source: sources.sessionDigest,
121
+ },
122
+ digestMinInterval: {
123
+ value: cfg.digestMinInterval,
124
+ source: sources.digestMinInterval,
125
+ },
126
+ maxContextResults: {
127
+ value: cfg.maxContextResults,
128
+ source: sources.maxContextResults,
129
+ },
130
+ recallMinScore: {
131
+ value: cfg.recallMinScore,
132
+ source: sources.recallMinScore,
133
+ },
134
+ apiUrl: { value: cfg.apiUrl || "(local)", source: sources.apiUrl },
135
+ apiKey: {
136
+ value: cfg.apiKey ? "(set)" : "(not set)",
137
+ source: sources.apiKey,
138
+ },
139
+ };
140
+
141
+ lines.push("");
142
+ lines.push(
143
+ "Priority: config file > OpenClaw settings > env vars > defaults",
144
+ );
145
+
146
+ return {
147
+ content: [{ type: "text", text: lines.join("\n") }],
148
+ details,
149
+ };
150
+ },
151
+ };
152
+ }