"use strict";
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2024-2026 Gracker (Chris)
// This file is part of SmartPerfetto. See LICENSE for details.
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.startSession = startSession;
exports.continueSession = continueSession;
exports.buildResumeContextQuery = buildResumeContextQuery;
exports.truncateAtBoundary = truncateAtBoundary;
/**
 * Turn runner — the shared per-turn flow used by `analyze`, `resume`,
 * and the REPL.
 *
 * Responsibilities:
 *   - Load / reload trace
 *   - Call CliAnalyzeService.runTurn()
 *   - Commit outputs to the session folder via `commitTurnOutputs`
 *
 * Out of scope:
 *   - Bootstrap (env/paths) — caller owns this
 *   - Service construction / teardown — caller owns the lifecycle
 *     (one-shot commands wrap a single turn; REPL keeps one service
 *      across many turns)
 *   - Error presentation beyond propagating exceptions
 */
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const paths_1 = require("../io/paths");
const turnPersistence_1 = require("./turnPersistence");
const sessionStore_1 = require("../io/sessionStore");
const indexJson_1 = require("../io/indexJson");
const transcriptWriter_1 = require("../io/transcriptWriter");
const comparisonAppendixService_1 = require("../../services/comparisonAppendixService");
const RESUME_CONTEXT_MAX_CHARS = 4000;
const RESUME_TURN_MAX_CHARS = 1200;
const RESUME_MAX_TURNS = 3;
/**
 * Fresh analyze — loads the trace, creates a new session, runs turn 1.
 * Equivalent to what `smartperfetto analyze <trace>` does, minus the
 * bootstrap / service-lifecycle work around it.
 */
