@nowledge/openclaw-nowledge-mem 0.7.1 → 0.8.1
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 +43 -1
- package/CLAUDE.md +30 -2
- package/README.md +71 -1
- package/SKILL.md +13 -0
- package/openclaw.plugin.json +56 -2
- package/package.json +2 -2
- package/src/client.js +4 -2
- package/src/config.js +93 -0
- package/src/context-engine.js +31 -4
- package/src/corpus-supplement.js +167 -0
- package/src/hooks/capture.js +125 -3
- package/src/hooks/recall.js +26 -19
- package/src/index.js +31 -3
- package/src/spawn-env.js +2 -2
- package/src/tools/memory-get.js +6 -2
- package/src/tools/status.js +68 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,49 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the Nowledge Mem OpenClaw plugin will be documented in this file.
|
|
4
4
|
|
|
5
|
-
## [
|
|
5
|
+
## [0.8.1] - 2026-04-07
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **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`.
|
|
10
|
+
- **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.
|
|
11
|
+
|
|
12
|
+
## [0.8.0] - 2026-04-07
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **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).
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
- **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.
|
|
22
|
+
|
|
23
|
+
Enable in your OpenClaw config:
|
|
24
|
+
```json
|
|
25
|
+
{ "corpusSupplement": true }
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Three new config keys control the supplement: `corpusSupplement` (boolean, default false), `corpusMaxResults` (1-20, default 5), `corpusMinScore` (0-100%, default 0).
|
|
29
|
+
|
|
30
|
+
- **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.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- **Minimum OpenClaw version bumped to `>=2026.4.5`** for `registerMemoryCorpusSupplement` API support. Graceful fallback for older versions (registration silently skipped).
|
|
35
|
+
|
|
36
|
+
## [0.7.3] - 2026-03-30
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
- **Status tool now checks plugin trust and memory slot.** `nowledge_mem_status` reads `plugins.allow` from the OpenClaw config: warns when the allowlist exists but the plugin is missing (loading will be blocked); shows a tip when no allowlist is set (plugin loads via `entries.enabled`, but trust is implicit). Also warns when the memory slot points to `memory-core` instead of `openclaw-nowledge-mem` — a common misconfiguration after upgrading to OpenClaw 3.22+. Both checks include the exact fix command.
|
|
41
|
+
- **Clarified that no `tools.*` config is needed.** All plugin tools register automatically when the plugin loads. Listing `nowledge_mem_*` names in `tools.allow` is a misconfiguration — OpenClaw silently strips allowlists that contain only plugin entries. The only config users need is `plugins.allow: ["openclaw-nowledge-mem"]`. Updated troubleshooting in README, SKILL.md, and website docs (EN + ZH).
|
|
42
|
+
|
|
43
|
+
## [0.7.2] - 2026-03-30
|
|
44
|
+
|
|
45
|
+
### Added
|
|
46
|
+
|
|
47
|
+
- **Status tool now reports memory slot configuration.** `nowledge_mem_status` checks whether the OpenClaw memory slot points to `openclaw-nowledge-mem`. If another plugin (e.g. the built-in `memory-core`) holds the slot, the tool shows a warning with the fix command. This catches a common misconfiguration after upgrading to OpenClaw 3.22+, where the implicit slot default changed to `memory-core`.
|
|
6
48
|
|
|
7
49
|
### Fixed
|
|
8
50
|
|
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:
|
|
@@ -118,7 +141,7 @@ When `contextEngine` points elsewhere (or is absent), hooks handle everything. N
|
|
|
118
141
|
- `nowledge_mem_thread_fetch` - fetch messages from a specific thread. Start with a small page, then use `offset` + `limit` for progressive retrieval only when more context is needed.
|
|
119
142
|
|
|
120
143
|
### Diagnostics
|
|
121
|
-
- `nowledge_mem_status` - show effective config (mode, apiUrl, apiKey set, sessionContext, sessionDigest, etc.), backend connectivity, and version. No parameters.
|
|
144
|
+
- `nowledge_mem_status` - show plugin trust status, memory slot status, effective config (mode, apiUrl, apiKey set, sessionContext, sessionDigest, etc.), backend connectivity, and version. Checks `plugins.allow`: warns if set but plugin not included (loading blocked); shows tip if not set (plugin loads via `entries.enabled` but trust is implicit). Warns if the memory slot points to `memory-core` instead of `openclaw-nowledge-mem` (common after OpenClaw 3.22+ upgrade). No parameters.
|
|
122
145
|
|
|
123
146
|
## Skill Surface
|
|
124
147
|
|
|
@@ -151,6 +174,11 @@ Credentials (apiUrl/apiKey): also reads `~/.nowledge-mem/config.json` (shared wi
|
|
|
151
174
|
| `maxContextResults` | integer 1-20 | `5` | `NMEM_MAX_CONTEXT_RESULTS` | How many memories to inject at prompt time |
|
|
152
175
|
| `recallMinScore` | integer 0-100 | `0` | `NMEM_RECALL_MIN_SCORE` | Min relevance score (%) to include in auto-recall |
|
|
153
176
|
| `maxThreadMessageChars` | integer 200-20000 | `800` | `NMEM_MAX_THREAD_MESSAGE_CHARS` | Max chars per captured thread message before truncation |
|
|
177
|
+
| `captureExclude` | string[] | `[]` | — | Session key glob patterns to skip during auto-capture. `*` matches within a colon-segment. Example: `["agent:*:cron:*"]` |
|
|
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) |
|
|
154
182
|
| `apiUrl` | string | `""` | `NMEM_API_URL` | Remote server URL. Empty = local (127.0.0.1:14242) |
|
|
155
183
|
| `apiKey` | string | `""` | `NMEM_API_KEY` | API key. Never logged. |
|
|
156
184
|
|
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ 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
|
|
|
@@ -187,6 +187,20 @@ Two entry points:
|
|
|
187
187
|
1. **From a memory**: `memory_search` or `memory_get` returns `sourceThreadId`, then fetch the source conversation
|
|
188
188
|
2. **Direct search**: `nowledge_mem_thread_search` finds conversations by keyword, then fetch any result
|
|
189
189
|
|
|
190
|
+
### Corpus Supplement (Dreaming Integration)
|
|
191
|
+
|
|
192
|
+
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).
|
|
193
|
+
|
|
194
|
+
**What happens:**
|
|
195
|
+
- Every time memory-core runs `memory_search`, it also searches Nowledge Mem's knowledge graph via the supplement.
|
|
196
|
+
- Recalled Nowledge Mem content accumulates frequency and relevance scores in memory-core's short-term store.
|
|
197
|
+
- High-scoring content is promoted to `MEMORY.md` during deep-phase dreaming (daily, 3 AM).
|
|
198
|
+
- The plugin's own search-based recall is automatically disabled to avoid duplicates. Working Memory injection continues as before.
|
|
199
|
+
|
|
200
|
+
**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`.
|
|
201
|
+
|
|
202
|
+
Requires OpenClaw >= 2026.4.5.
|
|
203
|
+
|
|
190
204
|
### Three Modes at a Glance
|
|
191
205
|
|
|
192
206
|
```mermaid
|
|
@@ -372,6 +386,9 @@ To change settings, use the OpenClaw plugin settings UI. Changes take effect on
|
|
|
372
386
|
| `maxContextResults` | integer | `5` | Max memories to inject at prompt time (1-20, only used when sessionContext is enabled) |
|
|
373
387
|
| `recallMinScore` | integer | `0` | Min relevance score (0-100%) to include in auto-recall. 0 = include all |
|
|
374
388
|
| `maxThreadMessageChars` | integer | `800` | Maximum characters preserved per captured OpenClaw thread message before truncation |
|
|
389
|
+
| `corpusSupplement` | boolean | `false` | Register as a searchable corpus inside memory-core's recall and dreaming pipeline |
|
|
390
|
+
| `corpusMaxResults` | integer | `5` | Max results per corpus supplement search (1-20) |
|
|
391
|
+
| `corpusMinScore` | integer | `0` | Min relevance score (0-100%) for supplement results. 0 = include all |
|
|
375
392
|
| `apiUrl` | string | `""` | Remote server URL. Empty = local (`http://127.0.0.1:14242`) |
|
|
376
393
|
| `apiKey` | string | `""` | API key for remote access. Injected as `NMEM_API_KEY` env var, never logged |
|
|
377
394
|
|
|
@@ -399,6 +416,9 @@ NMEM_DIGEST_MIN_INTERVAL=300
|
|
|
399
416
|
NMEM_MAX_CONTEXT_RESULTS=5
|
|
400
417
|
NMEM_RECALL_MIN_SCORE=0
|
|
401
418
|
NMEM_MAX_THREAD_MESSAGE_CHARS=800
|
|
419
|
+
NMEM_CORPUS_SUPPLEMENT=false
|
|
420
|
+
NMEM_CORPUS_MAX_RESULTS=5
|
|
421
|
+
NMEM_CORPUS_MIN_SCORE=0
|
|
402
422
|
NMEM_API_URL=https://...
|
|
403
423
|
NMEM_API_KEY=your-key
|
|
404
424
|
```
|
|
@@ -433,6 +453,56 @@ If OpenClaw logs a warning like `plugins.allow is empty; discovered non-bundled
|
|
|
433
453
|
|
|
434
454
|
If you also use linked or workspace copies, review `plugins.load.paths` before relying on an allowlist. OpenClaw allowlists plugin ids, not install provenance.
|
|
435
455
|
|
|
456
|
+
## Troubleshooting
|
|
457
|
+
|
|
458
|
+
**Only `memory_search` and `memory_get` show up — saves go to local markdown files**
|
|
459
|
+
|
|
460
|
+
The memory slot is probably still set to the built-in `memory-core`. OpenClaw 3.22+ defaults to `memory-core` when no explicit slot is configured. Reinstall to reset the slot automatically:
|
|
461
|
+
|
|
462
|
+
```bash
|
|
463
|
+
openclaw plugins install @nowledge/openclaw-nowledge-mem
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
Or set the slot manually in `~/.openclaw/openclaw.json`:
|
|
467
|
+
|
|
468
|
+
```json
|
|
469
|
+
{
|
|
470
|
+
"plugins": {
|
|
471
|
+
"slots": { "memory": "openclaw-nowledge-mem" }
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
Restart OpenClaw after either change.
|
|
477
|
+
|
|
478
|
+
**Plugin tools not available**
|
|
479
|
+
|
|
480
|
+
All 10 plugin tools register automatically when the plugin loads. No tool-level config is needed — just make sure the plugin itself is trusted:
|
|
481
|
+
|
|
482
|
+
```json
|
|
483
|
+
{
|
|
484
|
+
"plugins": {
|
|
485
|
+
"allow": ["openclaw-nowledge-mem"]
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
If `openclaw status` shows a CRITICAL warning about `plugins.allow`, this is the fix. Run `nowledge_mem_status` inside a conversation to check both plugin trust and memory slot status.
|
|
491
|
+
|
|
492
|
+
Do not list `nowledge_mem_*` tool names in `tools.allow` — OpenClaw silently strips allowlists that contain only plugin entries, so the config looks active but does nothing.
|
|
493
|
+
|
|
494
|
+
**Search timeouts with many concurrent agents**
|
|
495
|
+
|
|
496
|
+
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.
|
|
497
|
+
|
|
498
|
+
**`nmem --json status` fails in local mode**
|
|
499
|
+
|
|
500
|
+
The Nowledge Mem backend is not running. Start it from the desktop app, or run the backend manually.
|
|
501
|
+
|
|
502
|
+
**Remote mode not connecting**
|
|
503
|
+
|
|
504
|
+
Check `~/.nowledge-mem/config.json` credentials. Run `nmem --json --api-url "$URL" status` to verify the server is reachable.
|
|
505
|
+
|
|
436
506
|
## What Makes This Different
|
|
437
507
|
|
|
438
508
|
- **Local-first**: no API key, no cloud account. Your knowledge stays on your machine.
|
package/SKILL.md
CHANGED
|
@@ -186,6 +186,18 @@ First check the plugin status:
|
|
|
186
186
|
openclaw nowledge-mem status
|
|
187
187
|
```
|
|
188
188
|
|
|
189
|
+
If only `memory_search` and `memory_get` tools are available (other Nowledge Mem tools missing), the memory slot may still point to the built-in `memory-core`. Verify the slot in `~/.openclaw/openclaw.json`:
|
|
190
|
+
|
|
191
|
+
```json
|
|
192
|
+
{
|
|
193
|
+
"plugins": {
|
|
194
|
+
"slots": { "memory": "openclaw-nowledge-mem" }
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
If missing, reinstall (`openclaw plugins install @nowledge/openclaw-nowledge-mem`) which sets the slot automatically. This is common after upgrading OpenClaw to 3.22+ where the default memory slot changed.
|
|
200
|
+
|
|
189
201
|
Then verify in chat.
|
|
190
202
|
|
|
191
203
|
For a clean synthetic check:
|
|
@@ -237,6 +249,7 @@ What to expect:
|
|
|
237
249
|
| `plugins.allow is empty` warning | Add `openclaw-nowledge-mem` to `plugins.allow` if the user wants explicit trust |
|
|
238
250
|
| Remote config seems ignored | Check whether `~/.nowledge-mem/openclaw.json` is overriding plugin settings |
|
|
239
251
|
| Local mode unexpectedly talks to a remote server | Check for stale `NMEM_API_URL` / `NMEM_API_KEY` in the environment or an overriding `~/.nowledge-mem/openclaw.json` |
|
|
252
|
+
| Plugin tools missing | Ensure the plugin is in `plugins.allow: ["openclaw-nowledge-mem"]`. Do **not** put `nowledge_mem_*` tool names in `tools.allow` — OpenClaw silently strips plugin-only allowlists. No `tools.*` config is needed; plugin tools load automatically when the plugin is allowed. |
|
|
240
253
|
|
|
241
254
|
## Notes for Agents
|
|
242
255
|
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "openclaw-nowledge-mem",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"kind": "memory",
|
|
5
|
-
"skills": [
|
|
5
|
+
"skills": [
|
|
6
|
+
"skills/memory-guide"
|
|
7
|
+
],
|
|
6
8
|
"uiHints": {
|
|
7
9
|
"sessionContext": {
|
|
8
10
|
"label": "Session context injection",
|
|
@@ -28,6 +30,26 @@
|
|
|
28
30
|
"label": "Max thread message chars",
|
|
29
31
|
"help": "Maximum characters preserved per captured OpenClaw thread message (200-20000). Higher values keep more of long messages in Nowledge thread history."
|
|
30
32
|
},
|
|
33
|
+
"captureExclude": {
|
|
34
|
+
"label": "Exclude session patterns",
|
|
35
|
+
"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."
|
|
36
|
+
},
|
|
37
|
+
"captureSkipMarker": {
|
|
38
|
+
"label": "Skip marker",
|
|
39
|
+
"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."
|
|
40
|
+
},
|
|
41
|
+
"corpusSupplement": {
|
|
42
|
+
"label": "Register as corpus supplement",
|
|
43
|
+
"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)."
|
|
44
|
+
},
|
|
45
|
+
"corpusMaxResults": {
|
|
46
|
+
"label": "Corpus supplement max results",
|
|
47
|
+
"help": "Maximum results returned per corpus supplement search call (1-20). Only used when corpusSupplement is enabled."
|
|
48
|
+
},
|
|
49
|
+
"corpusMinScore": {
|
|
50
|
+
"label": "Corpus supplement min score (%)",
|
|
51
|
+
"help": "Minimum relevance score (0-100%) for corpus supplement results. Set to 0 to include all. Only used when corpusSupplement is enabled."
|
|
52
|
+
},
|
|
31
53
|
"apiUrl": {
|
|
32
54
|
"label": "Server URL (remote mode)",
|
|
33
55
|
"help": "Leave empty for local mode (default: http://127.0.0.1:14242). Set to your remote server URL for cross-device or team access. See: https://docs.nowledge.co/docs/remote-access"
|
|
@@ -79,6 +101,38 @@
|
|
|
79
101
|
"maximum": 20000,
|
|
80
102
|
"description": "Maximum characters preserved per captured OpenClaw thread message before truncation."
|
|
81
103
|
},
|
|
104
|
+
"captureExclude": {
|
|
105
|
+
"type": "array",
|
|
106
|
+
"items": {
|
|
107
|
+
"type": "string"
|
|
108
|
+
},
|
|
109
|
+
"default": [],
|
|
110
|
+
"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."
|
|
111
|
+
},
|
|
112
|
+
"captureSkipMarker": {
|
|
113
|
+
"type": "string",
|
|
114
|
+
"default": "#nmem-skip",
|
|
115
|
+
"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."
|
|
116
|
+
},
|
|
117
|
+
"corpusSupplement": {
|
|
118
|
+
"type": "boolean",
|
|
119
|
+
"default": false,
|
|
120
|
+
"description": "Register Nowledge Mem as a MemoryCorpusSupplement for memory-core's recall pipeline and dreaming. Enable when memory-core is your memory slot."
|
|
121
|
+
},
|
|
122
|
+
"corpusMaxResults": {
|
|
123
|
+
"type": "integer",
|
|
124
|
+
"default": 5,
|
|
125
|
+
"minimum": 1,
|
|
126
|
+
"maximum": 20,
|
|
127
|
+
"description": "Maximum results per corpus supplement search call (only used when corpusSupplement is enabled)"
|
|
128
|
+
},
|
|
129
|
+
"corpusMinScore": {
|
|
130
|
+
"type": "integer",
|
|
131
|
+
"default": 0,
|
|
132
|
+
"minimum": 0,
|
|
133
|
+
"maximum": 100,
|
|
134
|
+
"description": "Minimum relevance score (0-100%) for corpus supplement results (only used when corpusSupplement is enabled)"
|
|
135
|
+
},
|
|
82
136
|
"apiUrl": {
|
|
83
137
|
"type": "string",
|
|
84
138
|
"default": "",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nowledge/openclaw-nowledge-mem",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Nowledge Mem memory plugin for OpenClaw, local-first personal knowledge base",
|
|
6
6
|
"author": {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"format": "biome format --write ."
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"openclaw": ">=2026.
|
|
35
|
+
"openclaw": ">=2026.4.5"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@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
|
@@ -21,6 +21,11 @@ const ALLOWED_KEYS = new Set([
|
|
|
21
21
|
"maxContextResults",
|
|
22
22
|
"recallMinScore",
|
|
23
23
|
"maxThreadMessageChars",
|
|
24
|
+
"captureExclude",
|
|
25
|
+
"captureSkipMarker",
|
|
26
|
+
"corpusSupplement",
|
|
27
|
+
"corpusMaxResults",
|
|
28
|
+
"corpusMinScore",
|
|
24
29
|
"apiUrl",
|
|
25
30
|
"apiKey",
|
|
26
31
|
// Legacy aliases — accepted but not advertised
|
|
@@ -195,6 +200,9 @@ function firstDefined(...options) {
|
|
|
195
200
|
* NMEM_MAX_CONTEXT_RESULTS — integer (1-20)
|
|
196
201
|
* NMEM_RECALL_MIN_SCORE — integer (0-100)
|
|
197
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)
|
|
198
206
|
* NMEM_API_URL — remote server URL
|
|
199
207
|
* NMEM_API_KEY — API key (never logged)
|
|
200
208
|
*/
|
|
@@ -343,6 +351,86 @@ export function parseConfig(raw, logger) {
|
|
|
343
351
|
const apiKey = ak.value;
|
|
344
352
|
_sources.apiKey = ak.source;
|
|
345
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
|
+
|
|
397
|
+
// --- captureExclude: file > pluginConfig > default ---
|
|
398
|
+
const captureExclude = (() => {
|
|
399
|
+
const fromFile = Array.isArray(resolvedFile.captureExclude)
|
|
400
|
+
? resolvedFile.captureExclude
|
|
401
|
+
: null;
|
|
402
|
+
const fromPlugin = Array.isArray(resolvedPlugin.captureExclude)
|
|
403
|
+
? resolvedPlugin.captureExclude
|
|
404
|
+
: null;
|
|
405
|
+
_sources.captureExclude = fromFile
|
|
406
|
+
? "file"
|
|
407
|
+
: fromPlugin
|
|
408
|
+
? "pluginConfig"
|
|
409
|
+
: "default";
|
|
410
|
+
const entries = fromFile ?? fromPlugin ?? [];
|
|
411
|
+
return entries
|
|
412
|
+
.filter((v) => typeof v === "string" && v.trim())
|
|
413
|
+
.map((v) => v.trim());
|
|
414
|
+
})();
|
|
415
|
+
|
|
416
|
+
// --- captureSkipMarker: file > pluginConfig > default ---
|
|
417
|
+
const captureSkipMarker = (() => {
|
|
418
|
+
const fromFile =
|
|
419
|
+
typeof resolvedFile.captureSkipMarker === "string"
|
|
420
|
+
? resolvedFile.captureSkipMarker.trim()
|
|
421
|
+
: undefined;
|
|
422
|
+
const fromPlugin =
|
|
423
|
+
typeof resolvedPlugin.captureSkipMarker === "string"
|
|
424
|
+
? resolvedPlugin.captureSkipMarker.trim()
|
|
425
|
+
: undefined;
|
|
426
|
+
_sources.captureSkipMarker = fromFile
|
|
427
|
+
? "file"
|
|
428
|
+
: fromPlugin
|
|
429
|
+
? "pluginConfig"
|
|
430
|
+
: "default";
|
|
431
|
+
return fromFile || fromPlugin || "#nmem-skip";
|
|
432
|
+
})();
|
|
433
|
+
|
|
346
434
|
return {
|
|
347
435
|
sessionContext,
|
|
348
436
|
sessionDigest,
|
|
@@ -350,6 +438,11 @@ export function parseConfig(raw, logger) {
|
|
|
350
438
|
maxContextResults,
|
|
351
439
|
recallMinScore,
|
|
352
440
|
maxThreadMessageChars,
|
|
441
|
+
captureExclude,
|
|
442
|
+
captureSkipMarker,
|
|
443
|
+
corpusSupplement,
|
|
444
|
+
corpusMaxResults,
|
|
445
|
+
corpusMinScore,
|
|
353
446
|
apiUrl,
|
|
354
447
|
apiKey,
|
|
355
448
|
_sources,
|
package/src/context-engine.js
CHANGED
|
@@ -22,7 +22,13 @@
|
|
|
22
22
|
|
|
23
23
|
import { ceState } from "./ce-state.js";
|
|
24
24
|
import { BASE_GUIDANCE, SESSION_CONTEXT_GUIDANCE } from "./hooks/behavioral.js";
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
appendOrCreateThread,
|
|
27
|
+
hasSkipMarker,
|
|
28
|
+
isCronCaptureSessionKey,
|
|
29
|
+
matchesExcludePattern,
|
|
30
|
+
triageAndDistill,
|
|
31
|
+
} from "./hooks/capture.js";
|
|
26
32
|
import {
|
|
27
33
|
MAX_QUERY_LENGTH,
|
|
28
34
|
SHORT_QUERY_THRESHOLD,
|
|
@@ -165,7 +171,7 @@ export function createNowledgeMemContextEngineFactory(client, cfg, logger) {
|
|
|
165
171
|
info: {
|
|
166
172
|
id: "nowledge-mem",
|
|
167
173
|
name: "Nowledge Mem",
|
|
168
|
-
version: "0.
|
|
174
|
+
version: "0.8.0",
|
|
169
175
|
ownsCompaction: false,
|
|
170
176
|
},
|
|
171
177
|
|
|
@@ -230,7 +236,8 @@ export function createNowledgeMemContextEngineFactory(client, cfg, logger) {
|
|
|
230
236
|
}
|
|
231
237
|
|
|
232
238
|
// 3. Recalled memories (when sessionContext enabled)
|
|
233
|
-
|
|
239
|
+
// Skipped when corpus supplement handles search-based recall via memory-core.
|
|
240
|
+
if (cfg.sessionContext && !cfg.corpusSupplement) {
|
|
234
241
|
const query = buildAssembleSearchQuery(prompt, messages);
|
|
235
242
|
if (query) {
|
|
236
243
|
try {
|
|
@@ -342,8 +349,28 @@ export function createNowledgeMemContextEngineFactory(client, cfg, logger) {
|
|
|
342
349
|
}) {
|
|
343
350
|
if (isHeartbeat) return;
|
|
344
351
|
|
|
352
|
+
// Normalize sessionKey consistently with hook handlers
|
|
353
|
+
const normalizedKey = String(sessionKey || sessionId || "");
|
|
354
|
+
|
|
355
|
+
if (isCronCaptureSessionKey(normalizedKey)) {
|
|
356
|
+
logger.debug?.(`ce: skipped cron session ${normalizedKey}`);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Capture exclusion: pattern-based and marker-based filters
|
|
361
|
+
if (matchesExcludePattern(normalizedKey, cfg.captureExclude)) {
|
|
362
|
+
logger.debug?.(`ce: skipped excluded session ${normalizedKey}`);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (hasSkipMarker(messages, cfg.captureSkipMarker)) {
|
|
366
|
+
logger.debug?.(
|
|
367
|
+
`ce: skipped session with skip marker ${normalizedKey}`,
|
|
368
|
+
);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
345
372
|
const event = { messages, sessionFile };
|
|
346
|
-
const ctx = { sessionId, sessionKey };
|
|
373
|
+
const ctx = { sessionId, sessionKey: normalizedKey };
|
|
347
374
|
|
|
348
375
|
// 1. Always capture thread (idempotent, deduped)
|
|
349
376
|
const captureResult = await appendOrCreateThread({
|
|
@@ -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/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;
|
|
@@ -40,6 +41,82 @@ function _setSyncedMessageCount(threadId, count) {
|
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Capture exclusion filters
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Compile a glob pattern (where `*` matches within a colon-delimited segment)
|
|
50
|
+
* into a RegExp. Results are cached since patterns are immutable after parse.
|
|
51
|
+
*/
|
|
52
|
+
const _compiledPatterns = new Map();
|
|
53
|
+
function _compileGlob(pattern) {
|
|
54
|
+
const key = String(pattern).toLowerCase();
|
|
55
|
+
let re = _compiledPatterns.get(key);
|
|
56
|
+
if (!re) {
|
|
57
|
+
re = new RegExp(
|
|
58
|
+
"^" +
|
|
59
|
+
key
|
|
60
|
+
.replace(/[.+?^${}()|[\]\\]/g, "\\$&")
|
|
61
|
+
.replace(/\*/g, "[^:]*") +
|
|
62
|
+
"$",
|
|
63
|
+
);
|
|
64
|
+
_compiledPatterns.set(key, re);
|
|
65
|
+
}
|
|
66
|
+
return re;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Test whether a session key matches any exclusion glob pattern.
|
|
71
|
+
* Glob `*` matches within a colon-delimited segment (not across colons).
|
|
72
|
+
* Example: "agent:*:cron:*" matches "agent:main:cron:abc123"
|
|
73
|
+
*
|
|
74
|
+
* Exported for reuse by Context Engine and other capture paths.
|
|
75
|
+
*/
|
|
76
|
+
export function matchesExcludePattern(sessionKey, patterns) {
|
|
77
|
+
if (!Array.isArray(patterns) || patterns.length === 0) return false;
|
|
78
|
+
const key = String(sessionKey || "").toLowerCase();
|
|
79
|
+
return patterns.some((pattern) => _compileGlob(pattern).test(key));
|
|
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
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Check if any message contains the skip marker text.
|
|
103
|
+
* Scans both raw message content and nested message objects.
|
|
104
|
+
*
|
|
105
|
+
* Exported for reuse by Context Engine and other capture paths.
|
|
106
|
+
*/
|
|
107
|
+
export function hasSkipMarker(messages, marker) {
|
|
108
|
+
if (!marker || typeof marker !== "string" || !Array.isArray(messages)) return false;
|
|
109
|
+
const markerLc = marker.toLowerCase();
|
|
110
|
+
return messages.some((msg) => {
|
|
111
|
+
const text = extractText(msg?.content ?? msg?.message?.content);
|
|
112
|
+
return text.toLowerCase().includes(markerLc);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// Message normalization utilities
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
|
|
43
120
|
export function truncate(text, max = DEFAULT_MAX_MESSAGE_CHARS) {
|
|
44
121
|
const str = String(text || "").trim();
|
|
45
122
|
if (!str) return "";
|
|
@@ -197,8 +274,9 @@ export async function appendOrCreateThread({
|
|
|
197
274
|
ctx,
|
|
198
275
|
reason,
|
|
199
276
|
maxMessageChars = DEFAULT_MAX_MESSAGE_CHARS,
|
|
277
|
+
resolvedMessages,
|
|
200
278
|
}) {
|
|
201
|
-
const rawMessages = await resolveHookMessages(event);
|
|
279
|
+
const rawMessages = resolvedMessages ?? (await resolveHookMessages(event));
|
|
202
280
|
if (!Array.isArray(rawMessages) || rawMessages.length === 0) return;
|
|
203
281
|
|
|
204
282
|
const threadId = buildStableThreadId(event, ctx);
|
|
@@ -413,6 +491,27 @@ export function buildAgentEndCaptureHandler(client, cfg, logger) {
|
|
|
413
491
|
if (!event?.success) return;
|
|
414
492
|
if (ctx?.trigger === "heartbeat") return;
|
|
415
493
|
|
|
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)
|
|
501
|
+
if (matchesExcludePattern(sessionKey, cfg.captureExclude)) {
|
|
502
|
+
logger.debug?.(`capture: skipped excluded session ${sessionKey}`);
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Layer 2: marker-based exclusion (user typed #nmem-skip in conversation)
|
|
507
|
+
const resolvedMessages = await resolveHookMessages(event);
|
|
508
|
+
if (hasSkipMarker(resolvedMessages, cfg.captureSkipMarker)) {
|
|
509
|
+
logger.debug?.(
|
|
510
|
+
`capture: skipped session with skip marker ${sessionKey}`,
|
|
511
|
+
);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
|
|
416
515
|
const captureResult = await appendOrCreateThread({
|
|
417
516
|
client,
|
|
418
517
|
logger,
|
|
@@ -420,6 +519,7 @@ export function buildAgentEndCaptureHandler(client, cfg, logger) {
|
|
|
420
519
|
ctx,
|
|
421
520
|
reason: "agent_end",
|
|
422
521
|
maxMessageChars: cfg.maxThreadMessageChars,
|
|
522
|
+
resolvedMessages,
|
|
423
523
|
});
|
|
424
524
|
|
|
425
525
|
await triageAndDistill({ client, cfg, logger, captureResult, ctx });
|
|
@@ -435,10 +535,31 @@ export function buildAgentEndCaptureHandler(client, cfg, logger) {
|
|
|
435
535
|
*
|
|
436
536
|
* Heartbeat sessions are skipped (same rationale as agent_end).
|
|
437
537
|
*/
|
|
438
|
-
export function buildBeforeResetCaptureHandler(client,
|
|
538
|
+
export function buildBeforeResetCaptureHandler(client, cfg, logger) {
|
|
439
539
|
return async (event, ctx) => {
|
|
440
540
|
if (ceState.active) return;
|
|
441
541
|
if (ctx?.trigger === "heartbeat") return;
|
|
542
|
+
|
|
543
|
+
const sessionKey = String(ctx?.sessionKey || ctx?.sessionId || "");
|
|
544
|
+
if (isCronCaptureSessionKey(sessionKey)) {
|
|
545
|
+
logger.debug?.(`capture: skipped cron session ${sessionKey}`);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (matchesExcludePattern(sessionKey, cfg.captureExclude)) {
|
|
550
|
+
logger.debug?.(`capture: skipped excluded session ${sessionKey}`);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Layer 2: marker-based exclusion
|
|
555
|
+
const resolvedMessages = await resolveHookMessages(event);
|
|
556
|
+
if (hasSkipMarker(resolvedMessages, cfg.captureSkipMarker)) {
|
|
557
|
+
logger.debug?.(
|
|
558
|
+
`capture: skipped session with skip marker ${sessionKey}`,
|
|
559
|
+
);
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
|
|
442
563
|
const reason = typeof event?.reason === "string" ? event.reason : undefined;
|
|
443
564
|
await appendOrCreateThread({
|
|
444
565
|
client,
|
|
@@ -446,7 +567,8 @@ export function buildBeforeResetCaptureHandler(client, _cfg, logger) {
|
|
|
446
567
|
event,
|
|
447
568
|
ctx,
|
|
448
569
|
reason,
|
|
449
|
-
maxMessageChars:
|
|
570
|
+
maxMessageChars: cfg.maxThreadMessageChars,
|
|
571
|
+
resolvedMessages,
|
|
450
572
|
});
|
|
451
573
|
};
|
|
452
574
|
}
|
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,
|
|
@@ -54,8 +55,14 @@ export default {
|
|
|
54
55
|
api.registerTool(createThreadSearchTool(client, logger));
|
|
55
56
|
api.registerTool(createThreadFetchTool(client, logger));
|
|
56
57
|
|
|
57
|
-
// Diagnostics
|
|
58
|
-
|
|
58
|
+
// Diagnostics — pass runtime config so the status tool can detect:
|
|
59
|
+
// - plugins.allow missing or not including this plugin
|
|
60
|
+
// - memory slot pointing elsewhere (e.g. memory-core)
|
|
61
|
+
const memorySlot = api.config?.plugins?.slots?.memory;
|
|
62
|
+
const pluginsAllow = api.config?.plugins?.allow;
|
|
63
|
+
api.registerTool(
|
|
64
|
+
createStatusTool(client, logger, cfg, { memorySlot, pluginsAllow }),
|
|
65
|
+
);
|
|
59
66
|
|
|
60
67
|
// --- Context Engine registration ---
|
|
61
68
|
// When the user sets `plugins.slots.contextEngine: "nowledge-mem"`,
|
|
@@ -74,6 +81,27 @@ export default {
|
|
|
74
81
|
);
|
|
75
82
|
}
|
|
76
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
|
+
|
|
77
105
|
// --- Hooks (fallback when CE is not active) ---
|
|
78
106
|
// Each hook checks ceState.active and returns early when the CE handles
|
|
79
107
|
// the same lifecycle through assemble/afterTurn.
|
|
@@ -114,7 +142,7 @@ export default {
|
|
|
114
142
|
|
|
115
143
|
const remoteMode = !isDefaultApiUrl(cfg.apiUrl);
|
|
116
144
|
logger.info(
|
|
117
|
-
`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"})`,
|
|
118
146
|
);
|
|
119
147
|
},
|
|
120
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
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { isDefaultApiUrl } from "../config.js";
|
|
9
9
|
|
|
10
|
-
export function createStatusTool(client, _logger, cfg) {
|
|
10
|
+
export function createStatusTool(client, _logger, cfg, runtimeInfo = {}) {
|
|
11
11
|
return {
|
|
12
12
|
name: "nowledge_mem_status",
|
|
13
13
|
description:
|
|
@@ -23,6 +23,73 @@ export function createStatusTool(client, _logger, cfg) {
|
|
|
23
23
|
const details = {};
|
|
24
24
|
const sources = cfg._sources || {};
|
|
25
25
|
|
|
26
|
+
// 0a. Plugin trust check — detect missing plugins.allow
|
|
27
|
+
const pluginsAllow = runtimeInfo.pluginsAllow;
|
|
28
|
+
const pluginId = "openclaw-nowledge-mem";
|
|
29
|
+
details.pluginsAllow = !Array.isArray(pluginsAllow)
|
|
30
|
+
? "(not set)"
|
|
31
|
+
: pluginsAllow.includes(pluginId)
|
|
32
|
+
? "listed"
|
|
33
|
+
: "set but plugin not included";
|
|
34
|
+
|
|
35
|
+
if (
|
|
36
|
+
Array.isArray(pluginsAllow) &&
|
|
37
|
+
pluginsAllow.length > 0 &&
|
|
38
|
+
!pluginsAllow.includes(pluginId)
|
|
39
|
+
) {
|
|
40
|
+
lines.push(
|
|
41
|
+
`⚠ plugins.allow is set but does not include "${pluginId}".`,
|
|
42
|
+
);
|
|
43
|
+
lines.push(
|
|
44
|
+
" The plugin will be BLOCKED from loading unless it holds the memory slot or is explicitly enabled.",
|
|
45
|
+
);
|
|
46
|
+
lines.push(
|
|
47
|
+
` Fix: add "${pluginId}" to plugins.allow in your OpenClaw config.`,
|
|
48
|
+
);
|
|
49
|
+
lines.push("");
|
|
50
|
+
} else if (!Array.isArray(pluginsAllow) || pluginsAllow.length === 0) {
|
|
51
|
+
lines.push(
|
|
52
|
+
"Plugin trust: plugins.allow not set (plugin loads via entries.enabled)",
|
|
53
|
+
);
|
|
54
|
+
lines.push(
|
|
55
|
+
` Tip: set plugins.allow to ["${pluginId}"] to pin trust explicitly.`,
|
|
56
|
+
);
|
|
57
|
+
lines.push("");
|
|
58
|
+
} else {
|
|
59
|
+
lines.push(`Plugin trust: ${pluginId} (allowlisted)`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 0b. Memory slot check — show current mode and options
|
|
63
|
+
const memorySlot = runtimeInfo.memorySlot;
|
|
64
|
+
details.memorySlot = memorySlot ?? "(unknown)";
|
|
65
|
+
if (memorySlot && memorySlot !== "openclaw-nowledge-mem") {
|
|
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
|
+
}
|
|
88
|
+
lines.push("");
|
|
89
|
+
} else if (memorySlot === "openclaw-nowledge-mem") {
|
|
90
|
+
lines.push("Memory slot: openclaw-nowledge-mem (active)");
|
|
91
|
+
}
|
|
92
|
+
|
|
26
93
|
// 1. Mode + connection target
|
|
27
94
|
const remote = !isDefaultApiUrl(cfg.apiUrl);
|
|
28
95
|
const mode = remote ? "remote" : "local";
|