/**
 * Memory Store
 *
 * Core CRUD operations for the memory database.
 * Handles storage, retrieval, and management of memories.
 */
import { getDatabase } from '../database/init.js';
import { DEFAULT_CONFIG, } from './types.js';
import { calculateSalience, suggestCategory, extractTags, } from './salience.js';
import { calculateDecayedScore, calculateReinforcementBoost, calculatePriority, } from './decay.js';
import { activateMemory as spreadActivation, getActivationBoost, pruneActivationCache, } from './activation.js';
import { jaccardSimilarity } from './similarity.js';
import { emitMemoryCreated, emitMemoryAccessed, emitMemoryDeleted, emitMemoryUpdated, persistEvent, } from '../api/events.js';
import { generateEmbedding, cosineSimilarity } from '../embeddings/index.js';
import { isPaused } from '../api/control.js';
import { extractFromMemory } from '../graph/extract.js';
import { processExtractionResult } from '../graph/resolve.js';
import { runDefencePipeline, storeFragmentationData, syncQuarantineToCloud } from '../defence/index.js';
import { checkAccess } from '../defence/trust/access-control.js';
import { scoreSource } from '../defence/trust/source-scorer.js';
import { logAudit } from '../defence/audit/logger.js';
// Anti-bloat: Maximum content size per memory (10KB)
const MAX_CONTENT_SIZE = 10 * 1024;
// Track truncation info globally for the last addMemory call
let lastTruncationInfo = null;
/**
 * Truncate content if it exceeds max size (in bytes, not characters)
 * Uses byte length to handle multi-byte Unicode correctly (emoji, CJK, etc.)
 * Returns both the content and truncation info
 */
function truncateContent(content) {
    const byteLength = Buffer.byteLength(content, 'utf-8');
    if (byteLength > MAX_CONTENT_SIZE) {
        // Binary search to find the right truncation point in bytes
        // Reserve 50 bytes for the truncation message
        const targetBytes = MAX_CONTENT_SIZE - 50;
        let low = 0;
        let high = content.length;
        while (low < high) {
            const mid = Math.floor((low + high + 1) / 2);
            if (Buffer.byteLength(content.slice(0, mid), 'utf-8') <= targetBytes) {
                low = mid;
            }
            else {
                high = mid - 1;
            }
        }
        return {
            content: content.slice(0, low) + '\n\n[Content truncated - exceeded 10KB limit]',
            wasTruncated: true,
            originalLength: byteLength,
        };
    }
    return { content, wasTruncated: false, originalLength: byteLength };
}
/**
 * Get truncation info from the last addMemory call
 */
export function getLastTruncationInfo() {
    return lastTruncationInfo;
}
/**
 * Escape FTS5 query to prevent syntax errors
 * FTS5 interprets:
 * - "word-word" as "column:value" syntax
 * - AND, OR, NOT as boolean operators
 * - &, | as boolean operators
 * We quote individual terms to search them literally
 */
function escapeFts5Query(query) {
    // Split on whitespace, process each term, filter empty, rejoin
    return query
        .split(/\s+/)
        .filter(term => term.length > 0)
        .map(term => {
        // FTS5 boolean operators - quote them to search literally
        const upperTerm = term.toUpperCase();
        if (upperTerm === 'AND' || upperTerm === 'OR' || upperTerm === 'NOT') {
            return `"${term}"`;
        }
        // If term contains special FTS5 characters, quote it
        // Including: - : * ^ ( ) & | . , ; ! ? and quotes
        if (/[^a-zA-Z0-9_]/.test(term)) {
            // Escape existing quotes and wrap in quotes
            return `"${term.replace(/"/g, '""')}"`;
        }
        return term;
    })
        .join(' ');
}
/**
 * Safely parse JSON with fallback — prevents corrupted DB values from crashing queries
 */
function safeJsonParse(value, fallback) {
    if (!value)
        return fallback;
    try {
        return JSON.parse(value);
    }
    catch {
        return fallback;
    }
}
/**
 * Convert database row to Memory object
 */
export function rowToMemory(row) {
    return {
        id: row.id,
        type: row.type,
        category: row.category,
        title: row.title,
        content: row.content,
        project: row.project,
        tags: safeJsonParse(row.tags, []),
        salience: row.salience,
        accessCount: row.access_count,
        lastAccessed: new Date(row.last_accessed),
        createdAt: new Date(row.created_at),
        decayedScore: row.decayed_score ?? row.salience,
        metadata: safeJsonParse(row.metadata, {}),
        embedding: row.embedding,
        scope: row.scope ?? 'project',
        transferable: Boolean(row.transferable),
    };
}
/**
 * Detect if memory content suggests global applicability
 * Used to auto-set scope to 'global' for transferable knowledge
 */
function detectGlobalPattern(content, category, tags) {
    const globalCategories = ['pattern', 'preference', 'learning'];
    const globalKeywords = ['always', 'never', 'best practice', 'general rule', 'universal'];
    const globalTags = ['universal', 'global', 'general', 'cross-project'];
    if (globalCategories.includes(category))
        return true;
    if (globalKeywords.some(k => content.toLowerCase().includes(k)))
        return true;
    if (tags.some(t => globalTags.includes(t.toLowerCase())))
        return true;
    return false;
}
// ── Rate Limiter ──
// Tracks write attempts per source to prevent audit log flooding
const RATE_LIMIT_WINDOW_MS = 60_000; // 1 minute
const RATE_LIMIT_MAX = 20; // max writes per window per source
const rateLimitMap = new Map();
let rateLimitCheckCount = 0;
function pruneRateLimitMap() {
    const now = Date.now();
    for (const [key, entry] of rateLimitMap) {
        if (now - entry.windowStart > RATE_LIMIT_WINDOW_MS * 2) {
            rateLimitMap.delete(key);
        }
    }
}
function checkRateLimit(source) {
    // Prune stale entries every 50 checks
    if (++rateLimitCheckCount % 50 === 0) {
        pruneRateLimitMap();
    }
    const key = `${source.type}:${source.identifier}`;
    const now = Date.now();
    const entry = rateLimitMap.get(key);
    if (!entry || now - entry.windowStart > RATE_LIMIT_WINDOW_MS) {
        rateLimitMap.set(key, { count: 1, windowStart: now });
        return true;
    }
    entry.count++;
    if (entry.count > RATE_LIMIT_MAX) {
        return false; // rate limited
    }
    return true;
}
// ── Read-Time Access Control ──
function logAccessDenial(memoryId, source, reason) {
    const trust = scoreSource(source).score;
    logAudit({
        memory_id: memoryId,
        project: null,
        timestamp: new Date().toISOString(),
        source_type: source.type,
        source_identifier: source.identifier,
        trust_score: trust,
        sensitivity_level: 'INTERNAL',
        firewall_result: 'BLOCK',
        anomaly_score: 0,
        threat_indicators: '[]',
        blocked_patterns: '[]',
        reason: `Access denied: ${reason}`,
        fragmentation_score: null,
        pipeline_duration_ms: 0,
    });
}
/**
 * Filter raw DB rows by access control before converting to Memory objects.
 * Returns only rows the source is allowed to read.
 */
