/**
 * Sleep-Time Consolidation — LLM-driven background memory hygiene for CLEO BRAIN.
 *
 * Implements the "sleep-time compute" pattern inspired by Letta OS: after a
 * session ends, a cheap LLM pass runs in the background to:
 *   1. Merge near-duplicate entries (embedding similarity > 0.85)
 *   2. Prune short-tier stale entries with low quality (7d old, quality < 0.4)
 *   3. Synthesize frequently-cited learnings into higher-quality patterns
 *   4. Extract cross-cutting insights from clusters of related observations
 *
 * All LLM calls use `claude-haiku-4-5-20251001` (cheapest available model).
 * No API key = silent no-op for LLM steps; structural steps still run.
 * All errors are caught and logged — nothing here may block session end.
 *
 * ## Configuration
 *
 * Add to `config.json` under `brain.sleepConsolidation`:
 * ```json
 * {
 *   "brain": {
 *     "sleepConsolidation": {
 *       "enabled": true
 *     }
 *   }
 * }
 * ```
 *
 * @task T555
 * @epic T549
 * @see packages/core/src/memory/observer-reflector.ts (Observer/Reflector pattern)
 * @see packages/core/src/memory/brain-lifecycle.ts (runConsolidation)
 */
import { randomBytes } from 'node:crypto';
import { getBrainNativeDb } from '../store/brain-sqlite.js';
import { typedAll } from '../store/typed-query.js';
import { resolveAnthropicApiKey } from './anthropic-key-resolver.js';
import { storeLearning } from './learnings.js';
import { storePattern } from './patterns.js';
// ============================================================================
// Constants
// ============================================================================
/** Cheap model for all sleep-consolidation LLM calls. */
const SLEEP_MODEL = 'claude-haiku-4-5-20251001';
/** Embedding similarity threshold above which two entries are considered duplicates. */
const DUPLICATE_SIMILARITY_THRESHOLD = 0.85;
/** Minimum age (days) before a short-tier entry can be pruned for low quality. */
const STALE_AGE_DAYS = 7;
/** Maximum quality score for a short-tier entry to be considered for pruning. */
const PRUNE_QUALITY_THRESHOLD = 0.4;
/** Minimum citation count to trigger pattern synthesis for a learning. */
const SYNTHESIS_CITATION_MIN = 3;
/** Maximum tokens for LLM responses. */
const MAX_RESPONSE_TOKENS = 1024;
/** Source tag written to brain_observations for sleep-consolidation results. */
const SLEEP_SOURCE = 'sleep-consolidation';
/**
 * Load sleep consolidation configuration from the project config.
 * Defaults to enabled=true when config is missing or unreadable.
 *
 * @param projectRoot - Project root directory.
 */
async function loadSleepConfig(projectRoot) {
    try {
        const { loadConfig } = await import('../config.js');
        const config = await loadConfig(projectRoot);
        const brain = config.brain;
        const sc = brain?.['sleepConsolidation'];
        return { enabled: sc?.['enabled'] !== false };
    }
    catch {
        return { enabled: true };
    }
}
/**
 * Call the Anthropic Messages API via native fetch using the cheap model.
 *
 * Uses `resolveAnthropicApiKey()` — never accesses ANTHROPIC_API_KEY directly.
 * Returns null when the key is unavailable or the call fails.
 *
 * @param systemPrompt - System instruction for the LLM.
 * @param userContent - User message content.
 */
