@nowledge/openclaw-nowledge-mem 0.7.1 → 0.7.3

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,7 +2,18 @@
2
2
 
3
3
  All notable changes to the Nowledge Mem OpenClaw plugin will be documented in this file.
4
4
 
5
- ## [Unreleased]
5
+ ## [0.7.3] - 2026-03-30
6
+
7
+ ### Added
8
+
9
+ - **Status tool now checks plugin trust and memory slot.** `nowledge_mem_status` reads `plugins.allow` from the OpenClaw config: warns when the allowlist exists but the plugin is missing (loading will be blocked); shows a tip when no allowlist is set (plugin loads via `entries.enabled`, but trust is implicit). Also warns when the memory slot points to `memory-core` instead of `openclaw-nowledge-mem` — a common misconfiguration after upgrading to OpenClaw 3.22+. Both checks include the exact fix command.
10
+ - **Clarified that no `tools.*` config is needed.** All plugin tools register automatically when the plugin loads. Listing `nowledge_mem_*` names in `tools.allow` is a misconfiguration — OpenClaw silently strips allowlists that contain only plugin entries. The only config users need is `plugins.allow: ["openclaw-nowledge-mem"]`. Updated troubleshooting in README, SKILL.md, and website docs (EN + ZH).
11
+
12
+ ## [0.7.2] - 2026-03-30
13
+
14
+ ### Added
15
+
16
+ - **Status tool now reports memory slot configuration.** `nowledge_mem_status` checks whether the OpenClaw memory slot points to `openclaw-nowledge-mem`. If another plugin (e.g. the built-in `memory-core`) holds the slot, the tool shows a warning with the fix command. This catches a common misconfiguration after upgrading to OpenClaw 3.22+, where the implicit slot default changed to `memory-core`.
6
17
 
7
18
  ### Fixed
8
19
 
package/CLAUDE.md CHANGED
@@ -118,7 +118,7 @@ When `contextEngine` points elsewhere (or is absent), hooks handle everything. N
118
118
  - `nowledge_mem_thread_fetch` - fetch messages from a specific thread. Start with a small page, then use `offset` + `limit` for progressive retrieval only when more context is needed.
119
119
 
120
120
  ### Diagnostics
121
- - `nowledge_mem_status` - show effective config (mode, apiUrl, apiKey set, sessionContext, sessionDigest, etc.), backend connectivity, and version. No parameters.
121
+ - `nowledge_mem_status` - show plugin trust status, memory slot status, effective config (mode, apiUrl, apiKey set, sessionContext, sessionDigest, etc.), backend connectivity, and version. Checks `plugins.allow`: warns if set but plugin not included (loading blocked); shows tip if not set (plugin loads via `entries.enabled` but trust is implicit). Warns if the memory slot points to `memory-core` instead of `openclaw-nowledge-mem` (common after OpenClaw 3.22+ upgrade). No parameters.
122
122
 
123
123
  ## Skill Surface
124
124
 
