/*
 * SPEC: PJ2026-010403 API契约 draft-2026-06-17-r0; PJ2026-0102 Agent编排 draft-2026-06-17-r0; PJ2026-01060505 Workbench Performance draft-2026-06-17-p0; PJ2026-0104010803 唯一投影 draft-2026-06-20-p1-zero-split-durable-realtime
 * 职责: Code Agent trace capture。实时内存 trace 可加速 SSE/详情面板，但 append 事件必须同步投影到 durable store。
 */
import { randomUUID } from "node:crypto";

const DEFAULT_MAX_TRACES = 256;
const DEFAULT_MAX_EVENTS = 6000;
const TEXT_LIMIT = 1200;
const SECRET_PATTERN = /\b(?:sk-[A-Za-z0-9_-]+|OPENAI_API_KEY|DATABASE_URL|secretRef:[^\s,;]+|secret|token|password|passwd|credential|private[_ -]?key|kubeconfig|BEGIN [A-Z ]*PRIVATE KEY)\b/giu;

export function createCodeAgentTraceStore(options = {}) {
  const maxTraces = positiveInteger(options.maxTraces, DEFAULT_MAX_TRACES);
  const maxEvents = positiveInteger(options.maxEvents, DEFAULT_MAX_EVENTS);
  const traces = new Map();

  function ensure(traceId, meta = {}) {
    const id = cleanTraceId(traceId) || `trc_${randomUUID()}`;
    let trace = traces.get(id);
    if (!trace) {
      const timestamp = timestampFor(meta.now);
      trace = {
        traceId: id,
        status: "running",
        createdAt: timestamp,
        updatedAt: timestamp,
        startedAt: timestamp,
        finishedAt: null,
        nextSeq: 1,
        events: [],
        sourceEventKeys: new Set(),
        eventSignatures: new Set(),
        assistantStreams: new Map(),
        listeners: new Set(),
        meta: {
          runnerKind: meta.runnerKind ?? null,
          workspace: meta.workspace ?? null,
          sandbox: meta.sandbox ?? null,
          sessionMode: meta.sessionMode ?? null,
          implementationType: meta.implementationType ?? null
        }
      };
      traces.set(id, trace);
      prune();
    } else {
      trace.meta = {
        ...trace.meta,
        ...dropEmpty({
          runnerKind: meta.runnerKind,
          workspace: meta.workspace,
          sandbox: meta.sandbox,
          sessionMode: meta.sessionMode,
          implementationType: meta.implementationType
        })
      };
    }
    return trace;
  }

  function append(traceId, event = {}, meta = {}) {
    const trace = ensure(traceId, meta);
    const incomingSourceKey = traceSourceEventKey(event);
    if (incomingSourceKey && trace.sourceEventKeys.has(incomingSourceKey)) {
      return trace.events.find((candidate) => traceSourceEventKey(candidate) === incomingSourceKey) ?? null;
    }
    const normalized = normalizeTraceEvent(event, {
      traceId: trace.traceId,
      seq: trace.nextSeq,
      now: meta.now,
      fallbackRunnerKind: trace.meta.runnerKind
    });
    const normalizedSignature = traceEventSignature(normalized);
    if (normalizedSignature && trace.eventSignatures.has(normalizedSignature)) {
      return trace.events.find((candidate) => traceEventSignature(candidate) === normalizedSignature) ?? null;
    }
    if (shouldSuppressPostTerminalEvent(trace, normalized)) return null;
    const normalizedSourceKey = traceSourceEventKey(normalized);
    if (normalizedSourceKey && trace.sourceEventKeys.has(normalizedSourceKey)) {
      return trace.events.find((candidate) => traceSourceEventKey(candidate) === normalizedSourceKey) ?? null;
    }
    const hadEvents = trace.events.length > 0;
    trace.nextSeq += 1;
    trace.events.push(normalized);
    if (normalizedSourceKey) trace.sourceEventKeys.add(normalizedSourceKey);
    if (normalizedSignature) trace.eventSignatures.add(normalizedSignature);
    if (trace.events.length > maxEvents) {
      trace.events.splice(0, trace.events.length - maxEvents);
      rebuildTraceEventIndexes(trace);
    }
    trace.createdAt = minTimestamp(trace.createdAt, normalized.createdAt);
    trace.startedAt = minTimestamp(trace.startedAt, normalized.createdAt);
    trace.updatedAt = hadEvents ? maxTimestamp(trace.updatedAt, normalized.createdAt) : normalized.createdAt;
    if (!trace.finishedAt) {
      if (normalized.terminal === true) {
        trace.status = normalized.status === "completed"
          ? "completed"
          : normalized.status === "canceled"
            ? "canceled"
            : normalized.type;
        trace.finishedAt = normalized.createdAt;
        trace.updatedAt = maxTimestamp(trace.updatedAt, trace.finishedAt);
      } else if (normalized.status === "failed" || normalized.type === "error" || normalized.type === "timeout") {
        trace.status = normalized.type;
        trace.finishedAt = normalized.createdAt;
        trace.updatedAt = maxTimestamp(trace.updatedAt, trace.finishedAt);
      }
    }
    notify(trace, normalized);
    return normalized;
  }

  function appendAssistantDelta(traceId, delta = {}, meta = {}) {
    const trace = ensure(traceId, meta);
    const normalized = normalizeAssistantDelta(delta, {
      traceId: trace.traceId,
      now: meta.now,
      fallbackRunnerKind: trace.meta.runnerKind
    });
    if (!normalized) return null;
    const key = normalized.itemId || "assistant-message";
    const previous = trace.assistantStreams.get(key) ?? {
      traceId: trace.traceId,
      itemId: key,
      type: "assistant_message_stream",
      status: "streaming",
      label: "assistant:stream",
      createdAt: normalized.createdAt,
      updatedAt: normalized.createdAt,
      runnerKind: normalized.runnerKind,
      sessionId: normalized.sessionId,
      sessionStatus: normalized.sessionStatus,
      turn: normalized.turn,
      threadId: normalized.threadId,
      turnId: normalized.turnId,
      waitingFor: normalized.waitingFor,
      chunkCount: 0,
      text: "",
      lastChunk: "",
      valuesPrinted: false
    };
    const text = `${previous.text ?? ""}${normalized.chunk}`;
    const stream = dropUndefined({
      ...previous,
      updatedAt: normalized.createdAt,
      runnerKind: normalized.runnerKind ?? previous.runnerKind,
      sessionId: normalized.sessionId ?? previous.sessionId,
      sessionStatus: normalized.sessionStatus ?? previous.sessionStatus,
      turn: normalized.turn ?? previous.turn,
      threadId: normalized.threadId ?? previous.threadId,
      turnId: normalized.turnId ?? previous.turnId,
      waitingFor: normalized.waitingFor ?? previous.waitingFor,
      chunkCount: previous.chunkCount + 1,
      text,
      lastChunk: normalized.chunk,
      valuesPrinted: false
    });
    trace.assistantStreams.set(key, stream);
    trace.updatedAt = normalized.createdAt;
    notify(trace, null);
    return stream;
  }

  function snapshot(traceId, extra = {}) {
    const trace = traces.get(cleanTraceId(traceId));
    if (!trace) return emptySnapshot(traceId, extra);
    const events = trace.events.map((event) => ({ ...event }));
    const assistantStreams = assistantStreamsSnapshot(trace);
    const lastEvent = events.at(-1) ?? null;
    return {
      traceId: trace.traceId,
      status: trace.status,
      createdAt: trace.createdAt,
      updatedAt: trace.updatedAt,
      startedAt: trace.startedAt,
      finishedAt: trace.finishedAt,
      eventCount: events.length,
      events,
      assistantStreams,
      eventLabels: events.map((event) => event.label).filter(Boolean),
      lastEvent,
      elapsedMs: elapsedMs(trace.startedAt, trace.finishedAt ?? trace.updatedAt),
      waitingFor: snapshotWaitingFor(trace, events, assistantStreams, lastEvent),
      runnerKind: extra.runnerKind ?? trace.meta.runnerKind ?? null,
      workspace: extra.workspace ?? trace.meta.workspace ?? null,
      sandbox: extra.sandbox ?? trace.meta.sandbox ?? null,
      sessionMode: extra.sessionMode ?? trace.meta.sessionMode ?? null,
      implementationType: extra.implementationType ?? trace.meta.implementationType ?? null,
      sessionId: extra.sessionId ?? lastEvent?.sessionId ?? null,
      sessionStatus: extra.sessionStatus ?? lastEvent?.sessionStatus ?? null,
      sessionLifecycleStatus: extra.sessionLifecycleStatus ?? lastEvent?.sessionLifecycleStatus ?? null,
      turn: extra.turn ?? lastEvent?.turn ?? null,
      outputTruncated: events.some((event) => event.outputTruncated === true),
      valuesPrinted: false
    };
  }

  function subscribe(traceId, listener, meta = {}) {
    const trace = ensure(traceId, meta);
    trace.listeners.add(listener);
    return () => {
      trace.listeners.delete(listener);
    };
  }

  function clear() {
    traces.clear();
  }

  function prune() {
    if (traces.size <= maxTraces) return;
    const stale = [...traces.values()]
      .sort((left, right) => String(left.updatedAt).localeCompare(String(right.updatedAt)))
      .slice(0, traces.size - maxTraces);
    for (const trace of stale) {
      traces.delete(trace.traceId);
    }
  }

  return {
    ensure,
    append,
    appendAssistantDelta,
    snapshot,
    subscribe,
    clear
  };
}

