@nowledge/openclaw-nowledge-mem 0.6.2 → 0.6.6

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/README.md CHANGED
@@ -17,7 +17,7 @@ openclaw plugins install @nowledge/openclaw-nowledge-mem
17
17
 
18
18
  ### Local mode (default)
19
19
 
20
- Start Nowledge Mem desktop app or run `nmem serve`, then configure:
20
+ Start Nowledge Mem desktop app or run `nmem serve`, then enable the plugin:
21
21
 
22
22
  ```json
23
23
  {
@@ -32,30 +32,145 @@ Start Nowledge Mem desktop app or run `nmem serve`, then configure:
32
32
  }
33
33
  ```
34
34
 
35
- That's it. The agent gets 7 tools and calls them on demand. No extra tokens wasted.
35
+ That's it. The agent gets 9 tools and calls them on demand. No extra tokens wasted.
36
36
 
37
37
  ### Remote mode
38
38
 
39
- Connect to a Nowledge Mem server running elsewhere — on a VPS, a home server, or shared team instance. See [remote access guide](https://mem.nowledge.co/docs/remote-access) for server setup.
39
+ Connect to a Nowledge Mem server running elsewhere (a VPS, a home server, or a shared team instance). See [remote access guide](https://mem.nowledge.co/docs/remote-access) for server setup.
40
+
41
+ Enable the plugin the same way as local mode, then set your remote server in `~/.nowledge-mem/openclaw.json`:
40
42
 
41
43
  ```json
42
44
  {
43
- "plugins": {
44
- "slots": { "memory": "openclaw-nowledge-mem" },
45
- "entries": {
46
- "openclaw-nowledge-mem": {
47
- "enabled": true,
48
- "config": {
49
- "apiUrl": "https://nowledge.example.com",
50
- "apiKey": "your-api-key-here"
51
- }
52
- }
53
- }
54
- }
45
+ "apiUrl": "https://nowledge.example.com",
46
+ "apiKey": "your-api-key-here"
55
47
  }
56
48
  ```
57
49
 
58
- The `apiKey` is injected as `NMEM_API_KEY` into the nmem CLI process — never passed as a CLI argument, never logged.
50
+ The `apiKey` is injected as `NMEM_API_KEY` into the nmem CLI process. Never passed as a CLI argument, never logged.
51
+
52
+ ## How It Works
53
+
54
+ ### Per-Turn Flow
55
+
56
+ Every user message triggers hooks before the agent sees it, then the agent decides which tools to call.
57
+
58
+ ```mermaid
59
+ flowchart TD
60
+ A["User sends message"] --> B["before_prompt_build hook"]
61
+
62
+ B --> C["Behavioral guidance(always, ~110 tokens)"]
63
+ B -.->|"sessionContext: true"| D["Working Memory +relevant memories (~1-2 KB)"]
64
+
65
+ C --> E["Agent processes message(9 tools available)"]
66
+ D --> E
67
+
68
+ E --> F{"Needs past context?"}
69
+ F -->|"Yes"| G["memory_search"]
70
+ G --> R
71
+
72
+ E --> H{"Something worth keeping?"}
73
+ H -->|"Yes"| I["nowledge_mem_save(typed, labeled, temporal)"]
74
+ I --> R
75
+
76
+ E --> R["Respond to user"]
77
+ ```
78
+
79
+ ### When Each Tool Gets Called
80
+
81
+ The behavioral hook nudges the agent to **search before answering** and **save after deciding**. Here's when each tool fires:
82
+
83
+ | Scenario | Tool | What happens |
84
+ |----------|------|--------------|
85
+ | User asks a question | `memory_search` | Search knowledge base before answering. Also returns `relatedThreads` snippets and `sourceThreadId`. |
86
+ | Agent needs full memory text | `memory_get` | Read one memory by ID or path. `MEMORY.md` alias reads Working Memory. Returns `sourceThreadId`. |
87
+ | Decision made, insight learned | `nowledge_mem_save` | Structured save with `unit_type`, `labels`, `event_start`, `importance`. |
88
+ | "What was I doing last week?" | `nowledge_mem_timeline` | Activity feed grouped by day. Supports exact date ranges. |
89
+ | "How is X connected to Y?" | `nowledge_mem_connections` | Graph walk: edges, entities, EVOLVES chains, document provenance. |
90
+ | Need today's focus/priorities | `nowledge_mem_context` | Read Working Memory daily briefing. Supports section-level patch. |
91
+ | Memory has `sourceThreadId` | `nowledge_mem_thread_fetch` | Fetch full source conversation. Pagination via `offset` + `limit`. |
92
+ | "Find our discussion about X" | `nowledge_mem_thread_search` | Search past conversations by keyword. Returns matched snippets. |
93
+ | "Forget X" | `nowledge_mem_forget` | Delete by ID or search query. |
94
+
95
+ ### Session Lifecycle (Automatic Capture)
96
+
97
+ When sessions end, conversations are captured and optionally distilled. No user action needed.
98
+
99
+ ```mermaid
100
+ flowchart TD
101
+ A["Session lifecycle event"] --> B{"Event type"}
102
+
103
+ B -->|"agent_end"| C["Append messages to thread(idempotent, deduped)"]
104
+ B -->|"after_compaction"| D["Append messages to thread(checkpoint only)"]
105
+ B -->|"before_reset"| E["Append messages to thread(checkpoint only)"]
106
+
107
+ C --> F{"sessionDigest enabled+ enough messages+ cooldown passed?"}
108
+ F -->|"Yes"| G["LLM triage (~100 tokens):worth distilling?"]
109
+ F -->|"No"| H["Done - thread saved"]
110
+ G -->|"Decisions, insights,preferences found"| I["Full distillation ->structured memories(with sourceThreadId)"]
111
+ G -->|"Routine chat"| H
112
+
113
+ D --> H
114
+ E --> H
115
+ ```
116
+
117
+ **Key points:**
118
+ - Thread capture is unconditional: every conversation is saved and searchable via `nowledge_mem_thread_search`
119
+ - LLM distillation only runs at `agent_end`, not during compaction/reset checkpoints
120
+ - Distilled memories carry `sourceThreadId`, linking them back to the source conversation
121
+ - Cooldown (`digestMinInterval`, default 300s) prevents burst distillation
122
+
123
+ ### Progressive Retrieval (Memory -> Thread -> Messages)
124
+
125
+ Memories distilled from conversations carry a `sourceThreadId`. This creates a retrieval chain:
126
+
127
+ ```mermaid
128
+ flowchart TD
129
+ A["memory_search'PostgreSQL decision'"] --> B["Result includessourceThreadId"]
130
+ B --> C["nowledge_mem_thread_fetch(offset=0, limit=50)"]
131
+ C --> D["50 messageshasMore: true"]
132
+ D --> E["nowledge_mem_thread_fetch(offset=50, limit=50)"]
133
+ E --> F["Next page"]
134
+ ```
135
+
136
+ Direct conversation search also works:
137
+
138
+ ```mermaid
139
+ flowchart LR
140
+ G["nowledge_mem_thread_search 'database architecture'"] --> H["Threads with matched snippets"]
141
+ H --> I["nowledge_mem_thread_fetch(threadId from results)"]
142
+ I --> J["Full messages"]
143
+ ```
144
+
145
+ Two entry points:
146
+ 1. **From a memory**: `memory_search` or `memory_get` returns `sourceThreadId`, then fetch the source conversation
147
+ 2. **Direct search**: `nowledge_mem_thread_search` finds conversations by keyword, then fetch any result
148
+
149
+ ### Three Modes at a Glance
150
+
151
+ ```mermaid
152
+ flowchart LR
153
+ subgraph "default" ["Default (recommended)"]
154
+ direction TB
155
+ D1["Every turn: behavioral guidance (~50 tokens)"]
156
+ D2["Agent calls 9 tools on demand"]
157
+ D3["Session end: thread capture + LLM distillation"]
158
+ end
159
+
160
+ subgraph context ["Session Context"]
161
+ direction TB
162
+ C1["Every turn: guidance + Working Memory+ recalled memories (~1-2 KB)"]
163
+ C2["Agent calls 9 tools on demand"]
164
+ C3["Session end: thread capture + LLM distillation"]
165
+ end
166
+
167
+ subgraph minimal ["Minimal"]
168
+ direction TB
169
+ M1["Every turn: behavioral guidance (~50 tokens)"]
170
+ M2["Agent calls 9 tools on demand"]
171
+ M3["No automatic capture"]
172
+ end
173
+ ```
59
174
 
60
175
  ## Tools
61
176
 
@@ -63,15 +178,15 @@ The `apiKey` is injected as `NMEM_API_KEY` into the nmem CLI process — never p
63
178
 
64
179
  These satisfy the OpenClaw memory slot contract and activate the "Memory Recall" section in OpenClaw's system prompt.
65
180
 
66
- **memory_search** — Multi-signal recall using embedding, BM25, label match, graph signals, and recency decay. Returns structured source paths (`nowledgemem://memory/<id>`) for follow-up with `memory_get` or `nowledge_mem_connections`.
181
+ **memory_search** - Multi-signal recall using embedding, BM25, label match, graph signals, and recency decay. Returns structured source paths (`nowledgemem://memory/<id>`) for follow-up with `memory_get` or `nowledge_mem_connections`. Also returns relevant past conversation snippets (`relatedThreads`) and `sourceThreadId` (link to the conversation the memory was distilled from) when available.
67
182
 
68
- **memory_get** — Read a specific memory by ID or path. Supports `MEMORY.md` alias for Working Memory.
183
+ **memory_get** - Read a specific memory by ID or path. Supports `MEMORY.md` alias for Working Memory. Returns `sourceThreadId` when the memory was distilled from a conversation.
69
184
 
70
185
  ### Nowledge Mem Native
71
186
 
72
187
  These reflect capabilities unique to Nowledge Mem's knowledge graph architecture.
73
188
 
74
- **nowledge_mem_save** — Capture structured knowledge with type classification, labels, and temporal context.
189
+ **nowledge_mem_save** - Capture structured knowledge with type classification, labels, and temporal context.
75
190
 
76
191
  ```
77
192
  text: "We decided to use PostgreSQL with JSONB for the task events table"
@@ -81,18 +196,18 @@ importance: 0.8
81
196
  labels: ["backend", "architecture"]
82
197
  event_start: 2024-03
83
198
  temporal_context: past
84
- → Saved: Task events database choice [decision] (id: mem_abc) · labels: backend, architecture · event: 2024-03
199
+ > Saved: Task events database choice [decision] (id: mem_abc) - labels: backend, architecture - event: 2024-03
85
200
  ```
86
201
 
87
- 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.
202
+ 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.
88
203
 
89
- **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.
204
+ **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.
90
205
 
91
- **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).
206
+ **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).
92
207
 