async function callLlm(systemPrompt, userContent) {
    const apiKey = resolveAnthropicApiKey();
    if (!apiKey)
        return null;
    try {
        const response = await fetch('https://api.anthropic.com/v1/messages', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'x-api-key': apiKey,
                'anthropic-version': '2023-06-01',
            },
            body: JSON.stringify({
                model: SLEEP_MODEL,
                max_tokens: MAX_RESPONSE_TOKENS,
                system: systemPrompt,
                messages: [{ role: 'user', content: userContent }],
            }),
        });
        if (!response.ok) {
            const body = await response.text().catch(() => '');
            console.warn(`[sleep-consolidation] Anthropic API error ${response.status}: ${body.slice(0, 200)}`);
            return null;
        }
        const data = (await response.json());
        const textBlock = data.content.find((b) => b.type === 'text');
        return textBlock?.text ?? null;
    }
    catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        console.warn(`[sleep-consolidation] LLM call failed: ${msg}`);
        return null;
    }
}
/**
 * Attempt to parse LLM response as JSON. Strips markdown code fences before
 * parsing. Returns null on parse failure.
 */
function parseJson(text) {
    try {
        const cleaned = text
            .replace(/^```(?:json)?\s*/m, '')
            .replace(/\s*```\s*$/m, '')
            .trim();
        return JSON.parse(cleaned);
    }
    catch {
        return null;
    }
}
// ============================================================================
// Cosine similarity helper
// ============================================================================
/**
 * Compute cosine similarity between two Float32 embedding buffers.
 *
 * Returns 0 when either buffer is null, empty, or different lengths.
 * Embeddings are stored as raw Buffer of 4-byte floats (sqlite-vec format).
 *
 * @param a - First embedding buffer.
 * @param b - Second embedding buffer.
 */
function cosineSimilarity(a, b) {
    if (!a || !b || a.length === 0 || a.length !== b.length)
        return 0;
    const floatCount = Math.floor(a.length / 4);
    if (floatCount === 0)
        return 0;
    let dot = 0;
    let normA = 0;
    let normB = 0;
    for (let i = 0; i < floatCount; i++) {
        const va = a.readFloatLE(i * 4);
        const vb = b.readFloatLE(i * 4);
        dot += va * vb;
        normA += va * va;
        normB += vb * vb;
    }
    const denom = Math.sqrt(normA) * Math.sqrt(normB);
    return denom === 0 ? 0 : dot / denom;
}
// ============================================================================
// Step 1: Merge Duplicates
// ============================================================================
/**
 * Find near-duplicate entries using embedding cosine similarity > 0.85.
 *
 * For each pair above the threshold, asks the LLM whether to keep/merge.
 * The LLM confirms or overrides the merge decision. Kept entry gains the
 * evicted entry's citation count. Duplicates are soft-evicted (invalid_at set).
 *
 * Falls back to structural merge (keep higher quality) when no API key is
 * available or the LLM call fails.
 *
 * @param projectRoot - Project root for brain.db resolution.
 */
