@nowledge/openclaw-nowledge-mem 0.6.8 → 0.6.12
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 +40 -0
- package/CLAUDE.md +18 -8
- package/README.md +67 -15
- package/SKILL.md +245 -0
- package/openclaw.plugin.json +18 -6
- package/package.json +11 -2
- package/skills/memory-guide/SKILL.md +65 -0
- package/src/client.js +105 -73
- package/src/commands/slash.js +2 -2
- package/src/config.js +18 -1
- package/src/hooks/behavioral.js +11 -11
- package/src/hooks/capture.js +7 -2
- package/src/hooks/recall.js +147 -9
- package/src/index.js +1 -1
- package/src/spawn-env.js +19 -0
- package/src/tools/connections.js +1 -3
- package/src/tools/forget.js +2 -2
- package/src/tools/save.js +1 -1
- package/src/tools/status.js +8 -1
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nowledge-mem-guide
|
|
3
|
+
description: >
|
|
4
|
+
Cross-AI personal knowledge graph memory (Nowledge Mem). Stores memories from this tool,
|
|
5
|
+
Claude Code, Cursor, browser capture, imported documents, and more. Use when: (1) the user
|
|
6
|
+
asks about prior work, decisions, preferences, people, dates, plans, or todos, (2) the user
|
|
7
|
+
references something discussed before or says "remind me" / "what did we decide" / "what was
|
|
8
|
+
I working on", (3) the conversation produces a decision, preference, plan, or learning worth
|
|
9
|
+
keeping, (4) the user wants to browse recent activity or explore how ideas connect, (5) the
|
|
10
|
+
user asks about a past conversation or wants to find a specific thread.
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Nowledge Mem
|
|
14
|
+
|
|
15
|
+
## Search (memory_search)
|
|
16
|
+
|
|
17
|
+
Use natural language queries. This is semantic search, not file search.
|
|
18
|
+
|
|
19
|
+
- "database choice for task events" (not "MEMORY.md" or file paths)
|
|
20
|
+
- "meeting with Sarah about API redesign"
|
|
21
|
+
- "deployment procedure for production"
|
|
22
|
+
|
|
23
|
+
Results include `matchedVia` (scoring breakdown), `importance`, `labels`, `sourceThreadId`.
|
|
24
|
+
|
|
25
|
+
When results include `relatedThreads`, use `nowledge_mem_thread_fetch` with the `threadId` for full conversation context. Start with a small page; fetch more via `offset` + `limit` only when needed.
|
|
26
|
+
|
|
27
|
+
Bi-temporal filters narrow results by time:
|
|
28
|
+
|
|
29
|
+
- `event_date_from` / `event_date_to` — when the fact happened
|
|
30
|
+
- `recorded_date_from` / `recorded_date_to` — when it was saved
|
|
31
|
+
|
|
32
|
+
## Save (nowledge_mem_save)
|
|
33
|
+
|
|
34
|
+
Save proactively when the conversation produces a decision, preference, plan, procedure, learning, or important context. Do not wait to be asked.
|
|
35
|
+
|
|
36
|
+
Structure saves:
|
|
37
|
+
|
|
38
|
+
- `unit_type`: fact | preference | decision | plan | procedure | learning | context | event
|
|
39
|
+
- `labels`: lowercase-hyphenated topic tags
|
|
40
|
+
- `importance`: 0.0-1.0 (most: 0.5; key decisions/preferences: 0.7-0.9)
|
|
41
|
+
- `event_start` / `event_end`: ISO dates when temporal
|
|
42
|
+
|
|
43
|
+
Skip trivial exchanges, greetings, and meta-conversation about memory itself.
|
|
44
|
+
|
|
45
|
+
## Connections (nowledge_mem_connections)
|
|
46
|
+
|
|
47
|
+
Pass a `memoryId` from search results to explore related memories, evolution chains, and source documents. Edge types: EVOLVES, CRYSTALLIZED_FROM, SOURCED_FROM, MENTIONS.
|
|
48
|
+
|
|
49
|
+
## Timeline (nowledge_mem_timeline)
|
|
50
|
+
|
|
51
|
+
Browse recent activity grouped by day. Filter by `event_type` or exact `date_from` / `date_to` range.
|
|
52
|
+
|
|
53
|
+
## Threads (nowledge_mem_thread_search + nowledge_mem_thread_fetch)
|
|
54
|
+
|
|
55
|
+
Find past conversations by keyword, then progressively fetch messages. Threads span all sources: this tool, other AI tools, browser capture, imports.
|
|
56
|
+
|
|
57
|
+
## Working Memory (nowledge_mem_context)
|
|
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.
|
|
60
|
+
|
|
61
|
+
## Important
|
|
62
|
+
|
|
63
|
+
- Search uses natural language queries, not file paths. Never search for "MEMORY.md" or "memory/*.md".
|
|
64
|
+
- When `relatedThreads` appear in results, they often contain the most useful context.
|
|
65
|
+
- Use tools directly; do not tell the user to "check your memory."
|
package/src/client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { buildNmemSpawnEnv } from "./spawn-env.js";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Patch a single markdown section in a Working Memory document.
|
|
@@ -66,18 +66,21 @@ function patchWmSection(currentContent, heading, { content, append } = {}) {
|
|
|
66
66
|
*
|
|
67
67
|
* Credential rules:
|
|
68
68
|
* - apiUrl: passed to CLI via --api-url flag (not a secret)
|
|
69
|
-
* - apiKey: injected into the
|
|
69
|
+
* - apiKey: injected into the command runner env as NMEM_API_KEY ONLY
|
|
70
70
|
* (never passed as a CLI arg to avoid exposure in `ps aux`)
|
|
71
71
|
* (never logged, even at debug level)
|
|
72
72
|
*/
|
|
73
73
|
export class NowledgeMemClient {
|
|
74
74
|
/**
|
|
75
75
|
* @param {object} logger
|
|
76
|
+
* @param {object} runtimeSystem
|
|
76
77
|
* @param {{ apiUrl?: string; apiKey?: string }} [credentials]
|
|
77
78
|
*/
|
|
78
|
-
constructor(logger, credentials = {}) {
|
|
79
|
+
constructor(logger, runtimeSystem, credentials = {}) {
|
|
79
80
|
this.logger = logger;
|
|
81
|
+
this.runtimeSystem = runtimeSystem;
|
|
80
82
|
this.nmemCmd = null;
|
|
83
|
+
this.nmemCmdPromise = null;
|
|
81
84
|
// Resolved once from config + env (config wins over env, both win over default)
|
|
82
85
|
this._apiUrl =
|
|
83
86
|
(credentials.apiUrl || "").trim() || "http://127.0.0.1:14242";
|
|
@@ -138,28 +141,42 @@ export class NowledgeMemClient {
|
|
|
138
141
|
|
|
139
142
|
// ── CLI helpers ────────────────────────────────────────────────────────────
|
|
140
143
|
|
|
141
|
-
resolveCommand() {
|
|
144
|
+
async resolveCommand() {
|
|
142
145
|
if (this.nmemCmd) return this.nmemCmd;
|
|
146
|
+
if (this.nmemCmdPromise) return this.nmemCmdPromise;
|
|
143
147
|
|
|
144
148
|
const candidates = [["nmem"], ["uvx", "--from", "nmem-cli", "nmem"]];
|
|
145
149
|
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
150
|
+
this.nmemCmdPromise = (async () => {
|
|
151
|
+
for (const cmd of candidates) {
|
|
152
|
+
try {
|
|
153
|
+
const result = await this.runtimeSystem.runCommandWithTimeout(
|
|
154
|
+
[...cmd, "--version"],
|
|
155
|
+
{
|
|
156
|
+
timeoutMs: 10_000,
|
|
157
|
+
env: this._spawnEnv(),
|
|
158
|
+
},
|
|
159
|
+
);
|
|
160
|
+
if ((result.code ?? 1) === 0) {
|
|
161
|
+
this.nmemCmd = cmd;
|
|
162
|
+
this.logger.info(`nmem resolved: ${cmd.join(" ")}`);
|
|
163
|
+
return cmd;
|
|
164
|
+
}
|
|
165
|
+
} catch {
|
|
166
|
+
// Try the next candidate.
|
|
167
|
+
}
|
|
157
168
|
}
|
|
158
|
-
}
|
|
159
169
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
170
|
+
throw new Error(
|
|
171
|
+
"nmem CLI not found. Install with: pip install nmem-cli (or use uvx)",
|
|
172
|
+
);
|
|
173
|
+
})();
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
return await this.nmemCmdPromise;
|
|
177
|
+
} finally {
|
|
178
|
+
this.nmemCmdPromise = null;
|
|
179
|
+
}
|
|
163
180
|
}
|
|
164
181
|
|
|
165
182
|
/**
|
|
@@ -167,15 +184,10 @@ export class NowledgeMemClient {
|
|
|
167
184
|
* apiKey is injected here — NEVER via CLI args.
|
|
168
185
|
*/
|
|
169
186
|
_spawnEnv() {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
if (this._apiKey) {
|
|
176
|
-
env.NMEM_API_KEY = this._apiKey;
|
|
177
|
-
}
|
|
178
|
-
return env;
|
|
187
|
+
return buildNmemSpawnEnv({
|
|
188
|
+
apiUrl: this._apiUrl,
|
|
189
|
+
apiKey: this._apiKey,
|
|
190
|
+
});
|
|
179
191
|
}
|
|
180
192
|
|
|
181
193
|
/**
|
|
@@ -188,41 +200,68 @@ export class NowledgeMemClient {
|
|
|
188
200
|
: [];
|
|
189
201
|
}
|
|
190
202
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const [
|
|
203
|
+
_commandLabel(cmd, args) {
|
|
204
|
+
let i = cmd.length;
|
|
205
|
+
const argv = [...cmd, ...this._apiUrlArgs(), ...args];
|
|
206
|
+
while (i < argv.length) {
|
|
207
|
+
const token = argv[i];
|
|
208
|
+
if (token === "--api-url") {
|
|
209
|
+
i += 2;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
const route = argv
|
|
215
|
+
.slice(i, i + 2)
|
|
216
|
+
.filter(Boolean)
|
|
217
|
+
.join(" ");
|
|
218
|
+
return route ? `nmem ${route}` : cmd.join(" ");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
_summarizeExecError(message) {
|
|
222
|
+
if (!message) return "failed";
|
|
223
|
+
if (message.includes("timed out")) return "timed out";
|
|
224
|
+
const codeMatch = message.match(/exited with code\s+(\d+)/i);
|
|
225
|
+
if (codeMatch) return `exited with code ${codeMatch[1]}`;
|
|
226
|
+
return "failed";
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async exec(args, timeout = 30_000) {
|
|
230
|
+
const cmd = await this.resolveCommand();
|
|
231
|
+
const argv = [...cmd, ...this._apiUrlArgs(), ...args];
|
|
232
|
+
const commandLabel = this._commandLabel(cmd, args);
|
|
194
233
|
try {
|
|
195
|
-
const result =
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
throw
|
|
234
|
+
const result = await this.runtimeSystem.runCommandWithTimeout(argv, {
|
|
235
|
+
timeoutMs: timeout,
|
|
236
|
+
env: this._spawnEnv(),
|
|
237
|
+
});
|
|
238
|
+
if (
|
|
239
|
+
result.termination === "timeout" ||
|
|
240
|
+
result.termination === "no-output-timeout"
|
|
241
|
+
) {
|
|
242
|
+
const message =
|
|
243
|
+
(result.stderr || result.stdout || "").trim() ||
|
|
244
|
+
`nmem timed out after ${timeout}ms`;
|
|
245
|
+
throw new Error(message);
|
|
207
246
|
}
|
|
208
|
-
if (result.
|
|
247
|
+
if ((result.code ?? 1) !== 0) {
|
|
209
248
|
const message = (result.stderr || result.stdout || "").trim();
|
|
210
249
|
throw new Error(
|
|
211
|
-
message || `nmem exited with code ${String(result.
|
|
250
|
+
message || `nmem exited with code ${String(result.code)}`,
|
|
212
251
|
);
|
|
213
252
|
}
|
|
214
253
|
return String(result.stdout ?? "").trim();
|
|
215
254
|
} catch (err) {
|
|
216
255
|
const message = err instanceof Error ? err.message : String(err);
|
|
217
256
|
this.logger.error(
|
|
218
|
-
`nmem command failed
|
|
257
|
+
`nmem command failed (${commandLabel}): ${this._summarizeExecError(message)}`,
|
|
219
258
|
);
|
|
220
259
|
throw err;
|
|
221
260
|
}
|
|
222
261
|
}
|
|
223
262
|
|
|
224
|
-
execJson(args, timeout = 30_000) {
|
|
225
|
-
const raw = this.exec(args, timeout);
|
|
263
|
+
async execJson(args, timeout = 30_000) {
|
|
264
|
+
const raw = await this.exec(args, timeout);
|
|
226
265
|
return JSON.parse(raw);
|
|
227
266
|
}
|
|
228
267
|
|
|
@@ -237,7 +276,7 @@ export class NowledgeMemClient {
|
|
|
237
276
|
100,
|
|
238
277
|
Math.max(1, Math.trunc(Number(limit) || 5)),
|
|
239
278
|
);
|
|
240
|
-
const data = this.execJson([
|
|
279
|
+
const data = await this.execJson([
|
|
241
280
|
"--json",
|
|
242
281
|
"m",
|
|
243
282
|
"search",
|
|
@@ -296,7 +335,7 @@ export class NowledgeMemClient {
|
|
|
296
335
|
|
|
297
336
|
let data;
|
|
298
337
|
try {
|
|
299
|
-
data = this.execJson(args);
|
|
338
|
+
data = await this.execJson(args);
|
|
300
339
|
} catch (err) {
|
|
301
340
|
// Graceful fallback: CLI doesn't know --event-from yet → use API directly
|
|
302
341
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -388,7 +427,7 @@ export class NowledgeMemClient {
|
|
|
388
427
|
];
|
|
389
428
|
|
|
390
429
|
try {
|
|
391
|
-
return this.execJson(args);
|
|
430
|
+
return await this.execJson(args);
|
|
392
431
|
} catch (err) {
|
|
393
432
|
const message = err instanceof Error ? err.message : String(err);
|
|
394
433
|
const needsFallback =
|
|
@@ -422,7 +461,7 @@ export class NowledgeMemClient {
|
|
|
422
461
|
String(limit),
|
|
423
462
|
];
|
|
424
463
|
try {
|
|
425
|
-
return this.execJson(args);
|
|
464
|
+
return await this.execJson(args);
|
|
426
465
|
} catch (err) {
|
|
427
466
|
const message = err instanceof Error ? err.message : String(err);
|
|
428
467
|
const needsFallback =
|
|
@@ -472,7 +511,7 @@ export class NowledgeMemClient {
|
|
|
472
511
|
if (dateTo) args.push("--to", String(dateTo));
|
|
473
512
|
|
|
474
513
|
try {
|
|
475
|
-
const data = this.execJson(args);
|
|
514
|
+
const data = await this.execJson(args);
|
|
476
515
|
return Array.isArray(data) ? data : (data.events ?? []);
|
|
477
516
|
} catch (err) {
|
|
478
517
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -506,7 +545,7 @@ export class NowledgeMemClient {
|
|
|
506
545
|
if (importance !== undefined && Number.isFinite(Number(importance))) {
|
|
507
546
|
args.push("-i", String(importance));
|
|
508
547
|
}
|
|
509
|
-
const data = this.execJson(args);
|
|
548
|
+
const data = await this.execJson(args);
|
|
510
549
|
return String(data.id ?? data.memory?.id ?? data.memory_id ?? "created");
|
|
511
550
|
}
|
|
512
551
|
|
|
@@ -535,7 +574,7 @@ export class NowledgeMemClient {
|
|
|
535
574
|
if (threadId) {
|
|
536
575
|
args.push("--id", String(threadId));
|
|
537
576
|
}
|
|
538
|
-
data = this.execJson(args);
|
|
577
|
+
data = await this.execJson(args);
|
|
539
578
|
} catch (err) {
|
|
540
579
|
const message = err instanceof Error ? err.message : String(err);
|
|
541
580
|
const needsApiFallback =
|
|
@@ -588,7 +627,7 @@ export class NowledgeMemClient {
|
|
|
588
627
|
if (idempotencyKey) {
|
|
589
628
|
args.push("--idempotency-key", String(idempotencyKey));
|
|
590
629
|
}
|
|
591
|
-
const data = this.execJson(args);
|
|
630
|
+
const data = await this.execJson(args);
|
|
592
631
|
return {
|
|
593
632
|
messagesAdded: Number(data.messages_added ?? 0),
|
|
594
633
|
totalMessages: Number(data.total_messages ?? 0),
|
|
@@ -627,7 +666,7 @@ export class NowledgeMemClient {
|
|
|
627
666
|
if (!id) {
|
|
628
667
|
throw new Error("getMemory requires memoryId");
|
|
629
668
|
}
|
|
630
|
-
return this.execJson(["--json", "m", "show", id]);
|
|
669
|
+
return await this.execJson(["--json", "m", "show", id]);
|
|
631
670
|
}
|
|
632
671
|
|
|
633
672
|
isThreadNotFoundError(err) {
|
|
@@ -643,7 +682,7 @@ export class NowledgeMemClient {
|
|
|
643
682
|
|
|
644
683
|
async readWorkingMemory() {
|
|
645
684
|
try {
|
|
646
|
-
const data = this.execJson(["--json", "wm", "read"], 15_000);
|
|
685
|
+
const data = await this.execJson(["--json", "wm", "read"], 15_000);
|
|
647
686
|
const content = String(data.content ?? "").trim();
|
|
648
687
|
const exists = Boolean(data.exists);
|
|
649
688
|
return { content, available: exists && content.length > 0 };
|
|
@@ -667,7 +706,7 @@ export class NowledgeMemClient {
|
|
|
667
706
|
if (append !== undefined) args.push("--append", String(append));
|
|
668
707
|
|
|
669
708
|
try {
|
|
670
|
-
return this.execJson(args, 20_000);
|
|
709
|
+
return await this.execJson(args, 20_000);
|
|
671
710
|
} catch (err) {
|
|
672
711
|
const message = err instanceof Error ? err.message : String(err);
|
|
673
712
|
const notSupported =
|
|
@@ -772,7 +811,7 @@ export class NowledgeMemClient {
|
|
|
772
811
|
* Unlike searchThreads() (best-effort, swallows errors), this throws on failure
|
|
773
812
|
* and supports additional parameters.
|
|
774
813
|
*
|
|
775
|
-
* Uses CLI: `nmem --json t search "<query>"
|
|
814
|
+
* Uses CLI: `nmem --json t search "<query>" -n N`
|
|
776
815
|
* Falls back to API: GET /threads/search
|
|
777
816
|
*
|
|
778
817
|
* @param {string} query Search keywords
|
|
@@ -789,13 +828,13 @@ export class NowledgeMemClient {
|
|
|
789
828
|
"t",
|
|
790
829
|
"search",
|
|
791
830
|
String(query || ""),
|
|
792
|
-
"
|
|
831
|
+
"-n",
|
|
793
832
|
String(normalizedLimit),
|
|
794
833
|
];
|
|
795
834
|
if (source) args.push("--source", String(source));
|
|
796
835
|
|
|
797
836
|
try {
|
|
798
|
-
const data = this.execJson(args);
|
|
837
|
+
const data = await this.execJson(args);
|
|
799
838
|
return {
|
|
800
839
|
threads: data.threads ?? [],
|
|
801
840
|
totalFound: Number(data.total_found ?? data.total ?? 0),
|
|
@@ -830,7 +869,7 @@ export class NowledgeMemClient {
|
|
|
830
869
|
* Fetch full messages from a specific thread.
|
|
831
870
|
* Supports pagination for progressive retrieval of long conversations.
|
|
832
871
|
*
|
|
833
|
-
* Uses CLI: `nmem --json t show <id>
|
|
872
|
+
* Uses CLI: `nmem --json t show <id> -n N --offset O`
|
|
834
873
|
* Falls back to API: GET /threads/{id}
|
|
835
874
|
*
|
|
836
875
|
* @param {string} threadId Thread ID
|
|
@@ -846,20 +885,13 @@ export class NowledgeMemClient {
|
|
|
846
885
|
);
|
|
847
886
|
const normalizedOffset = Math.max(0, Math.trunc(Number(offset) || 0));
|
|
848
887
|
|
|
849
|
-
const args = [
|
|
850
|
-
"--json",
|
|
851
|
-
"t",
|
|
852
|
-
"show",
|
|
853
|
-
id,
|
|
854
|
-
"--limit",
|
|
855
|
-
String(normalizedLimit),
|
|
856
|
-
];
|
|
888
|
+
const args = ["--json", "t", "show", id, "-n", String(normalizedLimit)];
|
|
857
889
|
if (normalizedOffset > 0) {
|
|
858
890
|
args.push("--offset", String(normalizedOffset));
|
|
859
891
|
}
|
|
860
892
|
|
|
861
893
|
try {
|
|
862
|
-
const data = this.execJson(args);
|
|
894
|
+
const data = await this.execJson(args);
|
|
863
895
|
return {
|
|
864
896
|
threadId: data.thread_id ?? data.id ?? id,
|
|
865
897
|
title: data.title ?? "(untitled)",
|
|
@@ -906,7 +938,7 @@ export class NowledgeMemClient {
|
|
|
906
938
|
|
|
907
939
|
async checkHealth() {
|
|
908
940
|
try {
|
|
909
|
-
this.
|
|
941
|
+
await this.execJson(["--json", "status"]);
|
|
910
942
|
return true;
|
|
911
943
|
} catch {
|
|
912
944
|
return false;
|
package/src/commands/slash.js
CHANGED
|
@@ -38,7 +38,7 @@ export function createForgetCommand(client, logger) {
|
|
|
38
38
|
|
|
39
39
|
try {
|
|
40
40
|
if (isLikelyId) {
|
|
41
|
-
client.exec(["--json", "m", "delete", "-f", text]);
|
|
41
|
+
await client.exec(["--json", "m", "delete", "-f", text]);
|
|
42
42
|
logger.info(`/forget: deleted memory ${text}`);
|
|
43
43
|
return { text: `Forgotten: memory ${text} deleted.` };
|
|
44
44
|
}
|
|
@@ -50,7 +50,7 @@ export function createForgetCommand(client, logger) {
|
|
|
50
50
|
|
|
51
51
|
if (results[0].score >= 0.85) {
|
|
52
52
|
const target = results[0];
|
|
53
|
-
client.exec(["--json", "m", "delete", "-f", target.id]);
|
|
53
|
+
await client.exec(["--json", "m", "delete", "-f", target.id]);
|
|
54
54
|
logger.info(`/forget: deleted memory ${target.id}`);
|
|
55
55
|
const preview = target.title || target.content.slice(0, 60);
|
|
56
56
|
return {
|
package/src/config.js
CHANGED
|
@@ -19,6 +19,7 @@ const ALLOWED_KEYS = new Set([
|
|
|
19
19
|
"sessionDigest",
|
|
20
20
|
"digestMinInterval",
|
|
21
21
|
"maxContextResults",
|
|
22
|
+
"recallMinScore",
|
|
22
23
|
"apiUrl",
|
|
23
24
|
"apiKey",
|
|
24
25
|
// Legacy aliases — accepted but not advertised
|
|
@@ -135,7 +136,7 @@ function firstDefined(...options) {
|
|
|
135
136
|
* will configure via OpenClaw's plugin settings UI (pluginConfig).
|
|
136
137
|
*
|
|
137
138
|
* Canonical keys: sessionContext, sessionDigest, digestMinInterval,
|
|
138
|
-
* maxContextResults, apiUrl, apiKey
|
|
139
|
+
* maxContextResults, recallMinScore, apiUrl, apiKey
|
|
139
140
|
*
|
|
140
141
|
* Legacy aliases (accepted from all sources; never shown in docs):
|
|
141
142
|
* autoRecall → sessionContext
|
|
@@ -148,6 +149,7 @@ function firstDefined(...options) {
|
|
|
148
149
|
* NMEM_SESSION_DIGEST — true/1/yes to enable (alias: NMEM_AUTO_CAPTURE)
|
|
149
150
|
* NMEM_DIGEST_MIN_INTERVAL — seconds (0-86400)
|
|
150
151
|
* NMEM_MAX_CONTEXT_RESULTS — integer (1-20)
|
|
152
|
+
* NMEM_RECALL_MIN_SCORE — integer (0-100)
|
|
151
153
|
* NMEM_API_URL — remote server URL
|
|
152
154
|
* NMEM_API_KEY — API key (never logged)
|
|
153
155
|
*/
|
|
@@ -234,6 +236,20 @@ export function parseConfig(raw, logger) {
|
|
|
234
236
|
const maxContextResults = Math.min(20, Math.max(1, Math.trunc(mcr.value)));
|
|
235
237
|
_sources.maxContextResults = mcr.source;
|
|
236
238
|
|
|
239
|
+
// --- recallMinScore: file > pluginConfig > env > default ---
|
|
240
|
+
const rmsEnv = envInt("NMEM_RECALL_MIN_SCORE");
|
|
241
|
+
const rms = firstDefined(
|
|
242
|
+
{ value: pickNum(resolvedFile, "recallMinScore"), source: "file" },
|
|
243
|
+
{
|
|
244
|
+
value: pickNum(resolvedPlugin, "recallMinScore"),
|
|
245
|
+
source: "pluginConfig",
|
|
246
|
+
},
|
|
247
|
+
{ value: rmsEnv, source: "env" },
|
|
248
|
+
{ value: 0, source: "default" },
|
|
249
|
+
);
|
|
250
|
+
const recallMinScore = Math.min(100, Math.max(0, Math.trunc(rms.value)));
|
|
251
|
+
_sources.recallMinScore = rms.source;
|
|
252
|
+
|
|
237
253
|
// --- apiUrl: file > pluginConfig > env > "" ---
|
|
238
254
|
const fileUrl =
|
|
239
255
|
typeof resolvedFile.apiUrl === "string" && resolvedFile.apiUrl.trim();
|
|
@@ -267,6 +283,7 @@ export function parseConfig(raw, logger) {
|
|
|
267
283
|
sessionDigest,
|
|
268
284
|
digestMinInterval,
|
|
269
285
|
maxContextResults,
|
|
286
|
+
recallMinScore,
|
|
270
287
|
apiUrl,
|
|
271
288
|
apiKey,
|
|
272
289
|
_sources,
|
package/src/hooks/behavioral.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* This is the single most impactful change for memory adoption:
|
|
5
5
|
* most LLMs ignore "call this proactively" in tool descriptions,
|
|
6
|
-
* but reliably follow behavioral guidance in
|
|
6
|
+
* but reliably follow behavioral guidance in system-prompt space.
|
|
7
7
|
*
|
|
8
8
|
* Cost: ~50 tokens per turn. Negligible.
|
|
9
9
|
*
|
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
const BASE_GUIDANCE = [
|
|
16
16
|
"<nowledge-mem-guidance>",
|
|
17
17
|
"You have access to the user's personal knowledge graph (Nowledge Mem).",
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"When
|
|
18
|
+
"Before answering questions about prior work, decisions, dates, people, preferences, or plans:",
|
|
19
|
+
"search with memory_search using natural language queries (not file paths).",
|
|
20
|
+
"memory_search returns memories with scores, labels, and relevant past conversation snippets.",
|
|
21
|
+
"When results include a sourceThreadId, use nowledge_mem_thread_fetch for full conversation context.",
|
|
22
|
+
"When the conversation produces a decision, preference, plan, or learning, save it with nowledge_mem_save.",
|
|
23
23
|
"</nowledge-mem-guidance>",
|
|
24
24
|
].join("\n");
|
|
25
25
|
|
|
@@ -27,10 +27,10 @@ const SESSION_CONTEXT_GUIDANCE = [
|
|
|
27
27
|
"<nowledge-mem-guidance>",
|
|
28
28
|
"You have access to the user's personal knowledge graph (Nowledge Mem).",
|
|
29
29
|
"Relevant memories and your Working Memory have already been injected into this prompt.",
|
|
30
|
-
"Use memory_search
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"When
|
|
30
|
+
"Use memory_search when you need something specific beyond what was auto-recalled (natural language queries, not file paths).",
|
|
31
|
+
"memory_search also returns relevant past conversation snippets alongside memories.",
|
|
32
|
+
"When results include a sourceThreadId, use nowledge_mem_thread_fetch for full conversation context.",
|
|
33
|
+
"When the conversation produces a decision, preference, plan, or learning, save it with nowledge_mem_save.",
|
|
34
34
|
"</nowledge-mem-guidance>",
|
|
35
35
|
].join("\n");
|
|
36
36
|
|
|
@@ -38,6 +38,6 @@ export function buildBehavioralHook(logger, { sessionContext = false } = {}) {
|
|
|
38
38
|
const guidance = sessionContext ? SESSION_CONTEXT_GUIDANCE : BASE_GUIDANCE;
|
|
39
39
|
return (_event, _ctx) => {
|
|
40
40
|
logger.debug?.("behavioral: injecting guidance");
|
|
41
|
-
return {
|
|
41
|
+
return { appendSystemContext: guidance };
|
|
42
42
|
};
|
|
43
43
|
}
|
package/src/hooks/capture.js
CHANGED
|
@@ -250,7 +250,10 @@ function buildConversationText(normalized) {
|
|
|
250
250
|
const parts = [];
|
|
251
251
|
let total = 0;
|
|
252
252
|
for (const m of normalized) {
|
|
253
|
-
const text = truncate(
|
|
253
|
+
const text = truncate(
|
|
254
|
+
m.fullContent || m.content,
|
|
255
|
+
MAX_DISTILL_MESSAGE_CHARS,
|
|
256
|
+
);
|
|
254
257
|
const line = `${m.role}: ${text}`;
|
|
255
258
|
if (total + line.length > MAX_CONVERSATION_CHARS) break;
|
|
256
259
|
parts.push(line);
|
|
@@ -297,7 +300,9 @@ export function buildAgentEndCaptureHandler(client, cfg, logger) {
|
|
|
297
300
|
|
|
298
301
|
// Skip when no new messages were added (e.g. heartbeat re-sync).
|
|
299
302
|
if (!result || result.messagesAdded === 0) {
|
|
300
|
-
logger.debug?.(
|
|
303
|
+
logger.debug?.(
|
|
304
|
+
"capture: no new messages since last sync, skipping triage",
|
|
305
|
+
);
|
|
301
306
|
return;
|
|
302
307
|
}
|
|
303
308
|
|