@@ -151,6 +151,8 @@ Credentials (apiUrl/apiKey): also reads `~/.nowledge-mem/config.json` (shared wi
151
151
  | `maxContextResults` | integer 1-20 | `5` | `NMEM_MAX_CONTEXT_RESULTS` | How many memories to inject at prompt time |
152
152
  | `recallMinScore` | integer 0-100 | `0` | `NMEM_RECALL_MIN_SCORE` | Min relevance score (%) to include in auto-recall |
153
153
  | `maxThreadMessageChars` | integer 200-20000 | `800` | `NMEM_MAX_THREAD_MESSAGE_CHARS` | Max chars per captured thread message before truncation |
154
+ | `captureExclude` | string[] | `[]` | — | Session key glob patterns to skip during auto-capture. `*` matches within a colon-segment. Example: `["agent:*:cron:*"]` |
155
+ | `captureSkipMarker` | string | `"#nmem-skip"` | — | In-band marker: any message containing this text skips capture for the session. Not sticky across compaction |
154
156
  | `apiUrl` | string | `""` | `NMEM_API_URL` | Remote server URL. Empty = local (127.0.0.1:14242) |
155
157
  | `apiKey` | string | `""` | `NMEM_API_KEY` | API key. Never logged. |
156
158
 
package/README.md CHANGED
@@ -433,6 +433,56 @@ If OpenClaw logs a warning like `plugins.allow is empty; discovered non-bundled
433
433
 
434
434
  If you also use linked or workspace copies, review `plugins.load.paths` before relying on an allowlist. OpenClaw allowlists plugin ids, not install provenance.
435
435
 
436
+ ## Troubleshooting
437
+
438
+ **Only `memory_search` and `memory_get` show up — saves go to local markdown files**
439
+
440
+ The memory slot is probably still set to the built-in `memory-core`. OpenClaw 3.22+ defaults to `memory-core` when no explicit slot is configured. Reinstall to reset the slot automatically:
441
+
442
+ ```bash
443
+ openclaw plugins install @nowledge/openclaw-nowledge-mem
444
+ ```
445
+
446
+ Or set the slot manually in `~/.openclaw/openclaw.json`:
447
+
448
+ ```json
449
+ {
450
+ "plugins": {
451
+ "slots": { "memory": "openclaw-nowledge-mem" }
452
+ }
453
+ }
454
+ ```
455
+
456
+ Restart OpenClaw after either change.
457
+
458
+ **Plugin tools not available**
459
+
460
+ All 10 plugin tools register automatically when the plugin loads. No tool-level config is needed — just make sure the plugin itself is trusted:
461
+
462
+ ```json
463
+ {
464
+ "plugins": {
465
+ "allow": ["openclaw-nowledge-mem"]
466
+ }
467
+ }
468
+ ```
469
+
470
+ If `openclaw status` shows a CRITICAL warning about `plugins.allow`, this is the fix. Run `nowledge_mem_status` inside a conversation to check both plugin trust and memory slot status.
471
+
472
+ Do not list `nowledge_mem_*` tool names in `tools.allow` — OpenClaw silently strips allowlists that contain only plugin entries, so the config looks active but does nothing.
473
+
474
+ **Search timeouts with many concurrent agents**
475
+
476
+ When running many agents in parallel, all searches share a single database connection. Upgrade to Nowledge Mem v0.6.12+ (backend) so scoring writes no longer block search responses.
477
+
478
+ **`nmem --json status` fails in local mode**
479
+
480
+ The Nowledge Mem backend is not running. Start it from the desktop app, or run the backend manually.
481
+
482
+ **Remote mode not connecting**
483
+
484
+ Check `~/.nowledge-mem/config.json` credentials. Run `nmem --json --api-url "$URL" status` to verify the server is reachable.
485
+
436
486
  ## What Makes This Different
437
487
 
438
488
  - **Local-first**: no API key, no cloud account. Your knowledge stays on your machine.
package/SKILL.md CHANGED
@@ -186,6 +186,18 @@ First check the plugin status:
186
186
  openclaw nowledge-mem status
187
187
  ```
188
188
 
189
+ If only `memory_search` and `memory_get` tools are available (other Nowledge Mem tools missing), the memory slot may still point to the built-in `memory-core`. Verify the slot in `~/.openclaw/openclaw.json`:
190
+
191
+ ```json
192
+ {
193
+ "plugins": {
194
+ "slots": { "memory": "openclaw-nowledge-mem" }
195
+ }
196
+ }
197
+ ```
198
+
199
+ If missing, reinstall (`openclaw plugins install @nowledge/openclaw-nowledge-mem`) which sets the slot automatically. This is common after upgrading OpenClaw to 3.22+ where the default memory slot changed.
200
+
189
201
  Then verify in chat.
190
202
 
191
203
  For a clean synthetic check:
@@ -237,6 +249,7 @@ What to expect:
237
249
  | `plugins.allow is empty` warning | Add `openclaw-nowledge-mem` to `plugins.allow` if the user wants explicit trust |
238
250
  | Remote config seems ignored | Check whether `~/.nowledge-mem/openclaw.json` is overriding plugin settings |
239
251
  | Local mode unexpectedly talks to a remote server | Check for stale `NMEM_API_URL` / `NMEM_API_KEY` in the environment or an overriding `~/.nowledge-mem/openclaw.json` |
252
+ | Plugin tools missing | Ensure the plugin is in `plugins.allow: ["openclaw-nowledge-mem"]`. Do **not** put `nowledge_mem_*` tool names in `tools.allow` — OpenClaw silently strips plugin-only allowlists. No `tools.*` config is needed; plugin tools load automatically when the plugin is allowed. |
240
253
 
241
254
  ## Notes for Agents
242
255
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "openclaw-nowledge-mem",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "kind": "memory",
5
5
  "skills": ["skills/memory-guide"],
6
6
  "uiHints": {
@@ -28,6 +28,14 @@
28
28
  "label": "Max thread message chars",
29
29
  "help": "Maximum characters preserved per captured OpenClaw thread message (200-20000). Higher values keep more of long messages in Nowledge thread history."
30
30
  },
31
+ "captureExclude": {
32
+ "label": "Exclude session patterns",
33
+ "help": "Session key glob patterns to skip during auto-capture. Use * to match within a colon-segment. Example: agent:*:cron:*"
34
+ },
35
+ "captureSkipMarker": {
36
+ "label": "Skip marker",
37
+ "help": "Text marker in messages that prevents capture for the session. Default: #nmem-skip. Note: if compaction drops the marked message, capture resumes — use captureExclude patterns for persistent filtering."
38
+ },
31
39
  "apiUrl": {
32
40
  "label": "Server URL (remote mode)",
33
41
  "help": "Leave empty for local mode (default: http://127.0.0.1:14242). Set to your remote server URL for cross-device or team access. See: https://docs.nowledge.co/docs/remote-access"
@@ -79,6 +87,17 @@
79
87
  "maximum": 20000,
80
88
  "description": "Maximum characters preserved per captured OpenClaw thread message before truncation."
81
89
  },
90
+ "captureExclude": {
91
+ "type": "array",
92
+ "items": { "type": "string" },
93
+ "default": [],
94
+ "description": "Session key glob patterns to exclude from auto-capture. Glob * matches within a colon-delimited segment. Example: agent:*:cron:* excludes all cron job sessions."
95
+ },
96
+ "captureSkipMarker": {
97
+ "type": "string",
98
+ "default": "#nmem-skip",
99
+ "description": "Text marker in messages that prevents capture for the entire session. When any message contains this marker, the session is skipped. Note: compaction may drop the marked message — use captureExclude for persistent filtering."
100
+ },
82
101
  "apiUrl": {
83
102
  "type": "string",
84
103
  "default": "",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowledge/openclaw-nowledge-mem",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
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
@@ -21,6 +21,8 @@ const ALLOWED_KEYS = new Set([
21
21
  "maxContextResults",
22
22
  "recallMinScore",
23
23
  "maxThreadMessageChars",
24
+ "captureExclude",
25
+ "captureSkipMarker",
24
26
  "apiUrl",
25
27
  "apiKey",
26
28
  // Legacy aliases — accepted but not advertised
@@ -343,6 +345,43 @@ export function parseConfig(raw, logger) {
343
345
  const apiKey = ak.value;
344
346
  _sources.apiKey = ak.source;
345
347
 
348
+ // --- captureExclude: file > pluginConfig > default ---
349
+ const captureExclude = (() => {
350
+ const fromFile = Array.isArray(resolvedFile.captureExclude)
351
+ ? resolvedFile.captureExclude
352
+ : null;
353
+ const fromPlugin = Array.isArray(resolvedPlugin.captureExclude)
354
+ ? resolvedPlugin.captureExclude
355
+ : null;
356
+ _sources.captureExclude = fromFile
357
+ ? "file"
358
+ : fromPlugin
359
+ ? "pluginConfig"
360
+ : "default";
361
+ const entries = fromFile ?? fromPlugin ?? [];
362
+ return entries
363
+ .filter((v) => typeof v === "string" && v.trim())
364
+ .map((v) => v.trim());
365
+ })();
366
+
367
+ // --- captureSkipMarker: file > pluginConfig > default ---
368
+ const captureSkipMarker = (() => {
369
+ const fromFile =
370
+ typeof resolvedFile.captureSkipMarker === "string"
371
+ ? resolvedFile.captureSkipMarker.trim()
372
+ : undefined;
373
+ const fromPlugin =
374
+ typeof resolvedPlugin.captureSkipMarker === "string"
375
+ ? resolvedPlugin.captureSkipMarker.trim()
376
+ : undefined;
377
+ _sources.captureSkipMarker = fromFile
378
+ ? "file"
379
+ : fromPlugin
380
+ ? "pluginConfig"
381
+ : "default";
382
+ return fromFile || fromPlugin || "#nmem-skip";
383
+ })();
384
+
346
385
  return {
347
386
  sessionContext,
348
387
  sessionDigest,
@@ -350,6 +389,8 @@ export function parseConfig(raw, logger) {
350
389
  maxContextResults,
351
390
  recallMinScore,
352
391
  maxThreadMessageChars,
392
+ captureExclude,
393
+ captureSkipMarker,
353
394
  apiUrl,
354
395
  apiKey,
355
396
  _sources,
@@ -22,7 +22,12 @@
22
22
 
23
23
  import { ceState } from "./ce-state.js";
24
24
  import { BASE_GUIDANCE, SESSION_CONTEXT_GUIDANCE } from "./hooks/behavioral.js";
25
- import { appendOrCreateThread, triageAndDistill } from "./hooks/capture.js";
25
+ import {
26
+ appendOrCreateThread,
27
+ hasSkipMarker,
28
+ matchesExcludePattern,
29
+ triageAndDistill,
30
+ } from "./hooks/capture.js";
26
31
  import {
27
32
  MAX_QUERY_LENGTH,
28
33
  SHORT_QUERY_THRESHOLD,
@@ -342,8 +347,23 @@ export function createNowledgeMemContextEngineFactory(client, cfg, logger) {
342
347
  }) {
343
348
  if (isHeartbeat) return;
344
349
 
350
+ // Normalize sessionKey consistently with hook handlers
351
+ const normalizedKey = String(sessionKey || sessionId || "");
352
+
353
+ // Capture exclusion: pattern-based and marker-based filters
354
+ if (matchesExcludePattern(normalizedKey, cfg.captureExclude)) {
355
+ logger.debug?.(`ce: skipped excluded session ${normalizedKey}`);
356
+ return;
357
+ }
358
+ if (hasSkipMarker(messages, cfg.captureSkipMarker)) {
359
+ logger.debug?.(
360
+ `ce: skipped session with skip marker ${normalizedKey}`,
361
+ );
362
+ return;
363
+ }
364
+
345
365
  const event = { messages, sessionFile };
346
- const ctx = { sessionId, sessionKey };
366
+ const ctx = { sessionId, sessionKey: normalizedKey };
347
367
 
348
368
  // 1. Always capture thread (idempotent, deduped)
349
369
  const captureResult = await appendOrCreateThread({
@@ -40,6 +40,63 @@ function _setSyncedMessageCount(threadId, count) {
40
40
  }
41
41
  }
42
42
 
43
+ // ---------------------------------------------------------------------------
44
+ // Capture exclusion filters
45
+ // ---------------------------------------------------------------------------
46
+
47
+ /**
48
+ * Compile a glob pattern (where `*` matches within a colon-delimited segment)
49
+ * into a RegExp. Results are cached since patterns are immutable after parse.
50
+ */
51
+ const _compiledPatterns = new Map();
52
+ function _compileGlob(pattern) {
53
+ const key = String(pattern).toLowerCase();
54
+ let re = _compiledPatterns.get(key);
55
+ if (!re) {
56
+ re = new RegExp(
57
+ "^" +
58
+ key
59
+ .replace(/[.+?^${}()|[\]\\]/g, "\\$&")
60
+ .replace(/\*/g, "[^:]*") +
61
+ "$",
62
+ );
63
+ _compiledPatterns.set(key, re);
64
+ }
65
+ return re;
66
+ }
67
+
68
+ /**
69
+ * Test whether a session key matches any exclusion glob pattern.
70
+ * Glob `*` matches within a colon-delimited segment (not across colons).
71
+ * Example: "agent:*:cron:*" matches "agent:main:cron:abc123"
72
+ *
73
+ * Exported for reuse by Context Engine and other capture paths.
74
+ */
75
+ export function matchesExcludePattern(sessionKey, patterns) {
76
+ if (!Array.isArray(patterns) || patterns.length === 0) return false;
77
+ const key = String(sessionKey || "").toLowerCase();
78
+ return patterns.some((pattern) => _compileGlob(pattern).test(key));
79
+ }
80
+
81
+ /**
82
+ * Check if any message contains the skip marker text.
83
+ * Scans both raw message content and nested message objects.
84
+ *
85
+ * Exported for reuse by Context Engine and other capture paths.
86
+ */
87
+ export function hasSkipMarker(messages, marker) {
88
+ if (!marker || typeof marker !== "string" || !Array.isArray(messages)) return false;
89
+ const markerLc = marker.toLowerCase();
90
+ return messages.some((msg) => {
91
+ const text = extractText(msg?.content ?? msg?.message?.content);
92
+ return text.toLowerCase().includes(markerLc);
93
+ });
94
+ }
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // Message normalization utilities
98
+ // ---------------------------------------------------------------------------
99
+
43
100
  export function truncate(text, max = DEFAULT_MAX_MESSAGE_CHARS) {
44
101
  const str = String(text || "").trim();
45
102
  if (!str) return "";
@@ -197,8 +254,9 @@ export async function appendOrCreateThread({
197
254
  ctx,
198
255
  reason,
199
256
  maxMessageChars = DEFAULT_MAX_MESSAGE_CHARS,
257
+ resolvedMessages,
200
258
  }) {
201
- const rawMessages = await resolveHookMessages(event);
259
+ const rawMessages = resolvedMessages ?? (await resolveHookMessages(event));
202
260
  if (!Array.isArray(rawMessages) || rawMessages.length === 0) return;
203
261
 
204
262
  const threadId = buildStableThreadId(event, ctx);
@@ -413,6 +471,22 @@ export function buildAgentEndCaptureHandler(client, cfg, logger) {
413
471
  if (!event?.success) return;
414
472
  if (ctx?.trigger === "heartbeat") return;
415
473
 
474
+ // Layer 1: pattern-based exclusion (e.g. cron jobs, subagent sessions)
475
+ const sessionKey = String(ctx?.sessionKey || ctx?.sessionId || "");
476
+ if (matchesExcludePattern(sessionKey, cfg.captureExclude)) {
477
+ logger.debug?.(`capture: skipped excluded session ${sessionKey}`);
478
+ return;
479
+ }
480
+
481
+ // Layer 2: marker-based exclusion (user typed #nmem-skip in conversation)
482
+ const resolvedMessages = await resolveHookMessages(event);
483
+ if (hasSkipMarker(resolvedMessages, cfg.captureSkipMarker)) {
484
+ logger.debug?.(
485
+ `capture: skipped session with skip marker ${sessionKey}`,
486
+ );
487
+ return;
488
+ }
489
+
416
490
  const captureResult = await appendOrCreateThread({
417
491
  client,
418
492
  logger,
@@ -420,6 +494,7 @@ export function buildAgentEndCaptureHandler(client, cfg, logger) {
420
494
  ctx,
421
495
  reason: "agent_end",
422
496
  maxMessageChars: cfg.maxThreadMessageChars,
497
+ resolvedMessages,
423
498
  });
424
499
 
425
500
  await triageAndDistill({ client, cfg, logger, captureResult, ctx });
@@ -435,10 +510,27 @@ export function buildAgentEndCaptureHandler(client, cfg, logger) {
435
510
  *
436
511
  * Heartbeat sessions are skipped (same rationale as agent_end).
437
512
  */
438
- export function buildBeforeResetCaptureHandler(client, _cfg, logger) {
513
+ export function buildBeforeResetCaptureHandler(client, cfg, logger) {
439
514
  return async (event, ctx) => {
440
515
  if (ceState.active) return;
441
516
  if (ctx?.trigger === "heartbeat") return;
517
+
518
+ // Layer 1: pattern-based exclusion
519
+ const sessionKey = String(ctx?.sessionKey || ctx?.sessionId || "");
520
+ if (matchesExcludePattern(sessionKey, cfg.captureExclude)) {
521
+ logger.debug?.(`capture: skipped excluded session ${sessionKey}`);
522
+ return;
523
+ }
524
+
525
+ // Layer 2: marker-based exclusion
526
+ const resolvedMessages = await resolveHookMessages(event);
527
+ if (hasSkipMarker(resolvedMessages, cfg.captureSkipMarker)) {
528
+ logger.debug?.(
529
+ `capture: skipped session with skip marker ${sessionKey}`,
530
+ );
531
+ return;
532
+ }
533
+
442
534
  const reason = typeof event?.reason === "string" ? event.reason : undefined;
443
535
  await appendOrCreateThread({
444
536
  client,
@@ -446,7 +538,8 @@ export function buildBeforeResetCaptureHandler(client, _cfg, logger) {
446
538
  event,
447
539
  ctx,
448
540
  reason,
449
- maxMessageChars: _cfg?.maxThreadMessageChars,
541
+ maxMessageChars: cfg.maxThreadMessageChars,
542
+ resolvedMessages,
450
543
  });
451
544
  };
452
545
  }
package/src/index.js CHANGED
@@ -54,8 +54,14 @@ export default {
54
54
  api.registerTool(createThreadSearchTool(client, logger));
55
55
  api.registerTool(createThreadFetchTool(client, logger));
56
56
 
57
- // Diagnostics
58
- api.registerTool(createStatusTool(client, logger, cfg));
57
+ // Diagnostics — pass runtime config so the status tool can detect:
58
+ // - plugins.allow missing or not including this plugin
59
+ // - memory slot pointing elsewhere (e.g. memory-core)
60
+ const memorySlot = api.config?.plugins?.slots?.memory;
61
+ const pluginsAllow = api.config?.plugins?.allow;
62
+ api.registerTool(
63
+ createStatusTool(client, logger, cfg, { memorySlot, pluginsAllow }),
64
+ );
59
65
 
60
66
  // --- Context Engine registration ---
61
67
  // When the user sets `plugins.slots.contextEngine: "nowledge-mem"`,
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { isDefaultApiUrl } from "../config.js";
9
9
 
10
- export function createStatusTool(client, _logger, cfg) {
10
+ export function createStatusTool(client, _logger, cfg, runtimeInfo = {}) {
11
11
  return {
12
12
  name: "nowledge_mem_status",
13
13
  description:
@@ -23,6 +23,60 @@ export function createStatusTool(client, _logger, cfg) {
23
23
  const details = {};
24
24
  const sources = cfg._sources || {};
25
25
 
26
+ // 0a. Plugin trust check — detect missing plugins.allow
27
+ const pluginsAllow = runtimeInfo.pluginsAllow;
28
+ const pluginId = "openclaw-nowledge-mem";
29
+ details.pluginsAllow = !Array.isArray(pluginsAllow)
30
+ ? "(not set)"
31
+ : pluginsAllow.includes(pluginId)
32
+ ? "listed"
33
+ : "set but plugin not included";
34
+
35
+ if (
36
+ Array.isArray(pluginsAllow) &&
37
+ pluginsAllow.length > 0 &&
38
+ !pluginsAllow.includes(pluginId)
39
+ ) {
40
+ lines.push(
41
+ `⚠ plugins.allow is set but does not include "${pluginId}".`,
42
+ );
43
+ lines.push(
44
+ " The plugin will be BLOCKED from loading unless it holds the memory slot or is explicitly enabled.",
45
+ );
46
+ lines.push(
47
+ ` Fix: add "${pluginId}" to plugins.allow in your OpenClaw config.`,
48
+ );
49
+ lines.push("");
50
+ } else if (!Array.isArray(pluginsAllow) || pluginsAllow.length === 0) {
51
+ lines.push(
52
+ "Plugin trust: plugins.allow not set (plugin loads via entries.enabled)",
53
+ );
54
+ lines.push(
55
+ ` Tip: set plugins.allow to ["${pluginId}"] to pin trust explicitly.`,
56
+ );
57
+ lines.push("");
58
+ } else {
59
+ lines.push(`Plugin trust: ${pluginId} (allowlisted)`);
60
+ }
61
+
62
+ // 0b. Memory slot check — detect misconfiguration early
63
+ const memorySlot = runtimeInfo.memorySlot;
64
+ details.memorySlot = memorySlot ?? "(unknown)";
65
+ if (memorySlot && memorySlot !== "openclaw-nowledge-mem") {
66
+ lines.push(
67
+ `⚠ Memory slot: "${memorySlot}" — Nowledge Mem is NOT the active memory plugin.`,
68
+ );
69
+ lines.push(
70
+ " Only memory_search and memory_get will work. Other tools (save, connections, timeline, etc.) are inactive.",
71
+ );
72
+ lines.push(
73
+ ' Fix: run "openclaw plugins install @nowledge/openclaw-nowledge-mem" or set plugins.slots.memory to "openclaw-nowledge-mem" in your config.',
74
+ );
75
+ lines.push("");
76
+ } else if (memorySlot === "openclaw-nowledge-mem") {
77
+ lines.push("Memory slot: openclaw-nowledge-mem (active)");
78
+ }
79
+
26
80
  // 1. Mode + connection target
27
81
  const remote = !isDefaultApiUrl(cfg.apiUrl);
28
82
  const mode = remote ? "remote" : "local";