async function stepMergeDuplicates(projectRoot) {
    const { getBrainDb } = await import('../store/brain-sqlite.js');
    await getBrainDb(projectRoot);
    const nativeDb = getBrainNativeDb();
    if (!nativeDb)
        return { merged: 0, llmDecisions: 0 };
    let merged = 0;
    let llmDecisions = 0;
    const now = new Date().toISOString().replace('T', ' ').slice(0, 19);
    // Fetch observations that have embeddings and are active
    let rows;
    try {
        rows = typedAll(nativeDb.prepare(`
        SELECT id, title, narrative, quality_score, citation_count, memory_tier, created_at, embedding
        FROM brain_observations
        WHERE embedding IS NOT NULL
          AND invalid_at IS NULL
          AND memory_tier = 'short'
        ORDER BY quality_score DESC
        LIMIT 200
      `));
    }
    catch {
        return { merged: 0, llmDecisions: 0 };
    }
    if (rows.length < 2)
        return { merged: 0, llmDecisions: 0 };
    // Build candidate pairs above the similarity threshold
    const pairs = [];
    for (let i = 0; i < rows.length; i++) {
        for (let j = i + 1; j < rows.length; j++) {
            const sim = cosineSimilarity(rows[i].embedding, rows[j].embedding);
            if (sim >= DUPLICATE_SIMILARITY_THRESHOLD) {
                pairs.push({ a: rows[i], b: rows[j], similarity: sim });
            }
        }
    }
    if (pairs.length === 0)
        return { merged: 0, llmDecisions: 0 };
    // Ask the LLM for a batch merge decision (max 10 pairs per call)
    const pairBatch = pairs.slice(0, 10);
    const pairDescriptions = pairBatch.map(({ a, b, similarity }, idx) => ({
        pair: idx,
        similarity: Math.round(similarity * 100) / 100,
        a: { id: a.id, text: `${a.title ?? ''} ${a.narrative ?? ''}`.trim().slice(0, 120) },
        b: { id: b.id, text: `${b.title ?? ''} ${b.narrative ?? ''}`.trim().slice(0, 120) },
    }));
    const systemPrompt = 'You are a memory deduplication assistant. Given pairs of nearly-identical memory entries ' +
        'evaluate whether they should be merged. For each pair output: {"pair":N,"merge":true/false,"keep":"<id>"}. ' +
        'Output a JSON array only, no prose. Merge when content is substantially the same; keep when content is distinct.';
    const userContent = `Memory entry pairs to evaluate:\n${JSON.stringify(pairDescriptions, null, 2)}`;
    let decisions = [];
    const rawResponse = await callLlm(systemPrompt, userContent);
    if (rawResponse) {
        const parsed = parseJson(rawResponse);
        if (Array.isArray(parsed)) {
            decisions = parsed;
            llmDecisions = decisions.filter((d) => d.merge).length;
        }
    }
    // Apply decisions (structural fallback when LLM unavailable)
    const processedIds = new Set();
    for (let idx = 0; idx < pairBatch.length; idx++) {
        const { a, b } = pairBatch[idx];
        if (processedIds.has(a.id) || processedIds.has(b.id))
            continue;
        const decision = decisions.find((d) => d.pair === idx);
        const shouldMerge = decision ? decision.merge : true; // default: merge near-duplicates
        if (!shouldMerge)
            continue;
        // Determine which to keep: prefer LLM decision, fallback to higher quality
        let keepId;
        let evictId;
        if (decision?.keep === a.id || decision?.keep === b.id) {
            keepId = decision.keep;
            evictId = keepId === a.id ? b.id : a.id;
        }
        else {
            const aQ = a.quality_score ?? 0.5;
            const bQ = b.quality_score ?? 0.5;
            keepId = aQ >= bQ ? a.id : b.id;
            evictId = keepId === a.id ? b.id : a.id;
        }
        const keepRow = a.id === keepId ? a : b;
        const evictRow = a.id === evictId ? a : b;
        const combinedCitations = (keepRow.citation_count ?? 0) + (evictRow.citation_count ?? 0);
        try {
            nativeDb
                .prepare(`UPDATE brain_observations SET invalid_at = ?, updated_at = ? WHERE id = ?`)
                .run(now, now, evictId);
            if (combinedCitations > (keepRow.citation_count ?? 0)) {
                nativeDb
                    .prepare(`UPDATE brain_observations SET citation_count = ?, updated_at = ? WHERE id = ?`)
                    .run(combinedCitations, now, keepId);
            }
            merged++;
            processedIds.add(a.id);
            processedIds.add(b.id);
        }
        catch {
            /* best-effort */
        }
    }
    return { merged, llmDecisions };
}
// ============================================================================
// Step 2: Prune Stale Entries
// ============================================================================
/**
 * Prune short-tier entries older than STALE_AGE_DAYS with quality < PRUNE_QUALITY_THRESHOLD.
 *
 * Before evicting, asks the LLM whether any entries should be preserved despite
 * their low score. Preserved entries have their quality_score bumped to 0.5 so
 * they survive future prune passes.
 *
 * @param projectRoot - Project root for brain.db resolution.
 */
