@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 +32 -0
- package/CLAUDE.md +1 -1
- package/README.md +44 -9
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -6
- package/src/ce-state.js +3 -3
- package/src/context-engine.js +10 -3
- package/src/hooks/capture.js +78 -15
- package/src/index.js +24 -10
- package/src/tools/status.js +58 -0
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.
|
|
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
|
-
|
|
411
|
+
Configure this machine once:
|
|
412
412
|
|
|
413
|
-
```
|
|
414
|
-
|
|
415
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
package/openclaw.plugin.json
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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 };
|
package/src/context-engine.js
CHANGED
|
@@ -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
|
|
6
|
-
*
|
|
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
|
|
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)) {
|
package/src/hooks/capture.js
CHANGED
|
@@ -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
|
|
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
|
|
244
|
+
export function buildThreadTitle(ctx) {
|
|
188
245
|
const session = ctx?.sessionKey || ctx?.sessionId || "session";
|
|
189
|
-
|
|
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
|
|
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
|
|
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
|
-
*
|
|
481
|
-
*
|
|
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
|
-
*
|
|
534
|
-
*
|
|
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
|
|
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
|
-
|
|
64
|
-
|
|
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
|
|
70
|
-
//
|
|
71
|
-
//
|
|
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
|
|
106
|
-
//
|
|
107
|
-
//
|
|
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.
|
package/src/tools/status.js
CHANGED
|
@@ -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)",
|