/**
 * Memory Consolidation System
 *
 * Like sleep consolidation in human brains, this system:
 * - Moves worthy short-term memories to long-term storage
 * - Strengthens frequently accessed memories
 * - Cleans up decayed/irrelevant memories
 * - Merges similar memories to reduce redundancy
 */
import { getDatabase, withTransaction } from '../database/init.js';
import { DEFAULT_CONFIG, } from './types.js';
import { getMemoriesByType, getRecentMemories, promoteMemory, deleteMemory, searchMemories, getMemoryStats, updateDecayScores, addMemory, rowToMemory, } from './store.js';
import { processDecay, } from './decay.js';
import { detectContradictions, linkContradictions, } from './contradiction.js';
import { jaccardSimilarity } from './similarity.js';
import { pruneActivationCache } from './activation.js';
/**
 * Run full consolidation process
 * This is like the brain's sleep consolidation - should be run periodically
 */
export function consolidate(config = DEFAULT_CONFIG) {
    // Wrap entire consolidation in a transaction for atomicity
    return withTransaction(() => {
        const db = getDatabase();
        let consolidated = 0;
        let decayed = 0;
        let deleted = 0;
        // Get all short-term memories
        const shortTermMemories = getMemoriesByType('short_term', config.maxShortTermMemories * 2);
        // Process decay for all memories
        const { toDelete, toPromote, updated } = processDecay(shortTermMemories, config);
        // Promote worthy memories
        for (const id of toPromote) {
            promoteMemory(id);
            consolidated++;
        }
        // Delete decayed memories (excluding those just promoted)
        for (const id of toDelete) {
            if (!toPromote.includes(id)) {
                deleteMemory(id);
                deleted++;
            }
        }
        // Update decayed scores in database
        const updateStmt = db.prepare('UPDATE memories SET salience = ? WHERE id = ?');
        for (const [id, score] of updated) {
            if (!toDelete.includes(id)) {
                updateStmt.run(score, id);
                decayed++;
            }
        }
        // Enforce memory limits
        deleted += enforceMemoryLimits(config);
        // Persist updated decay scores for efficient sorting
        updateDecayScores();
        // Evolve salience based on structural importance
        let salienceEvolved = 0;
        try {
            salienceEvolved = evolveSalience(db);
        }
        catch (e) {
            console.error('[shieldcortex] Salience evolution failed:', e);
        }
        // ORGANIC FEATURE: Contradiction Detection (Phase 3)
        // Detect and link contradicting memories during consolidation
        let contradictionsFound = 0;
        let contradictionsLinked = 0;
        try {
            const contradictions = detectContradictions({ minScore: 0.5, limit: 50 });
            contradictionsFound = contradictions.length;
            contradictionsLinked = linkContradictions(contradictions);
        }
        catch (e) {
            console.error('[shieldcortex] Contradiction detection failed:', e);
        }
        // Prune activation cache to prevent unbounded growth
        pruneActivationCache();
        // Content-aware deduplication of long-term memories
        let deduplicated = 0;
        try {
            const dedup = deduplicateMemories();
            deduplicated = dedup.merged;
        }
        catch (e) {
            console.error('[shieldcortex] Deduplication failed:', e);
        }
        return { consolidated, decayed, deleted, contradictionsFound, contradictionsLinked, salienceEvolved, deduplicated };
    });
}
/**
 * Adjust salience based on structural importance (link count, contradiction status).
 * Called during consolidation.
 */
function evolveSalience(db) {
    let updated = 0;
    // Boost highly-linked memories (hub bonus)
    const hubs = db.prepare(`
    SELECT m.id, m.salience,
      (SELECT COUNT(*) FROM memory_links WHERE source_id = m.id OR target_id = m.id) as link_count
    FROM memories m
    WHERE m.type IN ('long_term', 'episodic')
  `).all();
    for (const hub of hubs) {
        if (hub.link_count < 2)
            continue;
        const linkBonus = Math.min(0.1, Math.log2(hub.link_count) * 0.03);
        const newSalience = Math.min(1.0, hub.salience + linkBonus);
        if (newSalience > hub.salience) {
            db.prepare('UPDATE memories SET salience = ? WHERE id = ?').run(newSalience, hub.id);
            updated++;
        }
    }
    // Penalize contradicted memories slightly (both sides)
    const contradicted = db.prepare(`
    SELECT DISTINCT source_id, target_id
    FROM memory_links
    WHERE relationship = 'contradicts'
  `).all();
    for (const pair of contradicted) {
        for (const id of [pair.source_id, pair.target_id]) {
            const mem = db.prepare('SELECT salience FROM memories WHERE id = ?').get(id);
            if (mem && mem.salience > 0.3) {
                db.prepare('UPDATE memories SET salience = ? WHERE id = ?')
                    .run(mem.salience - 0.02, id);
                updated++;
            }
        }
    }
    return updated;
}
/**
 * Compute Levenshtein distance between two strings.
 * Returns normalised similarity (0.0 = no match, 1.0 = identical).
 */
