@nowledge/openclaw-nowledge-mem 0.8.1 → 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,57 @@
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
+
37
+ ## [0.8.3] - 2026-04-08
38
+
39
+ ### Added
40
+
41
+ - Added a release validator and ClawHub publish runbook so package metadata, manifest defaults, and publish-time expectations can be checked before submission.
42
+
43
+ ### Changed
44
+
45
+ - Added the external code-plugin metadata ClawHub expects in `package.json` and aligned the manifest-facing package description for official catalog review.
46
+ - Synced the checked-in `package-lock.json` package version with the actual plugin version so pack and publish surfaces stay coherent.
47
+ - Tightened the release validator so it now catches changelog drift, `openclaw.release` flag drift, unreadable required files, and config-schema contract changes before a ClawHub publish.
48
+ - Updated install guidance to use ClawHub as the primary path while keeping bare package names documented as a resolver fallback.
49
+
50
+ ## [0.8.2] - 2026-04-08
51
+
52
+ ### Changed
53
+
54
+ - **Assertive behavioral guidance replaces conditional phrasing.** The per-turn system prompt injection now uses "Search proactively... Do not wait to be asked" and "Save autonomously... Do not wait to be asked" instead of the previous "Before answering questions about prior work... search." This directly addresses reports of GPT 5.4 and Kimi K2.5 not proactively searching memories. Also trimmed redundant lines (signal examples, output-format description) to keep the injection tight (~60 tokens), restoring the "(not file paths)" hint.
55
+
5
56
  ## [0.8.1] - 2026-04-07
6
57
 
7
58
  ### Fixed
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
@@ -14,11 +14,25 @@ This is not a flat note store. Nowledge Mem links related knowledge into a graph
14
14
  ## Installation
15
15
 
16
16
  ```bash
17
- openclaw plugins install @nowledge/openclaw-nowledge-mem
17
+ openclaw plugins install clawhub:@nowledge/openclaw-nowledge-mem
18
18
  ```
19
19
 
20
20
  OpenClaw's installer writes the install record, enables the plugin, and switches the `memory` slot to `openclaw-nowledge-mem`. Restart OpenClaw after install to load it.
21
21
 
22
+ If you prefer the default resolver path, bare package names also work because OpenClaw resolves external plugins from ClawHub first and then falls back to npm:
23
+
24
+ ```bash
25
+ openclaw plugins install @nowledge/openclaw-nowledge-mem
26
+ ```
27
+
28
+ If you need a pinned local checkout for validation or development, use local linking:
29
+
30
+ ```bash
31
+ git clone https://github.com/nowledge-co/community.git
32
+ cd community/nowledge-mem-openclaw-plugin
33
+ openclaw plugins install --link .
34
+ ```
35
+
22
36
  ### Agent-assisted setup
23
37
 
24
38
  If another AI agent is helping you install this plugin, point it at the bundled [SKILL.md](./SKILL.md).
@@ -351,7 +365,7 @@ The behavioral guidance adjusts when `sessionContext` is enabled. Instead of "se
351
365
 
352
366
  **What happens to conversations I don't explicitly save?**
353
367
 
354
- 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.
355
369
 
356
370
  **Can memories be wrong or outdated?**
357
371
 
@@ -394,15 +408,15 @@ To change settings, use the OpenClaw plugin settings UI. Changes take effect on
394
408
 
395
409
  ### Remote access
396
410
 
397
- 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:
398
412
 
399
- ```json
400
- {
401
- "apiUrl": "https://<your-url>",
402
- "apiKey": "nmem_..."
403
- }
413
+ ```bash
414
+ nmem config client set url https://<your-url>
415
+ nmem config client set api-key nmem_...
404
416
  ```
405
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
+
406
420
  See [Access Mem Anywhere](https://mem.nowledge.co/docs/remote-access).
407
421
 
408
422
  ### Advanced: environment variables
@@ -435,7 +449,7 @@ Credentials (apiUrl, apiKey):
435
449
  openclaw.json (legacy) > OpenClaw dashboard > config.json (shared) > env vars > defaults
436
450
  ```
437
451
 
438
- `~/.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 ...`.
439
453
 
440
454
  Use `nowledge_mem_status` (or `openclaw nowledge-mem status`) to see where each value comes from.
441
455
 
@@ -460,7 +474,7 @@ If you also use linked or workspace copies, review `plugins.load.paths` before r
460
474
  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:
461
475
 
462
476
  ```bash
463
- openclaw plugins install @nowledge/openclaw-nowledge-mem
477
+ openclaw plugins install clawhub:@nowledge/openclaw-nowledge-mem
464
478
  ```
465
479
 
466
480
  Or set the slot manually in `~/.openclaw/openclaw.json`:
@@ -491,6 +505,41 @@ If `openclaw status` shows a CRITICAL warning about `plugins.allow`, this is the
491
505
 
492
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.
493
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
+
494
543
  **Search timeouts with many concurrent agents**
495
544
 
496
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.
@@ -501,7 +550,7 @@ The Nowledge Mem backend is not running. Start it from the desktop app, or run t
501
550
 
502
551
  **Remote mode not connecting**
503
552
 
504
- 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.
505
554
 
506
555
  ## What Makes This Different
507
556
 
package/SKILL.md CHANGED
@@ -102,20 +102,13 @@ Success means the backend responds correctly through `nmem`. Do not skip this st
102
102
  Default install:
103
103
 
104
104
  ```bash
105
- openclaw plugins install @nowledge/openclaw-nowledge-mem
105
+ openclaw plugins install clawhub:@nowledge/openclaw-nowledge-mem
106
106
  ```
107
107
 