async function stepPruneStale(projectRoot) {
    const { getBrainDb } = await import('../store/brain-sqlite.js');
    await getBrainDb(projectRoot);
    const nativeDb = getBrainNativeDb();
    if (!nativeDb)
        return { pruned: 0, preserved: 0 };
    const staleCutoff = new Date(Date.now() - STALE_AGE_DAYS * 24 * 60 * 60 * 1000)
        .toISOString()
        .replace('T', ' ')
        .slice(0, 19);
    const now = new Date().toISOString().replace('T', ' ').slice(0, 19);
    let candidates;
    try {
        candidates = typedAll(nativeDb.prepare(`
        SELECT id, title, narrative, quality_score, citation_count, memory_tier, created_at, embedding
        FROM brain_observations
        WHERE memory_tier = 'short'
          AND invalid_at IS NULL
          AND quality_score IS NOT NULL
          AND quality_score < ?
          AND created_at < ?
        ORDER BY quality_score ASC
        LIMIT 50
      `), PRUNE_QUALITY_THRESHOLD, staleCutoff);
    }
    catch {
        return { pruned: 0, preserved: 0 };
    }
    if (candidates.length === 0)
        return { pruned: 0, preserved: 0 };
    // Ask LLM which entries to preserve despite low quality
    const candidateDescriptions = candidates.slice(0, 20).map((row) => ({
        id: row.id,
        age_days: Math.round((Date.now() - new Date(row.created_at ?? 0).getTime()) / (24 * 60 * 60 * 1000)),
        quality: Math.round((row.quality_score ?? 0) * 100) / 100,
        citations: row.citation_count,
        text: `${row.title ?? ''} ${row.narrative ?? ''}`.trim().slice(0, 100),
    }));
    const systemPrompt = 'You are a memory curator. Given a list of low-quality, stale memory entries, ' +
        'decide which ones are worth preserving (i.e. contain unique, non-redundant information ' +
        'that would be hard to reconstruct). Return a JSON array of IDs to preserve: {"preserve":["id1","id2",...]}. ' +
        'Only preserve entries with genuinely unique information. When in doubt, allow eviction.';
    const userContent = `Candidate entries for eviction:\n${JSON.stringify(candidateDescriptions, null, 2)}`;
    let preserveIds = new Set();
    const rawResponse = await callLlm(systemPrompt, userContent);
    if (rawResponse) {
        const parsed = parseJson(rawResponse);
        if (parsed && Array.isArray(parsed.preserve)) {
            preserveIds = new Set(parsed.preserve);
        }
    }
    let pruned = 0;
    let preserved = 0;
    for (const row of candidates) {
        if (preserveIds.has(row.id)) {
            // Bump quality so it won't be pruned next pass
            try {
                nativeDb
                    .prepare(`UPDATE brain_observations SET quality_score = 0.5, updated_at = ? WHERE id = ?`)
                    .run(now, row.id);
                preserved++;
            }
            catch {
                /* best-effort */
            }
        }
        else {
            try {
                nativeDb
                    .prepare(`UPDATE brain_observations SET invalid_at = ?, updated_at = ? WHERE id = ?`)
                    .run(now, now, row.id);
                pruned++;
            }
            catch {
                /* best-effort */
            }
        }
    }
    return { pruned, preserved };
}
// ============================================================================
// Step 3: Strengthen Patterns
// ============================================================================
/**
 * Find learnings cited >= SYNTHESIS_CITATION_MIN times and ask the LLM to
 * synthesize them into a single higher-quality pattern entry.
 *
 * The synthesized pattern is stored via storePattern() with
 * source='sleep-consolidation'. The original learnings are left intact.
 *
 * @param projectRoot - Project root for brain.db resolution.
 */