function filterRowsByAccess(rows, source) {
    return rows.filter(row => {
        const policy = checkAccess({
            id: row.id,
            source: row.source,
            sensitivity_level: row.sensitivity_level,
        }, source, 'read');
        if (!policy.canRead) {
            logAccessDenial(row.id, source, policy.reason);
            return false;
        }
        return true;
    });
}
/**
 * Error thrown when memory creation is paused
 */
export class MemoryPausedError extends Error {
    constructor() {
        super('Memory creation is currently paused. Use the dashboard to resume.');
        this.name = 'MemoryPausedError';
    }
}
/**
 * Error thrown when memory is blocked by the defence firewall
 */
export class MemoryBlockedError extends Error {
    constructor(reason) {
        super(`Memory blocked by defence firewall: ${reason}`);
        this.name = 'MemoryBlockedError';
    }
}
/**
 * Store a blocked memory in quarantine for later review
 */
function quarantineMemory(input, source, result) {
    try {
        const db = getDatabase();
        db.prepare(`INSERT INTO quarantine (original_title, original_content, project, source_type, source_identifier, reason, threat_indicators, anomaly_score, firewall_result, audit_id, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending')`)
            .run(input.title, input.content, input.project ?? null, source.type, source.identifier, result.firewall.reason, JSON.stringify(result.firewall.threatIndicators), result.firewall.anomalyScore, result.firewall.result, result.auditId);
        // Sync quarantine entry to cloud (fire-and-forget)
        const indicators = result.firewall.threatIndicators.map(t => typeof t === 'string' ? t : t.pattern ?? String(t));
        syncQuarantineToCloud({
            original_content: input.content,
            original_title: input.title,
            source_type: source.type,
            source_identifier: source.identifier,
            reason: result.firewall.reason,
            threat_indicators: indicators,
            anomaly_score: result.firewall.anomalyScore,
            firewall_result: result.firewall.result,
        });
    }
    catch (e) {
        console.error('[shieldcortex] Failed to quarantine memory:', e);
    }
}
/**
 * Add a new memory
 */
export function addMemory(input, config = DEFAULT_CONFIG, source) {
    // Check if memory creation is paused
    if (isPaused()) {
        throw new MemoryPausedError();
    }
    // RATE LIMITING: Block sources that exceed write rate
    if (source && !checkRateLimit(source)) {
        logAudit({
            memory_id: null,
            project: input.project ?? null,
            timestamp: new Date().toISOString(),
            source_type: source.type,
            source_identifier: source.identifier,
            trust_score: scoreSource(source).score,
            sensitivity_level: 'INTERNAL',
            firewall_result: 'BLOCK',
            anomaly_score: 1.0,
            threat_indicators: JSON.stringify(['rate_limit_exceeded']),
            blocked_patterns: '[]',
            reason: `Rate limited: exceeded ${RATE_LIMIT_MAX} writes/minute`,
            fragmentation_score: null,
            pipeline_duration_ms: 0,
        });
        throw new MemoryBlockedError(`Rate limited: exceeded ${RATE_LIMIT_MAX} writes per minute`);
    }
    // DEFENCE PIPELINE: Scan content before storage
    let defenceResult = null;
    if (source) {
        defenceResult = runDefencePipeline(input.content, input.title, source, undefined, input.project);
        // Auto-quarantine sub-agent writes (trust 0.5–0.7)
        const trust = defenceResult.trust.score;
        if (defenceResult.allowed && trust >= 0.5 && trust < 0.7) {
            defenceResult.allowed = false;
            defenceResult.firewall.result = 'QUARANTINE';
            defenceResult.firewall.reason = `Sub-agent write (trust=${trust.toFixed(3)}) requires parent approval`;
        }
        if (!defenceResult.allowed) {
            // Store in quarantine instead of memory
            quarantineMemory(input, source, defenceResult);
            throw new MemoryBlockedError(defenceResult.firewall.reason);
        }
    }
    const db = getDatabase();
    // Calculate salience if not provided
    const salience = input.salience ?? calculateSalience(input);
    // Suggest category if not provided
    const category = input.category ?? suggestCategory(input);
    // Extract tags
    const tags = extractTags(input);
    // Determine type
    const type = input.type ?? (salience >= config.consolidationThreshold ? 'long_term' : 'short_term');
    // Determine scope and transferable flag for cross-project knowledge
    const scope = input.scope ??
        (detectGlobalPattern(input.content, category, tags) ? 'global' : 'project');
    const transferable = input.transferable ?? (scope === 'global' ? 1 : 0);
    const stmt = db.prepare(`
    INSERT INTO memories (type, category, title, content, project, tags, salience, metadata, scope, transferable)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  `);
    // Anti-bloat: Truncate content if too large
    const truncationResult = truncateContent(input.content);
    // Store truncation info for the remember tool to access
    lastTruncationInfo = {
        wasTruncated: truncationResult.wasTruncated,
        originalLength: truncationResult.originalLength,
        truncatedLength: truncationResult.content.length,
    };
    // Transaction: INSERT + defence UPDATE must be atomic to prevent wrong trust scores
    const insertedId = db.transaction(() => {
        const result = stmt.run(type, category, input.title, truncationResult.content, input.project || null, JSON.stringify(tags), salience, JSON.stringify(input.metadata || {}), scope, transferable);
        if (defenceResult) {
            db.prepare(`UPDATE memories SET trust_score = ?, sensitivity_level = ?, source = ? WHERE id = ?`)
                .run(defenceResult.trust.score, defenceResult.sensitivity.level, `${source.type}:${source.identifier}`, result.lastInsertRowid);
        }
        return result.lastInsertRowid;
    })();
    const memory = getMemoryById(insertedId);
    // Emit event for real-time dashboard (in-process)
    emitMemoryCreated(memory);
    // Persist event for cross-process IPC (MCP → Dashboard)
    persistEvent('memory_created', { memory });
    // ORGANIC FEATURE: Auto-link to related memories
    // This builds the knowledge graph automatically as memories are created
    try {
        const relationships = detectRelationships(memory);
        for (const rel of relationships.slice(0, 3)) { // Top 3 most relevant
            createMemoryLink(memory.id, rel.targetId, rel.relationship, rel.strength);
        }
    }
    catch (e) {
        // Don't fail memory creation if linking fails
        console.error('[shieldcortex] Auto-link failed:', e);
    }
    // ONTOLOGY: Extract entities and triples from this memory
    try {
        const extraction = extractFromMemory(input.title, truncationResult.content, category);
        if (extraction.entities.length > 0) {
            processExtractionResult(extraction, memory.id);
        }
    }
    catch (e) {
        console.error('[shieldcortex] Entity extraction failed:', e);
    }
    // DEFENCE: Store fragmentation data for cross-memory payload detection
    if (source) {
        try {
            storeFragmentationData(memory.id, truncationResult.content);
        }
        catch (e) {
            console.warn('[shieldcortex] Fragmentation data storage failed:', e instanceof Error ? e.message : e);
        }
    }
    // SEMANTIC SEARCH: Generate embedding asynchronously (don't block INSERT)
    const memoryId = memory.id;
    generateEmbedding(input.title + ' ' + truncationResult.content)
        .then(embedding => {
        try {
            // Validate embedding exists and has expected structure
            if (embedding && embedding.buffer) {
                db.prepare('UPDATE memories SET embedding = ? WHERE id = ?')
                    .run(Buffer.from(embedding.buffer), memoryId);
            }
            else {
                console.warn('[shieldcortex] Embedding generation returned invalid result for memory', memoryId);
            }
        }
        catch (e) {
            console.error('[shieldcortex] Failed to store embedding:', e);
        }
    })
        .catch(e => {
        console.error('[shieldcortex] Failed to generate embedding:', e);
    });
    // Anti-bloat: Check if limits exceeded and trigger async cleanup
    // We use setImmediate to not block the insert response
    setImmediate(() => {
        try {
            const stats = getMemoryStats();
            if (stats.shortTerm > config.maxShortTermMemories ||
                stats.longTerm > config.maxLongTermMemories) {
                // Import dynamically to avoid circular dependency
                import('./consolidate.js').then(({ enforceMemoryLimits }) => {
                    if (typeof enforceMemoryLimits === 'function') {
                        enforceMemoryLimits(config);
                    }
                }).catch((e) => {
                    // Log but don't fail - consolidation will happen on next scheduled run
                    console.warn('[shieldcortex] Async cleanup import failed:', e instanceof Error ? e.message : e);
                });
            }
        }
        catch (e) {
            // Log unexpected errors in async cleanup
            console.warn('[shieldcortex] Async cleanup check failed:', e instanceof Error ? e.message : e);
        }
    });
    return memory;
}
/**
 * Get a memory by ID
 */
