@nowledge/openclaw-nowledge-mem 0.8.23 → 0.8.25

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
@@ -4,6 +4,20 @@ All notable changes to the Nowledge Mem OpenClaw plugin will be documented in th
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## [0.8.25] - 2026-06-06
8
+
9
+ ### Improved
10
+
11
+ - `nowledge_mem_context`, `sessionContext`, and Context Engine prompt injection now load Context Bundle when available, then fall back to Working Memory for older `nmem` clients. OpenClaw can receive owner identity, AI Identity, active space, active rules, and current priorities through the existing context surface without a tool-name change.
12
+
13
+ ## [0.8.24] - 2026-05-15
14
+
15
+ ### Fixed
16
+
17
+ - OpenClaw session capture now handles newer per-turn `agent_end` hook payloads from Codex app-server runtimes. The plugin no longer mistakes those shorter batches for transcript compaction, so continued turns keep syncing into the same Mem thread after an OpenClaw upgrade.
18
+ - Captured Codex app-server messages now use OpenClaw's stable mirror identity for Mem-side dedupe, keeping Context Engine `afterTurn` and hook backstop capture idempotent when both paths see the same turn.
19
+ - Capture setup no longer depends on runtime-only OpenClaw SDK imports, keeping the plugin loadable on existing OpenClaw installer builds while still matching the host session-key contract.
20
+
7
21
  ## [0.8.23] - 2026-05-02
8
22
 
9
23
  ### Fixed
package/CLAUDE.md CHANGED
@@ -44,7 +44,7 @@ src/
44
44
  memory-search.js - OpenClaw compat; multi-signal; bi-temporal; relevance_reason; sourceThreadId
45
45
  memory-get.js - OpenClaw compat; supports MEMORY.md alias; sourceThreadId
46
46
  save.js - structured capture: unit_type, labels, temporal, importance; pre-save dedup check
47
- context.js - Working Memory daily briefing (read + section-level patch)
47
+ context.js - startup context read + Working Memory section-level patch
48
48
  connections.js - graph exploration: edge types, relationship strength, provenance
49
49
  timeline.js - activity feed: daily grouping, event_type filter, memoryId hints
50
50
  forget.js - memory deletion by ID or search
@@ -66,7 +66,7 @@ When `corpusSupplement: true`, the plugin registers a `MemoryCorpusSupplement` v
66
66
 
67
67
  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.
68
68
  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.
69
- 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
+ 3. **Dedup**: When active, the recall hook and CE `assemble()` skip their own search-based recall (memories section only). Startup context injection continues as before.
70
70
 
71
71
  ### When to enable
72
72
 
@@ -132,7 +132,7 @@ When `contextEngine` points elsewhere (or is absent), hooks handle everything. N
132
132
 
133
133
  ### Nowledge Mem Native (differentiators)
134
134
  - `nowledge_mem_save` - structured capture: `unit_type`, `labels[]`, `event_start`, `event_end`, `temporal_context`, `importance`. All fields wired to CLI and API.
135
- - `nowledge_mem_context` - Working Memory daily briefing. Read-only by default; supports section-level patch via `patch_section` + `patch_content`/`patch_append` params (uses `nmem wm patch` client-side read-modify-write).
135
+ - `nowledge_mem_context` - startup context. Read-only by default: Context Bundle when available, Working Memory fallback on older `nmem` clients. Also supports Working Memory section-level patch via `patch_section` + `patch_content`/`patch_append` params (uses `nmem wm patch` client-side read-modify-write).
136
136
  - `nowledge_mem_connections` - graph exploration. Edges JOIN-ed to nodes by type: CRYSTALLIZED_FROM (crystal → source memories), EVOLVES (with sub-relations: supersedes/enriches/confirms/challenges), SOURCED_FROM (document provenance), MENTIONS (entities). Each connection shows strength % and memoryId.
137
137
  - `nowledge_mem_timeline` - activity feed via `nmem f`. Groups by day. `event_type` filter. Exact date range via `date_from`/`date_to` (YYYY-MM-DD). Entries include `(id: <memoryId>)` for chaining to connections.
138
138
  - `nowledge_mem_forget` - delete by ID or fuzzy query.
@@ -151,7 +151,7 @@ When `contextEngine` points elsewhere (or is absent), hooks handle everything. N
151
151
  ## Hook Surface
152
152
 
153
153
  - `before_prompt_build` (always-on) - directive behavioral guidance in system-prompt space: tells agent to search before answering questions about prior work/decisions/preferences, and to save decisions/learnings proactively. Explicitly notes semantic search (not file paths) to counter OpenClaw's hardcoded memory section. Adjusts when sessionContext is on to avoid redundant searches.
154
- - `before_prompt_build` (sessionContext) - session context: Working Memory + `searchRich()` memories with `relevanceReason`. Note: does NOT inject thread snippets — threads are available via `memory_search` tool and `nowledge_mem_thread_fetch`.
154
+ - `before_prompt_build` (sessionContext) - session context: Context Bundle / Working Memory + `searchRich()` memories with `relevanceReason`. Note: does NOT inject thread snippets — threads are available via `memory_search` tool and `nowledge_mem_thread_fetch`.
155
155
  - `agent_end` - thread capture + LLM triage/distillation (requires `sessionDigest: true`)
156
156
  - `after_compaction` - thread append
157
157
  - `before_reset` - thread append