async function startSession(ctx, input) {
    const tracePath = path.resolve(input.tracePath);
    logText(ctx, `Loading trace: ${tracePath}`);
    // loadTraceFromFilePath throws on ENOENT; we let it propagate so there's
    // one source of truth for the existence check.
    const traceId = await ctx.service.loadTrace(tracePath);
    logText(ctx, `Trace loaded (traceId=${traceId.slice(0, 8)}…)`);
    let referenceTracePath;
    let referenceTraceId;
    let reportAppendix;
    if (input.referenceTracePath) {
        referenceTracePath = path.resolve(input.referenceTracePath);
        if (referenceTracePath === tracePath) {
            throw new Error('reference trace must be different from current trace');
        }
        logText(ctx, `Loading reference trace: ${referenceTracePath}`);
        referenceTraceId = await ctx.service.loadTrace(referenceTracePath);
        logText(ctx, `Reference trace loaded (traceId=${referenceTraceId.slice(0, 8)}…)`);
        reportAppendix = await (0, comparisonAppendixService_1.buildComparisonAppendix)(ctx.service, {
            currentTraceId: traceId,
            referenceTraceId,
        }).catch((err) => ({
            markdown: [
                '## SmartPerfetto 确定性对比附录',
                '',
                `- 固定 SQL 附录生成失败：${err.message}`,
                '',
            ].join('\n'),
            html: `<section><h2>SmartPerfetto 确定性对比附录</h2><p>固定 SQL 附录生成失败：${escapeHtml(err.message)}</p></section>`,
        }));
    }
    const startedAt = Date.now();
    let sp;
    let streamFile = null;
    let resolvedSessionId;
    const result = await ctx.service.runTurn({
        traceId,
        referenceTraceId,
        query: input.query,
        analysisMode: input.analysisMode,
        codeAwareMode: input.codeAwareMode,
        codebaseIds: input.codebaseIds,
        onSessionReady: (sid) => {
            sp = (0, paths_1.sessionPaths)(ctx.paths, sid);
            (0, paths_1.ensureSessionLayout)(sp);
            resolvedSessionId = sid;
            streamFile = sp.stream;
        },
        onEvent: (update) => {
            ctx.renderer.onEvent(update);
            if (streamFile)
                (0, transcriptWriter_1.appendStreamEvent)(streamFile, update);
        },
    });
    // Defensive: if onSessionReady didn't fire (future refactor hazard) we
    // still land on a valid session folder using the resolved sessionId.
    if (!resolvedSessionId || !sp) {
        resolvedSessionId = result.sessionId;
        sp = (0, paths_1.sessionPaths)(ctx.paths, resolvedSessionId);
        (0, paths_1.ensureSessionLayout)(sp);
    }
    const now = Date.now();
    const config = {
        sessionId: resolvedSessionId,
        backendSessionId: result.sessionId,
        tracePath,
        traceId,
        referenceTracePath,
        referenceTraceId,
        providerId: result.providerId,
        agentRuntimeKind: result.agentRuntimeKind,
        providerSnapshotHash: result.providerSnapshotHash,
        sdkSessionId: result.sdkSessionId,
        model: result.model,
        analysisMode: input.analysisMode,
        codeAwareMode: input.codeAwareMode,
        codebaseIds: input.codebaseIds,
        capture: input.capture,
        createdAt: startedAt,
        lastTurnAt: now,
        turnCount: 1,
    };
    (0, turnPersistence_1.commitTurnOutputs)({
        paths: ctx.paths,
        sp,
        renderer: ctx.renderer,
        sessionId: resolvedSessionId,
        turn: 1,
        query: input.query,
        result,
        config,
        turnMarkdown: formatTurnMarkdown(1, input.query, result.result.conclusion || '', result.result, false),
        reportAppendix,
        indexEntry: {
            sessionId: resolvedSessionId,
            createdAt: startedAt,
            lastTurnAt: now,
            tracePath,
            traceFilename: referenceTracePath
                ? `${path.basename(tracePath)} vs ${path.basename(referenceTracePath)}`
                : path.basename(tracePath),
            firstQuery: input.query,
            turnCount: 1,
            status: result.result.success ? 'completed' : 'failed',
        },
    });
    return {
        sessionId: resolvedSessionId,
        sessionDir: sp.dir,
        turn: 1,
        success: result.result.success,
        degraded: false,
    };
}
function escapeHtml(value) {
    return value
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;');
}
/**
 * Continue an existing session — reloads the trace (with the original id
 * when possible), runs turn N+1, and commits outputs to the same folder.
 *
 * Three-level degradation (plan §G.3): Level 1/2 keep sessionId+sdkSessionId
 * intact; Level 3 falls back to a fresh load with the prior conclusion
 * injected as preamble but keeps the CLI-visible session id stable.
 */