function levenshteinSimilarity(a, b) {
    const la = a.length;
    const lb = b.length;
    if (la === 0 && lb === 0)
        return 1;
    if (la === 0 || lb === 0)
        return 0;
    const prev = new Array(lb + 1);
    const curr = new Array(lb + 1);
    for (let j = 0; j <= lb; j++)
        prev[j] = j;
    for (let i = 1; i <= la; i++) {
        curr[0] = i;
        for (let j = 1; j <= lb; j++) {
            const cost = a[i - 1] === b[j - 1] ? 0 : 1;
            curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
        }
        for (let j = 0; j <= lb; j++)
            prev[j] = curr[j];
    }
    const maxLen = Math.max(la, lb);
    return 1 - prev[lb] / maxLen;
}
/**
 * Extract words from text, lowercased and deduplicated.
 */
function extractWords(text) {
    return new Set(text.toLowerCase().replace(/[^a-z0-9\s]/g, ' ').split(/\s+/).filter(w => w.length > 1));
}
/**
 * Compute word-overlap ratio between two texts.
 * Returns the proportion of shared words relative to the smaller set.
 */
function wordOverlap(a, b) {
    const wordsA = extractWords(a);
    const wordsB = extractWords(b);
    if (wordsA.size === 0 || wordsB.size === 0)
        return 0;
    let shared = 0;
    for (const w of wordsA) {
        if (wordsB.has(w))
            shared++;
    }
    const minSize = Math.min(wordsA.size, wordsB.size);
    return shared / minSize;
}
/**
 * Count shared words between two texts.
 */
function sharedWordCount(a, b) {
    const wordsA = extractWords(a);
    const wordsB = extractWords(b);
    let count = 0;
    for (const w of wordsA) {
        if (wordsB.has(w))
            count++;
    }
    return count;
}
export function findDuplicateMemoryPairs(options) {
    const db = getDatabase();
    const limit = options?.limit ?? 20;
    const rows = options?.project
        ? db.prepare("SELECT * FROM memories WHERE type = 'long_term' AND project = ? AND COALESCE(status, 'active') NOT IN ('archived', 'suppressed') ORDER BY created_at ASC").all(options.project)
        : db.prepare("SELECT * FROM memories WHERE type = 'long_term' AND COALESCE(status, 'active') NOT IN ('archived', 'suppressed') ORDER BY created_at ASC").all();
    const groups = new Map();
    for (const mem of rows) {
        const cat = mem.category || 'note';
        if (!groups.has(cat))
            groups.set(cat, []);
        groups.get(cat).push(mem);
    }
    const pairs = [];
    const seen = new Set();
    for (const [, group] of groups) {
        if (group.length < 2)
            continue;
        for (let i = 0; i < group.length && pairs.length < limit; i++) {
            const memA = group[i];
            for (let j = i + 1; j < group.length && pairs.length < limit; j++) {
                const memB = group[j];
                const idA = memA.id;
                const idB = memB.id;
                const pairKey = `${Math.min(idA, idB)}:${Math.max(idA, idB)}`;
                if (seen.has(pairKey))
                    continue;
                const titleA = memA.title || '';
                const titleB = memB.title || '';
                const contentA = memA.content || '';
                const contentB = memB.content || '';
                const titleSimilarity = levenshteinSimilarity(titleA.toLowerCase(), titleB.toLowerCase());
                const sharedWords = sharedWordCount(titleA, titleB);
                const titlesMatch = titleSimilarity > 0.7 || sharedWords >= 3 || titleA.toLowerCase() === titleB.toLowerCase();
                if (!titlesMatch)
                    continue;
                const contentOverlap = wordOverlap(contentA, contentB);
                if (contentOverlap <= 0.5)
                    continue;
                const salienceA = memA.salience || 0;
                const salienceB = memB.salience || 0;
                const createdA = new Date(memA.created_at || 0).getTime();
                const createdB = new Date(memB.created_at || 0).getTime();
                const recommendedKeepId = salienceA > salienceB || (salienceA === salienceB && createdA >= createdB)
                    ? idA
                    : idB;
                pairs.push({
                    memoryA: rowToMemory(memA),
                    memoryB: rowToMemory(memB),
                    recommendedKeepId,
                    similarity: `title=${titleSimilarity.toFixed(2)}, content=${contentOverlap.toFixed(2)}`,
                    titleSimilarity,
                    contentOverlap,
                    sharedWords,
                });
                seen.add(pairKey);
            }
        }
    }
    return pairs;
}
/**
 * Find memories with very similar titles/content and merge them.
 * Keeps the newer one, appends unique content from the older one.
 */