async function stepStrengthenPatterns(projectRoot) {
    const { getBrainDb } = await import('../store/brain-sqlite.js');
    await getBrainDb(projectRoot);
    const nativeDb = getBrainNativeDb();
    if (!nativeDb)
        return { synthesized: 0, patternsGenerated: 0 };
    let candidates;
    try {
        candidates = typedAll(nativeDb.prepare(`
        SELECT id, insight, confidence, citation_count, source, memory_tier
        FROM brain_learnings
        WHERE citation_count >= ?
          AND invalid_at IS NULL
        ORDER BY citation_count DESC, confidence DESC
        LIMIT 10
      `), SYNTHESIS_CITATION_MIN);
    }
    catch {
        return { synthesized: 0, patternsGenerated: 0 };
    }
    if (candidates.length === 0)
        return { synthesized: 0, patternsGenerated: 0 };
    // Check if we already have a sleep-consolidation pattern from these
    // (avoid re-synthesizing the same learnings every session)
    let existingPatterns;
    try {
        existingPatterns = typedAll(nativeDb.prepare(`
        SELECT id, pattern, context, impact, frequency, memory_tier
        FROM brain_patterns
        WHERE source_type = ?
          AND invalid_at IS NULL
        ORDER BY frequency DESC
        LIMIT 5
      `), SLEEP_SOURCE);
    }
    catch {
        existingPatterns = [];
    }
    const existingPatternTexts = existingPatterns.map((p) => p.pattern.slice(0, 80)).join('; ');
    const learningDescriptions = candidates.map((l) => ({
        id: l.id,
        insight: l.insight.slice(0, 200),
        citations: l.citation_count,
        confidence: Math.round(l.confidence * 100) / 100,
    }));
    const systemPrompt = 'You are a knowledge synthesizer. Given frequently-cited learnings, extract 1-3 ' +
        'higher-order patterns that capture the essence of what has been repeatedly confirmed. ' +
        'Each pattern should be actionable and generalizable. ' +
        'Return JSON: {"patterns":[{"pattern":"...","context":"...","impact":"high|medium|low"}]}. ' +
        'Skip patterns already captured in the existing list. Output JSON only, no prose.';
    const userContent = `Frequently-cited learnings to synthesize:\n${JSON.stringify(learningDescriptions, null, 2)}\n\n` +
        `Already captured patterns (do not duplicate): ${existingPatternTexts || 'none'}`;
    const rawResponse = await callLlm(systemPrompt, userContent);
    if (!rawResponse)
        return { synthesized: candidates.length, patternsGenerated: 0 };
    const parsed = parseJson(rawResponse);
    if (!parsed || !Array.isArray(parsed.patterns)) {
        return { synthesized: candidates.length, patternsGenerated: 0 };
    }
    let patternsGenerated = 0;
    for (const p of parsed.patterns) {
        if (!p.pattern?.trim())
            continue;
        try {
            const impact = p.impact === 'high' || p.impact === 'medium' || p.impact === 'low' ? p.impact : 'medium';
            await storePattern(projectRoot, {
                type: 'optimization',
                pattern: p.pattern.slice(0, 500),
                context: (p.context ?? '').slice(0, 500),
                impact,
                source: SLEEP_SOURCE,
            });
            patternsGenerated++;
        }
        catch {
            /* best-effort */
        }
    }
    return { synthesized: candidates.length, patternsGenerated };
}
// ============================================================================
// Step 4: Generate Cross-Cutting Insights
// ============================================================================
/**
 * Cluster recent observations by shared entity overlap and ask the LLM to
 * extract a cross-cutting insight for each cluster.
 *
 * Insights are stored as brain_observations with source='sleep-consolidation'
 * and memory_tier='medium' (they represent synthesized knowledge).
 *
 * @param projectRoot - Project root for brain.db resolution.
 */