export function getMemoryById(id, source) {
    const db = getDatabase();
    const row = db.prepare('SELECT * FROM memories WHERE id = ?').get(id);
    if (!row)
        return null;
    // ACCESS CONTROL: Check read permission
    if (source) {
        const policy = checkAccess({ id: row.id, source: row.source, sensitivity_level: row.sensitivity_level }, source, 'read');
        if (!policy.canRead) {
            logAccessDenial(id, source, policy.reason);
            return null;
        }
    }
    return rowToMemory(row);
}
/**
 * Update a memory
 */
export function updateMemory(id, updates) {
    const db = getDatabase();
    const existing = getMemoryById(id);
    if (!existing)
        return null;
    const fields = [];
    const values = [];
    if (updates.title !== undefined) {
        fields.push('title = ?');
        values.push(updates.title);
    }
    if (updates.content !== undefined) {
        fields.push('content = ?');
        values.push(updates.content);
    }
    if (updates.type !== undefined) {
        fields.push('type = ?');
        values.push(updates.type);
    }
    if (updates.category !== undefined) {
        fields.push('category = ?');
        values.push(updates.category);
    }
    if (updates.project !== undefined) {
        fields.push('project = ?');
        values.push(updates.project);
    }
    if (updates.tags !== undefined) {
        fields.push('tags = ?');
        values.push(JSON.stringify(updates.tags));
    }
    if (updates.salience !== undefined) {
        fields.push('salience = ?');
        values.push(updates.salience);
    }
    if (updates.metadata !== undefined) {
        fields.push('metadata = ?');
        values.push(JSON.stringify(updates.metadata));
    }
    if (fields.length === 0)
        return existing;
    values.push(id);
    db.prepare(`UPDATE memories SET ${fields.join(', ')} WHERE id = ?`).run(...values);
    const updatedMemory = getMemoryById(id);
    // Emit event for real-time dashboard (in-process)
    emitMemoryUpdated(updatedMemory);
    // Persist event for cross-process IPC (MCP → Dashboard)
    persistEvent('memory_updated', { memory: updatedMemory });
    return updatedMemory;
}
/**
 * Delete a memory
 */
export function deleteMemory(id, source) {
    const db = getDatabase();
    // ACCESS CONTROL: Check delete permission
    if (source) {
        const row = db.prepare('SELECT id, source, sensitivity_level FROM memories WHERE id = ?').get(id);
        if (row) {
            const policy = checkAccess({ id: row.id, source: row.source, sensitivity_level: row.sensitivity_level }, source, 'delete');
            if (!policy.canDelete) {
                logAccessDenial(id, source, policy.reason);
                return false;
            }
        }
    }
    // Get memory before deletion for event
    const memory = getMemoryById(id);
    const result = db.prepare('DELETE FROM memories WHERE id = ?').run(id);
    // Emit event for real-time dashboard (in-process)
    if (result.changes > 0 && memory) {
        emitMemoryDeleted(id, memory.title);
        // Persist event for cross-process IPC (MCP → Dashboard)
        persistEvent('memory_deleted', { memoryId: id, title: memory.title });
    }
    return result.changes > 0;
}
/**
 * Access a memory (updates access count and timestamp, returns reinforced memory)
 */