export function deduplicateMemories(options) {
    const dryRun = options?.dryRun ?? false;
    return withTransaction(() => {
        const db = getDatabase();
        const pairs = [];
        const removed = new Set();
        for (const pair of findDuplicateMemoryPairs()) {
            if (removed.has(pair.memoryA.id) || removed.has(pair.memoryB.id))
                continue;
            const removedId = pair.recommendedKeepId === pair.memoryA.id ? pair.memoryB.id : pair.memoryA.id;
            if (!dryRun) {
                const kept = pair.recommendedKeepId === pair.memoryA.id ? pair.memoryA : pair.memoryB;
                const discarded = pair.recommendedKeepId === pair.memoryA.id ? pair.memoryB : pair.memoryA;
                const keptSentences = new Set(kept.content.split(/[.!?\n]+/).map(s => s.trim().toLowerCase()).filter(s => s.length > 0));
                const uniqueSentences = discarded.content
                    .split(/[.!?\n]+/)
                    .map(s => s.trim())
                    .filter(s => s.length > 0 && !keptSentences.has(s.toLowerCase()));
                if (uniqueSentences.length > 0) {
                    const mergedContent = kept.content + '\n\nMerged from duplicate:\n' + uniqueSentences.join('. ') + '.';
                    db.prepare('UPDATE memories SET content = ? WHERE id = ?').run(mergedContent, kept.id);
                }
                deleteMemory(removedId);
            }
            removed.add(removedId);
            pairs.push({
                kept: pair.recommendedKeepId,
                removed: removedId,
                similarity: pair.similarity,
            });
        }
        return { merged: pairs.length, pairs };
    });
}
/**
 * Group memories by topic and create summary memories for large clusters.
 */
export function clusterAndSummarise(options) {
    const minSize = options?.minClusterSize ?? 5;
    return withTransaction(() => {
        const db = getDatabase();
        let clusters = 0;
        let summariesCreated = 0;
        // Get all LTM memories with their tags
        const ltmMemories = db.prepare("SELECT * FROM memories WHERE type = 'long_term' ORDER BY category ASC, created_at ASC").all();
        // Group by category + shared tags
        // Key: "category|tag1,tag2,..." (sorted tags)
        const tagGroups = new Map();
        for (const mem of ltmMemories) {
            let tags = [];
            try {
                tags = JSON.parse(mem.tags || '[]');
            }
            catch {
                // skip invalid tags
            }
            // Skip auto-summary memories from being clustered again
            const title = mem.title || '';
            if (title.startsWith('Summary:'))
                continue;
            const category = mem.category || 'note';
            const sortedTags = tags.filter(t => t !== 'auto-summary').sort();
            const key = `${category}|${sortedTags.join(',')}`;
            if (!tagGroups.has(key))
                tagGroups.set(key, []);
            tagGroups.get(key).push(mem);
        }
        for (const [key, group] of tagGroups) {
            if (group.length < minSize)
                continue;
            clusters++;
            const [category, tagsStr] = key.split('|');
            const commonTags = tagsStr || 'general';
            const summaryTitle = `Summary: ${category} — ${commonTags}`;
            // Check if a summary already exists for this cluster
            const existing = db.prepare("SELECT id FROM memories WHERE title = ? AND type = 'long_term'").get(summaryTitle);
            if (existing)
                continue;
            // Create bullet list of all memory titles
            const bulletList = group
                .map(m => `- ${m.title || 'Untitled'}`)
                .join('\n');
            const summaryContent = `Cluster of ${group.length} memories:\n${bulletList}`;
            // Determine project from group (use most common)
            const projectCounts = new Map();
            for (const m of group) {
                const proj = m.project || '';
                projectCounts.set(proj, (projectCounts.get(proj) || 0) + 1);
            }
            let bestProject = '';
            let bestCount = 0;
            for (const [proj, count] of projectCounts) {
                if (count > bestCount) {
                    bestProject = proj;
                    bestCount = count;
                }
            }
            addMemory({
                type: 'long_term',
                category: category,
                title: summaryTitle,
                content: summaryContent,
                project: bestProject || undefined,
                tags: ['auto-summary'],
                salience: 0.6,
            });
            summariesCreated++;
        }
        return { clusters, summariesCreated };
    });
}
/**
 * Enforce maximum memory limits
 * Removes lowest-priority memories when limits are exceeded
 */