@@ -170,7 +170,7 @@ Ambient space: choose one real lane through plugin config `space` or `spaceTempl
170
170
 
171
171
  | Key | Type | Default | Env Var | Description |
172
172
  |-----|------|---------|---------|-------------|
173
- | `sessionContext` | boolean | `false` | `NMEM_SESSION_CONTEXT` | Inject Working Memory + relevant memories at prompt time |
173
+ | `sessionContext` | boolean | `false` | `NMEM_SESSION_CONTEXT` | Inject Context Bundle / Working Memory + relevant memories at prompt time |
174
174
  | `sessionDigest` | boolean | `true` | `NMEM_SESSION_DIGEST` | Thread capture + LLM distillation at session end |
175
175
  | `digestMinInterval` | integer 0-86400 | `300` | `NMEM_DIGEST_MIN_INTERVAL` | Minimum seconds between session digests |
176
176
  | `maxContextResults` | integer 1-20 | `5` | `NMEM_MAX_CONTEXT_RESULTS` | How many memories to inject at prompt time |
package/README.md CHANGED
@@ -83,7 +83,7 @@ If you manage config manually, or you want to verify what the installer selected
83
83
  }
84
84
  ```
85
85
 
86
- By default the agent gets 10 tools on demand, a behavioral skill that teaches it when to search and save, a short always-on system hint, and end-of-session capture. Working Memory injection is optional and controlled by `sessionContext`.
86
+ By default the agent gets 10 tools on demand, a behavioral skill that teaches it when to search and save, a short always-on system hint, and end-of-session capture. Context Bundle / Working Memory injection is optional and controlled by `sessionContext`.
87
87
 
88
88
  ### Remote mode
89
89
 
@@ -117,7 +117,7 @@ The OpenClaw plugin settings can own that lane directly:
117
117
  }
118
118
  ```
119
119
 
120
- Use `space` when one OpenClaw profile belongs to one stable lane. Use `spaceTemplate` only when your launcher or host environment already sets a trustworthy variable that identifies the lane. If your OpenClaw runtime does not expose per-agent identity to plugins, do not fake it. Use one profile/process per lane or stay on `Default`.
120
+ Use `space` when one OpenClaw profile belongs to one stable lane. Use `spaceTemplate` only when your launcher or host environment already sets a trustworthy variable that identifies the lane. If your OpenClaw runtime does not expose per-AI Identity to plugins, do not fake it. Use one profile/process per lane or stay on `Default`.
121
121
 
122
122
  If you are running OpenClaw from a launcher or script with no richer config surface, you can still set one session-wide fallback lane with:
123
123
 
@@ -127,6 +127,8 @@ NMEM_SPACE="Research Agent"
127
127
 
128
128
  Nowledge Mem's CLI-backed Working Memory, memory search/save, and the plugin's API-backed thread/feed fallbacks will then stay in that lane together. There is no second OpenClaw-only vault setting; the shared Mem boundary is still one hidden space key, but humans and agents should normally work with the space name instead.
129
129
 
130
+ For multi-agent orchestrators, set `NMEM_AGENT_ID=<agent-slug>` per spawned OpenClaw worker. Add `NMEM_SPACE` only when that run should override the AI Identity's default space. `NMEM_HOST_AGENT_ID` is for advanced host-id aliases. Context Bundle will use the stable identity while keeping `source_app=openclaw` for provenance. Keep these as launcher environment variables instead of plugin config keys so OpenClaw's strict config validation remains stable.
131
+
130
132
  Shared spaces, default retrieval, and agent guidance still live in Mem's own space profile. OpenClaw chooses the lane and preserves it across transports; it should not duplicate the profile semantics.
131
133
 
132
134
  ### Configure via WebUI of OpenClaw
@@ -145,7 +147,7 @@ flowchart TD
145
147
  A["User sends message"] --> B["before_prompt_build hook"]
146
148
 
147
149
  B --> C["Behavioral guidance(cacheable system hint)"]
148
- B -.->|"sessionContext: true"| D["Working Memory +relevant memories (~1-2 KB)"]
150
+ B -.->|"sessionContext: true"| D["Context Bundle / Working Memory +relevant memories (~1-2 KB)"]
149
151
 
150
152
  C --> E["Agent processes message(10 tools available)"]
151
153
  D --> E
@@ -172,7 +174,7 @@ The behavioral skill and always-on hook nudge the agent to **search before answe
172
174
  | Decision made, insight learned | `nowledge_mem_save` | Structured save with `unit_type`, `labels`, `event_start`, `importance`. |
173
175
  | "What was I doing last week?" | `nowledge_mem_timeline` | Activity feed grouped by day. Supports exact date ranges. |
174
176
  | "How is X connected to Y?" | `nowledge_mem_connections` | Graph walk: edges, entities, EVOLVES chains, document provenance. |
175
- | Need today's focus/priorities | `nowledge_mem_context` | Read Working Memory daily briefing. Supports section-level patch. |
177
+ | Need startup context or today's focus/priorities | `nowledge_mem_context` | Read Context Bundle when available, with Working Memory fallback. Supports Working Memory section-level patch. |
176
178
  | Memory has `sourceThreadId` | `nowledge_mem_thread_fetch` | Fetch full source conversation. Pagination via `offset` + `limit`. |
177
179
  | "Find our discussion about X" | `nowledge_mem_thread_search` | Search past conversations by keyword. Returns matched snippets. |
178
180
  | "Forget X" | `nowledge_mem_forget` | Delete by ID or search query. |
@@ -241,7 +243,7 @@ When OpenClaw's memory-core is the primary memory slot and Nowledge Mem runs alo
241
243
  - Every time memory-core runs `memory_search`, it also searches Nowledge Mem's knowledge graph via the supplement.
242
244
  - Recalled Nowledge Mem content accumulates frequency and relevance scores in memory-core's short-term store.
243
245
  - High-scoring content is promoted to `MEMORY.md` during deep-phase dreaming (daily, 3 AM).
244
- - The plugin's own search-based recall is automatically disabled to avoid duplicates. Working Memory injection continues as before.
246
+ - The plugin's own search-based recall is automatically disabled to avoid duplicates. Startup context injection continues as before.
245
247
 
246
248
  **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`.
