/** Trace source loading and parsing for Claude JSONL / OpenClaw JSONL / generic markdown logs. */
import { readFileSync, readdirSync, statSync } from 'node:fs';
import { basename, dirname, join, relative } from 'node:path';
import { extractMarkdownLogSkill } from './trace-attribution.js';
import { isToolResultFailureText } from './text-signals.js';
// ---------- Load ----------
/**
 * 加载一个目录(或单个 JSONL/agent markdown log 文件)下的所有 session。
 * 递归扫描目录,覆盖 Claude Code 主 session 旁边的 subagents/*.jsonl,
 * 也支持 agent workspace/logs/*.log 里的 Markdown 对话记录。
 */
export function loadCcSessions(path) {
    const stat = statSync(path);
    if (stat.isFile()) {
        return parseTraceFile(path).map((session) => withStandaloneTraceMetadata(session));
    }
    const entries = collectTraceFiles(path);
    return annotateSessionGroups(path, entries.flatMap(parseTraceFile));
}
function collectTraceFiles(dir) {
    const files = [];
    for (const entry of readdirSync(dir)) {
        if (entry === 'node_modules' || entry === '.git')
            continue;
        const entryPath = join(dir, entry);
        let stat;
        try {
            stat = statSync(entryPath);
        }
        catch {
            continue;
        }
        if (stat.isDirectory()) {
            files.push(...collectTraceFiles(entryPath));
        }
        else if (entry.endsWith('.jsonl') || entry.endsWith('.log')) {
            files.push(entryPath);
        }
    }
    return files.sort();
}
function parseTraceFile(filePath) {
    if (filePath.endsWith('.jsonl'))
        return [parseJsonlSessionFile(filePath)];
    if (filePath.endsWith('.log'))
        return parseMarkdownLogFile(filePath);
    return [];
}
function withStandaloneTraceMetadata(session) {
    return {
        ...session,
        sessionGroupId: session.sessionId,
        sessionGroupPath: dirname(session.sourcePath),
        traceId: traceIdFor(session),
        traceRole: session.traceRole ?? 'standalone',
        traceLabel: session.traceLabel ?? basename(session.sourcePath),
    };
}
function annotateSessionGroups(rootPath, sessions) {
    const groupRoots = new Set();
    for (const session of sessions) {
        const subagentRoot = subagentGroupRoot(session.sourcePath);
        if (subagentRoot)
            groupRoots.add(subagentRoot);
    }
    if (groupRoots.size === 0) {
        return sessions.map((session) => withStandaloneTraceMetadata(session));
    }
    const groupIdByRoot = new Map();
    for (const root of groupRoots) {
        const mainSession = sessions
            .filter((session) => groupRootForPath(session.sourcePath, groupRoots) === root && !isSubagentTrace(session.sourcePath))
            .sort((a, b) => a.sourcePath.localeCompare(b.sourcePath))[0];
        groupIdByRoot.set(root, mainSession?.sessionId || basename(root) || relative(dirname(rootPath), root) || root);
    }
    return sessions.map((session) => {
        const groupRoot = groupRootForPath(session.sourcePath, groupRoots);
        if (!groupRoot)
            return withStandaloneTraceMetadata(session);
        const role = isSubagentTrace(session.sourcePath) ? 'subagent' : 'main';
        return {
            ...session,
            sessionGroupId: groupIdByRoot.get(groupRoot) ?? session.sessionId,
            sessionGroupPath: groupRoot,
            traceId: traceIdFor(session),
            traceRole: role,
            traceLabel: traceLabelFor(session.sourcePath, groupRoot, role),
        };
    });
}
function traceIdFor(session) {
    return `${session.sessionId}\u0000${session.sourcePath}`;
}
function isSubagentTrace(filePath) {
    return filePath.split('/').includes('subagents');
}
function subagentGroupRoot(filePath) {
    const marker = '/subagents/';
    const index = filePath.indexOf(marker);
    if (index < 0)
        return undefined;
    return filePath.slice(0, index);
}
function groupRootForPath(filePath, groupRoots) {
    const subagentRoot = subagentGroupRoot(filePath);
    if (subagentRoot && groupRoots.has(subagentRoot))
        return subagentRoot;
    const parent = dirname(filePath);
    if (groupRoots.has(parent))
        return parent;
    return undefined;
}
function traceLabelFor(filePath, groupRoot, role) {
    const rel = relative(groupRoot, filePath) || basename(filePath);
    return role === 'subagent' ? rel : `main/${basename(filePath)}`;
}
function parseJsonlSessionFile(filePath) {
    const content = readFileSync(filePath, 'utf-8');
    const records = [];
    for (const line of content.split('\n')) {
        const trimmed = line.trim();
        if (!trimmed)
            continue;
        try {
            records.push(JSON.parse(trimmed));
        }
        catch {
            // malformed line → skip. cc 有罕见的截断 record, 不让单行 fail 整个 session
        }
    }
    return isOpenClawJsonl(records) ? parseOpenClawSessionFile(filePath, records) : parseCcSessionFile(filePath, records);
}
function parseCcSessionFile(filePath, records) {
    const first = records.find((r) => 'sessionId' in r && typeof r.sessionId === 'string');
    const last = [...records].reverse().find((r) => 'timestamp' in r && typeof r.timestamp === 'string');
    return {
        sessionId: first?.sessionId ?? filePath.split('/').pop().replace('.jsonl', ''),
        sourcePath: filePath,
        sourceKind: 'claude',
        records,
        cwd: first?.cwd,
        gitBranch: first?.gitBranch,
        entrypoint: first?.entrypoint,
        startTimestamp: first?.timestamp,
        endTimestamp: last?.timestamp,
    };
}
function isOpenClawJsonl(records) {
    return records.some((record) => record.type === 'session' && typeof record.id === 'string')
        && records.some((record) => record.type === 'message' && isRecordObject(record.message));
}
function parseOpenClawSessionFile(filePath, rawRecords) {
    const sessionRecord = rawRecords.find((record) => record.type === 'session');
    const sessionId = typeof sessionRecord?.id === 'string'
        ? sessionRecord.id
        : filePath.split('/').pop().replace(/\.jsonl$/, '');
    const cwd = typeof sessionRecord?.cwd === 'string' ? sessionRecord.cwd : undefined;
    const sourceMetadata = extractOpenClawSourceMetadata(rawRecords);
    const records = [];
    for (const raw of rawRecords) {
        const converted = convertOpenClawRecord(raw, sessionId, cwd);
        if (converted) {
            records.push(converted);
        }
        else if (isSessionBoundaryRawRecord(raw)) {
            records.push({ ...raw, sessionId });
        }
    }
    const first = records.find((record) => typeof record.timestamp === 'string');
    const last = [...records].reverse().find((record) => typeof record.timestamp === 'string');
    return {
        sessionId,
        sourcePath: filePath,
        sourceKind: 'openclaw',
        records,
        cwd,
        entrypoint: 'openclaw',
        sourceMetadata,
        startTimestamp: first?.timestamp ?? (typeof sessionRecord?.timestamp === 'string' ? sessionRecord.timestamp : undefined),
        endTimestamp: last?.timestamp,
    };
}
function extractOpenClawSourceMetadata(rawRecords) {
    const meta = {};
    const commands = new Set();
    for (const raw of rawRecords) {
        if (raw.type === 'model_change') {
            const modelChange = raw;
            if (typeof modelChange.provider === 'string')
                meta.provider = modelChange.provider;
            if (typeof modelChange.modelId === 'string')
                meta.model = modelChange.modelId;
            continue;
        }
        if (raw.type === 'custom') {
            const custom = raw;
            if (custom.customType === 'model-snapshot' && isRecordObject(custom.data)) {
                if (typeof custom.data.provider === 'string')
                    meta.provider = custom.data.provider;
                if (typeof custom.data.modelId === 'string')
                    meta.model = custom.data.modelId;
                if (typeof custom.data.modelApi === 'string')
                    meta.modelApi = custom.data.modelApi;
            }
            continue;
        }
        if (raw.type !== 'message')
            continue;
        const message = raw.message;
        if (!isRecordObject(message))
            continue;
        if (typeof message.provider === 'string')
            meta.provider = message.provider;
        if (typeof message.model === 'string')
            meta.model = message.model;
        if (typeof message.api === 'string')
            meta.modelApi = message.api;
        const text = openClawContentText(message.content);
        for (const name of extractBusinessActionNames(text))
            commands.add(name);
        const conversationInfo = extractOpenClawConversationInfo(text);
        if (conversationInfo.channel)
            meta.channel = conversationInfo.channel;
        if (conversationInfo.sender)
            meta.sender = conversationInfo.sender;
        if (conversationInfo.senderId)
            meta.senderId = conversationInfo.senderId;
    }
    if (commands.size > 0)
        meta.businessActions = Array.from(commands).sort();
    return meta;
}
function extractOpenClawConversationInfo(text) {
    const match = text.match(/Conversation info \(untrusted metadata\):\s*```json\s*([\s\S]*?)\s*```/);
    if (!match)
        return {};
    try {
        const parsed = JSON.parse(match[1]);
        return {
            channel: typeof parsed.channel === 'string' ? parsed.channel : undefined,
            sender: typeof parsed.sender === 'string' ? parsed.sender : undefined,
            senderId: typeof parsed.sender_id === 'string' ? parsed.sender_id : typeof parsed.senderId === 'string' ? parsed.senderId : undefined,
        };
    }
    catch {
        return {};
    }
}
function extractBusinessActionNames(text) {
    const names = [];
    const re = /<[a-z][\w.-]*-cmd\b[^>]*\bname=["']([^"']+)["'][^>]*>/g;
    for (const match of text.matchAll(re)) {
        if (match[1]?.trim())
            names.push(match[1].trim());
    }
    return names;
}
function convertOpenClawRecord(raw, sessionId, cwd) {
    if (raw.type !== 'message')
        return null;
    const messageRecord = raw;
    const message = messageRecord.message;
    if (!message || typeof message.role !== 'string')
        return null;
    const uuid = typeof messageRecord.id === 'string' ? messageRecord.id : `${sessionId}-${recordsafeTimestamp(messageRecord.timestamp)}`;
    const parentUuid = typeof messageRecord.parentId === 'string' ? messageRecord.parentId : null;
    const timestamp = typeof messageRecord.timestamp === 'string' ? messageRecord.timestamp : new Date().toISOString();
    if (message.role === 'user') {
        return {
            type: 'user',
            uuid,
            parentUuid,
            sessionId,
            timestamp,
            entrypoint: 'openclaw',
            message: {
                role: 'user',
                content: openClawTextParts(message.content),
            },
        };
    }
    if (message.role === 'assistant') {
        return {
            type: 'assistant',
            uuid,
            parentUuid,
            sessionId,
            timestamp,
            cwd,
            entrypoint: 'openclaw',
            message: {
                role: 'assistant',
                model: typeof message.model === 'string' ? message.model : undefined,
                content: openClawAssistantContent(message.content),
                stop_reason: typeof message.stopReason === 'string' ? message.stopReason : undefined,
                usage: openClawUsage(message.usage),
            },
        };
    }
    if (message.role === 'toolResult') {
        const toolUseId = typeof message.toolCallId === 'string' ? message.toolCallId : undefined;
        if (!toolUseId)
            return null;
        const content = openClawToolResultText(message.content);
        return {
            type: 'user',
            uuid,
            parentUuid,
            sessionId,
            timestamp,
            entrypoint: 'openclaw',
            message: {
                role: 'user',
                content: [{
                        type: 'tool_result',
                        tool_use_id: toolUseId,
                        content,
                        is_error: message.isError === true || isToolResultFailureText(content),
                    }],
            },
        };
    }
    return null;
}
function isSessionBoundaryRawRecord(raw) {
    const type = typeof raw.type === 'string' ? raw.type : '';
    return /^session[._-](?:started|ended)$/i.test(type);
}
function recordsafeTimestamp(value) {
    return typeof value === 'string' ? value.replace(/[^a-zA-Z0-9_-]/g, '') : Math.random().toString(36).slice(2);
}
function openClawTextParts(content) {
    const text = openClawContentText(content);
    const split = splitOpenClawRuntimeMetadata(text);
    return split.map((part) => ({ type: 'text', text: part }));
}
function splitOpenClawRuntimeMetadata(text) {
    const match = text.match(/^(Conversation info \(untrusted metadata\):\s*```json[\s\S]*?```\s*)([\s\S]*)$/);
    if (!match)
        return text ? [text] : [];
    const metadata = match[1].trim();
    const rest = match[2].trim();
    return rest ? [metadata, rest] : [metadata];
}
function openClawAssistantContent(content) {
    if (!Array.isArray(content))
        return [];
    const parts = [];
    for (const part of content) {
        if (!isRecordObject(part) || typeof part.type !== 'string')
            continue;
        if (part.type === 'thinking') {
            parts.push({ type: 'thinking', thinking: typeof part.thinking === 'string' ? part.thinking : '' });
            continue;
        }
        if (part.type === 'text') {
            parts.push({ type: 'text', text: typeof part.text === 'string' ? part.text : '' });
            continue;
        }
        if (part.type === 'toolCall') {
            const id = typeof part.id === 'string' ? part.id : undefined;
            const name = typeof part.name === 'string' ? normalizeOpenClawToolName(part.name) : undefined;
            if (!id || !name)
                continue;
            parts.push({
                type: 'tool_use',
                id,
                name,
                input: normalizeOpenClawToolInput(name, isRecordObject(part.arguments) ? part.arguments : {}),
            });
        }
    }
    return parts;
}
function normalizeOpenClawToolName(name) {
    const normalized = name.trim();
    const lower = normalized.toLowerCase();
    if (lower === 'read')
        return 'Read';
    if (lower === 'grep' || lower === 'search')
        return 'Grep';
    // OpenClaw uses exec/run/shell labels for command execution; collapse them for tool-count comparability.
    if (lower === 'bash' || lower === 'shell' || lower === 'run' || lower === 'exec')
        return 'Bash';
    if (lower === 'edit')
        return 'Edit';
    if (lower === 'write')
        return 'Write';
    return normalized;
}
function normalizeOpenClawToolInput(toolName, input) {
    if (toolName === 'Read' && typeof input.path === 'string' && typeof input.file_path !== 'string') {
        return { ...input, file_path: input.path };
    }
    return input;
}
function openClawUsage(value) {
    if (!isRecordObject(value))
        return undefined;
    return {
        input_tokens: numberValue(value.input ?? value.input_tokens),
        output_tokens: numberValue(value.output ?? value.output_tokens),
        cache_read_input_tokens: numberValue(value.cacheRead ?? value.cache_read_input_tokens),
        cache_creation_input_tokens: numberValue(value.cacheWrite ?? value.cache_creation_input_tokens),
    };
}
function numberValue(value) {
    return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
}
function openClawToolResultText(content) {
    return openClawContentText(content);
}
function openClawContentText(content) {
    if (typeof content === 'string')
        return content;
    if (!Array.isArray(content))
        return '';
    const parts = [];
    for (const part of content) {
        if (typeof part === 'string') {
            parts.push(part);
        }
        else if (isRecordObject(part) && typeof part.text === 'string') {
            parts.push(part.text);
        }
    }
    return parts.join('\n');
}
function isRecordObject(value) {
    return !!value && typeof value === 'object' && !Array.isArray(value);
}
const MARKDOWN_LOG_BLOCK_RE = /(?:^|\n)---\s*\n## \[([^\]]+)\] 对话记录[^\n]*\n([\s\S]*?)(?=\n---\s*\n## \[|$)/g;
function parseMarkdownLogFile(filePath) {
    const content = readFileSync(filePath, 'utf-8');
    if (!content.includes('### 用户输入') || !content.includes('### AI 回复'))
        return [];
    const sessions = [];
    let index = 0;
    for (const match of content.matchAll(MARKDOWN_LOG_BLOCK_RE)) {
        const timestamp = markdownLogTimestampToIso(match[1]);
        const body = match[2] ?? '';
        const blockCwd = body.match(/^\*\*工作目录\*\*:\s*(.+)$/m)?.[1]?.trim();
        const blockSessionId = body.match(/^\*\*会话 ID\*\*:\s*(.+)$/m)?.[1]?.trim();
        const requestId = body.match(/^\*\*请求 ID\*\*:\s*(.+)$/m)?.[1]?.trim() ?? String(index);
        const userText = extractMarkdownLogSection(body, '### 用户输入', '### AI 回复');
        const assistantText = extractMarkdownLogSection(body, '### AI 回复');
        if (!userText && !assistantText)
            continue;
        const sessionId = blockSessionId || `${filePath.split('/').pop().replace(/\.log$/, '')}:${requestId}`;
        const cwd = blockCwd;
        const skill = extractMarkdownLogSkill(`${userText}\n${assistantText}`);
        const userContent = skill ? `<command-name>/${skill}</command-name>\n${userText}` : userText;
        const records = [{
                type: 'user',
                uuid: `markdown-log-${requestId}-user-${index}`,
                parentUuid: null,
                sessionId,
                timestamp,
                message: { role: 'user', content: userContent },
            }, {
                type: 'assistant',
                uuid: `markdown-log-${requestId}-assistant-${index}`,
                parentUuid: `markdown-log-${requestId}-user-${index}`,
                sessionId,
                timestamp,
                cwd,
                attributionSkill: skill,
                message: {
                    role: 'assistant',
                    content: assistantText ? [{ type: 'text', text: assistantText }] : [],
                    stop_reason: 'end_turn',
                },
            }];
        sessions.push({
            sessionId,
            sourcePath: filePath,
            sourceKind: 'markdown_log',
            records,
            cwd,
            entrypoint: 'markdown_log',
            startTimestamp: timestamp,
            endTimestamp: timestamp,
        });
        index += 1;
    }
    return sessions;
}
function markdownLogTimestampToIso(value) {
    const m = value.match(/^(\d{4})\/(\d{2})\/(\d{2})\s+(\d{2}):(\d{2}):(\d{2})$/);
    if (!m)
        return new Date().toISOString();
    return `${m[1]}-${m[2]}-${m[3]}T${m[4]}:${m[5]}:${m[6]}+08:00`;
}
function extractMarkdownLogSection(body, startMarker, endMarker) {
    const start = body.indexOf(startMarker);
    if (start < 0)
        return '';
    const from = start + startMarker.length;
    const end = endMarker ? body.indexOf(endMarker, from) : -1;
    return body.slice(from, end >= 0 ? end : undefined).trim();
}
//# sourceMappingURL=trace-source.js.map