export function enforceMemoryLimits(config = DEFAULT_CONFIG) {
    // Note: If called within consolidate(), this is already in a transaction
    // If called standalone, we wrap it for safety
    const db = getDatabase();
    let deleted = 0;
    // Check short-term memory limit
    const shortTermCount = db.prepare("SELECT COUNT(*) as count FROM memories WHERE type = 'short_term'").get().count;
    if (shortTermCount > config.maxShortTermMemories) {
        const toRemove = shortTermCount - config.maxShortTermMemories;
        const lowPriority = db.prepare(`
      SELECT id FROM memories
      WHERE type = 'short_term'
      ORDER BY salience ASC, last_accessed ASC
      LIMIT ?
    `).all(toRemove);
        for (const { id } of lowPriority) {
            deleteMemory(id);
            deleted++;
        }
    }
    // Check long-term memory limit (more lenient)
    const longTermCount = db.prepare("SELECT COUNT(*) as count FROM memories WHERE type = 'long_term'").get().count;
    if (longTermCount > config.maxLongTermMemories) {
        const toRemove = longTermCount - config.maxLongTermMemories;
        const lowPriority = db.prepare(`
      SELECT id FROM memories
      WHERE type = 'long_term'
      ORDER BY salience ASC, access_count ASC, last_accessed ASC
      LIMIT ?
    `).all(toRemove);
        for (const { id } of lowPriority) {
            deleteMemory(id);
            deleted++;
        }
    }
    return deleted;
}
/**
 * Find and merge similar short-term memories into coherent long-term entries.
 * Groups memories by project|category, then clusters by Jaccard similarity
 * on content (0.6 weight) + title (0.4 weight).
 * Returns count of deleted (merged) memories.
 */
