/**
 * Backfill historical VisionClaw runtime logs into the transcript-memory
 * database.
 *
 * IMPORTANT — terminology:
 *  - "Log files" = VisionClaw runtime logs stored at
 *    `~/.visionclaw/profiles/<profile>/logs/YYYY-MM-DD.jsonl(.gz)`.
 *    Format: `{timestamp, level, category, message, data}`.
 *  - "Transcript files" = Claude Agent SDK model transcripts stored at
 *    `~/.claude/projects/<slug>/<sessionId>.jsonl`.
 *    Format: `{type:"user"|"assistant", message:{...}}`.
 *
 * This module operates on **log files** (not Claude transcripts).
 *
 * Reads JSONL (and JSONL.gz) log files from the configured logs directory,
 * segments them into wake cycles (wake → next wake / end-of-file boundaries),
 * matching the existing data-processing segmenter, then generates
 * summaries + embeddings via Gemini, and inserts them into the same SQLite
 * DB used by the live transcript indexer.
 *
 * Usage:
 *   visionclaw backfill-memory [--dry-run] [--since YYYY-MM-DD] [--concurrency N]
 */
import fs from "node:fs";
import path from "node:path";
import readline from "node:readline";
import { createGunzip } from "node:zlib";
import { pipeline } from "node:stream/promises";
import { PassThrough } from "node:stream";
import { getConfigDir, loadConfig } from "../../config/index.js";
import { generateSummary, generateEmbedding, extractToolCallsFromLines, insertWakeCycleRecord, wakeCycleExists, getWakeCycleCount, closeTranscriptMemoryDb, } from "./transcript-indexer.js";
import { claudeTranscriptParser } from "../transcript-parsers/index.js";
import { logger } from "../../logger.js";
// ---------------------------------------------------------------------------
// Log file discovery and streaming
// ---------------------------------------------------------------------------
function discoverLogFiles(logsDir, since) {
    if (!fs.existsSync(logsDir)) {
        console.error(`Logs directory not found: ${logsDir}`);
        return [];
    }
    const files = fs
        .readdirSync(logsDir)
        .filter((f) => (f.endsWith(".jsonl") || f.endsWith(".jsonl.gz")) &&
        !f.includes(".archive."))
        .sort();
    if (!since)
        return files.map((f) => path.join(logsDir, f));
    // Filter files whose date portion is >= since
    return files
        .filter((f) => {
        // File names are like "2026-03-30.jsonl" or "2026-03-30.jsonl.gz"
        const datePart = f.slice(0, 10);
        return datePart >= since;
    })
        .map((f) => path.join(logsDir, f));
}
function createLogInputStream(filePath) {
    const isGz = filePath.endsWith(".gz");
    if (isGz) {
        const fileStream = fs.createReadStream(filePath);
        const gunzip = createGunzip();
        const passthrough = new PassThrough();
        void pipeline(fileStream, gunzip, passthrough).catch(() => {
            // Stream errors will surface when readline tries to read
        });
        return passthrough;
    }
    return fs.createReadStream(filePath, { encoding: "utf-8" });
}
async function* streamLogEntries(filePath) {
    const fileName = path.basename(filePath);
    let lineIdx = 0;
    const rl = readline.createInterface({
        input: createLogInputStream(filePath),
        crlfDelay: Infinity,
    });
    for await (const raw of rl) {
        const trimmed = raw.trim();
        if (!trimmed) {
            lineIdx++;
            continue;
        }
        try {
            const obj = JSON.parse(trimmed);
            yield {
                timestamp: obj.timestamp ?? "",
                level: obj.level ?? "",
                category: obj.category ?? "",
                message: obj.message ?? "",
                data: obj.data ?? {},
                sourceFile: fileName,
                lineIndex: lineIdx,
            };
        }
        catch {
            // Skip malformed lines
        }
        lineIdx++;
    }
}
class AbortStreamingBackfillError extends Error {
    constructor() {
        super("streaming backfill aborted");
    }
}
async function streamWakeCycles(filePath, onCycle) {
    let current;
    let entryCount = 0;
    let cycleCount = 0;
    for await (const entry of streamLogEntries(filePath)) {
        entryCount++;
        if (entry.category === "wake") {
            if (current) {
                current.endLine = entry.lineIndex; // exclusive: up to the next wake
                await onCycle(current, cycleCount);
                cycleCount++;
            }
            current = {
                entries: [entry],
                sourceFile: filePath,
                startLine: entry.lineIndex,
                endLine: entry.lineIndex + 1,
            };
            continue;
        }
        if (!current)
            continue;
        current.entries.push(entry);
        current.endLine = entry.lineIndex + 1;
    }
    if (current) {
        await onCycle(current, cycleCount);
        cycleCount++;
    }
    return { entryCount, cycleCount };
}
// ---------------------------------------------------------------------------
// Condensed text generation from VisionClaw log entries
// ---------------------------------------------------------------------------
function condenseCycleEntries(entries) {
    const parts = [];
    for (const entry of entries) {
        // TODO: [thinking] are purposely ignored because [thinking] is usually noisy internal narration rather than durable memory content.
        switch (entry.category) {
            case "incoming": {
                const channel = entry.data.channel ?? "";
                const sender = entry.data.sender ?? "";
                const text = entry.data.text ?? entry.message;
                parts.push(`[User] [${channel}] ${sender}: ${text}`);
                break;
            }
            case "send": {
                const text = entry.data.text ?? entry.message;
                if (text.length > 500) {
                    parts.push(`[Sent to Model] ${text.slice(0, 500)}...[truncated]`);
                }
                else {
                    parts.push(`[Sent to Model] ${text}`);
                }
                break;
            }
            case "assistant":
                parts.push(`[Assistant] ${entry.message}`);
                break;
            case "tool_call": {
                const name = entry.data.name ?? "unknown";
                const input = entry.data.input;
                const inputStr = input ? JSON.stringify(input) : "";
                const truncated = inputStr.length > 500
                    ? inputStr.slice(0, 500) + "...[truncated]"
                    : inputStr;
                parts.push(`[Tool Call] ${name}(${truncated})`);
                break;
            }
            case "tool_result": {
                // Only include errors in the condensed version
                if (entry.level === "error") {
                    const name = entry.data.name ?? "unknown";
                    const msg = entry.message.length > 300
                        ? entry.message.slice(0, 300) + "...[truncated]"
                        : entry.message;
                    parts.push(`[Tool Error] ${name}: ${msg}`);
                }
                break;
            }
            case "wake": {
                const trigger = entry.data.trigger ?? "unknown";
                parts.push(`[Wake] trigger=${trigger}`);
                break;
            }
            case "result": {
                const turns = entry.data.turns;
                const cost = entry.data.cost;
                parts.push(`[Result] turns=${turns ?? "?"} cost=$${cost ?? "?"}`);
                break;
            }
            // Skip debug, system, channel (noise) categories
        }
    }
    return parts.join("\n");
}
// ---------------------------------------------------------------------------
// Trivial wake cycle detection (backfill log format)
// ---------------------------------------------------------------------------
/**
 * Base tool name suffixes considered "housekeeping". MCP tools are often
 * prefixed by the SDK (e.g. `mcp__visionclaw__memory`), so we match by
 * suffix to handle both bare and prefixed names.
 */