async function stepGenerateInsights(projectRoot) {
    const { getBrainDb } = await import('../store/brain-sqlite.js');
    await getBrainDb(projectRoot);
    const nativeDb = getBrainNativeDb();
    if (!nativeDb)
        return { clustersProcessed: 0, insightsStored: 0 };
    // Fetch recent non-sleep observations (last 14 days)
    const recent14d = new Date(Date.now() - 14 * 24 * 60 * 60 * 1000)
        .toISOString()
        .replace('T', ' ')
        .slice(0, 19);
    let observations;
    try {
        observations = typedAll(nativeDb.prepare(`
        SELECT id,
               COALESCE(title, '') || ' ' || COALESCE(narrative, '') AS text
        FROM brain_observations
        WHERE created_at >= ?
          AND invalid_at IS NULL
          AND (source_type IS NULL OR source_type NOT IN (?, 'observer-compressed'))
        ORDER BY quality_score DESC
        LIMIT 60
      `), recent14d, SLEEP_SOURCE);
    }
    catch {
        return { clustersProcessed: 0, insightsStored: 0 };
    }
    if (observations.length < 5)
        return { clustersProcessed: 0, insightsStored: 0 };
    // Simple entity-based clustering: tokenise each observation into words >=4
    // chars, group observations sharing >= 3 tokens into the same cluster.
    const STOP = new Set([
        'this',
        'that',
        'with',
        'from',
        'have',
        'been',
        'will',
        'when',
        'then',
        'they',
        'were',
        'also',
        'into',
        'over',
        'some',
        'more',
        'very',
        'just',
        'each',
        'both',
    ]);
    function tokenize(text) {
        const tokens = text
            .toLowerCase()
            .replace(/[^a-z0-9\s]/g, ' ')
            .split(/\s+/)
            .filter((t) => t.length >= 4 && !STOP.has(t));
        return new Set(tokens);
    }
    const tokenSets = observations.map((o) => ({ id: o.id, text: o.text, tokens: tokenize(o.text) }));
    // Build clusters greedily (each observation joins the first compatible cluster)
    const clusters = [];
    for (const obs of tokenSets) {
        let placed = false;
        for (const cluster of clusters) {
            // Check overlap with the first member of the cluster
            const firstText = tokenSets.find((t) => t.id === cluster.memberIds[0]);
            if (!firstText)
                continue;
            let shared = 0;
            for (const tok of obs.tokens) {
                if (firstText.tokens.has(tok))
                    shared++;
            }
            if (shared >= 3) {
                cluster.memberIds.push(obs.id);
                cluster.texts.push(obs.text.slice(0, 120));
                placed = true;
                break;
            }
        }
        if (!placed && clusters.length < 5) {
            clusters.push({ memberIds: [obs.id], texts: [obs.text.slice(0, 120)] });
        }
    }
    // Only process clusters with >= 3 members
    const validClusters = clusters.filter((c) => c.memberIds.length >= 3);
    if (validClusters.length === 0)
        return { clustersProcessed: 0, insightsStored: 0 };
    const clusterDescriptions = validClusters.map((c, i) => ({
        cluster: i,
        entries: c.texts.slice(0, 5),
    }));
    const systemPrompt = 'You are a cross-domain insight extractor. Given clusters of related memory entries, ' +
        'identify one cross-cutting insight per cluster that would not be obvious from any single entry. ' +
        'Return JSON: {"insights":[{"cluster":N,"insight":"...","confidence":0.0-1.0}]}. ' +
        'Only include high-value insights (confidence >= 0.7). Output JSON only, no prose.';
    const userContent = `Memory clusters to analyse:\n${JSON.stringify(clusterDescriptions, null, 2)}`;
    const rawResponse = await callLlm(systemPrompt, userContent);
    if (!rawResponse)
        return { clustersProcessed: validClusters.length, insightsStored: 0 };
    const parsed = parseJson(rawResponse);
    if (!parsed || !Array.isArray(parsed.insights)) {
        return { clustersProcessed: validClusters.length, insightsStored: 0 };
    }
    let insightsStored = 0;
    const now = new Date().toISOString().replace('T', ' ').slice(0, 19);
    for (const insight of parsed.insights) {
        if (!insight.insight?.trim())
            continue;
        const confidence = Math.max(0, Math.min(1, insight.confidence ?? 0.7));
        if (confidence < 0.7)
            continue;
        try {
            // Store as a learning (cross-cutting insights are learnings, not observations)
            await storeLearning(projectRoot, {
                insight: insight.insight.slice(0, 500),
                source: SLEEP_SOURCE,
                confidence,
                actionable: true,
                application: 'Cross-cutting insight synthesized from clustered observations',
            });
            insightsStored++;
        }
        catch {
            /* best-effort */
        }
    }
    // Log the run itself as an observation for traceability
    if (insightsStored > 0) {
        try {
            const runId = `O-${randomBytes(4).toString('hex')}`;
            nativeDb
                .prepare(`
          INSERT INTO brain_observations
            (id, type, title, narrative, source_type, quality_score, memory_tier, created_at)
          VALUES (?, 'change', ?, ?, ?, 0.6, 'short', ?)
        `)
                .run(runId, `[sleep-consolidation] Insight generation run`, `Generated ${insightsStored} cross-cutting insights from ${validClusters.length} clusters.`, SLEEP_SOURCE, now);
        }
        catch {
            /* best-effort */
        }
    }
    return { clustersProcessed: validClusters.length, insightsStored };
}
// ============================================================================
// Main entry point
// ============================================================================
/**
 * Run the full sleep-time consolidation pipeline for CLEO BRAIN.
 *
 * This is the main entry point for LLM-driven background memory hygiene.
 * It is designed to run after session end (via setImmediate) and must never
 * throw — all errors are caught and logged.
 *
 * Steps (in order):
 *   1. Merge duplicates — embedding-similarity-based dedup with LLM confirmation
 *   2. Prune stale — evict low-quality short-tier entries; LLM may preserve some
 *   3. Strengthen patterns — synthesize frequently-cited learnings into patterns
 *   4. Generate insights — extract cross-cutting insights from observation clusters
 *
 * Graceful degradation: when no Anthropic API key is available, LLM steps
 * silently skip their LLM call and fall back to structural heuristics.
 *
 * @param projectRoot - Project root directory for brain.db resolution.
 * @returns Aggregated result counts from each step.
 */