93
208
  ```
94
209
  memoryId: "mem_abc"
95
- → Connected memories:
210
+ > Connected memories:
96
211
  - PostgreSQL optimization patterns: Use JSONB GIN indexes for...
97
212
  - Redis caching layer decision: For frequently accessed task lists...
98
213
  Source documents (provenance):
@@ -104,41 +219,88 @@ memoryId: "mem_abc"
104
219
  - superseded by newer understanding (version chain)
105
220
  ```
106
221
 
107
- **nowledge_mem_timeline** — Browse your knowledge history chronologically. Use for questions like "what was I working on last week?" or "what happened yesterday?". Groups activity by day: memories saved, documents ingested, insights generated, and more.
222
+ **nowledge_mem_timeline** - Browse your knowledge history chronologically. Use for questions like "what was I working on last week?" or "what happened yesterday?". Groups activity by day: memories saved, documents ingested, insights generated, and more.
108
223
 
109
224
  ```
110
225
  last_n_days: 7
111
- → 2026-02-18:
112
- - [Memory saved] UV guide — Python toolchain setup
226
+ > 2026-02-18:
227
+ - [Memory saved] UV guide - Python toolchain setup
113
228
  - [Knowledge extracted from document] api-spec.pdf
114
229
  2026-02-17:
115
230
  - [Daily briefing] Focus: NebulaGraph, AI biotech...
116
231
  - [Insight] Connection between Redis caching and...
117
232
  ```
