// transcript-archive.ts — central, full-retention transcript archive core.
//
// Mirrors project-local `.forge/transcripts/<entityId>/` runs into the
// permanent cross-project archive under `~/.pi/forge-cli/transcripts/`
// (FORGE_CLI_HOME-aware via paths/paths.ts). Copy-up only: this module has
// ZERO delete paths — project-local transcripts stay untouched (live
// debugging and verify-governance.sh depend on them; the archive is the
// permanent mirror, `.forge/` is the ephemeral working copy).
//
// Compression is node:zlib gzip — byte archival of phase payloads (~10:1).
// NOT @entelligentsia/forge-compress, which is token compression for LLM
// contexts; nothing here ever re-enters a model context uncompressed.
//
// Every write path is best-effort and never throws: archiving is
// observability data, not load-bearing pipeline state. Every read path
// Value.Checks untrusted disk JSON and returns null/defaults on mismatch.
import * as crypto from "node:crypto";
import * as fs from "node:fs";
import * as path from "node:path";
import { gunzipSync, gzipSync } from "node:zlib";
import { Value } from "typebox/value";
import { getRunArchiveDir, getTranscriptArchiveRoot, getTranscriptIndexPath, getTranscriptProjectsPath, } from "./paths/paths.js";
import { IndexEntrySchema, PhaseUsageSchema, ProjectsRegistrySchema, RunManifestSchema, } from "./transcript-archive-types.js";
/**
 * Read `{prefix, name, projectDir}` from a `.forge/config.json`. Defaults on
 * any failure (missing file, malformed JSON, absent fields) — never throws.
 * `projectDir` is derived from the config path itself (`<dir>/.forge/config.json`).
 */
export function readProjectIdentity(configPath) {
    const projectDir = path.dirname(path.dirname(configPath));
    const fallbackName = path.basename(projectDir) || "project";
    try {
        const raw = JSON.parse(fs.readFileSync(configPath, "utf8"));
        const prefix = typeof raw.project?.prefix === "string" && raw.project.prefix.length > 0
            ? raw.project.prefix
            : "project";
        const name = typeof raw.project?.name === "string" && raw.project.name.length > 0
            ? raw.project.name
            : fallbackName;
        return { prefix, name, projectDir };
    }
    catch {
        return { prefix: "project", name: fallbackName, projectDir };
    }
}
/**
 * `<prefix>-<sha256(realpath(projectDir)).slice(0,8)>` — prefix alone can
 * collide across projects (two repos both named "FORGE"); the path hash
 * disambiguates. Falls back to the raw path when realpath throws (deleted
 * or unmounted project dir — archive entries must remain addressable).
 */
