import { createCompactionSummaryMessage, createCustomMessage } from './custom-messages.js';
export const CURRENT_SESSION_VERSION = 3;
function stripAgentMessageId(message) {
    const cleanMessage = { ...message };
    delete cleanMessage.id;
    return cleanMessage;
}
/** Exported for compaction.test.ts */
/** Tolerantly parse JSONL session content, skipping malformed lines. */
export function parseSessionEntries(content) {
    const entries = [];
    const lines = content.trim().split('\n');
    for (const line of lines) {
        if (!line.trim())
            continue;
        try {
            const entry = JSON.parse(line);
            entries.push(entry);
        }
        catch {
            // Skip malformed lines
        }
    }
    return entries;
}
// JSONL serialization, strict parsing, and invariants used by hydrate and replay.
// CLI import remains tolerant; runtime hydration fails fast on malformed traces.
const KNOWN_ENTRY_TYPES = new Set([
    'message',
    'thinking_level_change',
    'model_change',
    'compaction',
    'custom',
    'custom_message',
    'label',
    'session_info',
    'turn_start',
    'turn_end',
    'system_prompt',
    'tool_definitions',
    'tool_execution_end',
    'llm_call'
]);
/** Serialize file entries to JSONL text with a trailing newline. */
export function serializeFileEntries(entries) {
    if (entries.length === 0)
        return '';
    return entries.map((entry) => JSON.stringify(entry)).join('\n') + '\n';
}
/** Throws on malformed JSON / non-object / missing type. Skips unknown `type` as warning (forward-compat). */
export function parseSessionEntriesStrict(content) {
    if (typeof content !== 'string' || content.length === 0) {
        throw asParseError({ kind: 'empty', message: 'JSONL input is empty.' });
    }
    const rawLines = content.split('\n');
    const warnings = [];
    let header = null;
    const entries = [];
    let sawAnyContent = false;
    for (let i = 0; i < rawLines.length; i++) {
        const lineNo = i + 1;
        const raw = rawLines[i];
        if (raw.trim().length === 0)
            continue;
        let parsed;
        try {
            parsed = JSON.parse(raw);
        }
        catch (err) {
            throw asParseError({
                kind: 'malformed-line',
                message: `Line ${lineNo}: invalid JSON (${err.message}).`,
                line: lineNo
            });
        }
        if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
            throw asParseError({
                kind: 'invalid-shape',
                message: `Line ${lineNo}: expected JSON object, got ${describeParsedValue(parsed)}.`,
                line: lineNo
            });
        }
        const obj = parsed;
        if (typeof obj.type !== 'string') {
            throw asParseError({
                kind: 'invalid-shape',
                message: `Line ${lineNo}: missing or non-string \`type\` field.`,
                line: lineNo
            });
        }
        sawAnyContent = true;
        if (obj.type === 'session') {
            if (header !== null) {
                warnings.push(`Line ${lineNo}: duplicate session header — keeping the first.`);
                continue;
            }
            if (entries.length > 0) {
                warnings.push(`Line ${lineNo}: session header appears after entries — keeping it but order is unusual.`);
            }
            header = obj;
            continue;
        }
        if (!KNOWN_ENTRY_TYPES.has(obj.type)) {
            warnings.push(`Line ${lineNo}: unknown entry type "${obj.type}" — skipped.`);
            continue;
        }
        entries.push(obj);
    }
    if (!sawAnyContent) {
        throw asParseError({ kind: 'empty', message: 'JSONL input contains no parseable lines.' });
    }
    return { header, entries, warnings };
}
/**
 * Canonicalize older user-message entries into the multi-block array shape.
 * This is separate from strict parsing so parse/serialize roundtrips remain
 * byte-stable while hydrate still feeds a stable message shape to the runtime.
 */
export function canonicalizeUserMessageEntries(entries) {
    return entries.map((entry) => {
        if (entry.type !== 'message')
            return entry;
        const message = entry.message;
        if (message.role !== 'user' || typeof message.content !== 'string')
            return entry;
        const legacyText = message.content;
        return {
            ...entry,
            message: {
                ...message,
                content: [{ type: 'text', text: legacyText }]
            }
        };
    });
}
/**
 * Throws if any toolCall lacks a matching toolResult, or if any user message
 * violates the multi-block shape required by UI and LLM projection.
 */
