From 90bea07ae17d0375462b6027437c13ee4801156a Mon Sep 17 00:00:00 2001 From: SUXIAOHUA Date: Sun, 22 Mar 2026 13:31:09 +0800 Subject: [PATCH] feat(capture): add captureExclude and captureSkipMarker config options Add two-layer filtering to skip unwanted sessions from auto-capture: 1. Pattern-based exclusion (captureExclude): array of glob patterns matched against ctx.sessionKey. Glob `*` matches within a colon-delimited segment. Example: "agent:*:cron:*" excludes all cron job sessions without affecting other conversations. 2. Marker-based exclusion (captureSkipMarker): when any message in the session contains the marker text (default: "#nmem-skip"), the entire session is skipped. Gives users ad-hoc control over which conversations are captured. Both layers apply to buildAgentEndCaptureHandler (thread append + triage/distill) and buildBeforeResetCaptureHandler (thread-only checkpoints). When a session is excluded, neither thread creation nor distillation occurs. Use case: OpenClaw users with scheduled cron jobs (news digests, token reports) and executor subagent sessions that produce hundreds of low-value threads, degrading Working Memory quality. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../openclaw.plugin.json | 19 ++++++ nowledge-mem-openclaw-plugin/src/config.js | 29 ++++++++ .../src/hooks/capture.js | 68 ++++++++++++++++++- 3 files changed, 115 insertions(+), 1 deletion(-) diff --git a/nowledge-mem-openclaw-plugin/openclaw.plugin.json b/nowledge-mem-openclaw-plugin/openclaw.plugin.json index 693a6d01..3cc732c5 100644 --- a/nowledge-mem-openclaw-plugin/openclaw.plugin.json +++ b/nowledge-mem-openclaw-plugin/openclaw.plugin.json @@ -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" @@ -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": "", diff --git a/nowledge-mem-openclaw-plugin/src/config.js b/nowledge-mem-openclaw-plugin/src/config.js index bd77c932..5c4a32a3 100644 --- a/nowledge-mem-openclaw-plugin/src/config.js +++ b/nowledge-mem-openclaw-plugin/src/config.js @@ -21,6 +21,8 @@ const ALLOWED_KEYS = new Set([ "maxContextResults", "recallMinScore", "maxThreadMessageChars", + "captureExclude", + "captureSkipMarker", "apiUrl", "apiKey", // Legacy aliases — accepted but not advertised @@ -343,6 +345,31 @@ 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, @@ -350,6 +377,8 @@ export function parseConfig(raw, logger) { maxContextResults, recallMinScore, maxThreadMessageChars, + captureExclude, + captureSkipMarker, apiUrl, apiKey, _sources, diff --git a/nowledge-mem-openclaw-plugin/src/hooks/capture.js b/nowledge-mem-openclaw-plugin/src/hooks/capture.js index f00f3d29..a8a9e914 100644 --- a/nowledge-mem-openclaw-plugin/src/hooks/capture.js +++ b/nowledge-mem-openclaw-plugin/src/hooks/capture.js @@ -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 ""; @@ -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. @@ -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,