const TRIVIAL_TOOL_SUFFIXES = [
    "memory",
    "memory_transcript_search",
    "memory_transcript_detail",
    "finish",
    "wait",
    "switch_session",
];
/**
 * Exact tool names from the Claude Agent SDK that are trivial.
 */
const TRIVIAL_EXACT_NAMES = new Set([
    "ToolSearch",
]);
/**
 * Check whether a tool name is trivial/housekeeping, handling MCP-prefixed
 * names like `mcp__visionclaw__memory` as well as bare names like `memory`.
 */
function isTrivialTool(name) {
    if (TRIVIAL_EXACT_NAMES.has(name))
        return true;
    for (const suffix of TRIVIAL_TOOL_SUFFIXES) {
        if (name === suffix || name.endsWith(`__${suffix}`))
            return true;
    }
    return false;
}
/**
 * Check whether a backfill wake cycle only contains trivial/housekeeping
 * tool calls and no substantive work.
 */
function isOnlyTrivialToolCallsBackfill(entries) {
    let hasToolCalls = false;
    for (const entry of entries) {
        if (entry.category === "tool_call") {
            hasToolCalls = true;
            const name = entry.data.name ?? "";
            if (!isTrivialTool(name)) {
                return false;
            }
        }
    }
    return hasToolCalls;
}
// ---------------------------------------------------------------------------
// Wake cycle ID generation for backfill
// ---------------------------------------------------------------------------
function generateBackfillCycleId(cycle, cycleIndex) {
    const wakeEntry = cycle.entries.find((e) => e.category === "wake");
    const timestamp = wakeEntry?.timestamp ?? "";
    // Extract date from ISO timestamp (e.g., "2026-03-30T14:32:00.123+08:00" → "2026-03-30")
    const date = timestamp.slice(0, 10) || "unknown";
    // Detect session type from "send" entry
    const sendEntry = cycle.entries.find((e) => e.category === "send");
    let sessionType = "general";
    if (sendEntry) {
        const match = /\[Active session: (\w+)]/.exec(sendEntry.message);
        if (match?.[1])
            sessionType = match[1];
    }
    const seq = String(cycleIndex + 1).padStart(3, "0");
    return `${date}_${sessionType}_${seq}`;
}
// ---------------------------------------------------------------------------
// Main backfill logic
// ---------------------------------------------------------------------------
async function processOneCycle(cycle, cycleId, config, dryRun) {
    // Check if already indexed
    if (wakeCycleExists(cycleId)) {
        return "skipped_existing";
    }
    // Skip cycles with only trivial/housekeeping tool calls
    if (isOnlyTrivialToolCallsBackfill(cycle.entries)) {
        return "skipped_trivial";
    }
    // Condense entries
    const condensed = condenseCycleEntries(cycle.entries);
    if (condensed.length < 50) {
        return "skipped_trivial";
    }
    // Detect session type
    const sendEntry = cycle.entries.find((e) => e.category === "send");
    let sessionType = "general";
    if (sendEntry) {
        const match = /\[Active session: (\w+)]/.exec(sendEntry.message);
        if (match?.[1])
            sessionType = match[1];
    }
    // Extract timestamps
    const wakeEntry = cycle.entries.find((e) => e.category === "wake");
    const resultEntry = [...cycle.entries]
        .reverse()
        .find((e) => e.category === "result");
    const timestampStart = wakeEntry?.timestamp ?? new Date().toISOString();
    const timestampEnd = resultEntry?.timestamp ?? timestampStart;
    if (dryRun) {
        console.log(`  [DRY RUN] Would index ${cycleId}: ${condensed.length} chars, ` +
            `${cycle.entries.length} entries, ${sessionType} session`);
        return "indexed";
    }
    // Generate summary
    const { summary, key_excerpts } = await generateSummary(condensed, config);
    // Generate embedding (best-effort)
    const embeddingVec = await generateEmbedding(summary, config);
    const embeddingBlob = embeddingVec
        ? Buffer.from(embeddingVec.buffer, embeddingVec.byteOffset, embeddingVec.byteLength)
        : null;
    // Resolve source file path — store the absolute path to the VisionClaw log file
    const sourceFile = path.isAbsolute(cycle.sourceFile)
        ? cycle.sourceFile
        : path.join(getConfigDir(), "logs", cycle.sourceFile);
    // Insert into DB
    insertWakeCycleRecord({
        wakeCycleId: cycleId,
        sessionType,
        timestampStart,
        timestampEnd,
        summary,
        keyExcerpts: key_excerpts,
        sourceFile,
        startLine: cycle.startLine + 1, // Convert 0-indexed inclusive start → 1-indexed inclusive
        endLine: cycle.endLine + 1, // Convert 0-indexed exclusive end → 1-indexed exclusive
        embeddingBlob,
    });
    return "indexed";
}
async function sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
}
const CLAUDE_WAKE_CYCLE_GAP_SECONDS = 120;
function discoverClaudeTranscripts(projectDir, since) {
    if (!fs.existsSync(projectDir))
        return [];
    const files = fs
        .readdirSync(projectDir)
        .filter((f) => f.endsWith(".jsonl") &&
        !f.includes(".image-archive."))
        .sort();
    if (!since)
        return files.map((f) => path.join(projectDir, f));
    return files
        .filter((f) => {
        // Archive files have date in name: <sessionId>.archive.YYYY-MM-DD.jsonl
        const dateMatch = /\.archive\.(\d{4}-\d{2}-\d{2})\.jsonl$/.exec(f);
        if (dateMatch)
            return dateMatch[1] >= since;
        // Main session file — always include (contains recent data)
        return true;
    })
        .map((f) => path.join(projectDir, f));
}
function parseClaudeTranscriptEntries(filePath) {
    const content = fs.readFileSync(filePath, "utf-8");
    const entries = [];
    const lines = content.split("\n");
    for (let i = 0; i < lines.length; i++) {
        const trimmed = lines[i].trim();
        if (!trimmed)
            continue;
        try {
            const obj = JSON.parse(trimmed);
            entries.push({
                type: obj.type ?? "",
                timestamp: obj.timestamp ?? "",
                isMeta: obj.isMeta === true,
                lineIndex: i,
                raw: trimmed,
            });
        }
        catch {
            // Skip malformed lines
        }
    }
    return entries;
}
function segmentClaudeWakeCycles(entries, filePath, rawLines) {
    // Filter to substantive entries (skip meta, skip system housekeeping)
    const substantive = entries.filter((e) => !e.isMeta && e.type !== "system" && e.type !== "last-prompt" && e.timestamp);
    if (substantive.length === 0)
        return [];
    const cycles = [];
    let segStart = 0;
    for (let i = 1; i < substantive.length; i++) {
        const prev = new Date(substantive[i - 1].timestamp);
        const curr = new Date(substantive[i].timestamp);
        if (Number.isNaN(prev.getTime()) || Number.isNaN(curr.getTime()))
            continue;
        const gapSec = (curr.getTime() - prev.getTime()) / 1000;
        if (gapSec >= CLAUDE_WAKE_CYCLE_GAP_SECONDS) {
            // Found a boundary — emit the previous segment
            const segEntries = substantive.slice(segStart, i);
            if (segEntries.length > 0) {
                const startLine = segEntries[0].lineIndex;
                const endLine = segEntries[segEntries.length - 1].lineIndex + 1;
                cycles.push({
                    lines: rawLines.slice(startLine, endLine),
                    sourceFile: filePath,
                    startLine: startLine + 1,
                    endLine: endLine + 1,
                    timestampStart: segEntries[0].timestamp,
                    timestampEnd: segEntries[segEntries.length - 1].timestamp,
                });
            }
            segStart = i;
        }
    }
    // Emit final segment
    const segEntries = substantive.slice(segStart);
    if (segEntries.length > 0) {
        const startLine = segEntries[0].lineIndex;
        const endLine = segEntries[segEntries.length - 1].lineIndex + 1;
        cycles.push({
            lines: rawLines.slice(startLine, endLine),
            sourceFile: filePath,
            startLine: startLine + 1,
            endLine: endLine + 1,
            timestampStart: segEntries[0].timestamp,
            timestampEnd: segEntries[segEntries.length - 1].timestamp,
        });
    }
    return cycles;
}
function generateClaudeBackfillCycleId(cycle, cycleIndex) {
    const date = cycle.timestampStart.slice(0, 10) || "unknown";
    const seq = String(cycleIndex + 1).padStart(3, "0");
    return `${date}_claude_${seq}`;
}
async function processClaudeCycle(cycle, cycleId, config, dryRun) {
    if (wakeCycleExists(cycleId)) {
        return "skipped_existing";
    }
    if (claudeTranscriptParser.isOnlyTrivialToolCalls(cycle.lines)) {
        return "skipped_trivial";
    }
    const condensed = claudeTranscriptParser.parseForSummary(cycle.lines);
    if (condensed.length < 50) {
        return "skipped_trivial";
    }
    if (dryRun) {
        console.log(`  [DRY RUN] Would index ${cycleId}: ${condensed.length} chars, ` +
            `${cycle.lines.length} lines`);
        return "indexed";
    }
    const { summary, key_excerpts } = await generateSummary(condensed, config);
    const embeddingVec = await generateEmbedding(summary, config);
    const embeddingBlob = embeddingVec
        ? Buffer.from(embeddingVec.buffer, embeddingVec.byteOffset, embeddingVec.byteLength)
        : null;
    const toolCallExtraction = extractToolCallsFromLines(cycle.lines, {
        cycleStartedAt: cycle.timestampStart,
        cycleEndedAt: cycle.timestampEnd,
    });
    insertWakeCycleRecord({
        wakeCycleId: cycleId,
        sessionType: "general",
        timestampStart: cycle.timestampStart,
        timestampEnd: cycle.timestampEnd,
        summary,
        keyExcerpts: key_excerpts,
        sourceFile: cycle.sourceFile,
        startLine: cycle.startLine,
        endLine: cycle.endLine,
        embeddingBlob,
        toolCallsJson: JSON.stringify(toolCallExtraction.toolCalls),
        toolCallCount: toolCallExtraction.toolCallCount,
        toolCallsTruncated: toolCallExtraction.truncated,
        reportedToolCallCount: toolCallExtraction.reportedToolCallCount,
    });
    return "indexed";
}
async function runClaudeBackfill(files, config, options) {
    const stats = {
        totalFiles: files.length,
        totalCycles: 0,
        indexedCycles: 0,
        skippedTrivial: 0,
        skippedExisting: 0,
        errors: 0,
    };
    for (const filePath of files) {
        const fileName = path.basename(filePath);
        console.log(`Processing ${fileName}...`);
        const rawContent = fs.readFileSync(filePath, "utf-8");
        const rawLines = rawContent.split("\n");
        const entries = parseClaudeTranscriptEntries(filePath);
        if (entries.length === 0) {
            console.log("  No entries found, skipping");
            continue;
        }
        const cycles = segmentClaudeWakeCycles(entries, filePath, rawLines);
        console.log(`  ${entries.length} entries → ${cycles.length} wake cycle(s)`);
        for (let i = 0; i < cycles.length; i++) {
            stats.totalCycles++;
            const cycle = cycles[i];
            const cycleId = generateClaudeBackfillCycleId(cycle, i);
            try {
                const result = await processClaudeCycle(cycle, cycleId, config, options.dryRun);
                switch (result) {
                    case "indexed":
                        stats.indexedCycles++;
                        console.log(`  ✓ ${cycleId} (${cycle.lines.length} lines)`);
                        break;
                    case "skipped_trivial":
                        stats.skippedTrivial++;
                        break;
                    case "skipped_existing":
                        stats.skippedExisting++;
                        break;
                    case "error":
                        stats.errors++;
                        break;
                }
                if (result === "indexed" && !options.dryRun) {
                    await sleep(200);
                }
            }
            catch (err) {
                stats.errors++;
                console.error(`  ✗ ${cycleId}: ${err instanceof Error ? err.message : String(err)}`);
                await sleep(1000);
            }
        }
        console.log();
    }
    return stats;
}
// ---------------------------------------------------------------------------
// CLI entry point
// ---------------------------------------------------------------------------
function getClaudeProjectDir() {
    const configDir = getConfigDir();
    const profileDir = path.dirname(configDir);
    const slug = profileDir.replace(/\//g, "-").replace(/^-/, "-");
    return path.join(process.env.HOME ?? "/tmp", ".claude", "projects", slug);
}
function printBackfillSummary(stats) {
    console.log("─".repeat(50));
    console.log("Backfill Summary:");
    console.log(`  Files processed:    ${stats.totalFiles}`);
    console.log(`  Wake cycles found:  ${stats.totalCycles}`);
    console.log(`  Indexed:            ${stats.indexedCycles}`);
    console.log(`  Skipped (trivial):  ${stats.skippedTrivial}`);
    console.log(`  Skipped (existing): ${stats.skippedExisting}`);
    console.log(`  Errors:             ${stats.errors}`);
}
export async function runBackfillMemory(args) {
    const options = {
        dryRun: args.includes("--dry-run"),
        since: null,
        concurrency: 1,
    };
    const sinceIdx = args.indexOf("--since");
    if (sinceIdx !== -1 && sinceIdx + 1 < args.length) {
        options.since = args[sinceIdx + 1];
    }
    const concIdx = args.indexOf("--concurrency");
    if (concIdx !== -1 && concIdx + 1 < args.length) {
        options.concurrency = Math.max(1, parseInt(args[concIdx + 1], 10) || 1);
    }
    const sourceIdx = args.indexOf("--source");
    const source = sourceIdx !== -1 && sourceIdx + 1 < args.length
        ? args[sourceIdx + 1]
        : "logs";
    console.log("VisionClaw Transcript Memory Backfill");
    console.log("─".repeat(50));
    if (options.dryRun)
        console.log("Mode: DRY RUN (no changes will be made)");
    if (options.since)
        console.log(`Since: ${options.since}`);
    console.log(`Source: ${source}`);
    console.log();
    let config;
    try {
        config = loadConfig();
    }
    catch {
        console.error("Failed to load config. Run setup first.");
        process.exit(1);
    }
    if (source === "claude") {
        const projectDir = getClaudeProjectDir();
        const files = discoverClaudeTranscripts(projectDir, options.since);
        if (files.length === 0) {
            console.log(`No .claude transcript files found in ${projectDir}`);
            return;
        }
        console.log(`Found ${files.length} .claude transcript file(s) to process\n`);
        try {
            const stats = await runClaudeBackfill(files, config, options);
            printBackfillSummary(stats);
        }
        finally {
            closeTranscriptMemoryDb();
        }
        return;
    }
    // Default: process VisionClaw runtime logs
    const logsDir = path.join(getConfigDir(), "logs");
    const files = discoverLogFiles(logsDir, options.since);
    if (files.length === 0) {
        console.log("No log files found to process.");
        return;
    }
    console.log(`Found ${files.length} log file(s) to process\n`);
    const stats = {
        totalFiles: files.length,
        totalCycles: 0,
        indexedCycles: 0,
        skippedTrivial: 0,
        skippedExisting: 0,
        errors: 0,
    };
    try {
        for (const filePath of files) {
            const fileName = path.basename(filePath);
            console.log(`Processing ${fileName}...`);
            let firstCycle = true;
            const streamStats = await streamWakeCycles(filePath, async (cycle, cycleIndex) => {
                if (firstCycle) {
                    firstCycle = false;
                }
                stats.totalCycles++;
                const cycleId = generateBackfillCycleId(cycle, cycleIndex);
                try {
                    const result = await processOneCycle(cycle, cycleId, config, options.dryRun);
                    switch (result) {
                        case "indexed":
                            stats.indexedCycles++;
                            console.log(`  ✓ ${cycleId} (${cycle.entries.length} entries)`);
                            break;
                        case "skipped_trivial":
                            stats.skippedTrivial++;
                            break;
                        case "skipped_existing":
                            stats.skippedExisting++;
                            break;
                        case "error":
                            stats.errors++;
                            break;
                    }
                    if (result === "indexed" && !options.dryRun) {
                        await sleep(200);
                    }
                }
                catch (err) {
                    stats.errors++;
                    console.error(`  ✗ ${cycleId}: ${err instanceof Error ? err.message : String(err)}`);
                    await sleep(1000);
                }
            });
            if (streamStats.entryCount === 0) {
                console.log(`  No entries found, skipping`);
                continue;
            }
            console.log(`  ${streamStats.entryCount} entries → ${streamStats.cycleCount} wake cycle(s)`);
            console.log();
        }
    }
    finally {
        closeTranscriptMemoryDb();
    }
    printBackfillSummary(stats);
}
// ---------------------------------------------------------------------------
// Auto-backfill: one-time background backfill on agent startup
// ---------------------------------------------------------------------------
const BACKFILL_MARKER = "transcript-memory-backfilled";
function writeMarker(markerPath, data) {
    try {
        fs.writeFileSync(markerPath, JSON.stringify(data, null, 2) + "\n");
    }
    catch {
        // best-effort
    }
}
/**
 * AbortController exposed to the caller so the agent loop can cancel
 * auto-backfill during graceful shutdown.
 */
let backfillAbortController = null;
/**
 * Cancel any in-progress auto-backfill. Safe to call multiple times or when
 * no backfill is running.
 */
export function cancelAutoBackfill() {
    backfillAbortController?.abort();
}
/**
 * Check whether auto-backfill should run and execute it if needed.
 * Called from the agent loop on startup (fire-and-forget).
 *
 * Conditions for auto-backfill:
 *  1. No marker file exists (hasn't been done before)
 *  2. The transcript-memory DB has no entries (empty/new)
 *  3. Historical log files exist in the logs directory
 *
 * Robustness:
 *  - Respects an AbortSignal so the agent can stop it on shutdown.
 *  - Idempotent: already-indexed cycles are skipped via `wakeCycleExists()`.
 *  - Crash-safe: the marker file is only written after all cycles are
 *    processed. On restart the backfill resumes, skipping existing records.
 *  - Concurrent writes: live `indexWakeCycle()` can run in parallel. SQLite
 *    WAL mode + in-process synchronous driver ensures serialized writes.
 *  - DB closed during backfill: all DB operations are wrapped in try/catch
 *    so a closed-DB error does not crash the process.
 *
 * The backfill runs with a longer delay between API calls (500ms)
 * to avoid competing with the agent's own Gemini usage.
 */
export async function maybeAutoBackfill(config) {
    const configDir = getConfigDir();
    const markerPath = path.join(configDir, BACKFILL_MARKER);
    // 1. Already done?
    if (fs.existsSync(markerPath)) {
        return;
    }
    // 2. DB already has entries? (e.g. manual backfill was run, or live indexing
    //    has been running for a while)
    const existingCount = getWakeCycleCount();
    if (existingCount > 0) {
        // DB already has data — write marker and skip
        writeMarker(markerPath, { skipped: true, existingCount });
        logger.system(`[transcript-backfill] Skipping auto-backfill: DB already has ${existingCount} entries`);
        return;
    }
    // 3. Any files to process? (both VisionClaw logs and .claude transcripts)
    const logsDir = path.join(configDir, "logs");
    const logFiles = discoverLogFiles(logsDir, null);
    const claudeProjectDir = getClaudeProjectDir();
    const claudeFiles = discoverClaudeTranscripts(claudeProjectDir, null);
    if (logFiles.length === 0 && claudeFiles.length === 0) {
        writeMarker(markerPath, { skipped: true, reason: "no_files" });
        logger.system("[transcript-backfill] Skipping auto-backfill: no historical log or transcript files found");
        return;
    }
    // 4. Set up cancellation
    backfillAbortController = new AbortController();
    const { signal } = backfillAbortController;
    // 5. Run backfill (VisionClaw logs first, then .claude transcripts)
    const files = logFiles;
    logger.system(`[transcript-backfill] Starting auto-backfill: ${logFiles.length} log file(s), ${claudeFiles.length} .claude transcript(s)...`);
    const stats = {
        totalFiles: files.length,
        totalCycles: 0,
        indexedCycles: 0,
        skippedTrivial: 0,
        skippedExisting: 0,
        errors: 0,
    };
    let aborted = false;
    for (const filePath of files) {
        if (signal.aborted) {
            aborted = true;
            break;
        }
        const fileName = path.basename(filePath);
        try {
            const streamStats = await streamWakeCycles(filePath, async (cycle, cycleIndex) => {
                // Re-check abort between cycles — the signal may have been set during
                // the previous processOneCycle() await. Cast via unknown to suppress
                // the linter's "always falsy" false positive (it doesn't model
                // AbortSignal mutation across awaits).
                if (signal.aborted) {
                    aborted = true;
                    throw new AbortStreamingBackfillError();
                }
                stats.totalCycles++;
                const cycleId = generateBackfillCycleId(cycle, cycleIndex);
                try {
                    const result = await processOneCycle(cycle, cycleId, config, false);
                    switch (result) {
                        case "indexed":
                            stats.indexedCycles++;
                            break;
                        case "skipped_trivial":
                            stats.skippedTrivial++;
                            break;
                        case "skipped_existing":
                            stats.skippedExisting++;
                            break;
                        case "error":
                            stats.errors++;
                            break;
                    }
                    // Longer delay for background operation to avoid starving
                    // the agent's own Gemini API calls
                    if (result === "indexed") {
                        await sleep(500);
                    }
                }
                catch (err) {
                    stats.errors++;
                    logger.warn(`[transcript-backfill] ${cycleId}: ${err instanceof Error ? err.message : String(err)}`);
                    await sleep(2000);
                }
            });
            logger.debug(`[transcript-backfill] ${fileName}: ${streamStats.cycleCount} cycles processed`);
        }
        catch (err) {
            if (err instanceof AbortStreamingBackfillError) {
                break;
            }
            logger.warn(`[transcript-backfill] Failed to process ${fileName}: ${err instanceof Error ? err.message : String(err)}`);
        }
    }
    // Phase 2: .claude transcript backfill
    if (!aborted && claudeFiles.length > 0) {
        logger.system(`[transcript-backfill] Processing ${claudeFiles.length} .claude transcript(s)...`);
        try {
            const claudeStats = await runClaudeBackfill(claudeFiles, config, {
                dryRun: false,
                since: null,
                concurrency: 1,
            });
            stats.totalFiles += claudeStats.totalFiles;
            stats.totalCycles += claudeStats.totalCycles;
            stats.indexedCycles += claudeStats.indexedCycles;
            stats.skippedTrivial += claudeStats.skippedTrivial;
            stats.skippedExisting += claudeStats.skippedExisting;
            stats.errors += claudeStats.errors;
        }
        catch (err) {
            logger.warn(`[transcript-backfill] .claude backfill failed: ${err instanceof Error ? err.message : String(err)}`);
        }
    }
    backfillAbortController = null;
    if (aborted) {
        logger.system(`[transcript-backfill] Auto-backfill interrupted (shutdown): ` +
            `${stats.indexedCycles} indexed so far. Will resume on next startup.`);
        return;
    }
    writeMarker(markerPath, {
        completedAt: new Date().toISOString(),
        ...stats,
    });
    logger.system(`[transcript-backfill] Auto-backfill complete: ` +
        `${stats.indexedCycles} indexed, ${stats.skippedTrivial} trivial, ` +
        `${stats.skippedExisting} existing, ${stats.errors} errors`);
}
//# sourceMappingURL=transcript-backfill.js.map