118
233
 
119
- **nowledge_mem_forget** — Delete a memory by ID or search query. Supports user confirmation when multiple matches are found.
234
+ **nowledge_mem_forget** - Delete a memory by ID or search query. Supports user confirmation when multiple matches are found.
235
+
236
+ ### Thread Tools
237
+
238
+ **nowledge_mem_thread_search** - Search past conversations by keyword. Returns matched threads with message snippets and relevance scores. Use when the user asks about a past discussion or wants to find a conversation from a specific time.
239
+
240
+ **nowledge_mem_thread_fetch** - Fetch full messages from a conversation thread with pagination. Pass a `sourceThreadId` from memory results or a `threadId` from thread search. Supports `offset` and `limit` for progressive retrieval of long conversations.
241
+
242
+ ```
243
+ threadId: "openclaw-db-arch-a1b2c3"
244
+ offset: 0, limit: 50
245
+ -> Thread: "Database architecture discussion" (128 messages)
246
+ [user] We need to decide on the database for task events...
247
+ [assistant] Based on the requirements, PostgreSQL with JSONB...
248
+ ... (126 more messages, use offset=50 for next page)
249
+ ```
120
250
 
121
251
  ## Operating Modes
122
252
 
123
- The plugin supports three modes. The default (tool-only) gives the agent full access to all tools with zero token overhead.
253
+ The plugin supports three modes. Choose based on how much you want to guarantee versus how much token budget you're willing to spend.
254
+
255
+ | Mode | Config | Behavior | Tradeoff |
256
+ |------|--------|----------|----------|
257
+ | **Default** (recommended) | `sessionContext: false, sessionDigest: true` | Agent calls 9 tools on demand. Behavioral guidance every turn (~50 tokens). Conversations captured + distilled at session end. | Lowest cost. Agent decides when to search, usually smart, occasionally forgets. |
258
+ | **Session context** | `sessionContext: true` | Working Memory + relevant memories injected at prompt time, plus all 9 tools still available. | ~1-2 KB/turn. Guarantees context is always present. Best for short sessions or critical workflows. |
259
+ | **Minimal** | `sessionDigest: false` | Tool-only, no automatic capture. | Zero overhead beyond guidance. Use when you handle memory manually. |
124
260
 
