@nowledge/openclaw-nowledge-mem 0.8.3 → 0.8.8

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,6 +2,38 @@
2
2
 
3
3
  All notable changes to the Nowledge Mem OpenClaw plugin will be documented in this file.
4
4
 
5
+ ## [0.8.8] - 2026-04-09
6
+
7
+ ### Fixed
8
+
9
+ - **Internal OpenClaw control sender wrappers no longer leak into saved thread content.** Messages wrapped with `Sender (untrusted metadata)` for the internal `openclaw-control-ui` source now persist only the user-visible message body, instead of storing the control metadata block in Mem.
10
+
11
+ ## [0.8.7] - 2026-04-09
12
+
13
+ ### Fixed
14
+
15
+ - **One OpenClaw chat now persists as one Mem thread.** Thread identity now prefers the stable OpenClaw `sessionKey` over transient runtime `sessionId`, so Context Engine `afterTurn` capture and hook-based `agent_end` capture append to the same thread instead of creating parallel copies of the same conversation.
16
+ - **Internal OpenClaw helper sessions no longer pollute Mem.** Temporary sessions like `temp:slug-generator` and internal subagent sessions are now excluded from thread auto-sync, keeping the thread list focused on real user conversations.
17
+ - **Stored thread content is cleaner.** The synthetic `/new` or `/reset` startup prompt is no longer persisted as a user message, and OpenClaw control tags like `[[reply_to_current]]` / `[[reply_to:...]]` / `[[audio_as_voice]]` are stripped before capture.
18
+
19
+ ## [0.8.6] - 2026-04-09
20
+
21
+ ### Fixed
22
+
23
+ - **Thread capture no longer depends entirely on the Context Engine path.** When `plugins.slots.contextEngine` points to `nowledge-mem`, behavioral and recall prompt injection still defer to the Context Engine, but capture hooks now remain active as a safety net. Thread append/create is already tail-synced and idempotent, so this backstop avoids missed syncs on sessions where the Context Engine lifecycle does not fire as expected without reintroducing duplicate threads.
24
+
25
+ ### Changed
26
+
27
+ - **Troubleshooting now distinguishes sandbox mode from capture mode.** A `session_status` label like `direct/non-main` reflects OpenClaw sandbox configuration, not a cron or non-interactive session. The OpenClaw main session key `agent:main:main` remains a normal interactive capture target.
28
+ - **Status now reports Context Engine activity precisely.** `nowledge_mem_status` only reports Context Engine capture as active when registration actually succeeded in the current runtime. If the slot points to `nowledge-mem` but the runtime falls back to hooks-only mode, the status output now says so directly.
29
+
30
+ ## [0.8.5] - 2026-04-09
31
+
32
+ ### Changed
33
+
34
+ - **Reduced loader coupling in the published package.** `openclaw.install.minHostVersion` and `peerDependencies.openclaw` have been removed. The plugin already uses runtime feature detection and graceful fallback, so those package-level host constraints were unnecessary. `openclaw.compat.pluginApi` and `openclaw.build.openclawVersion` remain for ClawHub validation.
35
+ - **`nowledge_mem_status` now explains capture routing and transport split.** It reports whether thread capture is running through lifecycle hooks or Context Engine `afterTurn`, and makes the transport model explicit: memory tools use the `nmem` CLI, while thread auto-sync uses the Mem HTTP API. This closes the common debugging gap where search works but thread sync does not.
36
+
5
37
  ## [0.8.3] - 2026-04-08
6
38
 
7
39
  ### Added
package/CLAUDE.md CHANGED
@@ -94,7 +94,7 @@ A shared `ceState.active` flag (in `ce-state.js`) coordinates the two paths:
94
94
  |-----------|-----------|---------------------|
95
95
  | Behavioral guidance | `assemble()` → `systemPromptAddition` | `before_prompt_build` → `appendSystemContext` |
96
96
  | Memory recall | `assemble()` → `systemPromptAddition` | `before_prompt_build` → `appendSystemContext` |
97
- | Thread capture | `afterTurn()` (every turn) | `agent_end` / `after_compaction` / `before_reset` |
97
+ | Thread capture | `afterTurn()` (every turn) + hook fallback | `agent_end` / `after_compaction` / `before_reset` |
98
98
  | Triage + distill | `afterTurn()` | `agent_end` only |