async function continueSession(ctx, input) {
    const userSessionId = input.sessionId;
    const sp = (0, paths_1.sessionPaths)(ctx.paths, userSessionId);
    const { config: existingConfig } = (0, sessionStore_1.loadSession)(ctx.paths, userSessionId);
    if (!existingConfig) {
        throw new Error(`no session found at ${sp.dir}`);
    }
    const nextTurn = existingConfig.turnCount + 1;
    const streamFile = sp.stream;
    logText(ctx, `Resuming session ${userSessionId} (turn ${nextTurn})`);
    const reloaded = await ctx.service.reloadTraceById(existingConfig.traceId);
    let effectiveTraceId;
    let effectiveQuery;
    let requestedSessionId;
    let degraded = false;
    if (reloaded) {
        effectiveTraceId = existingConfig.traceId;
        effectiveQuery = buildResumeContextQuery(sp, input.query);
        requestedSessionId = existingConfig.backendSessionId || userSessionId;
        logText(ctx, `Trace reloaded (traceId=${effectiveTraceId.slice(0, 8)}…)`);
    }
    else {
        logText(ctx, '(trace evicted from cache — loading fresh and replaying conclusion as preamble)');
        effectiveTraceId = await ctx.service.loadTrace(existingConfig.tracePath);
        effectiveQuery = buildResumeContextQuery(sp, input.query);
        requestedSessionId = undefined;
        degraded = true;
    }
    let effectiveReferenceTraceId = existingConfig.referenceTraceId;
    if (existingConfig.referenceTracePath) {
        const referenceReloaded = existingConfig.referenceTraceId
            ? await ctx.service.reloadTraceById(existingConfig.referenceTraceId)
            : false;
        if (!referenceReloaded) {
            effectiveReferenceTraceId = await ctx.service.loadTrace(existingConfig.referenceTracePath);
            logText(ctx, `Reference trace reloaded fresh (traceId=${effectiveReferenceTraceId.slice(0, 8)}…)`);
        }
    }
    else if (existingConfig.referenceTraceId) {
        const referenceReloaded = await ctx.service.reloadTraceById(existingConfig.referenceTraceId);
        if (!referenceReloaded) {
            throw new Error('comparison session is missing referenceTracePath; cannot reload reference trace');
        }
    }
    const runInput = {
        traceId: effectiveTraceId,
        referenceTraceId: effectiveReferenceTraceId,
        query: effectiveQuery,
        sessionId: requestedSessionId,
        codeAwareMode: existingConfig.codeAwareMode,
        codebaseIds: existingConfig.codebaseIds,
        analysisMode: existingConfig.analysisMode,
        onSessionReady: () => {
            (0, paths_1.ensureSessionLayout)(sp);
        },
        onEvent: (update) => {
            ctx.renderer.onEvent(update);
            (0, transcriptWriter_1.appendStreamEvent)(streamFile, update);
        },
    };
    let result;
    try {
        result = await ctx.service.runTurn(runInput);
    }
    catch (err) {
        if (!requestedSessionId || !isTraceIdMismatchError(err))
            throw err;
        logText(ctx, '(persisted backend session no longer matches this trace — starting a fresh backend turn with CLI transcript context)');
        degraded = true;
        requestedSessionId = undefined;
        result = await ctx.service.runTurn({
            ...runInput,
            sessionId: undefined,
        });
    }
    const now = Date.now();
    const updatedConfig = {
        ...existingConfig,
        sessionId: userSessionId,
        backendSessionId: result.sessionId,
        traceId: effectiveTraceId,
        referenceTraceId: effectiveReferenceTraceId,
        providerId: result.providerId ?? existingConfig.providerId,
        agentRuntimeKind: result.agentRuntimeKind ?? existingConfig.agentRuntimeKind,
        providerSnapshotHash: result.providerSnapshotHash ?? existingConfig.providerSnapshotHash,
        sdkSessionId: result.sdkSessionId || existingConfig.sdkSessionId,
        model: result.model || existingConfig.model,
        lastTurnAt: now,
        turnCount: nextTurn,
    };
    const idx = (0, indexJson_1.readIndex)(ctx.paths);
    const prev = idx.sessions[userSessionId];
    (0, turnPersistence_1.commitTurnOutputs)({
        paths: ctx.paths,
        sp,
        renderer: ctx.renderer,
        sessionId: userSessionId,
        turn: nextTurn,
        query: input.query,
        result,
        config: updatedConfig,
        turnMarkdown: formatTurnMarkdown(nextTurn, input.query, result.result.conclusion || '', result.result, degraded),
        indexEntry: {
            sessionId: userSessionId,
            createdAt: prev?.createdAt ?? existingConfig.createdAt,
            lastTurnAt: now,
            tracePath: existingConfig.tracePath,
            traceFilename: prev?.traceFilename ?? path.basename(existingConfig.tracePath),
            firstQuery: prev?.firstQuery ?? input.query,
            turnCount: nextTurn,
            status: result.result.success ? 'completed' : 'failed',
        },
    });
    if (degraded) {
        logText(ctx, '\nnote: SDK context was unavailable — replayed prior conclusion as preamble.');
    }
    return {
        sessionId: userSessionId,
        sessionDir: sp.dir,
        turn: nextTurn,
        success: result.result.success,
        degraded,
    };
}
function isTraceIdMismatchError(err) {
    return err instanceof Error && /traceId mismatch for requested session/i.test(err.message);
}
function logText(ctx, message) {
    if (ctx.renderer.format === 'text')
        console.log(message);
}
function buildResumeContextQuery(sp, userQuery) {
    const transcriptTurns = readTranscriptTurns(sp.transcript).slice(-RESUME_MAX_TURNS);
    let context = '';
    if (transcriptTurns.length > 0) {
        context = transcriptTurns
            .map((turn) => {
            const answer = turn.conclusionMd?.trim()
                ? truncateAtBoundary(turn.conclusionMd.trim(), RESUME_TURN_MAX_CHARS)
                : '(empty)';
            return [
                `Turn ${turn.turn}`,
                `Question: ${turn.question}`,
                `Conclusion: ${answer}`,
            ].join('\n');
        })
            .join('\n\n---\n\n');
    }
    else {
        context = readConclusionContext(sp.conclusion);
    }
    if (!context.trim())
        return userQuery;
    const trimmed = context.length > RESUME_CONTEXT_MAX_CHARS
        ? `${truncateAtBoundary(context, RESUME_CONTEXT_MAX_CHARS)}…（已截断）`
        : context;
    const sessionId = path.basename(sp.dir);
    return [
        '（continuing prior SmartPerfetto CLI session; previous context below）',
        `Session id: ${sessionId}`,
        `Session dir: ${sp.dir}`,
        `Report path: ${sp.report}`,
        '---',
        trimmed,
        '---',
        'Use the previous context when it is sufficient; only query the trace again if the new question needs new evidence.',
        `用户新问题: ${userQuery}`,
    ].join('\n');
}
function readConclusionContext(conclusionFile) {
    try {
        return fs.readFileSync(conclusionFile, 'utf-8');
    }
    catch {
        return '';
    }
}
function readTranscriptTurns(transcriptFile) {
    try {
        if (!fs.existsSync(transcriptFile))
            return [];
        return fs
            .readFileSync(transcriptFile, 'utf-8')
            .split('\n')
            .map((line) => line.trim())
            .filter(Boolean)
            .map((line) => {
            try {
                return JSON.parse(line);
            }
            catch {
                return null;
            }
        })
            .filter((turn) => Boolean(turn && typeof turn.turn === 'number'));
    }
    catch {
        return [];
    }
}
/**
 * Truncate at a sentence/paragraph boundary at or before `maxChars` so the
 * preamble doesn't end mid-sentence. Falls back to a hard char cut if no
 * suitable boundary exists in the trailing 30% of the window.
 *
 * Boundaries searched (CJK + Latin): paragraph break > full stop > newline.
 */