export function mergeSimilarMemories(project, similarityThreshold = 0.25) {
    return withTransaction(() => {
        const db = getDatabase();
        let deleted = 0;
        // Step 1: Get all short-term memories, optionally filtered by project
        let sql = "SELECT * FROM memories WHERE type = 'short_term'";
        const params = [];
        if (project) {
            sql += ' AND project = ?';
            params.push(project);
        }
        sql += ' ORDER BY created_at ASC';
        const memories = db.prepare(sql).all(...params);
        // Step 2: Group by project|category
        const groups = new Map();
        for (const mem of memories) {
            const key = `${mem.project || ''}|${mem.category || ''}`;
            if (!groups.has(key))
                groups.set(key, []);
            groups.get(key).push(mem);
        }
        // Step 3: For each group with 2+ members, find clusters
        for (const [, group] of groups) {
            if (group.length < 2)
                continue;
            // Track which memories have been clustered
            const clustered = new Set();
            for (let i = 0; i < group.length; i++) {
                if (clustered.has(i))
                    continue;
                const cluster = [i]; // indices into group
                const memA = group[i];
                for (let j = i + 1; j < group.length; j++) {
                    if (clustered.has(j))
                        continue;
                    const memB = group[j];
                    const contentSim = jaccardSimilarity(memA.content, memB.content);
                    const titleSim = jaccardSimilarity(memA.title, memB.title);
                    const combinedSim = contentSim * 0.6 + titleSim * 0.4;
                    if (combinedSim >= similarityThreshold) {
                        cluster.push(j);
                    }
                }
                if (cluster.length < 2)
                    continue;
                // Mark all as clustered
                for (const idx of cluster)
                    clustered.add(idx);
                // Step 4: Merge cluster
                // Sort by salience desc, pick highest as base
                const clusterMems = cluster.map(idx => group[idx]);
                clusterMems.sort((a, b) => b.salience - a.salience);
                const base = clusterMems[0];
                const others = clusterMems.slice(1);
                // Merge content
                const bulletPoints = others
                    .map(m => `- ${m.title}: ${m.content}`)
                    .join('\n');
                const mergedContent = `${base.content}\n\nConsolidated context:\n${bulletPoints}`;
                // Merge tags (union)
                const allTags = new Set();
                for (const m of clusterMems) {
                    try {
                        const tags = JSON.parse(m.tags || '[]');
                        for (const t of tags)
                            allTags.add(t);
                    }
                    catch {
                        // skip invalid tags
                    }
                }
                // Sum access counts
                const totalAccessCount = clusterMems.reduce((sum, m) => sum + (m.access_count || 0), 0);
                // New salience: base + 0.1, capped at 1.0
                const newSalience = Math.min(1.0, base.salience + 0.1);
                // Update base memory
                db.prepare(`
          UPDATE memories
          SET type = 'long_term',
              content = ?,
              tags = ?,
              salience = ?,
              access_count = ?
          WHERE id = ?
        `).run(mergedContent, JSON.stringify([...allTags]), newSalience, totalAccessCount, base.id);
                // Delete others
                for (const other of others) {
                    deleteMemory(other.id);
                    deleted++;
                }
            }
        }
        return deleted;
    });
}
/**
 * Generate a context summary for session start
 * Provides a high-level view of relevant memories
 */
export async function generateContextSummary(project, config = DEFAULT_CONFIG) {
    // Get recent memories
    const recentMemories = getRecentMemories(10, project);
    // Get key architecture decisions
    const keyDecisions = (await searchMemories({
        query: '',
        project,
        category: 'architecture',
        minSalience: 0.6,
        limit: 5,
    }, config)).map(r => r.memory);
    // Get active patterns
    const activePatterns = (await searchMemories({
        query: '',
        project,
        category: 'pattern',
        minSalience: 0.5,
        limit: 5,
    }, config)).map(r => r.memory);
    // Get pending items
    const pendingItems = (await searchMemories({
        query: '',
        project,
        category: 'todo',
        limit: 10,
    }, config)).map(r => r.memory);
    return {
        project,
        recentMemories,
        keyDecisions,
        activePatterns,
        pendingItems,
    };
}
/**
 * Format context summary as a readable string
 */
export function formatContextSummary(summary) {
    const lines = [];
    if (summary.project) {
        lines.push(`## Project: ${summary.project}\n`);
    }
    if (summary.keyDecisions.length > 0) {
        lines.push('### Key Decisions');
        for (const memory of summary.keyDecisions) {
            lines.push(`- **${memory.title}**: ${memory.content.slice(0, 100)}${memory.content.length > 100 ? '...' : ''}`);
        }
        lines.push('');
    }
    if (summary.activePatterns.length > 0) {
        lines.push('### Active Patterns');
        for (const memory of summary.activePatterns) {
            lines.push(`- **${memory.title}**: ${memory.content.slice(0, 100)}${memory.content.length > 100 ? '...' : ''}`);
        }
        lines.push('');
    }
    if (summary.pendingItems.length > 0) {
        lines.push('### Pending Items');
        for (const memory of summary.pendingItems) {
            lines.push(`- [ ] ${memory.title}`);
        }
        lines.push('');
    }
    if (summary.recentMemories.length > 0) {
        lines.push('### Recent Context');
        for (const memory of summary.recentMemories.slice(0, 5)) {
            lines.push(`- ${memory.title} (${memory.category})`);
        }
    }
    return lines.join('\n');
}
/**
 * Start a new session
 * Creates a session record and returns relevant context
 */