export const defaultCodeAgentTraceStore = createCodeAgentTraceStore();

export function createDurableCodeAgentTraceStore({ traceStore = defaultCodeAgentTraceStore, traceEventStore = null, workbenchEventWriter = null } = {}) {
  if (!traceEventStore || typeof traceEventStore.writeAgentTraceEvent !== "function") return traceStore;

  function append(traceId, event = {}, meta = {}) {
    const normalized = traceStore.append(traceId, event, meta);
    if (normalized?.traceId) persistTraceEvent(traceEventStore, normalized, meta, workbenchEventWriter);
    return normalized;
  }

  return {
    ensure: (...args) => traceStore.ensure(...args),
    append,
    appendAssistantDelta: (...args) => traceStore.appendAssistantDelta(...args),
    snapshot: (...args) => traceStore.snapshot(...args),
    subscribe: (...args) => traceStore.subscribe(...args),
    clear: (...args) => traceStore.clear(...args)
  };
}

function persistTraceEvent(traceEventStore, event, meta = {}, workbenchEventWriter = null) {
  void persistTraceEventAsync(traceEventStore, event, meta, workbenchEventWriter);
}

async function persistTraceEventAsync(traceEventStore, event, meta = {}, workbenchEventWriter = null) {
  try {
    const projectionContext = {
      traceId: event.traceId,
      agentSessionId: event.agentSessionId ?? event.sessionId ?? meta.agentSessionId ?? meta.sessionId,
      sessionId: event.sessionId ?? meta.sessionId
    };
    try {
      await traceEventStore.writeAgentTraceEvent({ event }, projectionContext);
    } catch (error) {
      console.error(`[workbench-projection] durable trace event write failed: traceId=${event.traceId} sourceEventId=${event.sourceEventId ?? "none"} error=${error?.message ?? error}`);
    }
    if (typeof workbenchEventWriter === "function") {
      try {
        await workbenchEventWriter(event, meta);
      } catch (error) {
        console.error(`[workbench-projection] workbench event writer failed: traceId=${event.traceId} error=${error?.message ?? error}`);
      }
    }
  } catch (error) {
    // Trace capture must not fail the user turn, but projection write errors must be visible for diagnostics.
    console.error(`[workbench-projection] persistTraceEvent sync error: traceId=${event.traceId} error=${error?.message ?? error}`);
  }
}

