@nowledge/openclaw-nowledge-mem 0.8.23 → 0.8.24
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 +8 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/hooks/capture.js +189 -40
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,14 @@ All notable changes to the Nowledge Mem OpenClaw plugin will be documented in th
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## [0.8.24] - 2026-05-15
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- OpenClaw session capture now handles newer per-turn `agent_end` hook payloads from Codex app-server runtimes. The plugin no longer mistakes those shorter batches for transcript compaction, so continued turns keep syncing into the same Mem thread after an OpenClaw upgrade.
|
|
12
|
+
- Captured Codex app-server messages now use OpenClaw's stable mirror identity for Mem-side dedupe, keeping Context Engine `afterTurn` and hook backstop capture idempotent when both paths see the same turn.
|
|
13
|
+
- Capture setup no longer depends on runtime-only OpenClaw SDK imports, keeping the plugin loadable on existing OpenClaw installer builds while still matching the host session-key contract.
|
|
14
|
+
|
|
7
15
|
## [0.8.23] - 2026-05-02
|
|
8
16
|
|
|
9
17
|
### Fixed
|
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 Working Memory, graph search, and session distillation.",
|
|
5
|
-
|
|
5
|
+
"version": "0.8.24",
|
|
6
6
|
"kind": ["memory", "context-engine"],
|
|
7
7
|
"skills": [
|
|
8
8
|
"skills/memory-guide"
|
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.24",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Nowledge Mem memory plugin for OpenClaw, local-first personal knowledge base",
|
|
7
7
|
"author": {
|
package/src/hooks/capture.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
1
|
import { createHash } from "node:crypto";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
buildStableThreadId,
|
|
6
|
-
} from "./thread-identity.js";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { buildStableThreadId } from "./thread-identity.js";
|
|
7
4
|
export {
|
|
8
5
|
_resetConversationRoots,
|
|
9
6
|
buildStableThreadId,
|
|
@@ -29,6 +26,38 @@ const _lastCaptureAt = new Map();
|
|
|
29
26
|
const _MAX_COOLDOWN_ENTRIES = 200;
|
|
30
27
|
const _syncedMessageCounts = new Map();
|
|
31
28
|
const _MAX_SYNC_CURSOR_ENTRIES = 500;
|
|
29
|
+
const CAPTURE_MESSAGE_MODE_AUTO = "auto";
|
|
30
|
+
const CAPTURE_MESSAGE_MODE_SNAPSHOT = "snapshot";
|
|
31
|
+
const CAPTURE_MESSAGE_MODE_DELTA = "delta";
|
|
32
|
+
|
|
33
|
+
function parseAgentSessionKey(sessionKey) {
|
|
34
|
+
const raw = String(sessionKey || "")
|
|
35
|
+
.trim()
|
|
36
|
+
.toLowerCase();
|
|
37
|
+
if (!raw) return null;
|
|
38
|
+
const parts = raw.split(":").filter(Boolean);
|
|
39
|
+
if (parts.length < 3 || parts[0] !== "agent") return null;
|
|
40
|
+
const agentId = parts[1];
|
|
41
|
+
const rest = parts.slice(2).join(":");
|
|
42
|
+
if (!agentId || !rest) return null;
|
|
43
|
+
return { agentId, rest };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isAgentCronSessionKey(sessionKey) {
|
|
47
|
+
const parsed = parseAgentSessionKey(sessionKey);
|
|
48
|
+
return parsed?.rest?.startsWith("cron:") === true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isSubagentSessionKey(sessionKey) {
|
|
52
|
+
const raw = String(sessionKey || "")
|
|
53
|
+
.trim()
|
|
54
|
+
.toLowerCase();
|
|
55
|
+
if (!raw) return false;
|
|
56
|
+
if (raw.startsWith("subagent:")) return true;
|
|
57
|
+
const parsed = parseAgentSessionKey(raw);
|
|
58
|
+
return parsed?.rest?.startsWith("subagent:") === true;
|
|
59
|
+
}
|
|
60
|
+
|
|
32
61
|
function _setLastCapture(threadId, now) {
|
|
33
62
|
_lastCaptureAt.set(threadId, now);
|
|
34
63
|
// Opportunistic eviction: sweep stale entries when map grows large
|
|
@@ -68,11 +97,7 @@ function _compileGlob(pattern) {
|
|
|
68
97
|
let re = _compiledPatterns.get(key);
|
|
69
98
|
if (!re) {
|
|
70
99
|
re = new RegExp(
|
|
71
|
-
"
|
|
72
|
-
key
|
|
73
|
-
.replace(/[.+?^${}()|[\]\\]/g, "\\$&")
|
|
74
|
-
.replace(/\*/g, "[^:]*") +
|
|
75
|
-
"$",
|
|
100
|
+
`^${key.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^:]*")}$`,
|
|
76
101
|
);
|
|
77
102
|
_compiledPatterns.set(key, re);
|
|
78
103
|
}
|
|
@@ -95,12 +120,11 @@ export function matchesExcludePattern(sessionKey, patterns) {
|
|
|
95
120
|
/**
|
|
96
121
|
* Whether automatic thread capture should skip this OpenClaw session.
|
|
97
122
|
*
|
|
98
|
-
* - Agent-scoped cron / isolated-agent keys
|
|
99
|
-
* `
|
|
100
|
-
* the gateway uses). Requires OpenClaw >=2026.3.22.
|
|
123
|
+
* - Agent-scoped cron / isolated-agent keys follow OpenClaw's canonical
|
|
124
|
+
* `agent:<id>:cron:*` session-key shape.
|
|
101
125
|
* - Bare `cron:*` keys are still excluded: some internal paths (e.g. delivery /
|
|
102
126
|
* failure bookkeeping) use that shape without the `agent:` prefix, and
|
|
103
|
-
*
|
|
127
|
+
* agent-scoped parsing intentionally does not classify bare keys.
|
|
104
128
|
*/
|
|
105
129
|
export function isCronCaptureSessionKey(sessionKey) {
|
|
106
130
|
const raw = String(sessionKey || "")
|
|
@@ -108,7 +132,7 @@ export function isCronCaptureSessionKey(sessionKey) {
|
|
|
108
132
|
.toLowerCase();
|
|
109
133
|
if (!raw) return false;
|
|
110
134
|
if (raw.startsWith("cron:")) return true;
|
|
111
|
-
return
|
|
135
|
+
return isAgentCronSessionKey(raw);
|
|
112
136
|
}
|
|
113
137
|
|
|
114
138
|
/**
|
|
@@ -118,7 +142,8 @@ export function isCronCaptureSessionKey(sessionKey) {
|
|
|
118
142
|
* Exported for reuse by Context Engine and other capture paths.
|
|
119
143
|
*/
|
|
120
144
|
export function hasSkipMarker(messages, marker) {
|
|
121
|
-
if (!marker || typeof marker !== "string" || !Array.isArray(messages))
|
|
145
|
+
if (!marker || typeof marker !== "string" || !Array.isArray(messages))
|
|
146
|
+
return false;
|
|
122
147
|
const markerLc = marker.toLowerCase();
|
|
123
148
|
return messages.some((msg) => {
|
|
124
149
|
const text = extractText(msg?.content ?? msg?.message?.content);
|
|
@@ -225,17 +250,45 @@ export function normalizeRoleMessage(
|
|
|
225
250
|
timestamp = msg.timestamp;
|
|
226
251
|
}
|
|
227
252
|
|
|
253
|
+
const openclawMeta =
|
|
254
|
+
msg.__openclaw && typeof msg.__openclaw === "object"
|
|
255
|
+
? msg.__openclaw
|
|
256
|
+
: raw.__openclaw && typeof raw.__openclaw === "object"
|
|
257
|
+
? raw.__openclaw
|
|
258
|
+
: null;
|
|
259
|
+
const msgMetadata =
|
|
260
|
+
msg.metadata && typeof msg.metadata === "object" ? msg.metadata : null;
|
|
261
|
+
const rawMetadata =
|
|
262
|
+
raw.metadata && typeof raw.metadata === "object" ? raw.metadata : null;
|
|
228
263
|
const externalHint = [
|
|
264
|
+
openclawMeta?.mirrorIdentity,
|
|
265
|
+
openclawMeta?.idempotencyKey,
|
|
266
|
+
msg.idempotencyKey,
|
|
267
|
+
msg.idempotency_key,
|
|
229
268
|
msg.external_id,
|
|
230
269
|
msg.externalId,
|
|
231
270
|
msg.message_id,
|
|
232
271
|
msg.messageId,
|
|
272
|
+
msg.openclaw_entry_id,
|
|
273
|
+
msg.openclawEntryId,
|
|
233
274
|
msg.id,
|
|
275
|
+
raw.idempotencyKey,
|
|
276
|
+
raw.idempotency_key,
|
|
234
277
|
raw.external_id,
|
|
235
278
|
raw.externalId,
|
|
236
279
|
raw.message_id,
|
|
237
280
|
raw.messageId,
|
|
281
|
+
raw.openclaw_entry_id,
|
|
282
|
+
raw.openclawEntryId,
|
|
238
283
|
raw.id,
|
|
284
|
+
msgMetadata?.external_id,
|
|
285
|
+
msgMetadata?.externalId,
|
|
286
|
+
msgMetadata?.message_id,
|
|
287
|
+
msgMetadata?.messageId,
|
|
288
|
+
rawMetadata?.external_id,
|
|
289
|
+
rawMetadata?.externalId,
|
|
290
|
+
rawMetadata?.message_id,
|
|
291
|
+
rawMetadata?.messageId,
|
|
239
292
|
]
|
|
240
293
|
.find((v) => typeof v === "string" && v.trim().length > 0)
|
|
241
294
|
?.trim();
|
|
@@ -263,11 +316,26 @@ function sanitizeIdPart(input, max = 48) {
|
|
|
263
316
|
return normalized.slice(0, max);
|
|
264
317
|
}
|
|
265
318
|
|
|
266
|
-
function buildExternalId({
|
|
319
|
+
function buildExternalId({
|
|
320
|
+
normalized,
|
|
321
|
+
index,
|
|
322
|
+
threadId,
|
|
323
|
+
sessionKey,
|
|
324
|
+
event,
|
|
325
|
+
ctx,
|
|
326
|
+
messageMode,
|
|
327
|
+
}) {
|
|
267
328
|
if (normalized.externalHint) {
|
|
268
329
|
return `oc:${sanitizeIdPart(normalized.externalHint, 96)}`;
|
|
269
330
|
}
|
|
270
|
-
const
|
|
331
|
+
const runId =
|
|
332
|
+
messageMode === CAPTURE_MESSAGE_MODE_DELTA
|
|
333
|
+
? String(ctx?.runId || event?.runId || "").trim()
|
|
334
|
+
: "";
|
|
335
|
+
const seedParts = [threadId, sessionKey];
|
|
336
|
+
if (runId) seedParts.push(`run:${runId}`);
|
|
337
|
+
seedParts.push(index, normalized.role, normalized.content);
|
|
338
|
+
const seed = seedParts.join("|");
|
|
271
339
|
const digest = createHash("sha1").update(seed).digest("hex");
|
|
272
340
|
return `oc-msg:${digest}`;
|
|
273
341
|
}
|
|
@@ -286,6 +354,18 @@ function buildAppendIdempotencyKey(threadId, reason, messages) {
|
|
|
286
354
|
return `oc-batch:${createHash("sha1").update(JSON.stringify(seed)).digest("hex")}`;
|
|
287
355
|
}
|
|
288
356
|
|
|
357
|
+
function hasStableExternalHints(normalized) {
|
|
358
|
+
return (
|
|
359
|
+
Array.isArray(normalized) &&
|
|
360
|
+
normalized.length > 0 &&
|
|
361
|
+
normalized.every(
|
|
362
|
+
(message) =>
|
|
363
|
+
typeof message?.externalHint === "string" &&
|
|
364
|
+
message.externalHint.trim().length > 0,
|
|
365
|
+
)
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
289
369
|
async function loadMessagesFromSessionFile(sessionFile) {
|
|
290
370
|
try {
|
|
291
371
|
const content = await readFile(sessionFile, "utf-8");
|
|
@@ -328,6 +408,7 @@ export async function appendOrCreateThread({
|
|
|
328
408
|
reason,
|
|
329
409
|
maxMessageChars = DEFAULT_MAX_MESSAGE_CHARS,
|
|
330
410
|
resolvedMessages,
|
|
411
|
+
messageMode = CAPTURE_MESSAGE_MODE_SNAPSHOT,
|
|
331
412
|
}) {
|
|
332
413
|
const rawMessages = resolvedMessages ?? (await resolveHookMessages(event));
|
|
333
414
|
if (!Array.isArray(rawMessages) || rawMessages.length === 0) return;
|
|
@@ -341,22 +422,32 @@ export async function appendOrCreateThread({
|
|
|
341
422
|
.filter(Boolean);
|
|
342
423
|
if (normalized.length === 0) return;
|
|
343
424
|
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
425
|
+
const buildMessages = (resolvedMessageMode) =>
|
|
426
|
+
normalized.map((message, index) => ({
|
|
427
|
+
role: message.role,
|
|
428
|
+
content: message.content,
|
|
429
|
+
timestamp: message.timestamp,
|
|
430
|
+
metadata: {
|
|
431
|
+
external_id: buildExternalId({
|
|
432
|
+
normalized: message,
|
|
433
|
+
index,
|
|
434
|
+
threadId,
|
|
435
|
+
sessionKey,
|
|
436
|
+
event,
|
|
437
|
+
ctx,
|
|
438
|
+
messageMode: resolvedMessageMode,
|
|
439
|
+
}),
|
|
440
|
+
source: "openclaw",
|
|
441
|
+
session_key: sessionKey,
|
|
442
|
+
session_id: sessionId || undefined,
|
|
443
|
+
},
|
|
444
|
+
}));
|
|
445
|
+
|
|
446
|
+
let allMessages = buildMessages(
|
|
447
|
+
messageMode === CAPTURE_MESSAGE_MODE_DELTA
|
|
448
|
+
? CAPTURE_MESSAGE_MODE_DELTA
|
|
449
|
+
: CAPTURE_MESSAGE_MODE_SNAPSHOT,
|
|
450
|
+
);
|
|
360
451
|
|
|
361
452
|
let syncedCount = _syncedMessageCounts.get(threadId);
|
|
362
453
|
if (syncedCount === undefined) {
|
|
@@ -366,6 +457,67 @@ export async function appendOrCreateThread({
|
|
|
366
457
|
}
|
|
367
458
|
}
|
|
368
459
|
|
|
460
|
+
const resolvedMessageMode =
|
|
461
|
+
messageMode === CAPTURE_MESSAGE_MODE_DELTA ||
|
|
462
|
+
(messageMode === CAPTURE_MESSAGE_MODE_AUTO &&
|
|
463
|
+
hasStableExternalHints(normalized))
|
|
464
|
+
? CAPTURE_MESSAGE_MODE_DELTA
|
|
465
|
+
: CAPTURE_MESSAGE_MODE_SNAPSHOT;
|
|
466
|
+
if (resolvedMessageMode === CAPTURE_MESSAGE_MODE_DELTA) {
|
|
467
|
+
allMessages = buildMessages(CAPTURE_MESSAGE_MODE_DELTA);
|
|
468
|
+
const idempotencyKey = buildAppendIdempotencyKey(
|
|
469
|
+
threadId,
|
|
470
|
+
reason,
|
|
471
|
+
allMessages,
|
|
472
|
+
);
|
|
473
|
+
try {
|
|
474
|
+
const appended = await client.appendThread({
|
|
475
|
+
threadId,
|
|
476
|
+
messages: allMessages,
|
|
477
|
+
deduplicate: true,
|
|
478
|
+
idempotencyKey,
|
|
479
|
+
});
|
|
480
|
+
const added = appended.messagesAdded ?? 0;
|
|
481
|
+
const total =
|
|
482
|
+
Number.isFinite(appended.totalMessages) && appended.totalMessages >= 0
|
|
483
|
+
? appended.totalMessages
|
|
484
|
+
: typeof syncedCount === "number"
|
|
485
|
+
? syncedCount + added
|
|
486
|
+
: allMessages.length;
|
|
487
|
+
_setSyncedMessageCount(threadId, total);
|
|
488
|
+
logger.info(
|
|
489
|
+
`capture: appended ${added} delta messages to ${threadId} (${reason || "event"})`,
|
|
490
|
+
);
|
|
491
|
+
return { threadId, normalized, messagesAdded: added };
|
|
492
|
+
} catch (err) {
|
|
493
|
+
if (!client.isThreadNotFoundError(err)) {
|
|
494
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
495
|
+
logger.warn(
|
|
496
|
+
`capture: thread append failed for ${threadId}: ${message}`,
|
|
497
|
+
);
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
try {
|
|
503
|
+
const createdId = await client.createThread({
|
|
504
|
+
threadId,
|
|
505
|
+
title,
|
|
506
|
+
messages: allMessages,
|
|
507
|
+
source: "openclaw",
|
|
508
|
+
});
|
|
509
|
+
_setSyncedMessageCount(threadId, allMessages.length);
|
|
510
|
+
logger.info(
|
|
511
|
+
`capture: created thread ${createdId} with ${allMessages.length} delta messages (${reason || "event"})`,
|
|
512
|
+
);
|
|
513
|
+
return { threadId, normalized, messagesAdded: allMessages.length };
|
|
514
|
+
} catch (err) {
|
|
515
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
516
|
+
logger.warn(`capture: thread create failed for ${threadId}: ${message}`);
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
369
521
|
if (typeof syncedCount === "number" && syncedCount > allMessages.length) {
|
|
370
522
|
// OpenClaw compaction can shrink the active transcript after we already
|
|
371
523
|
// stored the pre-compaction history. Reset the local cursor to the new
|
|
@@ -563,9 +715,7 @@ export function buildAgentEndCaptureHandler(client, cfg, logger) {
|
|
|
563
715
|
// Layer 2: marker-based exclusion (user typed #nmem-skip in conversation)
|
|
564
716
|
const resolvedMessages = await resolveHookMessages(event);
|
|
565
717
|
if (hasSkipMarker(resolvedMessages, cfg.captureSkipMarker)) {
|
|
566
|
-
logger.debug?.(
|
|
567
|
-
`capture: skipped session with skip marker ${sessionKey}`,
|
|
568
|
-
);
|
|
718
|
+
logger.debug?.(`capture: skipped session with skip marker ${sessionKey}`);
|
|
569
719
|
return;
|
|
570
720
|
}
|
|
571
721
|
|
|
@@ -577,6 +727,7 @@ export function buildAgentEndCaptureHandler(client, cfg, logger) {
|
|
|
577
727
|
reason: "agent_end",
|
|
578
728
|
maxMessageChars: cfg.maxThreadMessageChars,
|
|
579
729
|
resolvedMessages,
|
|
730
|
+
messageMode: CAPTURE_MESSAGE_MODE_AUTO,
|
|
580
731
|
});
|
|
581
732
|
|
|
582
733
|
await triageAndDistill({ client, cfg, logger, captureResult, ctx });
|
|
@@ -614,9 +765,7 @@ export function buildBeforeResetCaptureHandler(client, cfg, logger) {
|
|
|
614
765
|
// Layer 2: marker-based exclusion
|
|
615
766
|
const resolvedMessages = await resolveHookMessages(event);
|
|
616
767
|
if (hasSkipMarker(resolvedMessages, cfg.captureSkipMarker)) {
|
|
617
|
-
logger.debug?.(
|
|
618
|
-
`capture: skipped session with skip marker ${sessionKey}`,
|
|
619
|
-
);
|
|
768
|
+
logger.debug?.(`capture: skipped session with skip marker ${sessionKey}`);
|
|
620
769
|
return;
|
|
621
770
|
}
|
|
622
771
|
|