UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

327 lines (326 loc) 13.2 kB
/** * MemoryConsolidator — 记忆固化与冲突解决 * * 从 PersistentMemory.js 提取的智能固化逻辑。 * 负责: * - Extract-Update Consolidation (ADD / UPDATE / MERGE / NOOP) * - Mem0 风格冲突解决 (矛盾检测 + 自动替换) * - Legacy JSONL 迁移 * * @module MemoryConsolidator */ var _a; import fs from 'node:fs'; import path from 'node:path'; import { MemoryStore } from './MemoryStore.js'; // ─── 常量 ────────────────────────────────────────────── /** 相似度阈值 */ const SIMILARITY_UPDATE = 0.85; // ≥85% 同义 → UPDATE const SIMILARITY_MERGE = 0.6; // ≥60% 相关 → MERGE /** 详细日志开关 (合并时记录每次 MERGE/UPDATE/REPLACE 的内容摘要) */ const VERBOSE_CONSOLIDATION = true; // ─── 矛盾检测模式 (Mem0 风格冲突解决) ───────────────── /** 中文否定/禁止模式 */ const NEGATION_PATTERNS_ZH = /不(再)?使用|不(再)?用|禁止|废弃|移除|取消|停止|不要|不采用|弃用|淘汰/; /** 英文否定/禁止模式 */ const NEGATION_PATTERNS_EN = /\b(don'?t|do\s+not|never|no\s+longer|removed?|deprecated?|stop|avoid|disable|abandon|drop)\b/i; /** 共享词语最少匹配数 */ const MIN_TOPIC_OVERLAP_WORDS = 2; /** 共享词语比例阈值 */ const MIN_TOPIC_OVERLAP_RATIO = 0.3; export class MemoryConsolidator { #store; #logger; constructor(store, opts = {}) { this.#store = store; this.#logger = opts.logger || null; } // ═══════════════════════════════════════════════════════════ // 智能固化 // ═══════════════════════════════════════════════════════════ /** * 智能固化: 先执行冲突检测 (Mem0 风格),再执行 ADD / UPDATE / MERGE / NOOP * * @returns } */ consolidate(candidateMemories, { bootstrapSession } = {}) { // Phase 1: 冲突预解决 const { processed, replaced } = this.#preResolveConflicts(candidateMemories); // Phase 2: 正常 consolidate 流程 const stats = { added: 0, updated: 0, merged: 0, skipped: 0 }; const runConsolidate = this.#store.transaction(() => { for (const candidate of processed) { const content = (candidate.content || '').trim(); if (!content || content.length < 5) { stats.skipped++; continue; } // 搜索相似记忆 (同 type 优先) const similar = this.#store.findSimilar(content, candidate.type ?? null, 3); if (similar.length === 0) { this.#store.add({ ...candidate, bootstrapSession }); stats.added++; continue; } const topMatch = similar[0]; if ((topMatch.similarity ?? 0) >= SIMILARITY_UPDATE) { // UPDATE: 几乎同义 → 更新重要性和时间戳 this.#store.update(topMatch.id, { importance: Math.max(topMatch.importance, candidate.importance || 5), accessCount: topMatch.access_count + 1, }); stats.updated++; if (VERBOSE_CONSOLIDATION) { this.#logDebug(`UPDATE sim=${(topMatch.similarity ?? 0).toFixed(2)}: "${content.substring(0, 40)}..." → existing "${topMatch.content.substring(0, 40)}..."`); } } else if ((topMatch.similarity ?? 0) >= SIMILARITY_MERGE) { // MERGE: 相关但不同 → 合并信息 const mergedContent = `${topMatch.content}; ${content}`.substring(0, 500); const existingRelated = MemoryStore.safeParseJSON(topMatch.related_memories_raw, []); this.#store.update(topMatch.id, { content: mergedContent, importance: Math.max(topMatch.importance, candidate.importance || 5), relatedMemories: [...existingRelated, `merged:${Date.now()}`], }); stats.merged++; if (VERBOSE_CONSOLIDATION) { this.#logDebug(`MERGE sim=${(topMatch.similarity ?? 0).toFixed(2)}: "${content.substring(0, 40)}..." ⊕ "${topMatch.content.substring(0, 40)}..."`); } } else { this.#store.add({ ...candidate, bootstrapSession }); stats.added++; } } }); runConsolidate(); this.#log(`Consolidation: +${stats.added} ADD, ~${stats.updated} UPDATE, ⊕${stats.merged} MERGE, =${stats.skipped} SKIP`); // 容量控制 this.#store.enforceCapacity(); if (replaced > 0) { stats.replaced = replaced; } return stats; } // ═══════════════════════════════════════════════════════════ // Legacy Migration // ═══════════════════════════════════════════════════════════ /** * 从旧版 Memory.js JSONL 文件迁移数据到 SQLite * * @returns >} */ async migrateFromLegacy(projectRoot) { const legacyPath = path.join(projectRoot, '.autosnippet', 'memory.jsonl'); if (!fs.existsSync(legacyPath)) { return { migrated: 0, skipped: 0 }; } try { const raw = fs.readFileSync(legacyPath, 'utf-8').trim(); if (!raw) { return { migrated: 0, skipped: 0 }; } const lines = raw.split('\n').filter(Boolean); const candidates = lines .map((line) => { try { return JSON.parse(line); } catch { return null; } }) .filter(Boolean) .map((m) => ({ type: _a.#mapLegacyType(m.type), content: (m.content || '').trim(), source: m.source || 'user', importance: m.type === 'decision' ? 7 : 5, })) .filter((m) => m.content.length >= 5); if (candidates.length === 0) { return { migrated: 0, skipped: lines.length }; } const result = this.consolidate(candidates, { bootstrapSession: 'legacy-migration', }); // 迁移成功 → 重命名旧文件 try { fs.renameSync(legacyPath, `${legacyPath}.migrated`); } catch { /* rename failure non-critical */ } const migrated = result.added + result.merged; this.#log(`Legacy migration: ${migrated} migrated (${result.added} added, ${result.merged} merged), ${result.skipped} skipped from ${legacyPath}`); return { migrated, skipped: result.skipped }; } catch (err) { this.#log(`Legacy migration failed: ${err.message}`); return { migrated: 0, skipped: 0, error: err.message }; } } // ═══════════════════════════════════════════════════════════ // Private: 冲突预解决 (Mem0 风格) // ═══════════════════════════════════════════════════════════ /** * 在 consolidate 主流程前检测并解决矛盾 * @returns } */ #preResolveConflicts(candidates) { if (!candidates || candidates.length === 0) { return { processed: [], replaced: 0 }; } const processed = []; let replaced = 0; for (const candidate of candidates) { const content = (candidate.content || '').trim(); if (!content || content.length < 5) { processed.push(candidate); continue; } try { const similar = this.#store.findSimilar(content, null, 3); const deserialized = similar.map((r) => MemoryStore.deserialize(r)); let conflictResolved = false; for (const existing of deserialized) { if (existing.type === (candidate.type || 'fact')) { const isContradiction = _a.#detectContradiction(existing.content, content); if (isContradiction) { this.#store.update(existing.id, { content: content.substring(0, 500), importance: Math.max(existing.importance || 5, candidate.importance || 5), }); conflictResolved = true; replaced++; this.#log(`Conflict resolved: replaced "${existing.content.substring(0, 50)}..." with "${content.substring(0, 50)}..."`); break; } } } if (!conflictResolved) { processed.push(candidate); } } catch { processed.push(candidate); } } return { processed, replaced }; } /** 检测两段记忆内容是否矛盾 */ static #detectContradiction(contentA, contentB) { if (!contentA || !contentB) { return false; } const aNeg = NEGATION_PATTERNS_ZH.test(contentA) || NEGATION_PATTERNS_EN.test(contentA); const bNeg = NEGATION_PATTERNS_ZH.test(contentB) || NEGATION_PATTERNS_EN.test(contentB); if (aNeg === bNeg) { return false; } const wordsA = _a.#extractTopicWords(contentA); const wordsB = _a.#extractTopicWords(contentB); let overlap = 0; for (const w of wordsA) { if (wordsB.has(w)) { overlap++; } } const minSize = Math.min(wordsA.size, wordsB.size); if (minSize === 0) { return false; } return overlap >= MIN_TOPIC_OVERLAP_WORDS || overlap / minSize >= MIN_TOPIC_OVERLAP_RATIO; } /** 提取主题词 (去停用词 + 短词) */ static #extractTopicWords(text) { if (!text) { return new Set(); } const tokens = text .toLowerCase() .split(/[\s,;:!?。,;:!?\-_/\\|()[\]{}'"<>·、]+/) .filter((t) => t.length >= 2); const stopWords = new Set([ '我们', '使用', '项目', '需要', '可以', '应该', '建议', '目前', '已经', '这个', '那个', '一个', '进行', '通过', '对于', 'the', 'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could', 'should', 'may', 'might', 'shall', 'can', 'this', 'that', 'these', 'those', 'with', 'from', 'for', 'and', 'but', 'not', 'all', 'any', 'each', 'every', 'some', ]); return new Set(tokens.filter((t) => !stopWords.has(t))); } /** Legacy type 映射 */ static #mapLegacyType(legacyType) { switch (legacyType) { case 'preference': return 'preference'; default: return 'fact'; } } #logDebug(msg) { const formatted = `[MemoryConsolidator] ${msg}`; if (this.#logger?.debug) { this.#logger.debug(formatted); } else if (this.#logger?.info) { this.#logger.info(formatted); } } #log(msg) { const formatted = `[MemoryConsolidator] ${msg}`; if (this.#logger?.info) { this.#logger.info(formatted); } } } _a = MemoryConsolidator;