99
99
  | Compaction | `compact()` with memory-aware instructions | None (OpenClaw legacy) |
100
100
  | Subagent context | `prepareSubagentSpawn()` + `onSubagentEnded()` | None |
package/README.md CHANGED
@@ -365,7 +365,7 @@ The behavioral guidance adjusts when `sessionContext` is enabled. Instead of "se
365
365
 
366
366
  **What happens to conversations I don't explicitly save?**
367
367
 
368
- With `sessionDigest` enabled (the default), every conversation is saved as a searchable thread. You can find it later with `nowledge_mem_thread_search`. On top of that, a lightweight LLM triage checks if the conversation contained decisions, insights, or preferences worth keeping as structured memories. If yes, they're extracted with proper types, labels, and temporal context. If the conversation was routine ("fix this typo"), nothing extra is saved.
368
+ With `sessionDigest` enabled (the default), every conversation is saved as a searchable thread. One OpenClaw chat becomes one Mem thread: capture uses the stable OpenClaw `sessionKey`, not transient runtime ids, so Context Engine capture and hook-based capture append to the same conversation. Internal helper sessions like `temp:*` and subagent runs are excluded, so your recent threads stay focused on real chats. On top of thread capture, a lightweight LLM triage checks if the conversation contained decisions, insights, or preferences worth keeping as structured memories. If yes, they're extracted with proper types, labels, and temporal context. If the conversation was routine ("fix this typo"), nothing extra is saved.
369
369
 
370
370
  **Can memories be wrong or outdated?**
371
371
 
@@ -408,15 +408,15 @@ To change settings, use the OpenClaw plugin settings UI. Changes take effect on
408
408
 
409
409
  ### Remote access
410
410
 
411
- Create `~/.nowledge-mem/config.json` with your credentials. This file is shared by all Nowledge Mem integrations (nmem CLI, Bub, Claude Code, etc.) so one file connects everything:
411
+ Configure this machine once:
412
412
 
413
- ```json
414
- {
415
- "apiUrl": "https://<your-url>",
416
- "apiKey": "nmem_..."
417
- }
413
+ ```bash
414
+ nmem config client set url https://<your-url>
415
+ nmem config client set api-key nmem_...
418
416
  ```
419
417
 
418
+ That writes the shared local client config used by `nmem`, OpenClaw, Bub, Claude Code, and other integrations on this machine. You can still set **Server URL** and **API key** in the OpenClaw dashboard if you want plugin-specific overrides.
419
+
420
420
  See [Access Mem Anywhere](https://mem.nowledge.co/docs/remote-access).
421
421
 
422
422
  ### Advanced: environment variables
@@ -449,7 +449,7 @@ Credentials (apiUrl, apiKey):
449
449
  openclaw.json (legacy) > OpenClaw dashboard > config.json (shared) > env vars > defaults
450
450
  ```
451
451
 
452
- `~/.nowledge-mem/openclaw.json` is still honored for backward compatibility but is no longer the recommended path. New users should configure plugin-specific settings via the OpenClaw dashboard and shared credentials via `~/.nowledge-mem/config.json`.
452
+ `~/.nowledge-mem/openclaw.json` is still honored for backward compatibility but is no longer the recommended path. New users should configure plugin-specific settings via the OpenClaw dashboard and shared credentials via `nmem config client ...`.
453
453
 
454
454
  Use `nowledge_mem_status` (or `openclaw nowledge-mem status`) to see where each value comes from.
455
455
 
@@ -505,6 +505,41 @@ If `openclaw status` shows a CRITICAL warning about `plugins.allow`, this is the
505
505
 
506
506
  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.
507
507
 
