@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/CHANGELOG.md +135 -37
- package/CLAUDE.md +72 -52
- package/README.md +240 -45
- package/openclaw.plugin.json +23 -23
- package/package.json +1 -1
- package/src/client.js +182 -0
- package/src/config.js +234 -29
- package/src/hooks/behavioral.js +43 -0
- package/src/hooks/capture.js +3 -3
- package/src/hooks/recall.js +1 -1
- package/src/index.js +19 -6
- package/src/tools/forget.js +4 -3
- package/src/tools/memory-get.js +19 -9
- package/src/tools/memory-search.js +38 -9
- package/src/tools/save.js +33 -0
- package/src/tools/thread-fetch.js +106 -0
- package/src/tools/thread-search.js +108 -0
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
|
|
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
|
|
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
|
|
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
|
-
"
|
|
44
|
-
|
|
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
|
|
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**
|
|
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**
|
|
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**
|
|
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
|
-
|
|
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
|
|
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**
|
|
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**
|
|
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
|
-
|
|
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**
|
|
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
|
-
|
|
112
|
-
- [Memory saved] UV guide
|
|
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**
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
267
|
+
### Session Context (`sessionContext`, default: false)
|
|
134
268
|
|
|
135
|
-
|
|
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
|
|
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
|
-
| `
|
|
163
|
-
| `
|
|
164
|
-
| `
|
|
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
|
|
171
|
-
- **Working Memory**: an AI-generated daily briefing that evolves
|
|
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
|
|
174
|
-
- **Multi-signal search**: not just semantic similarity
|
|
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
|
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "openclaw-nowledge-mem",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.6",
|
|
4
4
|
"kind": "memory",
|
|
5
5
|
"uiHints": {
|
|
6
|
-
"
|
|
7
|
-
"label": "
|
|
8
|
-
"help": "Inject Working Memory and relevant memories at
|
|
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
|
-
"
|
|
11
|
-
"label": "
|
|
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
|
-
"
|
|
15
|
-
"label": "Minimum
|
|
16
|
-
"help": "Minimum seconds between
|
|
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
|
-
"
|
|
19
|
-
"label": "Max
|
|
20
|
-
"help": "How many memories to inject for each
|
|
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
|
-
"
|
|
35
|
+
"sessionContext": {
|
|
36
36
|
"type": "boolean",
|
|
37
37
|
"default": false,
|
|
38
|
-
"description": "Inject Working Memory and recalled memories at
|
|
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
|
-
"
|
|
40
|
+
"sessionDigest": {
|
|
41
41
|
"type": "boolean",
|
|
42
|
-
"default":
|
|
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
|
-
"
|
|
45
|
+
"digestMinInterval": {
|
|
46
46
|
"type": "integer",
|
|
47
47
|
"default": 300,
|
|
48
48
|
"minimum": 0,
|
|
49
49
|
"maximum": 86400,
|
|
50
|
-
"description": "Minimum seconds between
|
|
50
|
+
"description": "Minimum seconds between session digests for the same thread (0 = no limit). Prevents heartbeat-driven burst captures."
|
|
51
51
|
},
|
|
52
|
-
"
|
|
52
|
+
"maxContextResults": {
|
|
53
53
|
"type": "integer",
|
|
54
54
|
"default": 5,
|
|
55
55
|
"minimum": 1,
|
|
56
56
|
"maximum": 20,
|
|
57
|
-
"description": "Maximum memories to
|
|
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
|
|
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
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"]);
|