export function createCodeAgentTraceRecorder({
  traceStore = defaultCodeAgentTraceStore,
  traceId,
  now,
  runnerKind = null,
  workspace = null,
  sandbox = null,
  sessionMode = null,
  implementationType = null
} = {}) {
  const startedAt = timestampFor(now);
  const startedEpochMs = Date.now();
  const baseMeta = { now, runnerKind, workspace, sandbox, sessionMode, implementationType };
  traceStore.ensure(traceId, baseMeta);

  function append(event = {}) {
    return traceStore.append(traceId, {
      ...event,
      elapsedMs: Date.now() - startedEpochMs
    }, baseMeta);
  }

  function appendAssistantDelta(delta = {}) {
    return traceStore.appendAssistantDelta(traceId, delta, baseMeta);
  }

  function snapshot(extra = {}) {
    return traceStore.snapshot(traceId, { runnerKind, workspace, sandbox, sessionMode, implementationType, ...extra });
  }

  function runnerTrace(extra = {}) {
    const current = snapshot(extra);
    return {
      traceId: current.traceId,
      runnerKind: extra.runnerKind ?? current.runnerKind,
      workspace: extra.workspace ?? current.workspace,
      sandbox: extra.sandbox ?? current.sandbox,
      sessionMode: extra.sessionMode ?? current.sessionMode,
      sessionId: extra.sessionId ?? current.sessionId,
      sessionStatus: extra.sessionStatus ?? current.sessionStatus,
      sessionLifecycleStatus: extra.sessionLifecycleStatus ?? current.sessionLifecycleStatus,
      idleTimeoutMs: extra.idleTimeoutMs ?? null,
      lastTraceId: extra.lastTraceId ?? current.traceId,
      turn: extra.turn ?? current.turn,
      sessionReused: extra.sessionReused ?? false,
      implementationType: extra.implementationType ?? current.implementationType,
      limitations: Array.isArray(extra.limitations) ? extra.limitations : [],
      startedAt: extra.startedAt ?? startedAt,
      finishedAt: extra.finishedAt ?? current.finishedAt ?? timestampFor(now),
      updatedAt: current.updatedAt,
      events: current.events,
      assistantStreams: current.assistantStreams,
      eventLabels: current.eventLabels,
      lastEvent: current.lastEvent,
      elapsedMs: current.elapsedMs,
      waitingFor: current.waitingFor,
      outputTruncated: current.outputTruncated || extra.outputTruncated === true,
      valuesPrinted: false,
      note: extra.note ?? "Real-time runnerTrace is appended as Codex stdio/session events are captured."
    };
  }

  return {
    traceId,
    append,
    appendAssistantDelta,
    snapshot,
    runnerTrace
  };
}

