Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions nowledge-mem-openclaw-plugin/openclaw.plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@
"label": "Max thread message chars",
"help": "Maximum characters preserved per captured OpenClaw thread message (200-20000). Higher values keep more of long messages in Nowledge thread history."
},
"captureExclude": {
"label": "Exclude session patterns",
"help": "Session key glob patterns to skip during auto-capture. Use * to match within a colon-segment. Example: agent:*:cron:*"
},
"captureSkipMarker": {
"label": "Skip marker",
"help": "Text marker in messages that prevents capture for the session. Default: #nmem-skip"
},
"apiUrl": {
"label": "Server URL (remote mode)",
"help": "Leave empty for local mode (default: http://127.0.0.1:14242). Set to your remote server URL for cross-device or team access. See: https://docs.nowledge.co/docs/remote-access"
Expand Down Expand Up @@ -79,6 +87,17 @@
"maximum": 20000,
"description": "Maximum characters preserved per captured OpenClaw thread message before truncation."
},
"captureExclude": {
"type": "array",
"items": { "type": "string" },
"default": [],
"description": "Session key glob patterns to exclude from auto-capture. Glob * matches within a colon-delimited segment. Example: agent:*:cron:* excludes all cron job sessions."
},
"captureSkipMarker": {
"type": "string",
"default": "#nmem-skip",
"description": "Text marker in messages that prevents capture for the entire session. When any message contains this marker, the session is skipped."
},
"apiUrl": {
"type": "string",
"default": "",
Expand Down
29 changes: 29 additions & 0 deletions nowledge-mem-openclaw-plugin/src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ const ALLOWED_KEYS = new Set([
"maxContextResults",
"recallMinScore",
"maxThreadMessageChars",
"captureExclude",
"captureSkipMarker",
"apiUrl",
"apiKey",
// Legacy aliases — accepted but not advertised
Expand Down Expand Up @@ -343,13 +345,40 @@ export function parseConfig(raw, logger) {
const apiKey = ak.value;
_sources.apiKey = ak.source;

// --- captureExclude: file > pluginConfig > default ---
const captureExclude = (() => {
const fromFile = Array.isArray(resolvedFile.captureExclude)
? resolvedFile.captureExclude
: null;
const fromPlugin = Array.isArray(resolvedPlugin.captureExclude)
? resolvedPlugin.captureExclude
: null;
const raw = fromFile ?? fromPlugin ?? [];
return raw.filter((v) => typeof v === "string" && v.trim());
})();

// --- captureSkipMarker: file > pluginConfig > default ---
const captureSkipMarker = (() => {
const fromFile =
typeof resolvedFile.captureSkipMarker === "string"
? resolvedFile.captureSkipMarker.trim()
: undefined;
const fromPlugin =
typeof resolvedPlugin.captureSkipMarker === "string"
? resolvedPlugin.captureSkipMarker.trim()
: undefined;
return fromFile || fromPlugin || "#nmem-skip";
})();

return {
sessionContext,
sessionDigest,
digestMinInterval,
maxContextResults,
recallMinScore,
maxThreadMessageChars,
captureExclude,
captureSkipMarker,
apiUrl,
apiKey,
_sources,
Expand Down
68 changes: 67 additions & 1 deletion nowledge-mem-openclaw-plugin/src/hooks/capture.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,40 @@ function _setLastCapture(threadId, now) {
}
}

/**
* Test whether a session key matches any exclusion glob pattern.
* Glob `*` matches within a colon-delimited segment (not across colons).
* Example: "agent:*:cron:*" matches "agent:main:cron:abc123"
*/
function matchesExcludePattern(sessionKey, patterns) {
if (!Array.isArray(patterns) || patterns.length === 0) return false;
const key = String(sessionKey || "").toLowerCase();
return patterns.some((pattern) => {
const re = new RegExp(
"^" +
String(pattern)
.toLowerCase()
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
.replace(/\*/g, "[^:]*") +
"$",
);
return re.test(key);
});
}

/**
* Check if any message contains the skip marker text.
* Scans both raw message content and nested message objects.
*/
function hasSkipMarker(messages, marker) {
if (!marker || !Array.isArray(messages)) return false;
const markerLc = marker.toLowerCase();
return messages.some((msg) => {
const text = extractText(msg?.content ?? msg?.message?.content);
return text.toLowerCase().includes(markerLc);
});
}

function truncate(text, max = DEFAULT_MAX_MESSAGE_CHARS) {
const str = String(text || "").trim();
if (!str) return "";
Expand Down Expand Up @@ -284,6 +318,22 @@ export function buildAgentEndCaptureHandler(client, cfg, logger) {
return async (event, ctx) => {
if (!event?.success) return;

// Layer 1: pattern-based exclusion (e.g. cron jobs, subagent sessions)
const sessionKey = String(ctx?.sessionKey || ctx?.sessionId || "");
if (matchesExcludePattern(sessionKey, cfg.captureExclude)) {
logger.debug?.(`capture: skipped excluded session ${sessionKey}`);
return;
}

// Layer 2: marker-based exclusion (user typed #nmem-skip in conversation)
const rawMessages = await resolveHookMessages(event);
if (hasSkipMarker(rawMessages, cfg.captureSkipMarker)) {
logger.debug?.(
`capture: skipped session with skip marker ${sessionKey}`,
);
return;
}

// 1. Always thread-append (idempotent, self-guards on empty messages).
// Never skip this — messages must always be persisted regardless of
// cooldown state, since appendOrCreateThread is deduped and cheap.
Expand Down Expand Up @@ -376,8 +426,24 @@ export function buildAgentEndCaptureHandler(client, cfg, logger) {
* Capture thread messages before reset or after compaction.
* Thread-only (no distillation) — these are lifecycle checkpoints.
*/
export function buildBeforeResetCaptureHandler(client, _cfg, logger) {
export function buildBeforeResetCaptureHandler(client, cfg, logger) {
return async (event, ctx) => {
// Layer 1: pattern-based exclusion
const sessionKey = String(ctx?.sessionKey || ctx?.sessionId || "");
if (matchesExcludePattern(sessionKey, cfg.captureExclude)) {
logger.debug?.(`capture: skipped excluded session ${sessionKey}`);
return;
}

// Layer 2: marker-based exclusion
const rawMessages = await resolveHookMessages(event);
if (hasSkipMarker(rawMessages, cfg.captureSkipMarker)) {
logger.debug?.(
`capture: skipped session with skip marker ${sessionKey}`,
);
return;
}

const reason = typeof event?.reason === "string" ? event.reason : undefined;
await appendOrCreateThread({
client,
Expand Down