125
- | Mode | Config | Behavior |
126
- |------|--------|----------|
127
- | **Tool-only** (default) | `autoRecall: false, autoCapture: false` | Agent calls tools on demand. Zero overhead. |
128
- | **Auto-recall** | `autoRecall: true` | Working Memory + relevant memories injected at session start. |
129
- | **Auto-capture** | `autoCapture: true` | Thread capture + LLM distillation at session end. |
261
+ **Which mode should you use?**
130
262
 
131
- ### Auto-Recall (`autoRecall`, default: false)
263
+ - **Most users**: start with default. The agent gets behavioral guidance nudging it to search before answering and save after deciding. It works well for most conversations.
264
+ - **Short sessions or critical accuracy**: enable `sessionContext`. This guarantees relevant memories are present from the first turn. The agent doesn't need to decide whether to search. The tradeoff is ~1-2 KB of context per turn.
265
+ - **Full manual control**: set `sessionDigest: false`. You control what gets saved (via `/remember` or `nowledge_mem_save`) and nothing is captured automatically.
132
266
 
133
- When enabled, the plugin injects Working Memory and relevant search results at session start. Useful for giving the agent immediate context without waiting for it to search proactively.
267
+ ### Session Context (`sessionContext`, default: false)
134
268
 
135
- ### Auto-Capture (`autoCapture`, default: false)
269
+ When enabled, the plugin injects Working Memory and relevant search results at prompt time. The behavioral guidance automatically adjusts to tell the agent that context is already present, so `memory_search` should only be used for specific follow-up queries, not broad recall. This prevents the agent from redundantly searching for the same context that was just injected.
270
+
271
+ Useful for giving the agent immediate context without waiting for it to search proactively.
272
+
273
+ ### Session Digest (`sessionDigest`, default: true)
136
274
 
137
275
  When enabled, two things happen at `agent_end`, `after_compaction`, and `before_reset`:
138
276
 
139
277
  **1. Thread capture (always).** The full conversation is appended to a persistent thread. Unconditional, idempotent by message ID.
140
278
 
