@nowledge/openclaw-nowledge-mem 0.7.0 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/CLAUDE.md +3 -1
- package/README.md +55 -2
- package/SKILL.md +13 -0
- package/openclaw.plugin.json +20 -1
- package/package.json +1 -1
- package/src/client.js +47 -85
- package/src/config.js +41 -0
- package/src/context-engine.js +22 -2
- package/src/hooks/capture.js +158 -9
- package/src/index.js +8 -2
- package/src/tools/status.js +55 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the Nowledge Mem OpenClaw plugin will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.7.3] - 2026-03-30
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **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.
|
|
10
|
+
- **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).
|
|
11
|
+
|
|
12
|
+
## [0.7.2] - 2026-03-30
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **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`.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- **Thread sync no longer depends on argv-sized CLI payloads.** OpenClaw conversation capture now creates and appends threads through the Mem HTTP API instead of passing whole message arrays through `nmem ... -m '<json>'`. This removes the transport limit that caused repeated append failures on long or repetitive sessions.
|
|
21
|
+
- **Session capture now syncs only the unsynced tail.** The plugin preserves the real transcript, asks Mem how many messages are already stored, and appends only the new tail instead of replaying the whole session on every hook or Context Engine turn.
|
|
22
|
+
- **Remote config is still unified after the transport change.** The same resolved `apiUrl` and `apiKey` from OpenClaw settings / `~/.nowledge-mem/config.json` now drive both CLI-backed memory tools and API-backed thread sync.
|
|
23
|
+
- **Removed lossy repetitive-session collapse.** The temporary content-based dedup workaround for cron-style sessions has been removed so conversation structure is preserved faithfully.
|
|
24
|
+
|
|
25
|
+
## [0.7.1] - 2026-03-23
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- **Heartbeat sessions no longer trigger thread capture.** Sessions with `ctx.trigger === "heartbeat"` are now skipped in hook handlers. For cron-triggered heartbeat sessions (which use `trigger: "cron"`), a content-based dedup detects repetitive patterns: when >50% of messages in a session are duplicates, only unique messages are kept. This collapses 20 repetitive heartbeat messages down to 2, eliminating the CLI timeout caused by oversized payloads.
|
|
30
|
+
|
|
5
31
|
## [0.7.0] - 2026-03-23
|
|
6
32
|
|
|
7
33
|
### Added
|
package/CLAUDE.md
CHANGED
|
@@ -118,7 +118,7 @@ When `contextEngine` points elsewhere (or is absent), hooks handle everything. N
|
|
|
118
118
|
- `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
119
|
|
|
120
120
|
### Diagnostics
|
|
121
|
-
- `nowledge_mem_status` - show effective config (mode, apiUrl, apiKey set, sessionContext, sessionDigest, etc.), backend connectivity, and version. No parameters.
|
|
121
|
+
- `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
122
|
|
|
123
123
|
## Skill Surface
|
|
124
124
|
|
|
@@ -151,6 +151,8 @@ Credentials (apiUrl/apiKey): also reads `~/.nowledge-mem/config.json` (shared wi
|
|
|
151
151
|
| `maxContextResults` | integer 1-20 | `5` | `NMEM_MAX_CONTEXT_RESULTS` | How many memories to inject at prompt time |
|
|
152
152
|
| `recallMinScore` | integer 0-100 | `0` | `NMEM_RECALL_MIN_SCORE` | Min relevance score (%) to include in auto-recall |
|
|
153
153
|
| `maxThreadMessageChars` | integer 200-20000 | `800` | `NMEM_MAX_THREAD_MESSAGE_CHARS` | Max chars per captured thread message before truncation |
|
|
154
|
+
| `captureExclude` | string[] | `[]` | — | Session key glob patterns to skip during auto-capture. `*` matches within a colon-segment. Example: `["agent:*:cron:*"]` |
|
|
155
|
+
| `captureSkipMarker` | string | `"#nmem-skip"` | — | In-band marker: any message containing this text skips capture for the session. Not sticky across compaction |
|
|
154
156
|
| `apiUrl` | string | `""` | `NMEM_API_URL` | Remote server URL. Empty = local (127.0.0.1:14242) |
|
|
155
157
|
| `apiKey` | string | `""` | `NMEM_API_KEY` | API key. Never logged. |
|
|
156
158
|
|
package/README.md
CHANGED
|
@@ -70,7 +70,7 @@ Connect to a Nowledge Mem server running elsewhere (a VPS, a home server, or a s
|
|
|
70
70
|
|
|
71
71
|
Enable the plugin the same way as local mode, then set `apiUrl` and `apiKey` in the OpenClaw plugin settings.
|
|
72
72
|
|
|
73
|
-
Or use
|
|
73
|
+
Or use the shared config file at `~/.nowledge-mem/config.json` so OpenClaw, `nmem`, Bub, Claude Code, and other integrations all point at the same remote Mem:
|
|
74
74
|
|
|
75
75
|
```json
|
|
76
76
|
{
|
|
@@ -79,7 +79,9 @@ Or use a config file at `~/.nowledge-mem/openclaw.json`:
|
|
|
79
79
|
}
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
Legacy `~/.nowledge-mem/openclaw.json` is still honored first for backward compatibility.
|
|
83
|
+
|
|
84
|
+
The resolved `apiUrl` and `apiKey` are reused across the plugin: CLI-backed memory tools and API-backed thread sync both talk to the same backend. The `apiKey` is never passed as a CLI argument and is never logged.
|
|
83
85
|
|
|
84
86
|
### Configure via WebUI of OpenClaw
|
|
85
87
|
|
|
@@ -154,6 +156,7 @@ flowchart TD
|
|
|
154
156
|
|
|
155
157
|
**Key points:**
|
|
156
158
|
- Thread capture is unconditional: every conversation is saved and searchable via `nowledge_mem_thread_search`
|
|
159
|
+
- Thread sync is incremental: the plugin preserves the real transcript but appends only the unsynced tail instead of replaying the whole session
|
|
157
160
|
- LLM distillation only runs at `agent_end`, not during compaction/reset checkpoints
|
|
158
161
|
- Distilled memories carry `sourceThreadId`, linking them back to the source conversation
|
|
159
162
|
- Cooldown (`digestMinInterval`, default 300s) prevents burst distillation
|
|
@@ -430,6 +433,56 @@ If OpenClaw logs a warning like `plugins.allow is empty; discovered non-bundled
|
|
|
430
433
|
|
|
431
434
|
If you also use linked or workspace copies, review `plugins.load.paths` before relying on an allowlist. OpenClaw allowlists plugin ids, not install provenance.
|
|
432
435
|
|
|
436
|
+
## Troubleshooting
|
|
437
|
+
|
|
438
|
+
**Only `memory_search` and `memory_get` show up — saves go to local markdown files**
|
|
439
|
+
|
|
440
|
+
The memory slot is probably still set to the built-in `memory-core`. OpenClaw 3.22+ defaults to `memory-core` when no explicit slot is configured. Reinstall to reset the slot automatically:
|
|
441
|
+
|
|
442
|
+
```bash
|
|
443
|
+
openclaw plugins install @nowledge/openclaw-nowledge-mem
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
Or set the slot manually in `~/.openclaw/openclaw.json`:
|
|
447
|
+
|
|
448
|
+
```json
|
|
449
|
+
{
|
|
450
|
+
"plugins": {
|
|
451
|
+
"slots": { "memory": "openclaw-nowledge-mem" }
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
Restart OpenClaw after either change.
|
|
457
|
+
|
|
458
|
+
**Plugin tools not available**
|
|
459
|
+
|
|
460
|
+
All 10 plugin tools register automatically when the plugin loads. No tool-level config is needed — just make sure the plugin itself is trusted:
|
|
461
|
+
|
|
462
|
+
```json
|
|
463
|
+
{
|
|
464
|
+
"plugins": {
|
|
465
|
+
"allow": ["openclaw-nowledge-mem"]
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
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.
|
|
471
|
+
|
|
472
|
+
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.
|
|
473
|
+
|
|
474
|
+
**Search timeouts with many concurrent agents**
|
|
475
|
+
|
|
476
|
+
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.
|
|
477
|
+
|
|
478
|
+
**`nmem --json status` fails in local mode**
|
|
479
|
+
|
|
480
|
+
The Nowledge Mem backend is not running. Start it from the desktop app, or run the backend manually.
|
|
481
|
+
|
|
482
|
+
**Remote mode not connecting**
|
|
483
|
+
|
|
484
|
+
Check `~/.nowledge-mem/config.json` credentials. Run `nmem --json --api-url "$URL" status` to verify the server is reachable.
|
|
485
|
+
|
|
433
486
|
## What Makes This Different
|
|
434
487
|
|
|
435
488
|
- **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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "openclaw-nowledge-mem",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.3",
|
|
4
4
|
"kind": "memory",
|
|
5
5
|
"skills": ["skills/memory-guide"],
|
|
6
6
|
"uiHints": {
|
|
@@ -28,6 +28,14 @@
|
|
|
28
28
|
"label": "Max thread message chars",
|
|
29
29
|
"help": "Maximum characters preserved per captured OpenClaw thread message (200-20000). Higher values keep more of long messages in Nowledge thread history."
|
|
30
30
|
},
|
|
31
|
+
"captureExclude": {
|
|
32
|
+
"label": "Exclude session patterns",
|
|
33
|
+
"help": "Session key glob patterns to skip during auto-capture. Use * to match within a colon-segment. Example: agent:*:cron:*"
|
|
34
|
+
},
|
|
35
|
+
"captureSkipMarker": {
|
|
36
|
+
"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 — use captureExclude patterns for persistent filtering."
|
|
38
|
+
},
|
|
31
39
|
"apiUrl": {
|
|
32
40
|
"label": "Server URL (remote mode)",
|
|
33
41
|
"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 +87,17 @@
|
|
|
79
87
|
"maximum": 20000,
|
|
80
88
|
"description": "Maximum characters preserved per captured OpenClaw thread message before truncation."
|
|
81
89
|
},
|
|
90
|
+
"captureExclude": {
|
|
91
|
+
"type": "array",
|
|
92
|
+
"items": { "type": "string" },
|
|
93
|
+
"default": [],
|
|
94
|
+
"description": "Session key glob patterns to exclude from auto-capture. Glob * matches within a colon-delimited segment. Example: agent:*:cron:* excludes all cron job sessions."
|
|
95
|
+
},
|
|
96
|
+
"captureSkipMarker": {
|
|
97
|
+
"type": "string",
|
|
98
|
+
"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 — use captureExclude for persistent filtering."
|
|
100
|
+
},
|
|
82
101
|
"apiUrl": {
|
|
83
102
|
"type": "string",
|
|
84
103
|
"default": "",
|
package/package.json
CHANGED
package/src/client.js
CHANGED
|
@@ -54,15 +54,11 @@ function patchWmSection(currentContent, heading, { content, append } = {}) {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
/**
|
|
57
|
-
* Nowledge Mem client. Wraps the nmem CLI
|
|
57
|
+
* Nowledge Mem client. Wraps the nmem CLI plus direct API calls.
|
|
58
58
|
*
|
|
59
|
-
*
|
|
60
|
-
* -
|
|
61
|
-
*
|
|
62
|
-
* (see: https://docs.nowledge.co/docs/remote-access)
|
|
63
|
-
*
|
|
64
|
-
* Falls back to direct API calls when a CLI command is too new for the installed
|
|
65
|
-
* version. The fallback path uses the same apiUrl / apiKey.
|
|
59
|
+
* Most interactive reads/writes stay CLI-first for local-first ergonomics.
|
|
60
|
+
* Thread create/append are API-first because large message batches should
|
|
61
|
+
* travel in an HTTP body, not as a single argv-sized JSON argument.
|
|
66
62
|
*
|
|
67
63
|
* Credential rules:
|
|
68
64
|
* - apiUrl: passed to CLI via --api-url flag (not a secret)
|
|
@@ -559,42 +555,12 @@ export class NowledgeMemClient {
|
|
|
559
555
|
throw new Error("createThread requires at least one message");
|
|
560
556
|
}
|
|
561
557
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
"-t",
|
|
569
|
-
normalizedTitle,
|
|
570
|
-
"-m",
|
|
571
|
-
JSON.stringify(messages),
|
|
572
|
-
"-s",
|
|
573
|
-
String(source),
|
|
574
|
-
];
|
|
575
|
-
if (threadId) {
|
|
576
|
-
args.push("--id", String(threadId));
|
|
577
|
-
}
|
|
578
|
-
data = await this.execJson(args);
|
|
579
|
-
} catch (err) {
|
|
580
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
581
|
-
const needsApiFallback =
|
|
582
|
-
Boolean(threadId) &&
|
|
583
|
-
(message.includes("unrecognized arguments: --id") ||
|
|
584
|
-
message.includes("invalid choice"));
|
|
585
|
-
if (!needsApiFallback) {
|
|
586
|
-
throw err;
|
|
587
|
-
}
|
|
588
|
-
this.logger.warn(
|
|
589
|
-
"createThread: CLI missing --id support, falling back to API",
|
|
590
|
-
);
|
|
591
|
-
data = await this.apiJson("POST", "/threads", {
|
|
592
|
-
thread_id: String(threadId),
|
|
593
|
-
title: normalizedTitle,
|
|
594
|
-
source: String(source),
|
|
595
|
-
messages,
|
|
596
|
-
});
|
|
597
|
-
}
|
|
558
|
+
const data = await this.apiJson("POST", "/threads", {
|
|
559
|
+
...(threadId ? { thread_id: String(threadId) } : {}),
|
|
560
|
+
title: normalizedTitle,
|
|
561
|
+
source: String(source),
|
|
562
|
+
messages,
|
|
563
|
+
});
|
|
598
564
|
|
|
599
565
|
return String(
|
|
600
566
|
data.id ?? data.thread?.thread_id ?? data.thread_id ?? "created",
|
|
@@ -615,50 +581,46 @@ export class NowledgeMemClient {
|
|
|
615
581
|
return { messagesAdded: 0, totalMessages: 0 };
|
|
616
582
|
}
|
|
617
583
|
|
|
584
|
+
const data = await this.apiJson(
|
|
585
|
+
"POST",
|
|
586
|
+
`/threads/${encodeURIComponent(normalizedThreadId)}/append`,
|
|
587
|
+
{
|
|
588
|
+
messages,
|
|
589
|
+
deduplicate,
|
|
590
|
+
...(idempotencyKey
|
|
591
|
+
? { idempotency_key: String(idempotencyKey) }
|
|
592
|
+
: {}),
|
|
593
|
+
},
|
|
594
|
+
);
|
|
595
|
+
return {
|
|
596
|
+
messagesAdded: Number(data.messages_added ?? 0),
|
|
597
|
+
totalMessages: Number(data.total_messages ?? 0),
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
async getThreadMessageCount(threadId) {
|
|
602
|
+
const normalizedThreadId = String(threadId || "").trim();
|
|
603
|
+
if (!normalizedThreadId) {
|
|
604
|
+
throw new Error("getThreadMessageCount requires threadId");
|
|
605
|
+
}
|
|
606
|
+
|
|
618
607
|
try {
|
|
619
|
-
const args = [
|
|
620
|
-
"--json",
|
|
621
|
-
"t",
|
|
622
|
-
"append",
|
|
623
|
-
normalizedThreadId,
|
|
624
|
-
"-m",
|
|
625
|
-
JSON.stringify(messages),
|
|
626
|
-
...(deduplicate ? [] : ["--no-deduplicate"]),
|
|
627
|
-
];
|
|
628
|
-
if (idempotencyKey) {
|
|
629
|
-
args.push("--idempotency-key", String(idempotencyKey));
|
|
630
|
-
}
|
|
631
|
-
const data = await this.execJson(args);
|
|
632
|
-
return {
|
|
633
|
-
messagesAdded: Number(data.messages_added ?? 0),
|
|
634
|
-
totalMessages: Number(data.total_messages ?? 0),
|
|
635
|
-
};
|
|
636
|
-
} catch (err) {
|
|
637
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
638
|
-
const needsApiFallback =
|
|
639
|
-
message.includes("invalid choice") ||
|
|
640
|
-
message.includes("unrecognized arguments");
|
|
641
|
-
if (!needsApiFallback) {
|
|
642
|
-
throw err;
|
|
643
|
-
}
|
|
644
|
-
this.logger.warn(
|
|
645
|
-
"appendThread: CLI missing append support, falling back to API",
|
|
646
|
-
);
|
|
647
608
|
const data = await this.apiJson(
|
|
648
|
-
"
|
|
649
|
-
`/threads/${encodeURIComponent(normalizedThreadId)}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
deduplicate,
|
|
653
|
-
...(idempotencyKey
|
|
654
|
-
? { idempotency_key: String(idempotencyKey) }
|
|
655
|
-
: {}),
|
|
656
|
-
},
|
|
609
|
+
"GET",
|
|
610
|
+
`/threads/${encodeURIComponent(normalizedThreadId)}?limit=1`,
|
|
611
|
+
undefined,
|
|
612
|
+
15_000,
|
|
657
613
|
);
|
|
658
|
-
return
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
614
|
+
return Number(
|
|
615
|
+
data.message_count ??
|
|
616
|
+
data.total_messages ??
|
|
617
|
+
(Array.isArray(data.messages) ? data.messages.length : 0),
|
|
618
|
+
);
|
|
619
|
+
} catch (err) {
|
|
620
|
+
if (this.isThreadNotFoundError(err)) {
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
throw err;
|
|
662
624
|
}
|
|
663
625
|
}
|
|
664
626
|
|
package/src/config.js
CHANGED
|
@@ -21,6 +21,8 @@ const ALLOWED_KEYS = new Set([
|
|
|
21
21
|
"maxContextResults",
|
|
22
22
|
"recallMinScore",
|
|
23
23
|
"maxThreadMessageChars",
|
|
24
|
+
"captureExclude",
|
|
25
|
+
"captureSkipMarker",
|
|
24
26
|
"apiUrl",
|
|
25
27
|
"apiKey",
|
|
26
28
|
// Legacy aliases — accepted but not advertised
|
|
@@ -343,6 +345,43 @@ export function parseConfig(raw, logger) {
|
|
|
343
345
|
const apiKey = ak.value;
|
|
344
346
|
_sources.apiKey = ak.source;
|
|
345
347
|
|
|
348
|
+
// --- captureExclude: file > pluginConfig > default ---
|
|
349
|
+
const captureExclude = (() => {
|
|
350
|
+
const fromFile = Array.isArray(resolvedFile.captureExclude)
|
|
351
|
+
? resolvedFile.captureExclude
|
|
352
|
+
: null;
|
|
353
|
+
const fromPlugin = Array.isArray(resolvedPlugin.captureExclude)
|
|
354
|
+
? resolvedPlugin.captureExclude
|
|
355
|
+
: null;
|
|
356
|
+
_sources.captureExclude = fromFile
|
|
357
|
+
? "file"
|
|
358
|
+
: fromPlugin
|
|
359
|
+
? "pluginConfig"
|
|
360
|
+
: "default";
|
|
361
|
+
const entries = fromFile ?? fromPlugin ?? [];
|
|
362
|
+
return entries
|
|
363
|
+
.filter((v) => typeof v === "string" && v.trim())
|
|
364
|
+
.map((v) => v.trim());
|
|
365
|
+
})();
|
|
366
|
+
|
|
367
|
+
// --- captureSkipMarker: file > pluginConfig > default ---
|
|
368
|
+
const captureSkipMarker = (() => {
|
|
369
|
+
const fromFile =
|
|
370
|
+
typeof resolvedFile.captureSkipMarker === "string"
|
|
371
|
+
? resolvedFile.captureSkipMarker.trim()
|
|
372
|
+
: undefined;
|
|
373
|
+
const fromPlugin =
|
|
374
|
+
typeof resolvedPlugin.captureSkipMarker === "string"
|
|
375
|
+
? resolvedPlugin.captureSkipMarker.trim()
|
|
376
|
+
: undefined;
|
|
377
|
+
_sources.captureSkipMarker = fromFile
|
|
378
|
+
? "file"
|
|
379
|
+
: fromPlugin
|
|
380
|
+
? "pluginConfig"
|
|
381
|
+
: "default";
|
|
382
|
+
return fromFile || fromPlugin || "#nmem-skip";
|
|
383
|
+
})();
|
|
384
|
+
|
|
346
385
|
return {
|
|
347
386
|
sessionContext,
|
|
348
387
|
sessionDigest,
|
|
@@ -350,6 +389,8 @@ export function parseConfig(raw, logger) {
|
|
|
350
389
|
maxContextResults,
|
|
351
390
|
recallMinScore,
|
|
352
391
|
maxThreadMessageChars,
|
|
392
|
+
captureExclude,
|
|
393
|
+
captureSkipMarker,
|
|
353
394
|
apiUrl,
|
|
354
395
|
apiKey,
|
|
355
396
|
_sources,
|
package/src/context-engine.js
CHANGED
|
@@ -22,7 +22,12 @@
|
|
|
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
|
+
matchesExcludePattern,
|
|
29
|
+
triageAndDistill,
|
|
30
|
+
} from "./hooks/capture.js";
|
|
26
31
|
import {
|
|
27
32
|
MAX_QUERY_LENGTH,
|
|
28
33
|
SHORT_QUERY_THRESHOLD,
|
|
@@ -342,8 +347,23 @@ export function createNowledgeMemContextEngineFactory(client, cfg, logger) {
|
|
|
342
347
|
}) {
|
|
343
348
|
if (isHeartbeat) return;
|
|
344
349
|
|
|
350
|
+
// Normalize sessionKey consistently with hook handlers
|
|
351
|
+
const normalizedKey = String(sessionKey || sessionId || "");
|
|
352
|
+
|
|
353
|
+
// Capture exclusion: pattern-based and marker-based filters
|
|
354
|
+
if (matchesExcludePattern(normalizedKey, cfg.captureExclude)) {
|
|
355
|
+
logger.debug?.(`ce: skipped excluded session ${normalizedKey}`);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (hasSkipMarker(messages, cfg.captureSkipMarker)) {
|
|
359
|
+
logger.debug?.(
|
|
360
|
+
`ce: skipped session with skip marker ${normalizedKey}`,
|
|
361
|
+
);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
345
365
|
const event = { messages, sessionFile };
|
|
346
|
-
const ctx = { sessionId, sessionKey };
|
|
366
|
+
const ctx = { sessionId, sessionKey: normalizedKey };
|
|
347
367
|
|
|
348
368
|
// 1. Always capture thread (idempotent, deduped)
|
|
349
369
|
const captureResult = await appendOrCreateThread({
|
package/src/hooks/capture.js
CHANGED
|
@@ -12,6 +12,8 @@ export const MIN_MESSAGES_FOR_DISTILL = 4;
|
|
|
12
12
|
// Evicted opportunistically when new entries are set (see _setLastCapture).
|
|
13
13
|
const _lastCaptureAt = new Map();
|
|
14
14
|
const _MAX_COOLDOWN_ENTRIES = 200;
|
|
15
|
+
const _syncedMessageCounts = new Map();
|
|
16
|
+
const _MAX_SYNC_CURSOR_ENTRIES = 500;
|
|
15
17
|
|
|
16
18
|
function _setLastCapture(threadId, now) {
|
|
17
19
|
_lastCaptureAt.set(threadId, now);
|
|
@@ -24,6 +26,77 @@ function _setLastCapture(threadId, now) {
|
|
|
24
26
|
}
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
function _setSyncedMessageCount(threadId, count) {
|
|
30
|
+
if (!threadId || !Number.isFinite(count) || count < 0) return;
|
|
31
|
+
_syncedMessageCounts.set(threadId, Math.trunc(count));
|
|
32
|
+
if (_syncedMessageCounts.size > _MAX_SYNC_CURSOR_ENTRIES) {
|
|
33
|
+
const excess = _syncedMessageCounts.size - _MAX_SYNC_CURSOR_ENTRIES;
|
|
34
|
+
let removed = 0;
|
|
35
|
+
for (const key of _syncedMessageCounts.keys()) {
|
|
36
|
+
_syncedMessageCounts.delete(key);
|
|
37
|
+
removed += 1;
|
|
38
|
+
if (removed >= excess) break;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Capture exclusion filters
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Compile a glob pattern (where `*` matches within a colon-delimited segment)
|
|
49
|
+
* into a RegExp. Results are cached since patterns are immutable after parse.
|
|
50
|
+
*/
|
|
51
|
+
const _compiledPatterns = new Map();
|
|
52
|
+
function _compileGlob(pattern) {
|
|
53
|
+
const key = String(pattern).toLowerCase();
|
|
54
|
+
let re = _compiledPatterns.get(key);
|
|
55
|
+
if (!re) {
|
|
56
|
+
re = new RegExp(
|
|
57
|
+
"^" +
|
|
58
|
+
key
|
|
59
|
+
.replace(/[.+?^${}()|[\]\\]/g, "\\$&")
|
|
60
|
+
.replace(/\*/g, "[^:]*") +
|
|
61
|
+
"$",
|
|
62
|
+
);
|
|
63
|
+
_compiledPatterns.set(key, re);
|
|
64
|
+
}
|
|
65
|
+
return re;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Test whether a session key matches any exclusion glob pattern.
|
|
70
|
+
* Glob `*` matches within a colon-delimited segment (not across colons).
|
|
71
|
+
* Example: "agent:*:cron:*" matches "agent:main:cron:abc123"
|
|
72
|
+
*
|
|
73
|
+
* Exported for reuse by Context Engine and other capture paths.
|
|
74
|
+
*/
|
|
75
|
+
export function matchesExcludePattern(sessionKey, patterns) {
|
|
76
|
+
if (!Array.isArray(patterns) || patterns.length === 0) return false;
|
|
77
|
+
const key = String(sessionKey || "").toLowerCase();
|
|
78
|
+
return patterns.some((pattern) => _compileGlob(pattern).test(key));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Check if any message contains the skip marker text.
|
|
83
|
+
* Scans both raw message content and nested message objects.
|
|
84
|
+
*
|
|
85
|
+
* Exported for reuse by Context Engine and other capture paths.
|
|
86
|
+
*/
|
|
87
|
+
export function hasSkipMarker(messages, marker) {
|
|
88
|
+
if (!marker || typeof marker !== "string" || !Array.isArray(messages)) return false;
|
|
89
|
+
const markerLc = marker.toLowerCase();
|
|
90
|
+
return messages.some((msg) => {
|
|
91
|
+
const text = extractText(msg?.content ?? msg?.message?.content);
|
|
92
|
+
return text.toLowerCase().includes(markerLc);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// Message normalization utilities
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
|
|
27
100
|
export function truncate(text, max = DEFAULT_MAX_MESSAGE_CHARS) {
|
|
28
101
|
const str = String(text || "").trim();
|
|
29
102
|
if (!str) return "";
|
|
@@ -181,8 +254,9 @@ export async function appendOrCreateThread({
|
|
|
181
254
|
ctx,
|
|
182
255
|
reason,
|
|
183
256
|
maxMessageChars = DEFAULT_MAX_MESSAGE_CHARS,
|
|
257
|
+
resolvedMessages,
|
|
184
258
|
}) {
|
|
185
|
-
const rawMessages = await resolveHookMessages(event);
|
|
259
|
+
const rawMessages = resolvedMessages ?? (await resolveHookMessages(event));
|
|
186
260
|
if (!Array.isArray(rawMessages) || rawMessages.length === 0) return;
|
|
187
261
|
|
|
188
262
|
const threadId = buildStableThreadId(event, ctx);
|
|
@@ -194,7 +268,7 @@ export async function appendOrCreateThread({
|
|
|
194
268
|
.filter(Boolean);
|
|
195
269
|
if (normalized.length === 0) return;
|
|
196
270
|
|
|
197
|
-
const
|
|
271
|
+
const allMessages = normalized.map((message, index) => ({
|
|
198
272
|
role: message.role,
|
|
199
273
|
content: message.content,
|
|
200
274
|
timestamp: message.timestamp,
|
|
@@ -210,16 +284,46 @@ export async function appendOrCreateThread({
|
|
|
210
284
|
session_id: sessionId || undefined,
|
|
211
285
|
},
|
|
212
286
|
}));
|
|
213
|
-
|
|
287
|
+
|
|
288
|
+
let syncedCount = _syncedMessageCounts.get(threadId);
|
|
289
|
+
if (syncedCount === undefined) {
|
|
290
|
+
syncedCount = await client.getThreadMessageCount(threadId);
|
|
291
|
+
if (syncedCount !== null) {
|
|
292
|
+
_setSyncedMessageCount(threadId, syncedCount);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (typeof syncedCount === "number" && syncedCount > allMessages.length) {
|
|
297
|
+
// OpenClaw compaction can shrink the active transcript after we already
|
|
298
|
+
// stored the pre-compaction history. Reset the local cursor to the new
|
|
299
|
+
// compacted length so future turns append only the post-compaction tail.
|
|
300
|
+
_setSyncedMessageCount(threadId, allMessages.length);
|
|
301
|
+
return { threadId, normalized, messagesAdded: 0 };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const appendStart =
|
|
305
|
+
typeof syncedCount === "number" && syncedCount > 0
|
|
306
|
+
? Math.min(syncedCount, allMessages.length)
|
|
307
|
+
: 0;
|
|
308
|
+
const newMessages = allMessages.slice(appendStart);
|
|
309
|
+
if (newMessages.length === 0) {
|
|
310
|
+
return { threadId, normalized, messagesAdded: 0 };
|
|
311
|
+
}
|
|
312
|
+
const idempotencyKey = buildAppendIdempotencyKey(
|
|
313
|
+
threadId,
|
|
314
|
+
reason,
|
|
315
|
+
newMessages,
|
|
316
|
+
);
|
|
214
317
|
|
|
215
318
|
try {
|
|
216
319
|
const appended = await client.appendThread({
|
|
217
320
|
threadId,
|
|
218
|
-
messages,
|
|
321
|
+
messages: newMessages,
|
|
219
322
|
deduplicate: true,
|
|
220
323
|
idempotencyKey,
|
|
221
324
|
});
|
|
222
325
|
const added = appended.messagesAdded ?? 0;
|
|
326
|
+
_setSyncedMessageCount(threadId, allMessages.length);
|
|
223
327
|
logger.info(
|
|
224
328
|
`capture: appended ${added} messages to ${threadId} (${reason || "event"})`,
|
|
225
329
|
);
|
|
@@ -236,13 +340,14 @@ export async function appendOrCreateThread({
|
|
|
236
340
|
const createdId = await client.createThread({
|
|
237
341
|
threadId,
|
|
238
342
|
title,
|
|
239
|
-
messages,
|
|
343
|
+
messages: allMessages,
|
|
240
344
|
source: "openclaw",
|
|
241
345
|
});
|
|
346
|
+
_setSyncedMessageCount(threadId, allMessages.length);
|
|
242
347
|
logger.info(
|
|
243
|
-
`capture: created thread ${createdId} with ${
|
|
348
|
+
`capture: created thread ${createdId} with ${allMessages.length} messages (${reason || "event"})`,
|
|
244
349
|
);
|
|
245
|
-
return { threadId, normalized, messagesAdded:
|
|
350
|
+
return { threadId, normalized, messagesAdded: allMessages.length };
|
|
246
351
|
} catch (err) {
|
|
247
352
|
const message = err instanceof Error ? err.message : String(err);
|
|
248
353
|
logger.warn(`capture: thread create failed for ${threadId}: ${message}`);
|
|
@@ -354,11 +459,33 @@ export async function triageAndDistill({
|
|
|
354
459
|
*
|
|
355
460
|
* When the context engine is active, this hook is a no-op — afterTurn
|
|
356
461
|
* handles capture and distillation through the CE lifecycle.
|
|
462
|
+
*
|
|
463
|
+
* Heartbeat sessions (ctx.trigger === "heartbeat") are skipped — they
|
|
464
|
+
* produce repetitive status pings that aren't worth preserving.
|
|
465
|
+
* Other sessions use incremental tail sync: we preserve the real
|
|
466
|
+
* transcript, but only append messages that are not already stored.
|
|
357
467
|
*/
|
|
358
468
|
export function buildAgentEndCaptureHandler(client, cfg, logger) {
|
|
359
469
|
return async (event, ctx) => {
|
|
360
470
|
if (ceState.active) return;
|
|
361
471
|
if (!event?.success) return;
|
|
472
|
+
if (ctx?.trigger === "heartbeat") return;
|
|
473
|
+
|
|
474
|
+
// Layer 1: pattern-based exclusion (e.g. cron jobs, subagent sessions)
|
|
475
|
+
const sessionKey = String(ctx?.sessionKey || ctx?.sessionId || "");
|
|
476
|
+
if (matchesExcludePattern(sessionKey, cfg.captureExclude)) {
|
|
477
|
+
logger.debug?.(`capture: skipped excluded session ${sessionKey}`);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Layer 2: marker-based exclusion (user typed #nmem-skip in conversation)
|
|
482
|
+
const resolvedMessages = await resolveHookMessages(event);
|
|
483
|
+
if (hasSkipMarker(resolvedMessages, cfg.captureSkipMarker)) {
|
|
484
|
+
logger.debug?.(
|
|
485
|
+
`capture: skipped session with skip marker ${sessionKey}`,
|
|
486
|
+
);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
362
489
|
|
|
363
490
|
const captureResult = await appendOrCreateThread({
|
|
364
491
|
client,
|
|
@@ -367,6 +494,7 @@ export function buildAgentEndCaptureHandler(client, cfg, logger) {
|
|
|
367
494
|
ctx,
|
|
368
495
|
reason: "agent_end",
|
|
369
496
|
maxMessageChars: cfg.maxThreadMessageChars,
|
|
497
|
+
resolvedMessages,
|
|
370
498
|
});
|
|
371
499
|
|
|
372
500
|
await triageAndDistill({ client, cfg, logger, captureResult, ctx });
|
|
@@ -379,10 +507,30 @@ export function buildAgentEndCaptureHandler(client, cfg, logger) {
|
|
|
379
507
|
*
|
|
380
508
|
* When the context engine is active, this hook is a no-op — afterTurn
|
|
381
509
|
* handles capture through the CE lifecycle.
|
|
510
|
+
*
|
|
511
|
+
* Heartbeat sessions are skipped (same rationale as agent_end).
|
|
382
512
|
*/
|
|
383
|
-
export function buildBeforeResetCaptureHandler(client,
|
|
513
|
+
export function buildBeforeResetCaptureHandler(client, cfg, logger) {
|
|
384
514
|
return async (event, ctx) => {
|
|
385
515
|
if (ceState.active) return;
|
|
516
|
+
if (ctx?.trigger === "heartbeat") return;
|
|
517
|
+
|
|
518
|
+
// Layer 1: pattern-based exclusion
|
|
519
|
+
const sessionKey = String(ctx?.sessionKey || ctx?.sessionId || "");
|
|
520
|
+
if (matchesExcludePattern(sessionKey, cfg.captureExclude)) {
|
|
521
|
+
logger.debug?.(`capture: skipped excluded session ${sessionKey}`);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Layer 2: marker-based exclusion
|
|
526
|
+
const resolvedMessages = await resolveHookMessages(event);
|
|
527
|
+
if (hasSkipMarker(resolvedMessages, cfg.captureSkipMarker)) {
|
|
528
|
+
logger.debug?.(
|
|
529
|
+
`capture: skipped session with skip marker ${sessionKey}`,
|
|
530
|
+
);
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
|
|
386
534
|
const reason = typeof event?.reason === "string" ? event.reason : undefined;
|
|
387
535
|
await appendOrCreateThread({
|
|
388
536
|
client,
|
|
@@ -390,7 +538,8 @@ export function buildBeforeResetCaptureHandler(client, _cfg, logger) {
|
|
|
390
538
|
event,
|
|
391
539
|
ctx,
|
|
392
540
|
reason,
|
|
393
|
-
maxMessageChars:
|
|
541
|
+
maxMessageChars: cfg.maxThreadMessageChars,
|
|
542
|
+
resolvedMessages,
|
|
394
543
|
});
|
|
395
544
|
};
|
|
396
545
|
}
|
package/src/index.js
CHANGED
|
@@ -54,8 +54,14 @@ export default {
|
|
|
54
54
|
api.registerTool(createThreadSearchTool(client, logger));
|
|
55
55
|
api.registerTool(createThreadFetchTool(client, logger));
|
|
56
56
|
|
|
57
|
-
// Diagnostics
|
|
58
|
-
|
|
57
|
+
// Diagnostics — pass runtime config so the status tool can detect:
|
|
58
|
+
// - plugins.allow missing or not including this plugin
|
|
59
|
+
// - memory slot pointing elsewhere (e.g. memory-core)
|
|
60
|
+
const memorySlot = api.config?.plugins?.slots?.memory;
|
|
61
|
+
const pluginsAllow = api.config?.plugins?.allow;
|
|
62
|
+
api.registerTool(
|
|
63
|
+
createStatusTool(client, logger, cfg, { memorySlot, pluginsAllow }),
|
|
64
|
+
);
|
|
59
65
|
|
|
60
66
|
// --- Context Engine registration ---
|
|
61
67
|
// When the user sets `plugins.slots.contextEngine: "nowledge-mem"`,
|
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,60 @@ 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 — detect misconfiguration early
|
|
63
|
+
const memorySlot = runtimeInfo.memorySlot;
|
|
64
|
+
details.memorySlot = memorySlot ?? "(unknown)";
|
|
65
|
+
if (memorySlot && memorySlot !== "openclaw-nowledge-mem") {
|
|
66
|
+
lines.push(
|
|
67
|
+
`⚠ Memory slot: "${memorySlot}" — Nowledge Mem is NOT the active memory plugin.`,
|
|
68
|
+
);
|
|
69
|
+
lines.push(
|
|
70
|
+
" Only memory_search and memory_get will work. Other tools (save, connections, timeline, etc.) are inactive.",
|
|
71
|
+
);
|
|
72
|
+
lines.push(
|
|
73
|
+
' Fix: run "openclaw plugins install @nowledge/openclaw-nowledge-mem" or set plugins.slots.memory to "openclaw-nowledge-mem" in your config.',
|
|
74
|
+
);
|
|
75
|
+
lines.push("");
|
|
76
|
+
} else if (memorySlot === "openclaw-nowledge-mem") {
|
|
77
|
+
lines.push("Memory slot: openclaw-nowledge-mem (active)");
|
|
78
|
+
}
|
|
79
|
+
|
|
26
80
|
// 1. Mode + connection target
|
|
27
81
|
const remote = !isDefaultApiUrl(cfg.apiUrl);
|
|
28
82
|
const mode = remote ? "remote" : "local";
|