/**
 * @module        memory
 * @description   项目级持久化知识记忆系统
 *
 * @author        eddy
 * @organization  wykj
 * @ownership     wykj/eddy
 *
 * @created       2026-04-03
 * @updated       2026-04-03
 *
 * @role          util
 * @layer         core
 * @boundedContext memory
 *
 * Types: convention (no decay), decision (slow decay), lesson (normal decay), reference (no decay)
 */
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { basename, resolve } from 'node:path';
// ─── Types ──────────────────────────────────────────────────────
export const MEMORY_TYPES = ['convention', 'decision', 'lesson', 'reference'];
export const MEMORY_INDEX = 'MEMORY.md';
const MAX_INDEX_LINES = 200;
const MAX_MEMORY_FILES = 200;
const MAX_FILE_LINES = 200;
const MAX_CONTEXT_ENTRIES = 10;
const FRONTMATTER_MAX_LINES = 30;
// Types that don't decay — always relevant
const NO_DECAY_TYPES = ['convention', 'reference'];
// ─── Paths ──────────────────────────────────────────────────────
/** Get the user-level memory directory (cross-project preferences) */
export function getUserMemoryDir() {
    return resolve(homedir(), '.coral', 'memory', 'user');
}
/** Get the agent-level memory directory (per daemon instance) */
export function getAgentMemoryDir(agentId) {
    return resolve(homedir(), '.coral', 'memory', 'agents', agentId);
}
/** Get the memory directory for a project */
export function getProjectMemoryDir(project) {
    return resolve(homedir(), '.coral', 'memory', 'projects', project);
}
/** Ensure project memory directory exists */
export function ensureMemoryDir(project) {
    const dir = getProjectMemoryDir(project);
    if (!existsSync(dir))
        mkdirSync(dir, { recursive: true });
    return dir;
}
/** Ensure user memory directory exists */
export function ensureUserMemoryDir() {
    return ensureDir(getUserMemoryDir());
}
/** Ensure agent memory directory exists */
export function ensureAgentMemoryDir(agentId) {
    return ensureDir(getAgentMemoryDir(agentId));
}
/** Ensure any memory directory exists (generic) */
function ensureDir(dir) {
    if (!existsSync(dir))
        mkdirSync(dir, { recursive: true });
    return dir;
}
// ─── Scanning ───────────────────────────────────────────────────
/** Parse simple YAML frontmatter from file content (first N lines) */
function parseFrontmatter(content) {
    const lines = content.split('\n');
    if (lines[0]?.trim() !== '---')
        return {};
    const result = {};
    for (let i = 1; i < lines.length; i++) {
        if (lines[i].trim() === '---')
            break;
        const match = lines[i].match(/^(\w+):\s*(.+)$/);
        if (match) {
            result[match[1]] = match[2].trim();
        }
    }
    return result;
}
/** Scan memory directory for .md files, return headers sorted newest-first */
export function scanMemoryFiles(memoryDir) {
    if (!existsSync(memoryDir))
        return [];
    try {
        const files = readdirSync(memoryDir, { recursive: true })
            .map(f => typeof f === 'string' ? f : f.toString())
            .filter(f => f.endsWith('.md') && basename(f) !== MEMORY_INDEX);
        const headers = [];
        for (const relativePath of files) {
            try {
                const filePath = resolve(memoryDir, relativePath);
                const stat = statSync(filePath);
                const content = readFileSync(filePath, 'utf-8');
                // Only read first N lines for frontmatter
                const head = content.split('\n').slice(0, FRONTMATTER_MAX_LINES).join('\n');
                const fm = parseFrontmatter(head);
                headers.push({
                    filename: relativePath,
                    filePath,
                    mtimeMs: stat.mtimeMs,
                    name: fm.name || null,
                    description: fm.description || null,
                    type: MEMORY_TYPES.find(t => t === fm.type),
                });
            }
            catch { /* skip unreadable files */ }
        }
        return headers
            .sort((a, b) => b.mtimeMs - a.mtimeMs)
            .slice(0, MAX_MEMORY_FILES);
    }
    catch {
        return [];
    }
}
// ─── Reading ────────────────────────────────────────────────────
/** Read MEMORY.md index content, truncated to max lines */
export function readMemoryIndex(memoryDir) {
    const indexPath = resolve(memoryDir, MEMORY_INDEX);
    if (!existsSync(indexPath))
        return '';
    const content = readFileSync(indexPath, 'utf-8').trim();
    const lines = content.split('\n');
    if (lines.length > MAX_INDEX_LINES) {
        return lines.slice(0, MAX_INDEX_LINES).join('\n')
            + `\n\n> WARNING: ${MEMORY_INDEX} has ${lines.length} lines (limit: ${MAX_INDEX_LINES}). Truncated.`;
    }
    return content;
}
/** Read a memory file, truncated to max lines */
export function readMemoryFile(filePath) {
    if (!existsSync(filePath))
        return '';
    const content = readFileSync(filePath, 'utf-8');
    const lines = content.split('\n');
    if (lines.length > MAX_FILE_LINES) {
        return lines.slice(0, MAX_FILE_LINES).join('\n')
            + `\n\n> [Truncated — ${lines.length} lines total. Use file read for full content.]`;
    }
    return content;
}
/** Memory age in days */
function memoryAgeDays(mtimeMs) {
    return Math.floor((Date.now() - mtimeMs) / (1000 * 60 * 60 * 24));
}
/** Staleness caveat for old memories */
function stalenessCaveat(mtimeMs) {
    const days = memoryAgeDays(mtimeMs);
    if (days <= 1)
        return null;
    return `> This memory is ${days} days old. Verify against current code before acting on it.`;
}
// ─── Context Generation ─────────────────────────────────────────
/**
 * Select relevant memories for a worker prompt.
 * Priority: convention (always) > card-related > decision > lesson > reference
 * No decay types are always included. Lessons decay by age.
 */
