/**
 * Lightweight transcript summarizer for session resets.
 *
 * Reads the meaningful tail of a JSONL transcript and uses a fast LLM call to produce
 * a structured summary of the session's current state. Used by the /refresh
 * command to carry context across session resets.
 */
import fs from "node:fs";
import readline from "node:readline";
import { runSideCall } from "./model-provider.js";
import { logger } from "../logger.js";
const MAX_EXTRACTED_MESSAGES = 200;
const MAX_CHARS = 128_000;
const MAX_COMPACT_SUMMARY_CHARS = 12_000;
const TOOL_RESULT_MAX_CHARS = 1_000;
const RECENT_LINES_LIMIT = 800;
const SUMMARY_MAX_TOKENS = 4096;
const SUMMARY_TIMEOUT_MS = 60_000;
const SUMMARIZE_SYSTEM_PROMPT = `You are a session-state summarizer. Given a partial transcript of an AI agent's conversation, produce a concise summary that captures:

1. **Current task**: What the agent is working on right now
2. **Progress**: What has been accomplished so far
3. **Pending work**: What remains to be done (check any todo lists or plans mentioned)
4. **Key decisions**: Important choices or constraints established during the session
5. **Working context**: File paths, branch names, URLs, or other state the agent needs

Be factual and specific. Include exact file paths, command outputs, and decisions. Distinguish meaningful user requests and work progress from routine heartbeat wakes, idle monitoring, repeated finish calls, or synthetic continuation messages. The summary will be injected into a fresh session so the agent can continue where it left off.

Output plain text (no JSON), maximum 1500 words.`;
function asRecord(value) {
    return typeof value === "object" && value !== null
        ? value
        : null;
}
function isCompactBoundary(parsed) {
    const obj = asRecord(parsed);
    return obj?.type === "system" && obj.subtype === "compact_boundary";
}
function isCompactSummaryRecord(parsed) {
    return asRecord(parsed)?.isCompactSummary === true;
}
const CONTINUATION_TEXT = "Continue from where you left off.";
function isMetaContinuation(content) {
    if (typeof content === "string") {
        return content.trim() === CONTINUATION_TEXT;
    }
    if (Array.isArray(content) && content.length === 1) {
        const block = asRecord(content[0]);
        return block?.type === "text"
            && typeof block.text === "string"
            && block.text.trim() === CONTINUATION_TEXT;
    }
    return false;
}
function isLocalCommandNoise(content) {
    return content.includes("<local-command-caveat>")
        || content.includes("<command-name>/compact</command-name>")
        || content.includes("<local-command-stdout>Compacted");
}
function isSyntheticNoResponse(role, content) {
    return role === "assistant"
        && typeof content === "string"
        && content.trim() === "No response requested.";
}
function compactWhitespace(value) {
    return value.replace(/\n{3,}/g, "\n\n").trim();
}
function sanitizeEmbeddedData(value) {
    return value
        .replace(/data:image\/[a-zA-Z0-9.+-]+;base64,[A-Za-z0-9+/=\r\n]+/g, "[base64 image omitted]")
        .replace(/(?<![A-Za-z0-9+/=])[A-Za-z0-9+/]{240,}={0,2}(?![A-Za-z0-9+/=])/g, "[base64 data omitted]");
}
function cleanText(value) {
    return compactWhitespace(sanitizeEmbeddedData(value));
}
function safeJsonPreview(value, maxChars) {
    if (value === undefined || value === null)
        return "";
    const raw = sanitizeEmbeddedData(typeof value === "string" ? value : JSON.stringify(value));
    return raw.length > maxChars ? `${raw.slice(0, maxChars)}…` : raw;
}
function extractToolResultText(content) {
    const textParts = [];
    if (typeof content === "string") {
        textParts.push(content);
    }
    else if (Array.isArray(content)) {
        for (const item of content) {
            const block = asRecord(item);
            if (block?.type === "text" && typeof block.text === "string") {
                textParts.push(block.text);
            }
        }
    }
    const text = cleanText(textParts.join("\n"));
    if (!text)
        return null;
    const finishMatch = /^Finished:\s*([^\n]+)/.exec(text);
    if (finishMatch) {
        return `[finish: ${finishMatch[1]}]`;
    }
    const truncated = text.length > TOOL_RESULT_MAX_CHARS
        ? `${text.slice(0, TOOL_RESULT_MAX_CHARS)}…`
        : text;
    return `[tool_result]: ${truncated}`;
}
function extractContent(parsed) {
    const obj = asRecord(parsed);
    const msg = asRecord(obj?.message);
    if (!obj || !msg)
        return null;
    const role = typeof msg.role === "string" ? msg.role : null;
    const content = msg.content;
    if (!role || !content)
        return null;
    if (isMetaContinuation(content))
        return null;
    if (isSyntheticNoResponse(role, content))
        return null;
    if (typeof content === "string") {
        if (isLocalCommandNoise(content))
            return null;
        return {
            text: `[${role}]: ${cleanText(content)}`,
            isCompactSummary: obj.isCompactSummary === true,
        };
    }
    if (!Array.isArray(content))
        return null;
    const texts = [];
    for (const block of content) {
        const b = asRecord(block);
        if (!b)
            continue;
        if (b.type === "text" && typeof b.text === "string") {
            const text = cleanText(b.text);
            if (text && !isLocalCommandNoise(text))
                texts.push(text);
        }
        else if (b.type === "thinking" && typeof b.thinking === "string") {
            const thinking = cleanText(b.thinking);
            if (thinking)
                texts.push(`[thinking]: ${safeJsonPreview(thinking, 500)}`);
        }
        else if (b.type === "image") {
            // TODO: Include selected recent images in summary input once the summarizer supports multimodal context.
            texts.push("[image: screenshot omitted]");
        }
        else if (b.type === "tool_use") {
            const name = typeof b.name === "string" ? b.name : "?";
            const inputStr = safeJsonPreview(b.input, 300);
            texts.push(`[tool_use: ${name}${inputStr ? ` ${inputStr}` : ""}]`);
        }
        else if (b.type === "tool_result") {
            const result = extractToolResultText(b.content);
            if (result)
                texts.push(result);
        }
    }
    if (texts.length === 0)
        return null;
    if (role === "assistant" && texts.length === 1 && texts[0] === "No response requested.") {
        return null;
    }
    return {
        text: `[${role}]: ${texts.join("\n")}`,
        isCompactSummary: obj.isCompactSummary === true,
    };
}
function selectRecentMessages(messages) {
    const selectedReversed = [];
    let totalChars = 0;
    for (let i = messages.length - 1; i >= 0; i -= 1) {
        const text = messages[i];
        const separatorChars = selectedReversed.length === 0 ? 0 : 2;
        if (totalChars + separatorChars + text.length > MAX_CHARS) {
            if (selectedReversed.length > 0)
                break;
            selectedReversed.push(text.slice(Math.max(0, text.length - MAX_CHARS)));
            break;
        }
        selectedReversed.push(text);
        totalChars += separatorChars + text.length;
        if (selectedReversed.length >= MAX_EXTRACTED_MESSAGES)
            break;
    }
    return selectedReversed.reverse();
}
function buildTranscriptSummaryInputFromParsedEntries(parsedEntries) {
    let compactSummary = null;
    const meaningfulMessages = [];
    for (const parsed of parsedEntries) {
        const extracted = extractContent(parsed);
        if (!extracted)
            continue;
        if (extracted.isCompactSummary) {
            compactSummary = extracted.text.length > MAX_COMPACT_SUMMARY_CHARS
                ? `${extracted.text.slice(0, MAX_COMPACT_SUMMARY_CHARS)}…`
                : extracted.text;
            continue;
        }
        meaningfulMessages.push(extracted.text);
    }
    const selectedMessages = selectRecentMessages(meaningfulMessages);
    const sections = [];
    if (compactSummary) {
        sections.push(`## Prior compacted context\n${compactSummary}`);
    }
    if (selectedMessages.length > 0) {
        sections.push(`## Recent meaningful transcript\n${selectedMessages.join("\n\n")}`);
    }
    return sections.join("\n\n");
}
function buildTranscriptSummaryInputFromSelectedLines(lines, compactSummaryLine) {
    const parsedEntries = [];
    if (compactSummaryLine) {
        try {
            parsedEntries.push(JSON.parse(compactSummaryLine));
        }
        catch {
            // skip malformed
        }
    }
    for (const line of lines) {
        try {
            parsedEntries.push(JSON.parse(line));
        }
        catch {
            // skip malformed
        }
    }
    return buildTranscriptSummaryInputFromParsedEntries(parsedEntries);
}
export function buildTranscriptSummaryInputFromLines(lines) {
    const selected = selectTranscriptLinesForSummary(lines);
    return buildTranscriptSummaryInputFromSelectedLines(selected.lines, selected.compactSummaryLine);
}
function createTranscriptLineSelectionState() {
    return {
        buffer: new Array(RECENT_LINES_LIMIT),
        count: 0,
        writeIndex: 0,
        compactSummaryLine: undefined,
    };
}
function resetTranscriptLineSelectionState(state) {
    state.buffer.fill(undefined);
    state.count = 0;
    state.writeIndex = 0;
    state.compactSummaryLine = undefined;
}
function addTranscriptLineForSelection(state, line) {
    if (!line.trim())
        return;
    let isBoundary = false;
    let isCompactSummary = false;
    try {
        const parsed = JSON.parse(line);
        isBoundary = isCompactBoundary(parsed);
        isCompactSummary = isCompactSummaryRecord(parsed);
    }
    catch {
        // Malformed lines are retained as data.
    }
    if (isBoundary) {
        resetTranscriptLineSelectionState(state);
        return;
    }
    if (isCompactSummary) {
        state.compactSummaryLine = line;
    }
    state.buffer[state.writeIndex] = line;
    state.writeIndex = (state.writeIndex + 1) % RECENT_LINES_LIMIT;
    state.count += 1;
}
function getSelectedTranscriptLines(state) {
    const length = Math.min(state.count, RECENT_LINES_LIMIT);
    const lines = new Array(length);
    const startOffset = state.count > RECENT_LINES_LIMIT ? state.writeIndex : 0;
    for (let i = 0; i < length; i += 1) {
        const slot = state.buffer[(startOffset + i) % RECENT_LINES_LIMIT];
        if (slot !== undefined)
            lines[i] = slot;
    }
    return { lines, compactSummaryLine: state.compactSummaryLine };
}
export function selectTranscriptLinesForSummary(lines) {
    const state = createTranscriptLineSelectionState();
    for (const line of lines)
        addTranscriptLineForSelection(state, line);
    return getSelectedTranscriptLines(state);
}
async function readTranscriptTail(transcriptPath) {
    const input = fs.createReadStream(transcriptPath, { encoding: "utf8" });
    const rl = readline.createInterface({ input, crlfDelay: Infinity });
    const state = createTranscriptLineSelectionState();
    try {
        for await (const line of rl)
            addTranscriptLineForSelection(state, line);
    }
    finally {
        rl.close();
        input.close();
    }
    const selected = getSelectedTranscriptLines(state);
    return buildTranscriptSummaryInputFromSelectedLines(selected.lines, selected.compactSummaryLine);
}
/**
 * Fallback text used when there is no transcript to summarize, or when
 * the side LLM call fails. Kept verbatim with what /refresh injected
 * historically so behavior is identical across all reset paths.
 */