247
249
 
@@ -306,7 +308,7 @@ temporal_context: past
306
308
 
307
309
  Eight memory types: `fact`, `preference`, `decision`, `plan`, `procedure`, `learning`, `context`, `event`. Each becomes a typed node in the knowledge graph. Labels enable filtering in `memory_search`. `event_start` records *when* something happened, not just when you saved it, powering bi-temporal search.
308
310
 
309
- **nowledge_mem_context** - Read today's Working Memory: focus areas, priorities, unresolved flags, and recent activity. Generated by the Knowledge Agent each morning, updated throughout the day.
311
+ **nowledge_mem_context** - Read startup context. On current Nowledge Mem installs this is Context Bundle: owner identity, AI Identity, active space, active rules, Working Memory, and KFS paths. Older `nmem` clients fall back to today's Working Memory. Patch mode still edits Working Memory sections.
310
312
 
311
313
  **nowledge_mem_connections** - Explore the knowledge graph around a topic or memory. Returns connected memories, EVOLVES version chains (how understanding has grown), related entities, and source document provenance (which files or URLs knowledge was extracted from).
312
314
 
@@ -360,7 +362,7 @@ The plugin supports three modes. Choose based on how much you want to guarantee
360
362
  | Mode | Config | Behavior | Tradeoff |
361
363
  |------|--------|----------|----------|
362
364
  | **Default** (recommended) | `sessionContext: false, sessionDigest: true` | Agent calls 10 tools on demand. A short system hint stays on. Conversations captured + distilled at session end. | Lowest overhead. Agent decides when to search, usually smart, occasionally forgets. |
363
- | **Session context** | `sessionContext: true` | Working Memory + relevant memories injected at prompt time, plus all 10 tools still available. | Higher per-turn context cost. Guarantees context is present from the first turn. |
365
+ | **Session context** | `sessionContext: true` | Context Bundle / Working Memory + relevant memories injected at prompt time, plus all 10 tools still available. | Higher per-turn context cost. Guarantees context is present from the first turn. |
364
366
  | **Minimal** | `sessionDigest: false` | Tool-only, no automatic capture. | Small overhead from the always-on hint only. Use when you handle memory manually. |
365
367
 
366
368
  **Which mode should you use?**
@@ -438,7 +440,7 @@ OpenClaw does not currently have a separate user-owned instruction file in this
438
440
 
439
441
  | Key | Type | Default | Description |
440
442
  |-----|------|---------|-------------|
441
- | `sessionContext` | boolean | `false` | Inject Working Memory + relevant memories at prompt time |
443
+ | `sessionContext` | boolean | `false` | Inject Context Bundle / Working Memory + relevant memories at prompt time |
442
444
  | `sessionDigest` | boolean | `true` | Thread capture + LLM distillation at session end |
443
445
  | `digestMinInterval` | integer | `300` | Minimum seconds between session digests for the same thread (0-86400) |
444
446
  | `maxContextResults` | integer | `5` | Max memories to inject at prompt time (1-20, only used when sessionContext is enabled) |
package/SKILL.md CHANGED
@@ -74,7 +74,7 @@ nmem --version
74
74
  If this fails:
75
75
 
76
76
  - For users with the desktop app, direct them to **Settings > Preferences > Developer Tools > Install CLI**
77
- - Otherwise suggest `pip install nmem-cli`
77
+ - Otherwise suggest `pip install nmem-cli`, or on Arch Linux `yay -S nmem-cli` / `paru -S nmem-cli`
78
78
 
79
79
  ### Step 2 — Verify the Backend Before Plugin Setup
80
80
 
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "id": "openclaw-nowledge-mem",
3
3
  "name": "Nowledge Mem",
4
- "description": "Cross-tool knowledge graph memory for OpenClaw with Working Memory, graph search, and session distillation.",
5
- "version": "0.8.23",
4
+ "description": "Cross-tool knowledge graph memory for OpenClaw with startup context, graph search, and session distillation.",
5
+ "version": "0.8.25",
6
6
  "kind": ["memory", "context-engine"],