508
+ **Memory tools still work, but thread auto-sync does not**
509
+
510
+ This is usually a capture-path issue, not a search/save issue:
511
+
512
+ - memory tools use the local `nmem` CLI
513
+ - thread auto-sync uses the Mem HTTP API
514
+ - cron / isolated automation sessions are intentionally excluded from capture in `0.8.x`
515
+
516
+ Run `nowledge_mem_status` in a conversation and check, in this order:
517
+
518
+ - the plugin is loaded
519
+ - `sessionDigest` is still `true`
520
+ - the backend is reachable
521
+ - which capture path is active: hook events, or Context Engine `afterTurn` with hook fallback
522
+ - whether you changed plugin settings recently without restarting OpenClaw yet
523
+
524
+ If your config enables `plugins.slots.contextEngine: "nowledge-mem"`:
525
+
526
+ - on `0.8.6+`, Context Engine capture stays active and hooks remain enabled as a safety net
527
+ - on `0.8.5` and earlier, temporarily removing the `contextEngine` slot is a valid isolation step if thread sync stops while tools still work
528
+
529
+ Remember: OpenClaw applies plugin setting changes after restart. If you turned `sessionDigest` off earlier but had not restarted yet, thread sync could appear to keep working until the next restart, then stop.
530
+
531
+ What healthy OpenClaw thread sync looks like:
532
+
533
+ - one visible chat becomes one Mem thread
534
+ - helper sessions like `temp:slug-generator` do not appear
535
+ - the synthetic `/new` / `/reset` startup prompt is not stored as the first user message
536
+
537
+ To inspect the recent synced threads directly, run:
538
+
539
+ ```bash
540
+ nmem t list --source openclaw -n 20
541
+ ```
542
+
508
543
  **Search timeouts with many concurrent agents**
509
544
 
510
545
  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.
@@ -515,7 +550,7 @@ The Nowledge Mem backend is not running. Start it from the desktop app, or run t
515
550
 
516
551
  **Remote mode not connecting**
517
552
 
518
- Check `~/.nowledge-mem/config.json` credentials. Run `nmem --json --api-url "$URL" status` to verify the server is reachable.
553
+ Run `nmem config client show` and confirm the effective URL and API key state. Then run `nmem --json --api-url "$URL" status` to verify the server is reachable.
519
554
 
520
555
  ## What Makes This Different
521
556
 
@@ -2,7 +2,7 @@
2
2
  "id": "openclaw-nowledge-mem",
3
3
  "name": "Nowledge Mem",
4
4
  "description": "Cross-tool knowledge graph memory for OpenClaw with Working Memory, graph search, and session distillation.",
5
- "version": "0.8.3",
5
+ "version": "0.8.8",
6
6
  "kind": "memory",