export function runnerTraceFromSnapshot(snapshot = {}, extra = {}) {
  return {
    traceId: snapshot.traceId ?? extra.traceId ?? null,
    runnerKind: extra.runnerKind ?? snapshot.runnerKind ?? null,
    workspace: extra.workspace ?? snapshot.workspace ?? null,
    sandbox: extra.sandbox ?? snapshot.sandbox ?? null,
    sessionMode: extra.sessionMode ?? snapshot.sessionMode ?? null,
    sessionId: extra.sessionId ?? snapshot.sessionId ?? null,
    sessionStatus: extra.sessionStatus ?? snapshot.sessionStatus ?? null,
    sessionLifecycleStatus: extra.sessionLifecycleStatus ?? snapshot.sessionLifecycleStatus ?? null,
    idleTimeoutMs: extra.idleTimeoutMs ?? null,
    lastTraceId: extra.lastTraceId ?? snapshot.traceId ?? null,
    turn: extra.turn ?? snapshot.turn ?? null,
    sessionReused: extra.sessionReused ?? false,
    implementationType: extra.implementationType ?? snapshot.implementationType ?? null,
    limitations: Array.isArray(extra.limitations) ? extra.limitations : [],
    startedAt: extra.startedAt ?? snapshot.startedAt ?? null,
    finishedAt: extra.finishedAt ?? snapshot.finishedAt ?? snapshot.updatedAt ?? null,
    updatedAt: snapshot.updatedAt ?? null,
    events: Array.isArray(snapshot.events) ? snapshot.events : [],
    assistantStreams: Array.isArray(snapshot.assistantStreams) ? snapshot.assistantStreams : [],
    eventLabels: Array.isArray(snapshot.eventLabels) ? snapshot.eventLabels : [],
    lastEvent: snapshot.lastEvent ?? null,
    elapsedMs: snapshot.elapsedMs ?? null,
    waitingFor: snapshot.waitingFor ?? null,
    outputTruncated: snapshot.outputTruncated === true || extra.outputTruncated === true,
    valuesPrinted: false,
    note: extra.note ?? "runnerTrace snapshot"
  };
}