export function assertSessionInvariants(entries) {
    const pendingToolCallIds = new Set();
    let lastAssistantEntryId;
    for (const entry of entries) {
        if (entry.type !== 'message')
            continue;
        const message = entry.message;
        if (message.role === 'user') {
            // owned AI accepts either raw string content or a content-block array. The
            // runtime emits arrays, and hydrate canonicalizes older string traces.
            // If an array is present, it must start with the user query text block.
            const content = message.content;
            if (typeof content !== 'string') {
                if (!Array.isArray(content) || content.length === 0) {
                    throw makeInvariantError({
                        kind: 'user-message-shape',
                        message: `User entry ${entry.id}: content must be a string or non-empty array (got ${typeof content}).`,
                        entryId: entry.id
                    });
                }
                const head = content[0];
                if (head === null ||
                    typeof head !== 'object' ||
                    head.type !== 'text' ||
                    typeof head.text !== 'string') {
                    throw makeInvariantError({
                        kind: 'user-message-shape',
                        message: `User entry ${entry.id}: content[0] must be a TextContent block (multi-block shape).`,
                        entryId: entry.id
                    });
                }
            }
            const boundary = message.contextResetBoundary;
            if (boundary !== undefined && boundary !== true) {
                throw makeInvariantError({
                    kind: 'user-message-shape',
                    message: `User entry ${entry.id}: contextResetBoundary must be exactly \`true\` when present (got ${JSON.stringify(boundary)}).`,
                    entryId: entry.id
                });
            }
            continue;
        }
        if (message.role === 'assistant') {
            if (pendingToolCallIds.size > 0) {
                throw makeInvariantError({
                    kind: 'tool-pairing',
                    message: `Assistant entry ${entry.id} appears before tool_results for ${pendingToolCallIds.size} pending tool_use(s) from a prior assistant turn (entry ${lastAssistantEntryId ?? 'unknown'}).`,
                    entryId: entry.id
                });
            }
            lastAssistantEntryId = entry.id;
            const content = message.content;
            if (Array.isArray(content)) {
                for (const block of content) {
                    if (block !== null &&
                        typeof block === 'object' &&
                        block.type === 'toolCall') {
                        const id = block.id;
                        if (typeof id !== 'string' || id.length === 0) {
                            throw makeInvariantError({
                                kind: 'tool-pairing',
                                message: `Assistant entry ${entry.id}: toolCall block missing string id.`,
                                entryId: entry.id
                            });
                        }
                        pendingToolCallIds.add(id);
                    }
                }
            }
            continue;
        }
        if (message.role === 'toolResult') {
            const id = message.toolCallId;
            if (typeof id !== 'string' || id.length === 0) {
                throw makeInvariantError({
                    kind: 'tool-pairing',
                    message: `toolResult entry ${entry.id} has no toolCallId.`,
                    entryId: entry.id
                });
            }
            if (!pendingToolCallIds.has(id)) {
                throw makeInvariantError({
                    kind: 'tool-pairing',
                    message: `toolResult entry ${entry.id} references unknown toolCallId ${id} (no preceding tool_use).`,
                    entryId: entry.id
                });
            }
            pendingToolCallIds.delete(id);
            continue;
        }
    }
    if (pendingToolCallIds.size > 0) {
        throw makeInvariantError({
            kind: 'tool-pairing',
            message: `Session ends with ${pendingToolCallIds.size} unresolved tool_use(s): ${[...pendingToolCallIds].join(', ')}.`,
            entryId: lastAssistantEntryId
        });
    }
}
/** Insert synthetic `toolResult` entries for every orphan `toolCall`. Pure. */
export function repairOrphanToolUses(entries, options = {}) {
    const text = options.syntheticResultText ?? 'Tool call did not complete (turn ended early).';
    const fallbackName = options.fallbackToolName ?? 'unknown';
    const out = [];
    const patched = [];
    const usedIds = new Set();
    for (const e of entries)
        usedIds.add(e.id);
    const generateId = options.generateId ?? (() => generateUniqueShortId(usedIds));
    const now = options.now ?? (() => new Date().toISOString());
    let pending = [];
    let lastAssistantParentId = null;
    const flushPending = (parentBefore) => {
        let parentId = parentBefore;
        for (const p of pending) {
            const id = generateId();
            usedIds.add(id);
            const synthetic = {
                type: 'message',
                id,
                parentId,
                timestamp: now(),
                message: {
                    role: 'toolResult',
                    toolCallId: p.id,
                    toolName: p.name,
                    content: [{ type: 'text', text }],
                    isError: true,
                    timestamp: 0
                }
            };
            out.push(synthetic);
            patched.push(p.id);
            parentId = id;
        }
        pending = [];
        return { lastId: parentId };
    };
    for (const entry of entries) {
        if (entry.type !== 'message') {
            if (pending.length > 0) {
                const { lastId } = flushPending(out.length > 0 ? out[out.length - 1].id : entry.parentId);
                out.push(rewireParent(entry, lastId));
            }
            else {
                out.push(entry);
            }
            continue;
        }
        const msg = entry.message;
        if (msg.role === 'assistant') {
            if (pending.length > 0) {
                const lastOutId = out.length > 0 ? out[out.length - 1].id : entry.parentId;
                const { lastId } = flushPending(lastOutId);
                out.push(rewireParent(entry, lastId));
            }
            else {
                out.push(entry);
            }
            lastAssistantParentId = entry.id;
            const content = msg.content;
            if (Array.isArray(content)) {
                for (const block of content) {
                    if (block !== null &&
                        typeof block === 'object' &&
                        block.type === 'toolCall') {
                        const id = block.id;
                        const name = block.name;
                        if (typeof id === 'string' && id.length > 0) {
                            pending.push({
                                id,
                                name: typeof name === 'string' && name.length > 0 ? name : fallbackName
                            });
                        }
                    }
                }
            }
            continue;
        }
        if (msg.role === 'toolResult') {
            const id = msg.toolCallId;
            if (typeof id === 'string') {
                pending = pending.filter((p) => p.id !== id);
            }
            out.push(entry);
            continue;
        }
        if (pending.length > 0) {
            const lastOutId = out.length > 0 ? out[out.length - 1].id : entry.parentId;
            const { lastId } = flushPending(lastOutId);
            out.push(rewireParent(entry, lastId));
        }
        else {
            out.push(entry);
        }
    }
    if (pending.length > 0) {
        flushPending(out.length > 0 ? out[out.length - 1].id : lastAssistantParentId);
    }
    return { entries: out, patchedToolCallIds: patched };
}
/** Re-link a session entry to a new parent id, returning a copy on change. */
function rewireParent(entry, newParentId) {
    if (entry.parentId === newParentId)
        return entry;
    return { ...entry, parentId: newParentId };
}
/** Allocate an 8-char id that does not collide with the provided used-set. */
function generateUniqueShortId(used) {
    const c = globalThis.crypto;
    if (!c || typeof c.randomUUID !== 'function') {
        throw new Error('repairOrphanToolUses requires globalThis.crypto.randomUUID.');
    }
    for (let i = 0; i < 100; i++) {
        const id = c.randomUUID().slice(0, 8);
        if (!used.has(id)) {
            used.add(id);
            return id;
        }
    }
    const full = c.randomUUID();
    used.add(full);
    return full;
}
/** Wrap a parse-error detail in a typed Error subclass. */
function asParseError(detail) {
    const err = new Error(detail.message);
    err.name = 'SessionParseError';
    err.detail = detail;
    return err;
}
/** Wrap a session invariant-violation detail in a typed Error subclass. */
function makeInvariantError(detail) {
    const err = new Error(detail.message);
    err.name = 'SessionInvariantViolation';
    err.detail = detail;
    return err;
}
/** Describe a parsed JSON value as a human-readable type name for error messages. */
function describeParsedValue(value) {
    if (value === null)
        return 'null';
    if (Array.isArray(value))
        return 'array';
    return typeof value;
}
/** Find the most recent compaction entry in a list, or null if none exists. */
export function getLatestCompactionEntry(entries) {
    for (let i = entries.length - 1; i >= 0; i--) {
        if (entries[i].type === 'compaction') {
            return entries[i];
        }
    }
    return null;
}
/**
 * Build the session context from entries using tree traversal.
 * If leafId is provided, walks from that entry to root.
 * Handles compaction and branch summaries along the path.
 */