function truncateAtBoundary(text, maxChars) {
    if (text.length <= maxChars)
        return text;
    const window = text.slice(0, maxChars);
    const minAccept = Math.floor(maxChars * 0.7);
    const candidates = [
        window.lastIndexOf('\n\n'),
        window.lastIndexOf('。'),
        window.lastIndexOf('. '),
        window.lastIndexOf('！'),
        window.lastIndexOf('？'),
        window.lastIndexOf('\n'),
    ];
    const best = Math.max(...candidates);
    if (best >= minAccept) {
        // Include the boundary character itself for a clean cut.
        return window.slice(0, best + 1);
    }
    return window;
}
function formatTurnMarkdown(turn, query, conclusion, result, degraded) {
    const lines = [
        `# Turn ${turn}`,
        ``,
        `**Question**: ${query}`,
        ``,
        `**Confidence**: ${(result.confidence * 100).toFixed(0)}%  ·  **Rounds**: ${result.rounds}  ·  **Duration**: ${(result.totalDurationMs / 1000).toFixed(1)}s`,
        ``,
    ];
    if (degraded) {
        lines.push(`> _Note: SDK context was unavailable for this turn — prior conclusion was replayed as preamble._`, ``);
    }
    lines.push('## Conclusion', '', conclusion || '*(empty)*', '');
    return lines.join('\n');
}
//# sourceMappingURL=turnRunner.js.map