@nowledge/openclaw-nowledge-mem 0.8.36 → 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,26 @@ 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
+
7
27
  ## [0.8.36] - 2026-10-11
8
28
 
9
29
  ### Fixed
package/README.md CHANGED
@@ -203,7 +203,7 @@ The behavioral skill and always-on hook nudge the agent to **search before answe
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,7 +227,7 @@ 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
 
@@ -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.36",
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.36",
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": {
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 ────────────────────────────────────────────────────────
@@ -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;
@@ -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";