141
- **2. LLM distillation (when worthwhile).** A lightweight LLM triage determines if the conversation has save-worthy content. If yes, a full distillation pass creates structured memories with types, labels, and temporal data. Language-agnostic — works in any language.
279
+ **2. LLM distillation (when worthwhile).** A lightweight LLM triage determines if the conversation has save-worthy content. If yes, a full distillation pass creates structured memories with types, labels, and temporal data. Language-agnostic, works in any language.
280
+
281
+ ## Design Decisions
282
+
283
+ Honest answers to common questions about how the memory system works.
284
+
285
+ **Does the agent always search before answering?**
286
+
287
+ No. The behavioral guidance nudges the agent to search, but doesn't force it. This is a deliberate tradeoff: forcing a search on every turn would add latency and cost for messages that don't need past context (like "hello" or "thanks"). In practice, modern LLMs follow behavioral guidance reliably for knowledge-related questions. If guaranteed recall matters for your use case, enable `sessionContext: true`. That injects relevant memories at prompt time, before the agent even processes your message.
288
+
289
+ **What stops the agent from saving duplicate memories?**
290
+
291
+ Two layers. First, the plugin checks for near-identical existing memories before every save. If a memory with >=90% similarity already exists, the save is skipped and the existing memory is returned instead. Second, Nowledge Mem's Knowledge Agent runs in the background and handles deeper deduplication, identifying semantic overlap across memories and linking them via EVOLVES chains (replaces, enriches, confirms, or challenges). The plugin handles obvious duplicates; the Knowledge Agent handles subtle ones.
292
+
293
+ **When `sessionContext` is on, will the agent search again and get duplicate context?**
294
+
295
+ The behavioral guidance adjusts when `sessionContext` is enabled. Instead of "search with memory_search when prior context would help," it tells the agent "relevant memories have already been injected, use memory_search only for specific follow-up queries." This reduces redundant searches. The injected context and manual search results are in different XML blocks, so even if overlap occurs, it doesn't confuse the agent.
296
+
297
+ **What happens to conversations I don't explicitly save?**
298
+
299
+ With `sessionDigest` enabled (the default), every conversation is saved as a searchable thread. You can find it later with `nowledge_mem_thread_search`. On top of that, a lightweight LLM triage checks if the conversation contained decisions, insights, or preferences worth keeping as structured memories. If yes, they're extracted with proper types, labels, and temporal context. If the conversation was routine ("fix this typo"), nothing extra is saved.
300
+
301
+ **Can memories be wrong or outdated?**
302
+
303
+ Yes. Memories are what you or the AI saved at a point in time. Nowledge Mem's EVOLVES chains track how understanding changes: a newer memory can supersede, enrich, or challenge an older one. The Knowledge Agent identifies these relationships automatically. When you search, the relevance scoring considers recency, so newer memories rank higher by default.
142
304
 
143
305
  ## Slash Commands
144
306
 
@@ -157,21 +319,54 @@ openclaw nowledge-mem status
157
319
 
158
320
  ## Configuration
159
321
 
322
+ On first startup, the plugin creates a config file at `~/.nowledge-mem/openclaw.json` with defaults. Edit it to customize:
323
+
324
+ ```json
325
+ {
326
+ "sessionContext": false,
327
+ "sessionDigest": true,
328
+ "digestMinInterval": 300,
329
+ "maxContextResults": 5,
330
+ "apiUrl": "",
331
+ "apiKey": ""
332
+ }
333
+ ```
334
+
335
+ Edit the file and restart OpenClaw. The plugin reads it on every startup.
336
+
160
337
  | Key | Type | Default | Description |
161
338
  |-----|------|---------|-------------|
162
- | `autoRecall` | boolean | `false` | Inject Working Memory + relevant memories at session start |
163
- | `autoCapture` | boolean | `false` | Thread capture + LLM distillation at session end |
164
- | `maxRecallResults` | integer | `5` | Max memories to recall at session start (only used when autoRecall is enabled) |
339
+ | `sessionContext` | boolean | `false` | Inject Working Memory + relevant memories at prompt time |
340
+ | `sessionDigest` | boolean | `true` | Thread capture + LLM distillation at session end |
341
+ | `digestMinInterval` | integer | `300` | Minimum seconds between session digests for the same thread (0-86400) |
342
+ | `maxContextResults` | integer | `5` | Max memories to inject at prompt time (1-20, only used when sessionContext is enabled) |
343
+ | `apiUrl` | string | `""` | Remote server URL. Empty = local (`http://127.0.0.1:14242`) |
344
+ | `apiKey` | string | `""` | API key for remote access. Injected as `NMEM_API_KEY` env var, never logged |
345
+
346
+ ### Advanced: environment variables
347
+
348
+ For CI, scripts, or Docker deployments, all options also support environment variables:
349
+
350
+ ```bash
351
+ NMEM_SESSION_CONTEXT=true
352
+ NMEM_SESSION_DIGEST=true
353
+ NMEM_DIGEST_MIN_INTERVAL=300
354
+ NMEM_MAX_CONTEXT_RESULTS=5
355
+ NMEM_API_URL=https://...
356
+ NMEM_API_KEY=your-key
357
+ ```
358
+
359
+ **Priority**: OpenClaw plugin config > config file > env vars > defaults.
165
360
 
166
361
  ## What Makes This Different
167
362
 
168
363
  - **Local-first**: no API key, no cloud account. Your knowledge stays on your machine.
169
364
  - **Knowledge graph**: memories are connected nodes, not isolated vectors. EVOLVES edges track how understanding grows over time.