7
7
  "skills": [
8
8
  "skills/memory-guide"
@@ -10,7 +10,7 @@
10
10
  "uiHints": {
11
11
  "sessionContext": {
12
12
  "label": "Session context injection",
13
- "help": "Inject Working Memory and relevant memories at prompt time. Off by default; the agent can call memory_search on demand instead."
13
+ "help": "Inject Context Bundle / Working Memory and relevant memories at prompt time. Off by default; the agent can call memory_search on demand instead."
14
14
  },
15
15
  "sessionDigest": {
16
16
  "label": "Session digest at end",
@@ -42,7 +42,7 @@
42
42
  },
43
43
  "corpusSupplement": {
44
44
  "label": "Register as corpus supplement",
45
- "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)."
45
+ "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 (startup context injection continues)."
46
46
  },
47
47
  "corpusMaxResults": {
48
48
  "label": "Corpus supplement max results",
@@ -80,7 +80,7 @@
80
80
  "sessionContext": {
81
81
  "type": "boolean",
82
82
  "default": false,
83
- "description": "Inject Working Memory and recalled memories at prompt time. Off by default; the agent queries memory on demand via tools."
83
+ "description": "Inject Context Bundle / Working Memory and recalled memories at prompt time. Off by default; the agent queries memory on demand via tools."
84
84
  },
85
85
  "sessionDigest": {
86
86
  "type": "boolean",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nowledge/openclaw-nowledge-mem",
3
3
  "displayName": "Nowledge Mem for OpenClaw",
4
- "version": "0.8.23",
4
+ "version": "0.8.25",
5
5
  "type": "module",
6
6
  "description": "Nowledge Mem memory plugin for OpenClaw, local-first personal knowledge base",
7
7
  "author": {
@@ -31,7 +31,7 @@ Bi-temporal filters narrow results by time:
31
31
 
32
32
  ## Save (nowledge_mem_save)
33
33
 
34
- Save proactively when the conversation produces a decision, preference, plan, procedure, learning, or important context. Do not wait to be asked.
34
+ Save proactively when the conversation produces a durable fact, preference, decision, plan, procedure, learning, event, or important context. Do not wait to be asked.
35
35
 
36
36
  Structure saves:
37
37
 
@@ -54,9 +54,9 @@ Browse recent activity grouped by day. Filter by `event_type` or exact `date_fro
54
54
 
55
55
  Find past conversations by keyword, then progressively fetch messages. Threads span all sources: this tool, other AI tools, browser capture, imports.
56
56
 
57
- ## Working Memory (nowledge_mem_context)
57
+ ## Startup Context (nowledge_mem_context)
58
58
 
59
- Read the user's daily briefing (priorities, active projects, flags). Patch a specific section with `patch_section` + `patch_content`/`patch_append` when the conversation updates a priority or resolves a flag.
59
+ Read Context Bundle when identity, active space, active rules, or current priorities matter. Older `nmem` clients fall back to the user's Working Memory briefing. Patch a specific Working Memory section with `patch_section` + `patch_content`/`patch_append` when the conversation updates a priority or resolves a flag.
60
60
 
61
61
  ## Important
62
62
 
package/src/client.js CHANGED
@@ -690,12 +690,42 @@ export class NowledgeMemClient {
690
690
  const data = await this.execJson(["--json", "wm", "read"], 15_000);
691
691
  const content = String(data.content ?? "").trim();
692
692
  const exists = Boolean(data.exists);
693
- return { content, available: exists && content.length > 0 };
693
+ return {
694
+ content,
695
+ available: exists && content.length > 0,
696
+ source: "working-memory",
697
+ };
694
698
  } catch {
695
- return { content: "", available: false };
699
+ return { content: "", available: false, source: "working-memory" };
696
700
  }
697
701
  }
698
702
 
703
+ async readContextBundle() {
704
+ try {
705
+ const data = await this.execJson(
706
+ ["--json", "context", "--source-app", "openclaw"],
707
+ 15_000,
708
+ );
709
+ const content = String(
710
+ data.rendered_markdown ?? data.markdown ?? data.content ?? "",
711
+ ).trim();
712
+ return {
713
+ content,
714
+ available: content.length > 0,
715
+ source: "context-bundle",
716
+ raw: data,
717
+ };
718
+ } catch {
719
+ return { content: "", available: false, source: "context-bundle" };
720
+ }
721
+ }
722
+
723
+ async readStartupContext() {
724
+ const bundle = await this.readContextBundle();
725
+ if (bundle.available) return bundle;
726
+ return this.readWorkingMemory();
727
+ }
728
+
699
729
  /**
700
730
  * Patch a single section of Working Memory without touching the rest.
701
731
  * Uses `nmem wm patch` (CLI v0.4.1+) — client-side read-modify-write.
@@ -183,24 +183,24 @@ export function createNowledgeMemContextEngineFactory(
183
183
  info: {
184
184
  id: "nowledge-mem",
185
185
  name: "Nowledge Mem",
186
- version: "0.8.0",
186
+ version: "0.8.25",
187
187
  ownsCompaction: false,
188
188
  },
189
189
 
190
190
  // ------------------------------------------------------------------
191
- // bootstrap — pre-warm Working Memory for first assemble()
191
+ // bootstrap — pre-warm startup context for first assemble()
192
192
  // ------------------------------------------------------------------
193
193
  async bootstrap({ sessionId, sessionKey }) {
194
194
  const key = sessionKey || sessionId;
195
195
  try {
196
- const wm = await client.readWorkingMemory();
196
+ const wm = await client.readStartupContext();
197
197
  const state = getSessionState(key);
198
198
  state.wm = wm;
199
199
  state.lastWmFetch = Date.now();
200
- logger.debug?.(`ce: bootstrap — WM loaded for ${key}`);
200
+ logger.debug?.(`ce: bootstrap — startup context loaded for ${key}`);
201
201
  return { bootstrapped: true };
202
202
  } catch (err) {
203
- logger.warn(`ce: bootstrap — WM read failed: ${err}`);
203
+ logger.warn(`ce: bootstrap — startup context read failed: ${err}`);
204
204
  return { bootstrapped: false, reason: String(err) };
205
205
  }
206
206
  },
@@ -232,19 +232,23 @@ export function createNowledgeMemContextEngineFactory(
232
232
  cfg.sessionContext ? SESSION_CONTEXT_GUIDANCE : BASE_GUIDANCE,
233
233
  );
234
234
 
235
- // 2. Working Memory (refresh if stale)
235
+ // 2. Startup context (refresh if stale)
236
236
  try {
237
237
  if (!state.wm || Date.now() - state.lastWmFetch > WM_CACHE_TTL_MS) {
238
- state.wm = await client.readWorkingMemory();
238
+ state.wm = await client.readStartupContext();
239
239
  state.lastWmFetch = Date.now();
240
240
  }
241
241
  if (state.wm?.available) {
242
+ const tag =
243
+ state.wm.source === "context-bundle"
244
+ ? "nowledge-context-bundle"
245
+ : "working-memory";
242
246
  sections.push(
243
- `<working-memory>\n${escapeForPrompt(state.wm.content)}\n</working-memory>`,
247
+ `<${tag}>\n${escapeForPrompt(state.wm.content)}\n</${tag}>`,
244
248
  );
245
249
  }
246
250
  } catch (err) {
247
- logger.debug?.(`ce: assemble — WM read failed: ${err}`);
251
+ logger.debug?.(`ce: assemble — startup context read failed: ${err}`);
248
252
  }
249
253
 
250
254
  // 3. Recalled memories (when sessionContext enabled)
@@ -10,7 +10,7 @@
10
10
  * Enable via config: `corpusSupplement: true`
11
11
  *
12
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)
13
+ * - Startup context injection: still handled by the recall hook / CE
14
14
  * - Search-based recall: handled by this supplement when active, by hooks/CE when not
15
15
  *
16
16
  * Error handling: both search() and get() catch all errors and return [] / null.
@@ -1,9 +1,6 @@
1
- import { readFile } from "node:fs/promises";
2
1
  import { createHash } from "node:crypto";
3
- import { isCronSessionKey, isSubagentSessionKey } from "openclaw/plugin-sdk/routing";
4
- import {
5
- buildStableThreadId,
6
- } from "./thread-identity.js";
2
+ import { readFile } from "node:fs/promises";
3
+ import { buildStableThreadId } from "./thread-identity.js";
7
4
  export {
8
5
  _resetConversationRoots,
9
6
  buildStableThreadId,
@@ -29,6 +26,38 @@ const _lastCaptureAt = new Map();
29
26
  const _MAX_COOLDOWN_ENTRIES = 200;
30
27
  const _syncedMessageCounts = new Map();
31
28
  const _MAX_SYNC_CURSOR_ENTRIES = 500;
29
+ const CAPTURE_MESSAGE_MODE_AUTO = "auto";
30
+ const CAPTURE_MESSAGE_MODE_SNAPSHOT = "snapshot";
31
+ const CAPTURE_MESSAGE_MODE_DELTA = "delta";
32
+
33
+ function parseAgentSessionKey(sessionKey) {
34
+ const raw = String(sessionKey || "")
35
+ .trim()
36
+ .toLowerCase();
37
+ if (!raw) return null;
38
+ const parts = raw.split(":").filter(Boolean);
39
+ if (parts.length < 3 || parts[0] !== "agent") return null;
40
+ const agentId = parts[1];
41
+ const rest = parts.slice(2).join(":");
42
+ if (!agentId || !rest) return null;
43
+ return { agentId, rest };
44
+ }
45
+
46
+ function isAgentCronSessionKey(sessionKey) {
47
+ const parsed = parseAgentSessionKey(sessionKey);
48
+ return parsed?.rest?.startsWith("cron:") === true;
49
+ }
50
+
51
+ function isSubagentSessionKey(sessionKey) {
52
+ const raw = String(sessionKey || "")
53
+ .trim()
54
+ .toLowerCase();
55
+ if (!raw) return false;
56
+ if (raw.startsWith("subagent:")) return true;
57
+ const parsed = parseAgentSessionKey(raw);
58
+ return parsed?.rest?.startsWith("subagent:") === true;
59
+ }
60
+
32
61
  function _setLastCapture(threadId, now) {
33
62
  _lastCaptureAt.set(threadId, now);
34
63
  // Opportunistic eviction: sweep stale entries when map grows large
@@ -68,11 +97,7 @@ function _compileGlob(pattern) {
68
97
  let re = _compiledPatterns.get(key);
69
98
  if (!re) {
70
99
  re = new RegExp(
71
- "^" +
72
- key
73
- .replace(/[.+?^${}()|[\]\\]/g, "\\$&")
74
- .replace(/\*/g, "[^:]*") +
75
- "$",
100
+ `^${key.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^:]*")}$`,
76
101
  );
77
102
  _compiledPatterns.set(key, re);
78
103
  }
@@ -95,12 +120,11 @@ export function matchesExcludePattern(sessionKey, patterns) {
95
120
  /**
96
121
  * Whether automatic thread capture should skip this OpenClaw session.
97
122
  *
98
- * - Agent-scoped cron / isolated-agent keys are classified by
99
- * `isCronSessionKey` from `openclaw/plugin-sdk/routing` (same implementation
100
- * the gateway uses). Requires OpenClaw >=2026.3.22.
123
+ * - Agent-scoped cron / isolated-agent keys follow OpenClaw's canonical
124
+ * `agent:<id>:cron:*` session-key shape.
101
125
  * - Bare `cron:*` keys are still excluded: some internal paths (e.g. delivery /
102
126
  * failure bookkeeping) use that shape without the `agent:` prefix, and
103
- * `isCronSessionKey` only parses agent-scoped keys.
127
+ * agent-scoped parsing intentionally does not classify bare keys.
104
128
  */
105
129
  export function isCronCaptureSessionKey(sessionKey) {
106
130
  const raw = String(sessionKey || "")
@@ -108,7 +132,7 @@ export function isCronCaptureSessionKey(sessionKey) {
108
132
  .toLowerCase();
109
133
  if (!raw) return false;
110
134
  if (raw.startsWith("cron:")) return true;
111
- return isCronSessionKey(raw);
135
+ return isAgentCronSessionKey(raw);
112
136
  }
113
137
 
114
138
  /**
@@ -118,7 +142,8 @@ export function isCronCaptureSessionKey(sessionKey) {
118
142
  * Exported for reuse by Context Engine and other capture paths.
119
143
  */
120
144
  export function hasSkipMarker(messages, marker) {
121
- if (!marker || typeof marker !== "string" || !Array.isArray(messages)) return false;
145
+ if (!marker || typeof marker !== "string" || !Array.isArray(messages))
146
+ return false;
122
147
  const markerLc = marker.toLowerCase();
123
148
  return messages.some((msg) => {
124
149
  const text = extractText(msg?.content ?? msg?.message?.content);
@@ -225,17 +250,45 @@ export function normalizeRoleMessage(
225
250
  timestamp = msg.timestamp;
226
251
  }
227
252
 
253
+ const openclawMeta =
254
+ msg.__openclaw && typeof msg.__openclaw === "object"
255
+ ? msg.__openclaw
256
+ : raw.__openclaw && typeof raw.__openclaw === "object"
257
+ ? raw.__openclaw
258
+ : null;
259
+ const msgMetadata =
260
+ msg.metadata && typeof msg.metadata === "object" ? msg.metadata : null;
261
+ const rawMetadata =
262
+ raw.metadata && typeof raw.metadata === "object" ? raw.metadata : null;
228
263
  const externalHint = [
264
+ openclawMeta?.mirrorIdentity,
265
+ openclawMeta?.idempotencyKey,
266
+ msg.idempotencyKey,
267
+ msg.idempotency_key,
229
268
  msg.external_id,
230
269
  msg.externalId,
231
270
  msg.message_id,
232
271
  msg.messageId,
272
+ msg.openclaw_entry_id,
273
+ msg.openclawEntryId,
233
274
  msg.id,
275
+ raw.idempotencyKey,
276
+ raw.idempotency_key,
234
277
  raw.external_id,
235
278
  raw.externalId,
236
279
  raw.message_id,
237
280
  raw.messageId,
281
+ raw.openclaw_entry_id,
282
+ raw.openclawEntryId,
238
283
  raw.id,
284
+ msgMetadata?.external_id,
285
+ msgMetadata?.externalId,
286
+ msgMetadata?.message_id,
287
+ msgMetadata?.messageId,
288
+ rawMetadata?.external_id,
289
+ rawMetadata?.externalId,
290
+ rawMetadata?.message_id,
291
+ rawMetadata?.messageId,
239
292
  ]
240
293
  .find((v) => typeof v === "string" && v.trim().length > 0)
241
294
  ?.trim();
@@ -263,11 +316,26 @@ function sanitizeIdPart(input, max = 48) {
263
316
  return normalized.slice(0, max);
264
317
  }
265
318
 
266
- function buildExternalId({ normalized, index, threadId, sessionKey }) {
319
+ function buildExternalId({
320
+ normalized,
321
+ index,
322
+ threadId,
323
+ sessionKey,
324
+ event,
325
+ ctx,
326
+ messageMode,
327
+ }) {
267
328
  if (normalized.externalHint) {
268
329
  return `oc:${sanitizeIdPart(normalized.externalHint, 96)}`;
269
330
  }
270
- const seed = `${threadId}|${sessionKey}|${index}|${normalized.role}|${normalized.content}`;
331
+ const runId =
332
+ messageMode === CAPTURE_MESSAGE_MODE_DELTA
333
+ ? String(ctx?.runId || event?.runId || "").trim()
334
+ : "";
335
+ const seedParts = [threadId, sessionKey];
336
+ if (runId) seedParts.push(`run:${runId}`);
337
+ seedParts.push(index, normalized.role, normalized.content);
338
+ const seed = seedParts.join("|");
271
339
  const digest = createHash("sha1").update(seed).digest("hex");
272
340
  return `oc-msg:${digest}`;
273
341
  }
@@ -286,6 +354,18 @@ function buildAppendIdempotencyKey(threadId, reason, messages) {
286
354
  return `oc-batch:${createHash("sha1").update(JSON.stringify(seed)).digest("hex")}`;
287
355
  }
288
356
 
357
+ function hasStableExternalHints(normalized) {
358
+ return (
359
+ Array.isArray(normalized) &&
360
+ normalized.length > 0 &&
361
+ normalized.every(
362
+ (message) =>
363
+ typeof message?.externalHint === "string" &&
364
+ message.externalHint.trim().length > 0,
365
+ )
366
+ );
367
+ }
368
+
289
369
  async function loadMessagesFromSessionFile(sessionFile) {
290
370
  try {
291
371
  const content = await readFile(sessionFile, "utf-8");
@@ -328,6 +408,7 @@ export async function appendOrCreateThread({
328
408
  reason,
329
409
  maxMessageChars = DEFAULT_MAX_MESSAGE_CHARS,
330
410
  resolvedMessages,
411
+ messageMode = CAPTURE_MESSAGE_MODE_SNAPSHOT,
331
412
  }) {
332
413
  const rawMessages = resolvedMessages ?? (await resolveHookMessages(event));
333
414
  if (!Array.isArray(rawMessages) || rawMessages.length === 0) return;
@@ -341,22 +422,32 @@ export async function appendOrCreateThread({
341
422
  .filter(Boolean);
342
423
  if (normalized.length === 0) return;
343
424
 
344
- const allMessages = normalized.map((message, index) => ({
345
- role: message.role,
346
- content: message.content,
347
- timestamp: message.timestamp,
348
- metadata: {
349
- external_id: buildExternalId({
350
- normalized: message,
351
- index,
352
- threadId,
353
- sessionKey,
354
- }),
355
- source: "openclaw",
356
- session_key: sessionKey,
357
- session_id: sessionId || undefined,
358
- },
359
- }));
425
+ const buildMessages = (resolvedMessageMode) =>
426
+ normalized.map((message, index) => ({
427
+ role: message.role,
428
+ content: message.content,
429
+ timestamp: message.timestamp,
430
+ metadata: {
431
+ external_id: buildExternalId({
432
+ normalized: message,
433
+ index,
434
+ threadId,
435
+ sessionKey,
436
+ event,
437
+ ctx,
438
+ messageMode: resolvedMessageMode,
439
+ }),
440
+ source: "openclaw",
441
+ session_key: sessionKey,
442
+ session_id: sessionId || undefined,
443
+ },
444
+ }));
445
+
446
+ let allMessages = buildMessages(
447
+ messageMode === CAPTURE_MESSAGE_MODE_DELTA
448
+ ? CAPTURE_MESSAGE_MODE_DELTA
449
+ : CAPTURE_MESSAGE_MODE_SNAPSHOT,
450
+ );
360
451
 
361
452
  let syncedCount = _syncedMessageCounts.get(threadId);
362
453
  if (syncedCount === undefined) {
@@ -366,6 +457,67 @@ export async function appendOrCreateThread({
366
457
  }
367
458
  }
368
459
 
460
+ const resolvedMessageMode =
461
+ messageMode === CAPTURE_MESSAGE_MODE_DELTA ||
462
+ (messageMode === CAPTURE_MESSAGE_MODE_AUTO &&
463
+ hasStableExternalHints(normalized))
464
+ ? CAPTURE_MESSAGE_MODE_DELTA
465
+ : CAPTURE_MESSAGE_MODE_SNAPSHOT;
466
+ if (resolvedMessageMode === CAPTURE_MESSAGE_MODE_DELTA) {
467
+ allMessages = buildMessages(CAPTURE_MESSAGE_MODE_DELTA);
468
+ const idempotencyKey = buildAppendIdempotencyKey(
469
+ threadId,
470
+ reason,
471
+ allMessages,
472
+ );
473
+ try {
474
+ const appended = await client.appendThread({
475
+ threadId,
476
+ messages: allMessages,
477
+ deduplicate: true,
478
+ idempotencyKey,
479
+ });
480
+ const added = appended.messagesAdded ?? 0;
481
+ const total =
482
+ Number.isFinite(appended.totalMessages) && appended.totalMessages >= 0
483
+ ? appended.totalMessages
484
+ : typeof syncedCount === "number"
485
+ ? syncedCount + added
486
+ : allMessages.length;
487
+ _setSyncedMessageCount(threadId, total);
488
+ logger.info(
489
+ `capture: appended ${added} delta messages to ${threadId} (${reason || "event"})`,
490
+ );
491
+ return { threadId, normalized, messagesAdded: added };
492
+ } catch (err) {
493
+ if (!client.isThreadNotFoundError(err)) {
494
+ const message = err instanceof Error ? err.message : String(err);
495
+ logger.warn(
496
+ `capture: thread append failed for ${threadId}: ${message}`,
497
+ );
498
+ return null;
499
+ }
500
+ }
501
+
502
+ try {
503
+ const createdId = await client.createThread({
504
+ threadId,
505
+ title,
506
+ messages: allMessages,
507
+ source: "openclaw",
508
+ });
509
+ _setSyncedMessageCount(threadId, allMessages.length);
510
+ logger.info(
511
+ `capture: created thread ${createdId} with ${allMessages.length} delta messages (${reason || "event"})`,
512
+ );
513
+ return { threadId, normalized, messagesAdded: allMessages.length };
514
+ } catch (err) {
515
+ const message = err instanceof Error ? err.message : String(err);
516
+ logger.warn(`capture: thread create failed for ${threadId}: ${message}`);
517
+ return null;
518
+ }
519
+ }
520
+
369
521
  if (typeof syncedCount === "number" && syncedCount > allMessages.length) {
370
522
  // OpenClaw compaction can shrink the active transcript after we already
371
523
  // stored the pre-compaction history. Reset the local cursor to the new
@@ -563,9 +715,7 @@ export function buildAgentEndCaptureHandler(client, cfg, logger) {
563
715
  // Layer 2: marker-based exclusion (user typed #nmem-skip in conversation)
564
716
  const resolvedMessages = await resolveHookMessages(event);
565
717
  if (hasSkipMarker(resolvedMessages, cfg.captureSkipMarker)) {
566
- logger.debug?.(
567
- `capture: skipped session with skip marker ${sessionKey}`,
568
- );
718
+ logger.debug?.(`capture: skipped session with skip marker ${sessionKey}`);
569
719
  return;
570
720
  }
571
721
 
@@ -577,6 +727,7 @@ export function buildAgentEndCaptureHandler(client, cfg, logger) {
577
727
  reason: "agent_end",
578
728
  maxMessageChars: cfg.maxThreadMessageChars,
579
729
  resolvedMessages,
730
+ messageMode: CAPTURE_MESSAGE_MODE_AUTO,
580
731
  });
581
732
 
582
733
  await triageAndDistill({ client, cfg, logger, captureResult, ctx });
@@ -614,9 +765,7 @@ export function buildBeforeResetCaptureHandler(client, cfg, logger) {
614
765
  // Layer 2: marker-based exclusion
615
766
  const resolvedMessages = await resolveHookMessages(event);
616
767
  if (hasSkipMarker(resolvedMessages, cfg.captureSkipMarker)) {
617
- logger.debug?.(
618
- `capture: skipped session with skip marker ${sessionKey}`,
619
- );
768
+ logger.debug?.(`capture: skipped session with skip marker ${sessionKey}`);
620
769
  return;
621
770
  }
622
771
 
@@ -193,17 +193,21 @@ export function buildRecallHandler(client, cfg, logger, runtime = {}) {
193
193
 
194
194
  const sections = [];
195
195
 
196
- // 1. Working Memory — daily context, not a static profile.
197
- // Always injected regardless of search query (WM is independent of user message).
196
+ // 1. Startup context — identity/guidance plus daily context when available.
197
+ // Always injected regardless of search query (it is independent of user message).
198
198
  try {
199
- const wm = await client.readWorkingMemory();
200
- if (wm.available) {
199
+ const startupContext = await client.readStartupContext();
200
+ if (startupContext.available) {
201
+ const tag =
202
+ startupContext.source === "context-bundle"
203
+ ? "nowledge-context-bundle"
204
+ : "working-memory";
201
205
  sections.push(
202
- `<working-memory>\n${escapeForPrompt(wm.content)}\n</working-memory>`,
206
+ `<${tag}>\n${escapeForPrompt(startupContext.content)}\n</${tag}>`,
203
207
  );
204
208
  }
205
209
  } catch (err) {
206
- logger.error(`recall: working memory read failed: ${err}`);
210
+ logger.error(`recall: startup context read failed: ${err}`);
207
211
  }
208
212
 
209
213
  // 2. Relevant memories — enriched with scoring signals and labels.
@@ -1,25 +1,22 @@
1
1
  /**
2
- * nowledge_mem_context — read today's Working Memory.
2
+ * nowledge_mem_context — read startup context.
3
3
  *
4
- * Working Memory is Nowledge Mem's cognitive-science-grounded daily
5
- * artifact: focus areas, unresolved flags, recent changes, and
6
- * priorities — generated by the Knowledge Agent each morning and
7
- * updated throughout the day as events occur.
4
+ * Read mode prefers Context Bundle: owner identity, AI Identity, active
5
+ * space, active rules, Working Memory, and KFS paths. Older nmem clients
6
+ * fall back to Working Memory.
8
7
  *
9
- * This is fundamentally different from a static "user profile."
10
- * It's a living document that reflects what matters TODAY.
8
+ * Patch mode still edits Working Memory sections only.
11
9
  */
12
10
  export function createContextTool(client, logger) {
13
11
  return {
14
12
  name: "nowledge_mem_context",
15
13
  description:
16
- "Read or patch the user's Working Memory — today's focus areas, priorities, unresolved flags, and recent activity. " +
17
- "Generated by the Knowledge Agent each morning and updated throughout the day as new knowledge arrives. " +
18
- "READ MODE (default): Call with no parameters to understand what the user is currently focused on. " +
14
+ "Read the user's startup context, or patch Working Memory sections. " +
15
+ "READ MODE (default): Call with no parameters to load Context Bundle when available: owner identity, AI Identity, active space, active rules, Working Memory, and KFS paths. Older nmem clients fall back to Working Memory. " +
19
16
  "PATCH MODE: Supply patch_section to update just one section without overwriting the rest. " +
20
17
  "Use patch_content to replace the section body, or patch_append to add text to it. " +
21
18
  "Example: patch_section='## Notes', patch_append='New note: ...' to add a note without losing existing content. " +
22
- "Note: Working Memory is regenerated by the Knowledge Agent each morning. " +
19
+ "Note: patch mode edits Working Memory, which is regenerated by the Knowledge Agent each morning. " +
23
20
  "To see what happened recently by date, use nowledge_mem_timeline instead.",
24
21
  parameters: {
25
22
  type: "object",
@@ -100,22 +97,22 @@ export function createContextTool(client, logger) {
100
97
 
101
98
  // — READ MODE —
102
99
  try {
103
- const wm = await client.readWorkingMemory();
100
+ const context = await client.readStartupContext();
104
101
 
105
- if (!wm.available) {
102
+ if (!context.available) {
106
103
  return {
107
104
  content: [
108
105
  {
109
106
  type: "text",
110
- text: "Working Memory not available yet. It is generated by the Knowledge Agent during the daily morning briefing. Ensure Nowledge Mem is running with Background Intelligence enabled.",
107
+ text: "Startup context is not available yet. Ensure Nowledge Mem is running. Context Bundle requires a recent nmem CLI; older clients fall back to Working Memory generated by Background Intelligence.",
111
108
  },
112
109
  ],
113
110
  };
114
111
  }
115
112
 
116
113
  return {
117
- content: [{ type: "text", text: wm.content }],
118
- details: { available: true },
114
+ content: [{ type: "text", text: context.content }],
115
+ details: { available: true, source: context.source },
119
116
  };
120
117
  } catch (err) {
121
118
  const msg = err instanceof Error ? err.message : String(err);
@@ -124,7 +121,7 @@ export function createContextTool(client, logger) {
124
121
  content: [
125
122
  {
126
123
  type: "text",
127
- text: `Failed to read Working Memory: ${msg}`,
124
+ text: `Failed to read startup context: ${msg}`,
128
125
  },
129
126
  ],
130
127
  };
@@ -108,11 +108,11 @@ export function createStatusTool(client, _logger, cfg, runtimeInfo = {}) {
108
108
  );
109
109
  if (cfg.sessionContext) {
110
110
  lines.push(
111
- " Fallback active: Nowledge Mem still injects Working Memory and uses its own recall path.",
111
+ " Fallback active: Nowledge Mem still injects startup context and uses its own recall path.",
112
112
  );
113
113
  } else {
114
114
  lines.push(
115
- " Fallback: Nowledge Mem tools remain available. Enable sessionContext for prompt-time Working Memory and recall.",
115
+ " Fallback: Nowledge Mem tools remain available. Enable sessionContext for prompt-time startup context and recall.",
116
116
  );
117
117
  }
118
118
  if (corpusSupplementRegistrationError) {