function buildSessionContextProjection(entries, leafId, byId) {
    // Build uuid index if not available
    if (!byId) {
        byId = new Map();
        for (const entry of entries) {
            byId.set(entry.id, entry);
        }
    }
    // Find leaf
    let leaf;
    if (leafId === null) {
        // Explicitly null - return no messages (navigated to before first entry)
        return { projections: [], thinkingLevel: 'off', model: null };
    }
    if (leafId) {
        leaf = byId.get(leafId);
    }
    if (!leaf) {
        // Fallback to last entry (when leafId is undefined)
        leaf = entries[entries.length - 1];
    }
    if (!leaf) {
        return { projections: [], thinkingLevel: 'off', model: null };
    }
    // Walk from leaf to root, collecting path
    const path = [];
    let current = leaf;
    while (current) {
        path.unshift(current);
        current = current.parentId ? byId.get(current.parentId) : undefined;
    }
    // Extract settings and find compaction
    let thinkingLevel = 'off';
    let model = null;
    let compaction = null;
    for (const entry of path) {
        if (entry.type === 'thinking_level_change') {
            thinkingLevel = entry.thinkingLevel;
        }
        else if (entry.type === 'model_change') {
            model = { provider: entry.provider, modelId: entry.modelId };
        }
        else if (entry.type === 'message' && entry.message.role === 'assistant') {
            model = { provider: entry.message.provider, modelId: entry.message.model };
        }
        else if (entry.type === 'compaction') {
            compaction = entry;
        }
    }
    // Build messages and collect corresponding entries
    // When there's a compaction, we need to:
    // 1. Emit summary first (entry = compaction)
    // 2. Emit kept messages (from firstKeptEntryId up to compaction)
    // 3. Emit messages after compaction
    const projections = [];
    const appendMessage = (entry) => {
        if (entry.type === 'message') {
            projections.push({ message: entry.message, entryId: entry.id });
        }
        else if (entry.type === 'custom_message') {
            projections.push({
                message: createCustomMessage(entry.customType, entry.content, entry.display, entry.details, entry.timestamp),
                entryId: entry.id
            });
        }
    };
    if (compaction) {
        // Emit summary first
        projections.push({
            message: createCompactionSummaryMessage(compaction.summary, compaction.tokensBefore, compaction.timestamp),
            entryId: compaction.id
        });
        // Find compaction index in path
        const compactionIdx = path.findIndex((e) => e.type === 'compaction' && e.id === compaction.id);
        // Emit kept messages (before compaction, starting from firstKeptEntryId)
        let foundFirstKept = false;
        for (let i = 0; i < compactionIdx; i++) {
            const entry = path[i];
            if (entry.id === compaction.firstKeptEntryId) {
                foundFirstKept = true;
            }
            if (foundFirstKept) {
                appendMessage(entry);
            }
        }
        // Emit messages after compaction
        for (let i = compactionIdx + 1; i < path.length; i++) {
            const entry = path[i];
            appendMessage(entry);
        }
    }
    else {
        // No compaction - emit all messages, handle branch summaries and custom messages
        for (const entry of path) {
            appendMessage(entry);
        }
    }
    return { projections: dropOrphanedToolResultProjections(projections), thinkingLevel, model };
}
/** Build the LLM context (messages, thinking level, model) by walking entries to the leaf. */
export function buildSessionContext(entries, leafId, byId) {
    const projection = buildSessionContextProjection(entries, leafId, byId);
    return {
        messages: projection.projections.map((item) => stripAgentMessageId(item.message)),
        messageEntryIds: projection.projections.map((item) => item.entryId),
        thinkingLevel: projection.thinkingLevel,
        model: projection.model
    };
}
/** Return the entry ids of every message that participates in the projected LLM context. */
export function buildSessionContextMessageEntryIds(entries, leafId, byId) {
    return buildSessionContextProjection(entries, leafId, byId).projections.map((item) => item.entryId);
}
/** Drop tool-result projections that reference unknown tool calls so the LLM context stays consistent. */
function dropOrphanedToolResultProjections(projections) {
    const repaired = [];
    let pendingToolCallIds = new Set();
    for (const projection of projections) {
        const { message } = projection;
        if (message.role === 'assistant') {
            pendingToolCallIds = new Set(message.content
                .filter((block) => block.type === 'toolCall')
                .map((block) => block.id));
            repaired.push(projection);
            continue;
        }
        if (message.role === 'toolResult') {
            if (!pendingToolCallIds.has(message.toolCallId)) {
                continue;
            }
            pendingToolCallIds.delete(message.toolCallId);
            repaired.push(projection);
            continue;
        }
        pendingToolCallIds = new Set();
        repaired.push(projection);
    }
    return repaired;
}
//# sourceMappingURL=session-schema.js.map