@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 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
- - **`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.
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 provider)
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 at session lifecycle events
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 a config file at `~/.nowledge-mem/openclaw.json`:
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
- The `apiKey` is injected as `NMEM_API_KEY` into the nmem CLI process. Never passed as a CLI argument, never logged.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "openclaw-nowledge-mem",
3
- "version": "0.6.15",
3
+ "version": "0.7.1",
4
4
  "kind": "memory",
5
5
  "skills": ["skills/memory-guide"],
6
6
  "uiHints": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowledge/openclaw-nowledge-mem",
3
- "version": "0.6.15",
3
+ "version": "0.7.1",
4
4
  "type": "module",
5
5
  "description": "Nowledge Mem memory plugin for OpenClaw, local-first personal knowledge base",
6
6
  "author": {
@@ -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 for local-first and remote operations.
57
+ * Nowledge Mem client. Wraps the nmem CLI plus direct API calls.
58
58
  *
59
- * All operations go through the CLI first. This means:
60
- * - Local mode: CLI uses http://127.0.0.1:14242 automatically
61
- * - Remote mode: configure via apiUrl + apiKey (plugin config or env vars)
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
- ((credentials.apiUrl || "").trim() || "http://127.0.0.1:14242").replace(/\/+$/, "");
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
- let data;
562
- try {
563
- const args = [
564
- "--json",
565
- "t",
566
- "create",
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
- "POST",
648
- `/threads/${encodeURIComponent(normalizedThreadId)}/append`,
649
- {
650
- messages,
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
- messagesAdded: Number(data.messages_added ?? 0),
659
- totalMessages: Number(data.total_messages ?? 0),
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