export function accessMemory(id, config = DEFAULT_CONFIG, source) {
    const db = getDatabase();
    const memory = getMemoryById(id, source);
    if (!memory)
        return null;
    // Calculate new salience with reinforcement
    const newSalience = calculateReinforcementBoost(memory, config);
    db.prepare(`
    UPDATE memories
    SET access_count = access_count + 1,
        last_accessed = CURRENT_TIMESTAMP,
        salience = ?
    WHERE id = ?
  `).run(newSalience, id);
    const updatedMemory = getMemoryById(id);
    // Emit event for real-time dashboard (in-process)
    emitMemoryAccessed(updatedMemory, newSalience);
    // Persist event for cross-process IPC (MCP → Dashboard)
    persistEvent('memory_accessed', { memoryId: id, memory: updatedMemory, newSalience });
    // ORGANIC FEATURE: Link strengthening on co-access
    // If memory A and B are both accessed within 5 minutes, strengthen their link
    // This mimics Hebbian learning: "neurons that fire together, wire together"
    try {
        const recentlyAccessed = db.prepare(`
      SELECT id FROM memories
      WHERE last_accessed > datetime('now', '-5 minutes')
        AND id != ?
      LIMIT 10
    `).all(id);
        for (const recent of recentlyAccessed) {
            // Check if link exists in either direction
            const existingLink = db.prepare(`
        SELECT id, strength FROM memory_links
        WHERE (source_id = ? AND target_id = ?) OR (source_id = ? AND target_id = ?)
      `).get(id, recent.id, recent.id, id);
            if (existingLink) {
                // Strengthen existing link (cap at 1.0)
                const newStrength = Math.min(1.0, existingLink.strength + 0.05);
                db.prepare('UPDATE memory_links SET strength = ? WHERE id = ?')
                    .run(newStrength, existingLink.id);
            }
            else {
                // Create new weak link for co-accessed memories
                createMemoryLink(id, recent.id, 'related', 0.2);
            }
        }
    }
    catch (e) {
        // Don't fail memory access if link strengthening fails
        console.error('[shieldcortex] Link strengthening failed:', e);
    }
    // ORGANIC FEATURE: Spreading Activation (Phase 2)
    // Activate this memory and spread activation to linked memories
    // This makes related memories easier to recall in subsequent searches
    spreadActivation(id);
    return updatedMemory;
}
/**
 * Reinforce a memory that appeared in search results
 * Gives a small salience boost with diminishing returns based on access count.
 * ORGANIC FEATURE: Searched memories get reinforced, closing the feedback loop
 * so frequently-found memories grow stronger over time.
 */
export function reinforceFromSearch(memoryId) {
    const db = getDatabase();
    const memory = db.prepare('SELECT salience, access_count FROM memories WHERE id = ?').get(memoryId);
    if (!memory)
        return;
    // Small salience boost per search appearance (diminishing returns)
    const boost = Math.max(0.005, 0.02 / (1 + memory.access_count * 0.1));
    const newSalience = Math.min(1.0, memory.salience + boost);
    db.prepare(`
    UPDATE memories
    SET last_accessed = CURRENT_TIMESTAMP,
        access_count = access_count + 1,
        salience = ?
    WHERE id = ?
  `).run(newSalience, memoryId);
}
// ============================================
// ORGANIC FEATURE: Memory Enrichment (Phase 3)
// ============================================
// Enrichment configuration
const ENRICHMENT_SIMILARITY_THRESHOLD = 0.3; // Min similarity to trigger enrichment
const ENRICHMENT_COOLDOWN_HOURS = 1; // Don't enrich same memory within 1 hour
const MAX_ENRICHMENT_SIZE = 2000; // Max chars to add per enrichment
// Track last enrichment times (in-memory, ephemeral like activation cache)
const enrichmentTimestamps = new Map();
let enrichmentCallCount = 0;
function pruneEnrichmentTimestamps() {
    const now = Date.now();
    const cooldownMs = ENRICHMENT_COOLDOWN_HOURS * 60 * 60 * 1000;
    for (const [memoryId, timestamp] of enrichmentTimestamps) {
        if (now - timestamp > cooldownMs) {
            enrichmentTimestamps.delete(memoryId);
        }
    }
}
/**
 * Enrich a memory with additional context
 *
 * This adds timestamped context to a memory when:
 * 1. The new context is sufficiently related but different (new information)
 * 2. The memory hasn't been enriched recently (cooldown)
 * 3. The content won't exceed the size limit
 *
 * ORGANIC FEATURE: Memories grow with new context over time,
 * mimicking how human memories are reconsolidated with new information
 *
 * @param memoryId - ID of the memory to enrich
 * @param newContext - New context to add
 * @param contextType - Type of context ('search' | 'access' | 'related')
 * @returns EnrichmentResult indicating success or failure with reason
 */
export function enrichMemory(memoryId, newContext, contextType = 'access') {
    // Prune stale cooldown entries every 100 calls
    if (++enrichmentCallCount % 100 === 0) {
        pruneEnrichmentTimestamps();
    }
    const db = getDatabase();
    const memory = getMemoryById(memoryId);
    if (!memory) {
        return { enriched: false, reason: 'Memory not found' };
    }
    // Check cooldown
    const lastEnrichment = enrichmentTimestamps.get(memoryId);
    const now = Date.now();
    if (lastEnrichment && (now - lastEnrichment) < ENRICHMENT_COOLDOWN_HOURS * 60 * 60 * 1000) {
        return { enriched: false, reason: 'Enrichment cooldown active' };
    }
    // Check similarity - should be related but not too similar (new info)
    const similarity = jaccardSimilarity(memory.content, newContext);
    if (similarity > 0.8) {
        return { enriched: false, reason: 'Context too similar (no new information)' };
    }
    if (similarity < ENRICHMENT_SIMILARITY_THRESHOLD) {
        return { enriched: false, reason: 'Context not sufficiently related' };
    }
    // Truncate context if needed
    const truncatedContext = newContext.slice(0, MAX_ENRICHMENT_SIZE);
    // Build enrichment block with timestamp
    const timestamp = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
    const enrichmentBlock = `\n\n---\n[${timestamp}] ${contextType}: ${truncatedContext}`;
    // Check size limit (leave 500 char buffer for future enrichments)
    const newContent = memory.content + enrichmentBlock;
    if (newContent.length > MAX_CONTENT_SIZE - 500) {
        return { enriched: false, reason: 'Content size limit reached' };
    }
    // Update memory
    db.prepare(`
    UPDATE memories
    SET content = ?,
        last_accessed = CURRENT_TIMESTAMP
    WHERE id = ?
  `).run(newContent, memoryId);
    // Update cooldown timestamp
    enrichmentTimestamps.set(memoryId, now);
    // Emit update event for dashboard
    const updatedMemory = getMemoryById(memoryId);
    emitMemoryUpdated(updatedMemory);
    return { enriched: true, reason: `Added ${contextType} context (${truncatedContext.length} chars)` };
}
/**
 * Clear enrichment cooldown for a memory (for testing)
 */
