@nowledge/openclaw-nowledge-mem 0.8.3 → 0.8.12
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 +82 -0
- package/CLAUDE.md +5 -1
- package/README.md +71 -9
- package/SKILL.md +1 -0
- package/openclaw.plugin.json +19 -1
- package/package.json +3 -6
- package/src/ce-state.js +3 -3
- package/src/client.js +56 -10
- package/src/config.js +70 -12
- package/src/context-engine.js +10 -3
- package/src/hooks/capture.js +87 -27
- package/src/hooks/thread-identity.js +129 -0
- package/src/index.js +29 -10
- package/src/spawn-env.js +16 -2
- package/src/tools/status.js +68 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,88 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the Nowledge Mem OpenClaw plugin will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## Unreleased
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- OpenClaw now preserves an explicit empty `space` setting as a real choice to stay on `Default`, instead of silently falling through to ambient `NMEM_SPACE`.
|
|
10
|
+
- CLI-backed OpenClaw operations now honor that same explicit Default-space choice. Earlier builds could still inherit `NMEM_SPACE` in child `nmem` processes, which split CLI-backed reads and writes away from HTTP-backed thread sync.
|
|
11
|
+
- Fallback HTTP requests now carry the ambient lane through one shared path, so direct API calls stay aligned with CLI-backed memory operations.
|
|
12
|
+
- `nowledge_mem_status` now reports the configured Context Engine slot correctly instead of reading from an unset field.
|
|
13
|
+
- `nowledge_mem_status` now reports `legacy` when no Context Engine slot is configured, instead of surfacing `undefined` in default OpenClaw setups.
|
|
14
|
+
- Legacy `~/.nowledge-mem/openclaw.json` now gets the same strict key validation as dashboard config, so typoed or unsupported keys fail loudly instead of silently altering runtime behavior.
|
|
15
|
+
- Thread capture no longer fails at runtime after the thread-identity refactor. The crypto import used by external IDs and append idempotency keys is restored, so `sessionDigest` capture keeps creating and appending threads normally.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Lower-priority `spaceTemplate` values are now evaluated lazily. A broken template no longer overrides a higher-priority concrete `space` value.
|
|
20
|
+
- Thread-identity tests now reset conversation-root state between cases, so the suite matches real runtime boundaries more closely.
|
|
21
|
+
|
|
22
|
+
## [0.8.12] - 2026-04-10
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- OpenClaw's space settings and setup guidance now lead with profile-owned config (`space`, `spaceTemplate`) and keep `NMEM_SPACE` as a fallback for launcher-driven sessions without a richer config surface.
|
|
27
|
+
|
|
28
|
+
## [0.8.11] - 2026-04-10
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- OpenClaw profiles can now own one ambient space directly through plugin config `space` or `spaceTemplate`, instead of relying only on the process-wide `NMEM_SPACE` environment variable.
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
|
|
36
|
+
- `nowledge_mem_status` now reports the effective ambient space and where it came from.
|
|
37
|
+
|
|
38
|
+
## [0.8.10] - 2026-04-10
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
|
|
42
|
+
- Clarified OpenClaw's ambient space model. `NMEM_SPACE` now remains the single session lane across CLI-backed memory calls and API-backed thread/feed fallbacks, while shared spaces, default retrieval, and agent guidance stay in Mem's shared `/spaces` profile.
|
|
43
|
+
|
|
44
|
+
## [0.8.9] - 2026-04-10
|
|
45
|
+
|
|
46
|
+
### Fixed
|
|
47
|
+
|
|
48
|
+
- **`/new` once again starts a fresh Mem thread.** Thread identity now follows OpenClaw's session lifecycle instead of collapsing everything under the channel-level `sessionKey`. Explicit `/new`, `/reset`, idle rollovers, and daily rollovers create a new Mem thread as users expect.
|
|
49
|
+
- **Compaction still stays inside the same Mem thread.** When OpenClaw rotates the underlying transcript during compaction, the plugin now carries the existing conversation root forward so compaction does not fork a second thread for the same chat.
|
|
50
|
+
|
|
51
|
+
### Changed
|
|
52
|
+
|
|
53
|
+
- **Capture now uses lifecycle-aware conversation roots.** `session_end` and `session_start` hooks teach the plugin when a new chat begins and when a transcript rotation is only internal maintenance. Context Engine capture and hook-based capture still converge on the same thread, but `/new` is no longer merged into the prior conversation.
|
|
54
|
+
|
|
55
|
+
## [0.8.8] - 2026-04-09
|
|
56
|
+
|
|
57
|
+
### Fixed
|
|
58
|
+
|
|
59
|
+
- **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.
|
|
60
|
+
|
|
61
|
+
## [0.8.7] - 2026-04-09
|
|
62
|
+
|
|
63
|
+
### Fixed
|
|
64
|
+
|
|
65
|
+
- **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.
|
|
66
|
+
- **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.
|
|
67
|
+
- **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.
|
|
68
|
+
|
|
69
|
+
## [0.8.6] - 2026-04-09
|
|
70
|
+
|
|
71
|
+
### Fixed
|
|
72
|
+
|
|
73
|
+
- **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.
|
|
74
|
+
|
|
75
|
+
### Changed
|
|
76
|
+
|
|
77
|
+
- **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.
|
|
78
|
+
- **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.
|
|
79
|
+
|
|
80
|
+
## [0.8.5] - 2026-04-09
|
|
81
|
+
|
|
82
|
+
### Changed
|
|
83
|
+
|
|
84
|
+
- **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.
|
|
85
|
+
- **`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.
|
|
86
|
+
|
|
5
87
|
## [0.8.3] - 2026-04-08
|
|
6
88
|
|
|
7
89
|
### Added
|
package/CLAUDE.md
CHANGED
|
@@ -11,6 +11,7 @@ Continuation guide for `community/nowledge-mem-openclaw-plugin`.
|
|
|
11
11
|
- Architecture: **CLI-first via OpenClaw runtime** - all CLI execution goes through `api.runtime.system.runCommandWithTimeout`, not direct `child_process`
|
|
12
12
|
- Context engine: registered via `api.registerContextEngine("nowledge-mem", factory)`. Activated when user sets `plugins.slots.contextEngine: "nowledge-mem"`. Falls back to hooks when CE is not active.
|
|
13
13
|
- Remote mode: `~/.nowledge-mem/config.json` (shared) or OpenClaw dashboard. Legacy `openclaw.json` still honored.
|
|
14
|
+
- Ambient space lane: prefer plugin config `space` or `spaceTemplate`. Use `NMEM_SPACE` only when OpenClaw is launched in one stable lane and there is no better profile-owned config surface. Legacy `NMEM_SPACE_ID` still works for older setups.
|
|
14
15
|
|
|
15
16
|
## Design Philosophy
|
|
16
17
|
|
|
@@ -94,7 +95,7 @@ A shared `ceState.active` flag (in `ce-state.js`) coordinates the two paths:
|
|
|
94
95
|
|-----------|-----------|---------------------|
|
|
95
96
|
| Behavioral guidance | `assemble()` → `systemPromptAddition` | `before_prompt_build` → `appendSystemContext` |
|
|
96
97
|
| Memory recall | `assemble()` → `systemPromptAddition` | `before_prompt_build` → `appendSystemContext` |
|
|
97
|
-
| Thread capture | `afterTurn()` (every turn) | `agent_end` / `after_compaction` / `before_reset` |
|
|
98
|
+
| Thread capture | `afterTurn()` (every turn) + hook fallback | `agent_end` / `after_compaction` / `before_reset` |
|
|
98
99
|
| Triage + distill | `afterTurn()` | `agent_end` only |
|
|
99
100
|
| Compaction | `compact()` with memory-aware instructions | None (OpenClaw legacy) |
|
|
100
101
|
| Subagent context | `prepareSubagentSpawn()` + `onSubagentEnded()` | None |
|
|
@@ -165,6 +166,7 @@ When `contextEngine` points elsewhere (or is absent), hooks handle everything. N
|
|
|
165
166
|
|
|
166
167
|
Plugin settings: OpenClaw dashboard (pluginConfig). Legacy `~/.nowledge-mem/openclaw.json` still honored.
|
|
167
168
|
Credentials (apiUrl/apiKey): also reads `~/.nowledge-mem/config.json` (shared with all Nowledge Mem tools).
|
|
169
|
+
Ambient space: choose one real lane through plugin config `space` or `spaceTemplate` first. Use `NMEM_SPACE` only as the fallback for launcher-driven sessions that already belong to one real lane. Do not introduce a second plugin-local vault concept. Legacy `NMEM_SPACE_ID` remains compatibility-only.
|
|
168
170
|
|
|
169
171
|
| Key | Type | Default | Env Var | Description |
|
|
170
172
|
|-----|------|---------|---------|-------------|
|
|
@@ -181,6 +183,8 @@ Credentials (apiUrl/apiKey): also reads `~/.nowledge-mem/config.json` (shared wi
|
|
|
181
183
|
| `corpusMinScore` | integer 0-100 | `0` | `NMEM_CORPUS_MIN_SCORE` | Min score for supplement results (0 = all) |
|
|
182
184
|
| `apiUrl` | string | `""` | `NMEM_API_URL` | Remote server URL. Empty = local (127.0.0.1:14242) |
|
|
183
185
|
| `apiKey` | string | `""` | `NMEM_API_KEY` | API key. Never logged. |
|
|
186
|
+
| `space` | string | `""` | `NMEM_SPACE` (fallback only) | Fixed ambient space name for this OpenClaw profile or agent lane |
|
|
187
|
+
| `spaceTemplate` | string | `""` | — | Template with `${ENV_VAR}` placeholders, used only when `space` is empty |
|
|
184
188
|
|
|
185
189
|
**Priority** (plugin-specific keys): `openclaw.json` (legacy) > pluginConfig > env var > default.
|
|
186
190
|
**Priority** (apiUrl/apiKey): `openclaw.json` (legacy) > pluginConfig > `config.json` (shared) > env var > default.
|
package/README.md
CHANGED
|
@@ -97,6 +97,31 @@ Legacy `~/.nowledge-mem/openclaw.json` is still honored first for backward compa
|
|
|
97
97
|
|
|
98
98
|
The resolved `apiUrl` and `apiKey` are reused across the plugin: CLI-backed memory tools and API-backed thread sync both talk to the same backend. The `apiKey` is never passed as a CLI argument and is never logged.
|
|
99
99
|
|
|
100
|
+
### Spaces
|
|
101
|
+
|
|
102
|
+
Spaces are optional. OpenClaw should choose one ambient lane only when the profile or process already belongs to one real project or agent lane.
|
|
103
|
+
|
|
104
|
+
The OpenClaw plugin settings can own that lane directly:
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
{
|
|
108
|
+
"space": "Research Agent",
|
|
109
|
+
"spaceTemplate": "agent-${OPENCLAW_AGENT_NAME}"
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Use `space` when one OpenClaw profile belongs to one stable lane. Use `spaceTemplate` only when your launcher or host environment already sets a trustworthy variable that identifies the lane. If your OpenClaw runtime does not expose per-agent identity to plugins, do not fake it. Use one profile/process per lane or stay on `Default`.
|
|
114
|
+
|
|
115
|
+
If you are running OpenClaw from a launcher or script with no richer config surface, you can still set one session-wide fallback lane with:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
NMEM_SPACE="Research Agent"
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Nowledge Mem's CLI-backed Working Memory, memory search/save, and the plugin's API-backed thread/feed fallbacks will then stay in that lane together. There is no second OpenClaw-only vault setting; the shared Mem boundary is still one hidden space key, but humans and agents should normally work with the space name instead.
|
|
122
|
+
|
|
123
|
+
Shared spaces, default retrieval, and agent guidance still live in Mem's own space profile. OpenClaw chooses the lane and preserves it across transports; it should not duplicate the profile semantics.
|
|
124
|
+
|
|
100
125
|
### Configure via WebUI of OpenClaw
|
|
101
126
|
|
|
102
127
|
<img width="1192" height="2007" alt="openclaw-config" src="https://github.com/user-attachments/assets/a4abd855-db56-4a1f-b0ac-0114909d0c20" />
|
|
@@ -365,7 +390,7 @@ The behavioral guidance adjusts when `sessionContext` is enabled. Instead of "se
|
|
|
365
390
|
|
|
366
391
|
**What happens to conversations I don't explicitly save?**
|
|
367
392
|
|
|
368
|
-
With `sessionDigest` enabled (the default), every conversation is saved as a searchable thread.
|
|
393
|
+
With `sessionDigest` enabled (the default), every conversation is saved as a searchable thread. The boundary follows OpenClaw's own session lifecycle: one active chat becomes one Mem thread, `/new` or `/reset` starts a fresh thread, and internal compaction keeps appending to the same thread instead of forking a duplicate. Context Engine capture and hook-based capture still converge on 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
394
|
|
|
370
395
|
**Can memories be wrong or outdated?**
|
|
371
396
|
|
|
@@ -408,15 +433,15 @@ To change settings, use the OpenClaw plugin settings UI. Changes take effect on
|
|
|
408
433
|
|
|
409
434
|
### Remote access
|
|
410
435
|
|
|
411
|
-
|
|
436
|
+
Configure this machine once:
|
|
412
437
|
|
|
413
|
-
```
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
"apiKey": "nmem_..."
|
|
417
|
-
}
|
|
438
|
+
```bash
|
|
439
|
+
nmem config client set url https://<your-url>
|
|
440
|
+
nmem config client set api-key nmem_...
|
|
418
441
|
```
|
|
419
442
|
|
|
443
|
+
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.
|
|
444
|
+
|
|
420
445
|
See [Access Mem Anywhere](https://mem.nowledge.co/docs/remote-access).
|
|
421
446
|
|
|
422
447
|
### Advanced: environment variables
|
|
@@ -449,7 +474,7 @@ Credentials (apiUrl, apiKey):
|
|
|
449
474
|
openclaw.json (legacy) > OpenClaw dashboard > config.json (shared) > env vars > defaults
|
|
450
475
|
```
|
|
451
476
|
|
|
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
|
|
477
|
+
`~/.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
478
|
|
|
454
479
|
Use `nowledge_mem_status` (or `openclaw nowledge-mem status`) to see where each value comes from.
|
|
455
480
|
|
|
@@ -505,6 +530,43 @@ If `openclaw status` shows a CRITICAL warning about `plugins.allow`, this is the
|
|
|
505
530
|
|
|
506
531
|
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
532
|
|
|
533
|
+
**Memory tools still work, but thread auto-sync does not**
|
|
534
|
+
|
|
535
|
+
This is usually a capture-path issue, not a search/save issue:
|
|
536
|
+
|
|
537
|
+
- memory tools use the local `nmem` CLI
|
|
538
|
+
- thread auto-sync uses the Mem HTTP API
|
|
539
|
+
- cron / isolated automation sessions are intentionally excluded from capture in `0.8.x`
|
|
540
|
+
|
|
541
|
+
Run `nowledge_mem_status` in a conversation and check, in this order:
|
|
542
|
+
|
|
543
|
+
- the plugin is loaded
|
|
544
|
+
- `sessionDigest` is still `true`
|
|
545
|
+
- the backend is reachable
|
|
546
|
+
- which capture path is active: hook events, or Context Engine `afterTurn` with hook fallback
|
|
547
|
+
- whether you changed plugin settings recently without restarting OpenClaw yet
|
|
548
|
+
|
|
549
|
+
If your config enables `plugins.slots.contextEngine: "nowledge-mem"`:
|
|
550
|
+
|
|
551
|
+
- on `0.8.6+`, Context Engine capture stays active and hooks remain enabled as a safety net
|
|
552
|
+
- on `0.8.5` and earlier, temporarily removing the `contextEngine` slot is a valid isolation step if thread sync stops while tools still work
|
|
553
|
+
|
|
554
|
+
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.
|
|
555
|
+
|
|
556
|
+
What healthy OpenClaw thread sync looks like:
|
|
557
|
+
|
|
558
|
+
- one active OpenClaw chat becomes one Mem thread
|
|
559
|
+
- running `/new` or `/reset` starts a fresh Mem thread
|
|
560
|
+
- compaction does not fork a second thread for the same chat
|
|
561
|
+
- helper sessions like `temp:slug-generator` do not appear
|
|
562
|
+
- the synthetic `/new` / `/reset` startup prompt is not stored as the first user message
|
|
563
|
+
|
|
564
|
+
To inspect the recent synced threads directly, run:
|
|
565
|
+
|
|
566
|
+
```bash
|
|
567
|
+
nmem t list --source openclaw -n 20
|
|
568
|
+
```
|
|
569
|
+
|
|
508
570
|
**Search timeouts with many concurrent agents**
|
|
509
571
|
|
|
510
572
|
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 +577,7 @@ The Nowledge Mem backend is not running. Start it from the desktop app, or run t
|
|
|
515
577
|
|
|
516
578
|
**Remote mode not connecting**
|
|
517
579
|
|
|
518
|
-
|
|
580
|
+
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
581
|
|
|
520
582
|
## What Makes This Different
|
|
521
583
|
|
package/SKILL.md
CHANGED
|
@@ -63,6 +63,7 @@ Explain the difference plainly:
|
|
|
63
63
|
|
|
64
64
|
- Local mode: OpenClaw talks to the local Nowledge Mem app/server through `nmem`
|
|
65
65
|
- Remote mode: OpenClaw talks to a remote Nowledge Mem server through `nmem --api-url ...`; add `apiKey` when that server has auth enabled
|
|
66
|
+
- If the user wants one agent-specific lane, use plugin config `space` first, plugin config `spaceTemplate` only when the launcher already exposes a trustworthy lane signal, and `NMEM_SPACE="<space name>"` only as the fallback for CLI-style launches instead of inventing a second OpenClaw-only memory namespace
|
|
66
67
|
|
|
67
68
|
### Step 1 — Verify `nmem`
|
|
68
69
|
|
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.12",
|
|
6
6
|
"kind": "memory",
|
|
7
7
|
"skills": [
|
|
8
8
|
"skills/memory-guide"
|
|
@@ -60,6 +60,14 @@
|
|
|
60
60
|
"label": "API key (remote mode)",
|
|
61
61
|
"help": "Required when connecting to a remote server. Never logged or passed as a CLI argument. Injected as NMEM_API_KEY env var only.",
|
|
62
62
|
"sensitive": true
|
|
63
|
+
},
|
|
64
|
+
"space": {
|
|
65
|
+
"label": "Ambient space",
|
|
66
|
+
"help": "Optional fixed space name for this OpenClaw profile or agent lane. Leave empty to stay on Default."
|
|
67
|
+
},
|
|
68
|
+
"spaceTemplate": {
|
|
69
|
+
"label": "Ambient space template",
|
|
70
|
+
"help": "Optional template with ${ENV_VAR} placeholders, used only when Ambient space is empty and your launcher already exports a trustworthy lane signal. Example: agent-${OPENCLAW_AGENT_NAME}."
|
|
63
71
|
}
|
|
64
72
|
},
|
|
65
73
|
"configSchema": {
|
|
@@ -144,6 +152,16 @@
|
|
|
144
152
|
"type": "string",
|
|
145
153
|
"default": "",
|
|
146
154
|
"description": "API key for remote access. Stored in plugin config, injected as NMEM_API_KEY env var. Never logged."
|
|
155
|
+
},
|
|
156
|
+
"space": {
|
|
157
|
+
"type": "string",
|
|
158
|
+
"default": "",
|
|
159
|
+
"description": "Optional fixed space name for this OpenClaw profile or agent lane."
|
|
160
|
+
},
|
|
161
|
+
"spaceTemplate": {
|
|
162
|
+
"type": "string",
|
|
163
|
+
"default": "",
|
|
164
|
+
"description": "Optional template with ${ENV_VAR} placeholders. Used only when space is empty."
|
|
147
165
|
}
|
|
148
166
|
}
|
|
149
167
|
}
|
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.12",
|
|
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"
|
|
@@ -43,13 +42,11 @@
|
|
|
43
42
|
},
|
|
44
43
|
"main": "src/index.js",
|
|
45
44
|
"scripts": {
|
|
45
|
+
"test": "node --test tests/*.test.mjs",
|
|
46
46
|
"validate": "node scripts/validate-plugin.mjs",
|
|
47
47
|
"lint": "biome check .",
|
|
48
48
|
"format": "biome format --write ."
|
|
49
49
|
},
|
|
50
|
-
"peerDependencies": {
|
|
51
|
-
"openclaw": ">=2026.4.5"
|
|
52
|
-
},
|
|
53
50
|
"devDependencies": {
|
|
54
51
|
"@biomejs/biome": "^1.9.0"
|
|
55
52
|
}
|
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/client.js
CHANGED
|
@@ -70,7 +70,7 @@ export class NowledgeMemClient {
|
|
|
70
70
|
/**
|
|
71
71
|
* @param {object} logger
|
|
72
72
|
* @param {object} runtimeSystem
|
|
73
|
-
* @param {{ apiUrl?: string; apiKey?: string }} [credentials]
|
|
73
|
+
* @param {{ apiUrl?: string; apiKey?: string; space?: string; spaceId?: string }} [credentials]
|
|
74
74
|
*/
|
|
75
75
|
constructor(logger, runtimeSystem, credentials = {}) {
|
|
76
76
|
this.logger = logger;
|
|
@@ -82,6 +82,18 @@ export class NowledgeMemClient {
|
|
|
82
82
|
(credentials.apiUrl || "").trim() || "http://127.0.0.1:14242"
|
|
83
83
|
).replace(/\/+$/, "");
|
|
84
84
|
this._apiKey = (credentials.apiKey || "").trim();
|
|
85
|
+
const hasSpace = Object.prototype.hasOwnProperty.call(credentials, "space");
|
|
86
|
+
const hasSpaceId = Object.prototype.hasOwnProperty.call(
|
|
87
|
+
credentials,
|
|
88
|
+
"spaceId",
|
|
89
|
+
);
|
|
90
|
+
this._hasExplicitSpace = hasSpace || hasSpaceId;
|
|
91
|
+
const resolvedSpace = hasSpace
|
|
92
|
+
? credentials.space
|
|
93
|
+
: hasSpaceId
|
|
94
|
+
? credentials.spaceId
|
|
95
|
+
: process.env.NMEM_SPACE ?? process.env.NMEM_SPACE_ID ?? "";
|
|
96
|
+
this._spaceRef = String(resolvedSpace ?? "").trim();
|
|
85
97
|
}
|
|
86
98
|
|
|
87
99
|
// ── API helpers (fallback path and direct operations) ─────────────────────
|
|
@@ -99,15 +111,39 @@ export class NowledgeMemClient {
|
|
|
99
111
|
return headers;
|
|
100
112
|
}
|
|
101
113
|
|
|
114
|
+
_withSpaceQuery(path) {
|
|
115
|
+
if (!this._spaceRef) return path;
|
|
116
|
+
const [pathname, rawQuery = ""] = String(path).split("?", 2);
|
|
117
|
+
const query = new URLSearchParams(rawQuery);
|
|
118
|
+
if (!query.has("space_id")) {
|
|
119
|
+
query.set("space_id", this._spaceRef);
|
|
120
|
+
}
|
|
121
|
+
const rendered = query.toString();
|
|
122
|
+
return rendered ? `${pathname}?${rendered}` : pathname;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
_withSpaceBody(body) {
|
|
126
|
+
if (!this._spaceRef || body == null || Array.isArray(body)) {
|
|
127
|
+
return body;
|
|
128
|
+
}
|
|
129
|
+
if (Object.prototype.hasOwnProperty.call(body, "space_id")) {
|
|
130
|
+
return body;
|
|
131
|
+
}
|
|
132
|
+
return { ...body, space_id: this._spaceRef };
|
|
133
|
+
}
|
|
134
|
+
|
|
102
135
|
async apiJson(method, path, body, timeout = 30_000) {
|
|
103
136
|
const controller = new AbortController();
|
|
104
137
|
const timer = setTimeout(() => controller.abort(), timeout);
|
|
105
|
-
const
|
|
138
|
+
const scopedPath = this._withSpaceQuery(path);
|
|
139
|
+
const scopedBody = this._withSpaceBody(body);
|
|
140
|
+
const url = `${this.getApiBaseUrl()}${scopedPath}`;
|
|
106
141
|
try {
|
|
107
142
|
const response = await fetch(url, {
|
|
108
143
|
method,
|
|
109
144
|
headers: this.getApiHeaders(),
|
|
110
|
-
body:
|
|
145
|
+
body:
|
|
146
|
+
scopedBody === undefined ? undefined : JSON.stringify(scopedBody),
|
|
111
147
|
signal: controller.signal,
|
|
112
148
|
});
|
|
113
149
|
|
|
@@ -184,6 +220,8 @@ export class NowledgeMemClient {
|
|
|
184
220
|
return buildNmemSpawnEnv({
|
|
185
221
|
apiUrl: this._apiUrl,
|
|
186
222
|
apiKey: this._apiKey,
|
|
223
|
+
spaceId: this._spaceRef,
|
|
224
|
+
hasExplicitSpace: this._hasExplicitSpace,
|
|
187
225
|
});
|
|
188
226
|
}
|
|
189
227
|
|
|
@@ -557,12 +595,16 @@ export class NowledgeMemClient {
|
|
|
557
595
|
throw new Error("createThread requires at least one message");
|
|
558
596
|
}
|
|
559
597
|
|
|
560
|
-
const data = await this.apiJson(
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
598
|
+
const data = await this.apiJson(
|
|
599
|
+
"POST",
|
|
600
|
+
"/threads",
|
|
601
|
+
{
|
|
602
|
+
...(threadId ? { thread_id: String(threadId) } : {}),
|
|
603
|
+
title: normalizedTitle,
|
|
604
|
+
source: String(source),
|
|
605
|
+
messages,
|
|
606
|
+
},
|
|
607
|
+
);
|
|
566
608
|
|
|
567
609
|
return String(
|
|
568
610
|
data.id ?? data.thread?.thread_id ?? data.thread_id ?? "created",
|
|
@@ -692,7 +734,11 @@ export class NowledgeMemClient {
|
|
|
692
734
|
});
|
|
693
735
|
if (updated === null) throw new Error(`Section not found: ${heading}`);
|
|
694
736
|
|
|
695
|
-
return this.apiJson(
|
|
737
|
+
return this.apiJson(
|
|
738
|
+
"PUT",
|
|
739
|
+
"/agent/working-memory",
|
|
740
|
+
{ content: updated },
|
|
741
|
+
);
|
|
696
742
|
}
|
|
697
743
|
}
|
|
698
744
|
|
package/src/config.js
CHANGED
|
@@ -28,6 +28,8 @@ const ALLOWED_KEYS = new Set([
|
|
|
28
28
|
"corpusMinScore",
|
|
29
29
|
"apiUrl",
|
|
30
30
|
"apiKey",
|
|
31
|
+
"space",
|
|
32
|
+
"spaceTemplate",
|
|
31
33
|
// Legacy aliases — accepted but not advertised
|
|
32
34
|
"autoRecall",
|
|
33
35
|
"autoCapture",
|
|
@@ -128,6 +130,30 @@ function envInt(name) {
|
|
|
128
130
|
return Number.isFinite(n) ? n : undefined;
|
|
129
131
|
}
|
|
130
132
|
|
|
133
|
+
function pickStr(obj, key) {
|
|
134
|
+
if (typeof obj[key] !== "string") return undefined;
|
|
135
|
+
const trimmed = obj[key].trim();
|
|
136
|
+
return trimmed || undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function pickPresentString(obj, key) {
|
|
140
|
+
if (!Object.prototype.hasOwnProperty.call(obj, key)) return undefined;
|
|
141
|
+
if (typeof obj[key] !== "string") return undefined;
|
|
142
|
+
return obj[key].trim();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function resolveEnvTemplate(value) {
|
|
146
|
+
return value.replace(/\$\{([^}]+)\}/g, (_, envVar) => {
|
|
147
|
+
const envValue = process.env[envVar];
|
|
148
|
+
if (typeof envValue !== "string" || !envValue.trim()) {
|
|
149
|
+
throw new Error(
|
|
150
|
+
`nowledge-mem: environment variable ${envVar} is required by spaceTemplate`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
return envValue.trim();
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
131
157
|
// --- helpers to read a boolean/number from resolved sources --------------
|
|
132
158
|
|
|
133
159
|
function pickBool(obj, key) {
|
|
@@ -162,7 +188,8 @@ function resolveAliases(obj) {
|
|
|
162
188
|
*/
|
|
163
189
|
function firstDefined(...options) {
|
|
164
190
|
for (const opt of options) {
|
|
165
|
-
|
|
191
|
+
const value = typeof opt.resolve === "function" ? opt.resolve() : opt.value;
|
|
192
|
+
if (value !== undefined) return { ...opt, value };
|
|
166
193
|
}
|
|
167
194
|
return options[options.length - 1];
|
|
168
195
|
}
|
|
@@ -185,7 +212,8 @@ function firstDefined(...options) {
|
|
|
185
212
|
* but is no longer the recommended path.
|
|
186
213
|
*
|
|
187
214
|
* Canonical keys: sessionContext, sessionDigest, digestMinInterval,
|
|
188
|
-
* maxContextResults, recallMinScore, maxThreadMessageChars,
|
|
215
|
+
* maxContextResults, recallMinScore, maxThreadMessageChars,
|
|
216
|
+
* apiUrl, apiKey, space, spaceTemplate
|
|
189
217
|
*
|
|
190
218
|
* Legacy aliases (accepted from all sources; never shown in docs):
|
|
191
219
|
* autoRecall → sessionContext
|
|
@@ -205,6 +233,7 @@ function firstDefined(...options) {
|
|
|
205
233
|
* NMEM_CORPUS_MIN_SCORE — integer (0-100)
|
|
206
234
|
* NMEM_API_URL — remote server URL
|
|
207
235
|
* NMEM_API_KEY — API key (never logged)
|
|
236
|
+
* NMEM_SPACE — ambient space name (legacy: NMEM_SPACE_ID)
|
|
208
237
|
*/
|
|
209
238
|
export function parseConfig(raw, logger) {
|
|
210
239
|
const pluginCfg = raw && typeof raw === "object" ? raw : {};
|
|
@@ -215,16 +244,19 @@ export function parseConfig(raw, logger) {
|
|
|
215
244
|
const resolvedPlugin = resolveAliases(pluginCfg);
|
|
216
245
|
const resolvedFile = resolveAliases(fileCfg);
|
|
217
246
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
(
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
247
|
+
const validateKnownKeys = (obj, sourceLabel) => {
|
|
248
|
+
const unknownKeys = Object.keys(obj).filter((k) => !ALLOWED_KEYS.has(k));
|
|
249
|
+
if (unknownKeys.length > 0) {
|
|
250
|
+
throw new Error(
|
|
251
|
+
`nowledge-mem: unknown config key${unknownKeys.length > 1 ? "s" : ""} in ${sourceLabel}: ${unknownKeys.join(", ")}. ` +
|
|
252
|
+
`Allowed keys: ${[...ALLOWED_KEYS].filter((k) => !Object.keys(ALIAS_KEYS).includes(k)).join(", ")}`,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// Strict: reject unknown keys in both legacy file config and dashboard config.
|
|
258
|
+
validateKnownKeys(resolvedFile, "~/.nowledge-mem/openclaw.json");
|
|
259
|
+
validateKnownKeys(resolvedPlugin, "plugin config");
|
|
228
260
|
|
|
229
261
|
const _sources = {};
|
|
230
262
|
|
|
@@ -351,6 +383,31 @@ export function parseConfig(raw, logger) {
|
|
|
351
383
|
const apiKey = ak.value;
|
|
352
384
|
_sources.apiKey = ak.source;
|
|
353
385
|
|
|
386
|
+
// --- space: file (legacy) > pluginConfig > template > env > default ---
|
|
387
|
+
const fs = pickPresentString(resolvedFile, "space");
|
|
388
|
+
const ps = pickPresentString(resolvedPlugin, "space");
|
|
389
|
+
const fst = pickStr(resolvedFile, "spaceTemplate");
|
|
390
|
+
const pst = pickStr(resolvedPlugin, "spaceTemplate");
|
|
391
|
+
const spaceChoice = firstDefined(
|
|
392
|
+
{ value: fs, source: "file" },
|
|
393
|
+
{ value: ps, source: "pluginConfig" },
|
|
394
|
+
{
|
|
395
|
+
resolve: () => (fst ? resolveEnvTemplate(fst) : undefined),
|
|
396
|
+
source: "file:template",
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
resolve: () => (pst ? resolveEnvTemplate(pst) : undefined),
|
|
400
|
+
source: "pluginConfig:template",
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
value: envStr("NMEM_SPACE") ?? envStr("NMEM_SPACE_ID"),
|
|
404
|
+
source: "env",
|
|
405
|
+
},
|
|
406
|
+
{ value: "", source: "default" },
|
|
407
|
+
);
|
|
408
|
+
const space = spaceChoice.value || "";
|
|
409
|
+
_sources.space = spaceChoice.source;
|
|
410
|
+
|
|
354
411
|
// --- corpusSupplement: file > pluginConfig > env > default ---
|
|
355
412
|
const cs = firstDefined(
|
|
356
413
|
{ value: pickBool(resolvedFile, "corpusSupplement"), source: "file" },
|
|
@@ -445,6 +502,7 @@ export function parseConfig(raw, logger) {
|
|
|
445
502
|
corpusMinScore,
|
|
446
503
|
apiUrl,
|
|
447
504
|
apiKey,
|
|
505
|
+
space,
|
|
448
506
|
_sources,
|
|
449
507
|
};
|
|
450
508
|
}
|
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,26 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
1
|
import { readFile } from "node:fs/promises";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { isCronSessionKey, isSubagentSessionKey } from "openclaw/plugin-sdk/routing";
|
|
4
|
+
import {
|
|
5
|
+
buildStableThreadId,
|
|
6
|
+
} from "./thread-identity.js";
|
|
7
|
+
export {
|
|
8
|
+
_resetConversationRoots,
|
|
9
|
+
buildStableThreadId,
|
|
10
|
+
registerSessionEndConversation,
|
|
11
|
+
registerSessionStartConversation,
|
|
12
|
+
} from "./thread-identity.js";
|
|
5
13
|
|
|
6
14
|
export const DEFAULT_MAX_MESSAGE_CHARS = 800;
|
|
7
15
|
export const MAX_DISTILL_MESSAGE_CHARS = 2000;
|
|
8
16
|
export const MAX_CONVERSATION_CHARS = 30_000;
|
|
9
17
|
export const MIN_MESSAGES_FOR_DISTILL = 4;
|
|
18
|
+
const SESSION_RESET_PROMPT_PREFIX =
|
|
19
|
+
"A new session was started via /new or /reset.";
|
|
20
|
+
const OPENCLAW_DIRECTIVE_TAG_RE =
|
|
21
|
+
/\s*\[\[\s*(?:audio_as_voice|reply_to_current|reply_to\s*:\s*[^\]\n]+)\s*\]\]\s*/giu;
|
|
22
|
+
const OPENCLAW_INTERNAL_SENDER_BLOCK_RE =
|
|
23
|
+
/^Sender \(untrusted metadata\):\s*```json\s*([\s\S]*?)\s*```\s*([\s\S]*)$/iu;
|
|
10
24
|
|
|
11
25
|
// Per-thread triage cooldown: prevents burst triage/distillation from heartbeat.
|
|
12
26
|
// Maps threadId -> timestamp (ms) of last successful triage.
|
|
@@ -15,7 +29,6 @@ const _lastCaptureAt = new Map();
|
|
|
15
29
|
const _MAX_COOLDOWN_ENTRIES = 200;
|
|
16
30
|
const _syncedMessageCounts = new Map();
|
|
17
31
|
const _MAX_SYNC_CURSOR_ENTRIES = 500;
|
|
18
|
-
|
|
19
32
|
function _setLastCapture(threadId, now) {
|
|
20
33
|
_lastCaptureAt.set(threadId, now);
|
|
21
34
|
// Opportunistic eviction: sweep stale entries when map grows large
|
|
@@ -113,6 +126,52 @@ export function hasSkipMarker(messages, marker) {
|
|
|
113
126
|
});
|
|
114
127
|
}
|
|
115
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Internal helper / system sessions should not become user-facing Mem threads.
|
|
131
|
+
*
|
|
132
|
+
* - temp:* covers OpenClaw helper sessions such as slug generation
|
|
133
|
+
* - subagent sessions are execution internals, not first-class user chats
|
|
134
|
+
*/
|
|
135
|
+
export function isInternalCaptureSessionKey(sessionKey) {
|
|
136
|
+
const raw = String(sessionKey || "")
|
|
137
|
+
.trim()
|
|
138
|
+
.toLowerCase();
|
|
139
|
+
if (!raw) return false;
|
|
140
|
+
if (raw.startsWith("temp:")) return true;
|
|
141
|
+
return isSubagentSessionKey(raw);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function stripOpenClawDirectiveTags(text) {
|
|
145
|
+
return String(text || "")
|
|
146
|
+
.replace(OPENCLAW_DIRECTIVE_TAG_RE, " ")
|
|
147
|
+
.replace(/[ \t]+\n/g, "\n")
|
|
148
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
149
|
+
.trim();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function stripInternalSenderMetadata(text) {
|
|
153
|
+
const raw = String(text || "").trim();
|
|
154
|
+
if (!raw.startsWith("Sender (untrusted metadata):")) return raw;
|
|
155
|
+
const match = raw.match(OPENCLAW_INTERNAL_SENDER_BLOCK_RE);
|
|
156
|
+
if (!match) return raw;
|
|
157
|
+
const [, jsonBlock, remainder] = match;
|
|
158
|
+
try {
|
|
159
|
+
const parsed = JSON.parse(jsonBlock);
|
|
160
|
+
const label = String(parsed?.label || "")
|
|
161
|
+
.trim()
|
|
162
|
+
.toLowerCase();
|
|
163
|
+
const id = String(parsed?.id || "")
|
|
164
|
+
.trim()
|
|
165
|
+
.toLowerCase();
|
|
166
|
+
if (label === "openclaw-control-ui" || id === "openclaw-control-ui") {
|
|
167
|
+
return String(remainder || "").trim();
|
|
168
|
+
}
|
|
169
|
+
} catch {
|
|
170
|
+
return raw;
|
|
171
|
+
}
|
|
172
|
+
return raw;
|
|
173
|
+
}
|
|
174
|
+
|
|
116
175
|
// ---------------------------------------------------------------------------
|
|
117
176
|
// Message normalization utilities
|
|
118
177
|
// ---------------------------------------------------------------------------
|
|
@@ -151,7 +210,13 @@ export function normalizeRoleMessage(
|
|
|
151
210
|
raw.message && typeof raw.message === "object" ? raw.message : raw;
|
|
152
211
|
const role = typeof msg.role === "string" ? msg.role : "";
|
|
153
212
|
if (role !== "user" && role !== "assistant") return null;
|
|
154
|
-
const
|
|
213
|
+
const extractedText = extractText(msg.content);
|
|
214
|
+
if (!extractedText) return null;
|
|
215
|
+
const cleanedText = stripInternalSenderMetadata(extractedText);
|
|
216
|
+
if (role === "user" && cleanedText.startsWith(SESSION_RESET_PROMPT_PREFIX)) {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
const text = stripOpenClawDirectiveTags(cleanedText);
|
|
155
220
|
if (!text) return null;
|
|
156
221
|
if (role === "user" && text.startsWith("/")) return null;
|
|
157
222
|
|
|
@@ -184,10 +249,9 @@ export function normalizeRoleMessage(
|
|
|
184
249
|
};
|
|
185
250
|
}
|
|
186
251
|
|
|
187
|
-
export function buildThreadTitle(ctx
|
|
252
|
+
export function buildThreadTitle(ctx) {
|
|
188
253
|
const session = ctx?.sessionKey || ctx?.sessionId || "session";
|
|
189
|
-
|
|
190
|
-
return `OpenClaw ${session}${reasonSuffix}`;
|
|
254
|
+
return `OpenClaw ${session}`;
|
|
191
255
|
}
|
|
192
256
|
|
|
193
257
|
function sanitizeIdPart(input, max = 48) {
|
|
@@ -199,17 +263,6 @@ function sanitizeIdPart(input, max = 48) {
|
|
|
199
263
|
return normalized.slice(0, max);
|
|
200
264
|
}
|
|
201
265
|
|
|
202
|
-
export function buildStableThreadId(event, ctx) {
|
|
203
|
-
const base =
|
|
204
|
-
String(ctx?.sessionId || "").trim() ||
|
|
205
|
-
String(ctx?.sessionKey || "").trim() ||
|
|
206
|
-
String(event?.sessionFile || "").trim() ||
|
|
207
|
-
"session";
|
|
208
|
-
const slug = sanitizeIdPart(base);
|
|
209
|
-
const digest = createHash("sha1").update(base).digest("hex").slice(0, 10);
|
|
210
|
-
return `openclaw-${slug}-${digest}`;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
266
|
function buildExternalId({ normalized, index, threadId, sessionKey }) {
|
|
214
267
|
if (normalized.externalHint) {
|
|
215
268
|
return `oc:${sanitizeIdPart(normalized.externalHint, 96)}`;
|
|
@@ -282,7 +335,7 @@ export async function appendOrCreateThread({
|
|
|
282
335
|
const threadId = buildStableThreadId(event, ctx);
|
|
283
336
|
const sessionKey = String(ctx?.sessionKey || ctx?.sessionId || "session");
|
|
284
337
|
const sessionId = String(ctx?.sessionId || "").trim();
|
|
285
|
-
const title = buildThreadTitle(ctx
|
|
338
|
+
const title = buildThreadTitle(ctx);
|
|
286
339
|
const normalized = rawMessages
|
|
287
340
|
.map((message) => normalizeRoleMessage(message, maxMessageChars))
|
|
288
341
|
.filter(Boolean);
|
|
@@ -457,7 +510,7 @@ export async function triageAndDistill({
|
|
|
457
510
|
|
|
458
511
|
const distillResult = await client.distillThread({
|
|
459
512
|
threadId: captureResult.threadId,
|
|
460
|
-
title: buildThreadTitle(ctx
|
|
513
|
+
title: buildThreadTitle(ctx),
|
|
461
514
|
content: conversationText,
|
|
462
515
|
});
|
|
463
516
|
|
|
@@ -477,8 +530,9 @@ export async function triageAndDistill({
|
|
|
477
530
|
/**
|
|
478
531
|
* Capture thread + LLM-based distillation after a successful agent run.
|
|
479
532
|
*
|
|
480
|
-
*
|
|
481
|
-
*
|
|
533
|
+
* Even when the context engine is active, this hook remains as a safety net.
|
|
534
|
+
* Thread append/create is already idempotent, so the hook path can safely
|
|
535
|
+
* backstop CE afterTurn without duplicating stored messages.
|
|
482
536
|
*
|
|
483
537
|
* Heartbeat sessions (ctx.trigger === "heartbeat") are skipped — they
|
|
484
538
|
* produce repetitive status pings that aren't worth preserving.
|
|
@@ -487,7 +541,6 @@ export async function triageAndDistill({
|
|
|
487
541
|
*/
|
|
488
542
|
export function buildAgentEndCaptureHandler(client, cfg, logger) {
|
|
489
543
|
return async (event, ctx) => {
|
|
490
|
-
if (ceState.active) return;
|
|
491
544
|
if (!event?.success) return;
|
|
492
545
|
if (ctx?.trigger === "heartbeat") return;
|
|
493
546
|
|
|
@@ -496,6 +549,10 @@ export function buildAgentEndCaptureHandler(client, cfg, logger) {
|
|
|
496
549
|
logger.debug?.(`capture: skipped cron session ${sessionKey}`);
|
|
497
550
|
return;
|
|
498
551
|
}
|
|
552
|
+
if (isInternalCaptureSessionKey(sessionKey)) {
|
|
553
|
+
logger.debug?.(`capture: skipped internal session ${sessionKey}`);
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
499
556
|
|
|
500
557
|
// Pattern-based exclusion (e.g. subagent sessions, custom jobs)
|
|
501
558
|
if (matchesExcludePattern(sessionKey, cfg.captureExclude)) {
|
|
@@ -530,14 +587,13 @@ export function buildAgentEndCaptureHandler(client, cfg, logger) {
|
|
|
530
587
|
* Capture thread messages before reset or after compaction.
|
|
531
588
|
* Thread-only (no distillation) — these are lifecycle checkpoints.
|
|
532
589
|
*
|
|
533
|
-
*
|
|
534
|
-
*
|
|
590
|
+
* This hook also stays enabled when the context engine is active.
|
|
591
|
+
* The shared tail-sync dedup keeps the fallback path cheap and safe.
|
|
535
592
|
*
|
|
536
593
|
* Heartbeat sessions are skipped (same rationale as agent_end).
|
|
537
594
|
*/
|
|
538
595
|
export function buildBeforeResetCaptureHandler(client, cfg, logger) {
|
|
539
596
|
return async (event, ctx) => {
|
|
540
|
-
if (ceState.active) return;
|
|
541
597
|
if (ctx?.trigger === "heartbeat") return;
|
|
542
598
|
|
|
543
599
|
const sessionKey = String(ctx?.sessionKey || ctx?.sessionId || "");
|
|
@@ -545,6 +601,10 @@ export function buildBeforeResetCaptureHandler(client, cfg, logger) {
|
|
|
545
601
|
logger.debug?.(`capture: skipped cron session ${sessionKey}`);
|
|
546
602
|
return;
|
|
547
603
|
}
|
|
604
|
+
if (isInternalCaptureSessionKey(sessionKey)) {
|
|
605
|
+
logger.debug?.(`capture: skipped internal session ${sessionKey}`);
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
548
608
|
|
|
549
609
|
if (matchesExcludePattern(sessionKey, cfg.captureExclude)) {
|
|
550
610
|
logger.debug?.(`capture: skipped excluded session ${sessionKey}`);
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
const _conversationRootsBySessionId = new Map();
|
|
4
|
+
const _activeConversationRootsBySessionKey = new Map();
|
|
5
|
+
const _MAX_CONVERSATION_ROOT_ENTRIES = 500;
|
|
6
|
+
|
|
7
|
+
function sanitizeIdPart(input, max = 48) {
|
|
8
|
+
const normalized = String(input || "")
|
|
9
|
+
.toLowerCase()
|
|
10
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
11
|
+
.replace(/^-+|-+$/g, "");
|
|
12
|
+
if (!normalized) return "session";
|
|
13
|
+
return normalized.slice(0, max);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function _setConversationRoot({ sessionId, sessionKey, rootId }) {
|
|
17
|
+
const normalizedRoot = String(rootId || "").trim();
|
|
18
|
+
if (!normalizedRoot) return;
|
|
19
|
+
const normalizedSessionId = String(sessionId || "").trim();
|
|
20
|
+
const normalizedSessionKey = String(sessionKey || "").trim();
|
|
21
|
+
|
|
22
|
+
if (normalizedSessionId) {
|
|
23
|
+
_conversationRootsBySessionId.set(normalizedSessionId, normalizedRoot);
|
|
24
|
+
if (_conversationRootsBySessionId.size > _MAX_CONVERSATION_ROOT_ENTRIES) {
|
|
25
|
+
const excess =
|
|
26
|
+
_conversationRootsBySessionId.size - _MAX_CONVERSATION_ROOT_ENTRIES;
|
|
27
|
+
let removed = 0;
|
|
28
|
+
for (const key of _conversationRootsBySessionId.keys()) {
|
|
29
|
+
_conversationRootsBySessionId.delete(key);
|
|
30
|
+
removed += 1;
|
|
31
|
+
if (removed >= excess) break;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (normalizedSessionKey) {
|
|
37
|
+
_activeConversationRootsBySessionKey.set(
|
|
38
|
+
normalizedSessionKey,
|
|
39
|
+
normalizedRoot,
|
|
40
|
+
);
|
|
41
|
+
if (
|
|
42
|
+
_activeConversationRootsBySessionKey.size > _MAX_CONVERSATION_ROOT_ENTRIES
|
|
43
|
+
) {
|
|
44
|
+
const excess =
|
|
45
|
+
_activeConversationRootsBySessionKey.size - _MAX_CONVERSATION_ROOT_ENTRIES;
|
|
46
|
+
let removed = 0;
|
|
47
|
+
for (const key of _activeConversationRootsBySessionKey.keys()) {
|
|
48
|
+
_activeConversationRootsBySessionKey.delete(key);
|
|
49
|
+
removed += 1;
|
|
50
|
+
if (removed >= excess) break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function resolveConversationRoot(event, ctx) {
|
|
57
|
+
const sessionId = String(ctx?.sessionId || event?.sessionId || "").trim();
|
|
58
|
+
if (sessionId) {
|
|
59
|
+
const mappedRoot = _conversationRootsBySessionId.get(sessionId);
|
|
60
|
+
if (mappedRoot) return mappedRoot;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const sessionKey = String(ctx?.sessionKey || event?.sessionKey || "").trim();
|
|
64
|
+
if (sessionKey) {
|
|
65
|
+
const activeRoot = _activeConversationRootsBySessionKey.get(sessionKey);
|
|
66
|
+
if (activeRoot) return activeRoot;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
sessionId ||
|
|
71
|
+
sessionKey ||
|
|
72
|
+
String(event?.sessionFile || "").trim() ||
|
|
73
|
+
"session"
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function registerSessionStartConversation(event, ctx) {
|
|
78
|
+
const sessionId = String(event?.sessionId || ctx?.sessionId || "").trim();
|
|
79
|
+
const sessionKey = String(event?.sessionKey || ctx?.sessionKey || "").trim();
|
|
80
|
+
if (!sessionId && !sessionKey) return;
|
|
81
|
+
|
|
82
|
+
const resumedFrom = String(event?.resumedFrom || "").trim();
|
|
83
|
+
const inheritedRoot =
|
|
84
|
+
_conversationRootsBySessionId.get(sessionId) ||
|
|
85
|
+
(resumedFrom && _conversationRootsBySessionId.get(resumedFrom)) ||
|
|
86
|
+
sessionId;
|
|
87
|
+
|
|
88
|
+
_setConversationRoot({
|
|
89
|
+
sessionId,
|
|
90
|
+
sessionKey,
|
|
91
|
+
rootId: inheritedRoot || sessionId || sessionKey,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function registerSessionEndConversation(event, ctx) {
|
|
96
|
+
const sessionId = String(event?.sessionId || ctx?.sessionId || "").trim();
|
|
97
|
+
const sessionKey = String(event?.sessionKey || ctx?.sessionKey || "").trim();
|
|
98
|
+
if (!sessionId && !sessionKey) return;
|
|
99
|
+
|
|
100
|
+
const currentRoot = resolveConversationRoot(event, ctx);
|
|
101
|
+
_setConversationRoot({
|
|
102
|
+
sessionId,
|
|
103
|
+
sessionKey,
|
|
104
|
+
rootId: currentRoot,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const nextSessionId = String(event?.nextSessionId || "").trim();
|
|
108
|
+
if (!nextSessionId) return;
|
|
109
|
+
|
|
110
|
+
const reason = String(event?.reason || "").trim().toLowerCase();
|
|
111
|
+
const nextRoot = reason === "compaction" ? currentRoot : nextSessionId;
|
|
112
|
+
_setConversationRoot({
|
|
113
|
+
sessionId: nextSessionId,
|
|
114
|
+
sessionKey,
|
|
115
|
+
rootId: nextRoot,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function buildStableThreadId(event, ctx) {
|
|
120
|
+
const base = resolveConversationRoot(event, ctx);
|
|
121
|
+
const slug = sanitizeIdPart(base);
|
|
122
|
+
const digest = createHash("sha1").update(base).digest("hex").slice(0, 10);
|
|
123
|
+
return `openclaw-${slug}-${digest}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function _resetConversationRoots() {
|
|
127
|
+
_conversationRootsBySessionId.clear();
|
|
128
|
+
_activeConversationRootsBySessionKey.clear();
|
|
129
|
+
}
|
package/src/index.js
CHANGED
|
@@ -12,6 +12,8 @@ import { buildBehavioralHook } from "./hooks/behavioral.js";
|
|
|
12
12
|
import {
|
|
13
13
|
buildAgentEndCaptureHandler,
|
|
14
14
|
buildBeforeResetCaptureHandler,
|
|
15
|
+
registerSessionEndConversation,
|
|
16
|
+
registerSessionStartConversation,
|
|
15
17
|
} from "./hooks/capture.js";
|
|
16
18
|
import { buildRecallHandler } from "./hooks/recall.js";
|
|
17
19
|
import { createConnectionsTool } from "./tools/connections.js";
|
|
@@ -38,6 +40,7 @@ export default {
|
|
|
38
40
|
const client = new NowledgeMemClient(logger, api.runtime.system, {
|
|
39
41
|
apiUrl: cfg.apiUrl,
|
|
40
42
|
apiKey: cfg.apiKey,
|
|
43
|
+
space: cfg.space,
|
|
41
44
|
});
|
|
42
45
|
|
|
43
46
|
// OpenClaw memory-slot compatibility (required for system prompt activation)
|
|
@@ -57,30 +60,43 @@ export default {
|
|
|
57
60
|
|
|
58
61
|
// Diagnostics — pass runtime config so the status tool can detect:
|
|
59
62
|
// - plugins.allow missing or not including this plugin
|
|
60
|
-
// - memory slot
|
|
63
|
+
// - memory/context-engine slot routing for capture and recall
|
|
61
64
|
const memorySlot = api.config?.plugins?.slots?.memory;
|
|
65
|
+
const contextEngineSlot = api.config?.plugins?.slots?.contextEngine;
|
|
62
66
|
const pluginsAllow = api.config?.plugins?.allow;
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
);
|
|
67
|
+
let contextEngineRegistered = false;
|
|
68
|
+
let contextEngineRegistrationError = null;
|
|
66
69
|
|
|
67
70
|
// --- Context Engine registration ---
|
|
68
71
|
// When the user sets `plugins.slots.contextEngine: "nowledge-mem"`,
|
|
69
|
-
// this CE takes over from the hooks below (assemble
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
+
// this CE takes over prompt assembly from the hooks below (assemble
|
|
73
|
+
// replaces behavioral + recall prompt injection). Capture hooks remain
|
|
74
|
+
// enabled as a reliability backstop because thread sync is idempotent.
|
|
75
|
+
// When the CE slot points elsewhere, hooks continue working as before.
|
|
72
76
|
try {
|
|
73
77
|
api.registerContextEngine(
|
|
74
78
|
"nowledge-mem",
|
|
75
79
|
createNowledgeMemContextEngineFactory(client, cfg, logger),
|
|
76
80
|
);
|
|
81
|
+
contextEngineRegistered = true;
|
|
77
82
|
} catch (err) {
|
|
78
83
|
// OpenClaw < CE support — degrade gracefully to hooks-only mode
|
|
84
|
+
contextEngineRegistrationError = String(err);
|
|
79
85
|
logger.debug?.(
|
|
80
86
|
`nowledge-mem: context engine registration unavailable (${err}), using hooks`,
|
|
81
87
|
);
|
|
82
88
|
}
|
|
83
89
|
|
|
90
|
+
api.registerTool(
|
|
91
|
+
createStatusTool(client, logger, cfg, {
|
|
92
|
+
memorySlot,
|
|
93
|
+
contextEngineSlot,
|
|
94
|
+
pluginsAllow,
|
|
95
|
+
contextEngineRegistered,
|
|
96
|
+
contextEngineRegistrationError,
|
|
97
|
+
}),
|
|
98
|
+
);
|
|
99
|
+
|
|
84
100
|
// --- Corpus Supplement (when memory-core is the memory slot) ---
|
|
85
101
|
// Makes Nowledge Mem's knowledge graph searchable through memory-core's
|
|
86
102
|
// recall pipeline and dreaming promotion. Memories participate in
|
|
@@ -102,9 +118,10 @@ export default {
|
|
|
102
118
|
}
|
|
103
119
|
}
|
|
104
120
|
|
|
105
|
-
// --- Hooks
|
|
106
|
-
//
|
|
107
|
-
//
|
|
121
|
+
// --- Hooks ---
|
|
122
|
+
// Behavioral + recall hooks defer to the active CE to avoid duplicate
|
|
123
|
+
// prompt injection. Capture hooks remain enabled as a backstop because
|
|
124
|
+
// thread append/create is tail-synced and idempotent.
|
|
108
125
|
|
|
109
126
|
// Always-on: behavioral guidance so the agent proactively saves and searches.
|
|
110
127
|
// Fires every turn via before_prompt_build — ~50 tokens, negligible cost.
|
|
@@ -123,6 +140,8 @@ export default {
|
|
|
123
140
|
cfg,
|
|
124
141
|
logger,
|
|
125
142
|
);
|
|
143
|
+
api.on("session_start", registerSessionStartConversation);
|
|
144
|
+
api.on("session_end", registerSessionEndConversation);
|
|
126
145
|
api.on("agent_end", buildAgentEndCaptureHandler(client, cfg, logger));
|
|
127
146
|
api.on("after_compaction", (event, ctx) =>
|
|
128
147
|
threadCaptureHandler({ ...event, reason: "compaction" }, ctx),
|
package/src/spawn-env.js
CHANGED
|
@@ -3,17 +3,31 @@ const LOCAL_API_URL = "http://127.0.0.1:14242";
|
|
|
3
3
|
/**
|
|
4
4
|
* Build the child env for nmem CLI execution.
|
|
5
5
|
* apiKey stays in env only and is never passed as a CLI flag.
|
|
6
|
-
* Clear inherited
|
|
6
|
+
* Clear inherited API endpoint/auth values first so the resolved plugin config wins.
|
|
7
|
+
* Keep the ambient space lane unless an explicit override is provided. Prefer
|
|
8
|
+
* NMEM_SPACE for the human-facing contract, while preserving legacy
|
|
9
|
+
* NMEM_SPACE_ID for older setups. An explicit empty override means "stay on
|
|
10
|
+
* Default", so inherited ambient space vars must be cleared too.
|
|
7
11
|
*/
|
|
8
|
-
export function buildNmemSpawnEnv(
|
|
12
|
+
export function buildNmemSpawnEnv(
|
|
13
|
+
{ apiUrl, apiKey, spaceId, hasExplicitSpace = false } = {},
|
|
14
|
+
) {
|
|
9
15
|
const env = { ...process.env };
|
|
10
16
|
delete env.NMEM_API_URL;
|
|
11
17
|
delete env.NMEM_API_KEY;
|
|
18
|
+
if (hasExplicitSpace) {
|
|
19
|
+
delete env.NMEM_SPACE;
|
|
20
|
+
delete env.NMEM_SPACE_ID;
|
|
21
|
+
}
|
|
12
22
|
if (apiUrl && apiUrl !== LOCAL_API_URL) {
|
|
13
23
|
env.NMEM_API_URL = apiUrl;
|
|
14
24
|
}
|
|
15
25
|
if (apiKey) {
|
|
16
26
|
env.NMEM_API_KEY = apiKey;
|
|
17
27
|
}
|
|
28
|
+
if (hasExplicitSpace && typeof spaceId === "string" && spaceId.length > 0) {
|
|
29
|
+
env.NMEM_SPACE = spaceId;
|
|
30
|
+
env.NMEM_SPACE_ID = spaceId;
|
|
31
|
+
}
|
|
18
32
|
return env;
|
|
19
33
|
}
|
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
|
-
|
|
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;
|
|
71
|
+
details.memorySlot = memorySlot ?? "(unknown)";
|
|
72
|
+
const ceSlot =
|
|
73
|
+
typeof contextEngineSlot === "string" && contextEngineSlot.trim()
|
|
74
|
+
? 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,17 +103,61 @@ 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
|
+
} else {
|
|
138
|
+
lines.push(
|
|
139
|
+
" Thread identity follows OpenClaw session lifecycle: /new or /reset starts a fresh thread, while compaction stays in the same thread.",
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
93
143
|
// 1. Mode + connection target
|
|
94
144
|
const remote = !isDefaultApiUrl(cfg.apiUrl);
|
|
95
145
|
const mode = remote ? "remote" : "local";
|
|
96
146
|
details.mode = mode;
|
|
97
147
|
details.apiUrl = cfg.apiUrl || "http://127.0.0.1:14242";
|
|
98
148
|
details.apiKeySet = Boolean(cfg.apiKey);
|
|
149
|
+
details.space = cfg.space || null;
|
|
150
|
+
details.spaceSource = sources.space || "default";
|
|
99
151
|
|
|
100
152
|
lines.push(
|
|
101
153
|
`Mode: ${remote ? `Remote (${cfg.apiUrl})` : "Local (127.0.0.1:14242)"}`,
|
|
102
154
|
);
|
|
103
155
|
lines.push(`API key: ${cfg.apiKey ? "set" : "not set"}`);
|
|
156
|
+
lines.push(
|
|
157
|
+
`Ambient space: ${cfg.space ? `${cfg.space} (${details.spaceSource})` : "Default"}`,
|
|
158
|
+
);
|
|
159
|
+
lines.push("Memory tools: nmem CLI");
|
|
160
|
+
lines.push("Thread sync: Mem HTTP API");
|
|
104
161
|
|
|
105
162
|
// 2. CLI resolution
|
|
106
163
|
try {
|
|
@@ -156,6 +213,9 @@ export function createStatusTool(client, _logger, cfg, runtimeInfo = {}) {
|
|
|
156
213
|
} else {
|
|
157
214
|
lines.push(" Ensure the Nowledge Mem desktop app is running.");
|
|
158
215
|
}
|
|
216
|
+
lines.push(
|
|
217
|
+
" Memory tools may still work through the CLI, but thread auto-sync will fail while the HTTP API is unreachable.",
|
|
218
|
+
);
|
|
159
219
|
}
|
|
160
220
|
|
|
161
221
|
// 4. Effective config with sources
|
|
@@ -179,6 +239,9 @@ export function createStatusTool(client, _logger, cfg, runtimeInfo = {}) {
|
|
|
179
239
|
lines.push(
|
|
180
240
|
` maxThreadMessageChars: ${cfg.maxThreadMessageChars} (${sources.maxThreadMessageChars || "?"})`,
|
|
181
241
|
);
|
|
242
|
+
lines.push(
|
|
243
|
+
` corpusSupplement: ${cfg.corpusSupplement} (${sources.corpusSupplement || "?"})`,
|
|
244
|
+
);
|
|
182
245
|
|
|
183
246
|
details.config = {
|
|
184
247
|
sessionContext: {
|
|
@@ -205,6 +268,10 @@ export function createStatusTool(client, _logger, cfg, runtimeInfo = {}) {
|
|
|
205
268
|
value: cfg.maxThreadMessageChars,
|
|
206
269
|
source: sources.maxThreadMessageChars,
|
|
207
270
|
},
|
|
271
|
+
corpusSupplement: {
|
|
272
|
+
value: cfg.corpusSupplement,
|
|
273
|
+
source: sources.corpusSupplement,
|
|
274
|
+
},
|
|
208
275
|
apiUrl: { value: cfg.apiUrl || "(local)", source: sources.apiUrl },
|
|
209
276
|
apiKey: {
|
|
210
277
|
value: cfg.apiKey ? "(set)" : "(not set)",
|