170
- - **Source provenance**: the Library ingests PDFs, DOCX, URLs — extracted knowledge links back to the exact document section it came from.
171
- - **Working Memory**: an AI-generated daily briefing that evolves — not a static user profile.
365
+ - **Source provenance**: the Library ingests PDFs, DOCX, URLs. Extracted knowledge links back to the exact document section it came from.
366
+ - **Working Memory**: an AI-generated daily briefing that evolves, not a static user profile.
172
367
  - **Cross-AI continuity**: knowledge captured in any tool (Cursor, Claude, ChatGPT) flows to OpenClaw and back.
173
- - **Typed memories**: 8 knowledge types mapped to graph node properties — structured understanding, not text blobs.
174
- - **Multi-signal search**: not just semantic similarity — combines embedding, BM25 keyword, label match, graph & community signals, and recency/importance decay. See [Search & Relevance](https://mem.nowledge.co/docs/search-relevance).
368
+ - **Typed memories**: 8 knowledge types mapped to graph node properties. Structured understanding, not text blobs.
369
+ - **Multi-signal search**: not just semantic similarity. Combines embedding, BM25 keyword, label match, graph & community signals, and recency/importance decay. See [Search & Relevance](https://mem.nowledge.co/docs/search-relevance).
175
370
 
176
371
  ## License
177
372
 
@@ -1,23 +1,23 @@
1
1
  {
2
2
  "id": "openclaw-nowledge-mem",
3
- "version": "0.6.2",
3
+ "version": "0.6.6",
4
4
  "kind": "memory",
5
5
  "uiHints": {
6
- "autoRecall": {
7
- "label": "Auto-recall at session start",
8
- "help": "Inject Working Memory and relevant memories at session start. Off by default \u2014 the agent can call memory_search on demand instead."
6
+ "sessionContext": {
7
+ "label": "Session context injection",
8
+ "help": "Inject Working Memory and relevant memories at prompt time. Off by default \u2014 the agent can call memory_search on demand instead."
9
9
  },
10
- "autoCapture": {
11
- "label": "Auto-capture at session end",
12
- "help": "Capture conversation threads and distill key memories via LLM at session end"
10
+ "sessionDigest": {
11
+ "label": "Session digest at end",
12
+ "help": "Capture conversation threads and distill key memories via LLM at session end. On by default \u2014 negligible cost, one triage call per session."
13
13
  },
14
- "captureMinInterval": {
15
- "label": "Minimum capture interval (seconds)",
16
- "help": "Minimum seconds between auto-captures for the same thread. Prevents heartbeat-driven burst captures. Set to 0 for no limit."
14
+ "digestMinInterval": {
15
+ "label": "Minimum digest interval (seconds)",
16
+ "help": "Minimum seconds between session digests for the same thread. Prevents heartbeat-driven burst captures. Set to 0 for no limit."
17
17
  },
18
- "maxRecallResults": {
19
- "label": "Max recall results",
20
- "help": "How many memories to inject for each recall cycle (1\u201320)"
18
+ "maxContextResults": {
19
+ "label": "Max context results",
20
+ "help": "How many memories to inject for each context cycle (1\u201320)"
21
21
  },
22
22
  "apiUrl": {
23
23
  "label": "Server URL (remote mode)",
@@ -32,29 +32,29 @@
32
32
  "configSchema": {
33
33
  "type": "object",
34
34
  "properties": {
35
- "autoRecall": {
35
+ "sessionContext": {
36
36
  "type": "boolean",
37
37
  "default": false,
38
- "description": "Inject Working Memory and recalled memories at session start. Off by default \u2014 the agent queries memory on demand via tools."
38
+ "description": "Inject Working Memory and recalled memories at prompt time. Off by default \u2014 the agent queries memory on demand via tools."
39
39
  },
40
- "autoCapture": {
40
+ "sessionDigest": {
41
41
  "type": "boolean",
42
- "default": false,
43
- "description": "Capture conversation threads and distill key memories via LLM at session end"
42
+ "default": true,
43
+ "description": "Capture conversation threads and distill key memories via LLM at session end. On by default."
44
44
  },
45
- "captureMinInterval": {
45
+ "digestMinInterval": {
46
46
  "type": "integer",
47
47
  "default": 300,
48
48
  "minimum": 0,
49
49
  "maximum": 86400,
50
- "description": "Minimum seconds between auto-captures for the same thread (0 = no limit). Prevents heartbeat-driven burst captures."
50
+ "description": "Minimum seconds between session digests for the same thread (0 = no limit). Prevents heartbeat-driven burst captures."
51
51
  },
52
- "maxRecallResults": {
52
+ "maxContextResults": {
53
53
  "type": "integer",
54
54
  "default": 5,
55
55
  "minimum": 1,
56
56
  "maximum": 20,
57
- "description": "Maximum memories to recall at session start (only used when autoRecall is enabled)"
57
+ "description": "Maximum memories to inject at prompt time (only used when sessionContext is enabled)"
58
58
  },
59
59
  "apiUrl": {
60
60
  "type": "string",
@@ -64,7 +64,7 @@
64
64
  "apiKey": {
65
65
  "type": "string",
66
66
  "default": "",
67
- "description": "API key for remote access. Stored in plugin config, injected as NMEM_API_KEY env var \u2014 never logged."
67
+ "description": "API key for remote access. Stored in plugin config, injected as NMEM_API_KEY env var. Never logged."
68
68
  }
69
69
  },
70
70
  "additionalProperties": false
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowledge/openclaw-nowledge-mem",
3
- "version": "0.6.2",
3
+ "version": "0.6.6",
4
4
  "type": "module",
5
5
  "description": "Nowledge Mem memory plugin for OpenClaw, local-first personal knowledge base",
6
6
  "author": {
package/src/client.js CHANGED
@@ -359,6 +359,13 @@ export class NowledgeMemClient {
359
359
  eventStart: m.event_start ?? null,
360
360
  eventEnd: m.event_end ?? null,
361
361
  temporalContext: m.temporal_context ?? null,
362
+ // Thread provenance: links memory to the conversation it was distilled from.
363
+ // CLI returns `source_thread`, API metadata returns `source_thread_id`.
364
+ sourceThreadId:
365
+ m.source_thread ??
366
+ m.source_thread_id ??
367
+ m.metadata?.source_thread_id ??
368
+ null,
362
369
  };
363
370
  }
364
371
 
@@ -722,6 +729,181 @@ export class NowledgeMemClient {
722
729
  );
723
730
  }
724
731
 
732
+ // ── Thread search ────────────────────────────────────────────────────────
733
+
734
+ /**
735
+ * Search conversation threads by keyword. Uses BM25 full-text search
736
+ * across all stored messages.
737
+ *
738
+ * @param {string} query Search keywords
739
+ * @param {number} [limit=3] Max threads to return
740
+ * @returns {Promise<{ threads: Array, totalFound: number }>}
741
+ */
742
+ async searchThreads(query, limit = 3) {
743
+ const normalizedLimit = Math.min(
744
+ 20,
745
+ Math.max(1, Math.trunc(Number(limit) || 3)),
746
+ );
747
+ const qs = new URLSearchParams({
748
+ query: String(query || ""),
749
+ mode: "full",
750
+ limit: String(normalizedLimit),
751
+ });
752
+ try {
753
+ const data = await this.apiJson(
754
+ "GET",
755
+ `/threads/search?${qs.toString()}`,
756
+ );
757
+ return {
758
+ threads: data.threads ?? [],
759
+ totalFound: Number(data.total_found ?? 0),
760
+ };
761
+ } catch (err) {
762
+ // Thread search is best-effort — don't fail the whole operation
763
+ this.logger.warn?.(
764
+ `searchThreads failed: ${err instanceof Error ? err.message : String(err)}`,
765
+ );
766
+ return { threads: [], totalFound: 0 };
767
+ }
768
+ }
769
+
770
+ /**
771
+ * Full-featured thread search — used by nowledge_mem_thread_search tool.
772
+ * Unlike searchThreads() (best-effort, swallows errors), this throws on failure
773
+ * and supports additional parameters.
774
+ *
775
+ * Uses CLI: `nmem --json t search "<query>" --limit N`
776
+ * Falls back to API: GET /threads/search
777
+ *
778
+ * @param {string} query Search keywords
779
+ * @param {{ limit?: number; source?: string }} [options]
780
+ * @returns {Promise<{ threads: Array, totalFound: number }>}
781
+ */
782
+ async searchThreadsFull(query, { limit = 10, source } = {}) {
783
+ const normalizedLimit = Math.min(
784
+ 50,
785
+ Math.max(1, Math.trunc(Number(limit) || 10)),
786
+ );
787
+ const args = [
788
+ "--json",
789
+ "t",
790
+ "search",
791
+ String(query || ""),
792
+ "--limit",
793
+ String(normalizedLimit),
794
+ ];
795
+ if (source) args.push("--source", String(source));
796
+
797
+ try {
798
+ const data = this.execJson(args);
799
+ return {
800
+ threads: data.threads ?? [],
801
+ totalFound: Number(data.total_found ?? data.total ?? 0),
802
+ };
803
+ } catch (err) {
804
+ const message = err instanceof Error ? err.message : String(err);
805
+ const needsFallback =
806
+ message.includes("unrecognized arguments") ||
807
+ message.includes("invalid choice") ||
808
+ message.includes("argument command: invalid choice");
809
+ if (!needsFallback) throw err;
810
+
811
+ this.logger.warn("searchThreadsFull: CLI too old, falling back to API");
812
+ const qs = new URLSearchParams({
813
+ query: String(query || ""),
814
+ mode: "full",
815
+ limit: String(normalizedLimit),
816
+ });
817
+ if (source) qs.set("source", String(source));
818
+ const data = await this.apiJson(
819
+ "GET",
820
+ `/threads/search?${qs.toString()}`,
821
+ );
822
+ return {
823
+ threads: data.threads ?? [],
824
+ totalFound: Number(data.total_found ?? 0),
825
+ };
826
+ }
827
+ }
828
+
829
+ /**
830
+ * Fetch full messages from a specific thread.
831
+ * Supports pagination for progressive retrieval of long conversations.
832
+ *
833
+ * Uses CLI: `nmem --json t show <id> --limit N --offset O`
834
+ * Falls back to API: GET /threads/{id}
835
+ *
836
+ * @param {string} threadId Thread ID
837
+ * @param {{ offset?: number; limit?: number }} [options]
838
+ */
839
+ async fetchThread(threadId, { offset = 0, limit = 50 } = {}) {
840
+ const id = String(threadId || "").trim();
841
+ if (!id) throw new Error("fetchThread requires threadId");
842
+
843
+ const normalizedLimit = Math.min(
844
+ 200,
845
+ Math.max(1, Math.trunc(Number(limit) || 50)),
846
+ );
847
+ const normalizedOffset = Math.max(0, Math.trunc(Number(offset) || 0));
848
+
849
+ const args = [
850
+ "--json",
851
+ "t",
852
+ "show",
853
+ id,
854
+ "--limit",
855
+ String(normalizedLimit),
856
+ ];
857
+ if (normalizedOffset > 0) {
858
+ args.push("--offset", String(normalizedOffset));
859
+ }
860
+
861
+ try {
862
+ const data = this.execJson(args);
863
+ return {
864
+ threadId: data.thread_id ?? data.id ?? id,
865
+ title: data.title ?? "(untitled)",
866
+ source: data.source ?? "unknown",
867
+ messageCount: Number(data.message_count ?? data.total_messages ?? 0),
868
+ messages: (data.messages ?? []).map((msg) => ({
869
+ role: String(msg.role ?? "unknown"),
870
+ content: String(msg.content ?? ""),
871
+ timestamp: msg.timestamp ?? msg.created_at ?? null,
872
+ })),
873
+ };
874
+ } catch (err) {
875
+ const message = err instanceof Error ? err.message : String(err);
876
+ const needsFallback =
877
+ message.includes("unrecognized arguments") ||
878
+ message.includes("invalid choice") ||
879
+ message.includes("argument command: invalid choice");
880
+ if (!needsFallback) throw err;
881
+
882
+ this.logger.warn("fetchThread: CLI too old, falling back to API");
883
+ const qs = new URLSearchParams({
884
+ limit: String(normalizedLimit),
885
+ });
886
+ if (normalizedOffset > 0) {
887
+ qs.set("offset", String(normalizedOffset));
888
+ }
889
+ const data = await this.apiJson(
890
+ "GET",
891
+ `/threads/${encodeURIComponent(id)}?${qs.toString()}`,
892
+ );
893
+ return {
894
+ threadId: data.thread_id ?? data.id ?? id,
895
+ title: data.title ?? "(untitled)",
896
+ source: data.source ?? "unknown",
897
+ messageCount: Number(data.message_count ?? data.total_messages ?? 0),
898
+ messages: (data.messages ?? []).map((msg) => ({
899
+ role: String(msg.role ?? "unknown"),
900
+ content: String(msg.content ?? ""),
901
+ timestamp: msg.timestamp ?? msg.created_at ?? null,
902
+ })),
903
+ };
904
+ }
905
+ }
906
+
725
907
  async checkHealth() {
726
908
  try {
727
909
  this.exec(["status"]);