export const RESET_SUMMARY_FALLBACK = "The previous session transcript could not be summarized automatically. " +
    "Use the `memory_transcript_search` tool to look up your recent activity " +
    "and rebuild context. Also check your memory directory and todo list.";
/**
 * Convenience wrapper around {@link summarizeTranscript} that:
 *  - returns the {@link RESET_SUMMARY_FALLBACK} text when `transcriptPath`
 *    is `null` (no transcript yet) or summarization returns `null`,
 *  - never throws (the LLM-side errors are already swallowed by
 *    `summarizeTranscript`).
 *
 * Used by both the `/refresh` command and the startup engine-switch
 * boundary so they share a single summarize+fallback pipeline.
 */
export async function summarizeWithFallback(transcriptPath, config) {
    if (!transcriptPath)
        return RESET_SUMMARY_FALLBACK;
    const summary = await summarizeTranscript(transcriptPath, config);
    return summary ?? RESET_SUMMARY_FALLBACK;
}
export async function summarizeTranscript(transcriptPath, config) {
    if (!fs.existsSync(transcriptPath)) {
        logger.warn("[transcript-summarizer] transcript not found");
        return null;
    }
    try {
        const transcriptText = await readTranscriptTail(transcriptPath);
        if (!transcriptText || transcriptText.length < 100) {
            logger.debug("[transcript-summarizer] transcript too short to summarize");
            return null;
        }
        const text = await runSideCall(config, {
            systemPrompt: SUMMARIZE_SYSTEM_PROMPT,
            userMessage: `Here is the recent transcript to summarize:\n\n${transcriptText}`,
            maxTokens: SUMMARY_MAX_TOKENS,
            timeoutMs: SUMMARY_TIMEOUT_MS,
            useCase: "summary",
        });
        logger.debug(`[transcript-summarizer] summary generated (${text.length} chars)`);
        return text;
    }
    catch (err) {
        logger.warn(`[transcript-summarizer] failed: ${err instanceof Error ? err.message : String(err)}`);
        return null;
    }
}
//# sourceMappingURL=transcript-summarizer.js.map