function selectRelevantMemories(headers, _cardSeq) {
    const selected = [];
    // 1. All conventions and references (no decay, always relevant)
    for (const h of headers) {
        if (h.type && NO_DECAY_TYPES.includes(h.type)) {
            selected.push(h);
        }
    }
    // 2. Decisions (sorted newest-first, already in that order)
    for (const h of headers) {
        if (h.type === 'decision' && !selected.includes(h)) {
            selected.push(h);
        }
    }
    // 3. Lessons (normal decay — skip if older than 30 days)
    for (const h of headers) {
        if (h.type === 'lesson' && !selected.includes(h)) {
            if (memoryAgeDays(h.mtimeMs) <= 30) {
                selected.push(h);
            }
        }
    }
    // 4. Untyped memories (legacy)
    for (const h of headers) {
        if (!h.type && !selected.includes(h)) {
            selected.push(h);
        }
    }
    return selected.slice(0, MAX_CONTEXT_ENTRIES);
}
/**
 * Build the memory context string for prompt injection.
 * Includes: MEMORY.md index + selected file contents.
 */
export function buildMemoryContext(project, cardSeq) {
    const memoryDir = getProjectMemoryDir(project);
    if (!existsSync(memoryDir))
        return '';
    const sections = [];
    // 1. Read index
    const index = readMemoryIndex(memoryDir);
    // 2. Scan and select relevant memories
    const headers = scanMemoryFiles(memoryDir);
    if (headers.length === 0 && !index)
        return '';
    const selected = selectRelevantMemories(headers, cardSeq);
    sections.push('# Project Memory');
    sections.push('');
    if (index) {
        sections.push('## Index');
        sections.push(index);
        sections.push('');
    }
    // 3. Include selected memory file contents
    if (selected.length > 0) {
        sections.push('## Relevant Memories');
        sections.push('');
        for (const h of selected) {
            const content = readMemoryFile(h.filePath);
            const caveat = stalenessCaveat(h.mtimeMs);
            const age = memoryAgeDays(h.mtimeMs);
            const ageLabel = age === 0 ? 'today' : age === 1 ? 'yesterday' : `${age} days ago`;
            sections.push(`### ${h.name || h.filename} (saved ${ageLabel})`);
            if (caveat)
                sections.push(caveat);
            sections.push(content);
            sections.push('');
        }
    }
    return sections.join('\n').trim();
}
/**
 * Build user-level memory context (cross-project preferences).
 * Injected into every agent/worker prompt.
 */
export function buildUserMemoryContext() {
    const memoryDir = getUserMemoryDir();
    if (!existsSync(memoryDir))
        return '';
    const headers = scanMemoryFiles(memoryDir);
    const index = readMemoryIndex(memoryDir);
    if (headers.length === 0 && !index)
        return '';
    const sections = ['# User Preferences', ''];
    if (index) {
        sections.push(index);
        sections.push('');
    }
    // Include all user memories (small set, no filtering needed)
    for (const h of headers.slice(0, MAX_CONTEXT_ENTRIES)) {
        const content = readMemoryFile(h.filePath);
        sections.push(`### ${h.name || h.filename}`);
        sections.push(content);
        sections.push('');
    }
    return sections.join('\n').trim();
}
/**
 * Build agent-level memory context (daemon instance experience).
 * Injected when attaching to a daemon session.
 */
export function buildAgentMemoryContext(agentId) {
    const memoryDir = getAgentMemoryDir(agentId);
    if (!existsSync(memoryDir))
        return '';
    const headers = scanMemoryFiles(memoryDir);
    const index = readMemoryIndex(memoryDir);
    if (headers.length === 0 && !index)
        return '';
    const sections = ['# Agent Memory', ''];
    if (index) {
        sections.push(index);
        sections.push('');
    }
    for (const h of headers.slice(0, MAX_CONTEXT_ENTRIES)) {
        const content = readMemoryFile(h.filePath);
        const caveat = stalenessCaveat(h.mtimeMs);
        const age = memoryAgeDays(h.mtimeMs);
        const ageLabel = age === 0 ? 'today' : age === 1 ? 'yesterday' : `${age} days ago`;
        sections.push(`### ${h.name || h.filename} (saved ${ageLabel})`);
        if (caveat)
            sections.push(caveat);
        sections.push(content);
        sections.push('');
    }
    return sections.join('\n').trim();
}
/**
 * Build combined memory context for all applicable layers.
 * Used by StageEngine (user + project) and agent commands (user + agent).
 */
