import { createHash } from "node:crypto";
import { existsSync, readFileSync } from "node:fs";
import { parseSessionEntries, SessionManager } from "@mariozechner/pi-coding-agent";
import { childTraceOrder, compareTraceOrder, eventTraceOrder, transcriptTraceOrder } from "../../shared/trace-order.js";
import { isChatWebSessionArchived } from "./session-metadata.js";
export async function loadPiSessionMetadata(session, cwd = process.cwd()) {
    const piSession = await findPiSession(session, cwd);
    return metadataFromPiSession(piSession);
}
function metadataFromPiSession(piSession) {
    if (!piSession)
        return {};
    return {
        sessionPath: piSession.path,
        name: piSession.name,
        firstMessage: piSession.firstMessage,
        modified: piSession.modified,
    };
}
export async function buildSessionNodes(sessions, indexItems, cwd = process.cwd(), unreadCounts = new Map()) {
    const indexByKey = new Map(indexItems.map((item) => [item.piboSessionId, item]));
    const nodes = new Map();
    const piSessionsByCwd = new Map();
    for (const session of sessions) {
        const sessionCwd = session.workspace ?? cwd;
        let piSessions = piSessionsByCwd.get(sessionCwd);
        if (!piSessions) {
            piSessions = listPiSessions(sessionCwd);
            piSessionsByCwd.set(sessionCwd, piSessions);
        }
        const metadata = metadataFromPiSession((await piSessions).find((piSession) => piSession.id === session.piSessionId));
        const indexed = indexByKey.get(session.id);
        nodes.set(session.id, {
            piboSessionId: session.id,
            piSessionId: session.piSessionId,
            parentId: session.parentId,
            originId: session.originId,
            profile: session.profile,
            subagentName: stringValue(session.metadata?.subagentName),
            title: createSessionTitle(session, metadata),
            subtitle: session.id,
            archived: isChatWebSessionArchived(session),
            status: indexed?.status ?? "idle",
            lastActivityAt: indexed?.lastActivityAt ?? metadata.modified ?? session.updatedAt,
            unreadCount: unreadCounts.get(session.id) || undefined,
            derivedSessions: [],
            children: [],
        });
    }
    const roots = [];
    for (const node of nodes.values()) {
        const parent = node.parentId ? nodes.get(node.parentId) : undefined;
        if (parent) {
            parent.children.push(node);
        }
        else {
            roots.push(node);
        }
    }
    for (const node of nodes.values()) {
        if (!node.originId)
            continue;
        const origin = nodes.get(node.originId);
        if (!origin)
            continue;
        origin.derivedSessions.push({
            piboSessionId: node.piboSessionId,
            profile: node.profile,
            subagentName: node.subagentName,
            title: node.title,
            status: node.status,
            lastActivityAt: node.lastActivityAt,
        });
    }
    const sortNodes = (items) => {
        items.sort((left, right) => (right.lastActivityAt ?? "").localeCompare(left.lastActivityAt ?? ""));
        for (const item of items) {
            item.derivedSessions.sort((left, right) => (right.lastActivityAt ?? "").localeCompare(left.lastActivityAt ?? ""));
            sortNodes(item.children);
        }
    };
    sortNodes(roots);
    return roots;
}
export async function buildTraceView(input) {
    const metadata = await loadPiSessionMetadata(input.session, input.session.workspace ?? input.cwd);
    const allEntries = metadata.sessionPath ? readEntries(metadata.sessionPath) : [];
    const sessionStatus = input.status ?? "idle";
    const openTranscriptEventIds = findOpenTranscriptEventIds(input.events, sessionStatus);
    const entries = projectTranscriptEntries(allEntries, sessionStatus, openTranscriptEventIds);
    const nodes = traceNodesFromEntries(input.session.id, entries);
    const byId = mapTraceNodesById(nodes);
    const childByParent = mapChildren(input.sessions);
    const linkedChildByToolCallId = mapSubagentSessionLinks(input.events);
    const hasPersistedTranscript = entries.some((entry) => entry.type === "message");
    for (const storedEvent of input.events) {
        if (hasPersistedTranscript &&
            isTranscriptEchoEvent(storedEvent.payload) &&
            !shouldKeepTranscriptEchoEvent(storedEvent.payload, openTranscriptEventIds)) {
            continue;
        }
        if (hasPersistedTranscript && isStaleToolCallEchoEvent(storedEvent.payload, sessionStatus)) {
            continue;
        }
        if (storedEvent.payload.type === "assistant_delta") {
            mergeAssistantDeltaEvent(nodes, byId, storedEvent.payload, sessionStatus, storedEvent.createdAt, storedEvent.eventSequence);
            continue;
        }
        if (storedEvent.payload.type === "thinking_delta") {
            mergeThinkingDeltaEvent(nodes, byId, storedEvent.payload, sessionStatus, storedEvent.createdAt, storedEvent.eventSequence);
            continue;
        }
        const node = traceNodeFromEvent(input.session.id, storedEvent.payload, childByParent, linkedChildByToolCallId, sessionStatus, storedEvent.createdAt, storedEvent.eventSequence);
        if (!node)
            continue;
        if (node.type === "agent.turn" && node.eventId) {
            const existingTurn = [...byId.values()].find((candidate) => candidate.type === "agent.turn" && candidate.eventId === node.eventId);
            if (existingTurn) {
                existingTurn.status = node.status;
                existingTurn.completedAt = node.completedAt ?? existingTurn.completedAt;
                continue;
            }
        }
        if (node.type === "assistant.message") {
            const existing = byId.get(node.id);
            if (existing) {
                mergeAssistantMessageEvent(existing, node);
                closeParentTurnForFinalAssistant(byId, existing);
                continue;
            }
            closeParentTurnForFinalAssistant(byId, node);
        }
        if (node.type === "model.reasoning") {
            const existing = byId.get(node.id);
            if (existing) {
                mergeReasoningEvent(existing, node);
                continue;
            }
        }
        if (node.toolCallId) {
            const existing = [...byId.values()].find((candidate) => candidate.toolCallId === node.toolCallId &&
                (candidate.type === "tool.call" || candidate.type === "agent.delegation"));
            if (existing) {
                if (isRunStartToolNode(existing) && node.type === "agent.delegation") {
                    attachAsyncAgentRunNode(existing, input.session.id, storedEvent.createdAt, node);
                    continue;
                }
                mergeToolEvent(existing, node);
                attachAsyncAgentRunNode(existing, input.session.id, storedEvent.createdAt);
                continue;
            }
        }
        if (node.eventId && byId.has(node.id))
            continue;
        attachAsyncAgentRunNode(node, input.session.id, storedEvent.createdAt);
        nodes.push(node);
        for (const indexed of flattenTraceNodes([node]))
            byId.set(indexed.id, indexed);
    }
    const nestedNodes = nestTraceNodes(nodes);
    reconcileAsyncAgentRunStatuses(nestedNodes);
    sortTraceNodes(nestedNodes);
    return {
        piboSessionId: input.session.id,
        piSessionId: input.session.piSessionId,
        title: createSessionTitle(input.session, metadata),
        version: createTraceViewVersion({
            session: input.session,
            sessions: input.sessions,
            events: input.events,
            status: sessionStatus,
            metadata,
            latestStreamId: input.latestStreamId,
        }),
        latestStreamId: input.latestStreamId,
        nodes: nestedNodes,
        rawEvents: input.includeRawEvents === false ? [] : input.events.slice(-(input.rawEventsLimit ?? input.events.length)),
    };
}
export function createTraceViewVersion(input) {
    const relevantSessions = input.sessions
        .map((session) => ({
        id: session.id,
        parentId: session.parentId ?? null,
        originId: session.originId ?? null,
        updatedAt: session.updatedAt,
        title: session.title ?? null,
    }))
        .sort((left, right) => left.id.localeCompare(right.id));
    const eventTail = input.events.at(-1);
    return createHash("sha1")
        .update(JSON.stringify({
        session: {
            id: input.session.id,
            piSessionId: input.session.piSessionId,
            profile: input.session.profile,
            title: input.session.title ?? null,
            updatedAt: input.session.updatedAt,
        },
        metadata: {
            name: input.metadata?.name ?? null,
            firstMessage: input.metadata?.firstMessage ?? null,
            modified: input.metadata?.modified ?? null,
        },
        status: input.status ?? "idle",
        events: {
            lastSequence: eventTail?.eventSequence ?? null,
            lastCreatedAt: eventTail?.createdAt ?? null,
            latestStreamId: input.latestStreamId ?? null,
        },
        sessions: relevantSessions,
    }))
        .digest("hex");
}
export async function listPiSessions(cwd = process.cwd()) {
    const sessions = await SessionManager.list(cwd);
    return sessions.map((session) => ({
        path: session.path,
        id: session.id,
        cwd: session.cwd,
        name: session.name,
        parentSessionPath: session.parentSessionPath,
        created: session.created.toISOString(),
        modified: session.modified.toISOString(),
        messageCount: session.messageCount,
        firstMessage: session.firstMessage,
    }));
}
function createSessionTitle(session, metadata) {
    const candidate = session.title || metadata.name || metadata.firstMessage || session.id;
    return truncateTitle(candidate);
}
function truncateTitle(title, maxLength = 56) {
    const normalized = title.replace(/\s+/g, " ").trim();
    if (normalized.length <= maxLength)
        return normalized || "Untitled Session";
    return `${normalized.slice(0, maxLength - 1)}…`;
}
async function findPiSession(piboSession, cwd) {
    const sessions = await listPiSessions(cwd);
    return sessions.find((session) => session.id === piboSession.piSessionId);
}
function readEntries(path) {
    if (!existsSync(path))
        return [];
    const content = readFileSync(path, "utf8");
    return parseSessionEntries(content).filter((entry) => entry.type !== "session");
}
function projectTranscriptEntries(entries, sessionStatus, openTranscriptEventIds) {
    if (sessionStatus !== "running" || openTranscriptEventIds.size === 0)
        return entries;
    let lastUserMessageIndex = -1;
    for (let index = entries.length - 1; index >= 0; index -= 1) {
        const entry = entries[index];
        if (entry.type === "message" && messageRole(entry) === "user") {
            lastUserMessageIndex = index;
            break;
        }
    }
    return lastUserMessageIndex === -1 ? entries : entries.slice(0, lastUserMessageIndex);
}
function messageRole(entry) {
    return entry.message.role;
}
function messageContent(entry) {
    return entry.message.content;
}
function messageParts(entry) {
    const content = messageContent(entry);
    if (typeof content === "string")
        return [{ type: "text", text: content }];
    return Array.isArray(content) ? content : [];
}
export function traceNodesFromEntries(piboSessionId, entries) {
    const nodes = [];
    for (let index = 0; index < entries.length; index += 1) {
        const entry = entries[index];
        if (entry.type === "message") {
            const role = messageRole(entry);
            if (role === "user") {
                nodes.push(createUserMessageNode(piboSessionId, entry, messageContent(entry), index));
            }
            else if (role === "assistant" || role === "toolResult") {
                const turn = collectAssistantTurn(entries, index);
                nodes.push(...createAssistantTurnNodes(piboSessionId, turn.entries));
                index = turn.nextIndex - 1;
            }
        }
        else if (entry.type === "session_info" && entry.name) {
            nodes.push({
                id: `entry:${entry.id}`,
                entryId: entry.id,
                piboSessionId,
                type: "execution.command",
                title: "Session Info",
                status: "done",
                startedAt: entry.timestamp,
                output: { name: entry.name },
                source: "transcript",
                stableKey: `entry:${entry.id}`,
                orderKey: transcriptTraceOrder(index, 0, "execution.command"),
                children: [],
            });
        }
    }
    reconcileAsyncAgentRunStatuses(nodes);
    sortTraceNodes(nodes);
    return nodes;
}
function collectAssistantTurn(entries, startIndex) {
    const turnEntries = [];
    let index = startIndex;
    while (index < entries.length) {
        const entry = entries[index];
        if (entry.type !== "message")
            break;
        const role = messageRole(entry);
        if (role !== "assistant" && role !== "toolResult")
            break;
        turnEntries.push({ entry, index });
        index += 1;
    }
    return { entries: turnEntries, nextIndex: index };
}
function createUserMessageNode(piboSessionId, entry, content, entryIndex) {
    const text = extractText(content);
    const notification = parseRunNotificationText(text);
    if (notification) {
        return createRunNotificationNode({
            id: `entry:${entry.id}`,
            entryId: entry.id,
            piboSessionId,
            startedAt: entry.timestamp,
            orderKey: transcriptTraceOrder(entryIndex, 0, "yielded.run"),
            source: "transcript",
            stableKey: `entry:${entry.id}`,
            notification,
        });
    }
    return {
        id: `entry:${entry.id}`,
        entryId: entry.id,
        piboSessionId,
        type: "user.message",
        title: "User Message",
        status: "done",
        startedAt: entry.timestamp,
        summary: text,
        output: text,
        source: "transcript",
        stableKey: `entry:${entry.id}`,
        orderKey: transcriptTraceOrder(entryIndex, 0, "user.message"),
        children: [],
    };
}
function createAssistantTurnNodes(piboSessionId, entries) {
    const firstAssistant = entries.find(({ entry }) => messageRole(entry) === "assistant");
    if (!firstAssistant)
        return [];
    const orderedNodes = [];
    const toolsByCallId = new Map();
    for (const { entry, index: entryIndex } of entries) {
        if (messageRole(entry) === "toolResult") {
            mergePersistedToolResult(toolsByCallId, orderedNodes, entry, piboSessionId, entryIndex);
            continue;
        }
        const responseStatus = messageStatus(entry.message);
        const responseError = messageError(entry.message);
        let responseNode;
        for (const [index, part] of messageParts(entry).entries()) {
            const typed = part;
            if (typed.type === "thinking" && typeof typed.thinking === "string" && hasVisibleText(typed.thinking)) {
                orderedNodes.push(createReasoningNode(piboSessionId, entry, entryIndex, index, typed.thinking));
            }
            else if (typed.type === "text" && typeof typed.text === "string" && typed.text !== "") {
                if (!responseNode) {
                    responseNode = createAssistantMessageNode({
                        id: `entry:${entry.id}:response`,
                        piboSessionId,
                        entry,
                        entryIndex,
                        contentPartIndex: index,
                        status: responseStatus,
                        text: typed.text,
                        error: responseError,
                        children: [],
                        startedAt: entry.timestamp,
                        completedAt: entry.timestamp,
                    });
                    orderedNodes.push(responseNode);
                }
                else {
                    responseNode.summary = `${typeof responseNode.summary === "string" ? responseNode.summary : ""}${typed.text}`;
                    responseNode.output = `${typeof responseNode.output === "string" ? responseNode.output : ""}${typed.text}`;
                    responseNode.completedAt = entry.timestamp;
                }
            }
            else if (typed.type === "toolCall" && typeof typed.id === "string" && typeof typed.name === "string") {
                const toolNode = createToolCallNode(piboSessionId, entry, entryIndex, index, typed);
                orderedNodes.push(toolNode);
                toolsByCallId.set(typed.id, toolNode);
            }
        }
        if (responseNode) {
            responseNode.status = responseStatus;
            responseNode.error = responseError;
        }
    }
    return orderedNodes;
}
function createReasoningNode(piboSessionId, entry, entryIndex, index, thinking) {
    return {
        id: `entry:${entry.id}:thinking:${index}`,
        entryId: entry.id,
        piboSessionId,
        type: "model.reasoning",
        title: "Thinking",
        status: "done",
        startedAt: entry.timestamp,
        summary: thinking,
        output: thinking,
        source: "transcript",
        stableKey: `entry:${entry.id}:thinking:${index}`,
        orderKey: transcriptTraceOrder(entryIndex, index, "model.reasoning"),
        children: [],
    };
}
function createToolCallNode(piboSessionId, entry, entryIndex, contentPartIndex, part) {
    const name = typeof part.name === "string" ? part.name : "Tool Call";
    const toolCallId = typeof part.id === "string" ? part.id : undefined;
    return {
        id: `entry:${entry.id}:tool:${String(part.id)}`,
        entryId: entry.id,
        piboSessionId,
        toolCallId,
        type: isSubagentToolName(name) ? "agent.delegation" : "tool.call",
        title: name,
        status: "done",
        startedAt: entry.timestamp,
        input: part.arguments ?? {},
        source: "transcript",
        stableKey: toolCallId ? `tool:${toolCallId}` : `entry:${entry.id}:tool:${contentPartIndex}`,
        orderKey: transcriptTraceOrder(entryIndex, contentPartIndex, isSubagentToolName(name) ? "agent.delegation" : "tool.call"),
        children: [],
    };
}
function mergePersistedToolResult(toolsByCallId, childNodes, entry, piboSessionId, entryIndex) {
    const message = entry.message;
    const toolCallId = typeof message.toolCallId === "string" ? message.toolCallId : undefined;
    if (!toolCallId)
        return;
    let toolNode = toolsByCallId.get(toolCallId);
    if (!toolNode) {
        toolNode = createMissingToolResultNode(piboSessionId, entry, entryIndex, toolCallId);
        childNodes.push(toolNode);
        toolsByCallId.set(toolCallId, toolNode);
    }
    toolNode.status = message.isError === true ? "error" : "done";
    toolNode.completedAt = entry.timestamp;
    toolNode.output = toolResultOutput(message);
    toolNode.error = message.isError === true ? stringifyPreview(toolNode.output) : undefined;
    attachAsyncAgentRunNode(toolNode, piboSessionId, entry.timestamp);
}
function createMissingToolResultNode(piboSessionId, entry, entryIndex, toolCallId) {
    const message = entry.message;
    return {
        id: `entry:${entry.id}:tool-result:${toolCallId}`,
        entryId: entry.id,
        piboSessionId,
        toolCallId,
        type: "tool.result",
        title: typeof message.toolName === "string" ? message.toolName : "Tool Result",
        status: "done",
        startedAt: entry.timestamp,
        source: "transcript",
        stableKey: `tool:${toolCallId}`,
        orderKey: transcriptTraceOrder(entryIndex, 0, "tool.result"),
        children: [],
    };
}
function toolResultOutput(message) {
    if (message.details === undefined)
        return { content: message.content };
    return { content: message.content, details: message.details };
}
function createAssistantMessageNode(input) {
    return {
        id: input.id,
        entryId: input.entry.id,
        piboSessionId: input.piboSessionId,
        type: "assistant.message",
        title: "Agent Message",
        status: input.status,
        startedAt: input.startedAt ?? input.entry.timestamp,
        completedAt: input.completedAt,
        summary: input.text,
        output: input.text,
        error: input.error,
        source: "transcript",
        stableKey: `entry:${input.entry.id}:response:${input.contentPartIndex}`,
        orderKey: transcriptTraceOrder(input.entryIndex, input.contentPartIndex, "assistant.message"),
        children: input.children ?? [],
    };
}
function traceNodeFromEvent(piboSessionId, event, childByParent, linkedChildByToolCallId, sessionStatus, createdAt, eventSequence) {
    const eventId = "eventId" in event && typeof event.eventId === "string" ? event.eventId : undefined;
    const id = `event:${event.type}:${eventId ?? cryptoSafeId(event)}`;
    const turnParentId = eventId ? messageTurnNodeId(eventId) : undefined;
    const base = {
        id,
        piboSessionId,
        eventId,
        startedAt: createdAt,
        source: "event-log",
        stableKey: eventStableKey(event),
        orderKey: eventTraceNodeOrder(eventSequence, event.type),
        children: [],
    };
    switch (event.type) {
        case "message_queued": {
            const notification = parseRunNotificationText(event.text);
            if (event.source === "service" && notification) {
                return createRunNotificationNode({
                    id,
                    piboSessionId,
                    eventId,
                    startedAt: createdAt,
                    source: "event-log",
                    stableKey: eventId ? `run-notification:${eventId}` : id,
                    orderKey: eventTraceNodeOrder(eventSequence, event.type),
                    notification,
                });
            }
            return {
                ...base,
                type: "user.message",
                title: "User Message",
                status: "done",
                summary: event.text,
                output: event.text,
            };
        }
        case "message_started":
        case "message_finished":
            if (event.source === "service")
                return undefined;
            return {
                ...base,
                id: eventId ? messageTurnNodeId(eventId) : id,
                type: "agent.turn",
                title: "Agent Turn",
                status: event.type === "message_finished" || sessionStatus !== "running" ? "done" : "running",
                completedAt: event.type === "message_finished" ? createdAt : undefined,
                summary: event.type === "message_started" ? event.text : undefined,
                input: event.type === "message_started" ? { text: event.text, source: event.source } : undefined,
                stableKey: eventId ? `turn:${eventId}` : base.stableKey,
            };
        case "thinking_finished": {
            if (!hasVisibleText(event.text))
                return undefined;
            const thinkingId = thinkingEventNodeId(event);
            return {
                ...base,
                id: thinkingId ? thinkingNodeId(thinkingId) : id,
                parentId: turnParentId,
                type: "model.reasoning",
                title: "Thinking",
                status: "done",
                summary: event.text,
                output: event.text,
                stableKey: thinkingId ? `reasoning:${thinkingId}` : base.stableKey,
            };
        }
        case "assistant_message": {
            const assistantId = assistantEventNodeId(event);
            return {
                ...base,
                id: assistantId ? assistantMessageNodeId(assistantId) : id,
                parentId: turnParentId,
                type: "assistant.message",
                title: "Agent Message",
                status: "done",
                summary: event.text,
                output: event.text,
                stableKey: assistantId ? `assistant:${assistantId}` : base.stableKey,
            };
        }
        case "tool_call":
        case "tool_execution_started":
        case "tool_execution_updated":
        case "tool_execution_finished": {
            const subagentTool = isSubagentToolName(event.toolName);
            const linkedPiboSessionId = linkedChildByToolCallId.get(event.toolCallId) ??
                (subagentTool ? findLikelyChildSession(piboSessionId, event.toolName, event, childByParent) : undefined);
            return {
                ...base,
                id: `tool:${event.toolCallId}`,
                parentId: turnParentId,
                toolCallId: event.toolCallId,
                type: subagentTool ? "agent.delegation" : "tool.call",
                title: event.toolName,
                status: event.type === "tool_execution_finished"
                    ? event.isError
                        ? "error"
                        : "done"
                    : sessionStatus === "running" &&
                        (event.type === "tool_execution_started" || event.type === "tool_execution_updated")
                        ? "running"
                        : "done",
                completedAt: event.type === "tool_execution_finished" ? createdAt : undefined,
                input: "args" in event ? event.args : undefined,
                output: event.type === "tool_execution_finished"
                    ? event.result
                    : event.type === "tool_execution_updated"
                        ? event.partialResult
                        : undefined,
                error: event.type === "tool_execution_finished" && event.isError ? stringifyPreview(event.result) : undefined,
                linkedPiboSessionId,
                stableKey: `tool:${event.toolCallId}`,
                children: [],
            };
        }
        case "subagent_session":
            return {
                ...base,
                id: event.toolCallId ? `tool:${event.toolCallId}` : id,
                eventId,
                toolCallId: event.toolCallId,
                type: "agent.delegation",
                title: event.toolName,
                status: sessionStatus === "running" ? "running" : "done",
                summary: event.subagentName,
                input: { subagentName: event.subagentName, threadKey: event.threadKey },
                linkedPiboSessionId: event.childPiboSessionId,
                stableKey: event.toolCallId ? `tool:${event.toolCallId}` : `subagent:${event.childPiboSessionId}`,
                children: [],
            };
        case "execution_result":
            if (isInternalSessionOperation(event.action))
                return undefined;
            return {
                ...base,
                type: "execution.command",
                title: event.action,
                status: "done",
                input: { action: event.action },
                output: event.result,
            };
        case "session_error":
            return {
                ...base,
                type: "error",
                title: "Error",
                status: "error",
                error: event.error,
                output: event.error,
            };
        default:
            return undefined;
    }
}
function mergeAssistantDeltaEvent(nodes, byId, event, sessionStatus, createdAt, eventSequence) {
    if (event.text.length === 0)
        return;
    const assistantId = assistantEventNodeId(event);
    const id = assistantId ? assistantMessageNodeId(assistantId) : `event:assistant_delta:${cryptoSafeId(event)}`;
    const existing = byId.get(id);
    if (existing) {
        const text = `${typeof existing.output === "string" ? existing.output : ""}${event.text}`;
        existing.status = sessionStatus === "running" ? "running" : "done";
        existing.summary = text;
        existing.output = text;
        return;
    }
    const node = {
        id,
        parentId: event.eventId ? messageTurnNodeId(event.eventId) : undefined,
        piboSessionId: event.piboSessionId,
        eventId: event.eventId,
        type: "assistant.message",
        title: "Agent Message",
        status: sessionStatus === "running" ? "running" : "done",
        startedAt: createdAt,
        summary: event.text,
        output: event.text,
        source: "event-log",
        stableKey: assistantId ? `assistant:${assistantId}` : id,
        orderKey: eventTraceNodeOrder(eventSequence, event.type),
        children: [],
    };
    nodes.push(node);
    byId.set(node.id, node);
}
function mergeAssistantMessageEvent(target, update) {
    target.status = update.status;
    target.summary = update.summary ?? target.summary;
    target.output = update.output ?? target.output;
    target.error = update.error ?? target.error;
    target.completedAt = update.completedAt ?? target.completedAt;
}
function mergeThinkingDeltaEvent(nodes, byId, event, sessionStatus, createdAt, eventSequence) {
    if (event.text.length === 0)
        return;
    const thinkingId = thinkingEventNodeId(event);
    const id = thinkingId ? thinkingNodeId(thinkingId) : `event:thinking_delta:${cryptoSafeId(event)}`;
    const existing = byId.get(id);
    if (existing) {
        const text = `${typeof existing.output === "string" ? existing.output : ""}${event.text}`;
        existing.status = sessionStatus === "running" ? "running" : "done";
        existing.summary = text;
        existing.output = text;
        return;
    }
    const node = {
        id,
        parentId: event.eventId ? messageTurnNodeId(event.eventId) : undefined,
        piboSessionId: event.piboSessionId,
        eventId: event.eventId,
        type: "model.reasoning",
        title: "Thinking",
        status: sessionStatus === "running" ? "running" : "done",
        startedAt: createdAt,
        summary: event.text,
        output: event.text,
        source: "event-log",
        stableKey: thinkingId ? `reasoning:${thinkingId}` : id,
        orderKey: eventTraceNodeOrder(eventSequence, event.type),
        children: [],
    };
    nodes.push(node);
    byId.set(node.id, node);
}
function mergeReasoningEvent(target, update) {
    target.status = update.status;
    target.summary = update.summary ?? target.summary;
    target.output = update.output ?? target.output;
    target.completedAt = update.completedAt ?? target.completedAt;
}
function closeParentTurnForFinalAssistant(byId, assistant) {
    if (!assistant.parentId || assistant.status !== "done")
        return;
    const parent = byId.get(assistant.parentId);
    if (!parent || parent.type !== "agent.turn")
        return;
    parent.status = "done";
    parent.completedAt = assistant.completedAt ?? assistant.startedAt ?? parent.completedAt;
}
function isInternalSessionOperation(action) {
    return action === "session.fork" || action === "session.clone" || action === "session.switch";
}
function isTranscriptEchoEvent(event) {
    return (event.type === "message_queued" ||
        event.type === "message_started" ||
        event.type === "message_finished" ||
        event.type === "assistant_delta" ||
        event.type === "assistant_message" ||
        event.type === "thinking_started" ||
        event.type === "thinking_delta" ||
        event.type === "thinking_finished");
}
function shouldKeepTranscriptEchoEvent(event, openTranscriptEventIds) {
    const eventId = "eventId" in event && typeof event.eventId === "string" ? event.eventId : undefined;
    return Boolean(eventId && openTranscriptEventIds.has(eventId));
}
function isStaleToolCallEchoEvent(event, sessionStatus) {
    return sessionStatus !== "running" && event.type === "tool_call";
}
function findOpenTranscriptEventIds(events, sessionStatus) {
    if (sessionStatus !== "running")
        return new Set();
    const open = new Set();
    for (const storedEvent of events) {
        const event = storedEvent.payload;
        const eventId = "eventId" in event && typeof event.eventId === "string" ? event.eventId : undefined;
        if (!eventId)
            continue;
        if (isOpenTranscriptEvent(event)) {
            open.add(eventId);
        }
        else if (event.type === "message_finished" || event.type === "session_error") {
            open.delete(eventId);
        }
    }
    return open;
}
function isOpenTranscriptEvent(event) {
    return (event.type === "message_queued" ||
        event.type === "message_started" ||
        event.type === "assistant_delta" ||
        event.type === "assistant_message" ||
        event.type === "thinking_started" ||
        event.type === "thinking_delta" ||
        event.type === "thinking_finished");
}
function eventTraceNodeOrder(eventSequence, type) {
    return eventTraceOrder(eventSequence, eventNodeKind(type));
}
function eventNodeKind(type) {
    switch (type) {
        case "message_queued":
            return "user.message";
        case "message_started":
        case "message_finished":
            return "agent.turn";
        case "thinking_started":
        case "thinking_delta":
        case "thinking_finished":
            return "model.reasoning";
        case "tool_call":
        case "tool_execution_started":
        case "tool_execution_updated":
        case "tool_execution_finished":
        case "subagent_session":
            return "tool.call";
        case "assistant_delta":
        case "assistant_message":
            return "assistant.message";
        case "execution_result":
            return "execution.command";
        case "session_error":
            return "error";
        default:
            return "execution.command";
    }
}
function eventStableKey(event) {
    const eventId = "eventId" in event && typeof event.eventId === "string" ? event.eventId : undefined;
    if (event.type === "tool_call" || event.type === "tool_execution_started" || event.type === "tool_execution_updated" || event.type === "tool_execution_finished") {
        return `tool:${event.toolCallId}`;
    }
    if (event.type === "subagent_session" && event.toolCallId)
        return `tool:${event.toolCallId}`;
    if (eventId && (event.type === "message_started" || event.type === "message_finished"))
        return `turn:${eventId}`;
    if (event.type === "thinking_started" || event.type === "thinking_delta" || event.type === "thinking_finished") {
        const thinkingId = thinkingEventNodeId(event);
        if (thinkingId)
            return `reasoning:${thinkingId}`;
    }
    if (event.type === "assistant_delta" || event.type === "assistant_message") {
        const assistantId = assistantEventNodeId(event);
        if (assistantId)
            return `assistant:${assistantId}`;
    }
    return `event:${event.type}:${eventId ?? cryptoSafeId(event)}`;
}
function messageTurnNodeId(eventId) {
    return `event:message:${eventId}`;
}
function assistantMessageNodeId(eventId) {
    return `event:assistant:${eventId}`;
}
function assistantEventNodeId(event) {
    if (!event.eventId)
        return undefined;
    const partIndex = typeof event.assistantIndex === "number" ? event.assistantIndex : event.contentIndex;
    return typeof partIndex === "number" ? `${event.eventId}:assistant:${partIndex}` : event.eventId;
}
function thinkingNodeId(eventId) {
    return `event:thinking:${eventId}`;
}
function thinkingEventNodeId(event) {
    if (!event.eventId)
        return undefined;
    const partIndex = typeof event.thinkingIndex === "number" ? event.thinkingIndex : event.contentIndex;
    return typeof partIndex === "number" ? `${event.eventId}:thinking:${partIndex}` : event.eventId;
}
function mapTraceNodesById(nodes) {
    const byId = new Map();
    for (const node of flattenTraceNodes(nodes))
        byId.set(node.id, node);
    return byId;
}
function sortTraceNodes(nodes) {
    nodes.sort(compareTraceNodes);
    for (const node of nodes)
        sortTraceNodes(node.children);
}
function compareTraceNodes(left, right) {
    const byOrder = compareTraceOrder(left.orderKey, right.orderKey);
    if (byOrder !== 0)
        return byOrder;
    return (left.startedAt ?? "").localeCompare(right.startedAt ?? "") || left.id.localeCompare(right.id);
}
function flattenTraceNodes(nodes) {
    return nodes.flatMap((node) => [node, ...flattenTraceNodes(node.children)]);
}
function nestTraceNodes(nodes) {
    const byId = new Map();
    for (const node of nodes) {
        byId.set(node.id, { ...node, children: [...node.children] });
    }
    const nestedChildIds = new Set();
    for (const node of byId.values()) {
        if (!node.parentId)
            continue;
        const parent = byId.get(node.parentId);
        if (!parent)
            continue;
        parent.children.push(node);
        nestedChildIds.add(node.id);
    }
    const roots = [...byId.values()].filter((node) => !nestedChildIds.has(node.id));
    sortTraceNodes(roots);
    return roots;
}
function mergeToolEvent(target, update) {
    target.status = update.status;
    target.summary = update.summary ?? target.summary;
    target.input = update.input ?? target.input;
    target.output = update.output ?? target.output;
    target.error = update.error ?? target.error;
    target.completedAt = update.completedAt ?? target.completedAt;
    target.linkedPiboSessionId = update.linkedPiboSessionId ?? target.linkedPiboSessionId;
}
function attachAsyncAgentRunNode(parent, piboSessionId, startedAt, delegation) {
    const node = createAsyncAgentRunNode(parent, piboSessionId, startedAt, delegation);
    if (!node)
        return;
    const existing = parent.children.find((child) => child.id === node.id);
    if (existing) {
        mergeToolEvent(existing, node);
        existing.runId = node.runId ?? existing.runId;
        return;
    }
    parent.children.push(node);
}
function reconcileAsyncAgentRunStatuses(nodes) {
    const runSnapshots = new Map();
    for (const node of flattenTraceNodes(nodes)) {
        const snapshot = extractRunSnapshot(node.output);
        if (!snapshot)
            continue;
        const runId = stringValue(snapshot.runId);
        if (!runId)
            continue;
        runSnapshots.set(runId, {
            snapshot,
            completedAt: stringValue(snapshot.completedAt) ?? stringValue(snapshot.updatedAt) ?? node.completedAt,
        });
    }
    for (const node of flattenTraceNodes(nodes)) {
        if (node.type !== "agent.async" || !node.runId)
            continue;
        const latest = runSnapshots.get(node.runId);
        if (!latest)
            continue;
        const status = stringValue(latest.snapshot.status);
        if (status !== "completed" && status !== "cancelled" && status !== "failed")
            continue;
        node.status = status === "failed" ? "error" : "done";
        node.completedAt = latest.completedAt ?? node.completedAt;
        node.output = latest.snapshot;
        if (status === "failed")
            node.error = stringValue(latest.snapshot.summary) ?? node.error;
    }
}
function createAsyncAgentRunNode(parent, piboSessionId, startedAt, delegation) {
    if (!isRunStartToolNode(parent))
        return undefined;
    const run = extractRunSnapshot(parent.output);
    const input = isRecord(parent.input) ? parent.input : {};
    const toolName = stringValue(run?.toolName) ?? stringValue(input.toolName) ?? delegation?.title;
    if (!toolName || !isSubagentToolName(toolName))
        return undefined;
    const subagentName = stringValue(delegation?.summary) ?? subagentNameFromToolName(toolName);
    const runId = stringValue(run?.runId);
    const runStatus = stringValue(run?.status);
    const delegatedArguments = input.arguments;
    const completionPolicy = stringValue(run?.completionPolicy) ?? stringValue(input.completionPolicy);
    return {
        id: `${parent.id}:async-agent`,
        parentId: parent.id,
        piboSessionId,
        eventId: parent.eventId,
        toolCallId: parent.toolCallId,
        runId,
        type: "agent.async",
        title: subagentName,
        status: asyncAgentStatus(parent, runStatus),
        startedAt: delegation?.startedAt ?? startedAt ?? parent.startedAt,
        completedAt: runStatus === "completed" || runStatus === "cancelled" ? parent.completedAt : undefined,
        summary: `Started by ${parent.title}`,
        input: {
            startedBy: parent.title,
            startToolCallId: parent.toolCallId,
            toolName,
            subagentName,
            runId,
            completionPolicy,
            arguments: delegatedArguments,
            threadKey: isRecord(delegation?.input) ? delegation.input.threadKey : undefined,
        },
        output: run,
        error: parent.error,
        linkedPiboSessionId: delegation?.linkedPiboSessionId ?? parent.linkedPiboSessionId,
        source: parent.source,
        stableKey: runId ? `async-agent:${runId}` : `${parent.stableKey ?? parent.id}:async-agent`,
        orderKey: childTraceOrder(parent.orderKey, "agent.async"),
        children: [],
    };
}
function isRunStartToolNode(node) {
    return node.type === "tool.call" && node.title === "pibo_run_start";
}
function extractRunSnapshot(value) {
    if (!isRecord(value))
        return undefined;
    if (isRunSnapshot(value))
        return value;
    if (isRecord(value.details) && isRunSnapshot(value.details))
        return value.details;
    return undefined;
}
function isRunSnapshot(value) {
    return typeof value.runId === "string" && typeof value.toolName === "string";
}
function stringValue(value) {
    return typeof value === "string" ? value : undefined;
}
function subagentNameFromToolName(toolName) {
    return toolName.slice("pibo_subagent_".length);
}
function asyncAgentStatus(parent, runStatus) {
    if (parent.status === "error" || runStatus === "failed")
        return "error";
    if (runStatus === "completed" || runStatus === "cancelled")
        return "done";
    return "running";
}
function mapChildren(sessions) {
    const result = new Map();
    for (const session of sessions) {
        if (!session.parentId)
            continue;
        const children = result.get(session.parentId) ?? [];
        children.push(session);
        result.set(session.parentId, children);
    }
    return result;
}
function mapSubagentSessionLinks(events) {
    const result = new Map();
    for (const storedEvent of events) {
        const event = storedEvent.payload;
        if (event.type !== "subagent_session" || !event.toolCallId)
            continue;
        result.set(event.toolCallId, event.childPiboSessionId);
    }
    return result;
}
function findLikelyChildSession(piboSessionId, toolName, event, childByParent) {
    if (!isSubagentToolName(toolName))
        return undefined;
    const candidates = childByParent.get(piboSessionId)?.filter((session) => session.metadata?.subagentToolName === toolName) ?? [];
    const threadKey = toolEventThreadKey(event);
    if (threadKey) {
        return candidates.find((session) => session.metadata?.threadKey === threadKey)?.id;
    }
    return candidates.length === 1 ? candidates[0].id : undefined;
}
function isSubagentToolName(name) {
    return name.startsWith("pibo_subagent_");
}
function toolEventThreadKey(event) {
    const args = "args" in event && event.args && typeof event.args === "object" && !Array.isArray(event.args) ? event.args : undefined;
    const threadKey = args && "threadKey" in args ? args.threadKey : undefined;
    return typeof threadKey === "string" && threadKey.trim() ? threadKey.trim() : undefined;
}
function messageStatus(message) {
    if (message && typeof message === "object") {
        const stopReason = message.stopReason;
        const errorMessage = message.errorMessage;
        if (stopReason === "error" || typeof errorMessage === "string")
            return "error";
    }
    return "done";
}
function messageError(message) {
    if (!message || typeof message !== "object")
        return undefined;
    const errorMessage = message.errorMessage;
    return typeof errorMessage === "string" ? errorMessage : undefined;
}
function hasVisibleText(value) {
    return typeof value === "string" && value.trim().length > 0;
}
function extractText(content) {
    if (typeof content === "string")
        return content;
    if (!Array.isArray(content))
        return "";
    return content
        .map((part) => {
        const typed = part;
        if (typed.type === "text" && typeof typed.text === "string")
            return typed.text;
        return "";
    })
        .join("");
}
function stringifyPreview(value) {
    if (typeof value === "string")
        return value;
    try {
        return JSON.stringify(value);
    }
    catch {
        return String(value);
    }
}
function parseRunNotificationText(text) {
    const trimmed = text.trim();
    const start = "<pibo_run_notification>";
    const end = "</pibo_run_notification>";
    if (!trimmed.startsWith(start) || !trimmed.endsWith(end))
        return undefined;
    const jsonText = trimmed.slice(start.length, trimmed.length - end.length).trim();
    try {
        const parsed = JSON.parse(jsonText);
        if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
            return undefined;
        return parsed;
    }
    catch {
        return undefined;
    }
}
function createRunNotificationNode(input) {
    const runs = runNotificationRuns(input.notification);
    const singleRun = runs.length === 1 ? runs[0] : undefined;
    const failedCount = countRunGroup(input.notification.failed);
    const runningCount = countRunGroup(input.notification.running);
    return {
        id: input.id,
        entryId: input.entryId,
        piboSessionId: input.piboSessionId,
        eventId: input.eventId,
        runId: typeof singleRun?.runId === "string" ? singleRun.runId : undefined,
        type: "yielded.run",
        title: "Run Notification",
        status: failedCount > 0 ? "error" : runningCount > 0 ? "running" : "done",
        startedAt: input.startedAt,
        summary: runNotificationSummary(input.notification),
        output: input.notification,
        source: input.source,
        stableKey: input.stableKey ?? input.id,
        orderKey: input.orderKey,
        children: [],
    };
}
function runNotificationRuns(notification) {
    return [
        ...runGroup(notification.completed),
        ...runGroup(notification.failed),
        ...runGroup(notification.cancelled),
        ...runGroup(notification.running),
    ];
}
function runGroup(value) {
    return Array.isArray(value) ? value.filter(isRecord) : [];
}
function countRunGroup(value) {
    return Array.isArray(value) ? value.length : 0;
}
function runNotificationSummary(notification) {
    const parts = [
        [countRunGroup(notification.completed), "completed"],
        [countRunGroup(notification.failed), "failed"],
        [countRunGroup(notification.cancelled), "cancelled"],
        [countRunGroup(notification.running), "running"],
    ]
        .filter(([count]) => Number(count) > 0)
        .map(([count, label]) => `${count} ${label}`);
    return parts.length ? parts.join(", ") : "No yielded run updates";
}
function isRecord(value) {
    return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function cryptoSafeId(value) {
    return Buffer.from(JSON.stringify(value)).toString("base64url").slice(0, 48);
}