function normalizeTraceEvent(event, { traceId, seq, now, fallbackRunnerKind } = {}) {
  const type = safeToken(event.type ?? event.kind ?? "event");
  const status = safeToken(event.status ?? "observed");
  const toolName = safeText(event.toolName ?? event.name, 120);
  const label = safeText(event.label, 180) || labelFor({ type, status, toolName });
  const appendedAt = timestampFor(now);
  const eventCreatedAt = safeTimestamp(event.createdAt) ?? timestampFor(now);
  return dropUndefined({
    seq,
    traceId,
    type,
    stage: safeToken(event.stage ?? type),
    status,
    label,
    createdAt: eventCreatedAt,
    appendedAt,
    eventType: safeToken(event.eventType ?? type),
    backend: safeText(event.backend, 140),
    elapsedMs: typeof event.elapsedMs === "number" ? Math.max(0, Math.trunc(event.elapsedMs)) : null,
    runnerKind: safeText(event.runnerKind ?? fallbackRunnerKind, 140),
    source: safeText(event.source, 120),
    sourceSeq: Number.isInteger(event.sourceSeq) ? event.sourceSeq : undefined,
    runId: safeText(event.runId, 180),
    commandId: safeText(event.commandId, 180),
    attemptId: safeText(event.attemptId, 180),
    runnerId: safeText(event.runnerId, 180),
    jobName: safeText(event.jobName, 180),
    namespace: safeText(event.namespace, 180),
    sessionId: safeText(event.sessionId, 180),
    sessionStatus: safeText(event.sessionStatus, 80),
    sessionLifecycleStatus: safeText(event.sessionLifecycleStatus, 80),
    sessionReused: typeof event.sessionReused === "boolean" ? event.sessionReused : undefined,
    turn: typeof event.turn === "number" ? event.turn : undefined,
    toolName,
    itemId: safeText(event.itemId, 180),
    command: safeText(event.command, 900),
    exitCode: Number.isInteger(event.exitCode) ? event.exitCode : undefined,
    durationMs: typeof event.durationMs === "number" ? Math.max(0, Math.trunc(event.durationMs)) : undefined,
    outputBytes: typeof event.outputBytes === "number" ? Math.max(0, Math.trunc(event.outputBytes)) : undefined,
    promptId: safeText(event.promptId, 180),
    promptSha256: safeText(event.promptSha256, 80),
    promptSummary: safeText(event.promptSummary, 240),
    promptPreview: safeText(event.promptPreview, 500),
    outputSummary: safeText(event.outputSummary, 400),
    stdoutSummary: safeText(event.stdoutSummary, 600),
    stderrSummary: safeText(event.stderrSummary, 600),
    chunk: safeMessageText(event.chunk, 400),
    message: safeMessageText(event.message, 1200),
    errorCode: safeText(event.errorCode, 120),
    diagnosisCode: safeText(event.diagnosisCode ?? event.diagnosis?.code, 160),
    diagnosis: normalizeDiagnosis(event.diagnosis),
    details: normalizeDetails(event.details),
    waitingFor: safeText(event.waitingFor, 220),
    timeoutMs: typeof event.timeoutMs === "number" ? Math.trunc(event.timeoutMs) : undefined,
    hardTimeoutMs: typeof event.hardTimeoutMs === "number" ? Math.trunc(event.hardTimeoutMs) : undefined,
    lastActivityAt: safeText(event.lastActivityAt, 80),
    idleMs: typeof event.idleMs === "number" ? Math.max(0, Math.trunc(event.idleMs)) : undefined,
    outputTruncated: event.outputTruncated === true,
    terminal: event.terminal === true,
    valuesPrinted: false
  });
}