export function clearEnrichmentCooldown(memoryId) {
    enrichmentTimestamps.delete(memoryId);
}
/**
 * Get enrichment cooldown status for a memory
 */
export function getEnrichmentCooldownStatus(memoryId) {
    const lastEnrichment = enrichmentTimestamps.get(memoryId);
    if (!lastEnrichment) {
        return { onCooldown: false, remainingMs: 0 };
    }
    const cooldownMs = ENRICHMENT_COOLDOWN_HOURS * 60 * 60 * 1000;
    const elapsed = Date.now() - lastEnrichment;
    const remaining = Math.max(0, cooldownMs - elapsed);
    return {
        onCooldown: remaining > 0,
        remainingMs: remaining,
    };
}
/**
 * Update persisted decay scores for all memories
 * Called during consolidation and periodically by the API server
 * Returns the number of memories updated
 */
export function updateDecayScores() {
    const db = getDatabase();
    // Get all memories
    const memories = db.prepare('SELECT * FROM memories').all();
    let updated = 0;
    const updateStmt = db.prepare('UPDATE memories SET decayed_score = ? WHERE id = ?');
    for (const row of memories) {
        const memory = rowToMemory(row);
        const decayedScore = calculateDecayedScore(memory);
        // Only update if score has changed significantly (saves writes)
        const currentScore = row.decayed_score;
        if (currentScore === null || Math.abs(currentScore - decayedScore) > 0.01) {
            updateStmt.run(decayedScore, memory.id);
            updated++;
        }
    }
    return updated;
}
/**
 * Detect the likely category a query is asking about
 */
function detectQueryCategory(query) {
    const lower = query.toLowerCase();
    if (/architect|design|structure|pattern|system|schema|model/.test(lower)) {
        return 'architecture';
    }
    if (/error|bug|fix|issue|crash|exception|fail|problem/.test(lower)) {
        return 'error';
    }
    if (/prefer|always|never|style|convention|like|want/.test(lower)) {
        return 'preference';
    }
    if (/learn|discover|realiz|found\s+out|turns?\s+out/.test(lower)) {
        return 'learning';
    }
    if (/todo|task|pending|need\s+to|should\s+do/.test(lower)) {
        return 'todo';
    }
    if (/relation|depend|connect|link|reference/.test(lower)) {
        return 'relationship';
    }
    return null;
}
/**
 * Calculate a boost for memories linked to high-salience memories
 */
function calculateLinkBoost(memoryId, db) {
    try {
        // Get linked memories and their salience
        const linked = db.prepare(`
      SELECT m.salience, ml.strength
      FROM memory_links ml
      JOIN memories m ON (m.id = ml.target_id OR m.id = ml.source_id)
      WHERE (ml.source_id = ? OR ml.target_id = ?)
        AND m.id != ?
    `).all(memoryId, memoryId, memoryId);
        if (linked.length === 0)
            return 0;
        // Calculate weighted average of linked memory salience
        const totalWeight = linked.reduce((sum, l) => sum + l.strength, 0);
        if (totalWeight === 0)
            return 0;
        const weightedSalience = linked.reduce((sum, l) => sum + l.salience * l.strength, 0) / totalWeight;
        // Cap boost at 0.15
        return Math.min(0.15, weightedSalience * 0.2);
    }
    catch {
        return 0;
    }
}
/**
 * Calculate partial tag match score
 */
function calculateTagScore(queryTags, memoryTags) {
    if (queryTags.length === 0 || memoryTags.length === 0)
        return 0;
    // Count partial matches (substring matching)
    let matches = 0;
    for (const qt of queryTags) {
        const qtLower = qt.toLowerCase();
        if (memoryTags.some(mt => mt.toLowerCase().includes(qtLower) || qtLower.includes(mt.toLowerCase()))) {
            matches++;
        }
    }
    return (matches / queryTags.length) * 0.1;
}
/**
 * Extract potential tags from a query string
 */
function extractQueryTags(query) {
    // Extract words that might be tags (tech terms, project-specific terms)
    const words = query.toLowerCase().split(/\s+/);
    return words.filter(w => w.length > 2 &&
        /^[a-z][a-z0-9-]*$/.test(w) &&
        !['the', 'and', 'for', 'with', 'how', 'what', 'when', 'where', 'why'].includes(w));
}
/**
 * Search memories by vector similarity
 * Returns memories sorted by cosine similarity to the query embedding
 */
function vectorSearch(queryEmbedding, limit, project, includeGlobal = true) {
    const db = getDatabase();
    // Get memories with embeddings
    let query = `
    SELECT * FROM memories
    WHERE embedding IS NOT NULL
  `;
    const params = [];
    if (project && includeGlobal) {
        query += ` AND (project = ? OR scope = 'global')`;
        params.push(project);
    }
    else if (project) {
        query += ` AND project = ?`;
        params.push(project);
    }
    const rows = db.prepare(query).all(...params);
    // Calculate similarities
    const results = rows
        .map(row => {
        const embeddingBuffer = row.embedding;
        const embedding = new Float32Array(embeddingBuffer.buffer, embeddingBuffer.byteOffset, embeddingBuffer.length / 4);
        const similarity = cosineSimilarity(queryEmbedding, embedding);
        return {
            memory: rowToMemory(row),
            similarity,
        };
    })
        .filter(r => r.similarity > 0.3) // Threshold for relevance
        .sort((a, b) => b.similarity - a.similarity)
        .slice(0, limit);
    return results;
}
/**
 * Search memories using full-text search, vector similarity, and filters
 * Now uses hybrid search combining FTS5 keywords with semantic vector matching
 */