export function computeProjectKey(projectDir, prefix) {
    let resolved;
    try {
        resolved = fs.realpathSync(projectDir);
    }
    catch {
        resolved = projectDir;
    }
    const hash = crypto.createHash("sha256").update(resolved).digest("hex").slice(0, 8);
    const slug = prefix
        .toLowerCase()
        .replace(/[^a-z0-9._-]/g, "_")
        .slice(0, 40) || "project";
    return `${slug}-${hash}`;
}
// ── Orchestrator JSONL + phase-transcript parsing ───────────────────────
/** "2026-05-28T13:35:00.123Z" → "20260528T133500Z" (compact UTC). */
function compactIso(iso) {
    return iso.replace(/[-:]/g, "").replace(/\.\d+/, "");
}
/** "20260528T133500Z" → "2026-05-28T13:35:00Z" (inverse of compactIso). */
function expandCompactIso(compact) {
    const m = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z$/.exec(compact);
    if (!m)
        return compact;
    return `${m[1]}-${m[2]}-${m[3]}T${m[4]}:${m[5]}:${m[6]}Z`;
}
/** Tolerant JSONL parse — skips malformed/truncated lines, never throws. */
function readJsonlEvents(filePath) {
    let raw;
    try {
        raw = fs.readFileSync(filePath, "utf8");
    }
    catch {
        return [];
    }
    const events = [];
    for (const line of raw.split("\n")) {
        const trimmed = line.trim();
        if (!trimmed)
            continue;
        try {
            const parsed = JSON.parse(trimmed);
            if (parsed && typeof parsed === "object")
                events.push(parsed);
        }
        catch {
            // Crash-truncated tail line — skip.
        }
    }
    return events;
}
/** Filename shape written by forge-subagent.ts / orchestrator-transcript.ts. */
const PHASE_FILE_RE = /^(\d{8}T\d{6}Z)__(.+)__(.+)\.json$/;
const ORCH_FILE_RE = /^(\d{8}T\d{6}Z)__(.+)__orchestrator\.jsonl$/;
/** Live tail-view log written by persistTailLog (viewport/events.ts). */
const TAIL_FILE_RE = /^(\d{8}T\d{6}Z)__(.+)__(.+)\.tail\.jsonl$/;
/** Extract header fields from a phase-transcript JSON; null on parse failure. */
function readPhaseHeader(absPath) {
    try {
        const raw = JSON.parse(fs.readFileSync(absPath, "utf8"));
        const header = {};
        if (typeof raw.model === "string" && raw.model.length > 0)
            header.model = raw.model;
        if (typeof raw.provider === "string" && raw.provider.length > 0)
            header.provider = raw.provider;
        if (typeof raw.startedAt === "string")
            header.startedAt = raw.startedAt;
        if (raw.usage && typeof raw.usage === "object") {
            const candidate = raw.usage;
            if (Value.Check(PhaseUsageSchema, candidate))
                header.usage = candidate;
        }
        return header;
    }
    catch {
        return null;
    }
}
function emptyTotals() {
    return {
        input: 0,
        output: 0,
        cacheRead: 0,
        cacheWrite: 0,
        cost: 0,
        byModel: {},
        byProvider: {},
        revisionLoops: 0,
    };
}
function bumpAggregate(bucket, key, usage) {
    const agg = bucket[key] ?? { input: 0, output: 0, cost: 0, phases: 0 };
    agg.input += usage.input;
    agg.output += usage.output;
    agg.cost += usage.cost;
    agg.phases += 1;
    bucket[key] = agg;
}
function buildManifestForRun(runId, events, phaseFiles, tailFileNames, identity, projectKey, fallbackEntityId, fallbackStartIso, opts) {
    const pipelineStart = events.find((e) => e.kind === "pipeline-start");
    const pipelineEnd = events.find((e) => e.kind === "pipeline-end");
    const phaseEnds = events.filter((e) => e.kind === "phase-end");
    const revisionLoops = events.filter((e) => e.kind === "phase-loopback").length;
    const entityId = typeof pipelineStart?.entityId === "string" && pipelineStart.entityId.length > 0
        ? pipelineStart.entityId
        : fallbackEntityId;
    const rawKind = pipelineStart?.entityKind;
    const entityKind = rawKind === "task" || rawKind === "bug" || rawKind === "sprint" ? rawKind : "unknown";
    const rawOutcome = pipelineEnd?.outcome;
    const outcome = rawOutcome === "complete" || rawOutcome === "halted" || rawOutcome === "cancelled" || rawOutcome === "error"
        ? rawOutcome
        : "incomplete";
    // Phase records: one per phase-transcript file (chronological), decorated
    // with verdict/attempt/elapsedMs from the matching phase-end event. Two
    // matchers, tried in order:
    //   1. phase-end.subagentTranscriptPath basename (exact, when populated)
    //   2. same role, in chronological order (legacy runs without the path)
    const phases = [];
    const roleSeen = {};
    const usedEnds = new Set();
    for (const pf of [...phaseFiles].sort((a, b) => a.file.localeCompare(b.file))) {
        roleSeen[pf.role] = (roleSeen[pf.role] ?? 0) + 1;
        const header = readPhaseHeader(pf.absPath);
        const record = {
            role: pf.role,
            attempt: roleSeen[pf.role],
            verdict: "n/a",
            file: pf.file,
        };
        // Pair the phase with its live tail-view log when one was persisted
        // (same basename, .tail.jsonl suffix) — replay prefers it verbatim.
        const tailName = `${pf.file.slice(0, -".json".length)}.tail.jsonl`;
        if (tailFileNames.has(tailName))
            record.tailFile = tailName;
        if (header?.model)
            record.model = header.model;
        if (header?.provider)
            record.provider = header.provider;
        if (header?.usage)
            record.usage = header.usage;
        if (header?.startedAt)
            record.startedAt = header.startedAt;
        let matchIdx = phaseEnds.findIndex((e, i) => !usedEnds.has(i) &&
            typeof e.subagentTranscriptPath === "string" &&
            path.basename(e.subagentTranscriptPath) === pf.file);
        if (matchIdx < 0) {
            matchIdx = phaseEnds.findIndex((e, i) => !usedEnds.has(i) && e.phase === pf.role);
        }
        if (matchIdx >= 0) {
            usedEnds.add(matchIdx);
            const end = phaseEnds[matchIdx];
            if (typeof end.verdict === "string")
                record.verdict = end.verdict;
            if (typeof end.attempt === "number")
                record.attempt = end.attempt;
            if (typeof end.elapsedMs === "number")
                record.elapsedMs = end.elapsedMs;
        }
        phases.push(record);
    }
    // Phase-end events with no transcript file (export failed) still surface.
    phaseEnds.forEach((end, i) => {
        if (usedEnds.has(i))
            return;
        phases.push({
            role: typeof end.phase === "string" ? end.phase : "unknown",
            attempt: typeof end.attempt === "number" ? end.attempt : 1,
            verdict: typeof end.verdict === "string" ? end.verdict : "n/a",
            ...(typeof end.elapsedMs === "number" ? { elapsedMs: end.elapsedMs } : {}),
        });
    });
    const totals = emptyTotals();
    totals.revisionLoops = revisionLoops;
    for (const p of phases) {
        if (!p.usage)
            continue;
        totals.input += p.usage.input;
        totals.output += p.usage.output;
        totals.cacheRead += p.usage.cacheRead;
        totals.cacheWrite += p.usage.cacheWrite;
        totals.cost += p.usage.cost;
        bumpAggregate(totals.byModel, p.model ?? "unknown", p.usage);
        bumpAggregate(totals.byProvider, p.provider ?? "unknown", p.usage);
    }
    const startedAt = typeof pipelineStart?.ts === "string" ? pipelineStart.ts : undefined;
    const finishedAt = typeof pipelineEnd?.ts === "string" ? pipelineEnd.ts : undefined;
    const durationMs = typeof pipelineEnd?.elapsedMs === "number" ? pipelineEnd.elapsedMs : undefined;
    return {
        schema: "forge-run-manifest/v1",
        runId,
        projectKey,
        projectPath: identity.projectDir,
        projectName: identity.name,
        projectPrefix: identity.prefix,
        entityId,
        entityKind,
        ...(opts.sprintId ? { sprintId: opts.sprintId } : {}),
        startedAt: startedAt ?? fallbackStartIso,
        ...(finishedAt ? { finishedAt } : {}),
        ...(durationMs !== undefined ? { durationMs } : {}),
        outcome,
        phases,
        totals,
        archivedAt: new Date().toISOString(),
    };
}
/**
 * Discover every run in a project-local entity transcript dir
 * (`.forge/transcripts/<entityId>/`) and build a RunManifest for each.
 *
 * A "run" is one orchestrator JSONL file; its runId is the compact-ISO
 * pipeline-start timestamp (falling back to the filename prefix). Phase
 * transcript files are assigned to the latest run whose start precedes
 * them — compact ISO prefixes compare lexicographically as timestamps.
 */
