@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 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 - fetch full messages from a thread with pagination
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 (progressive conversation retrieval)
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` - fetch messages from a specific thread. Start with a small page, then use `offset` + `limit` for progressive retrieval only when more context is needed.
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()` | Fetch thread messages with pagination |
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` | Fetch full source conversation. Pagination via `offset` + `limit`. |
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
- When sessions end, conversations are captured and optionally distilled. No user action needed.
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["Full distillation ->structured memories(with sourceThreadId)"]
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
- - LLM distillation only runs at `agent_end`, not during compaction/reset checkpoints
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
- ### Progressive Retrieval (Memory -> Thread -> Messages)
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["nowledge_mem_thread_fetch(offset=0, limit=50)"]
241
- C --> D["50 messageshasMore: true"]
242
- D --> E["nowledge_mem_thread_fetch(offset=50, limit=50)"]
243
- E --> F["Next page"]
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** - 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.
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: 50
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 more messages, use offset=50 for next page)
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.
@@ -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.35",
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.35",
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 full conversation context. Start with a small page; fetch more via `offset` + `limit` only when needed.
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 progressively fetch messages. Threads span all sources: this tool, other AI tools, browser capture, imports.
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
- * Distill memories from a conversation thread.
853
- * Calls the full distillation endpoint which uses LLM to extract
854
- * structured memories (with unit_type, temporal data, labels).
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, content: string }} params
856
+ * @param {{ threadId: string, title?: string }} params
857
857
  */
858
- async distillThread({ threadId, title, content }) {
859
- return this.apiJson(
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: String(threadId || ""),
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
- * Fetch full messages from a specific thread.
972
- * Supports pagination for progressive retrieval of long conversations.
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 = 50 } = {}) {
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) || 50)),
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,3 @@
1
+ export * from "./thread-delta.js";
2
+ export * from "./acknowledgement.js";
3
+ export * from "./timeout.js";
@@ -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,4 @@
1
+ {
2
+ "private": true,
3
+ "type": "module"
4
+ }
@@ -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;
@@ -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
- "When results include a sourceThreadId, use nowledge_mem_thread_fetch for full conversation context.",
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
- "When results include a sourceThreadId, use nowledge_mem_thread_fetch for full conversation context.",
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");
@@ -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 and distillation.
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: distilled ${count} memories from ${captureResult.threadId}`,
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);
@@ -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
- const hash = createHash("sha256");
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" ? data.thread : data;
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 = thread.message_count ?? thread.total_messages;
55
- if (
56
- threadId !== String(expectedThreadId || "").trim() ||
57
- !Number.isInteger(totalMessages) ||
58
- totalMessages < 0
59
- ) {
60
- return null;
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(prefix, threadId, start, end, fingerprint) {
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
- const DEFAULT_THREAD_SYNC_TIMEOUT_MS = 120_000;
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 full messages from a conversation thread. Use to read the complete context " +
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
- "Supports pagination for long conversations: set offset to skip earlier messages. " +
9
- "Useful for progressive retrieval — fetch the first page, then request more if needed.",
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: "Skip first N messages (for pagination, default 0)",
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 50)",
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 ?? 50) || 50)),
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 explore the full context " +
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
- "To read full messages from a thread, pass its threadId to nowledge_mem_thread_fetch.",
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
  });