@nowledge/openclaw-nowledge-mem 0.7.3 → 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 +31 -0
- package/CLAUDE.md +27 -1
- package/README.md +21 -1
- package/openclaw.plugin.json +42 -7
- package/package.json +2 -2
- 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/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,37 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the Nowledge Mem OpenClaw plugin will be documented in this file.
|
|
4
4
|
|
|
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
|
+
|
|
5
36
|
## [0.7.3] - 2026-03-30
|
|
6
37
|
|
|
7
38
|
### 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,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
|
```
|
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",
|
|
@@ -30,11 +32,23 @@
|
|
|
30
32
|
},
|
|
31
33
|
"captureExclude": {
|
|
32
34
|
"label": "Exclude session patterns",
|
|
33
|
-
"help": "
|
|
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."
|
|
34
36
|
},
|
|
35
37
|
"captureSkipMarker": {
|
|
36
38
|
"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
|
|
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."
|
|
38
52
|
},
|
|
39
53
|
"apiUrl": {
|
|
40
54
|
"label": "Server URL (remote mode)",
|
|
@@ -89,14 +103,35 @@
|
|
|
89
103
|
},
|
|
90
104
|
"captureExclude": {
|
|
91
105
|
"type": "array",
|
|
92
|
-
"items": {
|
|
106
|
+
"items": {
|
|
107
|
+
"type": "string"
|
|
108
|
+
},
|
|
93
109
|
"default": [],
|
|
94
|
-
"description": "
|
|
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."
|
|
95
111
|
},
|
|
96
112
|
"captureSkipMarker": {
|
|
97
113
|
"type": "string",
|
|
98
114
|
"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
|
|
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)"
|
|
100
135
|
},
|
|
101
136
|
"apiUrl": {
|
|
102
137
|
"type": "string",
|
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
|
@@ -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/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)");
|