export function buildRunsForEntityDir(entityDir, identity, projectKey, opts = {}) {
    let names;
    try {
        names = fs.readdirSync(entityDir);
    }
    catch {
        return [];
    }
    const fallbackEntityId = path.basename(entityDir);
    const jsonls = [];
    const phaseFiles = [];
    const tailFiles = [];
    for (const name of names) {
        const orchMatch = ORCH_FILE_RE.exec(name);
        if (orchMatch) {
            jsonls.push({ absPath: path.join(entityDir, name), ts: orchMatch[1] });
            continue;
        }
        const tailMatch = TAIL_FILE_RE.exec(name);
        if (tailMatch) {
            tailFiles.push({
                absPath: path.join(entityDir, name),
                file: name,
                ts: tailMatch[1],
                role: tailMatch[3],
            });
            continue;
        }
        const phaseMatch = PHASE_FILE_RE.exec(name);
        if (phaseMatch) {
            phaseFiles.push({
                absPath: path.join(entityDir, name),
                file: name,
                ts: phaseMatch[1],
                role: phaseMatch[3],
            });
        }
    }
    jsonls.sort((a, b) => a.ts.localeCompare(b.ts));
    const runs = [];
    for (let i = 0; i < jsonls.length; i++) {
        const jsonl = jsonls[i];
        const nextTs = jsonls[i + 1]?.ts;
        const events = readJsonlEvents(jsonl.absPath);
        const pipelineStart = events.find((e) => e.kind === "pipeline-start");
        const runId = typeof pipelineStart?.ts === "string" ? compactIso(pipelineStart.ts) : jsonl.ts;
        const inWindow = (pf) => pf.ts >= jsonl.ts && (nextTs === undefined || pf.ts < nextTs);
        const runPhaseFiles = phaseFiles.filter(inWindow);
        const runTailFiles = tailFiles.filter(inWindow);
        runs.push({
            runId,
            jsonlPath: jsonl.absPath,
            phaseFiles: runPhaseFiles,
            tailFiles: runTailFiles,
            manifest: buildManifestForRun(runId, events, runPhaseFiles, new Set(runTailFiles.map((tf) => tf.file)), identity, projectKey, fallbackEntityId, expandCompactIso(jsonl.ts), opts),
        });
    }
    return runs;
}
// ── Archive writes ──────────────────────────────────────────────────────
function writeFileAtomic(target, content) {
    const tmp = `${target}.tmp-${process.pid}`;
    fs.writeFileSync(tmp, content, "utf8");
    fs.renameSync(tmp, target);
}
function indexKey(projectKey, entityId, runId) {
    return `${projectKey}/${entityId}/${runId}`;
}
function upsertProject(projectKey, identity, newRun) {
    const registry = readProjects();
    const now = new Date().toISOString();
    const existing = registry.projects[projectKey];
    registry.projects[projectKey] = {
        path: identity.projectDir,
        prefix: identity.prefix,
        name: identity.name,
        firstSeen: existing?.firstSeen ?? now,
        lastSeen: now,
        runCount: (existing?.runCount ?? 0) + (newRun ? 1 : 0),
    };
    writeFileAtomic(getTranscriptProjectsPath(), `${JSON.stringify(registry, null, 2)}\n`);
}
/**
 * Archive one discovered run: gzip phase transcripts into the run dir, copy
 * orchestrator.jsonl as-is, write manifest.json (tmp+rename), append the
 * index line (only if this runId isn't already indexed — crash between
 * manifest write and index append heals here), and upsert projects.json.
 *
 * Idempotent: re-archiving the same run (resume with the same runId, or a
 * sweep racing a live pipeline) overwrites the manifest/payloads with newer,
 * fuller data and appends no duplicate index line. Best-effort, never throws.
 */
