@nowledge/openclaw-nowledge-mem 0.7.3 → 0.8.3
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 +50 -0
- package/CLAUDE.md +27 -1
- package/README.md +37 -3
- package/SKILL.md +4 -11
- package/openclaw.plugin.json +44 -7
- package/package.json +19 -3
- package/src/client.js +4 -2
- package/src/config.js +52 -0
- package/src/context-engine.js +9 -2
- package/src/corpus-supplement.js +167 -0
- package/src/hooks/behavioral.js +4 -7
- package/src/hooks/capture.js +31 -2
- package/src/hooks/recall.js +26 -19
- package/src/index.js +23 -1
- package/src/spawn-env.js +2 -2
- package/src/tools/memory-get.js +6 -2
- package/src/tools/status.js +23 -10
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,56 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the Nowledge Mem OpenClaw plugin will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.8.3] - 2026-04-08
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added a release validator and ClawHub publish runbook so package metadata, manifest defaults, and publish-time expectations can be checked before submission.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Added the external code-plugin metadata ClawHub expects in `package.json` and aligned the manifest-facing package description for official catalog review.
|
|
14
|
+
- Synced the checked-in `package-lock.json` package version with the actual plugin version so pack and publish surfaces stay coherent.
|
|
15
|
+
- 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.
|
|
16
|
+
- Updated install guidance to use ClawHub as the primary path while keeping bare package names documented as a resolver fallback.
|
|
17
|
+
|
|
18
|
+
## [0.8.2] - 2026-04-08
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- **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.
|
|
23
|
+
|
|
24
|
+
## [0.8.1] - 2026-04-07
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- **Thread provenance linking now handles API response shape.** The backend returns `source_thread` as an `{id, title}` object, but `_normalizeMemory` and `memory-get.js` assigned it directly to `sourceThreadId`, producing `[object Object]` instead of a usable thread ID. Both paths now extract the `.id` string, with fallbacks for the CLI string format and metadata `source_thread_id`.
|
|
29
|
+
- **Child process env no longer leaks `"undefined"` strings.** `spawn-env.js` set `NMEM_API_URL` and `NMEM_API_KEY` to JavaScript `undefined` to clear inherited values, but Node's `child_process` stringifies that to the literal `"undefined"`. Now uses `delete` to properly remove the keys.
|
|
30
|
+
|
|
31
|
+
## [0.8.0] - 2026-04-07
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- **Cron and isolated-agent runs no longer sync into Mem threads by default.** OpenClaw schedules these as `agent:<agentId>:cron:...` (for example the `cron-worker` agent). They were previously captured like normal chat because `captureExclude` defaulted to empty and segment-based globs do not cover variable-depth keys. The plugin now calls OpenClaw's published `isCronSessionKey` from `openclaw/plugin-sdk/routing` (available since v2026.3.22, no duplicated parsing logic), also skips bare `cron:*` internal keys that the SDK does not classify, and applies this in hook handlers and Context Engine `afterTurn` (thread append + distillation).
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
- **Corpus supplement for OpenClaw dreaming.** Nowledge Mem's knowledge graph now participates in memory-core's recall pipeline and dreaming promotion. When `corpusSupplement: true` is set, memories stored in Nowledge Mem are searchable through memory-core's native `memory_search` tool and its three-phase dreaming system (light, deep, REM). Recalled Nowledge Mem content accumulates frequency, relevance, and diversity scores that feed into deep-phase promotion decisions. Cross-tool knowledge organically strengthens OpenClaw's local workspace memory.
|
|
41
|
+
|
|
42
|
+
Enable in your OpenClaw config:
|
|
43
|
+
```json
|
|
44
|
+
{ "corpusSupplement": true }
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Three new config keys control the supplement: `corpusSupplement` (boolean, default false), `corpusMaxResults` (1-20, default 5), `corpusMinScore` (0-100%, default 0).
|
|
48
|
+
|
|
49
|
+
- **Duplicate recall prevention.** When the corpus supplement is active, the plugin's own search-based recall (in the hook and Context Engine) is automatically disabled. Working Memory injection continues as before. This prevents the same memories from appearing twice in the agent's context.
|
|
50
|
+
|
|
51
|
+
### Changed
|
|
52
|
+
|
|
53
|
+
- **Minimum OpenClaw version bumped to `>=2026.4.5`** for `registerMemoryCorpusSupplement` API support. Graceful fallback for older versions (registration silently skipped).
|
|
54
|
+
|
|
5
55
|
## [0.7.3] - 2026-03-30
|
|
6
56
|
|
|
7
57
|
### Added
|
package/CLAUDE.md
CHANGED
|
@@ -7,7 +7,7 @@ Continuation guide for `community/nowledge-mem-openclaw-plugin`.
|
|
|
7
7
|
- Plugin target: OpenClaw plugin runtime (memory slot + context engine)
|
|
8
8
|
- Runtime: JS ESM modules under `src/`, no TS build pipeline
|
|
9
9
|
- Memory backend: `nmem` CLI (fallback: `uvx --from nmem-cli nmem`)
|
|
10
|
-
- OpenClaw minimum: `2026.
|
|
10
|
+
- OpenClaw minimum: `2026.4.5` (`registerMemoryCorpusSupplement` for dreaming integration)
|
|
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.
|
|
@@ -34,6 +34,7 @@ src/
|
|
|
34
34
|
client.js - CLI wrapper with API fallback; async runtime command execution; credential handling
|
|
35
35
|
spawn-env.js - env-only credential injection for the nmem runner
|
|
36
36
|
config.js - config cascade: openclaw.json (legacy) > pluginConfig > config.json (credentials) > env > defaults
|
|
37
|
+
corpus-supplement.js - MemoryCorpusSupplement for memory-core dreaming pipeline; search + get backed by nmem
|
|
37
38
|
hooks/
|
|
38
39
|
behavioral.js - always-on behavioral guidance (~50 tokens/turn); no-ops when CE active
|
|
39
40
|
recall.js - before_prompt_build: inject Working Memory + recalled memories; no-ops when CE active
|
|
@@ -56,6 +57,28 @@ openclaw.plugin.json - manifest + config schema (version, uiHints, configSchema,
|
|
|
56
57
|
~/.nowledge-mem/config.json - shared credentials (apiUrl/apiKey) read by all Nowledge Mem tools
|
|
57
58
|
```
|
|
58
59
|
|
|
60
|
+
## Corpus Supplement (Dreaming Integration)
|
|
61
|
+
|
|
62
|
+
When `corpusSupplement: true`, the plugin registers a `MemoryCorpusSupplement` via `api.registerMemoryCorpusSupplement()`. This makes Nowledge Mem's knowledge graph searchable through memory-core's recall pipeline, including its three-phase dreaming system (light, deep, REM).
|
|
63
|
+
|
|
64
|
+
### How it works
|
|
65
|
+
|
|
66
|
+
1. **Recall**: When memory-core's `memory_search` runs, it fans out to all registered corpus supplements. Our supplement calls `client.search()` and maps results to `MemoryCorpusSearchResult` format.
|
|
67
|
+
2. **Dreaming**: Recalled Nowledge Mem content accumulates frequency, relevance, and diversity scores in memory-core's short-term recall store. High-scoring content is promoted to `MEMORY.md` during deep-phase dreaming.
|
|
68
|
+
3. **Dedup**: When active, the recall hook and CE `assemble()` skip their own search-based recall (memories section only). Working Memory injection continues as before.
|
|
69
|
+
|
|
70
|
+
### When to enable
|
|
71
|
+
|
|
72
|
+
Enable when memory-core is the memory slot and you want cross-tool knowledge to participate in OpenClaw's native recall and dreaming. Most users have Nowledge Mem as the memory slot (default `false`).
|
|
73
|
+
|
|
74
|
+
### Config keys
|
|
75
|
+
|
|
76
|
+
| Key | Type | Default | Env Var | Description |
|
|
77
|
+
|-----|------|---------|---------|-------------|
|
|
78
|
+
| `corpusSupplement` | boolean | `false` | `NMEM_CORPUS_SUPPLEMENT` | Register as corpus supplement |
|
|
79
|
+
| `corpusMaxResults` | integer 1-20 | `5` | `NMEM_CORPUS_MAX_RESULTS` | Max results per search call |
|
|
80
|
+
| `corpusMinScore` | integer 0-100 | `0` | `NMEM_CORPUS_MIN_SCORE` | Min score filter (0 = include all) |
|
|
81
|
+
|
|
59
82
|
## Context Engine (CE) Architecture
|
|
60
83
|
|
|
61
84
|
The plugin registers both a **memory slot** (`kind: "memory"`) and a **context engine** (`api.registerContextEngine`). These are independent registrations:
|
|
@@ -153,6 +176,9 @@ Credentials (apiUrl/apiKey): also reads `~/.nowledge-mem/config.json` (shared wi
|
|
|
153
176
|
| `maxThreadMessageChars` | integer 200-20000 | `800` | `NMEM_MAX_THREAD_MESSAGE_CHARS` | Max chars per captured thread message before truncation |
|
|
154
177
|
| `captureExclude` | string[] | `[]` | — | Session key glob patterns to skip during auto-capture. `*` matches within a colon-segment. Example: `["agent:*:cron:*"]` |
|
|
155
178
|
| `captureSkipMarker` | string | `"#nmem-skip"` | — | In-band marker: any message containing this text skips capture for the session. Not sticky across compaction |
|
|
179
|
+
| `corpusSupplement` | boolean | `false` | `NMEM_CORPUS_SUPPLEMENT` | Register as MemoryCorpusSupplement for memory-core recall + dreaming |
|
|
180
|
+
| `corpusMaxResults` | integer 1-20 | `5` | `NMEM_CORPUS_MAX_RESULTS` | Max results per corpus supplement search |
|
|
181
|
+
| `corpusMinScore` | integer 0-100 | `0` | `NMEM_CORPUS_MIN_SCORE` | Min score for supplement results (0 = all) |
|
|
156
182
|
| `apiUrl` | string | `""` | `NMEM_API_URL` | Remote server URL. Empty = local (127.0.0.1:14242) |
|
|
157
183
|
| `apiKey` | string | `""` | `NMEM_API_KEY` | API key. Never logged. |
|
|
158
184
|
|
package/README.md
CHANGED
|
@@ -9,16 +9,30 @@ This is not a flat note store. Nowledge Mem links related knowledge into a graph
|
|
|
9
9
|
## Requirements
|
|
10
10
|
|
|
11
11
|
- [Nowledge Mem](https://mem.nowledge.co) desktop app **or** `nmem` CLI
|
|
12
|
-
- [OpenClaw](https://openclaw.ai) >= 2026.3.7
|
|
12
|
+
- [OpenClaw](https://openclaw.ai) >= 2026.4.5 (corpus supplement requires this version; core features work on >= 2026.3.7)
|
|
13
13
|
|
|
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).
|
|
@@ -187,6 +201,20 @@ Two entry points:
|
|
|
187
201
|
1. **From a memory**: `memory_search` or `memory_get` returns `sourceThreadId`, then fetch the source conversation
|
|
188
202
|
2. **Direct search**: `nowledge_mem_thread_search` finds conversations by keyword, then fetch any result
|
|
189
203
|
|
|
204
|
+
### Corpus Supplement (Dreaming Integration)
|
|
205
|
+
|
|
206
|
+
When OpenClaw's memory-core is the primary memory slot and Nowledge Mem runs alongside it, you can enable `corpusSupplement: true`. This registers Nowledge Mem as a searchable corpus inside memory-core's recall pipeline and three-phase dreaming system (light, deep, REM).
|
|
207
|
+
|
|
208
|
+
**What happens:**
|
|
209
|
+
- Every time memory-core runs `memory_search`, it also searches Nowledge Mem's knowledge graph via the supplement.
|
|
210
|
+
- Recalled Nowledge Mem content accumulates frequency and relevance scores in memory-core's short-term store.
|
|
211
|
+
- High-scoring content is promoted to `MEMORY.md` during deep-phase dreaming (daily, 3 AM).
|
|
212
|
+
- The plugin's own search-based recall is automatically disabled to avoid duplicates. Working Memory injection continues as before.
|
|
213
|
+
|
|
214
|
+
**When to enable:** When memory-core is the memory slot and you want your cross-tool knowledge graph to participate in OpenClaw's native dreaming cycle. Most users have Nowledge Mem as the memory slot directly, so this defaults to `false`.
|
|
215
|
+
|
|
216
|
+
Requires OpenClaw >= 2026.4.5.
|
|
217
|
+
|
|
190
218
|
### Three Modes at a Glance
|
|
191
219
|
|
|
192
220
|
```mermaid
|
|
@@ -372,6 +400,9 @@ To change settings, use the OpenClaw plugin settings UI. Changes take effect on
|
|
|
372
400
|
| `maxContextResults` | integer | `5` | Max memories to inject at prompt time (1-20, only used when sessionContext is enabled) |
|
|
373
401
|
| `recallMinScore` | integer | `0` | Min relevance score (0-100%) to include in auto-recall. 0 = include all |
|
|
374
402
|
| `maxThreadMessageChars` | integer | `800` | Maximum characters preserved per captured OpenClaw thread message before truncation |
|
|
403
|
+
| `corpusSupplement` | boolean | `false` | Register as a searchable corpus inside memory-core's recall and dreaming pipeline |
|
|
404
|
+
| `corpusMaxResults` | integer | `5` | Max results per corpus supplement search (1-20) |
|
|
405
|
+
| `corpusMinScore` | integer | `0` | Min relevance score (0-100%) for supplement results. 0 = include all |
|
|
375
406
|
| `apiUrl` | string | `""` | Remote server URL. Empty = local (`http://127.0.0.1:14242`) |
|
|
376
407
|
| `apiKey` | string | `""` | API key for remote access. Injected as `NMEM_API_KEY` env var, never logged |
|
|
377
408
|
|
|
@@ -399,6 +430,9 @@ NMEM_DIGEST_MIN_INTERVAL=300
|
|
|
399
430
|
NMEM_MAX_CONTEXT_RESULTS=5
|
|
400
431
|
NMEM_RECALL_MIN_SCORE=0
|
|
401
432
|
NMEM_MAX_THREAD_MESSAGE_CHARS=800
|
|
433
|
+
NMEM_CORPUS_SUPPLEMENT=false
|
|
434
|
+
NMEM_CORPUS_MAX_RESULTS=5
|
|
435
|
+
NMEM_CORPUS_MIN_SCORE=0
|
|
402
436
|
NMEM_API_URL=https://...
|
|
403
437
|
NMEM_API_KEY=your-key
|
|
404
438
|
```
|
|
@@ -440,7 +474,7 @@ If you also use linked or workspace copies, review `plugins.load.paths` before r
|
|
|
440
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:
|
|
441
475
|
|
|
442
476
|
```bash
|
|
443
|
-
openclaw plugins install
|
|
477
|
+
openclaw plugins install clawhub:@nowledge/openclaw-nowledge-mem
|
|
444
478
|
```
|
|
445
479
|
|
|
446
480
|
Or set the slot manually in `~/.openclaw/openclaw.json`:
|
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,8 +1,12 @@
|
|
|
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.3",
|
|
4
6
|
"kind": "memory",
|
|
5
|
-
"skills": [
|
|
7
|
+
"skills": [
|
|
8
|
+
"skills/memory-guide"
|
|
9
|
+
],
|
|
6
10
|
"uiHints": {
|
|
7
11
|
"sessionContext": {
|
|
8
12
|
"label": "Session context injection",
|
|
@@ -30,11 +34,23 @@
|
|
|
30
34
|
},
|
|
31
35
|
"captureExclude": {
|
|
32
36
|
"label": "Exclude session patterns",
|
|
33
|
-
"help": "
|
|
37
|
+
"help": "Extra session key glob patterns to skip during auto-capture (* = one colon segment). OpenClaw cron / isolated automation sessions are always excluded using the host's own session classification\u2014no patterns required. Use this list for subagents or other sessions you want to filter."
|
|
34
38
|
},
|
|
35
39
|
"captureSkipMarker": {
|
|
36
40
|
"label": "Skip marker",
|
|
37
|
-
"help": "Text marker in messages that prevents capture for the session. Default: #nmem-skip. Note: if compaction drops the marked message, capture resumes
|
|
41
|
+
"help": "Text marker in messages that prevents capture for the session. Default: #nmem-skip. Note: if compaction drops the marked message, capture resumes \u2014 use captureExclude patterns for persistent filtering."
|
|
42
|
+
},
|
|
43
|
+
"corpusSupplement": {
|
|
44
|
+
"label": "Register as corpus supplement",
|
|
45
|
+
"help": "Make Nowledge Mem searchable through memory-core's recall pipeline and dreaming promotion. Enable when memory-core is your memory slot and you want cross-tool knowledge to participate in OpenClaw's native recall and dreaming. When enabled, the plugin's own search-based recall is skipped to avoid duplicates (Working Memory injection continues)."
|
|
46
|
+
},
|
|
47
|
+
"corpusMaxResults": {
|
|
48
|
+
"label": "Corpus supplement max results",
|
|
49
|
+
"help": "Maximum results returned per corpus supplement search call (1-20). Only used when corpusSupplement is enabled."
|
|
50
|
+
},
|
|
51
|
+
"corpusMinScore": {
|
|
52
|
+
"label": "Corpus supplement min score (%)",
|
|
53
|
+
"help": "Minimum relevance score (0-100%) for corpus supplement results. Set to 0 to include all. Only used when corpusSupplement is enabled."
|
|
38
54
|
},
|
|
39
55
|
"apiUrl": {
|
|
40
56
|
"label": "Server URL (remote mode)",
|
|
@@ -89,14 +105,35 @@
|
|
|
89
105
|
},
|
|
90
106
|
"captureExclude": {
|
|
91
107
|
"type": "array",
|
|
92
|
-
"items": {
|
|
108
|
+
"items": {
|
|
109
|
+
"type": "string"
|
|
110
|
+
},
|
|
93
111
|
"default": [],
|
|
94
|
-
"description": "
|
|
112
|
+
"description": "Additional glob patterns to exclude from auto-capture (* = one colon segment). Cron and isolated automation are always excluded via OpenClaw's isCronSessionKey (plugin-sdk/routing) plus bare cron:* internal keys."
|
|
95
113
|
},
|
|
96
114
|
"captureSkipMarker": {
|
|
97
115
|
"type": "string",
|
|
98
116
|
"default": "#nmem-skip",
|
|
99
|
-
"description": "Text marker in messages that prevents capture for the entire session. When any message contains this marker, the session is skipped. Note: compaction may drop the marked message
|
|
117
|
+
"description": "Text marker in messages that prevents capture for the entire session. When any message contains this marker, the session is skipped. Note: compaction may drop the marked message \u2014 use captureExclude for persistent filtering."
|
|
118
|
+
},
|
|
119
|
+
"corpusSupplement": {
|
|
120
|
+
"type": "boolean",
|
|
121
|
+
"default": false,
|
|
122
|
+
"description": "Register Nowledge Mem as a MemoryCorpusSupplement for memory-core's recall pipeline and dreaming. Enable when memory-core is your memory slot."
|
|
123
|
+
},
|
|
124
|
+
"corpusMaxResults": {
|
|
125
|
+
"type": "integer",
|
|
126
|
+
"default": 5,
|
|
127
|
+
"minimum": 1,
|
|
128
|
+
"maximum": 20,
|
|
129
|
+
"description": "Maximum results per corpus supplement search call (only used when corpusSupplement is enabled)"
|
|
130
|
+
},
|
|
131
|
+
"corpusMinScore": {
|
|
132
|
+
"type": "integer",
|
|
133
|
+
"default": 0,
|
|
134
|
+
"minimum": 0,
|
|
135
|
+
"maximum": 100,
|
|
136
|
+
"description": "Minimum relevance score (0-100%) for corpus supplement results (only used when corpusSupplement is enabled)"
|
|
100
137
|
},
|
|
101
138
|
"apiUrl": {
|
|
102
139
|
"type": "string",
|
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.3",
|
|
4
5
|
"type": "module",
|
|
5
6
|
"description": "Nowledge Mem memory plugin for OpenClaw, local-first personal knowledge base",
|
|
6
7
|
"author": {
|
|
@@ -24,15 +25,30 @@
|
|
|
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
|
+
"minHostVersion": ">=2026.4.5"
|
|
32
|
+
},
|
|
33
|
+
"compat": {
|
|
34
|
+
"pluginApi": ">=2026.4.5"
|
|
35
|
+
},
|
|
36
|
+
"build": {
|
|
37
|
+
"openclawVersion": "2026.4.5"
|
|
38
|
+
},
|
|
39
|
+
"release": {
|
|
40
|
+
"publishToClawHub": true,
|
|
41
|
+
"publishToNpm": true
|
|
42
|
+
}
|
|
28
43
|
},
|
|
29
44
|
"main": "src/index.js",
|
|
30
45
|
"scripts": {
|
|
46
|
+
"validate": "node scripts/validate-plugin.mjs",
|
|
31
47
|
"lint": "biome check .",
|
|
32
48
|
"format": "biome format --write ."
|
|
33
49
|
},
|
|
34
50
|
"peerDependencies": {
|
|
35
|
-
"openclaw": ">=2026.
|
|
51
|
+
"openclaw": ">=2026.4.5"
|
|
36
52
|
},
|
|
37
53
|
"devDependencies": {
|
|
38
54
|
"@biomejs/biome": "^1.9.0"
|
package/src/client.js
CHANGED
|
@@ -396,9 +396,11 @@ export class NowledgeMemClient {
|
|
|
396
396
|
eventEnd: m.event_end ?? null,
|
|
397
397
|
temporalContext: m.temporal_context ?? null,
|
|
398
398
|
// Thread provenance: links memory to the conversation it was distilled from.
|
|
399
|
-
// CLI returns `source_thread
|
|
399
|
+
// CLI returns `source_thread` (string), API returns `source_thread: {id, title}` (object).
|
|
400
400
|
sourceThreadId:
|
|
401
|
-
m.source_thread
|
|
401
|
+
(typeof m.source_thread === "object" && m.source_thread?.id
|
|
402
|
+
? String(m.source_thread.id)
|
|
403
|
+
: typeof m.source_thread === "string" ? m.source_thread : null) ??
|
|
402
404
|
m.source_thread_id ??
|
|
403
405
|
m.metadata?.source_thread_id ??
|
|
404
406
|
null,
|
package/src/config.js
CHANGED
|
@@ -23,6 +23,9 @@ const ALLOWED_KEYS = new Set([
|
|
|
23
23
|
"maxThreadMessageChars",
|
|
24
24
|
"captureExclude",
|
|
25
25
|
"captureSkipMarker",
|
|
26
|
+
"corpusSupplement",
|
|
27
|
+
"corpusMaxResults",
|
|
28
|
+
"corpusMinScore",
|
|
26
29
|
"apiUrl",
|
|
27
30
|
"apiKey",
|
|
28
31
|
// Legacy aliases — accepted but not advertised
|
|
@@ -197,6 +200,9 @@ function firstDefined(...options) {
|
|
|
197
200
|
* NMEM_MAX_CONTEXT_RESULTS — integer (1-20)
|
|
198
201
|
* NMEM_RECALL_MIN_SCORE — integer (0-100)
|
|
199
202
|
* NMEM_MAX_THREAD_MESSAGE_CHARS — integer (200-20000)
|
|
203
|
+
* NMEM_CORPUS_SUPPLEMENT — true/1/yes to register as MemoryCorpusSupplement
|
|
204
|
+
* NMEM_CORPUS_MAX_RESULTS — integer (1-20)
|
|
205
|
+
* NMEM_CORPUS_MIN_SCORE — integer (0-100)
|
|
200
206
|
* NMEM_API_URL — remote server URL
|
|
201
207
|
* NMEM_API_KEY — API key (never logged)
|
|
202
208
|
*/
|
|
@@ -345,6 +351,49 @@ export function parseConfig(raw, logger) {
|
|
|
345
351
|
const apiKey = ak.value;
|
|
346
352
|
_sources.apiKey = ak.source;
|
|
347
353
|
|
|
354
|
+
// --- corpusSupplement: file > pluginConfig > env > default ---
|
|
355
|
+
const cs = firstDefined(
|
|
356
|
+
{ value: pickBool(resolvedFile, "corpusSupplement"), source: "file" },
|
|
357
|
+
{
|
|
358
|
+
value: pickBool(resolvedPlugin, "corpusSupplement"),
|
|
359
|
+
source: "pluginConfig",
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
value: envBool("NMEM_CORPUS_SUPPLEMENT"),
|
|
363
|
+
source: "env",
|
|
364
|
+
},
|
|
365
|
+
{ value: false, source: "default" },
|
|
366
|
+
);
|
|
367
|
+
const corpusSupplement = cs.value;
|
|
368
|
+
_sources.corpusSupplement = cs.source;
|
|
369
|
+
|
|
370
|
+
// --- corpusMaxResults: file > pluginConfig > env > default ---
|
|
371
|
+
const cmr = firstDefined(
|
|
372
|
+
{ value: pickNum(resolvedFile, "corpusMaxResults"), source: "file" },
|
|
373
|
+
{
|
|
374
|
+
value: pickNum(resolvedPlugin, "corpusMaxResults"),
|
|
375
|
+
source: "pluginConfig",
|
|
376
|
+
},
|
|
377
|
+
{ value: envInt("NMEM_CORPUS_MAX_RESULTS"), source: "env" },
|
|
378
|
+
{ value: 5, source: "default" },
|
|
379
|
+
);
|
|
380
|
+
const corpusMaxResults = Math.min(20, Math.max(1, Math.trunc(cmr.value)));
|
|
381
|
+
_sources.corpusMaxResults = cmr.source;
|
|
382
|
+
|
|
383
|
+
// --- corpusMinScore: file > pluginConfig > env > default ---
|
|
384
|
+
const cmsEnv = envInt("NMEM_CORPUS_MIN_SCORE");
|
|
385
|
+
const cms = firstDefined(
|
|
386
|
+
{ value: pickNum(resolvedFile, "corpusMinScore"), source: "file" },
|
|
387
|
+
{
|
|
388
|
+
value: pickNum(resolvedPlugin, "corpusMinScore"),
|
|
389
|
+
source: "pluginConfig",
|
|
390
|
+
},
|
|
391
|
+
{ value: cmsEnv, source: "env" },
|
|
392
|
+
{ value: 0, source: "default" },
|
|
393
|
+
);
|
|
394
|
+
const corpusMinScore = Math.min(100, Math.max(0, Math.trunc(cms.value)));
|
|
395
|
+
_sources.corpusMinScore = cms.source;
|
|
396
|
+
|
|
348
397
|
// --- captureExclude: file > pluginConfig > default ---
|
|
349
398
|
const captureExclude = (() => {
|
|
350
399
|
const fromFile = Array.isArray(resolvedFile.captureExclude)
|
|
@@ -391,6 +440,9 @@ export function parseConfig(raw, logger) {
|
|
|
391
440
|
maxThreadMessageChars,
|
|
392
441
|
captureExclude,
|
|
393
442
|
captureSkipMarker,
|
|
443
|
+
corpusSupplement,
|
|
444
|
+
corpusMaxResults,
|
|
445
|
+
corpusMinScore,
|
|
394
446
|
apiUrl,
|
|
395
447
|
apiKey,
|
|
396
448
|
_sources,
|
package/src/context-engine.js
CHANGED
|
@@ -25,6 +25,7 @@ import { BASE_GUIDANCE, SESSION_CONTEXT_GUIDANCE } from "./hooks/behavioral.js";
|
|
|
25
25
|
import {
|
|
26
26
|
appendOrCreateThread,
|
|
27
27
|
hasSkipMarker,
|
|
28
|
+
isCronCaptureSessionKey,
|
|
28
29
|
matchesExcludePattern,
|
|
29
30
|
triageAndDistill,
|
|
30
31
|
} from "./hooks/capture.js";
|
|
@@ -170,7 +171,7 @@ export function createNowledgeMemContextEngineFactory(client, cfg, logger) {
|
|
|
170
171
|
info: {
|
|
171
172
|
id: "nowledge-mem",
|
|
172
173
|
name: "Nowledge Mem",
|
|
173
|
-
version: "0.
|
|
174
|
+
version: "0.8.0",
|
|
174
175
|
ownsCompaction: false,
|
|
175
176
|
},
|
|
176
177
|
|
|
@@ -235,7 +236,8 @@ export function createNowledgeMemContextEngineFactory(client, cfg, logger) {
|
|
|
235
236
|
}
|
|
236
237
|
|
|
237
238
|
// 3. Recalled memories (when sessionContext enabled)
|
|
238
|
-
|
|
239
|
+
// Skipped when corpus supplement handles search-based recall via memory-core.
|
|
240
|
+
if (cfg.sessionContext && !cfg.corpusSupplement) {
|
|
239
241
|
const query = buildAssembleSearchQuery(prompt, messages);
|
|
240
242
|
if (query) {
|
|
241
243
|
try {
|
|
@@ -350,6 +352,11 @@ export function createNowledgeMemContextEngineFactory(client, cfg, logger) {
|
|
|
350
352
|
// Normalize sessionKey consistently with hook handlers
|
|
351
353
|
const normalizedKey = String(sessionKey || sessionId || "");
|
|
352
354
|
|
|
355
|
+
if (isCronCaptureSessionKey(normalizedKey)) {
|
|
356
|
+
logger.debug?.(`ce: skipped cron session ${normalizedKey}`);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
353
360
|
// Capture exclusion: pattern-based and marker-based filters
|
|
354
361
|
if (matchesExcludePattern(normalizedKey, cfg.captureExclude)) {
|
|
355
362
|
logger.debug?.(`ce: skipped excluded session ${normalizedKey}`);
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MemoryCorpusSupplement implementation for OpenClaw's memory-core recall pipeline.
|
|
3
|
+
*
|
|
4
|
+
* When registered, Nowledge Mem's knowledge graph becomes searchable through
|
|
5
|
+
* memory-core's native `memory_search` tool and its dreaming promotion pipeline.
|
|
6
|
+
* This means memories stored in Nowledge Mem participate in OpenClaw's recall
|
|
7
|
+
* scoring, temporal decay, and short-term-to-long-term promotion — without the
|
|
8
|
+
* user needing to explicitly call Nowledge Mem tools.
|
|
9
|
+
*
|
|
10
|
+
* Enable via config: `corpusSupplement: true`
|
|
11
|
+
*
|
|
12
|
+
* Designed to coexist with the plugin's own tools and hooks:
|
|
13
|
+
* - Working Memory injection: still handled by the recall hook / CE (only we know about WM)
|
|
14
|
+
* - Search-based recall: handled by this supplement when active, by hooks/CE when not
|
|
15
|
+
*
|
|
16
|
+
* Error handling: both search() and get() catch all errors and return [] / null.
|
|
17
|
+
* Supplement failures must never break OpenClaw's recall pipeline.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** Max snippet length in search results (matches memory-search.js truncation). */
|
|
21
|
+
const SNIPPET_MAX_CHARS = 700;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parse a memory ID from a path or bare ID string.
|
|
25
|
+
* Handles `nowledgemem://memory/<id>` URIs and bare alphanumeric IDs.
|
|
26
|
+
*/
|
|
27
|
+
function parseMemoryId(pathValue) {
|
|
28
|
+
const value = String(pathValue || "").trim();
|
|
29
|
+
if (!value) return null;
|
|
30
|
+
|
|
31
|
+
const fromDeeplink = value.match(
|
|
32
|
+
/^nowledgemem:\/\/memory\/([a-zA-Z0-9_-]+)$/u,
|
|
33
|
+
);
|
|
34
|
+
if (fromDeeplink) return fromDeeplink[1];
|
|
35
|
+
|
|
36
|
+
if (/^[a-zA-Z0-9_-]{8,}$/u.test(value)) {
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Slice lines from text content with 1-based line indexing.
|
|
45
|
+
*/
|
|
46
|
+
function sliceLines(text, from, lines) {
|
|
47
|
+
const allLines = String(text || "").split(/\r?\n/u);
|
|
48
|
+
const start = Math.max(1, Math.trunc(Number(from) || 1));
|
|
49
|
+
const maxLines = Math.max(1, Math.trunc(Number(lines) || allLines.length));
|
|
50
|
+
const startIdx = start - 1;
|
|
51
|
+
const endIdx = Math.min(allLines.length, startIdx + maxLines);
|
|
52
|
+
const selected = allLines.slice(startIdx, endIdx);
|
|
53
|
+
return {
|
|
54
|
+
text: selected.join("\n"),
|
|
55
|
+
startLine: start,
|
|
56
|
+
endLine: Math.max(start, start + selected.length - 1),
|
|
57
|
+
totalLines: allLines.length,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Create a MemoryCorpusSupplement backed by the Nowledge Mem knowledge graph.
|
|
63
|
+
*
|
|
64
|
+
* @param {import('./client.js').NowledgeMemClient} client
|
|
65
|
+
* @param {object} cfg Parsed plugin config (corpusMaxResults, corpusMinScore)
|
|
66
|
+
* @param {object} logger
|
|
67
|
+
* @returns {{ search: Function, get: Function }}
|
|
68
|
+
*/
|
|
69
|
+
export function createNowledgeMemCorpusSupplement(client, cfg, logger) {
|
|
70
|
+
const maxResults = cfg.corpusMaxResults ?? 5;
|
|
71
|
+
const minScore = (cfg.corpusMinScore ?? 0) / 100; // config is 0-100, API is 0-1
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
/**
|
|
75
|
+
* Search Nowledge Mem's knowledge graph for memories matching the query.
|
|
76
|
+
* Called by memory-core's recall pipeline on every turn.
|
|
77
|
+
*/
|
|
78
|
+
async search({ query, maxResults: requestedMax }) {
|
|
79
|
+
try {
|
|
80
|
+
const limit = Math.min(20, Math.max(1, requestedMax ?? maxResults));
|
|
81
|
+
const results = await client.search(query, limit);
|
|
82
|
+
|
|
83
|
+
const filtered =
|
|
84
|
+
minScore > 0
|
|
85
|
+
? results.filter((r) => (r.score ?? 0) >= minScore)
|
|
86
|
+
: results;
|
|
87
|
+
|
|
88
|
+
return filtered.map((m) => {
|
|
89
|
+
const snippet = String(m.content || "").slice(0, SNIPPET_MAX_CHARS);
|
|
90
|
+
const lineCount = Math.max(1, snippet.split(/\r?\n/u).length);
|
|
91
|
+
const result = {
|
|
92
|
+
corpus: "nowledge-mem",
|
|
93
|
+
path: `nowledgemem://memory/${m.id}`,
|
|
94
|
+
score: Number(m.score ?? 0),
|
|
95
|
+
snippet,
|
|
96
|
+
title: m.title || undefined,
|
|
97
|
+
kind: m.labels?.[0] || undefined,
|
|
98
|
+
id: m.id,
|
|
99
|
+
startLine: 1,
|
|
100
|
+
endLine: lineCount,
|
|
101
|
+
provenanceLabel: "Nowledge Mem",
|
|
102
|
+
source: "nowledge-mem",
|
|
103
|
+
sourceType: "knowledge-graph",
|
|
104
|
+
};
|
|
105
|
+
return result;
|
|
106
|
+
});
|
|
107
|
+
} catch (err) {
|
|
108
|
+
logger.warn?.(
|
|
109
|
+
`corpus-supplement: search failed: ${err instanceof Error ? err.message : err}`,
|
|
110
|
+
);
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Retrieve a specific memory by path or ID.
|
|
117
|
+
* Called when memory-core's agent requests full content for a search result.
|
|
118
|
+
*/
|
|
119
|
+
async get({ lookup, fromLine, lineCount }) {
|
|
120
|
+
try {
|
|
121
|
+
// Handle Working Memory alias
|
|
122
|
+
const lowerLookup = String(lookup || "")
|
|
123
|
+
.trim()
|
|
124
|
+
.toLowerCase();
|
|
125
|
+
if (lowerLookup === "memory.md" || lowerLookup === "memory") {
|
|
126
|
+
const wm = await client.readWorkingMemory();
|
|
127
|
+
if (!wm.available) return null;
|
|
128
|
+
const slice = sliceLines(wm.content, fromLine, lineCount);
|
|
129
|
+
return {
|
|
130
|
+
corpus: "nowledge-mem",
|
|
131
|
+
path: "nowledgemem://working-memory",
|
|
132
|
+
title: "Working Memory",
|
|
133
|
+
content: slice.text,
|
|
134
|
+
fromLine: slice.startLine,
|
|
135
|
+
lineCount: slice.endLine - slice.startLine + 1,
|
|
136
|
+
provenanceLabel: "Nowledge Mem",
|
|
137
|
+
sourceType: "working-memory",
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const memoryId = parseMemoryId(lookup);
|
|
142
|
+
if (!memoryId) return null;
|
|
143
|
+
|
|
144
|
+
const memory = await client.getMemory(memoryId);
|
|
145
|
+
if (!memory) return null;
|
|
146
|
+
const slice = sliceLines(memory.content ?? "", fromLine, lineCount);
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
corpus: "nowledge-mem",
|
|
150
|
+
path: `nowledgemem://memory/${memoryId}`,
|
|
151
|
+
title: memory.title || undefined,
|
|
152
|
+
content: slice.text,
|
|
153
|
+
fromLine: slice.startLine,
|
|
154
|
+
lineCount: slice.endLine - slice.startLine + 1,
|
|
155
|
+
id: memoryId,
|
|
156
|
+
provenanceLabel: "Nowledge Mem",
|
|
157
|
+
sourceType: "knowledge-graph",
|
|
158
|
+
};
|
|
159
|
+
} catch (err) {
|
|
160
|
+
logger.warn?.(
|
|
161
|
+
`corpus-supplement: get failed: ${err instanceof Error ? err.message : err}`,
|
|
162
|
+
);
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}
|
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,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { isCronSessionKey } from "openclaw/plugin-sdk/routing";
|
|
3
4
|
import { ceState } from "../ce-state.js";
|
|
4
5
|
|
|
5
6
|
export const DEFAULT_MAX_MESSAGE_CHARS = 800;
|
|
@@ -78,6 +79,25 @@ export function matchesExcludePattern(sessionKey, patterns) {
|
|
|
78
79
|
return patterns.some((pattern) => _compileGlob(pattern).test(key));
|
|
79
80
|
}
|
|
80
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Whether automatic thread capture should skip this OpenClaw session.
|
|
84
|
+
*
|
|
85
|
+
* - Agent-scoped cron / isolated-agent keys are classified by
|
|
86
|
+
* `isCronSessionKey` from `openclaw/plugin-sdk/routing` (same implementation
|
|
87
|
+
* the gateway uses). Requires OpenClaw >=2026.3.22.
|
|
88
|
+
* - Bare `cron:*` keys are still excluded: some internal paths (e.g. delivery /
|
|
89
|
+
* failure bookkeeping) use that shape without the `agent:` prefix, and
|
|
90
|
+
* `isCronSessionKey` only parses agent-scoped keys.
|
|
91
|
+
*/
|
|
92
|
+
export function isCronCaptureSessionKey(sessionKey) {
|
|
93
|
+
const raw = String(sessionKey || "")
|
|
94
|
+
.trim()
|
|
95
|
+
.toLowerCase();
|
|
96
|
+
if (!raw) return false;
|
|
97
|
+
if (raw.startsWith("cron:")) return true;
|
|
98
|
+
return isCronSessionKey(raw);
|
|
99
|
+
}
|
|
100
|
+
|
|
81
101
|
/**
|
|
82
102
|
* Check if any message contains the skip marker text.
|
|
83
103
|
* Scans both raw message content and nested message objects.
|
|
@@ -471,8 +491,13 @@ export function buildAgentEndCaptureHandler(client, cfg, logger) {
|
|
|
471
491
|
if (!event?.success) return;
|
|
472
492
|
if (ctx?.trigger === "heartbeat") return;
|
|
473
493
|
|
|
474
|
-
// Layer 1: pattern-based exclusion (e.g. cron jobs, subagent sessions)
|
|
475
494
|
const sessionKey = String(ctx?.sessionKey || ctx?.sessionId || "");
|
|
495
|
+
if (isCronCaptureSessionKey(sessionKey)) {
|
|
496
|
+
logger.debug?.(`capture: skipped cron session ${sessionKey}`);
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Pattern-based exclusion (e.g. subagent sessions, custom jobs)
|
|
476
501
|
if (matchesExcludePattern(sessionKey, cfg.captureExclude)) {
|
|
477
502
|
logger.debug?.(`capture: skipped excluded session ${sessionKey}`);
|
|
478
503
|
return;
|
|
@@ -515,8 +540,12 @@ export function buildBeforeResetCaptureHandler(client, cfg, logger) {
|
|
|
515
540
|
if (ceState.active) return;
|
|
516
541
|
if (ctx?.trigger === "heartbeat") return;
|
|
517
542
|
|
|
518
|
-
// Layer 1: pattern-based exclusion
|
|
519
543
|
const sessionKey = String(ctx?.sessionKey || ctx?.sessionId || "");
|
|
544
|
+
if (isCronCaptureSessionKey(sessionKey)) {
|
|
545
|
+
logger.debug?.(`capture: skipped cron session ${sessionKey}`);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
|
|
520
549
|
if (matchesExcludePattern(sessionKey, cfg.captureExclude)) {
|
|
521
550
|
logger.debug?.(`capture: skipped excluded session ${sessionKey}`);
|
|
522
551
|
return;
|
package/src/hooks/recall.js
CHANGED
|
@@ -180,15 +180,18 @@ export function buildRecalledKnowledgeBlock(
|
|
|
180
180
|
export function buildRecallHandler(client, cfg, logger) {
|
|
181
181
|
const minScore = (cfg.recallMinScore ?? 0) / 100; // config is 0-100, API is 0-1
|
|
182
182
|
|
|
183
|
+
// When corpus supplement is active, memory-core's recall pipeline searches
|
|
184
|
+
// Nowledge Mem via the supplement. We still inject Working Memory (only we
|
|
185
|
+
// know about WM), but skip our own search-based recall to avoid duplicates.
|
|
186
|
+
const skipSearchRecall = cfg.corpusSupplement;
|
|
187
|
+
|
|
183
188
|
return async (event) => {
|
|
184
189
|
if (ceState.active) return;
|
|
185
190
|
|
|
186
|
-
const searchQuery = buildSearchQuery(event);
|
|
187
|
-
if (!searchQuery) return;
|
|
188
|
-
|
|
189
191
|
const sections = [];
|
|
190
192
|
|
|
191
|
-
// 1. Working Memory — daily context, not a static profile
|
|
193
|
+
// 1. Working Memory — daily context, not a static profile.
|
|
194
|
+
// Always injected regardless of search query (WM is independent of user message).
|
|
192
195
|
try {
|
|
193
196
|
const wm = await client.readWorkingMemory();
|
|
194
197
|
if (wm.available) {
|
|
@@ -200,22 +203,26 @@ export function buildRecallHandler(client, cfg, logger) {
|
|
|
200
203
|
logger.error(`recall: working memory read failed: ${err}`);
|
|
201
204
|
}
|
|
202
205
|
|
|
203
|
-
// 2. Relevant memories — enriched with scoring signals and labels
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
206
|
+
// 2. Relevant memories — enriched with scoring signals and labels.
|
|
207
|
+
// Skipped when corpus supplement handles search-based recall via memory-core.
|
|
208
|
+
const searchQuery = buildSearchQuery(event);
|
|
209
|
+
if (!skipSearchRecall && searchQuery) {
|
|
210
|
+
try {
|
|
211
|
+
const results = await client.searchRich(
|
|
212
|
+
searchQuery,
|
|
213
|
+
cfg.maxContextResults,
|
|
214
|
+
);
|
|
215
|
+
// Filter by minimum score if configured
|
|
216
|
+
const filtered =
|
|
217
|
+
minScore > 0
|
|
218
|
+
? results.filter((r) => (r.score ?? 0) >= minScore)
|
|
219
|
+
: results;
|
|
220
|
+
if (filtered.length > 0) {
|
|
221
|
+
sections.push(buildRecalledKnowledgeBlock(filtered));
|
|
222
|
+
}
|
|
223
|
+
} catch (err) {
|
|
224
|
+
logger.error(`recall: search failed: ${err}`);
|
|
216
225
|
}
|
|
217
|
-
} catch (err) {
|
|
218
|
-
logger.error(`recall: search failed: ${err}`);
|
|
219
226
|
}
|
|
220
227
|
|
|
221
228
|
if (sections.length === 0) return;
|
package/src/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from "./commands/slash.js";
|
|
8
8
|
import { isDefaultApiUrl, parseConfig } from "./config.js";
|
|
9
9
|
import { createNowledgeMemContextEngineFactory } from "./context-engine.js";
|
|
10
|
+
import { createNowledgeMemCorpusSupplement } from "./corpus-supplement.js";
|
|
10
11
|
import { buildBehavioralHook } from "./hooks/behavioral.js";
|
|
11
12
|
import {
|
|
12
13
|
buildAgentEndCaptureHandler,
|
|
@@ -80,6 +81,27 @@ export default {
|
|
|
80
81
|
);
|
|
81
82
|
}
|
|
82
83
|
|
|
84
|
+
// --- Corpus Supplement (when memory-core is the memory slot) ---
|
|
85
|
+
// Makes Nowledge Mem's knowledge graph searchable through memory-core's
|
|
86
|
+
// recall pipeline and dreaming promotion. Memories participate in
|
|
87
|
+
// temporal decay scoring and short-term-to-long-term promotion.
|
|
88
|
+
if (cfg.corpusSupplement) {
|
|
89
|
+
try {
|
|
90
|
+
api.registerMemoryCorpusSupplement(
|
|
91
|
+
createNowledgeMemCorpusSupplement(client, cfg, logger),
|
|
92
|
+
);
|
|
93
|
+
logger.info(
|
|
94
|
+
"nowledge-mem: registered as MemoryCorpusSupplement for memory-core recall",
|
|
95
|
+
);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
// OpenClaw < corpus supplement support — fall back to normal recall
|
|
98
|
+
cfg.corpusSupplement = false;
|
|
99
|
+
logger.info?.(
|
|
100
|
+
`nowledge-mem: corpus supplement unavailable, falling back to normal recall (${err})`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
83
105
|
// --- Hooks (fallback when CE is not active) ---
|
|
84
106
|
// Each hook checks ceState.active and returns early when the CE handles
|
|
85
107
|
// the same lifecycle through assemble/afterTurn.
|
|
@@ -120,7 +142,7 @@ export default {
|
|
|
120
142
|
|
|
121
143
|
const remoteMode = !isDefaultApiUrl(cfg.apiUrl);
|
|
122
144
|
logger.info(
|
|
123
|
-
`nowledge-mem: initialized (context=${cfg.sessionContext}, digest=${cfg.sessionDigest}, mode=${remoteMode ? `remote → ${cfg.apiUrl}` : "local"})`,
|
|
145
|
+
`nowledge-mem: initialized (context=${cfg.sessionContext}, digest=${cfg.sessionDigest}, corpus=${cfg.corpusSupplement}, mode=${remoteMode ? `remote → ${cfg.apiUrl}` : "local"})`,
|
|
124
146
|
);
|
|
125
147
|
},
|
|
126
148
|
};
|
package/src/spawn-env.js
CHANGED
|
@@ -7,8 +7,8 @@ const LOCAL_API_URL = "http://127.0.0.1:14242";
|
|
|
7
7
|
*/
|
|
8
8
|
export function buildNmemSpawnEnv({ apiUrl, apiKey } = {}) {
|
|
9
9
|
const env = { ...process.env };
|
|
10
|
-
env.NMEM_API_URL
|
|
11
|
-
env.NMEM_API_KEY
|
|
10
|
+
delete env.NMEM_API_URL;
|
|
11
|
+
delete env.NMEM_API_KEY;
|
|
12
12
|
if (apiUrl && apiUrl !== LOCAL_API_URL) {
|
|
13
13
|
env.NMEM_API_URL = apiUrl;
|
|
14
14
|
}
|
package/src/tools/memory-get.js
CHANGED
|
@@ -135,9 +135,13 @@ export function createMemoryGetTool(client, logger) {
|
|
|
135
135
|
safeParams.lines,
|
|
136
136
|
);
|
|
137
137
|
|
|
138
|
-
// Thread provenance: link to the source conversation
|
|
138
|
+
// Thread provenance: link to the source conversation.
|
|
139
|
+
// API returns source_thread as {id, title} object; CLI returns a string.
|
|
140
|
+
const rawSt = memory.source_thread;
|
|
139
141
|
const sourceThreadId =
|
|
140
|
-
|
|
142
|
+
(typeof rawSt === "object" && rawSt?.id
|
|
143
|
+
? String(rawSt.id)
|
|
144
|
+
: typeof rawSt === "string" ? rawSt : null) ??
|
|
141
145
|
memory.source_thread_id ??
|
|
142
146
|
memory.metadata?.source_thread_id ??
|
|
143
147
|
null;
|
package/src/tools/status.js
CHANGED
|
@@ -59,19 +59,32 @@ export function createStatusTool(client, _logger, cfg, runtimeInfo = {}) {
|
|
|
59
59
|
lines.push(`Plugin trust: ${pluginId} (allowlisted)`);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
// 0b. Memory slot check —
|
|
62
|
+
// 0b. Memory slot check — show current mode and options
|
|
63
63
|
const memorySlot = runtimeInfo.memorySlot;
|
|
64
64
|
details.memorySlot = memorySlot ?? "(unknown)";
|
|
65
65
|
if (memorySlot && memorySlot !== "openclaw-nowledge-mem") {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
66
|
+
const corpusOn = cfg.corpusSupplement === true;
|
|
67
|
+
if (corpusOn) {
|
|
68
|
+
lines.push(
|
|
69
|
+
`Memory slot: "${memorySlot}" + corpus supplement active`,
|
|
70
|
+
);
|
|
71
|
+
lines.push(
|
|
72
|
+
" Nowledge Mem feeds into memory-core's recall and dreaming pipeline. All tools available.",
|
|
73
|
+
);
|
|
74
|
+
} else {
|
|
75
|
+
lines.push(
|
|
76
|
+
`ℹ Memory slot: "${memorySlot}"`,
|
|
77
|
+
);
|
|
78
|
+
lines.push(
|
|
79
|
+
" Two options:",
|
|
80
|
+
);
|
|
81
|
+
lines.push(
|
|
82
|
+
' - Full mode: set plugins.slots.memory to "openclaw-nowledge-mem" for the complete tool surface',
|
|
83
|
+
);
|
|
84
|
+
lines.push(
|
|
85
|
+
" - Dual mode: set corpusSupplement: true to feed your cross-tool knowledge into memory-core's recall and dreaming",
|
|
86
|
+
);
|
|
87
|
+
}
|
|
75
88
|
lines.push("");
|
|
76
89
|
} else if (memorySlot === "openclaw-nowledge-mem") {
|
|
77
90
|
lines.push("Memory slot: openclaw-nowledge-mem (active)");
|