function normalizeDetails(value, depth = 0) {
  if (value === undefined || value === null || depth > 3) return undefined;
  if (typeof value === "boolean") return value;
  if (typeof value === "number") return Number.isFinite(value) ? Math.trunc(value) : undefined;
  if (typeof value === "string") return safeText(value, 240) || undefined;
  if (Array.isArray(value)) {
    const items = value.slice(0, 40).map((item) => normalizeDetails(item, depth + 1)).filter((item) => item !== undefined);
    return items.length > 0 ? items : undefined;
  }
  if (typeof value !== "object") return undefined;
  const result = {};
  for (const [key, item] of Object.entries(value).slice(0, 80)) {
    const safeKey = safeToken(key);
    if (!safeKey || safeKey === "prompt" || safeKey === "content" || safeKey === "text") continue;
    const normalized = normalizeDetails(item, depth + 1);
    if (normalized !== undefined) result[safeKey] = normalized;
  }
  return Object.keys(result).length > 0 ? result : undefined;
}

function normalizeAssistantDelta(delta, { traceId, now, fallbackRunnerKind } = {}) {
  const chunk = safeMessageText(delta.chunk ?? delta.delta, 400);
  if (!chunk) return null;
  return dropUndefined({
    traceId,
    itemId: safeText(delta.itemId, 180),
    chunk,
    createdAt: safeTimestamp(delta.createdAt) ?? timestampFor(now),
    runnerKind: safeText(delta.runnerKind ?? fallbackRunnerKind, 140),
    sessionId: safeText(delta.sessionId, 180),
    sessionStatus: safeText(delta.sessionStatus, 80),
    turn: typeof delta.turn === "number" ? delta.turn : undefined,
    threadId: safeText(delta.threadId, 180),
    turnId: safeText(delta.turnId, 180),
    waitingFor: safeText(delta.waitingFor, 220),
    valuesPrinted: false
  });
}

function safeTimestamp(value) {
  const text = safeText(value, 80);
  if (!text) return null;
  const ms = Date.parse(text);
  return Number.isFinite(ms) ? new Date(ms).toISOString() : null;
}

function assistantStreamsSnapshot(trace) {
  if (!(trace?.assistantStreams instanceof Map)) return [];
  return [...trace.assistantStreams.values()].map((stream) => ({ ...stream }));
}

function labelFor({ type, status, toolName }) {
  if (type === "session") return `session:${status}`;
  if (type === "prompt") return `prompt:${status}`;
  if (type === "tool_call") return `tool:${toolName || "codex"}:${status}`;
  if (type === "assistant_message") return `assistant:${status}`;
  if (["timeout", "cancel", "error"].includes(type)) return type;
  if (type === "request") return `request:${status}`;
  if (type === "stdio") return `stdio:${status}`;
  return `${type}:${status}`;
}

function notify(trace, event) {
  const assistantStreams = assistantStreamsSnapshot(trace);
  const snapshot = {
    ...emptySnapshot(trace.traceId),
    ...{
      traceId: trace.traceId,
      status: trace.status,
      createdAt: trace.createdAt,
      updatedAt: trace.updatedAt,
      startedAt: trace.startedAt,
      finishedAt: trace.finishedAt,
      eventCount: trace.events.length,
      events: trace.events.map((item) => ({ ...item })),
      assistantStreams,
      eventLabels: trace.events.map((item) => item.label).filter(Boolean),
      lastEvent: event ?? trace.events.at(-1) ?? null,
      elapsedMs: elapsedMs(trace.startedAt, trace.finishedAt ?? trace.updatedAt),
      waitingFor: snapshotWaitingFor(trace, trace.events, assistantStreams, event ?? trace.events.at(-1) ?? null),
      runnerKind: trace.meta.runnerKind,
      workspace: trace.meta.workspace,
      sandbox: trace.meta.sandbox,
      sessionMode: trace.meta.sessionMode,
      implementationType: trace.meta.implementationType,
      sessionId: event?.sessionId ?? assistantStreams.at(-1)?.sessionId ?? null,
      sessionStatus: event?.sessionStatus ?? assistantStreams.at(-1)?.sessionStatus ?? null,
      sessionLifecycleStatus: event?.sessionLifecycleStatus ?? null,
      outputTruncated: trace.events.some((item) => item.outputTruncated === true),
      valuesPrinted: false
    }
  };
  for (const listener of trace.listeners) {
    try {
      listener(event, snapshot);
    } catch {
      trace.listeners.delete(listener);
    }
  }
}

