/** * Noise Filter * Filters out low-quality memories (meta-questions, agent denials, session boilerplate) * Inspired by openclaw-plugin-continuity's noise filtering approach. */ import { logInfo } from "./stderr-log.js"; import { redactForLog } from "./pii-detector.js"; // Agent-side denial patterns const DENIAL_PATTERNS = [ /i don'?t have (any )?(information|data|memory|record)/i, /i'?m not sure about/i, /i don'?t recall/i, /i don'?t remember/i, /it looks like i don'?t/i, /i wasn'?t able to find/i, /no (relevant )?memories found/i, /i don'?t have access to/i, // Chinese denial patterns (synced from upstream) /我没有(任何)?(相关)?(信息|数据|记忆|记录)/, /我不(太)?确定/, /我不记得/, /我想不起来/, /我没(有)?找到/, /找不到(相关)?记忆/, /没有(相关)?记忆/, /我无法(访问|获取)/, ]; // User-side meta-question patterns (about memory itself, not content) const META_QUESTION_PATTERNS = [ /\bdo you (remember|recall|know about)\b/i, /\bcan you (remember|recall)\b/i, /\bdid i (tell|mention|say|share)\b/i, /\bhave i (told|mentioned|said)\b/i, /\bwhat did i (tell|say|mention)\b/i, // Chinese meta-question patterns (synced from upstream) /你(还)?记得吗/, /你(还)?记不记得/, /你知道我(说过|提过|告诉|提到).*吗/, /我(有没有|是不是)(说过|提过|告诉|提到)/, /我之前(说过|提过|提到|告诉)/, /我(跟你)?说过.*吗/, ]; // Session boilerplate (anchored to prevent false positives on real content) const BOILERPLATE_PATTERNS = [ /^(hi|hello|hey|good morning|good evening|greetings)\b/i, /^fresh session/i, /^new session/i, /^HEARTBEAT/i, /^你好[!!\s,.,。]?$/, /^(早上好|早安|午安|晚上好|晚安)[!!\s,.,。]?$/, /^(嗨|哈[喽啰]|哈[喽啰]呀)[!!\s,.,。]?$/, /^(嗯|哦|噢|呃)[!!\s,.,。]?$/, /^新(会话|对话|聊天)/, ]; // Short boilerplate: only noise when total text is very short // "好的" alone = noise, "好的方案是使用Redis" = real content const SHORT_BOILERPLATE_PATTERNS = [ /^(好的|好吧|行|可以|没问题|OK|ok|收到|明白|了解|知道了)/i, /^(谢谢|感谢|多谢|谢啦|3Q|thx)/i, ]; const BOILERPLATE_MAX_LENGTH = 10; // OpenClaw v3.2+ injected metadata headers (backport from v1.0.29) const METADATA_HEADER_PATTERNS = [ /^Conversation info \(untrusted metadata\)/i, /^---\s*\n\s*\{[\s\S]*?\}\s*\n\s*---/m, // YAML-like JSON blocks /^\[?\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[^\]]*\]?\s*$/m, // bare timestamps ]; // Diagnostic artifact patterns (synced from upstream v1.1.0-beta.3) // Extractor validation outputs that leak into memory store const DIAGNOSTIC_ARTIFACT_PATTERNS = [ /^query\s*->\s*none$/i, /^no explicit solution$/i, /^如果你知道.+只回复/i, /^如果不知道.+只回复\s*none/i, /^只回复精确代号/i, ]; // CC bridge injected agent prompts: the Telegram bridge writes CC's own system // prompts (reply / self-decision templates) as `user` turns, which transcript // ingest then mis-extracts as profile/preferences. They are never user content. const AGENT_PROMPT_PATTERNS = [ /^#{1,3}\s*CC\s+(Reply|Self-Decision)\s+Prompt\b/im, /你不是\s*Alice\s*的数字分身/, /你是\s*CC[。.]?\s*(Alice\s*刚给你发了消息|现在你周期性醒来)/, // Fixed sections of the bridge prompt that survive ingest as standalone chunks // (the long prompt turn gets split, so the title/persona lines may be absent). /你的输出\s*[\r\n]+\s*直接回复\s*Alice/, /节奏要求[::]\s*[\r\n]+\s*[-*]\s*短话题/, ]; // CC bridge self-decision / RecallNest distill payloads embed structured JSON // (decision logs, conversation history, interaction stats). Long prompts get // split by transcript chunking, orphaning JSON field-lines that lost their // enclosing braces — these survive ingest looking like preferences. We match // the severed-fragment SHAPE, not JSON in general: a complete { } object is left // alone (it may be real discussion); only orphaned field-lines (start as "key": // with no brace) and dangling structural tokens (leading , } ]) qualify. const CONTEXT_JSON_FRAGMENT_PATTERNS = [ // bridge/distill proprietary schema keys — ANCHORED to line start (^). These // keys are also RecallNest's own schema fields, so a real memory may MENTION // them inline (e.g. 我们讨论 "distilled_at": 要不要保留); anchoring ensures only // an orphaned line that LEADS with the key is filtered, not such discussion. /^"?(?:next_check_hint|alice_interaction_stats(?:_7d)?|alice_last_cc_hours_ago|engagement_rate|seen_no_reply|distilled_at)"\s*:/, // orphan field-line: whole chunk starts as "key": with no { wrapper. // SCALAR values only (string/number/bool/null). A `{` or `[` value is left // alone so real config / package.json chunks like `"scripts": {` survive. /^"?[A-Za-z_][\w]*"\s*:\s*(?:"|\d|true\b|false\b|null\b)/, // severed JSON array/object element: a dangling , } ] then a NEWLINE into a // brace/quoted-key. Requiring the newline-then-JSON shape avoids real code // fragments like `} catch (e) {` or `});` (close-token followed by code on // the same line, never a newline into a JSON key). /^[,}\]]+[ \t]*\r?\n\s*[{[]?\s*"[A-Za-z_]/, ]; export interface NoiseFilterOptions { /** Filter agent denial responses (default: true) */ filterDenials?: boolean; /** Filter meta-questions about memory (default: true) */ filterMetaQuestions?: boolean; /** Filter session boilerplate (default: true) */ filterBoilerplate?: boolean; /** Filter CC bridge injected agent prompts (default: true) */ filterAgentPrompts?: boolean; } const DEFAULT_OPTIONS: Required = { filterDenials: true, filterMetaQuestions: true, filterBoilerplate: true, filterAgentPrompts: true, }; /** * Check if a memory text is noise that should be filtered out. * Returns true if the text is noise. */ export function isNoise(text: string, options: NoiseFilterOptions = {}): boolean { const opts = { ...DEFAULT_OPTIONS, ...options }; const trimmed = text.trim(); if (trimmed.length < 5) { logInfo(`[INFO] noise-filter: skipped short text (${trimmed.length} chars)`); return true; } if (opts.filterDenials && DENIAL_PATTERNS.some(p => p.test(trimmed))) { logInfo(`[INFO] noise-filter: denial pattern matched: "${redactForLog(trimmed)}..."`); return true; } if (opts.filterMetaQuestions && META_QUESTION_PATTERNS.some(p => p.test(trimmed))) { logInfo(`[INFO] noise-filter: meta-question filtered: "${redactForLog(trimmed)}..."`); return true; } if (opts.filterBoilerplate) { if (BOILERPLATE_PATTERNS.some(p => p.test(trimmed))) { logInfo(`[INFO] noise-filter: boilerplate filtered: "${redactForLog(trimmed)}..."`); return true; } if (trimmed.length <= BOILERPLATE_MAX_LENGTH && SHORT_BOILERPLATE_PATTERNS.some(p => p.test(trimmed))) { logInfo(`[INFO] noise-filter: short boilerplate filtered: "${redactForLog(trimmed)}"`); return true; } } // OpenClaw v3.2+ metadata noise (backport from v1.0.29) if (METADATA_HEADER_PATTERNS.some(p => p.test(trimmed))) { logInfo(`[INFO] noise-filter: metadata header filtered`); return true; } // Diagnostic artifact noise (synced from upstream v1.1.0-beta.3) if (DIAGNOSTIC_ARTIFACT_PATTERNS.some(p => p.test(trimmed))) { logInfo(`[INFO] noise-filter: diagnostic artifact filtered: "${redactForLog(trimmed)}..."`); return true; } // CC bridge injected agent prompts (reply/self-decision templates leaked as user turns) if (opts.filterAgentPrompts && AGENT_PROMPT_PATTERNS.some(p => p.test(trimmed))) { logInfo(`[INFO] noise-filter: agent prompt filtered: "${redactForLog(trimmed)}..."`); return true; } // Severed bridge/distill context-JSON fragments (orphaned field-lines from chunking) if (opts.filterAgentPrompts && CONTEXT_JSON_FRAGMENT_PATTERNS.some(p => p.test(trimmed))) { logInfo(`[INFO] noise-filter: context JSON fragment filtered: "${redactForLog(trimmed)}..."`); return true; } return false; } /** * Filter an array of items, removing noise entries. */ export function filterNoise( items: T[], getText: (item: T) => string, options?: NoiseFilterOptions ): T[] { const opts = { ...DEFAULT_OPTIONS, ...options }; return items.filter(item => !isNoise(getText(item), opts)); }