7
7
  "skills": [
8
8
  "skills/memory-guide"
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nowledge/openclaw-nowledge-mem",
3
3
  "displayName": "Nowledge Mem for OpenClaw",
4
- "version": "0.8.3",
4
+ "version": "0.8.8",
5
5
  "type": "module",
6
6
  "description": "Nowledge Mem memory plugin for OpenClaw, local-first personal knowledge base",
7
7
  "author": {
@@ -27,8 +27,7 @@
27
27
  "openclaw": {
28
28
  "extensions": ["./src/index.js"],
29
29
  "install": {
30
- "npmSpec": "@nowledge/openclaw-nowledge-mem",
31
- "minHostVersion": ">=2026.4.5"
30
+ "npmSpec": "@nowledge/openclaw-nowledge-mem"
32
31
  },
33
32
  "compat": {
34
33
  "pluginApi": ">=2026.4.5"
@@ -47,9 +46,6 @@
47
46
  "lint": "biome check .",
48
47
  "format": "biome format --write ."
49
48
  },
50
- "peerDependencies": {
51
- "openclaw": ">=2026.4.5"
52
- },
53
49
  "devDependencies": {
54
50
  "@biomejs/biome": "^1.9.0"
55
51
  }
package/src/ce-state.js CHANGED
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Shared mutable state flag for context engine activation.
3
3
  *
4
- * When the CE is bootstrapped, `active` is set to true. Event hooks
5
- * check this flag to avoid duplicate work — the CE's lifecycle methods
6
- * (assemble, afterTurn, etc.) replace hook behavior when active.
4
+ * When the CE is bootstrapped, `active` is set to true. Prompt-building hooks
5
+ * use this to avoid duplicate context injection. Capture hooks stay enabled as
6
+ * a safety net because thread sync is already tail-synced and idempotent.
7
7
  */
8
8
  export const ceState = { active: false };
@@ -2,8 +2,8 @@
2
2
  * Nowledge Mem Context Engine for OpenClaw.
3
3
  *
4
4
  * Registers alongside the memory slot (kind: "memory"). When users activate
5
- * this CE via `plugins.slots.contextEngine: "nowledge-mem"`, it replaces the
6
- * hook-based approach with the richer CE lifecycle:
5
+ * this CE via `plugins.slots.contextEngine: "nowledge-mem"`, it takes over
6
+ * prompt assembly and adds richer lifecycle behavior:
7
7
  *
8
8
  * assemble() — behavioral guidance + recalled memories via systemPromptAddition
9
9
  * afterTurn() — continuous thread capture + triage/distillation (every turn)
@@ -12,7 +12,9 @@
12
12
  * bootstrap() — pre-warm Working Memory for first assemble
13
13
  *
14
14
  * When not activated, the existing hooks (behavioral, recall, capture) work
15
- * as before — full backward compatibility.
15
+ * as before. When activated, behavioral + recall hooks defer to the CE to
16
+ * avoid duplicate prompt context, while capture hooks remain as a safe
17
+ * fallback if a CE post-turn lifecycle is missed.
16
18
  *
17
19
  * Design:
18
20
  * - ownsCompaction: false — we enhance compaction instructions, not the algorithm
@@ -25,6 +27,7 @@ import { BASE_GUIDANCE, SESSION_CONTEXT_GUIDANCE } from "./hooks/behavioral.js";
25
27
  import {
26
28
  appendOrCreateThread,
27
29
  hasSkipMarker,
30
+ isInternalCaptureSessionKey,
28
31
  isCronCaptureSessionKey,
29
32
  matchesExcludePattern,
30
33
  triageAndDistill,
@@ -356,6 +359,10 @@ export function createNowledgeMemContextEngineFactory(client, cfg, logger) {
356
359
  logger.debug?.(`ce: skipped cron session ${normalizedKey}`);
357
360
  return;
358
361
  }
362
+ if (isInternalCaptureSessionKey(normalizedKey)) {
363
+ logger.debug?.(`ce: skipped internal session ${normalizedKey}`);
364
+ return;
365
+ }
359
366
 
360
367
  // Capture exclusion: pattern-based and marker-based filters
361
368
  if (matchesExcludePattern(normalizedKey, cfg.captureExclude)) {
@@ -1,12 +1,17 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { readFile } from "node:fs/promises";
3
- import { isCronSessionKey } from "openclaw/plugin-sdk/routing";
4
- import { ceState } from "../ce-state.js";
3
+ import { isCronSessionKey, isSubagentSessionKey } from "openclaw/plugin-sdk/routing";
5
4
 
6
5
  export const DEFAULT_MAX_MESSAGE_CHARS = 800;
7
6
  export const MAX_DISTILL_MESSAGE_CHARS = 2000;
8
7
  export const MAX_CONVERSATION_CHARS = 30_000;
9
8
  export const MIN_MESSAGES_FOR_DISTILL = 4;
9
+ const SESSION_RESET_PROMPT_PREFIX =
10
+ "A new session was started via /new or /reset.";
11
+ const OPENCLAW_DIRECTIVE_TAG_RE =
12
+ /\s*\[\[\s*(?:audio_as_voice|reply_to_current|reply_to\s*:\s*[^\]\n]+)\s*\]\]\s*/giu;
13
+ const OPENCLAW_INTERNAL_SENDER_BLOCK_RE =
14
+ /^Sender \(untrusted metadata\):\s*```json\s*([\s\S]*?)\s*```\s*([\s\S]*)$/iu;
10
15
 
11
16
  // Per-thread triage cooldown: prevents burst triage/distillation from heartbeat.
12
17
  // Maps threadId -> timestamp (ms) of last successful triage.
@@ -113,6 +118,52 @@ export function hasSkipMarker(messages, marker) {
113
118
  });
114
119
  }
115
120
 
121
+ /**
122
+ * Internal helper / system sessions should not become user-facing Mem threads.
123
+ *
124
+ * - temp:* covers OpenClaw helper sessions such as slug generation
125
+ * - subagent sessions are execution internals, not first-class user chats
126
+ */
127
+ export function isInternalCaptureSessionKey(sessionKey) {
128
+ const raw = String(sessionKey || "")
129
+ .trim()
130
+ .toLowerCase();
131
+ if (!raw) return false;
132
+ if (raw.startsWith("temp:")) return true;
133
+ return isSubagentSessionKey(raw);
134
+ }
135
+
136
+ function stripOpenClawDirectiveTags(text) {
137
+ return String(text || "")
138
+ .replace(OPENCLAW_DIRECTIVE_TAG_RE, " ")
139
+ .replace(/[ \t]+\n/g, "\n")
140
+ .replace(/\n{3,}/g, "\n\n")
141
+ .trim();
142
+ }
143
+
144
+ function stripInternalSenderMetadata(text) {
145
+ const raw = String(text || "").trim();
146
+ if (!raw.startsWith("Sender (untrusted metadata):")) return raw;
147
+ const match = raw.match(OPENCLAW_INTERNAL_SENDER_BLOCK_RE);
148
+ if (!match) return raw;
149
+ const [, jsonBlock, remainder] = match;
150
+ try {
151
+ const parsed = JSON.parse(jsonBlock);
152
+ const label = String(parsed?.label || "")
153
+ .trim()
154
+ .toLowerCase();
155
+ const id = String(parsed?.id || "")
156
+ .trim()
157
+ .toLowerCase();
158
+ if (label === "openclaw-control-ui" || id === "openclaw-control-ui") {
159
+ return String(remainder || "").trim();
160
+ }
161
+ } catch {
162
+ return raw;
163
+ }
164
+ return raw;
165
+ }
166
+
116
167
  // ---------------------------------------------------------------------------
117
168
  // Message normalization utilities
118
169
  // ---------------------------------------------------------------------------
@@ -151,7 +202,13 @@ export function normalizeRoleMessage(
151
202
  raw.message && typeof raw.message === "object" ? raw.message : raw;
152
203
  const role = typeof msg.role === "string" ? msg.role : "";
153
204
  if (role !== "user" && role !== "assistant") return null;
154
- const text = extractText(msg.content);
205
+ const extractedText = extractText(msg.content);
206
+ if (!extractedText) return null;
207
+ const cleanedText = stripInternalSenderMetadata(extractedText);
208
+ if (role === "user" && cleanedText.startsWith(SESSION_RESET_PROMPT_PREFIX)) {
209
+ return null;
210
+ }
211
+ const text = stripOpenClawDirectiveTags(cleanedText);
155
212
  if (!text) return null;
156
213
  if (role === "user" && text.startsWith("/")) return null;
157
214
 
@@ -184,10 +241,9 @@ export function normalizeRoleMessage(
184
241
  };
185
242
  }
186
243
 
187
- export function buildThreadTitle(ctx, reason) {
244
+ export function buildThreadTitle(ctx) {
188
245
  const session = ctx?.sessionKey || ctx?.sessionId || "session";
189
- const reasonSuffix = reason ? ` (${reason})` : "";
190
- return `OpenClaw ${session}${reasonSuffix}`;
246
+ return `OpenClaw ${session}`;
191
247
  }
192
248
 
193
249
  function sanitizeIdPart(input, max = 48) {
@@ -201,8 +257,8 @@ function sanitizeIdPart(input, max = 48) {
201
257
 
202
258
  export function buildStableThreadId(event, ctx) {
203
259
  const base =
204
- String(ctx?.sessionId || "").trim() ||
205
260
  String(ctx?.sessionKey || "").trim() ||
261
+ String(ctx?.sessionId || "").trim() ||
206
262
  String(event?.sessionFile || "").trim() ||
207
263
  "session";
208
264
  const slug = sanitizeIdPart(base);
@@ -282,7 +338,7 @@ export async function appendOrCreateThread({
282
338
  const threadId = buildStableThreadId(event, ctx);
283
339
  const sessionKey = String(ctx?.sessionKey || ctx?.sessionId || "session");
284
340
  const sessionId = String(ctx?.sessionId || "").trim();
285
- const title = buildThreadTitle(ctx, reason);
341
+ const title = buildThreadTitle(ctx);
286
342
  const normalized = rawMessages
287
343
  .map((message) => normalizeRoleMessage(message, maxMessageChars))
288
344
  .filter(Boolean);
@@ -457,7 +513,7 @@ export async function triageAndDistill({
457
513
 
458
514
  const distillResult = await client.distillThread({
459
515
  threadId: captureResult.threadId,
460
- title: buildThreadTitle(ctx, "distilled"),
516
+ title: buildThreadTitle(ctx),
461
517
  content: conversationText,
462
518
  });
463
519
 
@@ -477,8 +533,9 @@ export async function triageAndDistill({
477
533
  /**
478
534
  * Capture thread + LLM-based distillation after a successful agent run.
479
535
  *
480
- * When the context engine is active, this hook is a no-op — afterTurn
481
- * handles capture and distillation through the CE lifecycle.
536
+ * Even when the context engine is active, this hook remains as a safety net.
537
+ * Thread append/create is already idempotent, so the hook path can safely
538
+ * backstop CE afterTurn without duplicating stored messages.
482
539
  *
483
540
  * Heartbeat sessions (ctx.trigger === "heartbeat") are skipped — they
484
541
  * produce repetitive status pings that aren't worth preserving.
@@ -487,7 +544,6 @@ export async function triageAndDistill({
487
544
  */
488
545
  export function buildAgentEndCaptureHandler(client, cfg, logger) {
489
546
  return async (event, ctx) => {
490
- if (ceState.active) return;
491
547
  if (!event?.success) return;
492
548
  if (ctx?.trigger === "heartbeat") return;
493
549
 
@@ -496,6 +552,10 @@ export function buildAgentEndCaptureHandler(client, cfg, logger) {
496
552
  logger.debug?.(`capture: skipped cron session ${sessionKey}`);
497
553
  return;
498
554
  }
555
+ if (isInternalCaptureSessionKey(sessionKey)) {
556
+ logger.debug?.(`capture: skipped internal session ${sessionKey}`);
557
+ return;
558
+ }
499
559
 
500
560
  // Pattern-based exclusion (e.g. subagent sessions, custom jobs)
501
561
  if (matchesExcludePattern(sessionKey, cfg.captureExclude)) {
@@ -530,14 +590,13 @@ export function buildAgentEndCaptureHandler(client, cfg, logger) {
530
590
  * Capture thread messages before reset or after compaction.
531
591
  * Thread-only (no distillation) — these are lifecycle checkpoints.
532
592
  *
533
- * When the context engine is active, this hook is a no-op — afterTurn
534
- * handles capture through the CE lifecycle.
593
+ * This hook also stays enabled when the context engine is active.
594
+ * The shared tail-sync dedup keeps the fallback path cheap and safe.
535
595
  *
536
596
  * Heartbeat sessions are skipped (same rationale as agent_end).
537
597
  */
538
598
  export function buildBeforeResetCaptureHandler(client, cfg, logger) {
539
599
  return async (event, ctx) => {
540
- if (ceState.active) return;
541
600
  if (ctx?.trigger === "heartbeat") return;
542
601
 
543
602
  const sessionKey = String(ctx?.sessionKey || ctx?.sessionId || "");
@@ -545,6 +604,10 @@ export function buildBeforeResetCaptureHandler(client, cfg, logger) {
545
604
  logger.debug?.(`capture: skipped cron session ${sessionKey}`);
546
605
  return;
547
606
  }
607
+ if (isInternalCaptureSessionKey(sessionKey)) {
608
+ logger.debug?.(`capture: skipped internal session ${sessionKey}`);
609
+ return;
610
+ }
548
611
 
549
612
  if (matchesExcludePattern(sessionKey, cfg.captureExclude)) {
550
613
  logger.debug?.(`capture: skipped excluded session ${sessionKey}`);
package/src/index.js CHANGED
@@ -57,30 +57,43 @@ export default {
57
57
 
58
58
  // Diagnostics — pass runtime config so the status tool can detect:
59
59
  // - plugins.allow missing or not including this plugin
60
- // - memory slot pointing elsewhere (e.g. memory-core)
60
+ // - memory/context-engine slot routing for capture and recall
61
61
  const memorySlot = api.config?.plugins?.slots?.memory;
62
+ const contextEngineSlot = api.config?.plugins?.slots?.contextEngine;
62
63
  const pluginsAllow = api.config?.plugins?.allow;
63
- api.registerTool(
64
- createStatusTool(client, logger, cfg, { memorySlot, pluginsAllow }),
65
- );
64
+ let contextEngineRegistered = false;
65
+ let contextEngineRegistrationError = null;
66
66
 
67
67
  // --- Context Engine registration ---
68
68
  // When the user sets `plugins.slots.contextEngine: "nowledge-mem"`,
69
- // this CE takes over from the hooks below (assemble replaces behavioral
70
- // + recall; afterTurn replaces agent_end + capture hooks). When the CE
71
- // slot points elsewhere, hooks continue working as before.
69
+ // this CE takes over prompt assembly from the hooks below (assemble
70
+ // replaces behavioral + recall prompt injection). Capture hooks remain
71
+ // enabled as a reliability backstop because thread sync is idempotent.
72
+ // When the CE slot points elsewhere, hooks continue working as before.
72
73
  try {
73
74
  api.registerContextEngine(
74
75
  "nowledge-mem",
75
76
  createNowledgeMemContextEngineFactory(client, cfg, logger),
76
77
  );
78
+ contextEngineRegistered = true;
77
79
  } catch (err) {
78
80
  // OpenClaw < CE support — degrade gracefully to hooks-only mode
81
+ contextEngineRegistrationError = String(err);
79
82
  logger.debug?.(
80
83
  `nowledge-mem: context engine registration unavailable (${err}), using hooks`,
81
84
  );
82
85
  }
83
86
 
87
+ api.registerTool(
88
+ createStatusTool(client, logger, cfg, {
89
+ memorySlot,
90
+ contextEngineSlot,
91
+ pluginsAllow,
92
+ contextEngineRegistered,
93
+ contextEngineRegistrationError,
94
+ }),
95
+ );
96
+
84
97
  // --- Corpus Supplement (when memory-core is the memory slot) ---
85
98
  // Makes Nowledge Mem's knowledge graph searchable through memory-core's
86
99
  // recall pipeline and dreaming promotion. Memories participate in
@@ -102,9 +115,10 @@ export default {
102
115
  }
103
116
  }
104
117
 
105
- // --- Hooks (fallback when CE is not active) ---
106
- // Each hook checks ceState.active and returns early when the CE handles
107
- // the same lifecycle through assemble/afterTurn.
118
+ // --- Hooks ---
119
+ // Behavioral + recall hooks defer to the active CE to avoid duplicate
120
+ // prompt injection. Capture hooks remain enabled as a backstop because
121
+ // thread append/create is tail-synced and idempotent.
108
122
 
109
123
  // Always-on: behavioral guidance so the agent proactively saves and searches.
110
124
  // Fires every turn via before_prompt_build — ~50 tokens, negligible cost.
@@ -61,7 +61,20 @@ export function createStatusTool(client, _logger, cfg, runtimeInfo = {}) {
61
61
 
62
62
  // 0b. Memory slot check — show current mode and options
63
63
  const memorySlot = runtimeInfo.memorySlot;
64
+ const contextEngineSlot = runtimeInfo.contextEngineSlot;
65
+ const contextEngineRegistered = runtimeInfo.contextEngineRegistered === true;
66
+ const contextEngineRegistrationError =
67
+ typeof runtimeInfo.contextEngineRegistrationError === "string" &&
68
+ runtimeInfo.contextEngineRegistrationError.trim()
69
+ ? runtimeInfo.contextEngineRegistrationError.trim()
70
+ : null;
64
71
  details.memorySlot = memorySlot ?? "(unknown)";
72
+ const ceSlot =
73
+ contextEngineSlot && String(contextEngineSlot).trim()
74
+ ? String(contextEngineSlot).trim()
75
+ : "legacy";
76
+ details.contextEngineSlot = ceSlot;
77
+ details.contextEngineRegistered = contextEngineRegistered;
65
78
  if (memorySlot && memorySlot !== "openclaw-nowledge-mem") {
66
79
  const corpusOn = cfg.corpusSupplement === true;
67
80
  if (corpusOn) {
@@ -90,6 +103,39 @@ export function createStatusTool(client, _logger, cfg, runtimeInfo = {}) {
90
103
  lines.push("Memory slot: openclaw-nowledge-mem (active)");
91
104
  }
92
105
 
106
+ // 0c. Capture routing — hooks vs context engine
107
+ const ceActive = ceSlot === "nowledge-mem" && contextEngineRegistered;
108
+ const captureMode = !cfg.sessionDigest
109
+ ? "disabled"
110
+ : ceActive ? "context-engine+hooks" : "hooks";
111
+ details.captureMode = captureMode;
112
+ if (ceActive) {
113
+ lines.push("Context Engine slot: nowledge-mem (active)");
114
+ lines.push(
115
+ " Thread capture runs through Context Engine afterTurn with hook fallback (agent_end, after_compaction, before_reset).",
116
+ );
117
+ } else if (ceSlot === "nowledge-mem") {
118
+ lines.push("Context Engine slot: nowledge-mem (configured, fallback to hooks)");
119
+ lines.push(
120
+ " The slot points to Nowledge Mem, but Context Engine registration is not active in this runtime. Thread capture is using hooks only.",
121
+ );
122
+ if (contextEngineRegistrationError) {
123
+ lines.push(` CE registration detail: ${contextEngineRegistrationError}`);
124
+ }
125
+ } else {
126
+ lines.push(
127
+ `Context Engine slot: ${ceSlot === "legacy" ? "legacy (default)" : `"${ceSlot}"`}`,
128
+ );
129
+ if (cfg.sessionDigest) {
130
+ lines.push(
131
+ " Thread capture runs through hooks: agent_end, after_compaction, before_reset.",
132
+ );
133
+ }
134
+ }
135
+ if (!cfg.sessionDigest) {
136
+ lines.push("Thread capture: disabled (sessionDigest=false)");
137
+ }
138
+
93
139
  // 1. Mode + connection target
94
140
  const remote = !isDefaultApiUrl(cfg.apiUrl);
95
141
  const mode = remote ? "remote" : "local";
@@ -101,6 +147,8 @@ export function createStatusTool(client, _logger, cfg, runtimeInfo = {}) {
101
147
  `Mode: ${remote ? `Remote (${cfg.apiUrl})` : "Local (127.0.0.1:14242)"}`,
102
148
  );
103
149
  lines.push(`API key: ${cfg.apiKey ? "set" : "not set"}`);
150
+ lines.push("Memory tools: nmem CLI");
151
+ lines.push("Thread sync: Mem HTTP API");
104
152
 
105
153
  // 2. CLI resolution
106
154
  try {
@@ -156,6 +204,9 @@ export function createStatusTool(client, _logger, cfg, runtimeInfo = {}) {
156
204
  } else {
157
205
  lines.push(" Ensure the Nowledge Mem desktop app is running.");
158
206
  }
207
+ lines.push(
208
+ " Memory tools may still work through the CLI, but thread auto-sync will fail while the HTTP API is unreachable.",
209
+ );
159
210
  }
160
211
 
161
212
  // 4. Effective config with sources
@@ -179,6 +230,9 @@ export function createStatusTool(client, _logger, cfg, runtimeInfo = {}) {
179
230
  lines.push(
180
231
  ` maxThreadMessageChars: ${cfg.maxThreadMessageChars} (${sources.maxThreadMessageChars || "?"})`,
181
232
  );
233
+ lines.push(
234
+ ` corpusSupplement: ${cfg.corpusSupplement} (${sources.corpusSupplement || "?"})`,
235
+ );
182
236
 
183
237
  details.config = {
184
238
  sessionContext: {
@@ -205,6 +259,10 @@ export function createStatusTool(client, _logger, cfg, runtimeInfo = {}) {
205
259
  value: cfg.maxThreadMessageChars,
206
260
  source: sources.maxThreadMessageChars,
207
261
  },
262
+ corpusSupplement: {
263
+ value: cfg.corpusSupplement,
264
+ source: sources.corpusSupplement,
265
+ },
208
266
  apiUrl: { value: cfg.apiUrl || "(local)", source: sources.apiUrl },
209
267
  apiKey: {
210
268
  value: cfg.apiKey ? "(set)" : "(not set)",