function emptySnapshot(traceId, extra = {}) {
  const id = cleanTraceId(traceId);
  return {
    traceId: id,
    status: "missing",
    createdAt: null,
    updatedAt: null,
    startedAt: null,
    finishedAt: null,
    eventCount: 0,
    events: [],
    assistantStreams: [],
    eventLabels: [],
    lastEvent: null,
    elapsedMs: null,
    waitingFor: null,
    runnerKind: extra.runnerKind ?? null,
    workspace: extra.workspace ?? null,
    sandbox: extra.sandbox ?? null,
    sessionMode: extra.sessionMode ?? null,
    implementationType: extra.implementationType ?? null,
    sessionId: null,
    sessionStatus: null,
    sessionLifecycleStatus: null,
    turn: null,
    outputTruncated: false,
    valuesPrinted: false
  };
}

function lastWaitingFor(events) {
  for (const event of [...events].reverse()) {
    if (event.waitingFor) return event.waitingFor;
    if (event.type === "tool_call" && ["started", "output_chunk"].includes(event.status)) {
      return `tool:${event.toolName ?? "codex"}`;
    }
    if (event.type === "prompt" && event.status === "sent") return "assistant-message";
  }
  return null;
}

function snapshotWaitingFor(trace, events, assistantStreams, lastEvent) {
  if (lastEvent?.terminal === true) {
    return lastEvent.waitingFor ?? null;
  }
  if (trace?.finishedAt) return null;
  return lastWaitingFor(events) ?? assistantStreams.at(-1)?.waitingFor ?? null;
}

function elapsedMs(start, end) {
  const startMs = Date.parse(start ?? "");
  const endMs = Date.parse(end ?? "");
  if (!Number.isFinite(startMs) || !Number.isFinite(endMs)) return null;
  return Math.max(0, endMs - startMs);
}

function normalizeDiagnosis(value) {
  if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
  const lastRoutedEvent = value.lastRoutedEvent && typeof value.lastRoutedEvent === "object" && !Array.isArray(value.lastRoutedEvent)
    ? dropUndefined({
        type: safeText(value.lastRoutedEvent.type, 80),
        status: safeText(value.lastRoutedEvent.status, 80),
        label: safeText(value.lastRoutedEvent.label, 180),
        stage: safeText(value.lastRoutedEvent.stage, 80),
        toolName: safeText(value.lastRoutedEvent.toolName, 120),
        itemId: safeText(value.lastRoutedEvent.itemId, 180),
        waitingFor: safeText(value.lastRoutedEvent.waitingFor, 220),
        exitCode: Number.isInteger(value.lastRoutedEvent.exitCode) ? value.lastRoutedEvent.exitCode : undefined,
        durationMs: typeof value.lastRoutedEvent.durationMs === "number" ? Math.max(0, Math.trunc(value.lastRoutedEvent.durationMs)) : undefined
      })
    : undefined;
  return dropUndefined({
    code: safeText(value.code, 160),
    layer: safeText(value.layer, 120),
    kind: safeText(value.kind, 120),
    summary: safeText(value.summary, 500),
    waitingFor: safeText(value.waitingFor, 220),
    lastActivityAt: safeText(value.lastActivityAt, 80),
    idleMs: typeof value.idleMs === "number" ? Math.max(0, Math.trunc(value.idleMs)) : undefined,
    lastActivityLabel: safeText(value.lastActivityLabel, 180),
    threadId: safeText(value.threadId, 180),
    turnId: safeText(value.turnId, 180),
    lastRoutedEvent
  });
}

