/**
 * Memory Daemon - Background worker that processes interactions into memories.
 *
 * Watches interactions.jsonl for new entries from ALL avis sessions,
 * extracts memory-worthy facts, and updates the memory store + semantic trie.
 *
 * Runs as a non-blocking interval inside each avis process. Uses file-based
 * locking and checkpointing to coordinate across multiple instances.
 *
 * Processing pipeline:
 *   1. Read new interactions since last checkpoint
 *   2. Buffer interactions into conversation windows (user + assistant pairs)
 *   3. Extract memory-worthy facts using heuristics (no LLM required)
 *   4. Deduplicate against existing memories
 *   5. Create/update memories and index in semantic trie
 *   6. Periodically rebuild the knowledge graph
 */
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
import * as http from "node:http";
import { dirname } from "node:path";
import { getInteractionsPath, getMemoryCheckpointPath, getMemoryDir } from "../config.js";
import { SemanticTrie } from "./semantic-trie.js";
import { vault } from "./vault.js";
// ============================================================================
// MemoryDaemon
// ============================================================================
// ============================================================================
// Ollama LLM extraction
// ============================================================================
const EXTRACTION_PROMPT = `You are a memory extraction system for an AI assistant called Avis.

Given a conversation snippet between a user (Kaio) and Avis, extract any NEW facts worth remembering long-term. Focus on:
- User preferences (likes, dislikes, communication style)
- Project details (names, paths, tech stack, status)
- Personal facts (location, interests, background)
- Decisions made or conventions established
- Corrections or clarifications the user made

Rules:
- Output ONLY a JSON array of memory objects
- Each memory: {"content": "...", "tags": ["tag1", "tag2"]}
- Keep content concise, factual, one idea per memory
- Tags should be lowercase, descriptive (e.g., "preference", "project", "personal")
- If nothing is worth remembering, output: []
- Do NOT extract transient task details (e.g., "user asked to count to 3")
- Do NOT repeat information that's already in existing memories

Existing memories (do not duplicate):
{existing_memories}

Conversation to analyze:
{conversation}

Output JSON array only, no markdown fences:`;
function callOllama(prompt, model, ollamaHost) {
    return new Promise((resolve) => {
        const url = new URL(`${ollamaHost}/api/generate`);
        const payload = JSON.stringify({
            model,
            prompt,
            stream: false,
            options: { temperature: 0.1, num_predict: 1024 },
        });
        const req = http.request({
            hostname: url.hostname,
            port: url.port || 11434,
            path: url.pathname,
            method: "POST",
            headers: { "Content-Type": "application/json" },
            timeout: 120_000,
        }, (res) => {
            let body = "";
            res.on("data", (chunk) => {
                body += chunk;
            });
            res.on("end", () => {
                try {
                    const data = JSON.parse(body);
                    resolve(data.response?.trim() ?? null);
                }
                catch {
                    resolve(null);
                }
            });
        });
        req.on("error", () => resolve(null));
        req.on("timeout", () => {
            req.destroy();
            resolve(null);
        });
        req.write(payload);
        req.end();
    });
}
function parseOllamaMemories(response) {
    try {
        let text = response.trim();
        if (text.includes("```")) {
            const parts = text.split("```");
            text = parts[1] ?? text;
            if (text.startsWith("json"))
                text = text.slice(4);
            text = text.trim();
        }
        const parsed = JSON.parse(text);
        if (!Array.isArray(parsed))
            return [];
        return parsed.filter((m) => typeof m === "object" && m !== null && "content" in m && typeof m.content === "string");
    }
    catch {
        return [];
    }
}
export class MemoryDaemon {
    constructor(memoryManager, options) {
        this._timer = null;
        this._processing = false;
        this._memoryManager = memoryManager;
        this._interactionsPath = options?.interactionsPath ?? getInteractionsPath();
        this._checkpointPath = options?.checkpointPath ?? getMemoryCheckpointPath();
        this._intervalMs = options?.intervalMs ?? 30_000; // 30s default
        this._ollamaModel = options?.ollamaModel ?? process.env.AVIS_MEMORY_MODEL ?? null;
        this._ollamaHost = options?.ollamaHost ?? process.env.OLLAMA_HOST ?? "http://localhost:11434";
        const trieDir = options?.trieDir ?? getMemoryDir();
        this._trie = new SemanticTrie(trieDir);
        this._patterns = this._buildPatterns();
        // Index existing memories into trie on startup
        this._indexExistingMemories();
    }
    get trie() {
        return this._trie;
    }
    // =========================================================================
    // Lifecycle
    // =========================================================================
    /** Start the background processing loop */
    start() {
        if (this._timer)
            return;
        // Process immediately on start, then on interval
        this._tick();
        this._timer = setInterval(() => this._tick(), this._intervalMs);
        // Ensure interval doesn't keep process alive
        if (this._timer.unref)
            this._timer.unref();
    }
    /** Stop the background processing loop */
    stop() {
        if (this._timer) {
            clearInterval(this._timer);
            this._timer = null;
        }
        this._trie.flush();
    }
    /** Process new interactions immediately (non-blocking) */
    async processNow() {
        return this._process();
    }
    // =========================================================================
    // Core processing
    // =========================================================================
    _tick() {
        if (this._processing)
            return;
        this._process().catch(() => { });
    }
    async _process() {
        if (this._processing)
            return { newMemories: 0, updatedMemories: 0 };
        this._processing = true;
        try {
            if (!existsSync(this._interactionsPath)) {
                return { newMemories: 0, updatedMemories: 0 };
            }
            const checkpoint = this._loadCheckpoint();
            const fileSize = statSync(this._interactionsPath).size;
            // Nothing new
            if (fileSize <= checkpoint.offset) {
                return { newMemories: 0, updatedMemories: 0 };
            }
            // Read new interactions from checkpoint offset
            const content = vault.ready
                ? vault.readFileSync(this._interactionsPath)
                : readFileSync(this._interactionsPath, "utf-8");
            const newContent = content.slice(checkpoint.offset);
            const interactions = [];
            for (const line of newContent.split("\n")) {
                if (!line.trim())
                    continue;
                try {
                    interactions.push(JSON.parse(line));
                }
                catch {
                    // skip malformed
                }
            }
            if (interactions.length === 0) {
                this._saveCheckpoint({ ...checkpoint, offset: fileSize });
                return { newMemories: 0, updatedMemories: 0 };
            }
            // Build conversation windows (pair user + assistant messages)
            const windows = this._buildWindows(interactions);
            // Extract memories from each window
            let newMemories = 0;
            let updatedMemories = 0;
            for (const window of windows) {
                const results = this._extractMemories(window);
                for (const result of results) {
                    const outcome = this._applyMemory(result.content, result.tags);
                    if (outcome === "created")
                        newMemories++;
                    else if (outcome === "updated")
                        updatedMemories++;
                }
            }
            // Also extract session-state memories (things like counting, ongoing tasks)
            const stateResults = this._extractSessionState(interactions);
            for (const result of stateResults) {
                const outcome = this._applyMemory(result.content, result.tags);
                if (outcome === "created")
                    newMemories++;
                else if (outcome === "updated")
                    updatedMemories++;
            }
            // LLM-based extraction via Ollama (if enabled)
            if (this._ollamaModel) {
                const llmResults = await this._extractWithOllama(windows);
                for (const result of llmResults) {
                    const outcome = this._applyMemory(result.content, result.tags);
                    if (outcome === "created")
                        newMemories++;
                    else if (outcome === "updated")
                        updatedMemories++;
                }
            }
            // Save checkpoint
            this._saveCheckpoint({
                offset: fileSize,
                lastTimestamp: interactions[interactions.length - 1].timestamp,
                totalProcessed: checkpoint.totalProcessed + interactions.length,
            });
            // Flush trie to disk
            this._trie.flush();
            // Rebuild graph if we added memories (non-blocking)
            if (newMemories > 0 || updatedMemories > 0) {
                this._memoryManager.rebuildGraphIfNeeded().catch(() => { });
            }
            return { newMemories, updatedMemories };
        }
        finally {
            this._processing = false;
        }
    }
    // =========================================================================
    // Conversation windowing
    // =========================================================================
    _buildWindows(interactions) {
        const windows = [];
        let i = 0;
        while (i < interactions.length) {
            if (interactions[i].role === "user") {
                const userMsg = interactions[i];
                // Look for the next assistant response (same pid preferred)
                let assistantMsg = null;
                for (let j = i + 1; j < Math.min(i + 5, interactions.length); j++) {
                    if (interactions[j].role === "assistant") {
                        assistantMsg = interactions[j];
                        i = j + 1;
                        break;
                    }
                }
                if (assistantMsg) {
                    windows.push({
                        userMessage: userMsg.content,
                        assistantMessage: assistantMsg.content,
                        timestamp: userMsg.timestamp,
                        pid: userMsg.pid,
                    });
                }
                else {
                    // Unpaired user message - still process it
                    windows.push({
                        userMessage: userMsg.content,
                        assistantMessage: "",
                        timestamp: userMsg.timestamp,
                        pid: userMsg.pid,
                    });
                    i++;
                }
            }
            else {
                i++;
            }
        }
        return windows;
    }
    // =========================================================================
    // Memory extraction (heuristic, no LLM)
    // =========================================================================
    _extractMemories(window) {
        const results = [];
        for (const pattern of this._patterns) {
            try {
                if (pattern.test(window.userMessage, window.assistantMessage)) {
                    const extracted = pattern.extract(window.userMessage, window.assistantMessage);
                    if (extracted && extracted.content.length > 10 && extracted.content.length < 500) {
                        results.push(extracted);
                    }
                }
            }
            catch {
                // skip broken patterns
            }
        }
        return results;
    }
    /**
     * Extract session-state updates from raw interactions.
     * Looks for sequential patterns like counting, ongoing tasks, etc.
     */
    _extractSessionState(interactions) {
        const results = [];
        // Detect counting sequences
        const numbers = [];
        for (const interaction of interactions) {
            const trimmed = interaction.content.trim();
            if (/^\d+$/.test(trimmed)) {
                numbers.push(parseInt(trimmed, 10));
            }
        }
        if (numbers.length >= 2) {
            const lastNumber = numbers[numbers.length - 1];
            const isSequential = numbers.every((n, i) => i === 0 || n === numbers[i - 1] + 1);
            if (isSequential) {
                results.push({
                    content: `Counting test with Kaio: last number was ${lastNumber}. Continue from ${lastNumber + 1} if asked to keep counting.`,
                    tags: ["test", "counting", "session-state"],
                });
            }
        }
        return results;
    }
    /**
     * LLM-based extraction via Ollama.
     * Sends conversation windows to a local model for deeper memory extraction.
     */
    async _extractWithOllama(windows) {
        if (!this._ollamaModel || windows.length === 0)
            return [];
        // Format conversation
        const conversation = windows
            .map((w) => {
            const user = w.userMessage.slice(0, 2000);
            const assistant = w.assistantMessage.slice(0, 2000);
            return `[user] ${user}\n[assistant] ${assistant}`;
        })
            .join("\n");
        // Existing memories summary (last 50)
        const allMemories = this._memoryManager.list();
        const existingSummary = allMemories
            .slice(-50)
            .map((m) => `- ${m.content} [${m.tags.join(", ")}]`)
            .join("\n") || "(none yet)";
        const prompt = EXTRACTION_PROMPT.replace("{existing_memories}", existingSummary).replace("{conversation}", conversation);
        const response = await callOllama(prompt, this._ollamaModel, this._ollamaHost);
        if (!response)
            return [];
        return parseOllamaMemories(response);
    }
    _buildPatterns() {
        return [
            // Explicit preference statements
            {
                test: (user) => /\b(i prefer|i like|i want|i don't want|i hate|please don't|stop|always|never)\b/i.test(user),
                extract: (user) => {
                    // Only save short, clear preference statements
                    if (user.length > 300)
                        return null;
                    const cleaned = user.replace(/\s+/g, " ").trim();
                    return {
                        content: `User preference: ${cleaned}`,
                        tags: ["preference", "user-stated"],
                    };
                },
            },
            // Project/path mentions
            {
                test: (user) => /\b(project|repo|codebase|directory)\b.*\b(at|in|lives|located)\b/i.test(user) ||
                    /\/Users\/\S+/.test(user) ||
                    /~\/\S+/.test(user),
                extract: (user) => {
                    const pathMatch = user.match(/(\/Users\/\S+|~\/\S+)/);
                    if (!pathMatch)
                        return null;
                    return {
                        content: `Referenced path: ${pathMatch[1]}`,
                        tags: ["path", "project"],
                    };
                },
            },
            // Name/identity reveals
            {
                test: (user) => /\b(my name is|i'm called|call me)\b/i.test(user),
                extract: (user) => {
                    const match = user.match(/(?:my name is|i'm called|call me)\s+(\S+)/i);
                    if (!match)
                        return null;
                    return {
                        content: `User's name: ${match[1]}`,
                        tags: ["identity", "user"],
                    };
                },
            },
            // Tech stack mentions
            {
                test: (user) => /\b(i use|we use|our stack|tech stack|using)\b/i.test(user) &&
                    /\b(typescript|react|next|python|swift|rust|go|vue|angular|svelte)\b/i.test(user),
                extract: (user) => {
                    const techs = user.match(/\b(typescript|react|next\.?js|python|swift|rust|golang|vue|angular|svelte|node|deno|bun)\b/gi);
                    if (!techs || techs.length === 0)
                        return null;
                    return {
                        content: `Tech stack includes: ${[...new Set(techs.map((t) => t.toLowerCase()))].join(", ")}`,
                        tags: ["tech-stack", "preference"],
                    };
                },
            },
            // Correction / feedback patterns (user correcting the agent)
            {
                test: (user) => /\b(no,|wrong|incorrect|that's not|actually,|not what i)\b/i.test(user),
                extract: (user, assistant) => {
                    if (user.length > 200 || !assistant)
                        return null;
                    return {
                        content: `User correction: "${user.slice(0, 150)}"`,
                        tags: ["feedback", "correction"],
                    };
                },
            },
        ];
    }
    // =========================================================================
    // Memory application (deduplicated)
    // =========================================================================
    /**
     * Apply an extracted memory: create if new, update if similar exists.
     * Uses the semantic trie for fast duplicate detection.
     */
    _applyMemory(content, tags) {
        // Fast duplicate check via trie
        const candidates = this._trie.search(content, 5);
        for (const candidate of candidates) {
            const existing = this._memoryManager.get(candidate.memoryId);
            if (!existing)
                continue;
            // Check for semantic similarity (simple: high word overlap)
            const similarity = this._textSimilarity(content, existing.content);
            if (similarity > 0.7) {
                // High similarity - update if the new content is more specific
                if (content.length > existing.content.length * 1.2) {
                    const mergedTags = [...new Set([...existing.tags, ...tags])];
                    this._memoryManager.update(existing.id, content, mergedTags);
                    this._trie.remove(existing.id);
                    this._indexMemory(existing.id, content, mergedTags);
                    return "updated";
                }
                return "skipped"; // existing is good enough
            }
        }
        // Also check exact content match (MemoryManager already does this but belt-and-suspenders)
        const allMemories = this._memoryManager.list();
        for (const m of allMemories) {
            if (this._textSimilarity(content, m.content) > 0.8) {
                return "skipped";
            }
        }
        // Create new memory
        const memory = this._memoryManager.create(content, tags);
        this._indexMemory(memory.id, memory.content, memory.tags);
        return "created";
    }
    /** Simple word-overlap similarity (Jaccard) */
    _textSimilarity(a, b) {
        const wordsA = new Set(a
            .toLowerCase()
            .split(/\W+/)
            .filter((w) => w.length > 2));
        const wordsB = new Set(b
            .toLowerCase()
            .split(/\W+/)
            .filter((w) => w.length > 2));
        if (wordsA.size === 0 || wordsB.size === 0)
            return 0;
        let intersection = 0;
        for (const w of wordsA) {
            if (wordsB.has(w))
                intersection++;
        }
        return intersection / Math.max(wordsA.size, wordsB.size);
    }
    // =========================================================================
    // Trie indexing
    // =========================================================================
    _indexMemory(id, content, tags) {
        const terms = this._extractTerms(content);
        this._trie.index(id, terms, tags);
    }
    _indexExistingMemories() {
        const memories = this._memoryManager.list();
        // Only re-index if trie is empty or stale
        if (this._trie.stats.indexedMemories >= memories.length)
            return;
        for (const memory of memories) {
            if (this._trie.search(memory.id, 1).some((r) => r.memoryId === memory.id))
                continue;
            this._indexMemory(memory.id, memory.content, memory.tags);
        }
        this._trie.flush();
    }
    _extractTerms(content) {
        const stopwords = new Set([
            "the",
            "this",
            "that",
            "with",
            "from",
            "have",
            "been",
            "were",
            "will",
            "would",
            "could",
            "should",
            "their",
            "there",
            "they",
            "them",
            "then",
            "than",
            "when",
            "what",
            "which",
            "where",
            "while",
            "about",
            "also",
            "into",
            "over",
            "after",
            "before",
            "between",
            "through",
            "during",
            "each",
            "some",
            "most",
            "more",
            "very",
            "just",
            "only",
            "like",
            "uses",
            "using",
            "used",
            "does",
            "make",
            "want",
            "wants",
            "need",
        ]);
        const terms = [];
        const seen = new Set();
        // Capitalized phrases (proper nouns, tech terms)
        const capitals = content.match(/[A-Z][a-zA-Z]+(?:\s+[A-Z][a-zA-Z]+)*/g);
        if (capitals) {
            for (const phrase of capitals) {
                const lower = phrase.toLowerCase();
                if (!seen.has(lower) && !stopwords.has(lower) && phrase.length > 2) {
                    terms.push(lower);
                    seen.add(lower);
                }
            }
        }
        // All significant words
        for (const word of content.split(/[\s,.;:!?()[\]{}'"]+/)) {
            const lower = word.toLowerCase();
            if (lower.length > 2 && !stopwords.has(lower) && !seen.has(lower) && !/^\d+$/.test(lower)) {
                terms.push(lower);
                seen.add(lower);
            }
        }
        return terms;
    }
    // =========================================================================
    // Checkpoint persistence
    // =========================================================================
    _loadCheckpoint() {
        if (!existsSync(this._checkpointPath)) {
            return { offset: 0, lastTimestamp: "", totalProcessed: 0 };
        }
        try {
            const content = vault.ready
                ? vault.readFileSync(this._checkpointPath)
                : readFileSync(this._checkpointPath, "utf-8");
            return JSON.parse(content);
        }
        catch {
            return { offset: 0, lastTimestamp: "", totalProcessed: 0 };
        }
    }
    _saveCheckpoint(checkpoint) {
        try {
            const dir = dirname(this._checkpointPath);
            if (!existsSync(dir))
                mkdirSync(dir, { recursive: true });
            const content = JSON.stringify(checkpoint, null, 2);
            if (vault.ready) {
                vault.writeFileSync(this._checkpointPath, content);
            }
            else {
                writeFileSync(this._checkpointPath, content);
            }
        }
        catch {
            // best-effort
        }
    }
}
//# sourceMappingURL=memory-daemon.js.map