export async function startSession(project) {
    const db = getDatabase();
    // Create session record
    const result = db.prepare(`
    INSERT INTO sessions (project) VALUES (?)
  `).run(project || null);
    const sessionId = result.lastInsertRowid;
    // Generate context summary
    const context = await generateContextSummary(project);
    return { sessionId, context };
}
/**
 * End a session
 * Updates session record with summary
 */
export function endSession(sessionId, summary) {
    const db = getDatabase();
    // Get counts from this session
    const stats = db.prepare(`
    SELECT
      (SELECT COUNT(*) FROM memories WHERE created_at >= s.started_at) as created,
      (SELECT COUNT(*) FROM memories WHERE last_accessed >= s.started_at) as accessed
    FROM sessions s WHERE s.id = ?
  `).get(sessionId);
    db.prepare(`
    UPDATE sessions
    SET ended_at = CURRENT_TIMESTAMP,
        summary = ?,
        memories_created = ?,
        memories_accessed = ?
    WHERE id = ?
  `).run(summary || null, stats?.created || 0, stats?.accessed || 0, sessionId);
}
/**
 * Get suggested context for the current query
 * Returns memories that might be relevant to what the user is working on
 */
export async function getSuggestedContext(currentContext, project, limit = 5) {
    // Search for relevant memories based on current context
    const results = await searchMemories({
        query: currentContext,
        project,
        minSalience: 0.4,
        limit,
        includeDecayed: false,
    });
    return results.map(r => r.memory);
}
/**
 * Export memories as JSON (for backup/transfer)
 */
export function exportMemories(project) {
    const db = getDatabase();
    let sql = 'SELECT * FROM memories';
    const params = [];
    if (project) {
        sql += ' WHERE project = ?';
        params.push(project);
    }
    sql += ' ORDER BY created_at ASC';
    const rows = db.prepare(sql).all(...params);
    return JSON.stringify(rows, null, 2);
}
/**
 * Import memories from JSON
 */
export function importMemories(json) {
    // Wrap in transaction for atomic import
    return withTransaction(() => {
        const db = getDatabase();
        const memories = JSON.parse(json);
        const stmt = db.prepare(`
      INSERT INTO memories (type, category, title, content, project, tags, salience, metadata)
      VALUES (?, ?, ?, ?, ?, ?, ?, ?)
    `);
        let imported = 0;
        for (const memory of memories) {
            try {
                stmt.run(memory.type, memory.category, memory.title, memory.content, memory.project || null, memory.tags || '[]', memory.salience || 0.5, memory.metadata || '{}');
                imported++;
            }
            catch {
                // Skip duplicates or invalid entries
            }
        }
        return imported;
    });
}
/**
 * Vacuum database to reclaim space after deletions
 * Run periodically or after major cleanup operations
 */
export function vacuumDatabase() {
    try {
        const db = getDatabase();
        db.exec('VACUUM');
        return { success: true, message: 'Database vacuumed successfully' };
    }
    catch (error) {
        return { success: false, message: `Vacuum failed: ${error}` };
    }
}
/**
 * Preview what consolidation would do without actually doing it
 * Useful for dry-run mode
 */
export function previewConsolidation(config = DEFAULT_CONFIG) {
    const db = getDatabase();
    // Get all short-term memories
    const shortTermMemories = getMemoriesByType('short_term', config.maxShortTermMemories * 2);
    // Get episodic memories too
    const episodicMemories = getMemoriesByType('episodic', 100);
    // Process decay to see what would happen
    const { toDelete: deleteIds, toPromote: promoteIds } = processDecay([...shortTermMemories, ...episodicMemories], config);
    // Map IDs back to memories
    const allMemories = [...shortTermMemories, ...episodicMemories];
    const toPromote = allMemories.filter(m => promoteIds.includes(m.id));
    const toDelete = allMemories.filter(m => deleteIds.includes(m.id) && !promoteIds.includes(m.id));
    // Get counts
    const totalShortTerm = db.prepare("SELECT COUNT(*) as count FROM memories WHERE type = 'short_term'").get().count;
    const totalLongTerm = db.prepare("SELECT COUNT(*) as count FROM memories WHERE type = 'long_term'").get().count;
    return { toPromote, toDelete, totalShortTerm, totalLongTerm };
}
/**
 * Check if consolidation should be triggered based on memory state
 * Returns true if consolidation is recommended
 */