export async function runSleepConsolidation(projectRoot) {
    const empty = {
        ran: false,
        mergeDuplicates: { merged: 0, llmDecisions: 0 },
        pruneStale: { pruned: 0, preserved: 0 },
        strengthenPatterns: { synthesized: 0, patternsGenerated: 0 },
        generateInsights: { clustersProcessed: 0, insightsStored: 0 },
    };
    // Check configuration
    let config;
    try {
        config = await loadSleepConfig(projectRoot);
    }
    catch {
        config = { enabled: true };
    }
    if (!config.enabled) {
        return empty;
    }
    const result = {
        ran: true,
        mergeDuplicates: { merged: 0, llmDecisions: 0 },
        pruneStale: { pruned: 0, preserved: 0 },
        strengthenPatterns: { synthesized: 0, patternsGenerated: 0 },
        generateInsights: { clustersProcessed: 0, insightsStored: 0 },
    };
    // Step 1: Merge duplicates
    try {
        result.mergeDuplicates = await stepMergeDuplicates(projectRoot);
    }
    catch (err) {
        console.warn('[sleep-consolidation] Step 1 (merge duplicates) failed:', err);
    }
    // Step 2: Prune stale
    try {
        result.pruneStale = await stepPruneStale(projectRoot);
    }
    catch (err) {
        console.warn('[sleep-consolidation] Step 2 (prune stale) failed:', err);
    }
    // Step 3: Strengthen patterns
    try {
        result.strengthenPatterns = await stepStrengthenPatterns(projectRoot);
    }
    catch (err) {
        console.warn('[sleep-consolidation] Step 3 (strengthen patterns) failed:', err);
    }
    // Step 4: Generate insights
    try {
        result.generateInsights = await stepGenerateInsights(projectRoot);
    }
    catch (err) {
        console.warn('[sleep-consolidation] Step 4 (generate insights) failed:', err);
    }
    return result;
}
//# sourceMappingURL=sleep-consolidation.js.map