@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 +51 -0
- package/CLAUDE.md +1 -1
- package/README.md +60 -11
- package/SKILL.md +4 -11
- package/openclaw.plugin.json +3 -1
- package/package.json +17 -5
- package/src/ce-state.js +3 -3
- package/src/context-engine.js +10 -3
- package/src/hooks/behavioral.js +4 -7
- 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,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
|
|
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.
|
|
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
|
-
|
|
411
|
+
Configure this machine once:
|
|
398
412
|
|
|
399
|
-
```
|
|
400
|
-
|
|
401
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
105
|
+
openclaw plugins install clawhub:@nowledge/openclaw-nowledge-mem
|
|
106
106
|
```
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
Alternatively, if the user is installing from OpenClaw's default registry rather than ClawHub, this also works:
|
|
109
109
|
|
|
110
110
|
```bash
|
|
111
|
-
|
|
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
|
|
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
|
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "openclaw-nowledge-mem",
|
|
3
|
-
"
|
|
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
|
-
"
|
|
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.
|
|
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/behavioral.js
CHANGED
|
@@ -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
|
-
"
|
|
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
|
-
"
|
|
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
|
|
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
|
-
"
|
|
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
|
|
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)",
|