export function shouldTriggerConsolidation(config = DEFAULT_CONFIG) {
    const stats = getMemoryStats();
    const stmFullness = stats.shortTerm / config.maxShortTermMemories;
    // Trigger early when approaching capacity
    if (stmFullness > 0.8) {
        return {
            shouldRun: true,
            reason: `Short-term memory at ${Math.round(stmFullness * 100)}% capacity`,
        };
    }
    // Check if many memories are below threshold
    const db = getDatabase();
    const lowScoreCount = db.prepare(`
    SELECT COUNT(*) as count FROM memories
    WHERE type = 'short_term' AND decayed_score < ?
  `).get(config.salienceThreshold).count;
    if (lowScoreCount > 10) {
        return {
            shouldRun: true,
            reason: `${lowScoreCount} memories below salience threshold`,
        };
    }
    return { shouldRun: false, reason: 'No consolidation needed' };
}
/**
 * Full cleanup: consolidate + vacuum
 * Best run periodically to keep database healthy
 */
export function fullCleanup(config = DEFAULT_CONFIG) {
    // Run consolidation
    const consolidation = consolidate(config);
    // Merge similar memories
    const merged = mergeSimilarMemories();
    // Expire old quarantine items
    let quarantineExpired = 0;
    try {
        const { expireQuarantineItems } = require('../defence/quarantine/auto-expire.js');
        quarantineExpired = expireQuarantineItems();
    }
    catch { /* defence module may not be available */ }
    // Vacuum if we deleted anything
    let vacuumed = false;
    if (consolidation.deleted > 0 || merged > 0) {
        const vacResult = vacuumDatabase();
        vacuumed = vacResult.success;
    }
    return { consolidation, vacuumed, merged, quarantineExpired };
}
/**
 * "Dream Mode" — comprehensive memory consolidation.
 *
 * 1. Find near-duplicates by embedding similarity >0.9, merge them
 * 2. Flag memories >30 days old with no access as archival candidates
 * 3. Detect and flag contradictions
 */
export function consolidateMemories() {
    const db = getDatabase();
    let nearDuplicatesMerged = 0;
    let archivalCandidates = 0;
    let contradictionsDetected = 0;
    // Step 1: Find and merge near-duplicates
    const duplicatePairs = findDuplicateMemoryPairs({ limit: 100 });
    for (const pair of duplicatePairs) {
        try {
            const memA = pair.memoryA;
            const memB = pair.memoryB;
            if (!memA || !memB)
                continue;
            // Keep the recommended one, delete the other
            const removedId = pair.recommendedKeepId === memA.id ? memB.id : memA.id;
            const keptId = pair.recommendedKeepId;
            // Merge tags from both
            const kept = keptId === memA.id ? memA : memB;
            const removed = keptId === memA.id ? memB : memA;
            const mergedTags = [...new Set([...kept.tags, ...removed.tags])];
            db.prepare('UPDATE memories SET tags = ? WHERE id = ?')
                .run(JSON.stringify(mergedTags), keptId);
            deleteMemory(removedId);
            nearDuplicatesMerged++;
        }
        catch {
            // Skip problematic pairs
        }
    }
    // Step 2: Flag stale memories for archival (>30 days, no access)
    const thirtyDaysAgo = new Date(Date.now() - 30 * 86_400_000).toISOString();
    const staleRows = db.prepare(`
    SELECT id FROM memories
    WHERE status = 'active'
      AND last_accessed < ?
      AND access_count <= 1
      AND pinned = 0
  `).all(thirtyDaysAgo);
    for (const row of staleRows) {
        try {
            db.prepare("UPDATE memories SET status = 'archived', updated_at = CURRENT_TIMESTAMP WHERE id = ?")
                .run(row.id);
            archivalCandidates++;
        }
        catch {
            // Skip
        }
    }
    // Step 3: Detect contradictions
    try {
        const contradictions = detectContradictions();
        if (contradictions.length > 0) {
            contradictionsDetected = linkContradictions(contradictions);
        }
    }
    catch {
        // Contradiction detection may fail on empty/small DBs
    }
    const totalProcessed = duplicatePairs.length + staleRows.length;
    return {
        nearDuplicatesMerged,
        archivalCandidates,
        contradictionsDetected,
        totalProcessed,
    };
}
