import { TOOL_NAME_BASH, TOOL_NAME_EDIT } from "../../core/tools/tool_names.js";
import { buildLineDiff, collapseLongUnchangedDiffRuns } from "../../core/utils/line_diff.js";
import { bytesToTokens, formatTokenEstimate, tokensToBytes } from "../../core/utils/token.js";
import { truncateToBytesFromStart } from "../../core/utils/truncate.js";
const DEFAULT_PRUNE_FRACTION = 0.25;
const PRUNED_TOOL_RESULT_PREFIX = "[Tool result pruned]";
const PRUNED_EDIT_RESULT_PREFIX = "[Tool result pruned] Edit diff";
const PRUNED_EDIT_ARGUMENT_MARKER = "[Content pruned]";
const PRUNE_EDIT_UNCHANGED_CONTEXT_LINES = 4;
const PRUNE_PREVIEW_MAX_TOKENS = 512;
const PRUNE_MAX_OVERAGE_RATIO = 0.1;
export class SessionMaintenanceService {
    engine;
    view;
    runStreamingTask;
    applyCompactedHistoryUi;
    requestSmartPruneSelection;
    constructor(options) {
        this.engine = options.engine;
        this.view = options.view;
        this.runStreamingTask = options.runStreamingTask;
        this.applyCompactedHistoryUi = options.applyCompactedHistoryUi;
        this.requestSmartPruneSelection = options.requestSmartPruneSelection;
    }
    async compactSummaryOnly(guidance) {
        this.view.addSystemMessage("summarizing session...", "success");
        let handledFailure = false;
        const outcome = await this.runStreamingTask(async (signal) => {
            const result = await this.engine.compact({
                mode: "only-summary",
                guidance,
                signal,
            });
            this.applyCompactedHistoryUi(result.compactionMessage);
            this.view.addSystemMessage("session compacted. previous context has been summarized.", "success");
        }, (taskOutcome) => {
            if (!taskOutcome.error || taskOutcome.aborted) {
                return;
            }
            handledFailure = true;
            this.handleCompactionError(taskOutcome.error);
        });
        if (!handledFailure && outcome.error && !outcome.aborted) {
            this.handleCompactionError(outcome.error);
        }
    }
    async compactSummaryAndLast(guidance) {
        this.view.addSystemMessage("summarizing session...", "success");
        let handledFailure = false;
        const outcome = await this.runStreamingTask(async (signal) => {
            const result = await this.engine.compact({
                mode: "with-last-assistant",
                guidance,
                signal,
            });
            this.applyCompactedHistoryUi(result.compactionMessage);
            const successText = result.includedLastAssistant
                ? "session compacted. previous context and last assistant message have been included."
                : "session compacted. previous context has been summarized.";
            this.view.addSystemMessage(successText, "success");
        }, (taskOutcome) => {
            if (!taskOutcome.error || taskOutcome.aborted) {
                return;
            }
            handledFailure = true;
            this.handleCompactionError(taskOutcome.error);
        });
        if (!handledFailure && outcome.error && !outcome.aborted) {
            this.handleCompactionError(outcome.error);
        }
    }
    pruneToolResults(strategy, extra) {
        const fraction = this.parsePruneFraction(extra);
        if (fraction === null) {
            this.view.addSystemMessage("invalid prune fraction. use a number between 0 and 1.", "error");
            return;
        }
        if (fraction === 0) {
            this.view.addSystemMessage("prune fraction is 0, nothing to prune.", "warn");
            return;
        }
        const historyEntries = this.engine.historyEntries;
        if (historyEntries.length === 0) {
            this.view.addSystemMessage("no conversation to prune.", "warn");
            return;
        }
        let editSummary;
        try {
            editSummary = this.pruneEditToolHistory(historyEntries);
        }
        catch (error) {
            this.view.addSystemMessage(`prune failed: ${error.message}`, "error");
            return;
        }
        const prune = this.preparePruneCandidates(historyEntries, fraction);
        if (prune.candidates.length === 0 || prune.totalTokens === 0) {
            this.reportPruneNoop(editSummary, "no bash tool results or edit tool calls to prune.");
            return;
        }
        if (prune.targetTokens <= 0) {
            this.reportPruneNoop(editSummary, "prune fraction is too small to remove anything.");
            return;
        }
        const toPrune = this.selectPruneCandidatesByStrategy(prune, strategy);
        if (toPrune.length === 0) {
            this.reportPruneNoop(editSummary, "no bash tool results or edit tool calls to prune.");
            return;
        }
        const applied = this.applyBashPruneCandidates(toPrune);
        this.view.addSystemMessage(this.buildPruneSummaryMessage(editSummary, applied.resultsPruned, applied.bytesPruned), "success");
    }
    async pruneToolResultsSmart(extra) {
        const parsed = this.parsePruneFractionAndGuidance(extra);
        if (parsed.fraction === null) {
            this.view.addSystemMessage("invalid prune fraction. use a number between 0 and 1.", "error");
            return;
        }
        const fraction = parsed.fraction;
        if (fraction === 0) {
            this.view.addSystemMessage("prune fraction is 0, nothing to prune.", "warn");
            return;
        }
        const historyEntries = this.engine.historyEntries;
        if (historyEntries.length === 0) {
            this.view.addSystemMessage("no conversation to prune.", "warn");
            return;
        }
        try {
            const prune = this.preparePruneCandidates(historyEntries, fraction);
            let editSummary;
            const getEditSummary = () => {
                if (!editSummary) {
                    editSummary = this.pruneEditToolHistory(historyEntries);
                }
                return editSummary;
            };
            if (prune.candidates.length === 0 || prune.totalTokens === 0) {
                this.reportPruneNoop(getEditSummary(), "no bash tool results or edit tool calls to prune.");
                return;
            }
            if (prune.targetTokens <= 0) {
                this.reportPruneNoop(getEditSummary(), "prune fraction is too small to remove anything.");
                return;
            }
            this.view.addSystemMessage("sampling prune candidates...", "success");
            let handledFailure = false;
            const selectionOutcome = await this.runStreamingTask(async (signal) => {
                const prompt = this.buildSmartPrunePrompt({
                    history: historyEntries.map((entry) => entry.message),
                    targetTokens: prune.targetTokens,
                    guidance: parsed.guidance,
                });
                const selection = await this.requestSmartPruneSelection(prompt, signal);
                if (selection.length === 0) {
                    this.reportPruneNoop(getEditSummary(), "model returned no prune candidates.");
                    return;
                }
                const toPrune = this.selectSmartPruneCandidates(selection, prune.candidates, prune.targetTokens);
                if (toPrune.length === 0) {
                    this.reportPruneNoop(getEditSummary(), "no bash tool results or edit tool calls to prune.");
                    return;
                }
                const applied = this.applyBashPruneCandidates(toPrune);
                this.view.addSystemMessage(this.buildPruneSummaryMessage(getEditSummary(), applied.resultsPruned, applied.bytesPruned), "success");
            }, (taskOutcome) => {
                if (!taskOutcome.error || taskOutcome.aborted) {
                    return;
                }
                handledFailure = true;
                this.view.addSystemMessage(`prune failed: ${taskOutcome.error.message}`, "error");
            });
            if (!handledFailure && selectionOutcome.error && !selectionOutcome.aborted) {
                this.view.addSystemMessage(`prune failed: ${selectionOutcome.error.message}`, "error");
            }
        }
        catch (error) {
            this.view.addSystemMessage(`prune failed: ${error.message}`, "error");
        }
    }
    handleCompactionError(err) {
        const message = err.message || "compaction failed";
        if (message === "no conversation to compact.") {
            this.view.addSystemMessage(message, "warn");
            return;
        }
        this.view.addSystemMessage(`compact failed: ${message}`, "error");
    }
    parsePruneFraction(extra) {
        if (!extra) {
            return DEFAULT_PRUNE_FRACTION;
        }
        const parsed = Number(extra);
        if (!Number.isFinite(parsed)) {
            return null;
        }
        if (parsed < 0 || parsed > 1) {
            return null;
        }
        return parsed;
    }
    parsePruneFractionAndGuidance(extra) {
        if (!extra) {
            return { fraction: DEFAULT_PRUNE_FRACTION };
        }
        const trimmed = extra.trim();
        if (!trimmed) {
            return { fraction: DEFAULT_PRUNE_FRACTION };
        }
        const [firstToken, ...rest] = trimmed.split(/\s+/);
        const parsed = Number(firstToken);
        if (Number.isFinite(parsed)) {
            if (parsed < 0 || parsed > 1) {
                return { fraction: null };
            }
            const guidance = rest.join(" ").trim();
            return guidance ? { fraction: parsed, guidance } : { fraction: parsed };
        }
        return { fraction: DEFAULT_PRUNE_FRACTION, guidance: trimmed };
    }
    preparePruneCandidates(historyEntries, fraction) {
        const candidates = [];
        let totalTokens = 0;
        for (const entry of historyEntries) {
            const message = entry.message;
            if (message.role !== "toolResult") {
                continue;
            }
            const toolResult = message;
            if (toolResult.toolName !== TOOL_NAME_BASH) {
                continue;
            }
            const inspection = this.inspectToolResultContent(toolResult);
            if (inspection.firstText?.startsWith(PRUNED_TOOL_RESULT_PREFIX) || inspection.hasImage) {
                continue;
            }
            const tokens = bytesToTokens(inspection.textBytes);
            candidates.push({
                historyEntryId: entry.id,
                toolResult,
                bytes: inspection.textBytes,
                tokens,
                order: candidates.length,
            });
            totalTokens += tokens;
        }
        return {
            candidates,
            totalTokens,
            targetTokens: Math.ceil(totalTokens * fraction),
        };
    }
    selectPruneCandidatesByStrategy(prune, strategy) {
        const ordered = strategy === "largest"
            ? [...prune.candidates].sort((a, b) => b.tokens - a.tokens || b.bytes - a.bytes || a.order - b.order)
            : prune.candidates;
        const selected = [];
        let selectedTokens = 0;
        for (const candidate of ordered) {
            if (selectedTokens >= prune.targetTokens) {
                break;
            }
            selected.push(candidate);
            selectedTokens += candidate.tokens;
        }
        return selected;
    }
    applyBashPruneCandidates(candidates) {
        let bytesPruned = 0;
        for (const candidate of candidates) {
            bytesPruned += candidate.bytes;
            const noticeText = this.buildPrunedToolResultNotice(candidate.toolResult, candidate.bytes);
            const prunedResult = {
                ...candidate.toolResult,
                content: [{ type: "text", text: noticeText }],
            };
            this.engine.replaceMessageById(candidate.historyEntryId, prunedResult);
            this.emitToolResultPrunedUiEvent(prunedResult.toolCallId, noticeText);
        }
        return { resultsPruned: candidates.length, bytesPruned };
    }
    reportPruneNoop(editSummary, fallbackMessage) {
        if (this.hasAnyPrunedEditChanges(editSummary)) {
            this.view.addSystemMessage(this.buildPruneSummaryMessage(editSummary), "success");
            return;
        }
        this.view.addSystemMessage(fallbackMessage, "warn");
    }
    hasAnyPrunedEditChanges(summary) {
        return summary.callsPruned > 0 || summary.resultsPruned > 0;
    }
    buildPruneSummaryMessage(editSummary, bashResultsPruned = 0, bashBytesPruned = 0) {
        const editSummaryText = this.formatPrunedEditSummary(editSummary);
        if (bashResultsPruned <= 0) {
            if (!editSummaryText) {
                return "no bash tool results or edit tool calls to prune.";
            }
            return `pruned ${editSummaryText}.`;
        }
        const bashLabel = formatTokenEstimate(bashBytesPruned);
        const bashNoun = bashResultsPruned === 1 ? "result" : "results";
        if (!editSummaryText) {
            return `pruned ${bashResultsPruned} bash tool ${bashNoun} (${bashLabel}).`;
        }
        return `pruned ${bashResultsPruned} bash tool ${bashNoun} (${bashLabel}) and ${editSummaryText}.`;
    }
    formatPrunedEditSummary(summary) {
        if (!this.hasAnyPrunedEditChanges(summary)) {
            return "";
        }
        const parts = [];
        if (summary.callsPruned > 0) {
            const noun = summary.callsPruned === 1 ? "call" : "calls";
            parts.push(`${summary.callsPruned} edit tool ${noun}`);
        }
        if (summary.resultsPruned > 0) {
            const noun = summary.resultsPruned === 1 ? "result" : "results";
            parts.push(`${summary.resultsPruned} edit tool ${noun}`);
        }
        const tokenEstimate = formatTokenEstimate(summary.bytesRemoved);
        return `${parts.join(" and ")} (${tokenEstimate})`;
    }
    pruneEditToolHistory(historyEntries) {
        const summary = {
            callsPruned: 0,
            resultsPruned: 0,
            bytesRemoved: 0,
        };
        const editCallsById = new Map();
        for (const entry of historyEntries) {
            const message = entry.message;
            if (message.role !== "assistant") {
                continue;
            }
            const assistant = message;
            const assistantContent = this.getAssistantContentOrThrow(assistant, "pruning edit tool calls");
            let changed = false;
            const nextContent = assistantContent.map((block) => {
                if (block.type !== "toolCall") {
                    return block;
                }
                const toolCall = block;
                if (toolCall.name !== TOOL_NAME_EDIT) {
                    return block;
                }
                const args = this.getToolCallArgumentsObject(toolCall);
                const oldText = this.getRequiredEditArgument(args, "oldText", toolCall.id);
                const newText = this.getRequiredEditArgument(args, "newText", toolCall.id);
                if (oldText === PRUNED_EDIT_ARGUMENT_MARKER && newText === PRUNED_EDIT_ARGUMENT_MARKER) {
                    return block;
                }
                changed = true;
                summary.callsPruned += 1;
                const oldBytes = Buffer.byteLength(oldText, "utf8");
                const newBytes = Buffer.byteLength(newText, "utf8");
                const markerBytes = Buffer.byteLength(PRUNED_EDIT_ARGUMENT_MARKER, "utf8") * 2;
                summary.bytesRemoved += Math.max(0, oldBytes + newBytes - markerBytes);
                editCallsById.set(toolCall.id, { oldText, newText });
                return {
                    ...toolCall,
                    arguments: {
                        ...args,
                        oldText: PRUNED_EDIT_ARGUMENT_MARKER,
                        newText: PRUNED_EDIT_ARGUMENT_MARKER,
                    },
                };
            });
            if (changed) {
                this.engine.replaceMessageById(entry.id, { ...assistant, content: nextContent });
            }
        }
        if (editCallsById.size === 0) {
            return summary;
        }
        for (const entry of historyEntries) {
            const message = entry.message;
            if (message.role !== "toolResult") {
                continue;
            }
            const toolResult = message;
            if (toolResult.toolName !== TOOL_NAME_EDIT) {
                continue;
            }
            if (toolResult.isError) {
                continue;
            }
            const callDiff = editCallsById.get(toolResult.toolCallId);
            if (!callDiff) {
                continue;
            }
            const inspection = this.inspectToolResultContent(toolResult);
            if (inspection.firstText?.startsWith(PRUNED_EDIT_RESULT_PREFIX)) {
                continue;
            }
            const prunedText = this.buildPrunedEditToolResult(callDiff);
            const prunedResult = {
                ...toolResult,
                content: [{ type: "text", text: prunedText }],
            };
            this.engine.replaceMessageById(entry.id, prunedResult);
            this.emitToolResultPrunedUiEvent(prunedResult.toolCallId, prunedText);
            summary.resultsPruned += 1;
        }
        return summary;
    }
    getAssistantContentOrThrow(assistant, context) {
        if (!Array.isArray(assistant.content)) {
            throw new Error(`invalid assistant message content while ${context}`);
        }
        return assistant.content;
    }
    getToolCallArgumentsObject(toolCall) {
        const { arguments: argumentsValue } = toolCall;
        if (!argumentsValue || typeof argumentsValue !== "object" || Array.isArray(argumentsValue)) {
            throw new Error(`invalid edit tool call arguments (tool_call_id=${toolCall.id})`);
        }
        return argumentsValue;
    }
    getRequiredEditArgument(args, key, toolCallId) {
        const value = args[key];
        if (typeof value !== "string") {
            throw new Error(`missing edit argument '${key}' (tool_call_id=${toolCallId})`);
        }
        return value;
    }
    buildPrunedEditToolResult(diff) {
        const diffText = this.buildPrunedEditDiff(diff.oldText, diff.newText);
        return `${PRUNED_EDIT_RESULT_PREFIX}\n${diffText}`;
    }
    buildPrunedEditDiff(oldText, newText) {
        const diff = buildLineDiff(oldText, newText);
        if (diff.added === 0 && diff.removed === 0) {
            return "(No textual changes)";
        }
        const collapsed = collapseLongUnchangedDiffRuns({
            diffLines: diff.lines,
            maxUnchangedLines: PRUNE_EDIT_UNCHANGED_CONTEXT_LINES,
        });
        return collapsed.length > 0 ? collapsed.join("\n") : "(No textual changes)";
    }
    buildPrunedToolResultNotice(toolResult, bytes) {
        const tokenEstimate = formatTokenEstimate(bytes);
        return `${PRUNED_TOOL_RESULT_PREFIX} ${toolResult.toolName} output removed (${tokenEstimate}). Re-run the command if needed.`;
    }
    emitToolResultPrunedUiEvent(toolCallId, content) {
        this.view.handleToolUiEvent({
            type: "tool_pruned",
            toolCallId,
            content,
        });
    }
    inspectToolResultContent(toolResult) {
        const parts = [];
        let textBytes = 0;
        let hasImage = false;
        let firstText;
        for (const block of toolResult.content) {
            if (block.type === "text") {
                if (firstText === undefined) {
                    firstText = block.text;
                }
                parts.push(block.text);
                textBytes += Buffer.byteLength(block.text, "utf8");
                continue;
            }
            if (block.type === "image") {
                hasImage = true;
            }
        }
        return {
            text: parts.join("\n").trimEnd(),
            textBytes,
            hasImage,
            firstText,
        };
    }
    buildSmartPrunePrompt(args) {
        const lines = [];
        lines.push(`Target pruning token budget: ${args.targetTokens}`);
        lines.push([
            "The budget is the total tokens to prune.",
            "It is better to slightly exceed it than to be significantly below it.",
        ].join(" "));
        lines.push('Return only JSON: {"prune":[...]} with tool_call_id values from the conversation history.');
        lines.push("Only include tool_call_id values for bash tool results.");
        if (args.guidance?.trim()) {
            lines.push(`Guidance (prioritize this): ${args.guidance.trim()}`);
        }
        else {
            lines.push("Guidance: (none provided, use your best judgment)");
        }
        lines.push("");
        lines.push("Conversation history:");
        lines.push("<conversation>");
        lines.push(...this.formatPruneConversationHistory(args.history));
        lines.push("</conversation>");
        return lines.join("\n");
    }
    formatPruneConversationHistory(history) {
        const lines = [];
        for (const message of history) {
            if (message.role === "user") {
                lines.push("<user>");
                this.appendPruneContentLines(lines, message.content);
                lines.push("</user>");
                lines.push("");
                continue;
            }
            if (message.role === "assistant") {
                lines.push("<assistant>");
                const assistant = message;
                const assistantContent = this.getAssistantContentOrThrow(assistant, "building smart prune prompt");
                for (const block of assistantContent) {
                    if (block.type === "text") {
                        this.appendPruneText(lines, block.text);
                        continue;
                    }
                    if (block.type === "toolCall") {
                        const toolCall = block;
                        lines.push(this.buildPruneToolCallTag(toolCall));
                    }
                }
                lines.push("</assistant>");
                lines.push("");
                continue;
            }
            if (message.role === "toolResult") {
                const toolResult = message;
                lines.push(...this.buildPruneToolResultLines(toolResult));
                lines.push("");
            }
        }
        if (lines[lines.length - 1] === "") {
            lines.pop();
        }
        return lines;
    }
    appendPruneContentLines(lines, content) {
        if (typeof content === "string") {
            this.appendPruneText(lines, content);
            return;
        }
        if (!Array.isArray(content)) {
            return;
        }
        for (const block of content) {
            if (block.type === "text") {
                this.appendPruneText(lines, block.text);
            }
        }
    }
    appendPruneText(lines, text) {
        const escaped = this.escapeXml(text);
        const parts = escaped.split(/\r?\n/);
        for (const part of parts) {
            lines.push(part);
        }
    }
    buildPruneToolCallTag(toolCall) {
        const name = this.escapeXmlAttribute(toolCall.name);
        const id = this.escapeXmlAttribute(toolCall.id);
        const argsValue = JSON.stringify(toolCall.arguments);
        const args = this.escapeXmlAttribute(argsValue);
        return `<tool-call name="${name}" id="${id}" args="${args}" />`;
    }
    buildPruneToolResultLines(toolResult) {
        const lines = [];
        const name = this.escapeXmlAttribute(toolResult.toolName);
        const toolCallId = this.escapeXmlAttribute(toolResult.toolCallId);
        const inspection = this.inspectToolResultContent(toolResult);
        if (inspection.hasImage) {
            lines.push(`<tool-result name="${name}" tool_call_id="${toolCallId}">`);
            lines.push("<preview>[image omitted]</preview>");
            lines.push("</tool-result>");
            return lines;
        }
        const preview = this.buildPruneToolResultPreview(inspection.text);
        lines.push(`<tool-result name="${name}" tool_call_id="${toolCallId}" total_tokens="${preview.totalTokens}">`);
        lines.push(`<preview max_tokens="${PRUNE_PREVIEW_MAX_TOKENS}">`);
        lines.push(...preview.lines);
        lines.push("</preview>");
        lines.push("</tool-result>");
        return lines;
    }
    buildPruneToolResultPreview(text) {
        const normalized = text.trimEnd();
        const totalBytes = Buffer.byteLength(normalized, "utf8");
        const totalTokens = bytesToTokens(totalBytes);
        if (totalTokens <= PRUNE_PREVIEW_MAX_TOKENS) {
            const escaped = this.escapeXml(normalized);
            const lines = escaped ? escaped.split(/\r?\n/) : [""];
            return { lines, totalTokens };
        }
        const head = truncateToBytesFromStart(normalized, tokensToBytes(PRUNE_PREVIEW_MAX_TOKENS));
        const remainingTokens = Math.max(0, totalTokens - PRUNE_PREVIEW_MAX_TOKENS);
        const suffix = `…${remainingTokens} more tokens…`;
        const previewText = head.endsWith("\n") || head === "" ? `${head}${suffix}` : `${head}\n${suffix}`;
        const escaped = this.escapeXml(previewText);
        const lines = escaped ? escaped.split(/\r?\n/) : [""];
        return { lines, totalTokens };
    }
    selectSmartPruneCandidates(ids, candidates, targetTokens) {
        const candidatesById = new Map();
        for (const candidate of candidates) {
            candidatesById.set(candidate.toolResult.toolCallId, candidate);
        }
        const selected = [];
        const seen = new Set();
        let tokens = 0;
        const maxTokens = Math.ceil(targetTokens * (1 + PRUNE_MAX_OVERAGE_RATIO));
        for (const id of ids) {
            if (seen.has(id))
                continue;
            const candidate = candidatesById.get(id);
            if (!candidate)
                continue;
            if (tokens + candidate.tokens > maxTokens)
                continue;
            selected.push(candidate);
            seen.add(id);
            tokens += candidate.tokens;
            if (tokens >= targetTokens) {
                break;
            }
        }
        return selected;
    }
    escapeXml(text) {
        return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
    }
    escapeXmlAttribute(text) {
        return this.escapeXml(text).replace(/"/g, "&quot;").replace(/'/g, "&apos;");
    }
}
//# sourceMappingURL=session_maintenance_service.js.map