let searchCount = 0;
export async function searchMemories(options, config = DEFAULT_CONFIG, source) {
    if (++searchCount % 100 === 0) {
        pruneActivationCache();
    }
    const db = getDatabase();
    const limit = options.limit || 20;
    const includeGlobal = options.includeGlobal ?? true;
    // Detect query category for boosting
    const detectedCategory = options.query ? detectQueryCategory(options.query) : null;
    const queryTags = options.query ? extractQueryTags(options.query) : [];
    // SEMANTIC SEARCH: Generate query embedding (may fail on first call while model loads)
    let queryEmbedding = null;
    let vectorResults = new Map(); // memoryId -> similarity
    if (options.query && options.query.trim()) {
        try {
            queryEmbedding = await generateEmbedding(options.query);
            const vectorHits = vectorSearch(queryEmbedding, limit * 2, options.project, includeGlobal);
            for (const hit of vectorHits) {
                vectorResults.set(hit.memory.id, hit.similarity);
            }
        }
        catch (e) {
            // Vector search unavailable - fall back to FTS only
            console.log('[shieldcortex] Vector search unavailable, using FTS only');
        }
    }
    let sql;
    const params = [];
    if (options.query && options.query.trim()) {
        // Use FTS search - escape query to prevent FTS5 syntax errors
        // FTS5 interprets "word-word" as "column:value", so we quote terms
        const escapedQuery = escapeFts5Query(options.query.trim());
        sql = `
      SELECT m.*, fts.rank
      FROM memories m
      JOIN memories_fts fts ON m.id = fts.rowid
      WHERE memories_fts MATCH ?
    `;
        params.push(escapedQuery);
    }
    else {
        // No query, just filter
        sql = `SELECT *, 0 as rank FROM memories m WHERE 1=1`;
    }
    // Add filters - include global memories if enabled
    if (options.project) {
        if (includeGlobal) {
            sql += ` AND (m.project = ? OR m.scope = 'global')`;
        }
        else {
            sql += ' AND m.project = ?';
        }
        params.push(options.project);
    }
    if (options.category) {
        sql += ' AND m.category = ?';
        params.push(options.category);
    }
    if (options.type) {
        sql += ' AND m.type = ?';
        params.push(options.type);
    }
    if (options.minSalience) {
        sql += ' AND m.salience >= ?';
        params.push(options.minSalience);
    }
    if (options.tags && options.tags.length > 0) {
        // Use json_each() for proper JSON array parsing
        // This avoids false positives from LIKE matching (e.g., "api" matching "api-gateway")
        const tagPlaceholders = options.tags.map(() => '?').join(',');
        sql += ` AND EXISTS (
      SELECT 1 FROM json_each(m.tags)
      WHERE json_each.value IN (${tagPlaceholders})
    )`;
        params.push(...options.tags);
    }
    sql += ' ORDER BY m.salience DESC, m.last_accessed DESC LIMIT ?';
    params.push(limit);
    const rows = db.prepare(sql).all(...params);
    // Convert to SearchResult with computed scores
    const results = rows.map(row => {
        const memory = rowToMemory(row);
        const decayedScore = calculateDecayedScore(memory, config);
        memory.decayedScore = decayedScore;
        // Improved FTS score normalization (BM25-style)
        // FTS5 rank is negative, closer to 0 = better match
        const rawRank = row.rank;
        const ftsScore = rawRank ? 1 / (1 + Math.abs(rawRank)) : 0.3;
        // Recency boost for recently accessed memories
        const hoursSinceAccess = (Date.now() - new Date(memory.lastAccessed).getTime()) / (1000 * 60 * 60);
        const recencyBoost = hoursSinceAccess < 1 ? 0.1 : (hoursSinceAccess < 24 ? 0.05 : 0);
        // Category match bonus
        const categoryBoost = detectedCategory && memory.category === detectedCategory ? 0.1 : 0;
        // Link boost - memories connected to high-salience memories rank higher
        const linkBoost = calculateLinkBoost(memory.id, db);
        // Partial tag match bonus
        const tagBoost = calculateTagScore(queryTags, memory.tags);
        // ORGANIC FEATURE: Spreading Activation boost (Phase 2)
        // Recently accessed memories and their linked neighbors get a boost
        const activationBoost = getActivationBoost(memory.id);
        // SEMANTIC SEARCH: Vector similarity boost (Phase 5)
        // If memory was found by vector search, add similarity as a boost
        const vectorSimilarity = vectorResults.get(memory.id) || 0;
        const vectorBoost = vectorSimilarity * 0.3; // 30% weight for vector similarity
        // Combined relevance score (adjusted weights to accommodate vector)
        const relevanceScore = (ftsScore * 0.25 + // Reduced from 0.3
            vectorBoost + // New: 0-0.3 from vector similarity
            decayedScore * 0.2 + // Reduced from 0.25
            calculatePriority(memory) * 0.05 + // Reduced from 0.1
            recencyBoost + categoryBoost + linkBoost + tagBoost + activationBoost);
        return { memory, relevanceScore };
    });
    // Sort by relevance and filter out too-decayed memories
    const sortedResults = results
        .filter(r => options.includeDecayed || r.memory.decayedScore >= config.salienceThreshold)
        .sort((a, b) => b.relevanceScore - a.relevanceScore);
    // ORGANIC FEATURE: Reinforce top search results
    // This closes the feedback loop - memories that appear in searches get a small
    // salience boost with diminishing returns, so useful memories grow stronger
    const topResults = sortedResults.slice(0, 5);
    for (const result of topResults) {
        reinforceFromSearch(result.memory.id);
    }
    // ORGANIC FEATURE: Link co-returned search results (top 5 only)
    // Memories that frequently appear together in searches become linked,
    // building the knowledge graph from usage patterns
    if (topResults.length >= 2) {
        for (let i = 0; i < topResults.length; i++) {
            for (let j = i + 1; j < topResults.length; j++) {
                const idA = topResults[i].memory.id;
                const idB = topResults[j].memory.id;
                const existing = db.prepare('SELECT strength FROM memory_links WHERE (source_id = ? AND target_id = ?) OR (source_id = ? AND target_id = ?)').get(idA, idB, idB, idA);
                if (existing) {
                    const newStrength = Math.min(1.0, existing.strength + 0.03);
                    db.prepare('UPDATE memory_links SET strength = ? WHERE (source_id = ? AND target_id = ?) OR (source_id = ? AND target_id = ?)').run(newStrength, idA, idB, idB, idA);
                }
                else {
                    try {
                        db.prepare('INSERT INTO memory_links (source_id, target_id, relationship, strength) VALUES (?, ?, ?, ?)').run(idA, idB, 'related', 0.2);
                    }
                    catch (e) {
                        // Only ignore UNIQUE constraint violations (expected for existing links)
                        if (!(e instanceof Error && e.message.includes('UNIQUE constraint'))) {
                            console.warn('[shieldcortex] Unexpected error linking co-returned memories:', e);
                        }
                    }
                }
            }
        }
    }
    // ORGANIC FEATURE: Enrich top result with search context
    // When a search query contains significant new information not already in the
    // top result, append it as enrichment context so memories accumulate knowledge
    if (sortedResults.length > 0 && options.query && options.query.length > 30) {
        const topResult = sortedResults[0];
        const queryWords = new Set(options.query.toLowerCase().split(/\s+/).filter(w => w.length > 3));
        const contentWords = new Set(topResult.memory.content.toLowerCase().split(/\s+/));
        const newWords = [...queryWords].filter(w => !contentWords.has(w));
        // Only enrich if query has significant new content (>30% new words)
        if (newWords.length > queryWords.size * 0.3 && options.query.length > 50) {
            try {
                enrichMemory(topResult.memory.id, options.query, 'search');
            }
            catch { /* enrichment is best-effort */ }
        }
    }
    // Look up contradictions for top results
    const finalResults = sortedResults.slice(0, limit);
    for (const result of finalResults) {
        const contradictions = db.prepare(`
      SELECT ml.strength,
        CASE WHEN ml.source_id = ? THEN ml.target_id ELSE ml.source_id END as other_id
      FROM memory_links ml
      WHERE ml.relationship = 'contradicts'
        AND (ml.source_id = ? OR ml.target_id = ?)
    `).all(result.memory.id, result.memory.id, result.memory.id);
        if (contradictions.length > 0) {
            result.contradictions = contradictions.map(c => {
                const other = db.prepare('SELECT title FROM memories WHERE id = ?').get(c.other_id);
                return { memoryId: c.other_id, title: other?.title || 'Unknown', score: c.strength };
            });
        }
    }
    // ACCESS CONTROL: Filter results by source trust level
    if (source) {
        const db2 = getDatabase();
        return finalResults.filter(result => {
            const row = db2.prepare('SELECT source, sensitivity_level FROM memories WHERE id = ?').get(result.memory.id);
            const policy = checkAccess({ id: result.memory.id, source: row?.source, sensitivity_level: row?.sensitivity_level }, source, 'read');
            if (!policy.canRead) {
                logAccessDenial(result.memory.id, source, policy.reason);
                return false;
            }
            return true;
        });
    }
    return finalResults;
}
/**
 * Get all memories for a project
 */