function writeRunArchive(run, identity, projectKey) {
    try {
        const { manifest } = run;
        const runDir = getRunArchiveDir(projectKey, manifest.entityId, run.runId);
        fs.mkdirSync(runDir, { recursive: true });
        for (const pf of [...run.phaseFiles, ...run.tailFiles]) {
            const gz = gzipSync(fs.readFileSync(pf.absPath));
            fs.writeFileSync(path.join(runDir, `${pf.file}.gz`), gz);
        }
        fs.copyFileSync(run.jsonlPath, path.join(runDir, "orchestrator.jsonl"));
        writeFileAtomic(path.join(runDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
        const alreadyIndexed = readIndex().some((e) => indexKey(e.projectKey, e.entityId, e.runId) === indexKey(projectKey, manifest.entityId, run.runId));
        if (!alreadyIndexed) {
            const entry = {
                runId: run.runId,
                projectKey,
                entityId: manifest.entityId,
                entityKind: manifest.entityKind,
                ...(manifest.sprintId ? { sprintId: manifest.sprintId } : {}),
                startedAt: manifest.startedAt,
                outcome: manifest.outcome,
                input: manifest.totals.input,
                output: manifest.totals.output,
                cost: manifest.totals.cost,
                ...(manifest.durationMs !== undefined ? { durationMs: manifest.durationMs } : {}),
                archivedAt: manifest.archivedAt,
            };
            // Single sub-1KB appendFileSync line — atomic on Linux.
            fs.appendFileSync(getTranscriptIndexPath(), `${JSON.stringify(entry)}\n`, "utf8");
        }
        upsertProject(projectKey, identity, !alreadyIndexed);
        return { archived: true, runDir };
    }
    catch (err) {
        const e = err;
        return { archived: false, error: e.message ?? "archive write failed" };
    }
}
/**
 * Archive the run identified by its project-local orchestrator transcript.
 * Called by orchestrators after the pipeline result is known. Best-effort,
 * never throws.
 */
export function archiveRun(opts) {
    try {
        const configPath = opts.configPath ?? path.join(opts.cwd, ".forge", "config.json");
        const identity = readProjectIdentity(configPath);
        const projectKey = computeProjectKey(identity.projectDir, identity.prefix);
        const entityDir = path.dirname(opts.orchestratorJsonlPath);
        const runs = buildRunsForEntityDir(entityDir, identity, projectKey, { sprintId: opts.sprintId });
        const wanted = path.resolve(opts.orchestratorJsonlPath);
        const run = runs.find((r) => path.resolve(r.jsonlPath) === wanted);
        if (!run)
            return { archived: false, error: `no run found for ${opts.orchestratorJsonlPath}` };
        return writeRunArchive(run, identity, projectKey);
    }
    catch (err) {
        const e = err;
        return { archived: false, error: e.message ?? "archiveRun failed" };
    }
}
/**
 * Adopt orphans: archive every project-local run whose runId isn't in the
 * central index. Covers crash recovery (pipeline died before its archive
 * call) and adoption of pre-existing history (transcripts written before
 * this feature shipped). Called best-effort at pipeline start; never throws.
 */
export function sweepProjectTranscripts(cwd, configPath) {
    try {
        const base = path.join(cwd, ".forge", "transcripts");
        if (!fs.existsSync(base))
            return { adopted: 0, errors: 0 };
        const identity = readProjectIdentity(configPath ?? path.join(cwd, ".forge", "config.json"));
        const projectKey = computeProjectKey(identity.projectDir, identity.prefix);
        const indexed = new Set(readIndex().map((e) => indexKey(e.projectKey, e.entityId, e.runId)));
        let adopted = 0;
        let errors = 0;
        for (const entry of fs.readdirSync(base, { withFileTypes: true })) {
            if (!entry.isDirectory())
                continue;
            const runs = buildRunsForEntityDir(path.join(base, entry.name), identity, projectKey);
            for (const run of runs) {
                if (indexed.has(indexKey(projectKey, run.manifest.entityId, run.runId)))
                    continue;
                const result = writeRunArchive(run, identity, projectKey);
                if (result.archived)
                    adopted++;
                else
                    errors++;
            }
        }
        return { adopted, errors };
    }
    catch {
        return { adopted: 0, errors: 1 };
    }
}
// ── Archive reads ───────────────────────────────────────────────────────
/** Read index.jsonl — skips malformed/truncated lines, never throws. */
export function readIndex() {
    const entries = [];
    for (const event of readJsonlEvents(getTranscriptIndexPath())) {
        if (Value.Check(IndexEntrySchema, event))
            entries.push(event);
    }
    return entries;
}
/** Read projects.json — empty registry on any failure, never throws. */
export function readProjects() {
    try {
        const raw = JSON.parse(fs.readFileSync(getTranscriptProjectsPath(), "utf8"));
        if (Value.Check(ProjectsRegistrySchema, raw))
            return raw;
    }
    catch {
        // fall through
    }
    return { version: 1, projects: {} };
}
/** Read `<runDir>/manifest.json` — null on missing/truncated/mismatched. */
export function readManifest(runDir) {
    try {
        const raw = JSON.parse(fs.readFileSync(path.join(runDir, "manifest.json"), "utf8"));
        if (Value.Check(RunManifestSchema, raw))
            return raw;
    }
    catch {
        // fall through
    }
    return null;
}
/**
 * Gunzip + parse an archived phase transcript. `file` is the manifest's
 * phase `file` field (without `.gz`). Null on any failure, never throws.
 */
export function gunzipPhase(runDir, file) {
    try {
        const gzPath = path.join(runDir, file.endsWith(".gz") ? file : `${file}.gz`);
        const parsed = JSON.parse(gunzipSync(fs.readFileSync(gzPath)).toString("utf8"));
        if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
            return parsed;
        }
    }
    catch {
        // fall through
    }
    return null;
}
/**
 * Gunzip + parse an archived live tail log (`*.tail.jsonl.gz`). Returns the
 * exact lines the dashboard rendered during the run, in order. Tolerant of
 * truncated tails; null when the file is missing/unreadable. Never throws.
 */
export function gunzipTailLog(runDir, file) {
    try {
        const gzPath = path.join(runDir, file.endsWith(".gz") ? file : `${file}.gz`);
        const raw = gunzipSync(fs.readFileSync(gzPath)).toString("utf8");
        const entries = [];
        for (const line of raw.split("\n")) {
            const trimmed = line.trim();
            if (!trimmed)
                continue;
            try {
                const parsed = JSON.parse(trimmed);
                if (typeof parsed.line === "string") {
                    entries.push(parsed.warning === true ? { line: parsed.line, warning: true } : { line: parsed.line });
                }
            }
            catch {
                // truncated tail line — skip
            }
        }
        return entries;
    }
    catch {
        return null;
    }
}
/** Resolve the archive dir for an index entry. */
export function runDirForEntry(entry) {
    return getRunArchiveDir(entry.projectKey, entry.entityId, entry.runId);
}
function fmtSize(n) {
    if (n >= 1_000_000)
        return `${(n / 1_000_000).toFixed(1)}M`;
    if (n >= 1_000)
        return `${(n / 1_000).toFixed(1)}k`;
    return String(n);
}
/**
 * Per-turn digest of a gunzipped phase payload — never a raw JSON dump.
 * Each assistant message is a turn: first text line + tool-call names;
 * toolResult messages render as compact result markers.
 */
export function digestPhasePayload(payload) {
    const messages = Array.isArray(payload.messages) ? payload.messages : [];
    const lines = [];
    let turn = 0;
    for (const msg of messages) {
        if (msg.role === "assistant") {
            turn++;
            const parts = Array.isArray(msg.content) ? msg.content : [];
            const textPart = parts.find((p) => p.type === "text" && typeof p.text === "string");
            const firstLine = textPart ? textPart.text.trim().split("\n")[0] : "";
            lines.push(`  t${turn}  ${firstLine ? (firstLine.length > 100 ? `${firstLine.slice(0, 100)}…` : firstLine) : "(no text)"}`);
            for (const p of parts) {
                if (p.type === "toolCall" && typeof p.name === "string")
                    lines.push(`       → ${p.name}`);
            }
        }
        else if (msg.role === "toolResult") {
            const size = typeof msg.content === "string" ? msg.content.length : JSON.stringify(msg.content ?? "").length;
            lines.push(`       ← ${msg.toolName ?? "tool"} ${msg.isError ? "✗" : "✓"} (${fmtSize(size)} chars)`);
        }
    }
    if (lines.length === 0)
        lines.push("  (no messages in payload)");
    return lines;
}
function firstLineOf(s, max = 120) {
    const line = s.trim().split("\n")[0];
    return line.length > max ? `${line.slice(0, max)}…` : line;
}
/** Extract readable text from a toolResult content value (string or parts array). */
function toolResultText(content) {
    if (typeof content === "string")
        return content;
    if (Array.isArray(content)) {
        return content
            .filter((p) => !!p && typeof p === "object" && p.type === "text" && typeof p.text === "string")
            .map((p) => p.text)
            .join("\n");
    }
    if (content === undefined || content === null)
        return "";
    try {
        return JSON.stringify(content);
    }
    catch {
        return "";
    }
}
/** Per-turn cap on assistant text in the verbose digest. */
const VERBOSE_TEXT_CAP = 1500;
/**
 * VERBOSE digest of a gunzipped phase payload — the replay-dashboard feed.
 * Unlike digestPhasePayload (compact markers for the `show` table), this
 * carries the actual transcript content so a replayed run is readable:
 * full assistant text (capped per turn), thinking first-lines, tool-call
 * argument previews, and tool-result first-line previews.
 *
 * Multi-line assistant text is pushed as ONE entry — the dashboard splits
 * lines at display time, and the tree's per-entry char cap still applies.
 */
export function digestPhasePayloadVerbose(payload) {
    const messages = Array.isArray(payload.messages) ? payload.messages : [];
    const lines = [];
    let turn = 0;
    for (const msg of messages) {
        if (msg.role === "assistant") {
            turn++;
            lines.push(`── t${turn} ──`);
            const parts = Array.isArray(msg.content) ? msg.content : [];
            for (const p of parts) {
                if (p.type === "thinking" && typeof p.thinking === "string" && p.thinking.trim()) {
                    lines.push(`✱ thinking: ${firstLineOf(p.thinking)}`);
                }
                else if (p.type === "text" && typeof p.text === "string" && p.text.trim()) {
                    const text = p.text.trim();
                    lines.push(text.length > VERBOSE_TEXT_CAP ? `${text.slice(0, VERBOSE_TEXT_CAP)}…(+${text.length - VERBOSE_TEXT_CAP}c)` : text);
                }
                else if (p.type === "toolCall" && typeof p.name === "string") {
                    let argsPreview = "";
                    try {
                        argsPreview = p.arguments === undefined ? "" : JSON.stringify(p.arguments);
                    }
                    catch {
                        argsPreview = "";
                    }
                    if (argsPreview.length > 140)
                        argsPreview = `${argsPreview.slice(0, 140)}…`;
                    lines.push(`→ ${p.name}${argsPreview && argsPreview !== "{}" ? ` ${argsPreview}` : ""}`);
                }
            }
        }
        else if (msg.role === "toolResult") {
            const text = toolResultText(msg.content);
            const preview = firstLineOf(text);
            lines.push(`← ${msg.toolName ?? "tool"} ${msg.isError ? "✗" : "✓"} (${fmtSize(text.length)}c)${preview ? `  ${preview}` : ""}`);
        }
    }
    if (lines.length === 0)
        lines.push("(no messages in payload)");
    return lines;
}
export { getTranscriptArchiveRoot };
//# sourceMappingURL=transcript-archive.js.map