function safeText(value, limit = TEXT_LIMIT) {
  if (value === undefined || value === null || value === "") return undefined;
  const redacted = String(value).replace(SECRET_PATTERN, "[redacted]").replace(/\s+/gu, " ").trim();
  if (!redacted) return undefined;
  return redacted.length > limit ? `${redacted.slice(0, limit - 3)}...` : redacted;
}

function safeMessageText(value, limit = TEXT_LIMIT) {
  if (value === undefined || value === null || value === "") return undefined;
  const redacted = String(value).replace(SECRET_PATTERN, "[redacted]").replace(/\r\n?/gu, "\n");
  if (!redacted.trim()) return undefined;
  return redacted.length > limit ? `${redacted.slice(0, limit - 3)}...` : redacted;
}

function safeToken(value) {
  return String(value ?? "")
    .trim()
    .replace(/[^A-Za-z0-9_.:-]/gu, "_")
    .slice(0, 80) || "event";
}

function cleanTraceId(value) {
  const id = String(value ?? "").trim();
  return /^trc_[A-Za-z0-9_.:-]+$/u.test(id) ? id : null;
}

function timestampFor(now) {
  const value = typeof now === "function" ? now() : now;
  const date = value ? new Date(value) : new Date();
  return Number.isNaN(date.getTime()) ? new Date().toISOString() : date.toISOString();
}

function traceSourceEventKey(event) {
  if (!event || typeof event !== "object") return null;
  const source = textOrNull(event.source);
  const sourceSeq = textOrNull(event.sourceSeq);
  if (!source || !sourceSeq) return null;
  const label = textOrNull(event.label ?? event.type) ?? "event";
  return `${source}:${sourceSeq}:${label}`;
}

function rebuildTraceEventIndexes(trace) {
  trace.sourceEventKeys = new Set();
  trace.eventSignatures = new Set();
  for (const event of trace.events) {
    const sourceKey = traceSourceEventKey(event);
    if (sourceKey) trace.sourceEventKeys.add(sourceKey);
    const signature = traceEventSignature(event);
    if (signature) trace.eventSignatures.add(signature);
  }
}

function traceEventSignature(event) {
  if (!event || typeof event !== "object") return null;
  const type = textOrNull(event.type);
  const status = textOrNull(event.status);
  const label = textOrNull(event.label);
  const createdAt = textOrNull(event.createdAt);
  if (!type || !status || !label || !createdAt) return null;
  return [
    type,
    status,
    label,
    createdAt,
    textOrNull(event.runId),
    textOrNull(event.commandId),
    textOrNull(event.itemId),
    textOrNull(event.message)
  ].map((item) => item ?? "").join("\u0001");
}

function shouldSuppressPostTerminalEvent(trace, event) {
  if (!trace?.finishedAt) return false;
  const finishedMs = Date.parse(trace.finishedAt);
  const eventMs = Date.parse(event?.createdAt ?? "");
  if (!Number.isFinite(finishedMs) || !Number.isFinite(eventMs)) return true;
  return eventMs > finishedMs;
}

function minTimestamp(left, right) {
  const leftMs = Date.parse(left ?? "");
  const rightMs = Date.parse(right ?? "");
  if (!Number.isFinite(leftMs)) return right;
  if (!Number.isFinite(rightMs)) return left;
  return rightMs < leftMs ? right : left;
}

function maxTimestamp(left, right) {
  const leftMs = Date.parse(left ?? "");
  const rightMs = Date.parse(right ?? "");
  if (!Number.isFinite(leftMs)) return right;
  if (!Number.isFinite(rightMs)) return left;
  return rightMs > leftMs ? right : left;
}

function textOrNull(value) {
  const text = String(value ?? "").trim();
  return text ? text : null;
}

function positiveInteger(value, fallback) {
  const parsed = Number.parseInt(value ?? "", 10);
  return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
}

function dropEmpty(value) {
  return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined && item !== null && item !== ""));
}

function dropUndefined(value) {
  return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
}
