UNPKG

open-memex

Version:

Local-first memory layer and protocol for AI coding agents. Markdown source of truth, SQLite FTS5 index, zero cloud.

77 lines (76 loc) • 3.46 kB
/** * D67: capture-body length bounds. The lower bound rejects fragments — a * trigger that yields "这个" or "OK" is a match on the trigger, not a memory, * and a 2-char memory is noise in every future session's injection. The floor * stays deliberately above 1: D67 considered lowering it and decided against * it. What changed is that a rejection is no longer silent (see KeywordDrop). */ export const MIN_CAPTURE_CHARS = 3; export const MAX_CAPTURE_CHARS = 2000; /** * Scan user text for memory-capture keyword triggers, reporting both what * would be captured and what matched but was rejected (D67) — a dropped match * that says nothing reads as a broken feature. * Each pattern must have a single capture group whose value becomes the memory body. * Personal patterns (cfg.keywordPersonalPatterns) force the personal scope. * * Personal patterns run first and claim their line: an explicit scope signal * (e.g. "记住我对花粉过敏") must not also fire the generic 记住 into the project scope. */ export function scanKeywords(userText, cfg) { const hits = []; const dropped = []; if (!cfg.keywordCaptureEnabled) return { hits, dropped }; const lines = userText.split(/\r?\n/); const claimed = new Set(); const scan = (patterns, personal, skipClaimed) => { for (const src of patterns) { let re; try { re = new RegExp(src, "i"); } catch { continue; } for (let i = 0; i < lines.length; i++) { if (skipClaimed && claimed.has(i)) continue; const m = lines[i].match(re); if (!m || !m[1]) continue; const content = m[1].trim().replace(/[.!?]$/, "").trim(); if (personal) claimed.add(i); if (content.length >= MIN_CAPTURE_CHARS && content.length <= MAX_CAPTURE_CHARS) { hits.push({ content, pattern: src, personal }); break; // one hit per pattern per message } // Rejected: record it once per pattern. The line stays claimed for // personal patterns even here - "记住我:OK" must not fall through to // the generic 记住 and be saved as "我:OK" (D67). if (!dropped.some((d) => d.pattern === src && d.content === content)) { dropped.push({ content, pattern: src, personal, reason: content.length < MIN_CAPTURE_CHARS ? "too-short" : "too-long", }); } break; } } }; scan(cfg.keywordPersonalPatterns, true, false); scan(cfg.keywordPatterns, false, true); if (cfg.logLevel === "debug" && dropped.length > 0) { for (const d of dropped) { console.error(`[open-memex] keyword match not captured (${d.reason === "too-short" ? `body ${d.content.length} chars < ${MIN_CAPTURE_CHARS}` : `body ${d.content.length} chars > ${MAX_CAPTURE_CHARS}`}): "${d.content}"`); } } return { hits, dropped }; } /** Capture-only view of {@link scanKeywords} (drops are logged, not returned). */ export function detectKeywords(userText, cfg) { return scanKeywords(userText, cfg).hits; }