108
- If the user is on mainland China or npmjs is unusually slow, prefer a one-shot mirror override instead of changing global npm config:
108
+ Alternatively, if the user is installing from OpenClaw's default registry rather than ClawHub, this also works:
109
109
 
110
110
  ```bash
111
- NPMJS_TIME="$(curl -o /dev/null -sS --connect-timeout 2 --max-time 6 -w '%{time_total}' https://registry.npmjs.org/@nowledge%2fopenclaw-nowledge-mem || echo timeout)"
112
- MIRROR_TIME="$(curl -o /dev/null -sS --connect-timeout 2 --max-time 6 -w '%{time_total}' https://registry.npmmirror.com/@nowledge%2fopenclaw-nowledge-mem || echo timeout)"
113
-
114
- if [ "$MIRROR_TIME" != "timeout" ] && { [ "$NPMJS_TIME" = "timeout" ] || awk "BEGIN { exit !($NPMJS_TIME > 2 && $MIRROR_TIME < $NPMJS_TIME) }"; }; then
115
- NPM_CONFIG_REGISTRY=https://registry.npmmirror.com openclaw plugins install @nowledge/openclaw-nowledge-mem
116
- else
117
- openclaw plugins install @nowledge/openclaw-nowledge-mem
118
- fi
111
+ openclaw plugins install @nowledge/openclaw-nowledge-mem
119
112
  ```
120
113
 
121
114
  Important facts:
@@ -196,7 +189,7 @@ If only `memory_search` and `memory_get` tools are available (other Nowledge Mem
196
189
  }
197
190
  ```
198
191
 
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.
192
+ If missing, reinstall (`openclaw plugins install clawhub:@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
193
 
201
194
  Then verify in chat.
202
195
 
@@ -1,6 +1,8 @@
1
1
  {
2
2
  "id": "openclaw-nowledge-mem",
3
- "version": "0.8.1",
3
+ "name": "Nowledge Mem",
4
+ "description": "Cross-tool knowledge graph memory for OpenClaw with Working Memory, graph search, and session distillation.",
5
+ "version": "0.8.8",
4
6
  "kind": "memory",
5
7
  "skills": [
6
8
  "skills/memory-guide"
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@nowledge/openclaw-nowledge-mem",
3
- "version": "0.8.1",
3
+ "displayName": "Nowledge Mem for OpenClaw",
4
+ "version": "0.8.8",
4
5
  "type": "module",
5
6
  "description": "Nowledge Mem memory plugin for OpenClaw, local-first personal knowledge base",
6
7
  "author": {
@@ -24,16 +25,27 @@
24
25
  "openclaw.plugin.json"
25
26
  ],
26
27
  "openclaw": {
27
- "extensions": ["./src/index.js"]
28
+ "extensions": ["./src/index.js"],
29
+ "install": {
30
+ "npmSpec": "@nowledge/openclaw-nowledge-mem"
31
+ },
32
+ "compat": {
33
+ "pluginApi": ">=2026.4.5"
34
+ },
35
+ "build": {
36
+ "openclawVersion": "2026.4.5"
37
+ },
38
+ "release": {
39
+ "publishToClawHub": true,
40
+ "publishToNpm": true
41
+ }
28
42
  },
29
43
  "main": "src/index.js",
30
44
  "scripts": {
45
+ "validate": "node scripts/validate-plugin.mjs",
31
46
  "lint": "biome check .",
32
47
  "format": "biome format --write ."
33
48
  },
34
- "peerDependencies": {
35
- "openclaw": ">=2026.4.5"
36
- },
37
49
  "devDependencies": {
38
50
  "@biomejs/biome": "^1.9.0"
39
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)) {
@@ -20,11 +20,9 @@ import { ceState } from "../ce-state.js";
20
20
  export const BASE_GUIDANCE = [
21
21
  "<nowledge-mem-guidance>",
22
22
  "You have access to the user's personal knowledge graph (Nowledge Mem).",
23
- "Before answering questions about prior work, decisions, dates, people, preferences, or plans:",
24
- "search with memory_search using natural language queries (not file paths).",
25
- "memory_search returns memories with scores, labels, and relevant past conversation snippets.",
23
+ "Search proactively with memory_search when past context would improve your answer. Do not wait to be asked. Use natural language queries, not file paths.",
26
24
  "When results include a sourceThreadId, use nowledge_mem_thread_fetch for full conversation context.",
27
- "When the conversation produces a decision, preference, plan, or learning, save it with nowledge_mem_save.",
25
+ "Save autonomously with nowledge_mem_save when the conversation produces a decision, preference, plan, procedure, or learning. Do not wait to be asked.",
28
26
  "</nowledge-mem-guidance>",
29
27
  ].join("\n");
30
28
 
@@ -32,10 +30,9 @@ export const SESSION_CONTEXT_GUIDANCE = [
32
30
  "<nowledge-mem-guidance>",
33
31
  "You have access to the user's personal knowledge graph (Nowledge Mem).",
34
32
  "Relevant memories and your Working Memory have already been injected into this prompt.",
35
- "Use memory_search when you need something specific beyond what was auto-recalled (natural language queries, not file paths).",
36
- "memory_search also returns relevant past conversation snippets alongside memories.",
33
+ "Use memory_search for specifics beyond what was auto-recalled, or when the user references prior work not covered above. Natural language queries, not file paths.",
37
34
  "When results include a sourceThreadId, use nowledge_mem_thread_fetch for full conversation context.",
38
- "When the conversation produces a decision, preference, plan, or learning, save it with nowledge_mem_save.",
35
+ "Save autonomously with nowledge_mem_save when the conversation produces a decision, preference, plan, procedure, or learning. Do not wait to be asked.",
39
36
  "</nowledge-mem-guidance>",
40
37
  ].join("\n");
41
38
 
@@ -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)",