export function buildFullMemoryContext(opts) {
    const parts = [];
    // Layer 1: User preferences (always)
    const userCtx = buildUserMemoryContext();
    if (userCtx)
        parts.push(userCtx);
    // Layer 2: Agent memory (if daemon)
    if (opts.agentId) {
        const agentCtx = buildAgentMemoryContext(opts.agentId);
        if (agentCtx)
            parts.push(agentCtx);
    }
    // Layer 3: Project memory (if project context)
    if (opts.project) {
        const projectCtx = buildMemoryContext(opts.project, opts.cardSeq);
        if (projectCtx)
            parts.push(projectCtx);
    }
    return parts.join('\n\n---\n\n');
}
/**
 * Build memory write instructions for prompt injection.
 * Tells the agent how and where to write memories across all three layers.
 */
export function buildMemoryWriteInstructions(project, agentId) {
    const userDir = getUserMemoryDir();
    const agentDir = agentId ? getAgentMemoryDir(agentId) : null;
    const isAgentMode = project === '_agent';
    // v0.50.9：极简化。只保留路径 + 最小 frontmatter 规范 + 强触发条件。
    // 详细写法（types 分类、Why/Scope 结构、避免清单）移到按需查阅——大多数任务
    // 根本不需要写 memory，没必要每张卡都灌一次规范全文。
    const dirs = [`- user: \`${userDir}/\``];
    if (agentDir)
        dirs.push(`- agent: \`${agentDir}/\``);
    if (!isAgentMode)
        dirs.push(`- project: \`${getProjectMemoryDir(project)}/\``);
    return `# Memory (optional)

Persistent memory lives at:
${dirs.join('\n')}

**Only save non-obvious, reusable facts** — user-stated rules, architectural decisions, pitfalls encountered, external resource pointers. Skip: code structure, file paths, git history, CLAUDE.md content, task state.

To save: write \`<slug>.md\` with frontmatter \`name / description / type\` (type ∈ convention / decision / lesson / reference), then append one line to \`${MEMORY_INDEX}\` in the same dir: \`- [Title](file.md) — hook\`. \`mkdir -p\` if the dir does not exist.

## 经验闭环 (agentmemory · 若工具可用)

如果当前可用 agentmemory 经验工具(\`memory_smart_search\` / \`memory_save\` / \`memory_lesson_save\`):
- **开工前**:先用 \`memory_smart_search\` 检索与本任务、同类问题相关的**过往经验与教训**——命中就采纳,别重蹈覆辙(例:做物理/台球类游戏先查"碰撞""瞄准"的坑)。
- **收尾**:把本次真正**非显然、可复用**的教训 / 踩过的坑 / 关键架构决策用 \`memory_save\`(教训类用 \`memory_lesson_save\`)写入经验库,供后续任务复用。一两条精华即可,别灌流水账。
工具不存在则跳过本段。`;
}
// ─── Writing (for CLI use) ──────────────────────────────────────
/**
 * Add a memory entry via CLI. Creates the file and updates MEMORY.md.
 */
export function addMemory(project, opts) {
    const memoryDir = ensureMemoryDir(project);
    // Generate filename from name
    const slug = opts.name
        .toLowerCase()
        .replace(/[^a-z0-9]+/g, '-')
        .replace(/^-|-$/g, '')
        .slice(0, 50);
    const filename = `${slug}.md`;
    const filePath = resolve(memoryDir, filename);
    // Write memory file
    const content = `---
name: ${opts.name}
description: ${opts.description}
type: ${opts.type}
---

${opts.body}
`;
    writeFileSync(filePath, content);
    // Update MEMORY.md index
    const indexPath = resolve(memoryDir, MEMORY_INDEX);
    let indexContent = '';
    if (existsSync(indexPath)) {
        indexContent = readFileSync(indexPath, 'utf-8').trim();
    }
    // Check for duplicate entry (same filename)
    const lines = indexContent ? indexContent.split('\n') : [];
    const existingIdx = lines.findIndex(l => l.includes(`(${filename})`));
    const newLine = `- [${opts.name}](${filename}) — ${opts.description}`;
    if (existingIdx >= 0) {
        lines[existingIdx] = newLine;
    }
    else {
        lines.push(newLine);
    }
    writeFileSync(indexPath, lines.join('\n') + '\n');
    return filePath;
}
//# sourceMappingURL=memory.js.map