export function getProjectMemories(project, config = DEFAULT_CONFIG) {
    const db = getDatabase();
    const rows = db.prepare(`
    SELECT * FROM memories
    WHERE project = ?
    ORDER BY salience DESC, last_accessed DESC
  `).all(project);
    return rows.map(row => {
        const memory = rowToMemory(row);
        memory.decayedScore = calculateDecayedScore(memory, config);
        return memory;
    });
}
/**
 * Get recent memories
 */
export function getRecentMemories(limit = 10, project, source) {
    const db = getDatabase();
    let sql = 'SELECT * FROM memories';
    const params = [];
    if (project) {
        sql += ' WHERE project = ?';
        params.push(project);
    }
    sql += ' ORDER BY last_accessed DESC LIMIT ?';
    params.push(source ? limit * 2 : limit); // over-fetch when filtering
    let rows = db.prepare(sql).all(...params);
    if (source)
        rows = filterRowsByAccess(rows, source);
    return rows.slice(0, limit).map(rowToMemory);
}
/**
 * Get memories by type
 */
export function getMemoriesByType(type, limit = 50) {
    const db = getDatabase();
    const rows = db.prepare(`
    SELECT * FROM memories
    WHERE type = ?
    ORDER BY salience DESC, last_accessed DESC
    LIMIT ?
  `).all(type, limit);
    return rows.map(rowToMemory);
}
/**
 * Get high-priority memories (for context injection)
 */
export function getHighPriorityMemories(limit = 10, project, source) {
    const db = getDatabase();
    let sql = `
    SELECT * FROM memories
    WHERE salience >= 0.6
  `;
    const params = [];
    if (project) {
        sql += ' AND project = ?';
        params.push(project);
    }
    sql += ' ORDER BY salience DESC, last_accessed DESC LIMIT ?';
    params.push(source ? limit * 2 : limit);
    let rows = db.prepare(sql).all(...params);
    if (source)
        rows = filterRowsByAccess(rows, source);
    return rows.slice(0, limit).map(rowToMemory);
}
/**
 * Promote a memory from short-term to long-term
 */
export function promoteMemory(id) {
    const db = getDatabase();
    db.prepare(`
    UPDATE memories
    SET type = 'long_term'
    WHERE id = ? AND type = 'short_term'
  `).run(id);
    return getMemoryById(id);
}
/**
 * Bulk delete decayed memories
 */
export function cleanupDecayedMemories(config = DEFAULT_CONFIG) {
    const db = getDatabase();
    // Get all short-term memories and check decay
    const shortTerm = getMemoriesByType('short_term', 1000);
    const toDelete = [];
    for (const memory of shortTerm) {
        const decayedScore = calculateDecayedScore(memory, config);
        if (decayedScore < config.salienceThreshold) {
            toDelete.push(memory.id);
        }
    }
    if (toDelete.length > 0) {
        const placeholders = toDelete.map(() => '?').join(',');
        db.prepare(`DELETE FROM memories WHERE id IN (${placeholders})`).run(...toDelete);
    }
    return toDelete.length;
}
/**
 * Get memory statistics
 */
export function getMemoryStats(project) {
    const db = getDatabase();
    let whereClause = '';
    const params = [];
    if (project) {
        whereClause = 'WHERE project = ?';
        params.push(project);
    }
    const total = db.prepare(`SELECT COUNT(*) as count FROM memories ${whereClause}`).get(...params).count;
    const shortTerm = db.prepare(`SELECT COUNT(*) as count FROM memories ${whereClause} ${whereClause ? 'AND' : 'WHERE'} type = 'short_term'`).get(...params).count;
    const longTerm = db.prepare(`SELECT COUNT(*) as count FROM memories ${whereClause} ${whereClause ? 'AND' : 'WHERE'} type = 'long_term'`).get(...params).count;
    const episodic = db.prepare(`SELECT COUNT(*) as count FROM memories ${whereClause} ${whereClause ? 'AND' : 'WHERE'} type = 'episodic'`).get(...params).count;
    const avgResult = db.prepare(`SELECT AVG(salience) as avg FROM memories ${whereClause}`).get(...params);
    const averageSalience = avgResult.avg || 0;
    // Get counts by category
    const categoryRows = db.prepare(`
    SELECT category, COUNT(*) as count
    FROM memories ${whereClause}
    GROUP BY category
  `).all(...params);
    const byCategory = {};
    for (const row of categoryRows) {
        byCategory[row.category] = row.count;
    }
    return {
        total,
        shortTerm,
        longTerm,
        episodic,
        byCategory,
        averageSalience,
    };
}
/**
 * Create a link between two memories
 */
