@nowledge/openclaw-nowledge-mem 0.8.35 → 0.8.39
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 +29 -0
- package/CLAUDE.md +10 -4
- package/README.md +19 -12
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/skills/memory-guide/SKILL.md +8 -2
- package/src/client.js +25 -14
- package/src/generated/plugin-core/acknowledgement.d.ts +6 -0
- package/src/generated/plugin-core/index.d.ts +3 -0
- package/src/generated/plugin-core/index.js +92 -0
- package/src/generated/plugin-core/manifest.json +22 -0
- package/src/generated/plugin-core/package.json +4 -0
- package/src/generated/plugin-core/thread-delta.d.ts +17 -0
- package/src/generated/plugin-core/timeout.d.ts +1 -0
- package/src/hooks/behavioral.js +4 -2
- package/src/hooks/capture.js +2 -7
- package/src/session-delta.js +32 -45
- package/src/thread-sync-timeout.js +1 -14
- package/src/tools/memory-search.js +3 -0
- package/src/tools/thread-fetch.js +6 -6
- package/src/tools/thread-search.js +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,35 @@ All notable changes to the Nowledge Mem OpenClaw plugin will be documented in th
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## [0.8.39] - 2026-10-18
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Schedule automatic distillation by stored thread ID, without the inline content removed in Mem 0.10.7.
|
|
12
|
+
- Report background task acceptance instead of a synchronous memory count, and reject missing scheduling acknowledgements or an unavailable background scheduler.
|
|
13
|
+
|
|
14
|
+
## [0.8.38] - 2026-10-17
|
|
15
|
+
|
|
16
|
+
- Refresh the embedded core 0.2.0 content manifest for Alma adoption; runtime behavior is unchanged.
|
|
17
|
+
|
|
18
|
+
## [0.8.37] - 2026-10-16
|
|
19
|
+
|
|
20
|
+
- Embed shared TypeScript core 0.2.0, including the shared prefix-hashing API.
|
|
21
|
+
- Reuse shared persistence acknowledgements and sync timeouts while retaining
|
|
22
|
+
OpenClaw message projection, compaction-shrink handling, legacy cursors, and
|
|
23
|
+
response-shape normalization. Reject unsafe acknowledgement counts.
|
|
24
|
+
- Retain embedded-core receipts for both npm and ClawHub artifacts.
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
## [0.8.36] - 2026-10-11
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- Search first, inspect at most one small targeted thread range per question, and report evidence gaps rather than walking offsets.
|
|
32
|
+
- Preserve explicit target ranges, progressive graph exploration and existing session capture.
|
|
33
|
+
- Default thread reads to eight messages when no limit is supplied.
|
|
34
|
+
- Preserve matched-message positions so late-thread evidence can be selected without page walking.
|
|
35
|
+
|
|
7
36
|
## [0.8.35] - 2026-10-08
|
|
8
37
|
|
|
9
38
|
- Align ClawHub-first code-plugin install metadata with the supported host baseline.
|
package/CLAUDE.md
CHANGED
|
@@ -49,7 +49,7 @@ src/
|
|
|
49
49
|
timeline.js - activity feed: daily grouping, event_type filter, memoryId hints
|
|
50
50
|
forget.js - memory deletion by ID or search
|
|
51
51
|
thread-search.js - search past conversations by keyword
|
|
52
|
-
thread-fetch.js -
|
|
52
|
+
thread-fetch.js - read one small targeted message range (default 8)
|
|
53
53
|
skills/
|
|
54
54
|
memory-guide/
|
|
55
55
|
SKILL.md - agent behavioral skill: when/how to search, save, explore memory (auto-discovered by OpenClaw)
|
|
@@ -139,9 +139,9 @@ When `contextEngine` points elsewhere (or is absent), hooks handle everything. N
|
|
|
139
139
|
- `nowledge_mem_timeline` - activity feed via `nmem f`. Groups by day. `event_type` filter. Exact date range via `date_from`/`date_to` (YYYY-MM-DD). Entries include `(id: <memoryId>)` for chaining to connections.
|
|
140
140
|
- `nowledge_mem_forget` - delete by ID or fuzzy query.
|
|
141
141
|
|
|
142
|
-
### Thread Tools (
|
|
142
|
+
### Thread Tools (bounded conversation retrieval)
|
|
143
143
|
- `nowledge_mem_thread_search` - search past conversations by keyword. Returns threads with matched message snippets, relevance scores, and message counts. Supports `source` filter.
|
|
144
|
-
- `nowledge_mem_thread_fetch` -
|
|
144
|
+
- `nowledge_mem_thread_fetch` - read one small targeted message range from a specific thread. Choose the needed range from search results before the single read; use a known `offset` once and a small `limit` (default 8).
|
|
145
145
|
|
|
146
146
|
### Diagnostics
|
|
147
147
|
- `nowledge_mem_status` - show plugin trust status, memory slot status, effective config (mode, apiUrl, apiKey set, sessionContext, sessionDigest, etc.), backend connectivity, and version. Checks `plugins.allow`: warns if set but plugin not included (loading blocked); shows tip if not set (plugin loads via `entries.enabled` but trust is implicit). Warns if the memory slot points to `memory-core` instead of `openclaw-nowledge-mem` (common after OpenClaw 3.22+ upgrade). No parameters.
|
|
@@ -216,7 +216,7 @@ Legacy aliases accepted silently in all sources for backward compat:
|
|
|
216
216
|
| `nmem --json m delete <id>` | `client.execJson()` in forget.js | Delete |
|
|
217
217
|
| `GET /threads/search?query=&limit=` | `client.searchThreads()` | Thread search enrichment (best-effort, API-only) |
|
|
218
218
|
| `nmem --json t search <q> -n N` | `client.searchThreadsFull()` | Full thread search (CLI-first, API fallback) |
|
|
219
|
-
| `nmem --json t show <id> -n N --offset O` | `client.fetchThread()` |
|
|
219
|
+
| `nmem --json t show <id> -n N --offset O` | `client.fetchThread()` | Read one small targeted message range (default 8) |
|
|
220
220
|
|
|
221
221
|
All commands have API fallback for older CLI versions.
|
|
222
222
|
|
|
@@ -272,3 +272,9 @@ After bumping, commit inside the `community/` submodule, then stage the updated
|
|
|
272
272
|
- Do NOT expose full WM overwrite from agents - section-level patch is the right granularity.
|
|
273
273
|
- Do NOT add cloud dependencies to the core path.
|
|
274
274
|
- Do NOT accept unknown config keys (strict parser in `config.js`).
|
|
275
|
+
|
|
276
|
+
### Bounded thread reads
|
|
277
|
+
|
|
278
|
+
Search memories or threads first, then choose the specific conversation and message range needed. Call `thread_fetch_messages`, a host thread-fetch tool, or `nmem --json t show` at most once per question, using a small message limit (for example 8) and a content limit of 1200 where supported. A known target range may use a nonzero offset once.
|
|
279
|
+
|
|
280
|
+
Never loop over offsets to reconstruct a conversation. Do not raise `--content-limit` to compensate for a slow or incomplete read. Small output limits do not guarantee cheap server-side work on every deployed backend. If a read is slow, incomplete, fails or times out, stop, report the evidence gap, and refine the search rather than retrying pages. A later explicit user request for another range is a new bounded read; a routine “continue” is not permission to drain the thread.
|
package/README.md
CHANGED
|
@@ -196,14 +196,14 @@ The behavioral skill and always-on hook nudge the agent to **search before answe
|
|
|
196
196
|
| "What was I doing last week?" | `nowledge_mem_timeline` | Activity feed grouped by day. Supports exact date ranges. |
|
|
197
197
|
| "How is X connected to Y?" | `nowledge_mem_connections` | Graph walk: edges, entities, EVOLVES chains, document provenance. |
|
|
198
198
|
| Need startup context or today's focus/priorities | `nowledge_mem_context` | Read Context Bundle when available, with Working Memory fallback. Supports Working Memory section-level patch. |
|
|
199
|
-
| Memory has `sourceThreadId` | `nowledge_mem_thread_fetch` |
|
|
199
|
+
| Memory has `sourceThreadId` | `nowledge_mem_thread_fetch` | Read one small targeted source-message range; do not walk offsets. |
|
|
200
200
|
| "Find our discussion about X" | `nowledge_mem_thread_search` | Search past conversations by keyword. Returns matched snippets. |
|
|
201
201
|
| "Forget X" | `nowledge_mem_forget` | Delete by ID or search query. |
|
|
202
202
|
| "Is my setup working?" | `nowledge_mem_status` | Show effective config, backend connectivity, and version. |
|
|
203
203
|
|
|
204
204
|
### Session Lifecycle (Automatic Capture)
|
|
205
205
|
|
|
206
|
-
|
|
206
|
+
Conversations are captured during the session lifecycle. After positive triage, the plugin schedules background distillation of the saved thread. Task acceptance is logged immediately; extracted memories appear after the backend finishes the task. No user action needed.
|
|
207
207
|
|
|
208
208
|
```mermaid
|
|
209
209
|
flowchart TD
|
|
@@ -216,7 +216,8 @@ flowchart TD
|
|
|
216
216
|
C --> F{"sessionDigest enabled+ enough messages+ cooldown passed?"}
|
|
217
217
|
F -->|"Yes"| G["LLM triage (~100 tokens):worth distilling?"]
|
|
218
218
|
F -->|"No"| H["Done - thread saved"]
|
|
219
|
-
G -->|"Decisions, insights,preferences found"| I["
|
|
219
|
+
G -->|"Decisions, insights,preferences found"| I["Schedule background distillation by thread ID"]
|
|
220
|
+
I --> J["Backend extracts structured memories(with sourceThreadId)"]
|
|
220
221
|
G -->|"Routine chat"| H
|
|
221
222
|
|
|
222
223
|
D --> H
|
|
@@ -226,21 +227,21 @@ flowchart TD
|
|
|
226
227
|
**Key points:**
|
|
227
228
|
- Thread capture is unconditional: every conversation is saved and searchable via `nowledge_mem_thread_search`
|
|
228
229
|
- Thread sync is incremental: the plugin preserves the real transcript, appends only the unsynced tail, and requires Mem's checkpointed acknowledgement before the local cursor advances
|
|
229
|
-
-
|
|
230
|
+
- Distillation is scheduled from `agent_end` or Context Engine `afterTurn`, not during compaction/reset checkpoints
|
|
230
231
|
- Distilled memories carry `sourceThreadId`, linking them back to the source conversation
|
|
231
232
|
- Cooldown (`digestMinInterval`, default 300s) prevents burst distillation
|
|
232
233
|
|
|
233
|
-
###
|
|
234
|
+
### Targeted Retrieval (Memory -> Thread -> Messages)
|
|
234
235
|
|
|
235
236
|
Memories distilled from conversations carry a `sourceThreadId`. This creates a retrieval chain:
|
|
236
237
|
|
|
237
238
|
```mermaid
|
|
238
239
|
flowchart TD
|
|
239
240
|
A["memory_search'PostgreSQL decision'"] --> B["Result includessourceThreadId"]
|
|
240
|
-
B --> C["
|
|
241
|
-
C --> D["
|
|
242
|
-
D --> E["
|
|
243
|
-
E --> F["
|
|
241
|
+
B --> C["Choose one known target message range"]
|
|
242
|
+
C --> D["nowledge_mem_thread_fetch(offset=target, limit=8) once"]
|
|
243
|
+
D --> E["Answer from evidence or report the gap"]
|
|
244
|
+
E --> F["Refine search if specific evidence is still missing"]
|
|
244
245
|
```
|
|
245
246
|
|
|
246
247
|
Direct conversation search also works:
|
|
@@ -365,15 +366,15 @@ last_n_days: 7
|
|
|
365
366
|
|
|
366
367
|
**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.
|
|
367
368
|
|
|
368
|
-
**nowledge_mem_thread_fetch** -
|
|
369
|
+
**nowledge_mem_thread_fetch** - Read one small targeted message range. Pass a `sourceThreadId` from memory results or a `threadId` from thread search. Supports `offset` and `limit` (default 8); a known nonzero offset is valid once, not a reason to walk pages.
|
|
369
370
|
|
|
370
371
|
```
|
|
371
372
|
threadId: "openclaw-db-arch-a1b2c3"
|
|
372
|
-
offset: 0, limit:
|
|
373
|
+
offset: 0, limit: 8
|
|
373
374
|
-> Thread: "Database architecture discussion" (128 messages)
|
|
374
375
|
[user] We need to decide on the database for task events...
|
|
375
376
|
[assistant] Based on the requirements, PostgreSQL with JSONB...
|
|
376
|
-
... (126
|
|
377
|
+
... (126 messages not shown; refine search for the specific evidence needed)
|
|
377
378
|
```
|
|
378
379
|
|
|
379
380
|
## Operating Modes
|
|
@@ -644,3 +645,9 @@ Run `nmem config client show` and confirm the effective URL and API key state. T
|
|
|
644
645
|
## License
|
|
645
646
|
|
|
646
647
|
MIT
|
|
648
|
+
|
|
649
|
+
### Bounded thread reads
|
|
650
|
+
|
|
651
|
+
Search memories or threads first, then choose the specific conversation and message range needed. Call `thread_fetch_messages`, a host thread-fetch tool, or `nmem --json t show` at most once per question, using a small message limit (for example 8) and a content limit of 1200 where supported. A known target range may use a nonzero offset once.
|
|
652
|
+
|
|
653
|
+
Never loop over offsets to reconstruct a conversation. Do not raise `--content-limit` to compensate for a slow or incomplete read. Small output limits do not guarantee cheap server-side work on every deployed backend. If a read is slow, incomplete, fails or times out, stop, report the evidence gap, and refine the search rather than retrying pages. A later explicit user request for another range is a new bounded read; a routine “continue” is not permission to drain the thread.
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "openclaw-nowledge-mem",
|
|
3
3
|
"name": "Nowledge Mem",
|
|
4
4
|
"description": "Cross-tool knowledge graph memory for OpenClaw with startup context, graph search, and session distillation.",
|
|
5
|
-
"version": "0.8.
|
|
5
|
+
"version": "0.8.39",
|
|
6
6
|
"kind": ["memory", "context-engine"],
|
|
7
7
|
"contracts": {
|
|
8
8
|
"tools": [
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nowledge/openclaw-nowledge-mem",
|
|
3
3
|
"displayName": "Nowledge Mem for OpenClaw",
|
|
4
|
-
"version": "0.8.
|
|
4
|
+
"version": "0.8.39",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Nowledge Mem memory plugin for OpenClaw, local-first personal knowledge base",
|
|
7
7
|
"author": {
|
|
@@ -22,7 +22,7 @@ Use natural language queries. This is semantic search, not file search.
|
|
|
22
22
|
|
|
23
23
|
Results include `matchedVia` (scoring breakdown), `importance`, `labels`, `sourceThreadId`.
|
|
24
24
|
|
|
25
|
-
When results include `relatedThreads`, use `nowledge_mem_thread_fetch` with the `threadId` for
|
|
25
|
+
When results include `relatedThreads`, use `nowledge_mem_thread_fetch` with the `threadId` for one small targeted message range. A known offset may select that range; do not advance through pages. Follow the bounded thread-read policy below.
|
|
26
26
|
|
|
27
27
|
Bi-temporal filters narrow results by time:
|
|
28
28
|
|
|
@@ -52,7 +52,7 @@ Browse recent activity grouped by day. Filter by `event_type` or exact `date_fro
|
|
|
52
52
|
|
|
53
53
|
## Threads (nowledge_mem_thread_search + nowledge_mem_thread_fetch)
|
|
54
54
|
|
|
55
|
-
Find past conversations by keyword, then
|
|
55
|
+
Find past conversations by keyword, then once for the specific messages needed fetch messages. Threads span all sources: this tool, other AI tools, browser capture, imports.
|
|
56
56
|
|
|
57
57
|
## Startup Context (nowledge_mem_context)
|
|
58
58
|
|
|
@@ -63,3 +63,9 @@ Read Context Bundle when identity, active space, active rules, or current priori
|
|
|
63
63
|
- Search uses natural language queries, not file paths. Never search for "MEMORY.md" or "memory/*.md".
|
|
64
64
|
- When `relatedThreads` appear in results, they often contain the most useful context.
|
|
65
65
|
- Use tools directly; do not tell the user to "check your memory."
|
|
66
|
+
|
|
67
|
+
### Bounded thread reads
|
|
68
|
+
|
|
69
|
+
Search memories or threads first, then choose the specific conversation and message range needed. Call `thread_fetch_messages`, a host thread-fetch tool, or `nmem --json t show` at most once per question, using a small message limit (for example 8) and a content limit of 1200 where supported. A known target range may use a nonzero offset once.
|
|
70
|
+
|
|
71
|
+
Never loop over offsets to reconstruct a conversation. Do not raise `--content-limit` to compensate for a slow or incomplete read. Small output limits do not guarantee cheap server-side work on every deployed backend. If a read is slow, incomplete, fails or times out, stop, report the evidence gap, and refine the search rather than retrying pages. A later explicit user request for another range is a new bounded read; a routine “continue” is not permission to drain the thread.
|
package/src/client.js
CHANGED
|
@@ -849,25 +849,36 @@ export class NowledgeMemClient {
|
|
|
849
849
|
}
|
|
850
850
|
|
|
851
851
|
/**
|
|
852
|
-
*
|
|
853
|
-
*
|
|
854
|
-
*
|
|
852
|
+
* Schedule background distillation of an already stored conversation thread.
|
|
853
|
+
* Mem 0.10.7+ no longer accepts inline content for distillation.
|
|
854
|
+
* The acknowledgement reports task acceptance, not completed memories.
|
|
855
855
|
*
|
|
856
|
-
* @param {{ threadId: string, title?: string
|
|
856
|
+
* @param {{ threadId: string, title?: string }} params
|
|
857
857
|
*/
|
|
858
|
-
async distillThread({ threadId, title
|
|
859
|
-
|
|
858
|
+
async distillThread({ threadId, title }) {
|
|
859
|
+
const normalizedThreadId = String(threadId || "").trim();
|
|
860
|
+
if (!normalizedThreadId) throw new Error("distillThread requires threadId");
|
|
861
|
+
const result = await this.apiJson(
|
|
860
862
|
"POST",
|
|
861
|
-
"/memories/distill",
|
|
863
|
+
"/memories/distill/schedule",
|
|
862
864
|
{
|
|
863
|
-
thread_id:
|
|
865
|
+
thread_id: normalizedThreadId,
|
|
864
866
|
thread_title: title || "",
|
|
865
|
-
thread_content: String(content || ""),
|
|
866
867
|
distillation_type: "simple_llm",
|
|
867
868
|
extraction_level: "swift",
|
|
868
869
|
},
|
|
869
|
-
60_000, // 60s timeout for LLM distillation
|
|
870
870
|
);
|
|
871
|
+
if (
|
|
872
|
+
result?.success !== true ||
|
|
873
|
+
typeof result.task_id !== "string" ||
|
|
874
|
+
!result.task_id.trim()
|
|
875
|
+
) {
|
|
876
|
+
throw new Error("Distillation scheduling was not acknowledged with a task ID");
|
|
877
|
+
}
|
|
878
|
+
if (result.enqueued === false) {
|
|
879
|
+
throw new Error("Distillation background scheduler is unavailable");
|
|
880
|
+
}
|
|
881
|
+
return result;
|
|
871
882
|
}
|
|
872
883
|
|
|
873
884
|
// ── Thread search ────────────────────────────────────────────────────────
|
|
@@ -968,8 +979,8 @@ export class NowledgeMemClient {
|
|
|
968
979
|
}
|
|
969
980
|
|
|
970
981
|
/**
|
|
971
|
-
*
|
|
972
|
-
*
|
|
982
|
+
* Read one small targeted message range from a specific thread.
|
|
983
|
+
* A known target offset is supported; do not walk offsets to reconstruct a conversation.
|
|
973
984
|
*
|
|
974
985
|
* Uses CLI: `nmem --json t show <id> -n N --offset O`
|
|
975
986
|
* Falls back to API: GET /threads/{id}
|
|
@@ -977,13 +988,13 @@ export class NowledgeMemClient {
|
|
|
977
988
|
* @param {string} threadId Thread ID
|
|
978
989
|
* @param {{ offset?: number; limit?: number }} [options]
|
|
979
990
|
*/
|
|
980
|
-
async fetchThread(threadId, { offset = 0, limit =
|
|
991
|
+
async fetchThread(threadId, { offset = 0, limit = 8 } = {}) {
|
|
981
992
|
const id = String(threadId || "").trim();
|
|
982
993
|
if (!id) throw new Error("fetchThread requires threadId");
|
|
983
994
|
|
|
984
995
|
const normalizedLimit = Math.min(
|
|
985
996
|
200,
|
|
986
|
-
Math.max(1, Math.trunc(Number(limit) ||
|
|
997
|
+
Math.max(1, Math.trunc(Number(limit) || 8)),
|
|
987
998
|
);
|
|
988
999
|
const normalizedOffset = Math.max(0, Math.trunc(Number(offset) || 0));
|
|
989
1000
|
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function isThreadAppendAck(data: unknown): boolean;
|
|
2
|
+
export declare function isCheckpointedAppendAck(data: unknown): boolean;
|
|
3
|
+
export declare function appendAcknowledgedRemoteCount(data: unknown): number | undefined;
|
|
4
|
+
export declare function isThreadCreateAck(data: unknown, expectedThreadId: string): boolean;
|
|
5
|
+
export declare function createAcknowledgedRemoteCount(data: unknown, expectedThreadId: string): number | undefined;
|
|
6
|
+
export declare function isCheckpointConflictResponse(data: unknown): boolean;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Generated from shared/plugin-core. Run npm --prefix shared/plugin-core run build.
|
|
2
|
+
|
|
3
|
+
// src/thread-delta.ts
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
function stableMessageFingerprint(message) {
|
|
6
|
+
return JSON.stringify(message);
|
|
7
|
+
}
|
|
8
|
+
function prefixFingerprint(messages, end, messageFingerprint) {
|
|
9
|
+
const hash = createHash("sha256");
|
|
10
|
+
for (const message of messages.slice(0, end)) {
|
|
11
|
+
const value = messageFingerprint(message);
|
|
12
|
+
hash.update(String(Buffer.byteLength(value)));
|
|
13
|
+
hash.update(":");
|
|
14
|
+
hash.update(value);
|
|
15
|
+
}
|
|
16
|
+
return hash.digest("hex");
|
|
17
|
+
}
|
|
18
|
+
function selectAcknowledgedDelta(messages, cursor, externalId, messageFingerprint = stableMessageFingerprint) {
|
|
19
|
+
let start = cursor?.count ?? 0;
|
|
20
|
+
const validCount = Number.isSafeInteger(start) && start >= 0 && start <= messages.length;
|
|
21
|
+
const prefixMatches = validCount && (start === 0 || externalId(messages[start - 1]) === cursor?.lastExternalId && prefixFingerprint(messages, start, messageFingerprint) === cursor?.prefixFingerprint);
|
|
22
|
+
const reset = !prefixMatches;
|
|
23
|
+
if (reset) start = 0;
|
|
24
|
+
const end = messages.length;
|
|
25
|
+
return {
|
|
26
|
+
start,
|
|
27
|
+
end,
|
|
28
|
+
messages: messages.slice(start),
|
|
29
|
+
next: {
|
|
30
|
+
count: end,
|
|
31
|
+
remoteCount: cursor?.remoteCount ?? end,
|
|
32
|
+
...end > 0 ? { lastExternalId: externalId(messages[end - 1]) } : {},
|
|
33
|
+
prefixFingerprint: prefixFingerprint(messages, end, messageFingerprint)
|
|
34
|
+
},
|
|
35
|
+
reset
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// src/acknowledgement.ts
|
|
40
|
+
function record(value) {
|
|
41
|
+
return typeof value === "object" && value !== null;
|
|
42
|
+
}
|
|
43
|
+
function messageCount(value) {
|
|
44
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
45
|
+
}
|
|
46
|
+
function isThreadAppendAck(data) {
|
|
47
|
+
return record(data) && data.success === true && messageCount(data.messages_added) && messageCount(data.total_messages);
|
|
48
|
+
}
|
|
49
|
+
function isCheckpointedAppendAck(data) {
|
|
50
|
+
return isThreadAppendAck(data) && data.append_mode === "checkpointed";
|
|
51
|
+
}
|
|
52
|
+
function appendAcknowledgedRemoteCount(data) {
|
|
53
|
+
return isThreadAppendAck(data) ? data.total_messages : void 0;
|
|
54
|
+
}
|
|
55
|
+
function isThreadCreateAck(data, expectedThreadId) {
|
|
56
|
+
return record(data) && record(data.thread) && data.thread.thread_id === expectedThreadId;
|
|
57
|
+
}
|
|
58
|
+
function createAcknowledgedRemoteCount(data, expectedThreadId) {
|
|
59
|
+
if (!isThreadCreateAck(data, expectedThreadId)) return void 0;
|
|
60
|
+
const result = data;
|
|
61
|
+
if (result.thread.message_count !== void 0) {
|
|
62
|
+
return messageCount(result.thread.message_count) ? result.thread.message_count : void 0;
|
|
63
|
+
}
|
|
64
|
+
return Array.isArray(result.messages) ? result.messages.length : void 0;
|
|
65
|
+
}
|
|
66
|
+
function isCheckpointConflictResponse(data) {
|
|
67
|
+
return record(data) && data.error_code === "checkpoint_conflict";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/timeout.ts
|
|
71
|
+
var DEFAULT_THREAD_SYNC_TIMEOUT_MS = 12e4;
|
|
72
|
+
var MIN_THREAD_SYNC_TIMEOUT_MS = 1e3;
|
|
73
|
+
var MAX_THREAD_SYNC_TIMEOUT_MS = 30 * 6e4;
|
|
74
|
+
function resolveThreadSyncTimeoutMs(raw) {
|
|
75
|
+
const parsed = Number(raw);
|
|
76
|
+
if (!raw?.trim() || !Number.isSafeInteger(parsed) || parsed <= 0) {
|
|
77
|
+
return DEFAULT_THREAD_SYNC_TIMEOUT_MS;
|
|
78
|
+
}
|
|
79
|
+
return Math.min(MAX_THREAD_SYNC_TIMEOUT_MS, Math.max(MIN_THREAD_SYNC_TIMEOUT_MS, parsed));
|
|
80
|
+
}
|
|
81
|
+
export {
|
|
82
|
+
appendAcknowledgedRemoteCount,
|
|
83
|
+
createAcknowledgedRemoteCount,
|
|
84
|
+
isCheckpointConflictResponse,
|
|
85
|
+
isCheckpointedAppendAck,
|
|
86
|
+
isThreadAppendAck,
|
|
87
|
+
isThreadCreateAck,
|
|
88
|
+
prefixFingerprint,
|
|
89
|
+
resolveThreadSyncTimeoutMs,
|
|
90
|
+
selectAcknowledgedDelta,
|
|
91
|
+
stableMessageFingerprint
|
|
92
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"core": {
|
|
4
|
+
"name": "@nowledge/plugin-core",
|
|
5
|
+
"version": "0.2.0",
|
|
6
|
+
"sourceDigest": "f06362cae4cdb5cd24a1eab4420860d41ab707f1558a25b779fcb3d74c651c56"
|
|
7
|
+
},
|
|
8
|
+
"build": {
|
|
9
|
+
"target": "es2022",
|
|
10
|
+
"format": "esm",
|
|
11
|
+
"esbuild": "0.25.9",
|
|
12
|
+
"typescript": "5.6.3"
|
|
13
|
+
},
|
|
14
|
+
"files": {
|
|
15
|
+
"acknowledgement.d.ts": "ffd0fd090f289c4deafba41d9faa83fb7f4a6070bd53588de5d1a947b076f65a",
|
|
16
|
+
"index.d.ts": "6e7965f903a043a34c3dd454f909e878134c1bf451ec7ff3c3026f035f1aaee0",
|
|
17
|
+
"index.js": "0e25a05abe9f9282dc6a7d3cc64a3da872c1e5192908308ee5086c0465f6c032",
|
|
18
|
+
"package.json": "609158e6c5fbc237939fa3ddf7faab80ab690bdc0c8d584414a885130103c4e8",
|
|
19
|
+
"thread-delta.d.ts": "83f31e3334086f6fdfe5e6e1b1c4b1a994bb6d49fda2fdedf450d14f4ec18cba",
|
|
20
|
+
"timeout.d.ts": "85bfd4185e9d0b22073250b5cbe19da38613d9af9fbc2022403ebb527dad9f8f"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type AcknowledgedCursor = {
|
|
2
|
+
count: number;
|
|
3
|
+
remoteCount: number;
|
|
4
|
+
lastExternalId?: string;
|
|
5
|
+
prefixFingerprint: string;
|
|
6
|
+
};
|
|
7
|
+
export type AcknowledgedDelta<T> = {
|
|
8
|
+
start: number;
|
|
9
|
+
end: number;
|
|
10
|
+
messages: T[];
|
|
11
|
+
next: AcknowledgedCursor;
|
|
12
|
+
reset: boolean;
|
|
13
|
+
};
|
|
14
|
+
export declare function stableMessageFingerprint(message: unknown): string;
|
|
15
|
+
export declare function prefixFingerprint<T>(messages: readonly T[], end: number, messageFingerprint: (message: T) => string): string;
|
|
16
|
+
/** The returned cursor is a candidate; only persistence acknowledgement commits it. */
|
|
17
|
+
export declare function selectAcknowledgedDelta<T>(messages: readonly T[], cursor: AcknowledgedCursor | undefined, externalId: (message: T) => string, messageFingerprint?: (message: T) => string): AcknowledgedDelta<T>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function resolveThreadSyncTimeoutMs(raw: string | undefined): number;
|
package/src/hooks/behavioral.js
CHANGED
|
@@ -17,11 +17,13 @@
|
|
|
17
17
|
|
|
18
18
|
import { ceState } from "../ce-state.js";
|
|
19
19
|
|
|
20
|
+
const THREAD_READ_GUIDANCE = "When results include a sourceThreadId, use nowledge_mem_thread_fetch for at most one small targeted message range per question (for example 8 messages). Never loop over offsets or increase content limits. If the read is slow, incomplete or fails, stop, report the evidence gap and refine search.";
|
|
21
|
+
|
|
20
22
|
export const BASE_GUIDANCE = [
|
|
21
23
|
"<nowledge-mem-guidance>",
|
|
22
24
|
"You have access to the user's personal knowledge graph (Nowledge Mem).",
|
|
23
25
|
"Search proactively with memory_search when past context would improve your answer. Do not wait to be asked. Use natural language queries, not file paths.",
|
|
24
|
-
|
|
26
|
+
THREAD_READ_GUIDANCE,
|
|
25
27
|
"Save autonomously with nowledge_mem_save when the conversation produces a decision, preference, plan, procedure, or learning. Do not wait to be asked.",
|
|
26
28
|
"</nowledge-mem-guidance>",
|
|
27
29
|
].join("\n");
|
|
@@ -31,7 +33,7 @@ export const SESSION_CONTEXT_GUIDANCE = [
|
|
|
31
33
|
"You have access to the user's personal knowledge graph (Nowledge Mem).",
|
|
32
34
|
"Relevant memories and your Working Memory have already been injected into this prompt.",
|
|
33
35
|
"Use memory_search for specifics beyond what was auto-recalled, or when the user references prior work not covered above. Natural language queries, not file paths.",
|
|
34
|
-
|
|
36
|
+
THREAD_READ_GUIDANCE,
|
|
35
37
|
"Save autonomously with nowledge_mem_save when the conversation produces a decision, preference, plan, procedure, or learning. Do not wait to be asked.",
|
|
36
38
|
"</nowledge-mem-guidance>",
|
|
37
39
|
].join("\n");
|
package/src/hooks/capture.js
CHANGED
|
@@ -742,7 +742,7 @@ async function persistMessages({
|
|
|
742
742
|
|
|
743
743
|
/**
|
|
744
744
|
* Build a plain-text conversation string from normalized messages.
|
|
745
|
-
* Used as input for triage
|
|
745
|
+
* Used as input for triage; distillation reads the stored thread.
|
|
746
746
|
*
|
|
747
747
|
* Per-message content is capped at MAX_DISTILL_MESSAGE_CHARS and the
|
|
748
748
|
* total output at MAX_CONVERSATION_CHARS to keep LLM API payloads
|
|
@@ -823,15 +823,10 @@ export async function triageAndDistill({
|
|
|
823
823
|
const distillResult = await client.distillThread({
|
|
824
824
|
threadId: captureResult.threadId,
|
|
825
825
|
title: buildThreadTitle(ctx, captureResult.normalized),
|
|
826
|
-
content: conversationText,
|
|
827
826
|
});
|
|
828
827
|
|
|
829
|
-
const count =
|
|
830
|
-
distillResult?.memories_created ??
|
|
831
|
-
distillResult?.created_memories?.length ??
|
|
832
|
-
0;
|
|
833
828
|
logger.info(
|
|
834
|
-
`capture:
|
|
829
|
+
`capture: scheduled background distillation for ${captureResult.threadId} (task ${distillResult.task_id})`,
|
|
835
830
|
);
|
|
836
831
|
} catch (err) {
|
|
837
832
|
const message = err instanceof Error ? err.message : String(err);
|
package/src/session-delta.js
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
createAcknowledgedRemoteCount,
|
|
4
|
+
prefixFingerprint as corePrefixFingerprint,
|
|
5
|
+
} from "./generated/plugin-core/index.js";
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
isCheckpointConflictResponse,
|
|
9
|
+
isCheckpointedAppendAck,
|
|
10
|
+
isThreadAppendAck,
|
|
11
|
+
} from "./generated/plugin-core/index.js";
|
|
2
12
|
|
|
3
13
|
export function sessionSyncLaneKey(threadId, apiUrl, apiKey, spaceId) {
|
|
4
14
|
const destination = createHash("sha256")
|
|
@@ -24,54 +34,27 @@ export function prefixFingerprint(
|
|
|
24
34
|
end,
|
|
25
35
|
messageFingerprint = stableMessageFingerprint,
|
|
26
36
|
) {
|
|
27
|
-
|
|
28
|
-
for (const message of messages.slice(0, end)) {
|
|
29
|
-
const value = messageFingerprint(message);
|
|
30
|
-
hash.update(String(Buffer.byteLength(value)));
|
|
31
|
-
hash.update(":");
|
|
32
|
-
hash.update(value);
|
|
33
|
-
}
|
|
34
|
-
return hash.digest("hex");
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function isThreadAppendAck(data) {
|
|
38
|
-
return (
|
|
39
|
-
data !== null &&
|
|
40
|
-
typeof data === "object" &&
|
|
41
|
-
data.success === true &&
|
|
42
|
-
Number.isInteger(data.messages_added) &&
|
|
43
|
-
data.messages_added >= 0 &&
|
|
44
|
-
Number.isInteger(data.total_messages) &&
|
|
45
|
-
data.total_messages >= 0
|
|
46
|
-
);
|
|
37
|
+
return corePrefixFingerprint(messages, end, messageFingerprint);
|
|
47
38
|
}
|
|
48
39
|
|
|
49
40
|
export function threadCreateAck(data, expectedThreadId) {
|
|
50
41
|
if (data === null || typeof data !== "object") return null;
|
|
51
42
|
const thread =
|
|
52
|
-
data.thread !== null && typeof data.thread === "object"
|
|
43
|
+
data.thread !== null && typeof data.thread === "object"
|
|
44
|
+
? data.thread
|
|
45
|
+
: data;
|
|
53
46
|
const threadId = String(thread.thread_id ?? "").trim();
|
|
54
|
-
const totalMessages =
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return { threadId, totalMessages };
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function isCheckpointedAppendAck(data) {
|
|
66
|
-
return isThreadAppendAck(data) && data.append_mode === "checkpointed";
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function isCheckpointConflictResponse(data) {
|
|
70
|
-
return (
|
|
71
|
-
data !== null &&
|
|
72
|
-
typeof data === "object" &&
|
|
73
|
-
data.error_code === "checkpoint_conflict"
|
|
47
|
+
const totalMessages = createAcknowledgedRemoteCount(
|
|
48
|
+
{
|
|
49
|
+
thread: {
|
|
50
|
+
thread_id: threadId,
|
|
51
|
+
message_count: thread.message_count ?? thread.total_messages,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
String(expectedThreadId || "").trim(),
|
|
74
55
|
);
|
|
56
|
+
if (totalMessages === undefined) return null;
|
|
57
|
+
return { threadId, totalMessages };
|
|
75
58
|
}
|
|
76
59
|
|
|
77
60
|
export function selectSnapshotDelta(
|
|
@@ -102,9 +85,7 @@ export function selectSnapshotDelta(
|
|
|
102
85
|
};
|
|
103
86
|
}
|
|
104
87
|
const acknowledgedPrefix =
|
|
105
|
-
start >= 0
|
|
106
|
-
? prefixFingerprint(messages, start, messageFingerprint)
|
|
107
|
-
: "";
|
|
88
|
+
start >= 0 ? prefixFingerprint(messages, start, messageFingerprint) : "";
|
|
108
89
|
const fingerprintTrusted = Boolean(cursor?.prefixFingerprint);
|
|
109
90
|
const externalIdTrusted = Boolean(cursor?.lastExternalId);
|
|
110
91
|
if (
|
|
@@ -134,7 +115,13 @@ export function selectSnapshotDelta(
|
|
|
134
115
|
};
|
|
135
116
|
}
|
|
136
117
|
|
|
137
|
-
export function contentBoundIdempotencyKey(
|
|
118
|
+
export function contentBoundIdempotencyKey(
|
|
119
|
+
prefix,
|
|
120
|
+
threadId,
|
|
121
|
+
start,
|
|
122
|
+
end,
|
|
123
|
+
fingerprint,
|
|
124
|
+
) {
|
|
138
125
|
return `${prefix}:${threadId}:${start}-${end}:${fingerprint}`;
|
|
139
126
|
}
|
|
140
127
|
|
|
@@ -1,14 +1 @@
|
|
|
1
|
-
|
|
2
|
-
const MIN_THREAD_SYNC_TIMEOUT_MS = 1_000;
|
|
3
|
-
const MAX_THREAD_SYNC_TIMEOUT_MS = 30 * 60_000;
|
|
4
|
-
|
|
5
|
-
export function resolveThreadSyncTimeoutMs(raw) {
|
|
6
|
-
const parsed = Number(raw);
|
|
7
|
-
if (!String(raw ?? "").trim() || !Number.isSafeInteger(parsed) || parsed <= 0) {
|
|
8
|
-
return DEFAULT_THREAD_SYNC_TIMEOUT_MS;
|
|
9
|
-
}
|
|
10
|
-
return Math.min(
|
|
11
|
-
MAX_THREAD_SYNC_TIMEOUT_MS,
|
|
12
|
-
Math.max(MIN_THREAD_SYNC_TIMEOUT_MS, parsed),
|
|
13
|
-
);
|
|
14
|
-
}
|
|
1
|
+
export { resolveThreadSyncTimeoutMs } from "./generated/plugin-core/index.js";
|
|
@@ -20,6 +20,7 @@ export function createMemorySearchTool(client, logger) {
|
|
|
20
20
|
"Uses multi-signal scoring: semantic, BM25 keyword, label, graph & community signals, recency/importance decay. " +
|
|
21
21
|
"Returns snippets with memoryIds — pass a memoryId to nowledge_mem_connections for cross-topic synthesis or source provenance. " +
|
|
22
22
|
"Also returns relevant past conversation snippets (relatedThreads) when available. " +
|
|
23
|
+
"Use a matched messageIndex, when present, to choose one small targeted thread range; never walk offsets or invent missing positions. " +
|
|
23
24
|
"Supports bi-temporal filtering: event_date_from/to (when the fact HAPPENED), recorded_date_from/to (when it was SAVED). " +
|
|
24
25
|
"For browsing recent activity by day use nowledge_mem_timeline instead.",
|
|
25
26
|
parameters: {
|
|
@@ -163,6 +164,8 @@ export function createMemorySearchTool(client, logger) {
|
|
|
163
164
|
matchedMessages: msgs.map((m) => ({
|
|
164
165
|
role: m.role,
|
|
165
166
|
snippet: truncateSnippet(m.snippet, 200),
|
|
167
|
+
...(Number.isInteger(m.message_index) && m.message_index >= 0
|
|
168
|
+
? { messageIndex: m.message_index } : {}),
|
|
166
169
|
})),
|
|
167
170
|
};
|
|
168
171
|
});
|
|
@@ -2,11 +2,11 @@ export function createThreadFetchTool(client, logger) {
|
|
|
2
2
|
return {
|
|
3
3
|
name: "nowledge_mem_thread_fetch",
|
|
4
4
|
description:
|
|
5
|
-
"Fetch
|
|
5
|
+
"Fetch a targeted range of messages from a conversation thread. Use to read context " +
|
|
6
6
|
"around a memory — pass the sourceThreadId from memory_search or memory_get results, " +
|
|
7
7
|
"or a threadId from nowledge_mem_thread_search. " +
|
|
8
|
-
"
|
|
9
|
-
"
|
|
8
|
+
"Use a known target offset to skip earlier messages once. " +
|
|
9
|
+
"Search first, then fetch at most one small targeted message range per question. Never loop over offsets to reconstruct a conversation or increase content limits. If the read is slow, incomplete or fails, stop, report the evidence gap and refine search.",
|
|
10
10
|
parameters: {
|
|
11
11
|
type: "object",
|
|
12
12
|
properties: {
|
|
@@ -17,11 +17,11 @@ export function createThreadFetchTool(client, logger) {
|
|
|
17
17
|
},
|
|
18
18
|
offset: {
|
|
19
19
|
type: "number",
|
|
20
|
-
description: "
|
|
20
|
+
description: "Known target message offset (default 0); never walk successive pages",
|
|
21
21
|
},
|
|
22
22
|
limit: {
|
|
23
23
|
type: "number",
|
|
24
|
-
description: "Max messages to return (1-200, default
|
|
24
|
+
description: "Max messages to return (1-200, default 8)",
|
|
25
25
|
},
|
|
26
26
|
},
|
|
27
27
|
required: ["threadId"],
|
|
@@ -35,7 +35,7 @@ export function createThreadFetchTool(client, logger) {
|
|
|
35
35
|
);
|
|
36
36
|
const limit = Math.min(
|
|
37
37
|
200,
|
|
38
|
-
Math.max(1, Math.trunc(Number(safeParams.limit ??
|
|
38
|
+
Math.max(1, Math.trunc(Number(safeParams.limit ?? 8) || 8)),
|
|
39
39
|
);
|
|
40
40
|
|
|
41
41
|
if (!threadId) {
|
|
@@ -10,10 +10,12 @@ export function createThreadSearchTool(client, logger) {
|
|
|
10
10
|
name: "nowledge_mem_thread_search",
|
|
11
11
|
description:
|
|
12
12
|
"Search past conversations by keyword. Use when the user asks about a past discussion, " +
|
|
13
|
-
"wants to find a conversation from a specific time, or when you want to
|
|
13
|
+
"wants to find a conversation from a specific time, or when you want to find relevant source messages " +
|
|
14
14
|
"around a topic beyond what memory_search returned. " +
|
|
15
15
|
"Returns matched threads with message snippets and relevance scores. " +
|
|
16
|
-
"
|
|
16
|
+
"A match's messageIndex, when present, identifies a target offset for a nearby range; do not invent positions when absent. " +
|
|
17
|
+
"Pass its threadId to nowledge_mem_thread_fetch for at most one small targeted message range per question. " +
|
|
18
|
+
"Never loop over offsets; on slow, incomplete or failed reads, report the evidence gap and refine search.",
|
|
17
19
|
parameters: {
|
|
18
20
|
type: "object",
|
|
19
21
|
properties: {
|
|
@@ -74,6 +76,8 @@ export function createThreadSearchTool(client, logger) {
|
|
|
74
76
|
matchedMessages: msgs.map((m) => ({
|
|
75
77
|
role: m.role,
|
|
76
78
|
snippet: truncateSnippet(m.snippet),
|
|
79
|
+
...(Number.isInteger(m.message_index) && m.message_index >= 0
|
|
80
|
+
? { messageIndex: m.message_index } : {}),
|
|
77
81
|
})),
|
|
78
82
|
};
|
|
79
83
|
});
|