// SPEC: PJ2026-01040111 long-running Workbench observation.
// Responsibility: Analyzer sample metrics, loading continuity, session rail title, and bounded report rows source fragment.

export function nodeWebObserveAnalyzerSampleMetricsSource(): string {
  return String.raw`function isTerminalTraceText(text) {
  return /轮次完成|轮次失败|轮次取消|已记录|已完成第\d+轮|final response|sealed final response|turn completed|turn failed|turn canceled|terminal result|\bcompleted\b|\bfailed\b|\bcanceled\b|\bcancelled\b|\bterminal\b|\bdone\b/iu.test(String(text || ""));
}

function isFinalResultText(text) {
  return /已完成第\d+轮|已按第\d+轮完成|final response|sealed final response|最终结果|已完成[:：]|smoke\s*测试结果|benchmark|PVC\/workspace|修改文件|Results:/iu.test(String(text || ""));
}

function buildSampleMetrics(samples, control) {
  const promptCommands = buildSendPromptCommandTimeline(control);
  const promptTimes = promptCommands.map((item) => item.tsMs);
  const timeline = samples.map((sample) => {
    const texts = sampleTexts(sample);
    const timingTexts = sampleTurnTimingTexts(sample);
    const tsMs = Date.parse(sample.ts);
    const promptIndex = Number.isFinite(tsMs) ? latestPromptIndex(promptTimes, tsMs) : 0;
    const totalElapsedValues = timingTexts.flatMap(parseTotalElapsedSeconds).filter(Number.isFinite);
    const recentUpdateValues = timingTexts.flatMap(parseRecentUpdateSeconds).filter(Number.isFinite);
    const diagnosticTexts = texts.filter(isDiagnosticText).slice(0, 5);
    const terminalTexts = texts.filter(isTerminalTraceText).slice(0, 5);
    const finalResultTexts = texts.filter(isFinalResultText).slice(0, 5);
    const loadings = Array.isArray(sample.loadings) ? sample.loadings : [];
    const loadingOwners = uniqueLoadingOwners(loadings);
    return {
      seq: sample.seq ?? null,
      ts: sample.ts ?? null,
      routeSessionId: sample.routeSessionId ?? null,
      activeSessionId: sample.activeSessionId ?? null,
      promptIndex,
      messageCount: Array.isArray(sample.messages) ? sample.messages.length : 0,
      traceRowCount: Array.isArray(sample.traceRows) ? sample.traceRows.length : 0,
      loadingCount: loadings.length,
    loadingOwnerCount: loadingOwners.length,
    loadingOwners: loadingOwners.map((item) => ({ ownerKey: item.ownerKey, ownerKind: item.ownerKind, ownerLabel: item.ownerLabel, count: item.count })).slice(0, 12),
      sessionRailVisibleCount: Number(sample?.sessionRail?.visibleCount ?? 0),
      sessionRailFallbackTitleCount: Number(sample?.sessionRail?.fallbackTitleCount ?? 0),
      sessionRailFallbackTitleRatio: Number(sample?.sessionRail?.fallbackTitleRatio ?? 0),
      totalElapsedSeconds: totalElapsedValues.length > 0 ? Math.max(...totalElapsedValues) : null,
      recentUpdateSeconds: recentUpdateValues.length > 0 ? Math.max(...recentUpdateValues) : null,
      terminalSeen: terminalTexts.length > 0,
      finalResultTextSeen: finalResultTexts.length > 0,
      diagnosticSeen: diagnosticTexts.length > 0,
      diagnosticTextHashes: diagnosticTexts.map(sha256).slice(0, 5),
      textDigest: digestSample(sample)
    };
  });
  const turnTiming = buildTurnTimingTable(samples, timeline);
  const traceOrder = buildTraceOrderMetrics(samples, timeline);
  const codeAgentCardTiming = buildCodeAgentCardTimingMetrics(samples, timeline, turnTiming);
  const codeAgentCardDurationUnderreported = buildCodeAgentCardDurationUnderreportedMetrics(samples, timeline);
  const codeAgentCardDurationMismatches = buildCodeAgentCardDurationMismatchMetrics(samples, timeline);
  if (codeAgentCardTiming && codeAgentCardTiming.summary) {
    codeAgentCardTiming.summary.durationUnderreportedCount = codeAgentCardDurationUnderreported.length;
    codeAgentCardTiming.summary.durationMismatchCount = codeAgentCardDurationMismatches.length;
    codeAgentCardTiming.durationUnderreported = codeAgentCardDurationUnderreported;
    codeAgentCardTiming.durationMismatches = codeAgentCardDurationMismatches;
  }
  const turnCells = turnTiming.rows.flatMap((row) => Object.values(row.cells || {}));
  const turnTimingNonMonotonic = Array.isArray(turnTiming.nonMonotonic) ? turnTiming.nonMonotonic : [];
  const turnTimingElapsedZeroResets = Array.isArray(turnTiming.elapsedZeroResets) ? turnTiming.elapsedZeroResets : [];
  const turnTimingTotalElapsedForwardJumps = Array.isArray(turnTiming.totalElapsedForwardJumps) ? turnTiming.totalElapsedForwardJumps : [];
  const turnTimingRecentUpdateSawtoothJumps = turnTimingNonMonotonic.filter((item) => item.metric === "recentUpdateSeconds" && item.anomaly === "jump");
  const turnTimingTerminalElapsedGrowth = Array.isArray(turnTiming.terminalElapsedGrowth) ? turnTiming.terminalElapsedGrowth : [];
  const turnTimingRecentUpdateResets = Array.isArray(turnTiming.recentUpdateResets) ? turnTiming.recentUpdateResets : [];
  const turnTimingRecentUpdateSteps = Array.isArray(turnTiming.recentUpdateSteps) ? turnTiming.recentUpdateSteps : [];
  const turnTimingTerminalElapsedGrowthDeltas = turnTimingTerminalElapsedGrowth
    .map((item) => Number(item.delta))
    .filter((value) => Number.isFinite(value) && value > 0);
  const turnTimingRecentUpdateLargestSteps = turnTimingRecentUpdateSteps
    .filter((item) => Number.isFinite(Number(item.delta)))
    .slice()
    .sort((a, b) => Number(b.delta) - Number(a.delta))
    .slice(0, 200);
  const turnTimingRecentUpdatePositiveSteps = turnTimingRecentUpdateSteps
    .map((item) => Number(item.delta))
    .filter((value) => Number.isFinite(value) && value >= 0);
  const turnTimingRecentUpdateExcessSteps = turnTimingRecentUpdateSteps
    .map((item) => Number(item.excessiveIncreaseSeconds))
    .filter((value) => Number.isFinite(value) && value > 0);
  const withTotal = timeline.filter((item) => item.totalElapsedSeconds !== null).length;
  const withRecent = timeline.filter((item) => item.recentUpdateSeconds !== null).length;
  const diagnostics = timeline.filter((item) => item.diagnosticSeen).length;
  const loading = buildLoadingMetrics(samples, timeline);
  const sessionRailTitles = buildSessionRailTitleMetrics(samples, timeline);
  const workbenchTurnStateTriad = buildWorkbenchTurnStateTriadMetrics(samples, timeline);
  const reportTurnTimingRows = boundedTurnTimingRowsForReport(turnTiming.rows);
  const reportTimeline = boundedRowsForReport(timeline);
  const rounds = buildRoundMetricSummaries(timeline, promptCommands, {
    columns: turnTiming.columns,
    rows: turnTiming.rows,
    nonMonotonic: turnTimingNonMonotonic,
    elapsedZeroResets: turnTimingElapsedZeroResets,
    totalElapsedForwardJumps: turnTimingTotalElapsedForwardJumps,
    terminalElapsedGrowth: turnTimingTerminalElapsedGrowth,
    recentUpdateResets: turnTimingRecentUpdateResets,
    recentUpdateSteps: turnTimingRecentUpdateSteps
  });
  const recentUpdateJumpCount = turnTimingRecentUpdateSawtoothJumps.length;
  return {
    summary: {
      sampleCount: timeline.length,
      withTotalElapsed: withTotal,
      withRecentUpdate: withRecent,
      diagnostics,
      loadingSampleCount: loading.summary.loadingSampleCount,
      loadingMaxCount: loading.summary.maxSimultaneousCount,
      loadingMaxOwnerCount: loading.summary.maxSimultaneousOwnerCount,
      loadingOwnerCount: loading.summary.ownerCount,
      loadingConcurrentSampleCount: loading.summary.concurrentLoadingSampleCount,
      loadingLongestContinuousSeconds: loading.summary.longestContinuousSeconds,
      loadingCurrentContinuousSeconds: loading.summary.currentContinuousSeconds,
      loadingOverFiveSecondSegmentCount: loading.summary.overFiveSecondSegmentCount,
      loadingOverBudgetSegmentCount: loading.summary.overBudgetSegmentCount,
      sessionRailSampleCount: sessionRailTitles.summary.sampleCount,
      sessionRailVisibleSampleCount: sessionRailTitles.summary.visibleSampleCount,
      sessionRailFallbackMajoritySampleCount: sessionRailTitles.summary.majorityFallbackSampleCount,
      sessionRailFallbackMaxRatio: sessionRailTitles.summary.maxFallbackRatio,
      sessionRailFallbackMaxVisibleCount: sessionRailTitles.summary.maxVisibleCount,
      sessionRailFallbackMaxCount: sessionRailTitles.summary.maxFallbackTitleCount,
      workbenchTurnStateTriadRows: workbenchTurnStateTriad.summary.rowCount,
      workbenchTurnStateTriadInvalidRows: workbenchTurnStateTriad.summary.invalidRowCount,
      workbenchTurnStateTriadFullInvalidRows: workbenchTurnStateTriad.summary.invalidFullTriadCount,
      workbenchTurnStateCardFinalResponseMismatchRows: workbenchTurnStateTriad.summary.cardFinalResponseMismatchCount,
      workbenchTurnStateCollectorMissingRows: workbenchTurnStateTriad.summary.collectorMissingRowCount,
      promptSegments: Math.max(0, promptTimes.length),
      rounds: rounds.length,
      turnColumns: turnTiming.columns.length,
      turnTimingRows: turnTiming.rows.length,
      turnCellsWithTotalElapsed: turnCells.filter((item) => item.totalElapsedSeconds !== null).length,
      turnCellsWithRecentUpdate: turnCells.filter((item) => item.recentUpdateSeconds !== null).length,
      turnTimingNonMonotonicCount: turnTimingNonMonotonic.length,
      turnTimingTotalElapsedDecreaseCount: turnTimingNonMonotonic.filter((item) => item.metric === "totalElapsedSeconds").length,
      turnTimingTotalElapsedZeroResetCount: turnTimingElapsedZeroResets.length,
      turnTimingTotalElapsedForwardJumpCount: turnTimingTotalElapsedForwardJumps.length,
      turnTimingTotalElapsedForwardJumpMaxSeconds: maxPositiveDelta(turnTimingTotalElapsedForwardJumps),
      turnTimingTerminalElapsedGrowthCount: turnTimingTerminalElapsedGrowth.length,
      turnTimingTerminalElapsedGrowthMaxSeconds: turnTimingTerminalElapsedGrowthDeltas.length > 0 ? Math.max(...turnTimingTerminalElapsedGrowthDeltas) : 0,
      turnTimingRecentUpdateJumpCount: recentUpdateJumpCount,
      turnTimingRecentUpdateSawtoothJumpCount: recentUpdateJumpCount,
      turnTimingRecentUpdateStepCount: turnTimingRecentUpdateSteps.length,
      turnTimingRecentUpdateMaxIncreaseSeconds: turnTimingRecentUpdatePositiveSteps.length > 0 ? Math.max(...turnTimingRecentUpdatePositiveSteps) : null,
      turnTimingRecentUpdateMaxExcessSeconds: turnTimingRecentUpdateExcessSteps.length > 0 ? Math.max(...turnTimingRecentUpdateExcessSteps) : 0,
      turnTimingRecentUpdateResetCount: turnTimingRecentUpdateResets.length,
      turnTimingRecentUpdateDecreaseCount: turnTimingRecentUpdateResets.length,
      codeAgentCardSampleCount: codeAgentCardTiming.summary.cardSampleCount,
      codeAgentCardMissingElapsedCount: codeAgentCardTiming.summary.missingElapsedCount,
      codeAgentCardMissingRecentUpdateCount: codeAgentCardTiming.summary.missingRecentUpdateCount,
      roundCompletionEventCount: codeAgentCardTiming.summary.roundCompletionEventCount,
      roundCompletionElapsedMismatchCount: codeAgentCardTiming.summary.roundCompletionElapsedMismatchCount,
      roundCompletionFinalResponseMissingCount: codeAgentCardTiming.summary.roundCompletionFinalResponseMissingCount,
      roundCompletionPostTimingChangeCount: codeAgentCardTiming.summary.roundCompletionPostTimingChangeCount,
      codeAgentCardDurationUnderreportedCount: codeAgentCardTiming.summary.durationUnderreportedCount,
      codeAgentCardDurationMismatchCount: codeAgentCardTiming.summary.durationMismatchCount,
      traceRowCount: traceOrder.summary.traceRowCount,
      traceRowOrderAnomalyCount: traceOrder.summary.orderAnomalyCount,
      traceRowCompletionNotLastCount: traceOrder.summary.completionNotLastCount,
      roundsWithTurnTimingNonMonotonic: rounds.filter((item) => item.turnTimingNonMonotonicCount > 0).length,
      roundsWithTurnTimingTotalElapsedForwardJumps: rounds.filter((item) => item.turnTimingTotalElapsedForwardJumpCount > 0).length,
      roundsWithTerminalElapsedGrowth: rounds.filter((item) => item.turnTimingTerminalElapsedGrowthCount > 0).length,
      roundsWithRecentUpdateJumps: rounds.filter((item) => item.turnTimingRecentUpdateJumpCount > 0).length
    },
    loading,
    sessionRailTitles,
    workbenchTurnStateTriad,
    codeAgentCardTiming,
    traceOrder,
    rounds,
    turnColumns: turnTiming.columns,
    turnTimingTable: reportTurnTimingRows.rows,
    turnTimingTableDisclosure: reportTurnTimingRows.disclosure,
    turnTimingNonMonotonic,
    turnTimingElapsedZeroResets,
    turnTimingTotalElapsedForwardJumps,
    turnTimingTerminalElapsedGrowth,
    turnTimingRecentUpdateSawtoothJumps,
    turnTimingRecentUpdateSteps,
    turnTimingRecentUpdateLargestSteps,
    turnTimingRecentUpdateResets,
    timeline: reportTimeline.rows,
    timelineDisclosure: reportTimeline.disclosure
  };
}

function boundedRowsForReport(rows) {
  const sourceRows = Array.isArray(rows) ? rows : [];
  const maxRows = 1200;
  const headRows = 120;
  if (sourceRows.length <= maxRows) return { rows: sourceRows, disclosure: { truncated: false, totalRows: sourceRows.length, includedRows: sourceRows.length, omittedRows: 0, headRows: sourceRows.length, tailRows: 0 } };
  const tailRows = Math.max(0, maxRows - headRows);
  return {
    rows: [...sourceRows.slice(0, headRows), ...sourceRows.slice(-tailRows)],
    disclosure: {
      truncated: true,
      totalRows: sourceRows.length,
      includedRows: maxRows,
      omittedRows: Math.max(0, sourceRows.length - maxRows),
      headRows,
      tailRows,
      policy: "report-bounded-head-tail; summary metrics are computed before truncation",
      valuesRedacted: true
    }
  };
}

function buildSendPromptCommandTimeline(control) {
  const byCommand = new Map();
  let ordinal = 0;
  for (const item of Array.isArray(control) ? control : []) {
    if (item?.type !== "sendPrompt") continue;
    const commandId = item.commandId ? String(item.commandId) : "sendPrompt-" + String(ordinal++);
    const existing = byCommand.get(commandId) || {
      commandId,
      startedAt: null,
      startedTsMs: null,
      completedAt: null,
      completedTsMs: null,
      failedAt: null,
      failedTsMs: null,
      textHash: null,
      textBytes: null,
    };
    if (!existing.textHash && item.input?.textHash) existing.textHash = item.input.textHash;
    if (!existing.textBytes && item.input?.textBytes) existing.textBytes = item.input.textBytes;
    const tsMs = Date.parse(item.ts);
    if (item.phase === "started" && Number.isFinite(tsMs)) {
      existing.startedAt = item.ts ?? existing.startedAt;
      existing.startedTsMs = tsMs;
    } else if (item.phase === "completed" && Number.isFinite(tsMs)) {
      existing.completedAt = item.ts ?? existing.completedAt;
      existing.completedTsMs = tsMs;
    } else if (item.phase === "failed" && Number.isFinite(tsMs)) {
      existing.failedAt = item.ts ?? existing.failedAt;
      existing.failedTsMs = tsMs;
    }
    byCommand.set(commandId, existing);
  }
  return Array.from(byCommand.values())
    .map((item) => {
      const tsMs = Number.isFinite(item.startedTsMs) ? item.startedTsMs : Number.isFinite(item.completedTsMs) ? item.completedTsMs : item.failedTsMs;
      const ts = Number.isFinite(item.startedTsMs) ? item.startedAt : Number.isFinite(item.completedTsMs) ? item.completedAt : item.failedAt;
      return { ...item, ts, tsMs };
    })
    .filter((item) => Number.isFinite(item.tsMs))
    .sort((a, b) => a.tsMs - b.tsMs)
    .map((item, index) => ({ ...item, promptIndex: index + 1 }));
}

function buildSessionRailTitleMetrics(samples, timeline) {
  const rows = [];
  const examplesByHash = new Map();
  for (let index = 0; index < (Array.isArray(samples) ? samples : []).length; index += 1) {
    const sample = samples[index];
    const rail = sample?.sessionRail && typeof sample.sessionRail === "object" ? sample.sessionRail : null;
    if (!rail) continue;
    const visibleCount = Number(rail.visibleCount ?? 0);
    const fallbackTitleCount = Number(rail.fallbackTitleCount ?? 0);
    const safeVisibleCount = Number.isFinite(visibleCount) && visibleCount > 0 ? visibleCount : 0;
    const safeFallbackTitleCount = Number.isFinite(fallbackTitleCount) && fallbackTitleCount > 0 ? fallbackTitleCount : 0;
    const fallbackTitleRatio = safeVisibleCount > 0 ? Number((safeFallbackTitleCount / safeVisibleCount).toFixed(4)) : 0;
    const fallbackItems = Array.isArray(rail.fallbackItems) ? rail.fallbackItems : [];
    for (const item of fallbackItems) {
      const hash = String(item?.titleHash || item?.titlePreview || item?.sessionIdPrefix || "").trim();
      if (!hash || examplesByHash.has(hash)) continue;
      examplesByHash.set(hash, {
        titleHash: item?.titleHash ?? null,
        titlePreview: limitText(String(item?.titlePreview || ""), 160),
        sessionIdPrefix: item?.sessionIdPrefix ?? null,
        active: item?.active === true,
        firstSeq: sample?.seq ?? null,
        firstAt: sample?.ts ?? null,
        pageRole: sample?.pageRole ?? null,
      });
    }
    rows.push({
      ...ref(sample),
      promptIndex: timeline[index]?.promptIndex ?? 0,
      visibleCount: safeVisibleCount,
      fallbackTitleCount: safeFallbackTitleCount,
      fallbackTitleRatio,
      majorityFallback: safeVisibleCount > 0 && safeFallbackTitleCount > safeVisibleCount / 2,
      overThreshold: safeVisibleCount > 0 && fallbackTitleRatio > alertThresholds.sessionRailFallbackRatio,
      examples: fallbackItems.slice(0, 5).map((item) => ({
        titleHash: item?.titleHash ?? null,
        titlePreview: limitText(String(item?.titlePreview || ""), 160),
        sessionIdPrefix: item?.sessionIdPrefix ?? null,
        active: item?.active === true,
      })),
    });
  }
  const visibleRows = rows.filter((item) => item.visibleCount > 0);
  const majorityRows = rows.filter((item) => item.majorityFallback);
  const overThresholdRows = rows.filter((item) => item.overThreshold);
  const fallbackRows = rows.filter((item) => item.fallbackTitleCount > 0);
  const maxFallbackRatio = rows.length > 0 ? Math.max(...rows.map((item) => Number(item.fallbackTitleRatio) || 0)) : 0;
  const maxVisibleCount = rows.length > 0 ? Math.max(...rows.map((item) => Number(item.visibleCount) || 0)) : 0;
  const maxFallbackTitleCount = rows.length > 0 ? Math.max(...rows.map((item) => Number(item.fallbackTitleCount) || 0)) : 0;
  return {
    summary: {
      sampleCount: rows.length,
      visibleSampleCount: visibleRows.length,
      fallbackSampleCount: fallbackRows.length,
      majorityFallbackSampleCount: majorityRows.length,
      overThresholdSampleCount: overThresholdRows.length,
      thresholdRatio: alertThresholds.sessionRailFallbackRatio,
      maxFallbackRatio,
      maxVisibleCount,
      maxFallbackTitleCount,
    },
    samples: majorityRows.slice(0, 80),
    examples: Array.from(examplesByHash.values()).slice(0, 80),
    timeline: rows.slice(-200),
    valuesRedacted: true
  };
}

function uniqueLoadingOwners(loadings) {
  const groups = new Map();
  for (let index = 0; index < (Array.isArray(loadings) ? loadings : []).length; index += 1) {
    const item = loadings[index];
    const ownerKey = loadingOwnerKey(item, index);
    const ownerIdentity = loadingOwnerIdentity(item);
    const existing = groups.get(ownerKey) || {
      ownerKey,
      ownerKind: item?.ownerKind ?? "unknown",
      ownerLabel: loadingOwnerLabel(item, ownerKey),
      ...ownerIdentity,
      count: 0,
      textHashes: []
    };
    existing.count += 1;
    if (item?.textHash && !existing.textHashes.includes(item.textHash)) existing.textHashes.push(item.textHash);
    for (const key of ["ownerSessionId", "ownerMessageId", "ownerTraceId"]) {
      if (!existing[key] && ownerIdentity[key]) existing[key] = ownerIdentity[key];
    }
    groups.set(ownerKey, existing);
  }
  return Array.from(groups.values()).sort((a, b) => b.count - a.count || String(a.ownerLabel).localeCompare(String(b.ownerLabel)));
}

function loadingOwnerIdentity(item) {
  const owner = item?.owner && typeof item.owner === "object" ? item.owner : {};
  return {
    ownerSessionId: owner.sessionId ?? null,
    ownerMessageId: owner.messageId ?? null,
    ownerTraceId: owner.traceId ?? null,
  };
}

function loadingOwnerKey(item, index = 0) {
  const key = String(item?.ownerKey || "").trim();
  if (key) return key.slice(0, 240);
  const owner = item?.owner && typeof item.owner === "object" ? item.owner : {};
  return [
    item?.ownerKind || "unknown",
    owner.testId || item?.testId || owner.id || owner.role || owner.className || item?.role || item?.tag || "node",
    owner.sessionId || owner.messageId || owner.traceId || item?.textHash || String(index)
  ].filter(Boolean).join(":").slice(0, 240);
}

function loadingOwnerLabel(item, fallback) {
  return limitText(String(item?.ownerLabel || item?.owner?.ariaLabel || item?.owner?.testId || item?.owner?.className || fallback || "unknown"), 160);
}

function buildLoadingMetrics(samples, timeline) {
  const events = samples.map((sample, index) => {
    const tsMs = Date.parse(sample?.ts);
    const loadings = Array.isArray(sample?.loadings) ? sample.loadings : [];
    const owners = uniqueLoadingOwners(loadings);
    return {
      seq: sample?.seq ?? null,
      ts: sample?.ts ?? null,
      tsMs,
      promptIndex: timeline[index]?.promptIndex ?? 0,
      routeSessionId: sample?.routeSessionId ?? null,
      activeSessionId: sample?.activeSessionId ?? null,
      loadingCount: loadings.length,
      ownerCount: owners.length,
      owners,
      ownerKeys: owners.map((item) => item.ownerKey),
      ownerLabels: owners.map((item) => item.ownerLabel).slice(0, 8)
    };
  }).filter((item) => Number.isFinite(item.tsMs));
  const continuityThresholdMs = loadingContinuityThresholdMs(events);
  const segments = buildLoadingSegments(events, continuityThresholdMs, (event) => event.loadingCount, (event) => event.owners)
    .sort((a, b) => Number(b.durationSeconds ?? 0) - Number(a.durationSeconds ?? 0) || Number(b.maxCount ?? 0) - Number(a.maxCount ?? 0));
  const ownerMap = new Map();
  for (const event of events) {
    for (const owner of event.owners) {
      const existing = ownerMap.get(owner.ownerKey) || {
        ownerKey: owner.ownerKey,
        ownerKind: owner.ownerKind,
        ownerLabel: owner.ownerLabel,
        ownerSessionId: owner.ownerSessionId ?? null,
        ownerMessageId: owner.ownerMessageId ?? null,
        ownerTraceId: owner.ownerTraceId ?? null,
        sampleCount: 0,
        occurrenceCount: 0,
        maxSimultaneousCount: 0,
        firstAt: event.ts,
        lastAt: event.ts,
        firstSeq: event.seq,
        lastSeq: event.seq,
        promptIndexes: new Set(),
        events: []
      };
      existing.sampleCount += 1;
      existing.occurrenceCount += owner.count;
      existing.maxSimultaneousCount = Math.max(existing.maxSimultaneousCount, owner.count);
      existing.lastAt = event.ts;
      existing.lastSeq = event.seq;
      for (const key of ["ownerSessionId", "ownerMessageId", "ownerTraceId"]) {
        if (!existing[key] && owner[key]) existing[key] = owner[key];
      }
      if (Number.isFinite(Number(event.promptIndex))) existing.promptIndexes.add(Number(event.promptIndex));
      existing.events.push({ ...event, loadingCount: owner.count, owners: [owner] });
      ownerMap.set(owner.ownerKey, existing);
    }
  }
  const owners = Array.from(ownerMap.values()).map((owner) => {
    const ownerSegments = buildLoadingSegments(owner.events, continuityThresholdMs, (event) => event.loadingCount, (event) => event.owners);
    const longest = ownerSegments.reduce((max, item) => Math.max(max, Number(item.durationSeconds ?? 0)), 0);
    return {
      ownerKey: owner.ownerKey,
      ownerKind: owner.ownerKind,
      ownerLabel: owner.ownerLabel,
      ownerSessionId: owner.ownerSessionId ?? null,
      ownerMessageId: owner.ownerMessageId ?? null,
      ownerTraceId: owner.ownerTraceId ?? null,
      sampleCount: owner.sampleCount,
      occurrenceCount: owner.occurrenceCount,
      maxSimultaneousCount: owner.maxSimultaneousCount,
      longestContinuousSeconds: longest,
      firstAt: owner.firstAt,
      lastAt: owner.lastAt,
      firstSeq: owner.firstSeq,
      lastSeq: owner.lastSeq,
      promptIndexes: Array.from(owner.promptIndexes).sort((a, b) => a - b),
      segments: ownerSegments.sort((a, b) => Number(b.durationSeconds ?? 0) - Number(a.durationSeconds ?? 0)).slice(0, 8),
      valuesRedacted: true
    };
  }).sort((a, b) => Number(b.longestContinuousSeconds ?? 0) - Number(a.longestContinuousSeconds ?? 0) || Number(b.occurrenceCount ?? 0) - Number(a.occurrenceCount ?? 0));
  const latest = events[events.length - 1] || null;
  const currentSegment = latest && latest.loadingCount > 0
    ? segments.find((segment) => segment.ongoing === true && segment.lastSeq === latest.seq) || null
    : null;
  const timelineRows = events
    .filter((event, index) => event.loadingCount > 0 || (index > 0 && events[index - 1]?.loadingCount > 0))
    .slice(0, 500)
    .map((event) => ({
      seq: event.seq,
      ts: event.ts,
      promptIndex: event.promptIndex,
      loadingCount: event.loadingCount,
      ownerCount: event.ownerCount,
      owners: event.owners.map((owner) => ({ ownerKind: owner.ownerKind, ownerLabel: owner.ownerLabel, ownerSessionId: owner.ownerSessionId ?? null, ownerMessageId: owner.ownerMessageId ?? null, ownerTraceId: owner.ownerTraceId ?? null, count: owner.count })).slice(0, 8)
    }));
  return {
    summary: {
      sampleCount: events.length,
      loadingSampleCount: events.filter((event) => event.loadingCount > 0).length,
      maxSimultaneousCount: events.reduce((max, event) => Math.max(max, event.loadingCount), 0),
      maxSimultaneousOwnerCount: events.reduce((max, event) => Math.max(max, event.ownerCount), 0),
      concurrentLoadingSampleCount: events.filter((event) => event.loadingCount > 1).length,
      ownerCount: owners.length,
      segmentCount: segments.length,
      overFiveSecondSegmentCount: segments.filter((segment) => Number(segment.durationSeconds ?? 0) > 5).length,
      overBudgetSegmentCount: segments.filter((segment) => Number(segment.durationSeconds ?? 0) > alertThresholds.visibleLoadingSlowMs / 1000).length,
      budgetSeconds: alertThresholds.visibleLoadingSlowMs / 1000,
      longestContinuousSeconds: segments.length > 0 ? Number(segments[0].durationSeconds ?? 0) : 0,
      currentContinuousSeconds: currentSegment ? Number(currentSegment.durationSeconds ?? 0) : 0,
      continuityThresholdMs,
      latestLoadingCount: latest?.loadingCount ?? 0,
      latestOwnerCount: latest?.ownerCount ?? 0,
      valuesRedacted: true
    },
    segments: segments.slice(0, 80),
    owners: owners.slice(0, 80),
    timeline: timelineRows,
    valuesRedacted: true
  };
}

function loadingContinuityThresholdMs(events) {
  const deltas = [];
  for (let index = 1; index < events.length; index += 1) {
    const delta = events[index].tsMs - events[index - 1].tsMs;
    if (Number.isFinite(delta) && delta > 0) deltas.push(delta);
  }
  if (deltas.length === 0) return 5000;
  const sorted = deltas.slice().sort((a, b) => a - b);
  const median = sorted[Math.floor(sorted.length / 2)];
  return Math.min(15000, Math.max(1500, Math.round(median * 2.5)));
}

function buildLoadingSegments(events, continuityThresholdMs, countForEvent, ownersForEvent) {
  const segments = [];
  let segment = null;
  let previousTsMs = null;
  for (const event of events) {
    const count = Number(countForEvent(event) ?? 0);
    const gapOk = previousTsMs === null || !Number.isFinite(event.tsMs) || event.tsMs - previousTsMs <= continuityThresholdMs;
    if (count > 0) {
      if (!segment || !gapOk) {
        if (segment) segments.push(finalizeLoadingSegment(segment, null));
        segment = {
          firstAt: event.ts,
          lastAt: event.ts,
          firstSeq: event.seq,
          lastSeq: event.seq,
          promptIndexes: new Set(),
          ownerKeys: new Set(),
          ownerLabels: new Map(),
          sampleCount: 0,
          maxCount: 0,
          ongoing: true
        };
      }
      segment.lastAt = event.ts;
      segment.lastSeq = event.seq;
      segment.sampleCount += 1;
      segment.maxCount = Math.max(segment.maxCount, count);
      if (Number.isFinite(Number(event.promptIndex))) segment.promptIndexes.add(Number(event.promptIndex));
      for (const owner of ownersForEvent(event) || []) {
        if (!owner?.ownerKey) continue;
        segment.ownerKeys.add(owner.ownerKey);
        if (!segment.ownerLabels.has(owner.ownerKey)) segment.ownerLabels.set(owner.ownerKey, { ownerKey: owner.ownerKey, ownerKind: owner.ownerKind, ownerLabel: owner.ownerLabel, count: 0 });
        const label = segment.ownerLabels.get(owner.ownerKey);
        label.count += owner.count ?? 1;
      }
    } else if (segment) {
      segment.ongoing = false;
      segment.endedAt = event.ts;
      segment.endSeq = event.seq;
      segments.push(finalizeLoadingSegment(segment, event));
      segment = null;
    }
    previousTsMs = event.tsMs;
  }
  if (segment) segments.push(finalizeLoadingSegment(segment, null));
  return segments;
}

function finalizeLoadingSegment(segment, absentEvent) {
  const startMs = Date.parse(segment.firstAt || "");
  const lastMs = Date.parse(segment.lastAt || "");
  const absentMs = Date.parse(absentEvent?.ts || "");
  const durationSeconds = Number.isFinite(startMs) && Number.isFinite(lastMs) && lastMs >= startMs ? Number(((lastMs - startMs) / 1000).toFixed(3)) : 0;
  const upperBoundSeconds = Number.isFinite(startMs) && Number.isFinite(absentMs) && absentMs >= startMs ? Number(((absentMs - startMs) / 1000).toFixed(3)) : durationSeconds;
  const endedGapSeconds = Number.isFinite(lastMs) && Number.isFinite(absentMs) && absentMs >= lastMs ? Number(((absentMs - lastMs) / 1000).toFixed(3)) : null;
  return {
    firstAt: segment.firstAt,
    lastAt: segment.lastAt,
    endedAt: absentEvent?.ts ?? null,
    firstSeq: segment.firstSeq,
    lastSeq: segment.lastSeq,
    endSeq: absentEvent?.seq ?? null,
    durationSeconds,
    upperBoundSeconds,
    endedGapSeconds,
    sampleCount: segment.sampleCount,
    maxCount: segment.maxCount,
    ownerCount: segment.ownerKeys.size,
    owners: Array.from(segment.ownerLabels.values()).sort((a, b) => b.count - a.count || String(a.ownerLabel).localeCompare(String(b.ownerLabel))).slice(0, 12),
    promptIndexes: Array.from(segment.promptIndexes).sort((a, b) => a - b),
    ongoing: absentEvent ? false : segment.ongoing === true,
    valuesRedacted: true
  };
}

function boundedTurnTimingRowsForReport(rows) {
  const sourceRows = Array.isArray(rows) ? rows : [];
  const maxRows = 1200;
  const headRows = 120;
  if (sourceRows.length <= maxRows) return { rows: sourceRows, disclosure: { truncated: false, totalRows: sourceRows.length, includedRows: sourceRows.length, omittedRows: 0, headRows: sourceRows.length, tailRows: 0 } };
  const tailRows = Math.max(0, maxRows - headRows);
  return {
    rows: [...sourceRows.slice(0, headRows), ...sourceRows.slice(-tailRows)],
    disclosure: {
      truncated: true,
      totalRows: sourceRows.length,
      includedRows: maxRows,
      omittedRows: Math.max(0, sourceRows.length - maxRows),
      headRows,
      tailRows,
      policy: "report-bounded-head-tail; full anomaly counters are computed before truncation",
      valuesRedacted: true
    }
  };
}
`;
}