export function createMemoryLink(sourceId, targetId, relationship, strength = 0.5) {
    const db = getDatabase();
    // Verify both memories exist
    const source = getMemoryById(sourceId);
    const target = getMemoryById(targetId);
    if (!source || !target)
        return null;
    // Prevent self-links
    if (sourceId === targetId)
        return null;
    try {
        const result = db.prepare(`
      INSERT INTO memory_links (source_id, target_id, relationship, strength)
      VALUES (?, ?, ?, ?)
    `).run(sourceId, targetId, relationship, strength);
        return {
            id: result.lastInsertRowid,
            sourceId,
            targetId,
            relationship,
            strength,
            createdAt: new Date(),
        };
    }
    catch {
        // Link already exists (UNIQUE constraint)
        return null;
    }
}
/**
 * Get all memories related to a given memory
 */
export function getRelatedMemories(memoryId) {
    const db = getDatabase();
    // Get outgoing links (this memory references others)
    const outgoing = db.prepare(`
    SELECT m.*, ml.relationship, ml.strength
    FROM memory_links ml
    JOIN memories m ON m.id = ml.target_id
    WHERE ml.source_id = ?
  `).all(memoryId);
    // Get incoming links (other memories reference this one)
    const incoming = db.prepare(`
    SELECT m.*, ml.relationship, ml.strength
    FROM memory_links ml
    JOIN memories m ON m.id = ml.source_id
    WHERE ml.target_id = ?
  `).all(memoryId);
    const results = [];
    for (const row of outgoing) {
        results.push({
            memory: rowToMemory(row),
            relationship: row.relationship,
            strength: row.strength,
            direction: 'outgoing',
        });
    }
    for (const row of incoming) {
        results.push({
            memory: rowToMemory(row),
            relationship: row.relationship,
            strength: row.strength,
            direction: 'incoming',
        });
    }
    return results;
}
/**
 * Delete a memory link
 */
export function deleteMemoryLink(sourceId, targetId) {
    const db = getDatabase();
    const result = db.prepare(`
    DELETE FROM memory_links WHERE source_id = ? AND target_id = ?
  `).run(sourceId, targetId);
    return result.changes > 0;
}
/**
 * Get all memory links
 */
export function getAllMemoryLinks() {
    const db = getDatabase();
    const rows = db.prepare(`SELECT * FROM memory_links ORDER BY created_at DESC`).all();
    return rows.map(row => ({
        id: row.id,
        sourceId: row.source_id,
        targetId: row.target_id,
        relationship: row.relationship,
        strength: row.strength,
        createdAt: new Date(row.created_at),
    }));
}
/**
 * Detect tag-based links for a memory.
 * Finds memories sharing tags and scores by overlap count.
 */
function detectTagLinks(db, memory, maxResults) {
    const results = [];
    if (memory.tags.length > 0) {
        const tagPlaceholders = memory.tags.map(() => '?').join(',');
        const tagMatches = db.prepare(`
      SELECT DISTINCT m.id, m.tags
      FROM memories m, json_each(m.tags)
      WHERE json_each.value IN (${tagPlaceholders})
        AND m.id != ?
      LIMIT ?
    `).all(...memory.tags, memory.id, maxResults);
        for (const match of tagMatches) {
            const matchTags = JSON.parse(match.tags);
            const sharedCount = memory.tags.filter(t => matchTags.includes(t)).length;
            const strength = Math.min(0.9, 0.3 + (sharedCount * 0.2));
            results.push({ targetId: match.id, relationship: 'related', strength });
        }
    }
    return results;
}
/**
 * Detect embedding-based semantic links for a memory.
 * Computes cosine similarity against top memories that have embeddings.
 */
function detectEmbeddingLinks(db, memory, maxResults) {
    if (!memory.embedding)
        return [];
    const candidates = db.prepare(`
    SELECT id, embedding FROM memories
    WHERE embedding IS NOT NULL AND id != ?
    ORDER BY decayed_score DESC
    LIMIT 100
  `).all(memory.id);
    const results = [];
    const sourceEmbedding = new Float32Array(memory.embedding.buffer, memory.embedding.byteOffset, memory.embedding.byteLength / 4);
    for (const candidate of candidates) {
        const candidateEmbedding = new Float32Array(candidate.embedding.buffer, candidate.embedding.byteOffset, candidate.embedding.byteLength / 4);
        const similarity = cosineSimilarity(sourceEmbedding, candidateEmbedding);
        if (similarity >= 0.6) {
            results.push({
                targetId: candidate.id,
                relationship: 'related',
                strength: Math.min(0.9, similarity),
            });
        }
    }
    return results;
}
/**
 * Detect content-based links using FTS5 and Jaccard similarity.
 * Fallback when embeddings are not available.
 */
function detectFtsLinks(db, memory, maxResults) {
    const queryText = `${memory.title} ${memory.content.slice(0, 200)}`;
    const escapedQuery = escapeFts5Query(queryText);
    if (!escapedQuery.trim())
        return [];
    let ftsMatches;
    try {
        ftsMatches = db.prepare(`
      SELECT m.id, m.title, m.content
      FROM memories_fts fts
      JOIN memories m ON m.id = fts.rowid
      WHERE memories_fts MATCH ?
        AND m.id != ?
      LIMIT ?
    `).all(escapedQuery, memory.id, maxResults * 2);
    }
    catch {
        return [];
    }
    const results = [];
    for (const match of ftsMatches) {
        const matchText = `${match.title} ${match.content.slice(0, 200)}`;
        const sim = jaccardSimilarity(queryText, matchText);
        if (sim >= 0.3) {
            results.push({
                targetId: match.id,
                relationship: 'related',
                strength: Math.min(0.7, sim + 0.2),
            });
        }
    }
    return results;
}
/**
 * Detect potential relationships for a new memory
 * Uses three strategies in priority order:
 * 1. Tag-based linking (shared tags)
 * 2. Embedding-based semantic linking (cosine similarity >= 0.6)
 * 3. FTS content similarity fallback (Jaccard similarity >= 0.3)
 */
export function detectRelationships(memory, maxResults = 5) {
    const db = getDatabase();
    const seen = new Set();
    const results = [];
    function addResults(links) {
        for (const link of links) {
            if (!seen.has(link.targetId)) {
                seen.add(link.targetId);
                results.push(link);
            }
        }
    }
    // 1. Tag-based linking
    addResults(detectTagLinks(db, memory, maxResults));
    // 2. Embedding-based semantic linking
    addResults(detectEmbeddingLinks(db, memory, maxResults));
    // 3. FTS content similarity fallback (when no embeddings)
    if (!memory.embedding) {
        addResults(detectFtsLinks(db, memory, maxResults));
    }
    // Sort by strength descending and limit
    return results
        .sort((a, b) => b.strength - a.strength)
        .slice(0, maxResults);
}
//# sourceMappingURL=store.js.map