@nowledge/openclaw-nowledge-mem 0.6.15 → 0.7.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 +33 -1
- package/CLAUDE.md +59 -5
- package/README.md +5 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/ce-state.js +8 -0
- package/src/client.js +50 -87
- package/src/context-engine.js +406 -0
- package/src/hooks/behavioral.js +8 -2
- package/src/hooks/capture.js +166 -105
- package/src/hooks/recall.js +38 -26
- package/src/index.js +22 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,38 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the Nowledge Mem OpenClaw plugin will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **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.
|
|
10
|
+
- **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.
|
|
11
|
+
- **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.
|
|
12
|
+
- **Removed lossy repetitive-session collapse.** The temporary content-based dedup workaround for cron-style sessions has been removed so conversation structure is preserved faithfully.
|
|
13
|
+
|
|
14
|
+
## [0.7.1] - 2026-03-23
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **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.
|
|
19
|
+
|
|
20
|
+
## [0.7.0] - 2026-03-23
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- **Context Engine support.** The plugin now registers a full OpenClaw Context Engine alongside its memory slot. When you set `plugins.slots.contextEngine: "nowledge-mem"` in your OpenClaw config, the engine takes over context assembly, capturing, and compaction — replacing the hook-based approach with richer lifecycle integration:
|
|
25
|
+
- **`assemble()`** — behavioral guidance and recalled memories injected via `systemPromptAddition` (system-prompt space, cache-friendly). Replaces the behavioral and recall hooks.
|
|
26
|
+
- **`afterTurn()`** — continuous thread capture and triage/distillation after every turn, not just session end. More granular than the `agent_end` hook.
|
|
27
|
+
- **`compact()`** — memory-aware compaction. When compacting old messages, the compactor is told which key decisions and learnings are already saved in your knowledge graph, so it can reference them concisely rather than losing them in summarization.
|
|
28
|
+
- **`prepareSubagentSpawn()`** — when OpenClaw spawns parallel research agents, child sessions inherit your Working Memory and recently recalled memories automatically.
|
|
29
|
+
- **`bootstrap()`** — pre-warms Working Memory on session start for instant first-turn context.
|
|
30
|
+
- **`dispose()`** — clean session teardown.
|
|
31
|
+
- **Backward compatible.** When the CE slot is not activated, hooks continue working exactly as before. No config changes required for existing users.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- **Recalled memories no longer hurt prompt cache.** The recall hook now injects context via `appendSystemContext` (system-prompt space) instead of `prependContext` (user-message space). This preserves OpenClaw's prompt cache across turns. The fix applies to both the hook path and the new CE path.
|
|
36
|
+
|
|
5
37
|
## [0.6.15] - 2026-03-18
|
|
6
38
|
|
|
7
39
|
### Changed
|
|
@@ -62,7 +94,7 @@ All notable changes to the Nowledge Mem OpenClaw plugin will be documented in th
|
|
|
62
94
|
|
|
63
95
|
### Added
|
|
64
96
|
|
|
65
|
-
-
|
|
97
|
+
- `recallMinScore` config option (0-100, default 0): Minimum relevance score threshold for auto-recalled memories. Set to e.g. 30 to filter out low-confidence results. Configurable via OpenClaw Config UI, config file, or `NMEM_RECALL_MIN_SCORE` env var.
|
|
66
98
|
|
|
67
99
|
## [0.6.8] - 2026-02-27
|
|
68
100
|
|
package/CLAUDE.md
CHANGED
|
@@ -4,11 +4,12 @@ Continuation guide for `community/nowledge-mem-openclaw-plugin`.
|
|
|
4
4
|
|
|
5
5
|
## Scope
|
|
6
6
|
|
|
7
|
-
- Plugin target: OpenClaw plugin runtime (memory slot
|
|
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
10
|
- OpenClaw minimum: `2026.3.7` (`appendSystemContext` / system-context guidance required)
|
|
11
11
|
- Architecture: **CLI-first via OpenClaw runtime** - all CLI execution goes through `api.runtime.system.runCommandWithTimeout`, not direct `child_process`
|
|
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.
|
|
12
13
|
- Remote mode: `~/.nowledge-mem/config.json` (shared) or OpenClaw dashboard. Legacy `openclaw.json` still honored.
|
|
13
14
|
|
|
14
15
|
## Design Philosophy
|
|
@@ -27,14 +28,16 @@ Reflects Nowledge Mem's genuine v0.6 strengths:
|
|
|
27
28
|
|
|
28
29
|
```
|
|
29
30
|
src/
|
|
30
|
-
index.js - plugin registration (tools, hooks, commands, CLI)
|
|
31
|
+
index.js - plugin registration (tools, hooks, CE, commands, CLI)
|
|
32
|
+
context-engine.js - Context Engine factory: assemble, afterTurn, compact, subagent hooks
|
|
33
|
+
ce-state.js - shared { active } flag for CE/hook coordination
|
|
31
34
|
client.js - CLI wrapper with API fallback; async runtime command execution; credential handling
|
|
32
35
|
spawn-env.js - env-only credential injection for the nmem runner
|
|
33
36
|
config.js - config cascade: openclaw.json (legacy) > pluginConfig > config.json (credentials) > env > defaults
|
|
34
37
|
hooks/
|
|
35
|
-
behavioral.js - always-on behavioral guidance (~50 tokens/turn)
|
|
36
|
-
recall.js - before_prompt_build: inject Working Memory + recalled memories
|
|
37
|
-
capture.js - thread capture + LLM triage/distillation
|
|
38
|
+
behavioral.js - always-on behavioral guidance (~50 tokens/turn); no-ops when CE active
|
|
39
|
+
recall.js - before_prompt_build: inject Working Memory + recalled memories; no-ops when CE active
|
|
40
|
+
capture.js - thread capture + LLM triage/distillation; shared functions used by both hooks and CE
|
|
38
41
|
tools/
|
|
39
42
|
memory-search.js - OpenClaw compat; multi-signal; bi-temporal; relevance_reason; sourceThreadId
|
|
40
43
|
memory-get.js - OpenClaw compat; supports MEMORY.md alias; sourceThreadId
|
|
@@ -53,6 +56,50 @@ openclaw.plugin.json - manifest + config schema (version, uiHints, configSchema,
|
|
|
53
56
|
~/.nowledge-mem/config.json - shared credentials (apiUrl/apiKey) read by all Nowledge Mem tools
|
|
54
57
|
```
|
|
55
58
|
|
|
59
|
+
## Context Engine (CE) Architecture
|
|
60
|
+
|
|
61
|
+
The plugin registers both a **memory slot** (`kind: "memory"`) and a **context engine** (`api.registerContextEngine`). These are independent registrations:
|
|
62
|
+
|
|
63
|
+
- **Memory slot**: provides `memory_search` + `memory_get`, activates OpenClaw's "Memory Recall" system prompt section. Always active.
|
|
64
|
+
- **Context engine**: activated when user sets `plugins.slots.contextEngine: "nowledge-mem"`. Replaces hooks with richer CE lifecycle.
|
|
65
|
+
|
|
66
|
+
### CE vs Hooks (dual-path design)
|
|
67
|
+
|
|
68
|
+
A shared `ceState.active` flag (in `ce-state.js`) coordinates the two paths:
|
|
69
|
+
|
|
70
|
+
| Lifecycle | CE active | CE inactive (hooks) |
|
|
71
|
+
|-----------|-----------|---------------------|
|
|
72
|
+
| Behavioral guidance | `assemble()` → `systemPromptAddition` | `before_prompt_build` → `appendSystemContext` |
|
|
73
|
+
| Memory recall | `assemble()` → `systemPromptAddition` | `before_prompt_build` → `appendSystemContext` |
|
|
74
|
+
| Thread capture | `afterTurn()` (every turn) | `agent_end` / `after_compaction` / `before_reset` |
|
|
75
|
+
| Triage + distill | `afterTurn()` | `agent_end` only |
|
|
76
|
+
| Compaction | `compact()` with memory-aware instructions | None (OpenClaw legacy) |
|
|
77
|
+
| Subagent context | `prepareSubagentSpawn()` + `onSubagentEnded()` | None |
|
|
78
|
+
| Session init | `bootstrap()` pre-warms WM | None |
|
|
79
|
+
|
|
80
|
+
### Key design decisions
|
|
81
|
+
|
|
82
|
+
- **`ownsCompaction: false`**: we enhance compaction instructions with memory context, but delegate the actual compaction to OpenClaw's runtime via `delegateCompactionToRuntime()`.
|
|
83
|
+
- **Messages pass through unchanged**: `assemble()` returns the same messages it receives. We only add `systemPromptAddition`. We never own message selection.
|
|
84
|
+
- **Per-session state**: `_sessions` map (bounded at 100 entries) caches Working Memory and recalled memories per session. `_childContext` map caches subagent context.
|
|
85
|
+
- **Cache-friendly injection**: both CE (`systemPromptAddition`) and hooks (`appendSystemContext`) inject into system-prompt space. Never use `prependContext` (user-message space, breaks cache).
|
|
86
|
+
|
|
87
|
+
### Activation
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
// openclaw.json
|
|
91
|
+
{
|
|
92
|
+
"plugins": {
|
|
93
|
+
"slots": {
|
|
94
|
+
"memory": "openclaw-nowledge-mem",
|
|
95
|
+
"contextEngine": "nowledge-mem"
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
When `contextEngine` points elsewhere (or is absent), hooks handle everything. No config change needed for existing users.
|
|
102
|
+
|
|
56
103
|
## Tool Surface (10 tools)
|
|
57
104
|
|
|
58
105
|
### OpenClaw Memory Slot (required for system prompt activation)
|
|
@@ -178,6 +225,13 @@ After bumping, commit inside the `community/` submodule, then stage the updated
|
|
|
178
225
|
4. **`unit_type` requires rebuilt backend** - `MemoryCreateRequest` includes `unit_type` (fixed). Restart backend after rebuild.
|
|
179
226
|
5. **Working Memory full-overwrite only via API** - the API (`PUT /agent/working-memory`) still takes full content. The section-level patch is implemented purely client-side. This is acceptable; the Knowledge Agent regenerates WM each morning anyway.
|
|
180
227
|
|
|
228
|
+
## Cache Safety Rules
|
|
229
|
+
|
|
230
|
+
- **Hooks**: always return `{ appendSystemContext }` — never `{ prependContext }`. `prependContext` injects into user-message space and breaks Anthropic's system prompt cache prefix on every turn.
|
|
231
|
+
- **CE assemble()**: return `systemPromptAddition` — same cache-safe position as `appendSystemContext`.
|
|
232
|
+
- **Never** embed dynamic content (timestamps, per-turn IDs) in system-prompt-level injection. Static behavioral guidance is fine; recalled memories are fine (they append after the cached prefix).
|
|
233
|
+
|
|
234
|
+
|
|
181
235
|
## Non-Goals
|
|
182
236
|
|
|
183
237
|
- Do NOT add `nowledge_mem_search` - `memory_search` covers it.
|
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
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/src/ce-state.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared mutable state flag for context engine activation.
|
|
3
|
+
*
|
|
4
|
+
* When the CE is bootstrapped, `active` is set to true. Event hooks
|
|
5
|
+
* check this flag to avoid duplicate work — the CE's lifecycle methods
|
|
6
|
+
* (assemble, afterTurn, etc.) replace hook behavior when active.
|
|
7
|
+
*/
|
|
8
|
+
export const ceState = { active: false };
|
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)
|
|
@@ -82,8 +78,9 @@ export class NowledgeMemClient {
|
|
|
82
78
|
this.nmemCmd = null;
|
|
83
79
|
this.nmemCmdPromise = null;
|
|
84
80
|
// Resolved once from config + env (config wins over env, both win over default)
|
|
85
|
-
this._apiUrl =
|
|
86
|
-
(
|
|
81
|
+
this._apiUrl = (
|
|
82
|
+
(credentials.apiUrl || "").trim() || "http://127.0.0.1:14242"
|
|
83
|
+
).replace(/\/+$/, "");
|
|
87
84
|
this._apiKey = (credentials.apiKey || "").trim();
|
|
88
85
|
}
|
|
89
86
|
|
|
@@ -558,42 +555,12 @@ export class NowledgeMemClient {
|
|
|
558
555
|
throw new Error("createThread requires at least one message");
|
|
559
556
|
}
|
|
560
557
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
"-t",
|
|
568
|
-
normalizedTitle,
|
|
569
|
-
"-m",
|
|
570
|
-
JSON.stringify(messages),
|
|
571
|
-
"-s",
|
|
572
|
-
String(source),
|
|
573
|
-
];
|
|
574
|
-
if (threadId) {
|
|
575
|
-
args.push("--id", String(threadId));
|
|
576
|
-
}
|
|
577
|
-
data = await this.execJson(args);
|
|
578
|
-
} catch (err) {
|
|
579
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
580
|
-
const needsApiFallback =
|
|
581
|
-
Boolean(threadId) &&
|
|
582
|
-
(message.includes("unrecognized arguments: --id") ||
|
|
583
|
-
message.includes("invalid choice"));
|
|
584
|
-
if (!needsApiFallback) {
|
|
585
|
-
throw err;
|
|
586
|
-
}
|
|
587
|
-
this.logger.warn(
|
|
588
|
-
"createThread: CLI missing --id support, falling back to API",
|
|
589
|
-
);
|
|
590
|
-
data = await this.apiJson("POST", "/threads", {
|
|
591
|
-
thread_id: String(threadId),
|
|
592
|
-
title: normalizedTitle,
|
|
593
|
-
source: String(source),
|
|
594
|
-
messages,
|
|
595
|
-
});
|
|
596
|
-
}
|
|
558
|
+
const data = await this.apiJson("POST", "/threads", {
|
|
559
|
+
...(threadId ? { thread_id: String(threadId) } : {}),
|
|
560
|
+
title: normalizedTitle,
|
|
561
|
+
source: String(source),
|
|
562
|
+
messages,
|
|
563
|
+
});
|
|
597
564
|
|
|
598
565
|
return String(
|
|
599
566
|
data.id ?? data.thread?.thread_id ?? data.thread_id ?? "created",
|
|
@@ -614,50 +581,46 @@ export class NowledgeMemClient {
|
|
|
614
581
|
return { messagesAdded: 0, totalMessages: 0 };
|
|
615
582
|
}
|
|
616
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
|
+
|
|
617
607
|
try {
|
|
618
|
-
const args = [
|
|
619
|
-
"--json",
|
|
620
|
-
"t",
|
|
621
|
-
"append",
|
|
622
|
-
normalizedThreadId,
|
|
623
|
-
"-m",
|
|
624
|
-
JSON.stringify(messages),
|
|
625
|
-
...(deduplicate ? [] : ["--no-deduplicate"]),
|
|
626
|
-
];
|
|
627
|
-
if (idempotencyKey) {
|
|
628
|
-
args.push("--idempotency-key", String(idempotencyKey));
|
|
629
|
-
}
|
|
630
|
-
const data = await this.execJson(args);
|
|
631
|
-
return {
|
|
632
|
-
messagesAdded: Number(data.messages_added ?? 0),
|
|
633
|
-
totalMessages: Number(data.total_messages ?? 0),
|
|
634
|
-
};
|
|
635
|
-
} catch (err) {
|
|
636
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
637
|
-
const needsApiFallback =
|
|
638
|
-
message.includes("invalid choice") ||
|
|
639
|
-
message.includes("unrecognized arguments");
|
|
640
|
-
if (!needsApiFallback) {
|
|
641
|
-
throw err;
|
|
642
|
-
}
|
|
643
|
-
this.logger.warn(
|
|
644
|
-
"appendThread: CLI missing append support, falling back to API",
|
|
645
|
-
);
|
|
646
608
|
const data = await this.apiJson(
|
|
647
|
-
"
|
|
648
|
-
`/threads/${encodeURIComponent(normalizedThreadId)}
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
deduplicate,
|
|
652
|
-
...(idempotencyKey
|
|
653
|
-
? { idempotency_key: String(idempotencyKey) }
|
|
654
|
-
: {}),
|
|
655
|
-
},
|
|
609
|
+
"GET",
|
|
610
|
+
`/threads/${encodeURIComponent(normalizedThreadId)}?limit=1`,
|
|
611
|
+
undefined,
|
|
612
|
+
15_000,
|
|
656
613
|
);
|
|
657
|
-
return
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
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;
|
|
661
624
|
}
|
|
662
625
|
}
|
|
663
626
|
|