From b70b8d7334cf289bdc2b3ede8e1aa11f283bd2cb Mon Sep 17 00:00:00 2001 From: Lyon <88232613+pikasTech@users.noreply.github.com> Date: Thu, 25 Jun 2026 03:59:34 +0800 Subject: [PATCH] fix: stabilize web observe trace views (#849) Co-authored-by: Codex --- .../hwlab-node-web-observe-analyzer-source.ts | 2399 +--------------- ...node-web-observe-analyzer-timing-source.ts | 2400 +++++++++++++++++ scripts/src/hwlab-node-web-observe-collect.ts | 162 +- 3 files changed, 2540 insertions(+), 2421 deletions(-) create mode 100644 scripts/src/hwlab-node-web-observe-analyzer-timing-source.ts diff --git a/scripts/src/hwlab-node-web-observe-analyzer-source.ts b/scripts/src/hwlab-node-web-observe-analyzer-source.ts index 7361e331..749fb764 100644 --- a/scripts/src/hwlab-node-web-observe-analyzer-source.ts +++ b/scripts/src/hwlab-node-web-observe-analyzer-source.ts @@ -1,5 +1,7 @@ // SPEC: PJ2026-01040111 long-running Workbench observation. // Responsibility: Source string for the offline web-probe observe analyzer. +import { nodeWebObserveAnalyzerTimingSource } from "./hwlab-node-web-observe-analyzer-timing-source"; + export function nodeWebObserveAnalyzerSource(): string { return String.raw`#!/usr/bin/env node import { createHash } from "node:crypto"; @@ -2603,2400 +2605,5 @@ function boundedTurnTimingRowsForReport(rows) { }; } -function buildTurnTimingTable(samples, timeline) { - const columns = []; - const registry = new Map(); - const promptAssignmentByKey = new Map(); - const rows = []; - for (let index = 0; index < samples.length; index += 1) { - const sample = samples[index]; - const timelineItem = timeline[index] || {}; - const cells = {}; - const rawMetrics = turnMetricItems(sample, timelineItem); - const domIndexes = rawMetrics.map((item) => Number(item.domIndex)).filter(Number.isFinite); - const maxDomIndex = domIndexes.length > 0 ? Math.max(...domIndexes) : null; - for (const rawMetric of rawMetrics) { - const scopedKey = turnTimingScopedMetricKey(rawMetric, sample); - const samplePromptIndex = Number(timelineItem.promptIndex ?? 0); - const evidencePromptIndex = inferTurnMetricPromptIndex(rawMetric, samplePromptIndex, maxDomIndex); - if (evidencePromptIndex !== null) { - const existingPromptIndex = promptAssignmentByKey.get(scopedKey); - if (existingPromptIndex === undefined || evidencePromptIndex > existingPromptIndex) promptAssignmentByKey.set(scopedKey, evidencePromptIndex); - } - const assignedPromptIndex = promptAssignmentByKey.get(scopedKey) ?? null; - const metric = { ...rawMetric, key: scopedKey, baseKey: rawMetric.key, promptIndex: assignedPromptIndex, samplePromptIndex, pageEpoch: Number(sample?.pageEpoch ?? rawMetric.pageEpoch ?? 0) || 0 }; - let column = registry.get(metric.key); - if (!column) { - column = { - id: "T" + String(columns.length + 1), - label: "T" + String(columns.length + 1), - keyHash: sha256(metric.key), - source: metric.source, - firstSeq: sample.seq ?? null, - firstTs: sample.ts ?? null, - lastSeq: sample.seq ?? null, - lastTs: sample.ts ?? null, - promptIndex: metric.promptIndex ?? null, - lastPromptIndex: metric.promptIndex ?? null, - traceId: metric.traceId ?? null, - messageId: metric.messageId ?? null, - domIndex: metric.domIndex ?? null, - pageRole: metric.pageRole ?? sample.pageRole ?? null, - pageId: metric.pageId ?? sample.pageId ?? null, - pageEpoch: metric.pageEpoch ?? null - }; - registry.set(metric.key, column); - columns.push(column); - } else { - column.lastSeq = sample.seq ?? null; - column.lastTs = sample.ts ?? null; - if (column.source !== "turn" && metric.source === "turn") column.source = "turn"; - if (metric.promptIndex && column.promptIndex !== metric.promptIndex) column.promptIndex = metric.promptIndex; - column.lastPromptIndex = metric.promptIndex ?? column.lastPromptIndex ?? null; - if (!column.traceId && metric.traceId) column.traceId = metric.traceId; - if (!column.messageId && metric.messageId) column.messageId = metric.messageId; - } - cells[column.id] = { - totalElapsedSeconds: metric.totalElapsedSeconds, - recentUpdateSeconds: metric.recentUpdateSeconds, - status: metric.status ?? null, - promptIndex: metric.promptIndex ?? null, - source: metric.source, - pageRole: metric.pageRole ?? sample.pageRole ?? null, - pageId: metric.pageId ?? sample.pageId ?? null, - pageEpoch: metric.pageEpoch ?? null, - sampleGroupSeq: sample.sampleGroupSeq ?? null, - traceId: metric.traceId ?? null, - messageId: metric.messageId ?? null, - textHash: metric.textHash ?? null, - samplePromptIndex: metric.samplePromptIndex ?? null - }; - } - rows.push({ - ts: sample.ts ?? null, - seq: sample.seq ?? null, - sampleGroupSeq: sample.sampleGroupSeq ?? null, - pageRole: sample.pageRole ?? null, - pageId: sample.pageId ?? null, - pageEpoch: Number(sample?.pageEpoch ?? 0) || 0, - promptIndex: timelineItem.promptIndex ?? 0, - routeSessionId: sample.routeSessionId ?? null, - activeSessionId: sample.activeSessionId ?? null, - cells - }); - } - const timingEvents = detectTurnTimingNonMonotonic(columns, rows); - return { - columns, - rows, - nonMonotonic: timingEvents.anomalies, - elapsedZeroResets: timingEvents.elapsedZeroResets, - totalElapsedForwardJumps: timingEvents.totalElapsedForwardJumps, - terminalElapsedGrowth: timingEvents.terminalElapsedGrowth, - recentUpdateResets: timingEvents.recentUpdateResets, - recentUpdateSteps: timingEvents.recentUpdateSteps - }; -} - -function turnTimingScopedMetricKey(metric, sample) { - return [ - metric?.key ?? "unknown", - metric?.pageRole ?? sample?.pageRole ?? "unknown-role", - metric?.pageId ?? sample?.pageId ?? "unknown-page", - Number(sample?.pageEpoch ?? metric?.pageEpoch ?? 0) || 0 - ].join("|page:"); -} - -function inferTurnMetricPromptIndex(metric, samplePromptIndex, maxDomIndex) { - const promptIndex = Number(samplePromptIndex); - if (!Number.isFinite(promptIndex) || promptIndex <= 0) return null; - if (metric?.source === "aggregate") return promptIndex; - if (isActiveTurnStatus(metric?.status)) return promptIndex; - const domIndex = Number(metric?.domIndex); - if (!isTerminalTurnStatus(metric?.status) && Number.isFinite(domIndex) && Number.isFinite(maxDomIndex) && domIndex === maxDomIndex && metric?.recentUpdateSeconds !== null && metric?.recentUpdateSeconds !== undefined) return promptIndex; - return null; -} - -function detectTurnTimingNonMonotonic(columns, rows) { - const anomalies = []; - const elapsedZeroResets = []; - const totalElapsedForwardJumps = []; - const terminalElapsedGrowth = []; - const recentUpdateResets = []; - const recentUpdateSteps = []; - for (const column of columns) { - const previousByMetric = new Map(); - let previousTerminalTotal = null; - for (const row of rows) { - const cell = row.cells?.[column.id]; - if (!cell) continue; - for (const metric of ["totalElapsedSeconds", "recentUpdateSeconds"]) { - const value = cell[metric]; - if (value === null || value === undefined || !Number.isFinite(Number(value))) continue; - const current = Number(value); - const previous = previousByMetric.get(metric); - if (previous && metric === "totalElapsedSeconds" && current < previous.value) { - const anomaly = previous.value > 0 && current === 0 ? "zero-reset" : "decrease"; - const event = { - columnId: column.id, - columnLabel: column.label, - metric, - anomaly, - expectedPattern: anomaly === "zero-reset" ? "total-elapsed-should-not-return-to-zero" : "total-elapsed-monotonic", - fromSeq: previous.seq, - fromTs: previous.ts, - fromValue: previous.value, - toSeq: row.seq ?? null, - toTs: row.ts ?? null, - toValue: current, - delta: current - previous.value, - traceId: cell.traceId ?? column.traceId ?? null, - messageId: cell.messageId ?? column.messageId ?? null, - promptIndex: cell.promptIndex ?? null, - samplePromptIndex: row.promptIndex ?? null, - source: cell.source ?? column.source ?? null, - pageRole: cell.pageRole ?? column.pageRole ?? null, - pageId: cell.pageId ?? column.pageId ?? null, - pageEpoch: cell.pageEpoch ?? row.pageEpoch ?? column.pageEpoch ?? null, - valuesRedacted: true - }; - anomalies.push(event); - if (anomaly === "zero-reset") elapsedZeroResets.push(event); - } - if (previous && metric === "totalElapsedSeconds" && current > previous.value) { - const sampleDeltaSeconds = elapsedSecondsBetween(previous.ts, row.ts); - const delta = current - previous.value; - const allowedIncreaseSeconds = sampleDeltaSeconds + alertThresholds.turnTimingSampleSlackSeconds; - const terminalTransition = !isTerminalTurnStatus(previous.status) && isTerminalTurnStatus(cell.status); - if (delta > allowedIncreaseSeconds && !terminalTransition) { - totalElapsedForwardJumps.push({ - columnId: column.id, - columnLabel: column.label, - metric, - anomaly: "forward-jump", - expectedPattern: "total-elapsed-increase-should-match-browser-sample-interval", - fromSeq: previous.seq, - fromTs: previous.ts, - fromValue: previous.value, - fromStatus: previous.status ?? null, - toSeq: row.seq ?? null, - toTs: row.ts ?? null, - toValue: current, - toStatus: cell.status ?? null, - delta, - sampleDeltaSeconds, - allowedIncreaseSeconds, - excessiveIncreaseSeconds: Number((delta - allowedIncreaseSeconds).toFixed(3)), - traceId: cell.traceId ?? column.traceId ?? null, - messageId: cell.messageId ?? column.messageId ?? null, - promptIndex: cell.promptIndex ?? null, - samplePromptIndex: row.promptIndex ?? null, - source: cell.source ?? column.source ?? null, - pageRole: cell.pageRole ?? column.pageRole ?? null, - pageId: cell.pageId ?? column.pageId ?? null, - pageEpoch: cell.pageEpoch ?? row.pageEpoch ?? column.pageEpoch ?? null, - valuesRedacted: true - }); - } - } - if (metric === "totalElapsedSeconds" && isTerminalTurnStatus(cell.status)) { - if (previousTerminalTotal && current > previousTerminalTotal.value) { - terminalElapsedGrowth.push({ - columnId: column.id, - columnLabel: column.label, - metric, - anomaly: "terminal-growth", - expectedPattern: "terminal-total-elapsed-sealed", - fromSeq: previousTerminalTotal.seq, - fromTs: previousTerminalTotal.ts, - fromValue: previousTerminalTotal.value, - fromStatus: previousTerminalTotal.status, - toSeq: row.seq ?? null, - toTs: row.ts ?? null, - toValue: current, - toStatus: cell.status ?? null, - delta: current - previousTerminalTotal.value, - traceId: cell.traceId ?? column.traceId ?? null, - messageId: cell.messageId ?? column.messageId ?? null, - promptIndex: cell.promptIndex ?? null, - samplePromptIndex: row.promptIndex ?? null, - source: cell.source ?? column.source ?? null, - pageRole: cell.pageRole ?? column.pageRole ?? null, - pageId: cell.pageId ?? column.pageId ?? null, - pageEpoch: cell.pageEpoch ?? row.pageEpoch ?? column.pageEpoch ?? null, - valuesRedacted: true - }); - } - previousTerminalTotal = { value: current, seq: row.seq ?? null, ts: row.ts ?? null, status: cell.status ?? null }; - } - if (previous && metric === "recentUpdateSeconds") { - const elapsedMs = Date.parse(String(row.ts ?? "")) - Date.parse(String(previous.ts ?? "")); - const elapsedSeconds = Number.isFinite(elapsedMs) && elapsedMs >= 0 ? elapsedMs / 1000 : null; - const increase = current - previous.value; - const allowedIncrease = elapsedSeconds === null - ? alertThresholds.turnTimingSampleSlackSeconds - : Math.max(alertThresholds.turnTimingSampleSlackSeconds, elapsedSeconds + alertThresholds.turnTimingSampleSlackSeconds); - const excessiveIncrease = increase > allowedIncrease ? increase - allowedIncrease : 0; - recentUpdateSteps.push({ - columnId: column.id, - columnLabel: column.label, - metric: "recentUpdateSeconds", - event: increase < 0 ? "reset" : excessiveIncrease > 0 ? "jump" : "increase", - expectedPattern: "sawtooth-increase-or-reset", - fromSeq: previous.seq, - fromTs: previous.ts, - fromValue: previous.value, - toSeq: row.seq ?? null, - toTs: row.ts ?? null, - toValue: current, - delta: increase, - sampleDeltaSeconds: elapsedSeconds, - allowedIncreaseSeconds: allowedIncrease, - excessiveIncreaseSeconds: excessiveIncrease, - traceId: cell.traceId ?? column.traceId ?? null, - messageId: cell.messageId ?? column.messageId ?? null, - promptIndex: cell.promptIndex ?? null, - samplePromptIndex: row.promptIndex ?? null, - source: cell.source ?? column.source ?? null, - pageRole: cell.pageRole ?? column.pageRole ?? null, - pageId: cell.pageId ?? column.pageId ?? null, - pageEpoch: cell.pageEpoch ?? row.pageEpoch ?? column.pageEpoch ?? null, - valuesRedacted: true - }); - if (increase < 0) { - recentUpdateResets.push({ - columnId: column.id, - columnLabel: column.label, - metric: "recentUpdateSeconds", - event: "reset", - fromSeq: previous.seq, - fromTs: previous.ts, - fromValue: previous.value, - toSeq: row.seq ?? null, - toTs: row.ts ?? null, - toValue: current, - delta: increase, - sampleDeltaSeconds: elapsedSeconds, - traceId: cell.traceId ?? column.traceId ?? null, - messageId: cell.messageId ?? column.messageId ?? null, - promptIndex: cell.promptIndex ?? null, - samplePromptIndex: row.promptIndex ?? null, - source: cell.source ?? column.source ?? null, - pageRole: cell.pageRole ?? column.pageRole ?? null, - pageId: cell.pageId ?? column.pageId ?? null, - pageEpoch: cell.pageEpoch ?? row.pageEpoch ?? column.pageEpoch ?? null, - valuesRedacted: true - }); - } - if (excessiveIncrease > 0) { - anomalies.push({ - columnId: column.id, - columnLabel: column.label, - metric: "recentUpdateSeconds", - anomaly: "jump", - expectedPattern: "sawtooth-increase-or-reset", - fromSeq: previous.seq, - fromTs: previous.ts, - fromValue: previous.value, - toSeq: row.seq ?? null, - toTs: row.ts ?? null, - toValue: current, - delta: increase, - sampleDeltaSeconds: elapsedSeconds, - allowedIncreaseSeconds: allowedIncrease, - excessiveIncreaseSeconds: excessiveIncrease, - traceId: cell.traceId ?? column.traceId ?? null, - messageId: cell.messageId ?? column.messageId ?? null, - promptIndex: cell.promptIndex ?? null, - samplePromptIndex: row.promptIndex ?? null, - source: cell.source ?? column.source ?? null, - pageRole: cell.pageRole ?? column.pageRole ?? null, - pageId: cell.pageId ?? column.pageId ?? null, - pageEpoch: cell.pageEpoch ?? row.pageEpoch ?? column.pageEpoch ?? null, - valuesRedacted: true - }); - } - } - previousByMetric.set(metric, { value: current, seq: row.seq ?? null, ts: row.ts ?? null, status: cell.status ?? null }); - } - } - } - return { anomalies, elapsedZeroResets, totalElapsedForwardJumps, terminalElapsedGrowth, recentUpdateResets, recentUpdateSteps }; -} - -function elapsedSecondsBetween(fromTs, toTs) { - const from = Date.parse(fromTs); - const to = Date.parse(toTs); - if (!Number.isFinite(from) || !Number.isFinite(to) || to < from) return 0; - return Number(((to - from) / 1000).toFixed(3)); -} - -function maxPositiveDelta(items) { - const values = (Array.isArray(items) ? items : []) - .map((item) => Number(item.delta)) - .filter((value) => Number.isFinite(value) && value > 0); - return values.length > 0 ? Math.max(...values) : 0; -} - -function isTerminalTurnStatus(value) { - const status = String(value ?? "").trim().toLowerCase().replace(/_/gu, "-"); - return ["completed", "succeeded", "success", "failed", "error", "blocked", "timeout", "canceled", "cancelled", "stale", "done", "terminal", "thread-resume-failed"].includes(status); -} - - - -function buildTraceOrderMetrics(samples, timeline) { - const rows = []; - const orderAnomalies = []; - const completionNotLast = []; - const groups = new Map(); - for (let sampleIndex = 0; sampleIndex < samples.length; sampleIndex += 1) { - const sample = samples[sampleIndex] || {}; - const sampleRows = traceTimingRowsForSample(sample, timeline[sampleIndex] || {}); - for (const row of sampleRows) { - const normalized = { - ...row, - sampleIndex, - sampleSeq: sample.seq ?? null, - timestamp: sample.ts || sample.timestamp || sample.collectedAt || sample.time || null, - pageRole: row.pageRole || sample.pageRole || sample.role || sample.contextRole || null, - pageId: row.pageId || sample.pageId || sample.contextId || null, - sessionId: row.sessionId || sample.sessionId || sample.workbenchSessionId || null, - }; - rows.push(normalized); - const key = traceRowGroupKey(normalized); - if (!groups.has(key)) groups.set(key, []); - groups.get(key).push(normalized); - } - } - const slackSeconds = Math.max(1, Number(alertThresholds?.turnTimingSampleSlackSeconds || 0)); - for (const groupRows of groups.values()) { - const sorted = groupRows.slice().sort((a, b) => { - if (a.sampleIndex !== b.sampleIndex) return a.sampleIndex - b.sampleIndex; - return (a.rowIndex ?? 0) - (b.rowIndex ?? 0); - }); - let previous = null; - for (const row of sorted) { - if (previous) { - const reasons = []; - if (Number.isFinite(previous.totalSeconds) && Number.isFinite(row.totalSeconds) && row.totalSeconds + slackSeconds < previous.totalSeconds) { - reasons.push('total-decreased'); - } - if (Number.isFinite(previous.projectedSeq) && Number.isFinite(row.projectedSeq) && row.projectedSeq < previous.projectedSeq) { - reasons.push('projected-seq-decreased'); - } - if (Number.isFinite(previous.clockSeconds) && Number.isFinite(row.clockSeconds)) { - const diff = previous.clockSeconds - row.clockSeconds; - if (diff > slackSeconds && diff < 43200) reasons.push('clock-decreased'); - } - if (Number.isFinite(previous.timestampMs) && Number.isFinite(row.timestampMs) && row.timestampMs + slackSeconds * 1000 < previous.timestampMs) { - reasons.push('timestamp-decreased'); - } - if (reasons.length) { - orderAnomalies.push({ - sampleIndex: row.sampleIndex, - sampleSeq: row.sampleSeq, - timestamp: row.timestamp, - pageRole: row.pageRole, - pageId: row.pageId, - sessionId: row.sessionId, - traceId: row.traceId || previous.traceId || null, - previousRowIndex: previous.rowIndex, - currentRowIndex: row.rowIndex, - reasons, - previousTotalSeconds: previous.totalSeconds, - currentTotalSeconds: row.totalSeconds, - previousProjectedSeq: previous.projectedSeq, - currentProjectedSeq: row.projectedSeq, - previousSourceSeq: previous.sourceSeq, - currentSourceSeq: row.sourceSeq, - previousEventSeq: previous.eventSeq, - currentEventSeq: row.eventSeq, - previousClockSeconds: previous.clockSeconds, - currentClockSeconds: row.clockSeconds, - previousTimestampMs: previous.timestampMs, - currentTimestampMs: row.timestampMs, - previousPreview: previous.preview, - currentPreview: row.preview, - }); - } - } - previous = row; - } - for (let index = 0; index < sorted.length; index += 1) { - const row = sorted[index]; - if (!row.isCompletion) continue; - const later = sorted.slice(index + 1).find((candidate) => { - if (candidate.isCompletion && candidate.preview === row.preview) return false; - return Number.isFinite(candidate.totalSeconds) || Number.isFinite(candidate.clockSeconds) || Number.isFinite(candidate.projectedSeq); - }); - if (later) { - completionNotLast.push({ - sampleIndex: row.sampleIndex, - sampleSeq: row.sampleSeq, - timestamp: row.timestamp, - pageRole: row.pageRole, - pageId: row.pageId, - sessionId: row.sessionId, - traceId: row.traceId || later.traceId || null, - completionRowIndex: row.rowIndex, - laterRowIndex: later.rowIndex, - completionTotalSeconds: row.totalSeconds, - laterTotalSeconds: later.totalSeconds, - completionProjectedSeq: row.projectedSeq, - laterProjectedSeq: later.projectedSeq, - completionSourceSeq: row.sourceSeq, - laterSourceSeq: later.sourceSeq, - completionEventSeq: row.eventSeq, - laterEventSeq: later.eventSeq, - completionPreview: row.preview, - laterPreview: later.preview, - }); - } - } - } - return { - summary: { - sampleCount: samples.length, - traceRowCount: rows.length, - orderAnomalyCount: orderAnomalies.length, - completionNotLastCount: completionNotLast.length, - }, - rows: rows.slice(0, 200), - orderAnomalies: orderAnomalies.slice(0, 100), - completionNotLast: completionNotLast.slice(0, 100), - }; -} - -function buildCodeAgentCardDurationUnderreportedMetrics(samples, timeline) { - const findings = []; - const slackSeconds = Math.max(5, Number(alertThresholds?.turnTimingSampleSlackSeconds || 0)); - for (let sampleIndex = 0; sampleIndex < samples.length; sampleIndex += 1) { - const sample = samples[sampleIndex] || {}; - const cards = codeAgentCardsForSample(sample); - if (!cards.length) continue; - const traceRows = traceTimingRowsForSample(sample, timeline[sampleIndex] || {}); - const terminalCards = cards.filter((card) => isCodeAgentCardTerminal(card)); - const sampleText = sampleVisibleText(sample, timeline[sampleIndex] || {}); - for (const card of terminalCards) { - const cardText = codeAgentCardText(card); - const parsedCardSeconds = parseTotalElapsedSeconds(cardText).filter(Number.isFinite); - const cardSeconds = Number.isFinite(Number(card.totalElapsedSeconds)) ? Number(card.totalElapsedSeconds) : parsedCardSeconds.length > 0 ? Math.max(...parsedCardSeconds) : NaN; - if (!Number.isFinite(cardSeconds)) continue; - const traceMatched = traceRows.filter((row) => traceRowMatchesCard(row, card, terminalCards.length)); - const traceEvidence = maxTraceDurationEvidence(traceMatched); - const textEvidence = maxSelfReportedDurationEvidence([card.text, card.preview, card.finalResponseText, card.runningRecordText, terminalCards.length === 1 ? sampleText : ''].filter(Boolean).join('\n')); - const evidences = [traceEvidence, textEvidence].filter((item) => item && item.exact === true && Number.isFinite(item.seconds)); - if (!evidences.length) continue; - const strongest = evidences.sort((a, b) => b.seconds - a.seconds)[0]; - const tolerance = Math.max(slackSeconds, Math.ceil(strongest.seconds * 0.05)); - if (strongest.seconds > cardSeconds + tolerance) { - findings.push({ - sampleIndex, - timestamp: sample.timestamp || sample.collectedAt || sample.time || null, - pageRole: card.pageRole || sample.pageRole || sample.role || sample.contextRole || null, - pageId: card.pageId || sample.pageId || sample.contextId || null, - sessionId: card.sessionId || sample.sessionId || sample.workbenchSessionId || null, - traceId: card.traceId || strongest.traceId || null, - status: card.status || card.state || card.phase || null, - cardTotalElapsedSeconds: cardSeconds, - expectedElapsedSeconds: strongest.seconds, - deltaSeconds: strongest.seconds - cardSeconds, - toleranceSeconds: tolerance, - evidenceKind: strongest.kind, - evidencePreview: strongest.preview, - cardPreview: card.preview || compactOneLine(cardText || ''), - }); - } - } - } - return findings.slice(0, 100); -} - -function buildCodeAgentCardDurationMismatchMetrics(samples, timeline) { - const findings = []; - const slackSeconds = Math.max(5, Number(alertThresholds?.turnTimingSampleSlackSeconds || 0)); - for (let sampleIndex = 0; sampleIndex < samples.length; sampleIndex += 1) { - const sample = samples[sampleIndex] || {}; - const cards = codeAgentCardsForSample(sample); - if (!cards.length) continue; - const traceRows = traceTimingRowsForSample(sample, timeline[sampleIndex] || {}); - const terminalCards = cards.filter((card) => isCodeAgentCardTerminal(card)); - const sampleText = sampleVisibleText(sample, timeline[sampleIndex] || {}); - for (const card of terminalCards) { - const cardText = codeAgentCardText(card); - const parsedCardSeconds = parseTotalElapsedSeconds(cardText).filter(Number.isFinite); - const cardSeconds = Number.isFinite(Number(card.totalElapsedSeconds)) ? Number(card.totalElapsedSeconds) : parsedCardSeconds.length > 0 ? Math.max(...parsedCardSeconds) : NaN; - if (!Number.isFinite(cardSeconds)) continue; - const traceMatched = traceRows.filter((row) => traceRowMatchesCard(row, card, terminalCards.length)); - const traceEvidence = maxTraceDurationEvidence(traceMatched); - const textEvidence = maxSelfReportedDurationEvidence([card.text, card.preview, card.finalResponseText, card.runningRecordText, terminalCards.length === 1 ? sampleText : ''].filter(Boolean).join('\n')); - const evidences = [traceEvidence, textEvidence].filter((item) => item && item.exact === true && Number.isFinite(item.seconds)); - if (!evidences.length) continue; - const strongest = evidences.sort((a, b) => b.seconds - a.seconds)[0]; - const exactEvidence = strongest.exact === true || strongest.kind === 'trace-completion-total' || strongest.kind === 'final-response-duration'; - const tolerance = Math.max(slackSeconds, Math.ceil(strongest.seconds * 0.05)); - const signedDelta = Number((cardSeconds - strongest.seconds).toFixed(3)); - const absoluteDelta = Math.abs(signedDelta); - const underreported = strongest.seconds > cardSeconds + tolerance; - const overreported = exactEvidence && cardSeconds > strongest.seconds + tolerance; - if (!underreported && !overreported) continue; - findings.push({ - sampleIndex, - timestamp: sample.timestamp || sample.collectedAt || sample.time || sample.ts || null, - pageRole: card.pageRole || sample.pageRole || sample.role || sample.contextRole || null, - pageId: card.pageId || sample.pageId || sample.contextId || null, - sessionId: card.sessionId || sample.sessionId || sample.workbenchSessionId || null, - traceId: card.traceId || strongest.traceId || null, - status: card.status || card.state || card.phase || null, - direction: underreported ? 'underreported' : 'overreported', - cardTotalElapsedSeconds: cardSeconds, - expectedElapsedSeconds: strongest.seconds, - signedDeltaSeconds: signedDelta, - deltaSeconds: Number(absoluteDelta.toFixed(3)), - toleranceSeconds: tolerance, - evidenceKind: strongest.kind, - exactEvidence, - evidencePreview: strongest.preview, - cardPreview: card.preview || compactOneLine(cardText || ''), - }); - } - } - return findings.slice(0, 100); -} - -function traceTimingRowsForSample(sample, timelineItem) { - const rows = []; - const seen = new Set(); - const appendRowsFromText = (text, source, baseIndex, meta = {}) => { - for (const extracted of extractTraceRowsFromText(text, source, baseIndex, meta)) { - const key = [extracted.pageRole || '', extracted.pageId || '', extracted.rowIndex, extracted.preview].join('|'); - if (seen.has(key)) continue; - seen.add(key); - rows.push(extracted); - } - }; - for (const candidate of traceRowCandidateArrays(sample, timelineItem)) { - const array = Array.isArray(candidate.rows) ? candidate.rows : []; - array.forEach((item, index) => { - if (typeof item === 'string') { - appendRowsFromText(item, candidate.source, index, candidate.meta || {}); - return; - } - if (!item || typeof item !== 'object') return; - const text = objectText(item); - if (!text) return; - appendRowsFromText(text, candidate.source, Number.isFinite(Number(item.index)) ? Number(item.index) : index, { - ...(candidate.meta || {}), - pageRole: item.pageRole || item.role || candidate.meta?.pageRole || null, - pageId: item.pageId || item.contextId || candidate.meta?.pageId || null, - sessionId: item.sessionId || item.workbenchSessionId || candidate.meta?.sessionId || null, - traceId: item.traceId || item.trace_id || candidate.meta?.traceId || null, - projectedSeq: item.projectedSeq ?? item.projected_seq ?? item.projectedSequence ?? null, - sourceSeq: item.sourceSeq ?? item.source_seq ?? item.sourceSequence ?? null, - eventSeq: item.eventSeq ?? item.event_seq ?? item.sequence ?? null, - eventTimestamp: item.eventTimestamp ?? item.event_ts ?? item.timestamp ?? item.ts ?? null, - eventTimeText: item.eventTimeText ?? item.timeText ?? null, - eventKind: item.eventKind ?? item.kind ?? item.status ?? null, - }); - }); - } - if (!rows.length) { - appendRowsFromText(sampleVisibleText(sample, timelineItem), 'visible-text', 0, { - pageRole: sample.pageRole || sample.role || sample.contextRole || null, - pageId: sample.pageId || sample.contextId || null, - sessionId: sample.sessionId || sample.workbenchSessionId || null, - }); - } - rows.sort((a, b) => (a.rowIndex ?? 0) - (b.rowIndex ?? 0)); - return rows; -} - -function extractTraceRowsFromText(text, source, baseIndex, meta = {}) { - const result = []; - const normalized = String(text || '').replace(/\r/g, '\n'); - if (!normalized.trim()) return result; - const lines = normalized.split('\n').map((line) => line.trim()).filter(Boolean); - for (let index = 0; index < lines.length; index += 1) { - const line = lines[index]; - if (!traceLineLooksRelevant(line)) continue; - const nextLine = index + 1 < lines.length && !/^\d{1,2}:\d{2}:\d{2}\b/.test(lines[index + 1]) ? lines[index + 1] : ''; - const preview = compactOneLine(nextLine ? line + ' ' + nextLine : line); - result.push(normalizeTraceTimingRow(preview, source, Number(baseIndex || 0) * 1000 + index, meta)); - } - return result; -} - -function normalizeTraceTimingRow(text, source, rowIndex, meta = {}) { - const preview = compactOneLine(text).slice(0, 240); - const projectedSeq = firstFiniteNumber(meta.projectedSeq, parseTraceRowProjectedSeq(preview)); - const sourceSeq = firstFiniteNumber(meta.sourceSeq); - const eventSeq = firstFiniteNumber(meta.eventSeq); - const timestampMs = parseTraceRowTimestampMs(meta.eventTimestamp || meta.eventTimeText || preview); - return { - source, - rowIndex, - preview, - pageRole: meta.pageRole || null, - pageId: meta.pageId || null, - sessionId: meta.sessionId || null, - traceId: meta.traceId || parseTraceRowTraceId(preview), - clockSeconds: parseTraceRowClockSeconds(preview) ?? parseTraceRowClockSeconds(meta.eventTimeText || ""), - timestampMs, - totalSeconds: parseTraceRowTotalSeconds(preview), - projectedSeq, - sourceSeq, - eventSeq, - eventTimestamp: meta.eventTimestamp || null, - eventTimeText: meta.eventTimeText || null, - eventKind: meta.eventKind || null, - isCompletion: traceRowIsTerminalCompletionText(preview, meta.eventKind || ""), - }; -} - -function traceRowIsTerminalCompletionText(preview, eventKind = "") { - const value = [preview, eventKind].map((item) => String(item || "")).join(" "); - if (/\bnon[-_ ]?terminal\b/i.test(value)) return false; - if (/轮次完成|turn\s+completed|completed\s+turn|backend[_: -]?turn[_: -]?finished/i.test(value)) return true; - return /\bterminal(?:[_: -]?status|Status)?\s*[=: -]\s*(?:completed|failed|cancelled|canceled|timeout)\b/i.test(value); -} - -function traceRowIsRoundCompletionText(preview, eventKind = "") { - const value = [preview, eventKind].map((item) => String(item || "")).join(" "); - if (/\bnon[-_ ]?terminal\b/i.test(value)) return false; - return /轮次完成|turn\s+completed|completed\s+turn|backend[_: -]?turn[_: -]?finished/i.test(value); -} - -function traceRowCandidateArrays(sample, timelineItem) { - const candidates = []; - const pushArray = (rows, source, meta = {}) => { - if (Array.isArray(rows) && rows.length) candidates.push({ rows, source, meta }); - }; - const directSources = [ - [sample?.traceRows, 'sample.traceRows'], - [sample?.eventRows, 'sample.eventRows'], - [sample?.activityRows, 'sample.activityRows'], - [sample?.timelineRows, 'sample.timelineRows'], - [sample?.dom?.traceRows, 'sample.dom.traceRows'], - [sample?.dom?.eventRows, 'sample.dom.eventRows'], - [sample?.dom?.activityRows, 'sample.dom.activityRows'], - [sample?.dom?.timelineRows, 'sample.dom.timelineRows'], - [timelineItem?.traceRows, 'timeline.traceRows'], - [timelineItem?.eventRows, 'timeline.eventRows'], - [timelineItem?.activityRows, 'timeline.activityRows'], - [timelineItem?.rows, 'timeline.rows'], - [timelineItem?.events, 'timeline.events'], - ]; - for (const [rows, source] of directSources) pushArray(rows, source, {}); - if (!candidates.length) collectNamedTraceArrays(sample, candidates, 'sample', 0); - if (!candidates.length) collectNamedTraceArrays(timelineItem, candidates, 'timeline', 0); - return candidates; -} - -function collectNamedTraceArrays(value, candidates, path, depth) { - if (!value || depth > 5) return; - if (Array.isArray(value)) { - const pathLooksTrace = /trace|timeline|activity|event|log|record/i.test(path); - const valueLooksTrace = value.slice(0, 5).some((item) => traceLineLooksRelevant(typeof item === 'string' ? item : objectText(item))); - if (pathLooksTrace || valueLooksTrace) candidates.push({ rows: value, source: path, meta: {} }); - return; - } - if (typeof value !== 'object') return; - for (const [key, child] of Object.entries(value)) { - if (!child) continue; - if (Array.isArray(child)) { - const childPath = path + '.' + key; - const pathLooksTrace = /trace|timeline|activity|event|log|record/i.test(key); - const valueLooksTrace = child.slice(0, 5).some((item) => traceLineLooksRelevant(typeof item === 'string' ? item : objectText(item))); - if (pathLooksTrace || valueLooksTrace) candidates.push({ rows: child, source: childPath, meta: {} }); - continue; - } - if (typeof child === 'object' && /dom|trace|timeline|activity|event|log|record|page|card|message|panel|diagnostic/i.test(key)) { - collectNamedTraceArrays(child, candidates, path + '.' + key, depth + 1); - } - } -} - -function traceLineLooksRelevant(text) { - const value = String(text || '').trim(); - if (!value) return false; - if (/^\d{1,2}:\d{2}:\d{2}\b/.test(value)) return true; - if (/\btotal=\d/.test(value)) return true; - if (/轮次完成(总耗时/.test(value)) return true; - if (/\bseq(?:uence)?[=:]\s*\d+/i.test(value)) return true; - return false; -} - -function parseTraceRowClockSeconds(text) { - const match = String(text || '').match(/^\s*(\d{1,2}):(\d{2}):(\d{2})\b/); - if (!match) return null; - return Number(match[1]) * 3600 + Number(match[2]) * 60 + Number(match[3]); -} - -function parseTraceRowTotalSeconds(text) { - const value = String(text || ''); - const totalMatch = value.match(/\btotal=([0-9:.]+)/i); - if (totalMatch) return parseTraceDurationSeconds(totalMatch[1]); - const completionMatch = value.match(/总耗时\s*([0-9:.]+)/); - if (completionMatch) return parseTraceDurationSeconds(completionMatch[1]); - return null; -} - -function parseTraceDurationSeconds(value) { - const text = String(value || '').trim(); - if (!text) return null; - const parts = text.split(':').map((part) => Number(part)); - if (parts.some((part) => !Number.isFinite(part))) return null; - if (parts.length === 3) return parts[0] * 3600 + parts[1] * 60 + parts[2]; - if (parts.length === 2) return parts[0] * 60 + parts[1]; - if (parts.length === 1) return parts[0]; - return null; -} - -function parseTraceRowProjectedSeq(text) { - const value = String(text || ''); - const match = value.match(/\b(?:projected[_-]?seq|seq(?:uence)?|event[_-]?seq)\s*[=:]\s*(\d+)/i); - return match ? Number(match[1]) : null; -} - -function parseTraceRowTimestampMs(value) { - const text = String(value || '').trim(); - if (!text) return null; - const parsed = Date.parse(text); - return Number.isFinite(parsed) ? parsed : null; -} - -function firstFiniteNumber(...values) { - for (const value of values) { - const numeric = Number(value); - if (Number.isFinite(numeric)) return numeric; - } - return null; -} - -function parseTraceRowTraceId(text) { - const match = String(text || '').match(/\b(?:trace_id=|traceId[:=]?\s*)(trc_[a-z0-9_-]+|[a-f0-9]{16,})\b/i); - return match ? match[1] : null; -} - -function traceRowGroupKey(row) { - const identity = row.traceId ? 'trace:' + row.traceId : 'sample:' + (row.sampleIndex ?? '-') + ':' + (row.source || 'unknown'); - return [row.pageRole || '', row.pageId || '', row.sessionId || '', row.source || '', row.sampleIndex ?? '', identity].join('|'); -} - -function traceRowMatchesCard(row, card, terminalCardCount) { - if (!row) return false; - if (card.traceId) return row.traceId === card.traceId; - if (row.traceId) return false; - if (row.sessionId && card.sessionId) return terminalCardCount === 1 && row.sessionId === card.sessionId; - if (terminalCardCount === 1) return true; - return false; -} - -function maxTraceDurationEvidence(rows) { - const finiteTotals = rows.map((row) => Number(row.totalSeconds)).filter(Number.isFinite); - const finiteClocks = rows.map((row) => Number(row.clockSeconds)).filter(Number.isFinite); - const evidences = []; - if (finiteTotals.length) { - const maxTotal = Math.max(...finiteTotals); - const source = rows.find((row) => Number(row.totalSeconds) === maxTotal); - const exact = source?.isCompletion === true; - evidences.push({ kind: exact ? 'trace-completion-total' : 'trace-total', seconds: maxTotal, traceId: source?.traceId || null, preview: source?.preview || '', exact }); - } - if (finiteClocks.length >= 2) { - const minClock = Math.min(...finiteClocks); - const maxClock = Math.max(...finiteClocks); - const span = maxClock - minClock; - if (span >= 0 && span < 43200) evidences.push({ kind: 'trace-clock-span', seconds: span, traceId: null, preview: 'visible trace row clock span', exact: false }); - } - if (!evidences.length) return null; - evidences.sort((a, b) => b.seconds - a.seconds); - return evidences[0]; -} - -function maxSelfReportedDurationEvidence(text) { - const value = String(text || ''); - const lines = value.split(/\n+/).map((line) => line.trim()).filter(Boolean); - let best = null; - for (let index = 0; index < lines.length; index += 1) { - const line = lines[index]; - const previous = index > 0 ? lines[index - 1] : ''; - const next = index + 1 < lines.length ? lines[index + 1] : ''; - const candidateText = selfReportedDurationCandidateText(previous, line, next); - if (!candidateText) continue; - const seconds = parseSelfReportedRoundDurationSeconds(candidateText); - if (!Number.isFinite(seconds)) continue; - if (!best || seconds > best.seconds) { - best = { kind: 'final-response-duration', seconds, preview: compactOneLine(candidateText).slice(0, 240), exact: true }; - } - } - return best; -} - -function selfReportedDurationCandidateText(previous, line, next) { - const current = String(line || ''); - const before = String(previous || ''); - const after = String(next || ''); - const windowText = [before, current, after].filter(Boolean).join(' '); - const hasDurationKeyword = /耗时|用时|duration|elapsed/i.test(current); - const hasNearbyDurationHeading = /(?:本轮|整轮|全程|任务|round|turn)?\s*(?:耗时|用时|duration|elapsed)/i.test(before) - || /(?:本轮|整轮|全程|任务|round|turn)?\s*(?:耗时|用时|duration|elapsed)/i.test(after); - const hasDurationValue = /(?:约|大约|around|about)?\s*\d+(?:\.\d+)?\s*(?:小时|分钟|分|秒|hour|hours|hr|hrs|min|mins|minute|minutes|sec|secs|second|seconds)/i.test(windowText); - const hasRoundContext = /本轮|整轮|全程|从.+到|全部通过|smoke|benchmark|round|turn|completed|passed/i.test(windowText); - if (hasDurationKeyword && hasDurationValue) return current; - if (hasNearbyDurationHeading && hasDurationValue) return windowText; - if (hasRoundContext && hasDurationValue && /(?:约|大约|around|about)\s*\d|\d+(?:\.\d+)?\s*(?:分钟|小时|minute|hour)/i.test(current)) return windowText; - return ''; -} - -function parseSelfReportedRoundDurationSeconds(text) { - const value = String(text || ''); - const clock = value.match(/(?:耗时|用时|duration|elapsed)[^0-9]{0,24}(\d{1,2}:\d{2}:\d{2}|\d{1,2}:\d{2})/i); - if (clock) return parseTraceDurationSeconds(clock[1]); - const hour = value.match(/(?:约|大约|around|about)?\s*(\d+(?:\.\d+)?)\s*(?:小时|hour|hours|hr|hrs)/i); - const minute = value.match(/(?:约|大约|around|about)?\s*(\d+(?:\.\d+)?)\s*(?:分钟|分|min|mins|minute|minutes)/i); - const second = value.match(/(?:约|大约|around|about)?\s*(\d+(?:\.\d+)?)\s*(?:秒|sec|secs|second|seconds)/i); - let total = 0; - if (hour) total += Number(hour[1]) * 3600; - if (minute) total += Number(minute[1]) * 60; - if (second) total += Number(second[1]); - return total > 0 ? total : null; -} - -function sampleVisibleText(sample, timelineItem) { - const chunks = []; - for (const source of [sample?.visibleText, sample?.text, sample?.innerText, sample?.dom?.visibleText, sample?.dom?.text, timelineItem?.visibleText, timelineItem?.text, timelineItem?.message, timelineItem?.summary]) { - if (typeof source === 'string' && source.trim()) chunks.push(source); - } - return chunks.join('\n'); -} - -function objectText(value) { - if (!value || typeof value !== 'object') return typeof value === 'string' ? value : ''; - const keys = ['text', 'innerText', 'visibleText', 'label', 'title', 'summary', 'message', 'content', 'body', 'preview', 'description']; - const chunks = []; - for (const key of keys) { - const part = value[key]; - if (typeof part === 'string' && part.trim()) chunks.push(part); - } - return chunks.join('\n'); -} - -function compactOneLine(value) { - return String(value || '').replace(/\s+/g, ' ').trim(); -} - -function buildCodeAgentCardTimingMetrics(samples, timeline, turnTiming) { - const missingElapsed = []; - const missingRecentUpdate = []; - const cardRows = []; - for (let index = 0; index < (Array.isArray(samples) ? samples : []).length; index += 1) { - const sample = samples[index]; - const timelineItem = timeline[index] || {}; - for (const card of codeAgentCardsForSample(sample)) { - const text = codeAgentCardText(card); - const totalElapsedValues = parseTotalElapsedSeconds(card?.durationText).filter(Number.isFinite); - const recentUpdateValues = parseRecentUpdateSeconds(card?.activityText).filter(Number.isFinite); - const terminal = isCodeAgentCardTerminal(card); - const row = { - ...ref(sample), - promptIndex: timelineItem.promptIndex ?? 0, - source: card.source ?? "turn", - status: card.status ?? null, - messageId: card.messageId ?? null, - traceId: card.traceId ?? firstTraceId([text]), - sessionId: card.sessionId ?? sample.sessionId ?? sample.workbenchSessionId ?? sample.routeSessionId ?? sample.activeSessionId ?? null, - durationText: limitText(card.durationText, 120), - activityText: limitText(card.activityText, 120), - totalElapsedSeconds: totalElapsedValues.length > 0 ? Math.max(...totalElapsedValues) : null, - recentUpdateSeconds: recentUpdateValues.length > 0 ? Math.max(...recentUpdateValues) : null, - terminal, - textHash: card.textHash ?? sha256(text), - textPreview: limitText(text, 180), - valuesRedacted: true - }; - cardRows.push(row); - if (row.totalElapsedSeconds === null) missingElapsed.push(row); - if (!terminal && row.recentUpdateSeconds === null) missingRecentUpdate.push(row); - } - } - const roundCompletion = buildRoundCompletionMetrics(samples, timeline, turnTiming); - return { - summary: { - cardSampleCount: cardRows.length, - terminalCardSampleCount: cardRows.filter((item) => item.terminal).length, - runningCardSampleCount: cardRows.filter((item) => !item.terminal).length, - missingElapsedCount: missingElapsed.length, - missingRecentUpdateCount: missingRecentUpdate.length, - roundCompletionEventCount: roundCompletion.events.length, - roundCompletionElapsedMismatchCount: roundCompletion.elapsedMismatches.length, - roundCompletionFinalResponseMissingCount: roundCompletion.finalResponseMissing.length, - roundCompletionPostTimingChangeCount: roundCompletion.postCompletionTimingChanges.length, - roundCompletionPostRecentUpdateVisibleCount: roundCompletion.postCompletionRecentUpdateVisible.length, - elapsedMismatchToleranceSeconds: alertThresholds.turnTimingSampleSlackSeconds, - }, - cardSamples: cardRows.slice(0, 200), - missingElapsed: missingElapsed.slice(0, 200), - missingRecentUpdate: missingRecentUpdate.slice(0, 200), - roundCompletion, - valuesRedacted: true - }; -} - -function codeAgentCardsForSample(sample) { - const turnCards = (Array.isArray(sample?.turns) ? sample.turns : []) - .map((item) => ({ ...item, source: item?.source || "turn" })) - .filter(isCodeAgentCardLike); - if (turnCards.length > 0) return turnCards; - return (Array.isArray(sample?.messages) ? sample.messages : []) - .map((item) => ({ ...item, source: item?.source || "message" })) - .filter(isCodeAgentCardLike); -} - -function isCodeAgentCardLike(item) { - const text = codeAgentCardText(item); - const role = String(item?.dataRole || item?.role || "").toLowerCase(); - if (/agent|assistant/u.test(role)) return true; - return /Code Agent|运行记录|耗时|最近\s*(?:\d+|一|两|三)|轮次完成|trace_id=trc_/iu.test(text); -} - -function codeAgentCardText(item) { - return [ - item?.durationText, - item?.activityText, - item?.text, - item?.textPreview, - item?.status - ].map((value) => String(value || "")).filter((value) => value.trim().length > 0).join("\n"); -} - -function isCodeAgentCardTerminal(item) { - const status = String(item?.status ?? item?.state ?? item?.phase ?? "").trim().toLowerCase().replace(/_/gu, "-"); - if (isActiveTurnStatus(status)) return false; - if (isTerminalTurnStatus(status)) return true; - if (item?.terminal === true || item?.sealed === true) return true; - const text = codeAgentCardText(item); - return isTerminalTraceText(text) || /轮次完成|轮次失败|轮次取消|已记录|completed|failed|canceled|cancelled|blocked/iu.test(text); -} - -function isActiveTurnStatus(value) { - const status = String(value ?? "").trim().toLowerCase().replace(/_/gu, "-"); - return ["pending", "running", "queued", "admitted", "dispatching", "in-progress", "inprogress", "executing", "progress", "thinking", "working", "active", "streaming", "created", "started"].includes(status); -} - -function buildRoundCompletionMetrics(samples, timeline, turnTiming) { - const events = []; - const elapsedMismatchToleranceSeconds = Math.max(5, Number(alertThresholds.turnTimingSampleSlackSeconds || 0)); - for (let index = 0; index < (Array.isArray(samples) ? samples : []).length; index += 1) { - const sample = samples[index]; - const timelineItem = timeline[index] || {}; - for (const event of roundCompletionEventsForSample(sample, timelineItem)) events.push(event); - } - const completionEvents = dedupeRoundCompletionEvents(events); - const elapsedMismatches = []; - const finalResponseMissing = []; - const postCompletionTimingChanges = []; - const postCompletionRecentUpdateVisible = []; - for (const event of completionEvents) { - const sampleIndex = samples.findIndex((sample) => sample?.seq === event.seq && sample?.pageRole === event.pageRole && sample?.pageId === event.pageId); - const sameSample = sampleIndex >= 0 ? samples[sampleIndex] : null; - const sameTimelineItem = sampleIndex >= 0 ? timeline[sampleIndex] || {} : {}; - const cards = sameSample ? cardMetricItemsForCompletion(sameSample, sameTimelineItem, event) : []; - const bestCard = cards.filter((card) => Number.isFinite(Number(card.totalElapsedSeconds)))[0] || null; - if (Number.isFinite(Number(event.elapsedSeconds)) && bestCard) { - const delta = Math.abs(Number(bestCard.totalElapsedSeconds) - Number(event.elapsedSeconds)); - if (delta > elapsedMismatchToleranceSeconds) { - elapsedMismatches.push({ - ...eventRef(event), - cardTotalElapsedSeconds: Number(bestCard.totalElapsedSeconds), - completionElapsedSeconds: Number(event.elapsedSeconds), - deltaSeconds: Number(delta.toFixed(3)), - toleranceSeconds: elapsedMismatchToleranceSeconds, - cardTraceId: bestCard.traceId ?? null, - cardMessageId: bestCard.messageId ?? null, - valuesRedacted: true - }); - } - } - if (!(sameSample && sampleHasTerminalAgentResultCard(sameSample, event)) && !(sameSample && sampleHasVisibleFinalResponse(sameSample, event)) && !hasFinalResponseAfterCompletion(samples, timeline, event)) { - finalResponseMissing.push({ - ...eventRef(event), - completionElapsedSeconds: event.elapsedSeconds, - finalResponseProbe: sameSample ? terminalAgentResultProbe(sameSample, event) : { ok: false, reason: "sample-not-found" }, - valuesRedacted: true - }); - } - const postTiming = detectPostCompletionTimingChanges(turnTiming, event); - postCompletionTimingChanges.push(...postTiming.timingChanges); - postCompletionRecentUpdateVisible.push(...postTiming.recentUpdateVisible); - } - return { - events: completionEvents.slice(0, 200), - elapsedMismatches: dedupeRoundCompletionRows(elapsedMismatches).slice(0, 200), - finalResponseMissing: dedupeRoundCompletionRows(finalResponseMissing).slice(0, 200), - postCompletionTimingChanges: dedupeRoundCompletionRows(postCompletionTimingChanges).slice(0, 200), - postCompletionRecentUpdateVisible: dedupeRoundCompletionRows(postCompletionRecentUpdateVisible).slice(0, 200), - valuesRedacted: true - }; -} - -function roundCompletionEventsForSample(sample, timelineItem) { - const rows = []; - for (const item of traceTimingRowsForSample(sample, timelineItem)) { - const text = String(item?.preview || item?.text || item?.textPreview || ""); - if (!traceRowIsRoundCompletionText(text, item?.eventKind || "")) continue; - const elapsedValues = [ - Number(item?.totalSeconds), - ...parseTotalElapsedSeconds(text).filter(Number.isFinite) - ].filter(Number.isFinite); - const attributed = inferRoundCompletionCard(sample, text); - rows.push({ - ...ref(sample), - promptIndex: timelineItem.promptIndex ?? 0, - traceId: item?.traceId ?? firstTraceId([text]) ?? attributed?.traceId ?? null, - messageId: item?.messageId ?? attributed?.messageId ?? null, - attributed: Boolean(item?.traceId || item?.messageId || attributed?.traceId || attributed?.messageId), - traceRowIndex: item?.rowIndex ?? item?.index ?? null, - elapsedSeconds: elapsedValues.length > 0 ? Math.max(...elapsedValues) : null, - textHash: item?.textHash ?? sha256(text), - preview: limitText(text, 180), - valuesRedacted: true - }); - } - return rows; -} - -function inferRoundCompletionCard(sample, text) { - const cards = codeAgentCardsForSample(sample) - .filter((card) => isCodeAgentCardTerminal(card)) - .filter((card) => card?.traceId || card?.messageId); - const textHash = sha256(String(text || "")); - const direct = cards.filter((card) => { - const cardText = codeAgentCardText(card); - return card?.textHash === textHash || cardText.includes(String(text || "").slice(0, 80)) || /轮次完成/iu.test(cardText); - }); - const candidates = direct.length > 0 ? direct : cards; - if (candidates.length !== 1) return null; - const card = candidates[0]; - return { traceId: card.traceId ?? null, messageId: card.messageId ?? card.id ?? null }; -} - -function cardMetricItemsForCompletion(sample, timelineItem, event) { - const metrics = turnMetricItems(sample, timelineItem) - .filter((item) => item.promptIndex === event.promptIndex) - .filter((item) => item.pageRole === event.pageRole || !event.pageRole) - .filter((item) => item.pageId === event.pageId || !event.pageId); - if (!event.traceId && !event.messageId) { - const withElapsed = metrics.filter((item) => Number.isFinite(Number(item.totalElapsedSeconds))); - return withElapsed.length === 1 ? withElapsed : []; - } - return metrics - .filter((item) => !event.traceId || !item.traceId || item.traceId === event.traceId) - .filter((item) => !event.messageId || !item.messageId || item.messageId === event.messageId) - .sort((left, right) => { - const leftTraceMatch = event.traceId && left.traceId === event.traceId ? 0 : 1; - const rightTraceMatch = event.traceId && right.traceId === event.traceId ? 0 : 1; - const leftMessageMatch = event.messageId && left.messageId === event.messageId ? 0 : 1; - const rightMessageMatch = event.messageId && right.messageId === event.messageId ? 0 : 1; - return leftTraceMatch - rightTraceMatch || leftMessageMatch - rightMessageMatch || String(left.source || "").localeCompare(String(right.source || "")); - }); -} - -function hasFinalResponseAfterCompletion(samples, timeline, event) { - if (!event.traceId && !event.messageId && !event.promptIndex) return true; - const eventTsMs = Date.parse(String(event.ts || "")); - for (let index = 0; index < (Array.isArray(samples) ? samples : []).length; index += 1) { - const sample = samples[index]; - const tsMs = Date.parse(String(sample?.ts || "")); - if (Number.isFinite(eventTsMs) && Number.isFinite(tsMs) && tsMs < eventTsMs) continue; - if (sample?.pageRole !== event.pageRole) continue; - const sampleSession = sample?.routeSessionId || sample?.activeSessionId || null; - const eventSession = event.routeSessionId || event.activeSessionId || null; - if (sampleSession && eventSession && sampleSession !== eventSession) continue; - const promptIndex = timeline[index]?.promptIndex ?? 0; - if (!event.traceId && !event.messageId && event.promptIndex && promptIndex !== event.promptIndex) continue; - if (sampleHasTerminalAgentResultCard(sample, event)) return true; - if (sampleHasVisibleFinalResponse(sample, event)) return true; - } - return false; -} - -function sampleHasTerminalAgentResultCard(sample, event = {}) { - return terminalAgentResultProbe(sample, event).ok === true; -} - -function terminalAgentResultProbe(sample, event = {}) { - const decisions = []; - for (const item of codeAgentCardsForSample(sample)) { - const text = normalizedText(codeAgentCardText(item)); - const traceMatched = Boolean(event.traceId && item?.traceId && item.traceId === event.traceId); - const decision = { - source: item?.source || null, - role: item?.role ?? null, - dataRole: item?.dataRole ?? null, - status: item?.status ?? null, - traceId: item?.traceId ?? null, - messageId: item?.messageId ?? null, - textBytes: Buffer.byteLength(text), - valuesRedacted: true - }; - if (event.traceId && item?.traceId && item.traceId !== event.traceId) { - decisions.push({ ...decision, decision: "skip-trace" }); - continue; - } - if (!traceMatched && event.messageId && item?.messageId && item.messageId !== event.messageId) { - decisions.push({ ...decision, decision: "skip-message" }); - continue; - } - if (!isAssistantFinalResponseCandidate(item, item?.source || "turn")) { - decisions.push({ ...decision, decision: "skip-role" }); - continue; - } - if (!isCodeAgentCardTerminal(item)) { - decisions.push({ ...decision, decision: "skip-non-terminal" }); - continue; - } - if (text.length < 24) { - decisions.push({ ...decision, decision: "skip-short" }); - continue; - } - decisions.push({ ...decision, decision: "accept-terminal-agent-card" }); - return { ok: true, decisions: decisions.slice(-8), valuesRedacted: true }; - } - return { ok: false, decisions: decisions.slice(-8), valuesRedacted: true }; -} - -function sampleHasVisibleFinalResponse(sample, event = {}) { - for (const [groupName, group] of [["messages", sample?.messages], ["turns", sample?.turns]]) { - if (!Array.isArray(group)) continue; - for (const item of group) { - const traceMatched = Boolean(event.traceId && item?.traceId && item.traceId === event.traceId); - if (event.traceId && item?.traceId && item.traceId !== event.traceId) continue; - if (!traceMatched && event.messageId && item?.messageId && item.messageId !== event.messageId) continue; - if (!isAssistantFinalResponseCandidate(item, groupName)) continue; - const text = normalizedText([item?.text, item?.textPreview].filter(Boolean).join(" ")); - if (text.length < 24) continue; - if (groupName === "messages" && isTerminalTurnStatus(item?.status)) return true; - if (isDiagnosticText(text)) continue; - if (isFinalResultText(text)) return true; - if (/运行记录/iu.test(text) && /(?:已完成|完成|新增|实现|验证|测试|结果|README|文件|summary)/iu.test(text)) return true; - } - } - return false; -} - -function normalizedDomRole(item) { - return String(item?.dataRole ?? item?.role ?? item?.ariaRole ?? "") - .trim() - .toLowerCase() - .replace(/[_\s]+/gu, "-"); -} - -function isAssistantFinalResponseCandidate(item, groupName) { - const role = normalizedDomRole(item); - if (/agent|assistant|code-agent|bot/iu.test(role)) return true; - if (/user|human|client|prompt/iu.test(role)) return false; - if (groupName === "turns") return isCodeAgentCardLike(item); - const text = codeAgentCardText(item); - if (isTerminalTurnStatus(item?.status) && /Code Agent|运行记录|assistant|agent/iu.test(text)) return true; - return false; -} - -function detectPostCompletionTimingChanges(turnTiming, event) { - const timingChanges = []; - const recentUpdateVisible = []; - if (!event.traceId && !event.messageId) return { timingChanges, recentUpdateVisible }; - const rows = Array.isArray(turnTiming?.rows) ? turnTiming.rows : []; - const columns = Array.isArray(turnTiming?.columns) ? turnTiming.columns : []; - const eventTsMs = Date.parse(String(event.ts || "")); - for (const column of columns) { - if (column.pageRole && event.pageRole && column.pageRole !== event.pageRole) continue; - if (event.traceId && column.traceId && column.traceId !== event.traceId) continue; - if (event.messageId && column.messageId && column.messageId !== event.messageId) continue; - if (event.promptIndex && column.promptIndex && column.promptIndex !== event.promptIndex && column.lastPromptIndex !== event.promptIndex) continue; - let previousTotal = null; - let previousRecent = null; - for (const row of rows) { - const rowTsMs = Date.parse(String(row.ts || "")); - if (Number.isFinite(eventTsMs) && Number.isFinite(rowTsMs) && rowTsMs < eventTsMs) continue; - if (row.pageRole && event.pageRole && row.pageRole !== event.pageRole) continue; - const cell = row.cells?.[column.id]; - if (!cell) continue; - if (event.traceId && cell.traceId && cell.traceId !== event.traceId) continue; - if (event.messageId && cell.messageId && cell.messageId !== event.messageId) continue; - const total = cell.totalElapsedSeconds === null || cell.totalElapsedSeconds === undefined ? NaN : Number(cell.totalElapsedSeconds); - if (Number.isFinite(total)) { - if (previousTotal && Math.abs(total - previousTotal.value) > alertThresholds.turnTimingSampleSlackSeconds) { - timingChanges.push({ - ...eventRef(event), - columnId: column.id, - columnLabel: column.label, - metric: "totalElapsedSeconds", - fromSeq: previousTotal.seq, - fromTs: previousTotal.ts, - fromValue: previousTotal.value, - toSeq: row.seq ?? null, - toTs: row.ts ?? null, - toValue: total, - delta: Number((total - previousTotal.value).toFixed(3)), - toleranceSeconds: alertThresholds.turnTimingSampleSlackSeconds, - traceId: cell.traceId ?? column.traceId ?? event.traceId ?? null, - messageId: cell.messageId ?? column.messageId ?? null, - valuesRedacted: true - }); - } - previousTotal = { value: total, seq: row.seq ?? null, ts: row.ts ?? null }; - } - const recent = cell.recentUpdateSeconds === null || cell.recentUpdateSeconds === undefined ? NaN : Number(cell.recentUpdateSeconds); - if (Number.isFinite(recent)) { - recentUpdateVisible.push({ - ...eventRef(event), - columnId: column.id, - columnLabel: column.label, - metric: "recentUpdateSeconds", - seq: row.seq ?? null, - ts: row.ts ?? null, - value: recent, - traceId: cell.traceId ?? column.traceId ?? event.traceId ?? null, - messageId: cell.messageId ?? column.messageId ?? null, - valuesRedacted: true - }); - if (previousRecent && recent !== previousRecent.value) { - timingChanges.push({ - ...eventRef(event), - columnId: column.id, - columnLabel: column.label, - metric: "recentUpdateSeconds", - fromSeq: previousRecent.seq, - fromTs: previousRecent.ts, - fromValue: previousRecent.value, - toSeq: row.seq ?? null, - toTs: row.ts ?? null, - toValue: recent, - delta: Number((recent - previousRecent.value).toFixed(3)), - traceId: cell.traceId ?? column.traceId ?? event.traceId ?? null, - messageId: cell.messageId ?? column.messageId ?? null, - valuesRedacted: true - }); - } - previousRecent = { value: recent, seq: row.seq ?? null, ts: row.ts ?? null }; - } - } - } - return { timingChanges, recentUpdateVisible }; -} - -function dedupeRoundCompletionEvents(rows) { - const result = []; - const seen = new Set(); - for (const row of Array.isArray(rows) ? rows : []) { - const key = [ - row?.pageRole ?? "", - row?.pageId ?? "", - row?.promptIndex ?? "", - row?.traceId ?? "", - row?.messageId ?? "", - row?.textHash ?? "", - row?.elapsedSeconds ?? "" - ].join("|"); - if (seen.has(key)) continue; - seen.add(key); - result.push(row); - } - return result; -} - -function eventRef(event) { - return { - seq: event?.seq ?? null, - sampleGroupSeq: event?.sampleGroupSeq ?? null, - ts: event?.ts ?? null, - pageRole: event?.pageRole ?? null, - pageId: event?.pageId ?? null, - routeSessionId: event?.routeSessionId ?? null, - activeSessionId: event?.activeSessionId ?? null, - promptIndex: event?.promptIndex ?? null, - traceId: event?.traceId ?? null, - messageId: event?.messageId ?? null, - }; -} - -function dedupeRoundCompletionRows(rows) { - const result = []; - const seen = new Set(); - for (const row of Array.isArray(rows) ? rows : []) { - const key = [ - row?.seq ?? row?.fromSeq ?? "", - row?.toSeq ?? "", - row?.pageRole ?? "", - row?.promptIndex ?? "", - row?.traceId ?? "", - row?.metric ?? "", - row?.textHash ?? "", - row?.columnId ?? "" - ].join("|"); - if (seen.has(key)) continue; - seen.add(key); - result.push(row); - } - return result; -} - -function turnMetricItems(sample, timelineItem) { - const promptIndex = timelineItem.promptIndex ?? 0; - const pageRole = sample?.pageRole || "control"; - const pageId = sample?.pageId || "unknown-page"; - const sessionKey = pageRole + ":" + pageId + ":" + (sample?.routeSessionId || sample?.activeSessionId || "unknown-session"); - const roundKey = String(promptIndex); - const items = []; - if (Array.isArray(sample?.turns) && sample.turns.length > 0) { - for (const turn of sample.turns) { - const texts = turnTexts(turn); - const totalElapsedValues = texts.flatMap(parseTotalElapsedSeconds).filter(Number.isFinite); - const recentUpdateValues = texts.flatMap(parseRecentUpdateSeconds).filter(Number.isFinite); - const traceId = turn.traceId || firstTraceId(texts); - const messageId = turn.messageId || null; - const turnId = turn.turnId || traceId || null; - const stableId = traceId || messageId || turnId || null; - const domIndex = Number.isFinite(Number(turn.index)) ? Number(turn.index) : items.length; - const key = stableId - ? "timing:" + sessionKey + ":id-" + stableId - : "turn:" + sessionKey + ":round-" + roundKey + ":dom-index-" + String(domIndex); - items.push({ - key, - source: "turn", - pageRole, - pageId, - promptIndex, - traceId, - messageId, - turnId, - domIndex, - status: turn.status ?? null, - totalElapsedSeconds: totalElapsedValues.length > 0 ? Math.max(...totalElapsedValues) : null, - recentUpdateSeconds: recentUpdateValues.length > 0 ? Math.max(...recentUpdateValues) : null, - textHash: turn.textHash || sha256(texts.join("\n")) - }); - } - return items; - } - if (Array.isArray(sample?.messages) && sample.messages.length > 0) { - for (const message of sample.messages) { - const text = [message?.durationText, message?.activityText].map((value) => String(value || "")).filter((value) => value.trim().length > 0).join("\n"); - const totalElapsedValues = parseTotalElapsedSeconds(text).filter(Number.isFinite); - const recentUpdateValues = parseRecentUpdateSeconds(text).filter(Number.isFinite); - if (totalElapsedValues.length === 0 && recentUpdateValues.length === 0) continue; - const domIndex = Number.isFinite(Number(message.index)) ? Number(message.index) : items.length; - const traceId = message.traceId || firstTraceId([text]); - const messageId = message.messageId || null; - const stableId = traceId || messageId || message.turnId || null; - items.push({ - key: stableId - ? "timing:" + sessionKey + ":id-" + stableId - : "message:" + sessionKey + ":round-" + roundKey + ":dom-index-" + String(domIndex), - source: "message", - pageRole, - pageId, - promptIndex, - traceId, - messageId, - turnId: message.turnId || traceId || null, - domIndex, - status: message.status ?? null, - totalElapsedSeconds: totalElapsedValues.length > 0 ? Math.max(...totalElapsedValues) : null, - recentUpdateSeconds: recentUpdateValues.length > 0 ? Math.max(...recentUpdateValues) : null, - textHash: message.textHash || sha256(text) - }); - } - if (items.length > 0) return items; - } - if (timelineItem.totalElapsedSeconds !== null || timelineItem.recentUpdateSeconds !== null) { - return [{ - key: "aggregate:" + sessionKey + ":round-" + roundKey, - source: "aggregate", - pageRole, - pageId, - promptIndex, - traceId: null, - messageId: null, - domIndex: null, - status: null, - totalElapsedSeconds: timelineItem.totalElapsedSeconds ?? null, - recentUpdateSeconds: timelineItem.recentUpdateSeconds ?? null, - textHash: timelineItem.textDigest ?? null - }]; - } - return []; -} - -function turnTexts(turn) { - return [ - turn?.durationText, - turn?.activityText - ].map((value) => String(value || "")).filter((value) => value.trim().length > 0); -} - -function firstTraceId(texts) { - for (const text of texts) { - const match = String(text || "").match(/\btrc_[A-Za-z0-9_-]+\b/u); - if (match) return match[0]; - } - return null; -} - -function buildRoundMetricSummaries(timeline, promptCommands, timing = {}) { - const rounds = []; - const timingColumns = Array.isArray(timing.columns) ? timing.columns : []; - const timingRows = Array.isArray(timing.rows) ? timing.rows : []; - const nonMonotonic = Array.isArray(timing.nonMonotonic) ? timing.nonMonotonic : []; - const totalElapsedForwardJumps = Array.isArray(timing.totalElapsedForwardJumps) ? timing.totalElapsedForwardJumps : []; - const elapsedZeroResets = Array.isArray(timing.elapsedZeroResets) ? timing.elapsedZeroResets : []; - const terminalElapsedGrowth = Array.isArray(timing.terminalElapsedGrowth) ? timing.terminalElapsedGrowth : []; - const recentUpdateResets = Array.isArray(timing.recentUpdateResets) ? timing.recentUpdateResets : []; - const recentUpdateSteps = Array.isArray(timing.recentUpdateSteps) ? timing.recentUpdateSteps : []; - for (let index = 0; index < promptCommands.length; index += 1) { - const promptIndex = index + 1; - const items = timeline.filter((item) => item.promptIndex === promptIndex); - const aggregateTotalElapsed = items.map((item) => item.totalElapsedSeconds).filter((value) => value !== null); - const aggregateRecentUpdate = items.map((item) => item.recentUpdateSeconds).filter((value) => value !== null); - const promptTurnTiming = roundPromptTurnTimingValues(timingRows, timingColumns, promptIndex, { - firstSeq: items[0]?.seq ?? null, - firstSampleAt: items[0]?.ts ?? null - }); - const totalElapsed = promptTurnTiming.totalElapsed.length > 0 ? promptTurnTiming.totalElapsed : aggregateTotalElapsed; - const recentUpdate = promptTurnTiming.recentUpdate.length > 0 ? promptTurnTiming.recentUpdate : aggregateRecentUpdate; - const loadingCounts = items.map((item) => Number(item.loadingCount ?? 0)).filter(Number.isFinite); - const loadingOwners = new Set(); - for (const item of items) { - for (const owner of Array.isArray(item.loadingOwners) ? item.loadingOwners : []) { - if (owner?.ownerKey) loadingOwners.add(owner.ownerKey); - } - } - const timingAnomalies = nonMonotonic.filter((item) => item.promptIndex === promptIndex); - const timingForwardJumps = totalElapsedForwardJumps.filter((item) => item.promptIndex === promptIndex); - const timingZeroResets = elapsedZeroResets.filter((item) => item.promptIndex === promptIndex); - const timingTerminalGrowth = terminalElapsedGrowth.filter((item) => item.promptIndex === promptIndex); - const timingResets = recentUpdateResets.filter((item) => item.promptIndex === promptIndex); - const timingSteps = recentUpdateSteps.filter((item) => item.promptIndex === promptIndex); - const terminalGrowthDeltas = timingTerminalGrowth.map((item) => Number(item.delta)).filter((value) => Number.isFinite(value) && value > 0); - const timingStepDeltas = timingSteps.map((item) => Number(item.delta)).filter((value) => Number.isFinite(value) && value >= 0); - const timingStepExcess = timingSteps.map((item) => Number(item.excessiveIncreaseSeconds)).filter((value) => Number.isFinite(value) && value > 0); - rounds.push({ - promptIndex, - promptCommandId: promptCommands[index].commandId, - promptTextHash: promptCommands[index].textHash, - promptTextBytes: promptCommands[index].textBytes, - promptCompletedAt: promptCommands[index].ts, - sampleCount: items.length, - firstSeq: items[0]?.seq ?? null, - lastSeq: items[items.length - 1]?.seq ?? null, - firstSampleAt: items[0]?.ts ?? null, - lastSampleAt: items[items.length - 1]?.ts ?? null, - withTotalElapsed: totalElapsed.length, - withRecentUpdate: recentUpdate.length, - loadingSamples: loadingCounts.filter((value) => value > 0).length, - maxLoadingCount: loadingCounts.length > 0 ? Math.max(...loadingCounts) : 0, - loadingOwnerCount: loadingOwners.size, - maxTotalElapsedSeconds: totalElapsed.length > 0 ? Math.max(...totalElapsed) : null, - lastTotalElapsedSeconds: lastNonNull(totalElapsed), - maxRecentUpdateSeconds: recentUpdate.length > 0 ? Math.max(...recentUpdate) : null, - lastRecentUpdateSeconds: lastNonNull(recentUpdate), - diagnosticSamples: items.filter((item) => item.diagnosticSeen).length, - terminalSamples: items.filter((item) => item.terminalSeen).length, - finalTextSamples: items.filter((item) => item.finalResultTextSeen).length, - turnTimingNonMonotonicCount: timingAnomalies.length, - turnTimingTotalElapsedDecreaseCount: timingAnomalies.filter((item) => item.metric === "totalElapsedSeconds").length, - turnTimingTotalElapsedZeroResetCount: timingZeroResets.length, - turnTimingTotalElapsedForwardJumpCount: timingForwardJumps.length, - turnTimingTotalElapsedForwardJumpMaxSeconds: maxPositiveDelta(timingForwardJumps), - turnTimingTerminalElapsedGrowthCount: timingTerminalGrowth.length, - turnTimingTerminalElapsedGrowthMaxSeconds: terminalGrowthDeltas.length > 0 ? Math.max(...terminalGrowthDeltas) : 0, - turnTimingRecentUpdateJumpCount: timingAnomalies.filter((item) => item.metric === "recentUpdateSeconds" && item.anomaly === "jump").length, - turnTimingRecentUpdateSawtoothJumpCount: timingAnomalies.filter((item) => item.metric === "recentUpdateSeconds" && item.anomaly === "jump").length, - turnTimingRecentUpdateStepCount: timingSteps.length, - turnTimingRecentUpdateMaxIncreaseSeconds: timingStepDeltas.length > 0 ? Math.max(...timingStepDeltas) : null, - turnTimingRecentUpdateMaxExcessSeconds: timingStepExcess.length > 0 ? Math.max(...timingStepExcess) : 0, - turnTimingRecentUpdateResetCount: timingResets.length, - turnTimingRecentUpdateDecreaseCount: timingResets.length - }); - } - return rounds; -} - -function roundPromptTurnTimingValues(rows, columns, promptIndex, round = {}) { - const roundFirstSeq = Number(round.firstSeq); - const roundFirstMs = Date.parse(String(round.firstSampleAt ?? "")); - const seqSlack = 3; - const timeSlackMs = 3000; - const promptColumnIds = new Set(columns - .filter((column) => { - if (Number(column?.promptIndex) === Number(promptIndex)) return true; - const firstSeq = Number(column?.firstSeq); - if (Number.isFinite(roundFirstSeq) && Number.isFinite(firstSeq) && firstSeq >= roundFirstSeq - seqSlack) return true; - const firstMs = Date.parse(String(column?.firstTs ?? "")); - return Number.isFinite(roundFirstMs) && Number.isFinite(firstMs) && firstMs >= roundFirstMs - timeSlackMs; - }) - .map((column) => String(column?.id || "")) - .filter(Boolean)); - if (promptColumnIds.size === 0) return { totalElapsed: [], recentUpdate: [] }; - const totalElapsed = []; - const recentUpdate = []; - for (const row of rows) { - if (Number(row?.promptIndex ?? 0) !== Number(promptIndex)) continue; - const cells = row?.cells && typeof row.cells === "object" ? row.cells : {}; - for (const [columnId, cell] of Object.entries(cells)) { - if (!promptColumnIds.has(columnId)) continue; - const total = Number(cell?.totalElapsedSeconds); - if (Number.isFinite(total)) totalElapsed.push(total); - const recent = Number(cell?.recentUpdateSeconds); - if (Number.isFinite(recent)) recentUpdate.push(recent); - } - } - return { totalElapsed, recentUpdate }; -} - -function sampleTexts(sample) { - const rows = []; - for (const group of [sample?.messages, sample?.traceRows, sample?.diagnostics]) { - if (!Array.isArray(group)) continue; - for (const item of group) { - const text = String(item?.textPreview || ""); - if (text.trim()) rows.push(text); - } - } - return rows; -} - -function sampleTurnTimingTexts(sample) { - const rows = []; - for (const group of [sample?.turns, sample?.messages]) { - if (!Array.isArray(group)) continue; - for (const item of group) { - for (const value of [item?.durationText, item?.activityText]) { - const text = String(value || ""); - if (text.trim()) rows.push(text); - } - } - } - return rows; -} - -function parseTotalElapsedSeconds(text) { - const values = []; - for (const match of String(text || "").matchAll(/(?:总耗时|耗时)\s*[=::]?\s*(\d{1,2}):(\d{2}):(\d{2})/giu)) { - values.push(Number(match[1]) * 3600 + Number(match[2]) * 60 + Number(match[3])); - } - for (const match of String(text || "").matchAll(/(?:总耗时|耗时)\s*[=::]?\s*(\d{1,2}):(\d{2})(?!:)/giu)) { - values.push(Number(match[1]) * 60 + Number(match[2])); - } - for (const match of String(text || "").matchAll(/(?:总耗时|耗时)\s*[=::]?\s*(?:(\d+)\s*天)?\s*(?:(\d+)\s*小时)?\s*(?:(\d+)\s*(?:分钟|分))?\s*(?:(\d+)\s*秒)?/giu)) { - const days = Number(match[1] || 0); - const hours = Number(match[2] || 0); - const minutes = Number(match[3] || 0); - const seconds = Number(match[4] || 0); - if (days || hours || minutes || seconds || /(?:天|小时|分钟|分|秒)/u.test(match[0] || "")) values.push(days * 86400 + hours * 3600 + minutes * 60 + seconds); - } - return values; -} - -function lastNonNull(values) { - for (let index = values.length - 1; index >= 0; index -= 1) if (values[index] !== null && values[index] !== undefined) return values[index]; - return null; -} - -function parseRecentUpdateSeconds(text) { - const values = []; - for (const match of String(text || "").matchAll(/最近\s*(?:(\d+)\s*天)?\s*(?:(\d+)\s*小时)?\s*(?:(\d+)\s*(?:分钟|分))?\s*(?:(\d+)\s*秒)?\s*前/giu)) { - const days = Number(match[1] || 0); - const hours = Number(match[2] || 0); - const minutes = Number(match[3] || 0); - const seconds = Number(match[4] || 0); - values.push(days * 86400 + hours * 3600 + minutes * 60 + seconds); - } - return values; -} - -function latestPromptIndex(promptTimes, tsMs) { - let index = 0; - for (let i = 0; i < promptTimes.length; i += 1) { - if (promptTimes[i] <= tsMs) index = i + 1; - else break; - } - return index; -} - -function promptIndexForTs(promptTimes, ts) { - const tsMs = Date.parse(ts); - return Number.isFinite(tsMs) ? latestPromptIndex(promptTimes, tsMs) : 0; -} - -function buildTransitions(samples) { - const rows = []; - let last = null; - for (const sample of samples) { - const digest = digestSample(sample); - if (digest !== last) { - rows.push({ seq: sample.seq, ts: sample.ts, url: sample.url, routeSessionId: sample.routeSessionId || null, activeSessionId: sample.activeSessionId || null, messageCount: Array.isArray(sample.messages) ? sample.messages.length : 0, traceRowCount: Array.isArray(sample.traceRows) ? sample.traceRows.length : 0, digest }); - last = digest; - } - } - return rows.slice(0, 200); -} - -function detectFinalFlicker(samples) { - const flickers = []; - const lastNonEmptyByScope = new Map(); - for (const sample of samples) { - const scope = finalFlickerScope(sample); - if (!scope) continue; - const messageText = Array.isArray(sample.messages) ? sample.messages.map((item) => item.textPreview || "").join("\n") : ""; - const nonEmpty = messageText.trim().length > 0; - const finalLike = /轮次完成|已记录|已完成第\d+轮|final response|terminal result/iu.test(messageText); - const diagnosticLike = /temporarily|timeout|无法连接|暂时|error|failed|超时/iu.test(messageText); - const lastNonEmpty = lastNonEmptyByScope.get(scope); - if (nonEmpty && finalLike && !diagnosticLike) lastNonEmptyByScope.set(scope, { sample, messageText }); - if (lastNonEmpty && nonEmpty && diagnosticLike) flickers.push({ scope, from: ref(lastNonEmpty.sample), to: ref(sample) }); - if (lastNonEmpty && !nonEmpty) flickers.push({ scope, from: ref(lastNonEmpty.sample), to: ref(sample), reason: "text-disappeared" }); - } - return flickers; -} - -function finalFlickerScope(sample) { - const pathname = samplePathname(sample); - const sessionId = sample?.routeSessionId || sample?.activeSessionId || workbenchSessionIdFromPath(pathname); - if (!sessionId) return null; - if (!pathname.startsWith("/workbench/sessions/" + sessionId)) return null; - return (sample?.pageRole || "control") + ":" + sessionId; -} - -function samplePathname(sample) { - const raw = String(sample?.path || sample?.url || "").trim(); - if (!raw) return ""; - try { - return new URL(raw, "http://hwlab.local").pathname || raw; - } catch { - return raw.split(/[?#]/u, 1)[0] || raw; - } -} - -function workbenchSessionIdFromPath(pathname) { - const match = String(pathname || "").match(/^\/workbench\/sessions\/([^/?#]+)/u); - return match ? match[1] : null; -} - -function detectTerminalZeroElapsed(samples) { - const rows = []; - for (const sample of samples) { - const turns = Array.isArray(sample?.turns) ? sample.turns : []; - const messages = Array.isArray(sample?.messages) ? sample.messages : []; - for (const item of [...turns, ...messages]) { - const text = [item?.durationText, item?.activityText, item?.status, item?.textPreview, item?.text].filter(Boolean).join("\n"); - if (!/(?:Code Agent|运行记录|completed|failed|canceled|blocked)/iu.test(text)) continue; - if (!/(?:completed|failed|canceled|blocked|已完成|失败|取消)/iu.test(text)) continue; - const timingText = [item?.durationText, item?.activityText].filter(Boolean).join("\n"); - const elapsedValues = parseTotalElapsedSeconds(timingText); - if (!elapsedValues.includes(0)) continue; - rows.push({ - ...ref(sample), - status: item?.status ?? null, - messageId: item?.messageId ?? null, - traceId: item?.traceId ?? null, - durationText: item?.durationText ?? null, - activityText: item?.activityText ?? null, - textPreview: limitText(item?.textPreview || item?.text || "", 180) - }); - } - } - return rows; -} - -function detectCrossPageProjectionDiffs(samples) { - const groups = new Map(); - for (const sample of samples) { - const key = sample?.sampleGroupSeq ?? sample?.seq; - if (key === null || key === undefined) continue; - const group = groups.get(key) || {}; - if (sample?.pageRole === "control") group.control = sample; - else if (sample?.pageRole === "observer") group.observer = sample; - groups.set(key, group); - } - const rows = []; - for (const [sampleGroupSeq, group] of groups.entries()) { - const control = group.control; - const observer = group.observer; - if (!control || !observer) continue; - const controlTraceIds = visibleTraceIds(control); - const observerTraceIds = visibleTraceIds(observer); - const missingInObserver = [...controlTraceIds].filter((item) => !observerTraceIds.has(item)); - const controlMessages = Array.isArray(control.messages) ? control.messages.length : 0; - const observerMessages = Array.isArray(observer.messages) ? observer.messages.length : 0; - const controlTraceRows = Array.isArray(control.traceRows) ? control.traceRows.length : 0; - const observerTraceRows = Array.isArray(observer.traceRows) ? observer.traceRows.length : 0; - const controlZero = detectTerminalZeroElapsed([control]).length > 0; - const observerZero = detectTerminalZeroElapsed([observer]).length > 0; - const sameSession = (control.routeSessionId || control.activeSessionId || null) && (control.routeSessionId || control.activeSessionId || null) === (observer.routeSessionId || observer.activeSessionId || null); - const controlMessageDigest = digestMessageTexts(control); - const observerMessageDigest = digestMessageTexts(observer); - const messageTextDigestDiff = controlMessageDigest !== observerMessageDigest; - const projectionDivergent = sameSession && (Math.abs(controlMessages - observerMessages) > 0 || controlZero !== observerZero); - const traceVisibilityDivergent = sameSession && !projectionDivergent && (missingInObserver.length > 0 || Math.abs(controlTraceRows - observerTraceRows) > 0); - if (!projectionDivergent && !traceVisibilityDivergent) continue; - rows.push({ - sampleGroupSeq, - diffKind: traceVisibilityDivergent ? "trace-visibility" : "projection", - control: ref(control), - observer: ref(observer), - controlTraceIds: [...controlTraceIds].slice(0, 8), - observerTraceIds: [...observerTraceIds].slice(0, 8), - missingTraceIdsInObserver: missingInObserver.slice(0, 8), - controlMessageCount: controlMessages, - observerMessageCount: observerMessages, - controlTraceRowCount: controlTraceRows, - observerTraceRowCount: observerTraceRows, - terminalZeroElapsedDiff: controlZero !== observerZero, - messageTextDigestDiff, - controlMessageDigest, - observerMessageDigest - }); - } - return rows; -} - -function mergeCrossPageDiffRows(...groups) { - const rows = []; - const seen = new Set(); - for (const group of groups) { - if (!Array.isArray(group)) continue; - for (const row of group) { - const key = [ - row?.diffKind || "projection", - row?.control?.seq ?? null, - row?.observer?.seq ?? null, - row?.controlMessageCount ?? null, - row?.observerMessageCount ?? null, - row?.controlTraceRowCount ?? null, - row?.observerTraceRowCount ?? null - ].join(":"); - if (seen.has(key)) continue; - seen.add(key); - rows.push(row); - } - } - return rows; -} - -function annotateCrossPageDiffTiming(rows) { - const groups = new Map(); - for (const row of Array.isArray(rows) ? rows : []) { - const controlAt = Date.parse(String(row?.control?.ts || "")); - const observerAt = Date.parse(String(row?.observer?.ts || "")); - const timestamps = [controlAt, observerAt].filter(Number.isFinite); - const startMs = timestamps.length > 0 ? Math.min(...timestamps) : null; - const endMs = timestamps.length > 0 ? Math.max(...timestamps) : null; - const sessionId = row?.control?.routeSessionId || row?.control?.activeSessionId || row?.observer?.routeSessionId || row?.observer?.activeSessionId || "unknown-session"; - const key = [row?.diffKind || "projection", sessionId].join(":"); - const group = groups.get(key) || { rows: [], firstMs: null, lastMs: null }; - const annotated = { - ...row, - sampleStartAt: startMs === null ? null : new Date(startMs).toISOString(), - sampleEndAt: endMs === null ? null : new Date(endMs).toISOString(), - pairSkewMs: startMs === null || endMs === null ? null : endMs - startMs, - }; - group.rows.push(annotated); - if (startMs !== null && (group.firstMs === null || startMs < group.firstMs)) group.firstMs = startMs; - if (endMs !== null && (group.lastMs === null || endMs > group.lastMs)) group.lastMs = endMs; - groups.set(key, group); - } - const result = []; - for (const group of groups.values()) { - const sortedRows = group.rows.slice().sort((a, b) => Number(Date.parse(String(a.sampleStartAt || ""))) - Number(Date.parse(String(b.sampleStartAt || "")))); - const segments = []; - const splitGapMs = Math.max(1000, Number(alertThresholds.crossPageProjectionDivergenceRedMs || alertThresholds.visibleLoadingSlowMs || 10_000)); - for (const row of sortedRows) { - const startMs = Date.parse(String(row.sampleStartAt || "")); - const endMs = Date.parse(String(row.sampleEndAt || row.sampleStartAt || "")); - const last = segments.at(-1); - const lastEndMs = last && last.lastMs !== null ? last.lastMs : null; - if (!last || (Number.isFinite(startMs) && lastEndMs !== null && startMs - lastEndMs > splitGapMs)) { - segments.push({ rows: [row], firstMs: Number.isFinite(startMs) ? startMs : null, lastMs: Number.isFinite(endMs) ? endMs : Number.isFinite(startMs) ? startMs : null }); - continue; - } - last.rows.push(row); - if (Number.isFinite(startMs) && (last.firstMs === null || startMs < last.firstMs)) last.firstMs = startMs; - const effectiveEndMs = Number.isFinite(endMs) ? endMs : Number.isFinite(startMs) ? startMs : null; - if (effectiveEndMs !== null && (last.lastMs === null || effectiveEndMs > last.lastMs)) last.lastMs = effectiveEndMs; - } - for (let segmentIndex = 0; segmentIndex < segments.length; segmentIndex += 1) { - const segment = segments[segmentIndex]; - const observedSpanMs = segment.firstMs === null || segment.lastMs === null ? null : segment.lastMs - segment.firstMs; - for (const row of segment.rows) { - result.push({ - ...row, - segmentIndex, - observedFirstAt: segment.firstMs === null ? null : new Date(segment.firstMs).toISOString(), - observedLastAt: segment.lastMs === null ? null : new Date(segment.lastMs).toISOString(), - observedSpanMs, - }); - } - } - } - return result; -} - -function detectAdjacentCrossPageProjectionDiffs(samples) { - const rows = []; - const ordered = (Array.isArray(samples) ? samples : []).slice().sort((a, b) => Number(a?.seq ?? 0) - Number(b?.seq ?? 0)); - for (let i = 1; i < ordered.length; i += 1) { - const a = ordered[i - 1]; - const b = ordered[i]; - const roles = new Set([a?.pageRole, b?.pageRole]); - if (!roles.has("control") || !roles.has("observer")) continue; - const control = a?.pageRole === "control" ? a : b; - const observer = a?.pageRole === "observer" ? a : b; - const controlSession = control.routeSessionId || control.activeSessionId || null; - const observerSession = observer.routeSessionId || observer.activeSessionId || null; - if (!controlSession || controlSession !== observerSession) continue; - const controlAt = Date.parse(String(control.ts || "")); - const observerAt = Date.parse(String(observer.ts || "")); - if (Number.isFinite(controlAt) && Number.isFinite(observerAt) && Math.abs(controlAt - observerAt) > 1500) continue; - const controlMessages = Array.isArray(control.messages) ? control.messages.length : 0; - const observerMessages = Array.isArray(observer.messages) ? observer.messages.length : 0; - const controlTraceRows = Array.isArray(control.traceRows) ? control.traceRows.length : 0; - const observerTraceRows = Array.isArray(observer.traceRows) ? observer.traceRows.length : 0; - const controlZero = detectTerminalZeroElapsed([control]).length > 0; - const observerZero = detectTerminalZeroElapsed([observer]).length > 0; - const missingInObserver = [...visibleTraceIds(control)].filter((item) => !visibleTraceIds(observer).has(item)); - const controlTraceIds = visibleTraceIds(control); - const observerTraceIds = visibleTraceIds(observer); - const controlMessageDigest = digestMessageTexts(control); - const observerMessageDigest = digestMessageTexts(observer); - const messageTextDigestDiff = controlMessageDigest !== observerMessageDigest; - const projectionDivergent = controlMessages !== observerMessages || controlZero !== observerZero; - const traceVisibilityDivergent = !projectionDivergent && (missingInObserver.length > 0 || controlTraceRows !== observerTraceRows); - if (!projectionDivergent && !traceVisibilityDivergent) continue; - rows.push({ - sampleGroupSeq: control.sampleGroupSeq ?? observer.sampleGroupSeq ?? null, - adjacentPair: true, - diffKind: traceVisibilityDivergent ? "trace-visibility" : "projection", - control: ref(control), - observer: ref(observer), - controlTraceIds: [...controlTraceIds].slice(0, 8), - observerTraceIds: [...observerTraceIds].slice(0, 8), - missingTraceIdsInObserver: missingInObserver.slice(0, 8), - controlMessageCount: controlMessages, - observerMessageCount: observerMessages, - controlTraceRowCount: controlTraceRows, - observerTraceRowCount: observerTraceRows, - terminalZeroElapsedDiff: controlZero !== observerZero, - messageTextDigestDiff, - controlMessageDigest, - observerMessageDigest - }); - } - return rows; -} - -function detectTraceMessageDuplication(samples) { - const rows = []; - for (const sample of samples) { - const finalText = normalizedText((Array.isArray(sample?.messages) ? sample.messages : []).map((item) => item?.textPreview || item?.text || "").join("\n")); - if (finalText.length < 40) continue; - const traceRows = Array.isArray(sample?.traceRows) ? sample.traceRows : []; - for (const row of traceRows) { - const rowTextRaw = String(row?.textPreview || row?.text || ""); - if (!/(?:助手消息|assistant\s+message|assistant)/iu.test(rowTextRaw)) continue; - const rowText = normalizedText(rowTextRaw); - if (rowText.length < 24) continue; - const overlap = longestSharedSubstringLength(finalText, rowText); - if (overlap < 40 && overlap < Math.min(rowText.length, finalText.length) * 0.55) continue; - rows.push({ ...ref(sample), traceId: row?.traceId ?? null, rowIndex: row?.index ?? null, rowTextPreview: limitText(rowTextRaw, 180) }); - } - } - return rows; -} - -function detectTurnTraceIdMissing(samples) { - const rows = []; - for (const sample of samples) { - for (const item of Array.isArray(sample?.turns) ? sample.turns : []) { - const text = [item?.status, item?.durationText, item?.activityText, item?.textPreview, item?.text].filter(Boolean).join("\n"); - if (!/(?:Code Agent|运行记录|耗时|最近)/iu.test(text)) continue; - if (item?.traceId) continue; - rows.push({ ...ref(sample), status: item?.status ?? null, messageId: item?.messageId ?? null, textPreview: limitText(item?.textPreview || item?.text || "", 180) }); - } - } - return rows; -} - -function visibleTraceIds(sample) { - const ids = new Set(); - for (const group of [sample?.turns, sample?.messages, sample?.traceRows]) { - if (!Array.isArray(group)) continue; - for (const item of group) if (item?.traceId) ids.add(String(item.traceId)); - } - return ids; -} - -function digestMessageTexts(sample) { - return sha256((Array.isArray(sample?.messages) ? sample.messages : []).map((item) => item?.textHash || normalizedText(item?.textPreview || item?.text || "")).join("|")); -} - -function normalizedText(value) { - return String(value || "").replace(/\s+/gu, " ").trim(); -} - -function longestSharedSubstringLength(a, b) { - if (!a || !b) return 0; - const left = a.length <= b.length ? a : b; - const right = a.length <= b.length ? b : a; - const max = Math.min(left.length, 280); - let best = 0; - for (let start = 0; start < max; start += 1) { - for (let end = Math.min(max, start + 160); end > start + best; end -= 1) { - if (right.includes(left.slice(start, end))) { - best = end - start; - break; - } - } - } - return best; -} - -function digestSample(sample) { - const messages = Array.isArray(sample.messages) ? sample.messages.map((item) => stableDigestItem(item, ["dataRole", "role", "status", "sessionId", "messageId", "traceId", "turnId"])).join("|") : ""; - const trace = Array.isArray(sample.traceRows) ? sample.traceRows.map((item) => stableDigestItem(item, ["status", "sessionId", "messageId", "traceId", "turnId", "projectedSeq", "sourceSeq", "eventSeq", "eventKind"])).join("|") : ""; - const diagnostics = Array.isArray(sample.diagnostics) ? sample.diagnostics.map((item) => stableDigestItem(item, ["className", "status", "sessionId", "messageId", "traceId", "turnId", "diagnosticCode"])).join("|") : ""; - return sha256((sample.routeSessionId || "") + "|" + (sample.activeSessionId || "") + "|" + messages + "|" + trace + "|" + diagnostics); -} - -function samplePageKey(sample) { - return String(sample?.pageRole || "control") + ":" + String(sample?.pageId || "default"); -} - -function stableDigestItem(item, fields) { - if (!item || typeof item !== "object") return ""; - const identity = fields.map((field) => String(item?.[field] ?? "")).join(":"); - return identity + ":" + stableVisibleDigestText(item?.textPreview || item?.text || item?.textHash || ""); -} - -function stableVisibleDigestText(value) { - return normalizedText(value) - .replace(/\btotal=\d{1,2}:\d{2}:\d{2}\b/giu, "total=") - .replace(/(?:总耗时|耗时)\s*[=::]?\s*(?:(?:\d+\s*天)?\s*(?:\d+\s*小时)?\s*(?:\d+\s*(?:分钟|分))?\s*(?:\d+\s*秒)?|\d{1,2}:\d{2}(?::\d{2})?)/giu, "耗时 ") - .replace(/最近\s*(?:(?:\d+\s*天)?\s*(?:\d+\s*小时)?\s*(?:\d+\s*(?:分钟|分))?\s*(?:\d+\s*秒)?)\s*前/giu, "最近 前"); -} - -function nearCommand(sample, commandTimes, windowMs) { - const ts = Date.parse(sample.ts); - return Number.isFinite(ts) && commandTimes.some((item) => Math.abs(ts - item) <= windowMs); -} - -function sampleRefs(samples, pick) { - const seen = new Set(); - const refs = []; - for (const sample of samples) { - const value = pick(sample); - if (!value || seen.has(value)) continue; - seen.add(value); - refs.push({ ...ref(sample), value }); - } - return refs.slice(0, 20); -} - -function ref(sample) { - if (!sample) return null; - return { seq: sample.seq ?? null, sampleGroupSeq: sample.sampleGroupSeq ?? null, ts: sample.ts ?? null, pageRole: sample.pageRole ?? null, pageId: sample.pageId ?? null, url: sample.url ?? null, routeSessionId: sample.routeSessionId ?? null, activeSessionId: sample.activeSessionId ?? null }; -} - -async function artifactSummary(artifacts) { - const items = artifacts.slice(-30).map((item) => ({ kind: item.kind, reason: item.reason, sampleSeq: item.sampleSeq, path: item.path, sha256: item.sha256, byteCount: item.byteCount })); - return { count: artifacts.length, latest: items }; -} - -function compactManifest(value) { - if (!value) return null; - return { jobId: value.jobId, stateDir: value.stateDir, baseUrl: value.baseUrl, targetPath: value.targetPath, startedAt: value.startedAt, status: value.status, pageAuthority: value.pageAuthority ?? null, sampling: value.sampling, pageProvenance: value.pageProvenance ?? null, safety: value.safety }; -} - -function compactHeartbeat(value) { - if (!value) return null; - return { jobId: value.jobId, pid: value.pid, status: value.status, sampleSeq: value.sampleSeq, commandSeq: value.commandSeq, pageId: value.pageId ?? null, observerPageId: value.observerPageId ?? null, currentUrl: value.currentUrl, observerUrl: value.observerUrl ?? null, observerRefreshIntervalMs: value.observerRefreshIntervalMs ?? null, lastObserverRefreshAt: value.lastObserverRefreshAt ?? null, pageProvenance: value.pageProvenance ?? null, updatedAt: value.updatedAt, uptimeMs: value.uptimeMs }; -} - -function renderTurnTimingTable(sampleMetrics) { - const columns = Array.isArray(sampleMetrics?.turnColumns) ? sampleMetrics.turnColumns : []; - const rows = Array.isArray(sampleMetrics?.turnTimingTable) ? sampleMetrics.turnTimingTable : []; - const disclosure = sampleMetrics?.turnTimingTableDisclosure || null; - if (columns.length === 0 || rows.length === 0) return "- 无 turn 时间表。"; - const header = ["时间戳"]; - for (const column of columns) { - const columnLabel = formatTurnColumnDisplayLabel(column); - header.push(columnLabel + " 总耗时(s)"); - header.push(columnLabel + " 最近更新(s)"); - } - const lines = []; - lines.push("| " + header.map(escapeMarkdownCell).join(" | ") + " |"); - lines.push("| " + header.map(() => "---").join(" | ") + " |"); - for (const row of rows) { - const cells = [row.ts || "-"]; - for (const column of columns) { - const cell = row.cells?.[column.id] || {}; - cells.push(formatMetricCell(cell.totalElapsedSeconds)); - cells.push(formatMetricCell(cell.recentUpdateSeconds)); - } - lines.push("| " + cells.map(escapeMarkdownCell).join(" | ") + " |"); - } - const columnLines = columns.map((column) => "- " + formatTurnColumnDisplayLabel(column) + ": pageRole=" + (column.pageRole || "-") + " pageId=" + (column.pageId || "-") + " source=" + (column.source || "-") + " prompt=" + (column.promptIndex ?? "-") + " lastPrompt=" + (column.lastPromptIndex ?? "-") + " firstSeq=" + (column.firstSeq ?? "-") + " lastSeq=" + (column.lastSeq ?? "-") + " traceId=" + (column.traceId || "-") + " messageId=" + (column.messageId || "-")).join("\n"); - const nonMonotonic = Array.isArray(sampleMetrics?.turnTimingNonMonotonic) ? sampleMetrics.turnTimingNonMonotonic : []; - const nonMonotonicLines = nonMonotonic.length > 0 - ? nonMonotonic.slice(0, 80).map((item) => "- " + formatTurnEventDisplayLabel(item) + " " + item.metric + (item.anomaly ? " " + item.anomaly : "") + " " + (item.fromValue ?? "-") + " -> " + (item.toValue ?? "-") + " delta=" + (item.delta ?? "-") + " sampleDelta=" + (item.sampleDeltaSeconds ?? "-") + " allowed=" + (item.allowedIncreaseSeconds ?? "-") + " seq " + (item.fromSeq ?? "-") + " -> " + (item.toSeq ?? "-") + " ts " + (item.fromTs || "-") + " -> " + (item.toTs || "-") + " traceId=" + (item.traceId || "-")).join("\n") - : "- 未观察到总耗时下降或最近更新异常跳增。"; - const terminalGrowth = Array.isArray(sampleMetrics?.turnTimingTerminalElapsedGrowth) ? sampleMetrics.turnTimingTerminalElapsedGrowth : []; - const terminalGrowthLines = terminalGrowth.length > 0 - ? terminalGrowth.slice(0, 80).map((item) => "- " + formatTurnEventDisplayLabel(item) + " terminal totalElapsed growth " + (item.fromValue ?? "-") + " -> " + (item.toValue ?? "-") + " delta=" + (item.delta ?? "-") + " status " + (item.fromStatus || "-") + " -> " + (item.toStatus || "-") + " seq " + (item.fromSeq ?? "-") + " -> " + (item.toSeq ?? "-") + " ts " + (item.fromTs || "-") + " -> " + (item.toTs || "-") + " traceId=" + (item.traceId || "-")).join("\n") - : "- 未观察到 terminal 后总耗时增长。"; - const elapsedZeroResets = Array.isArray(sampleMetrics?.turnTimingElapsedZeroResets) ? sampleMetrics.turnTimingElapsedZeroResets : []; - const elapsedZeroResetLines = elapsedZeroResets.length > 0 - ? elapsedZeroResets.slice(0, 80).map((item) => "- " + formatTurnEventDisplayLabel(item) + " totalElapsed zero-reset " + (item.fromValue ?? "-") + " -> " + (item.toValue ?? "-") + " delta=" + (item.delta ?? "-") + " seq " + (item.fromSeq ?? "-") + " -> " + (item.toSeq ?? "-") + " ts " + (item.fromTs || "-") + " -> " + (item.toTs || "-") + " traceId=" + (item.traceId || "-")).join("\n") - : "- 未观察到总耗时从非零跳回 0 秒。"; - const totalElapsedForwardJumps = Array.isArray(sampleMetrics?.turnTimingTotalElapsedForwardJumps) ? sampleMetrics.turnTimingTotalElapsedForwardJumps : []; - const totalElapsedForwardJumpLines = totalElapsedForwardJumps.length > 0 - ? totalElapsedForwardJumps.slice(0, 80).map((item) => "- " + formatTurnEventDisplayLabel(item) + " totalElapsed forward-jump " + (item.fromValue ?? "-") + " -> " + (item.toValue ?? "-") + " delta=" + (item.delta ?? "-") + " sampleDelta=" + (item.sampleDeltaSeconds ?? "-") + " allowed=" + (item.allowedIncreaseSeconds ?? "-") + " seq " + (item.fromSeq ?? "-") + " -> " + (item.toSeq ?? "-") + " ts " + (item.fromTs || "-") + " -> " + (item.toTs || "-") + " traceId=" + (item.traceId || "-")).join("\n") - : "- 未观察到总耗时超出采样间隔的异常前跳。"; - const sawtoothJumps = Array.isArray(sampleMetrics?.turnTimingRecentUpdateSawtoothJumps) - ? sampleMetrics.turnTimingRecentUpdateSawtoothJumps - : nonMonotonic.filter((item) => item.metric === "recentUpdateSeconds" && item.anomaly === "jump"); - const sawtoothJumpLines = sawtoothJumps.length > 0 - ? sawtoothJumps.slice(0, 80).map((item) => "- " + formatTurnEventDisplayLabel(item) + " recentUpdate sawtooth-jump " + (item.fromValue ?? "-") + " -> " + (item.toValue ?? "-") + " delta=" + (item.delta ?? "-") + " sampleDelta=" + (item.sampleDeltaSeconds ?? "-") + " allowed=" + (item.allowedIncreaseSeconds ?? "-") + " seq " + (item.fromSeq ?? "-") + " -> " + (item.toSeq ?? "-") + " ts " + (item.fromTs || "-") + " -> " + (item.toTs || "-") + " traceId=" + (item.traceId || "-")).join("\n") - : "- 未观察到最近更新三角波异常跳增。"; - const recentUpdateSteps = Array.isArray(sampleMetrics?.turnTimingRecentUpdateLargestSteps) - ? sampleMetrics.turnTimingRecentUpdateLargestSteps - : Array.isArray(sampleMetrics?.turnTimingRecentUpdateSteps) - ? sampleMetrics.turnTimingRecentUpdateSteps.filter((item) => Number.isFinite(Number(item.delta))).slice().sort((a, b) => Number(b.delta) - Number(a.delta)).slice(0, 200) - : []; - const stepLines = recentUpdateSteps.length > 0 - ? recentUpdateSteps.slice(0, 80).map((item) => "- " + formatTurnEventDisplayLabel(item) + " recentUpdate step " + (item.fromValue ?? "-") + " -> " + (item.toValue ?? "-") + " delta=" + (item.delta ?? "-") + " sampleDelta=" + (item.sampleDeltaSeconds ?? "-") + " allowed=" + (item.allowedIncreaseSeconds ?? "-") + " excess=" + (item.excessiveIncreaseSeconds ?? 0) + " event=" + (item.event || "-") + " seq " + (item.fromSeq ?? "-") + " -> " + (item.toSeq ?? "-") + " ts " + (item.fromTs || "-") + " -> " + (item.toTs || "-") + " traceId=" + (item.traceId || "-")).join("\n") - : "- 未观察到最近更新相邻采样 step。"; - const recentUpdateResets = Array.isArray(sampleMetrics?.turnTimingRecentUpdateResets) ? sampleMetrics.turnTimingRecentUpdateResets : []; - const resetLines = recentUpdateResets.length > 0 - ? recentUpdateResets.slice(0, 80).map((item) => "- " + formatTurnEventDisplayLabel(item) + " reset " + (item.fromValue ?? "-") + " -> " + (item.toValue ?? "-") + " delta=" + (item.delta ?? "-") + " sampleDelta=" + (item.sampleDeltaSeconds ?? "-") + " seq " + (item.fromSeq ?? "-") + " -> " + (item.toSeq ?? "-") + " ts " + (item.fromTs || "-") + " -> " + (item.toTs || "-") + " traceId=" + (item.traceId || "-")).join("\n") - : "- 未观察到最近更新归零/回落。"; - const disclosureLine = disclosure?.truncated - ? "表格披露:已按 head/tail 有界输出,totalRows=" + disclosure.totalRows + " includedRows=" + disclosure.includedRows + " omittedRows=" + disclosure.omittedRows + ";异常计数在截断前基于全量采样计算。" - : "表格披露:完整输出当前分析窗口的采样点。"; - return disclosureLine + "\n\n" + lines.join("\n") + "\n\n列说明:\n" + columnLines + "\n\n异常事件(仅报表暴露,不做下游 repair;最近更新按三角波模型检测异常跳增):\n" + nonMonotonicLines + "\n\nTerminal 后总耗时增长事件(终态 turn 的总耗时应 sealed,不应继续增长):\n" + terminalGrowthLines + "\n\n总耗时归零跳变事件(例如已显示真实耗时后又变成 0 秒):\n" + elapsedZeroResetLines + "\n\n总耗时异常前跳事件(预期按采样间隔近似递增;例如 14 秒 -> 1137 秒应被列入这里):\n" + totalElapsedForwardJumpLines + "\n\n最近更新 sawtooth jump 事件(预期每秒增长约 1,遇到新活动归零;例如 1 秒 -> 1 分 4 秒应被列入这里):\n" + sawtoothJumpLines + "\n\n最近更新相邻采样 step(按 delta 降序;用于人工识别一秒跳几十秒/一分钟的瞬态):\n" + stepLines + "\n\n最近更新 reset 事件(预期三角波归零,不计为异常):\n" + resetLines; -} - -function formatTurnColumnDisplayLabel(column) { - const base = String(column?.label || column?.id || "-"); - const role = String(column?.pageRole || "unknown"); - const pageId = compactPageId(column?.pageId); - return pageId ? base + "@" + role + "/" + pageId : base + "@" + role; -} - -function formatTurnEventDisplayLabel(item) { - const base = String(item?.columnLabel || item?.columnId || "-"); - const role = String(item?.pageRole || "unknown"); - const pageId = compactPageId(item?.pageId); - return pageId ? base + "@" + role + "/" + pageId : base + "@" + role; -} - -function compactPageId(value) { - if (value === null || value === undefined || value === "") return ""; - const text = String(value); - if (text.length <= 18) return text; - return text.slice(0, 12) + ".." + text.slice(-4); -} - -function formatMetricCell(value) { - if (value === null || value === undefined || !Number.isFinite(Number(value))) return "-"; - return String(Number(value)); -} - -function escapeMarkdownCell(value) { - return String(value ?? "-").replace(/\|/gu, "\\|"); -} - -function renderMarkdown(report) { - const findingLines = report.findings.length === 0 ? "- 无红灯项。" : report.findings.map((item) => "- " + item.severity + ": " + item.id + " - " + item.summary).join("\n"); - const commandLines = report.commandTimeline.length === 0 ? "- 无控制命令。" : report.commandTimeline.map((item) => "- " + item.ts + " " + item.phase + " " + item.type + " " + item.commandId + " " + (item.afterUrl || "")).join("\n"); - const commandFailureLines = Array.isArray(report.commandFailures) && report.commandFailures.length > 0 - ? report.commandFailures.slice(0, 80).map((item) => "- " + (item.ts || "-") + " type=" + (item.type || "-") + " commandId=" + (item.commandId || "-") + " durationMs=" + (item.durationMs ?? "-") + " sampleSeq=" + (item.sampleSeq ?? "-") + " path=" + (item.beforePath || "-") + "->" + (item.afterPath || "-") + " message=" + escapeMarkdownCell(item.message || item.failureKind || item.name || "-")).join("\n") - : "- 无失败控制命令。"; - const transitionLines = report.transitions.length === 0 ? "- 无状态变化。" : report.transitions.slice(0, 80).map((item) => "- #" + item.seq + " " + item.ts + " messages=" + item.messageCount + " traceRows=" + item.traceRowCount + " route=" + (item.routeSessionId || "-") + " active=" + (item.activeSessionId || "-")).join("\n"); - const metricSummary = report.sampleMetrics?.summary || {}; - const loading = report.sampleMetrics?.loading || {}; - const loadingSummary = loading.summary || {}; - const sessionRailTitles = report.sampleMetrics?.sessionRailTitles || {}; - const sessionRailTitleSummary = sessionRailTitles.summary || {}; - const codeAgentCardTiming = report.sampleMetrics?.codeAgentCardTiming || {}; - const codeAgentCardTimingSummary = codeAgentCardTiming.summary || {}; - const roundCompletion = codeAgentCardTiming.roundCompletion || {}; - const traceOrder = report.sampleMetrics?.traceOrder || {}; - const traceOrderSummary = traceOrder.summary || {}; - const alertSummary = report.runtimeAlerts?.summary || {}; - const httpAlertLines = Array.isArray(report.runtimeAlerts?.networkHttpErrorsByPath) && report.runtimeAlerts.networkHttpErrorsByPath.length > 0 - ? report.runtimeAlerts.networkHttpErrorsByPath.slice(0, 40).map((item) => "- HTTP " + (item.status ?? "-") + " " + item.method + " " + item.urlPath + " count=" + item.count + " prompts=" + (item.promptIndexes?.join(",") || "-") + " first=" + (item.firstAt || "-") + " last=" + (item.lastAt || "-")).join("\n") - : "- 无 HTTP 错误。"; - const requestFailedLines = Array.isArray(report.runtimeAlerts?.networkRequestFailedByPath) && report.runtimeAlerts.networkRequestFailedByPath.length > 0 - ? report.runtimeAlerts.networkRequestFailedByPath.slice(0, 40).map((item) => "- requestfailed " + item.method + " " + item.urlPath + " count=" + item.count + " failure=" + (item.failureKinds?.slice(0, 4).join(",") || "-") + " prompts=" + (item.promptIndexes?.join(",") || "-") + " first=" + (item.firstAt || "-") + " last=" + (item.lastAt || "-")).join("\n") - : "- 无 requestfailed。"; - const domDiagnosticLines = Array.isArray(report.runtimeAlerts?.domDiagnostics) && report.runtimeAlerts.domDiagnostics.length > 0 - ? report.runtimeAlerts.domDiagnostics.slice(0, 40).map((item) => "- #" + (item.seq ?? "-") + " " + (item.ts || "-") + " prompt=" + (item.promptIndex ?? "-") + " source=" + (item.source || "-") + " code=" + (item.diagnosticCode || "-") + " traceId=" + (item.traceId || "-") + " http=" + (item.httpStatus ?? "-") + " idle=" + (item.idleSeconds ?? "-") + " waitingFor=" + (item.waitingFor || "-") + " lastEventLabel=" + (item.lastEventLabel || "-") + " textHash=" + (item.textHash || "-") + " preview=" + escapeMarkdownCell(item.preview || "")).join("\n") - : "- 无 DOM 诊断文本。"; - const consoleAlertLines = Array.isArray(report.runtimeAlerts?.consoleAlerts) && report.runtimeAlerts.consoleAlerts.length > 0 - ? report.runtimeAlerts.consoleAlerts.slice(0, 40).map((item) => "- " + (item.ts || "-") + " prompt=" + (item.promptIndex ?? "-") + " type=" + (item.type || "-") + " status=" + (item.status ?? "-") + " path=" + (item.urlPath || "-") + " traceId=" + (item.traceId || "-") + " textHash=" + (item.textHash || "-") + " preview=" + escapeMarkdownCell(item.preview || "")).join("\n") - : "- 无 console warning/error。"; - const consoleAlertGroupLines = Array.isArray(report.runtimeAlerts?.consoleAlertsByPath) && report.runtimeAlerts.consoleAlertsByPath.length > 0 - ? report.runtimeAlerts.consoleAlertsByPath.slice(0, 40).map((item) => "- console " + (item.type || "-") + " status=" + (item.status ?? "-") + " path=" + (item.urlPath || "-") + " count=" + item.count + " prompts=" + (item.promptIndexes?.join(",") || "-") + " traces=" + (item.traceIds?.slice(0, 6).join(",") || "-")).join("\n") - : "- 无 console 分组。"; - const promptNetworkLines = Array.isArray(report.promptNetwork?.rounds) && report.promptNetwork.rounds.length > 0 - ? report.promptNetwork.rounds.map((item) => "- round " + item.promptIndex + " promptHash=" + (item.promptTextHash || "-") + " chatPostOk=" + String(item.chatPostOk) + " modes=" + (Array.isArray(item.submitModes) && item.submitModes.length > 0 ? item.submitModes.join(",") : "-") + " failure=" + (item.failureKind || "-") + " statuses=" + (Array.isArray(item.responseStatuses) && item.responseStatuses.length > 0 ? item.responseStatuses.join(",") : "-") + " firstChat=" + (item.firstChatEventAt || "-") + " lastChat=" + (item.lastChatEventAt || "-")).join("\n") - : "- 无 prompt 网络记录。"; - const roundLines = Array.isArray(report.sampleMetrics?.rounds) && report.sampleMetrics.rounds.length > 0 - ? report.sampleMetrics.rounds.map((item) => "- round " + item.promptIndex + " promptHash=" + (item.promptTextHash || "-") + " samples=" + item.sampleCount + " loadingSamples=" + (item.loadingSamples ?? 0) + " maxLoading=" + (item.maxLoadingCount ?? 0) + " loadingOwners=" + (item.loadingOwnerCount ?? 0) + " totalMax=" + (item.maxTotalElapsedSeconds ?? "-") + " totalLast=" + (item.lastTotalElapsedSeconds ?? "-") + " recentMax=" + (item.maxRecentUpdateSeconds ?? "-") + " recentLast=" + (item.lastRecentUpdateSeconds ?? "-") + " totalDecrease=" + (item.turnTimingTotalElapsedDecreaseCount ?? 0) + " totalForwardJump=" + (item.turnTimingTotalElapsedForwardJumpCount ?? 0) + " totalForwardJumpMax=" + (item.turnTimingTotalElapsedForwardJumpMaxSeconds ?? 0) + " terminalGrowth=" + (item.turnTimingTerminalElapsedGrowthCount ?? 0) + " terminalGrowthMax=" + (item.turnTimingTerminalElapsedGrowthMaxSeconds ?? 0) + " recentJump=" + (item.turnTimingRecentUpdateJumpCount ?? 0) + " recentSawtoothJump=" + (item.turnTimingRecentUpdateSawtoothJumpCount ?? item.turnTimingRecentUpdateJumpCount ?? 0) + " recentStep=" + (item.turnTimingRecentUpdateStepCount ?? 0) + " recentMaxIncrease=" + (item.turnTimingRecentUpdateMaxIncreaseSeconds ?? "-") + " recentMaxExcess=" + (item.turnTimingRecentUpdateMaxExcessSeconds ?? 0) + " recentReset=" + (item.turnTimingRecentUpdateResetCount ?? 0) + " diagnostics=" + item.diagnosticSamples + " terminal=" + item.terminalSamples + " finalText=" + item.finalTextSamples).join("\n") - : "- 无轮次指标。"; - const cardMissingElapsedLines = Array.isArray(codeAgentCardTiming.missingElapsed) && codeAgentCardTiming.missingElapsed.length > 0 - ? codeAgentCardTiming.missingElapsed.slice(0, 80).map((item) => "- #" + (item.seq ?? "-") + " " + (item.ts || "-") + " prompt=" + (item.promptIndex ?? "-") + " role=" + (item.pageRole || "-") + " status=" + (item.status || "-") + " traceId=" + (item.traceId || "-") + " messageId=" + (item.messageId || "-") + " preview=" + escapeMarkdownCell(item.textPreview || "")).join("\n") - : "- 未观察到 Code Agent 卡片缺少耗时。"; - const cardMissingRecentLines = Array.isArray(codeAgentCardTiming.missingRecentUpdate) && codeAgentCardTiming.missingRecentUpdate.length > 0 - ? codeAgentCardTiming.missingRecentUpdate.slice(0, 80).map((item) => "- #" + (item.seq ?? "-") + " " + (item.ts || "-") + " prompt=" + (item.promptIndex ?? "-") + " role=" + (item.pageRole || "-") + " status=" + (item.status || "-") + " traceId=" + (item.traceId || "-") + " messageId=" + (item.messageId || "-") + " total=" + (item.totalElapsedSeconds ?? "-") + " preview=" + escapeMarkdownCell(item.textPreview || "")).join("\n") - : "- 未观察到未终态 Code Agent 卡片缺少最近更新。"; - const roundCompletionLines = Array.isArray(roundCompletion.events) && roundCompletion.events.length > 0 - ? roundCompletion.events.slice(0, 80).map((item) => "- #" + (item.seq ?? "-") + " " + (item.ts || "-") + " prompt=" + (item.promptIndex ?? "-") + " role=" + (item.pageRole || "-") + " traceId=" + (item.traceId || "-") + " completionElapsed=" + (item.elapsedSeconds ?? "-") + " preview=" + escapeMarkdownCell(item.preview || "")).join("\n") - : "- 未观察到“轮次完成(总耗时 ...)”trace 行。"; - const roundCompletionMismatchLines = Array.isArray(roundCompletion.elapsedMismatches) && roundCompletion.elapsedMismatches.length > 0 - ? roundCompletion.elapsedMismatches.slice(0, 80).map((item) => "- #" + (item.seq ?? "-") + " " + (item.ts || "-") + " prompt=" + (item.promptIndex ?? "-") + " traceId=" + (item.traceId || "-") + " completion=" + (item.completionElapsedSeconds ?? "-") + " card=" + (item.cardTotalElapsedSeconds ?? "-") + " delta=" + (item.deltaSeconds ?? "-") + " tolerance=" + (item.toleranceSeconds ?? "-")).join("\n") - : "- 未观察到轮次完成耗时与卡片耗时不一致。"; - const roundCompletionFinalMissingLines = Array.isArray(roundCompletion.finalResponseMissing) && roundCompletion.finalResponseMissing.length > 0 - ? roundCompletion.finalResponseMissing.slice(0, 80).map((item) => "- #" + (item.seq ?? "-") + " " + (item.ts || "-") + " prompt=" + (item.promptIndex ?? "-") + " role=" + (item.pageRole || "-") + " traceId=" + (item.traceId || "-") + " completionElapsed=" + (item.completionElapsedSeconds ?? "-")).join("\n") - : "- 未观察到轮次完成后 final response 缺失。"; - const roundCompletionPostTimingLines = Array.isArray(roundCompletion.postCompletionTimingChanges) && roundCompletion.postCompletionTimingChanges.length > 0 - ? roundCompletion.postCompletionTimingChanges.slice(0, 80).map((item) => "- " + (item.columnLabel || item.columnId || "-") + " " + (item.metric || "-") + " " + (item.fromValue ?? "-") + " -> " + (item.toValue ?? "-") + " delta=" + (item.delta ?? "-") + " completionSeq=" + (item.seq ?? "-") + " seq " + (item.fromSeq ?? "-") + " -> " + (item.toSeq ?? "-") + " traceId=" + (item.traceId || "-")).join("\n") - : "- 未观察到轮次完成后耗时/最近更新继续变化。"; - const durationUnderreportedLines = Array.isArray(codeAgentCardTiming.durationUnderreported) && codeAgentCardTiming.durationUnderreported.length > 0 - ? codeAgentCardTiming.durationUnderreported.slice(0, 80).map((item) => "- sample=" + (item.sampleIndex ?? "-") + " " + (item.timestamp || "-") + " role=" + (item.pageRole || "-") + " status=" + (item.status || "-") + " traceId=" + (item.traceId || "-") + " card=" + (item.cardTotalElapsedSeconds ?? "-") + "s expected=" + (item.expectedElapsedSeconds ?? "-") + "s delta=" + (item.deltaSeconds ?? "-") + "s evidence=" + (item.evidenceKind || "-") + " preview=" + escapeMarkdownCell(item.evidencePreview || item.cardPreview || "")).join("\n") - : "- 未观察到 Code Agent 卡片耗时低于 trace/final-response 证据。"; - const durationMismatchLines = Array.isArray(codeAgentCardTiming.durationMismatches) && codeAgentCardTiming.durationMismatches.length > 0 - ? codeAgentCardTiming.durationMismatches.slice(0, 80).map((item) => "- sample=" + (item.sampleIndex ?? "-") + " " + (item.timestamp || "-") + " role=" + (item.pageRole || "-") + " status=" + (item.status || "-") + " traceId=" + (item.traceId || "-") + " direction=" + (item.direction || "-") + " card=" + (item.cardTotalElapsedSeconds ?? "-") + "s expected=" + (item.expectedElapsedSeconds ?? "-") + "s signedDelta=" + (item.signedDeltaSeconds ?? "-") + "s delta=" + (item.deltaSeconds ?? "-") + "s tolerance=" + (item.toleranceSeconds ?? "-") + "s evidence=" + (item.evidenceKind || "-") + " exact=" + String(item.exactEvidence === true) + " preview=" + escapeMarkdownCell(item.evidencePreview || item.cardPreview || "")).join("\n") - : "- 未观察到 Code Agent 卡片耗时与 completion/final-response 封口证据不一致。"; - const traceOrderAnomalyLines = Array.isArray(traceOrder.orderAnomalies) && traceOrder.orderAnomalies.length > 0 - ? traceOrder.orderAnomalies.slice(0, 80).map((item) => "- sample=" + (item.sampleIndex ?? "-") + " " + (item.timestamp || "-") + " role=" + (item.pageRole || "-") + " traceId=" + (item.traceId || "-") + " rows=" + (item.previousRowIndex ?? "-") + "->" + (item.currentRowIndex ?? "-") + " reasons=" + (Array.isArray(item.reasons) ? item.reasons.join(",") : "-") + " total=" + (item.previousTotalSeconds ?? "-") + "->" + (item.currentTotalSeconds ?? "-") + " clock=" + (item.previousClockSeconds ?? "-") + "->" + (item.currentClockSeconds ?? "-") + " preview=" + escapeMarkdownCell((item.previousPreview || "") + " / " + (item.currentPreview || ""))).join("\n") - : "- 未观察到可见 trace 行顺序非单调。"; - const traceCompletionNotLastLines = Array.isArray(traceOrder.completionNotLast) && traceOrder.completionNotLast.length > 0 - ? traceOrder.completionNotLast.slice(0, 80).map((item) => "- sample=" + (item.sampleIndex ?? "-") + " " + (item.timestamp || "-") + " role=" + (item.pageRole || "-") + " traceId=" + (item.traceId || "-") + " rows=" + (item.completionRowIndex ?? "-") + "->" + (item.laterRowIndex ?? "-") + " total=" + (item.completionTotalSeconds ?? "-") + "->" + (item.laterTotalSeconds ?? "-") + " completion=" + escapeMarkdownCell(item.completionPreview || "") + " later=" + escapeMarkdownCell(item.laterPreview || "")).join("\n") - : "- 未观察到 completion 行后还有同 trace 后续行。"; - const loadingSegmentLines = Array.isArray(loading.segments) && loading.segments.length > 0 - ? loading.segments.slice(0, 80).map((item) => "- observedDuration=" + (item.durationSeconds ?? 0) + "s upperBound=" + (item.upperBoundSeconds ?? item.durationSeconds ?? 0) + "s endedGap=" + (item.endedGapSeconds ?? "-") + "s samples=" + (item.sampleCount ?? 0) + " countMax=" + (item.maxCount ?? 0) + " owners=" + (item.ownerCount ?? 0) + " seq=" + (item.firstSeq ?? "-") + ".." + (item.lastSeq ?? "-") + " ts=" + (item.firstAt || "-") + ".." + (item.lastAt || "-") + " endedAt=" + (item.endedAt || (item.ongoing ? "ongoing" : "-")) + " ownerLabels=" + ((Array.isArray(item.owners) ? item.owners : []).slice(0, 6).map((owner) => (owner.ownerKind || "-") + ":" + (owner.ownerLabel || "-") + "x" + (owner.count ?? 0)).join(",") || "-")).join("\n") - : "- 未观察到“加载中”可见区间。"; - const loadingOwnerLines = Array.isArray(loading.owners) && loading.owners.length > 0 - ? loading.owners.slice(0, 80).map((item) => "- " + (item.ownerKind || "-") + " " + escapeMarkdownCell(item.ownerLabel || item.ownerKey || "-") + " traceId=" + (item.ownerTraceId || "-") + " messageId=" + (item.ownerMessageId || "-") + " sessionId=" + (item.ownerSessionId || "-") + " samples=" + (item.sampleCount ?? 0) + " occurrences=" + (item.occurrenceCount ?? 0) + " maxCount=" + (item.maxSimultaneousCount ?? 0) + " longest=" + (item.longestContinuousSeconds ?? 0) + "s seq=" + (item.firstSeq ?? "-") + ".." + (item.lastSeq ?? "-") + " prompts=" + (Array.isArray(item.promptIndexes) ? item.promptIndexes.join(",") : "-")).join("\n") - : "- 未观察到“加载中”归属。"; - const loadingTimelineLines = Array.isArray(loading.timeline) && loading.timeline.length > 0 - ? loading.timeline.slice(0, 160).map((item) => "- #" + (item.seq ?? "-") + " " + (item.ts || "-") + " prompt=" + (item.promptIndex ?? "-") + " loadingCount=" + (item.loadingCount ?? 0) + " ownerCount=" + (item.ownerCount ?? 0) + " owners=" + ((Array.isArray(item.owners) ? item.owners : []).slice(0, 6).map((owner) => (owner.ownerKind || "-") + ":" + (owner.ownerLabel || "-") + " trace=" + (owner.ownerTraceId || "-") + "x" + (owner.count ?? 0)).join(",") || "-")).join("\n") - : "- 未观察到“加载中”采样点。"; - const sessionRailTitleSampleLines = Array.isArray(sessionRailTitles.samples) && sessionRailTitles.samples.length > 0 - ? sessionRailTitles.samples.slice(0, 80).map((item) => "- #" + (item.seq ?? "-") + " " + (item.ts || "-") + " role=" + (item.pageRole || "-") + " visible=" + (item.visibleCount ?? 0) + " fallback=" + (item.fallbackTitleCount ?? 0) + " ratio=" + (item.fallbackTitleRatio ?? 0) + " examples=" + ((Array.isArray(item.examples) ? item.examples : []).slice(0, 4).map((example) => escapeMarkdownCell(example.titlePreview || example.titleHash || "-")).join(",") || "-")).join("\n") - : "- 未观察到超过一半 fallback 的 session 列表采样点。"; - const sessionRailTitleExampleLines = Array.isArray(sessionRailTitles.examples) && sessionRailTitles.examples.length > 0 - ? sessionRailTitles.examples.slice(0, 80).map((item) => "- firstSeq=" + (item.firstSeq ?? "-") + " role=" + (item.pageRole || "-") + " active=" + String(item.active === true) + " sessionPrefix=" + (item.sessionIdPrefix || "-") + " titleHash=" + (item.titleHash || "-") + " preview=" + escapeMarkdownCell(item.titlePreview || "")).join("\n") - : "- 无 fallback 标题示例。"; - const provenanceLines = Array.isArray(report.pageProvenance?.segments) && report.pageProvenance.segments.length > 0 - ? report.pageProvenance.segments.slice(0, 40).map((item) => "- fingerprint=" + (item.assetFingerprint || "-") + " samples=" + item.sampleCount + " seq=" + (item.firstSeq ?? "-") + ".." + (item.lastSeq ?? "-") + " ts=" + (item.firstAt || "-") + ".." + (item.lastAt || "-") + " scripts=" + (item.scriptCount ?? "-") + " styles=" + (item.stylesheetCount ?? "-") + " urlPaths=" + (Array.isArray(item.urlPaths) ? item.urlPaths.slice(0, 4).join(",") : "-")).join("\n") - : "- 无页面 provenance segment。"; - const ordinaryPerformanceItems = Array.isArray(report.pagePerformance?.sameOriginApiByPath) ? report.pagePerformance.sameOriginApiByPath.filter((item) => item.isLongLivedStream !== true) : []; - const streamPerformanceItems = Array.isArray(report.pagePerformance?.sameOriginApiByPath) ? report.pagePerformance.sameOriginApiByPath.filter((item) => item.isLongLivedStream === true) : []; - const sameOriginApiBudgetMs = Number(report.alertThresholds?.sameOriginApiSlowMs ?? report.pagePerformance?.summary?.budgetMs); - const streamOpenBudgetMs = Number(report.alertThresholds?.longLivedStreamOpenSlowMs); - const performanceLines = ordinaryPerformanceItems.length > 0 - ? ordinaryPerformanceItems.slice(0, 80).map((item) => "- " + item.path + " kind=" + (item.routeKind || "same-origin-api") + " budgetMetric=" + (item.budgetMetric || "durationMs") + " samples=" + item.sampleCount + " p50=" + (item.p50Ms ?? "-") + "ms p75=" + (item.p75Ms ?? "-") + "ms p95=" + (item.p95Ms ?? "-") + "ms max=" + (item.maxMs ?? "-") + "ms >budget=" + (item.overBudgetCount ?? item.overFiveSecondCount ?? 0) + " budgetMs=" + (item.budgetMs ?? sameOriginApiBudgetMs) + " legacy>5s=" + (item.overFiveSecondCount ?? 0) + " window=" + (item.firstAt || "-") + ".." + (item.lastAt || "-")).join("\n") - : "- 无同源 API Resource Timing 样本。"; - const streamPerformanceLines = streamPerformanceItems.length > 0 - ? streamPerformanceItems.slice(0, 80).map((item) => "- " + item.path + " kind=" + (item.routeKind || "same-origin-api-stream") + " samples=" + item.sampleCount + " streamOpenP50=" + (item.streamOpenP50Ms ?? "-") + "ms streamOpenP75=" + (item.streamOpenP75Ms ?? "-") + "ms streamOpenP95=" + (item.streamOpenP95Ms ?? "-") + "ms streamOpenMax=" + (item.streamOpenMaxMs ?? "-") + "ms streamOpen>budget=" + (item.streamOpenOverBudgetCount ?? item.streamOpenOverFiveSecondCount ?? 0) + " streamOpenBudgetMs=" + (item.streamOpenBudgetMs ?? streamOpenBudgetMs) + " streamOpenLegacy>5s=" + (item.streamOpenOverFiveSecondCount ?? 0) + " streamLifetime>5s=" + (item.streamLifetimeOverFiveSecondCount ?? 0) + " lifetimeMax=" + (item.maxMs ?? "-") + "ms window=" + (item.firstAt || "-") + ".." + (item.lastAt || "-")).join("\n") - : "- 无同源长连接 Resource Timing 样本。"; - const metricLines = Array.isArray(report.sampleMetrics?.timeline) && report.sampleMetrics.timeline.length > 0 - ? report.sampleMetrics.timeline.slice(0, 120).map((item) => "- #" + item.seq + " " + item.ts + " prompt=" + item.promptIndex + " loadingCount=" + (item.loadingCount ?? 0) + " loadingOwners=" + (item.loadingOwnerCount ?? 0) + " totalElapsedSeconds=" + (item.totalElapsedSeconds ?? "-") + " recentUpdateSeconds=" + (item.recentUpdateSeconds ?? "-") + " terminal=" + item.terminalSeen + " finalText=" + item.finalResultTextSeen + " diagnostic=" + item.diagnosticSeen).join("\n") - : "- 无采样指标。"; - const turnTimingTable = renderTurnTimingTable(report.sampleMetrics); - return "# web-probe observe analysis\n\n" - + "- stateDir: " + report.stateDir + "\n" - + "- generatedAt: " + report.generatedAt + "\n" - + "- samples: " + report.counts.samples + "\n" - + "- control: " + report.counts.control + "\n" - + "- network: " + report.counts.network + "\n" - + "- console: " + (report.counts.console ?? 0) + "\n" - + "- errors: " + report.counts.errors + "\n\n" - + "## Findings\n\n" + findingLines + "\n\n" - + "## Command failures\n\n" + commandFailureLines + "\n\n" - + "## Sample metrics\n\n" - + "- sampleCount: " + (metricSummary.sampleCount ?? 0) + "\n" - + "- withTotalElapsed: " + (metricSummary.withTotalElapsed ?? 0) + "\n" - + "- withRecentUpdate: " + (metricSummary.withRecentUpdate ?? 0) + "\n" - + "- diagnostics: " + (metricSummary.diagnostics ?? 0) + "\n" - + "- loadingSamples: " + (metricSummary.loadingSampleCount ?? 0) + "\n" - + "- loadingMaxCount: " + (metricSummary.loadingMaxCount ?? 0) + "\n" - + "- loadingMaxOwnerCount: " + (metricSummary.loadingMaxOwnerCount ?? 0) + "\n" - + "- loadingOwnerCount: " + (metricSummary.loadingOwnerCount ?? 0) + "\n" - + "- loadingLongestContinuousSeconds: " + (metricSummary.loadingLongestContinuousSeconds ?? 0) + "\n" - + "- loadingCurrentContinuousSeconds: " + (metricSummary.loadingCurrentContinuousSeconds ?? 0) + "\n" - + "- loadingOverFiveSecondSegmentCount: " + (metricSummary.loadingOverFiveSecondSegmentCount ?? 0) + "\n" - + "- sessionRailFallbackMajoritySampleCount: " + (metricSummary.sessionRailFallbackMajoritySampleCount ?? 0) + "\n" - + "- sessionRailFallbackMaxRatio: " + (metricSummary.sessionRailFallbackMaxRatio ?? 0) + "\n" - + "- sessionRailFallbackMaxCount: " + (metricSummary.sessionRailFallbackMaxCount ?? 0) + "\n" - + "- promptSegments: " + (metricSummary.promptSegments ?? 0) + "\n\n" - + "- turnColumns: " + (metricSummary.turnColumns ?? 0) + "\n" - + "- turnTimingRows: " + (metricSummary.turnTimingRows ?? 0) + "\n" - + "- turnTimingNonMonotonicCount: " + (metricSummary.turnTimingNonMonotonicCount ?? 0) + "\n" - + "- turnTimingTotalElapsedDecreaseCount: " + (metricSummary.turnTimingTotalElapsedDecreaseCount ?? 0) + "\n" - + "- turnTimingTotalElapsedForwardJumpCount: " + (metricSummary.turnTimingTotalElapsedForwardJumpCount ?? 0) + "\n" - + "- turnTimingTotalElapsedForwardJumpMaxSeconds: " + (metricSummary.turnTimingTotalElapsedForwardJumpMaxSeconds ?? 0) + "\n" - + "- turnTimingTerminalElapsedGrowthCount: " + (metricSummary.turnTimingTerminalElapsedGrowthCount ?? 0) + "\n" - + "- turnTimingTerminalElapsedGrowthMaxSeconds: " + (metricSummary.turnTimingTerminalElapsedGrowthMaxSeconds ?? 0) + "\n" - + "- turnTimingRecentUpdateJumpCount: " + (metricSummary.turnTimingRecentUpdateJumpCount ?? 0) + "\n" - + "- turnTimingRecentUpdateSawtoothJumpCount: " + (metricSummary.turnTimingRecentUpdateSawtoothJumpCount ?? metricSummary.turnTimingRecentUpdateJumpCount ?? 0) + "\n" - + "- turnTimingRecentUpdateStepCount: " + (metricSummary.turnTimingRecentUpdateStepCount ?? 0) + "\n" - + "- turnTimingRecentUpdateMaxIncreaseSeconds: " + (metricSummary.turnTimingRecentUpdateMaxIncreaseSeconds ?? "-") + "\n" - + "- turnTimingRecentUpdateMaxExcessSeconds: " + (metricSummary.turnTimingRecentUpdateMaxExcessSeconds ?? 0) + "\n" - + "- turnTimingRecentUpdateResetCount: " + (metricSummary.turnTimingRecentUpdateResetCount ?? 0) + "\n\n" - + "- codeAgentCardSampleCount: " + (metricSummary.codeAgentCardSampleCount ?? 0) + "\n" - + "- codeAgentCardMissingElapsedCount: " + (metricSummary.codeAgentCardMissingElapsedCount ?? 0) + "\n" - + "- codeAgentCardMissingRecentUpdateCount: " + (metricSummary.codeAgentCardMissingRecentUpdateCount ?? 0) + "\n" - + "- codeAgentCardDurationUnderreportedCount: " + (metricSummary.codeAgentCardDurationUnderreportedCount ?? 0) + "\n" - + "- codeAgentCardDurationMismatchCount: " + (metricSummary.codeAgentCardDurationMismatchCount ?? 0) + "\n" - + "- traceRowCount: " + (metricSummary.traceRowCount ?? 0) + "\n" - + "- traceRowOrderAnomalyCount: " + (metricSummary.traceRowOrderAnomalyCount ?? 0) + "\n" - + "- traceRowCompletionNotLastCount: " + (metricSummary.traceRowCompletionNotLastCount ?? 0) + "\n" - + "- roundCompletionEventCount: " + (metricSummary.roundCompletionEventCount ?? 0) + "\n" - + "- roundCompletionElapsedMismatchCount: " + (metricSummary.roundCompletionElapsedMismatchCount ?? 0) + "\n" - + "- roundCompletionFinalResponseMissingCount: " + (metricSummary.roundCompletionFinalResponseMissingCount ?? 0) + "\n" - + "- roundCompletionPostTimingChangeCount: " + (metricSummary.roundCompletionPostTimingChangeCount ?? 0) + "\n\n" - + "### Rounds\n\n" + roundLines + "\n\n" - + "### Code Agent card timing display\n\n" - + "- cardSampleCount: " + (codeAgentCardTimingSummary.cardSampleCount ?? 0) + "\n" - + "- runningCardSampleCount: " + (codeAgentCardTimingSummary.runningCardSampleCount ?? 0) + "\n" - + "- terminalCardSampleCount: " + (codeAgentCardTimingSummary.terminalCardSampleCount ?? 0) + "\n" - + "- missingElapsedCount: " + (codeAgentCardTimingSummary.missingElapsedCount ?? 0) + "\n" - + "- missingRecentUpdateCount: " + (codeAgentCardTimingSummary.missingRecentUpdateCount ?? 0) + "\n" - + "- durationUnderreportedCount: " + (codeAgentCardTimingSummary.durationUnderreportedCount ?? 0) + "\n" - + "- durationMismatchCount: " + (codeAgentCardTimingSummary.durationMismatchCount ?? 0) + "\n" - + "- policy: Code Agent 卡片无论终态/非终态都必须显示耗时;非终态必须显示最近更新。该 analyzer 只报告采样到的页面表现,不做下游 repair。\n\n" - + "#### Missing elapsed samples\n\n" + cardMissingElapsedLines + "\n\n" - + "#### Missing recent update samples\n\n" + cardMissingRecentLines + "\n\n" - + "#### Duration underreported samples\n\n" + durationUnderreportedLines + "\n\n" - + "#### Duration mismatch samples\n\n" + durationMismatchLines + "\n\n" - + "### Trace row visual order\n\n" - + "- traceRowCount: " + (traceOrderSummary.traceRowCount ?? 0) + "\n" - + "- orderAnomalyCount: " + (traceOrderSummary.orderAnomalyCount ?? 0) + "\n" - + "- completionNotLastCount: " + (traceOrderSummary.completionNotLastCount ?? 0) + "\n" - + "- policy: 可见 trace 行在同一 trace 内必须按 total/时钟/projected seq 单调展示;completion 行不得出现在同 trace 后续行之前。\n\n" - + "#### Trace order anomalies\n\n" + traceOrderAnomalyLines + "\n\n" - + "#### Completion row not last samples\n\n" + traceCompletionNotLastLines + "\n\n" - + "### Round completion consistency\n\n" - + "- completionEventCount: " + (codeAgentCardTimingSummary.roundCompletionEventCount ?? 0) + "\n" - + "- elapsedMismatchCount: " + (codeAgentCardTimingSummary.roundCompletionElapsedMismatchCount ?? 0) + "\n" - + "- finalResponseMissingCount: " + (codeAgentCardTimingSummary.roundCompletionFinalResponseMissingCount ?? 0) + "\n" - + "- postTimingChangeCount: " + (codeAgentCardTimingSummary.roundCompletionPostTimingChangeCount ?? 0) + "\n" - + "- postRecentUpdateVisibleCount: " + (codeAgentCardTimingSummary.roundCompletionPostRecentUpdateVisibleCount ?? 0) + "\n" - + "- elapsedMismatchToleranceSeconds: " + (codeAgentCardTimingSummary.elapsedMismatchToleranceSeconds ?? "-") + "\n" - + "- policy: 轮次完成(总耗时 ...) 的耗时必须与卡片总耗时一致;完成后 final response 必须可见,耗时/最近更新不得继续跳变。\n\n" - + "#### Round completion events\n\n" + roundCompletionLines + "\n\n" - + "#### Completion elapsed mismatches\n\n" + roundCompletionMismatchLines + "\n\n" - + "#### Final response missing after completion\n\n" + roundCompletionFinalMissingLines + "\n\n" - + "#### Post-completion timing changes\n\n" + roundCompletionPostTimingLines + "\n\n" - + "### Loading visibility: visible 加载中\n\n" - + "- sampleCount: " + (loadingSummary.sampleCount ?? 0) + "\n" - + "- loadingSampleCount: " + (loadingSummary.loadingSampleCount ?? 0) + "\n" - + "- maxSimultaneousCount: " + (loadingSummary.maxSimultaneousCount ?? 0) + "\n" - + "- maxSimultaneousOwnerCount: " + (loadingSummary.maxSimultaneousOwnerCount ?? 0) + "\n" - + "- concurrentLoadingSampleCount: " + (loadingSummary.concurrentLoadingSampleCount ?? 0) + "\n" - + "- ownerCount: " + (loadingSummary.ownerCount ?? 0) + "\n" - + "- segmentCount: " + (loadingSummary.segmentCount ?? 0) + "\n" - + "- overFiveSecondSegmentCount: " + (loadingSummary.overFiveSecondSegmentCount ?? 0) + "\n" - + "- longestContinuousSeconds: " + (loadingSummary.longestContinuousSeconds ?? 0) + "\n" - + "- currentContinuousSeconds: " + (loadingSummary.currentContinuousSeconds ?? 0) + "\n" - + "- budgetSeconds: " + (loadingSummary.budgetSeconds ?? (Number.isFinite(Number(report.alertThresholds?.visibleLoadingSlowMs)) ? Number(report.alertThresholds.visibleLoadingSlowMs) / 1000 : "unconfigured")) + "\n" - + "- policy: 该指标只能证明用户真实看到“加载中”的持续时间;修复必须降低真实请求/投影/渲染耗时,禁止提前展示未加载完内容来压低该指标。\n\n" - + "#### Loading segments\n\n" + loadingSegmentLines + "\n\n" - + "#### Loading owners\n\n" + loadingOwnerLines + "\n\n" - + "#### Loading sample timeline\n\n" + loadingTimelineLines + "\n\n" - + "### Session rail titles\n\n" - + "- sampleCount: " + (sessionRailTitleSummary.sampleCount ?? 0) + "\n" - + "- visibleSampleCount: " + (sessionRailTitleSummary.visibleSampleCount ?? 0) + "\n" - + "- fallbackSampleCount: " + (sessionRailTitleSummary.fallbackSampleCount ?? 0) + "\n" - + "- majorityFallbackSampleCount: " + (sessionRailTitleSummary.majorityFallbackSampleCount ?? 0) + "\n" - + "- maxFallbackRatio: " + (sessionRailTitleSummary.maxFallbackRatio ?? 0) + "\n" - + "- maxVisibleCount: " + (sessionRailTitleSummary.maxVisibleCount ?? 0) + "\n" - + "- maxFallbackTitleCount: " + (sessionRailTitleSummary.maxFallbackTitleCount ?? 0) + "\n" - + "- policy: 可见 session 列表中 'Session ses_...' fallback 标题超过一半必须报警;修复应让上游 session list projection 直接携带名称,不能靠点击详情后下游修补。\n\n" - + "#### Session rail fallback samples\n\n" + sessionRailTitleSampleLines + "\n\n" - + "#### Session rail fallback examples\n\n" + sessionRailTitleExampleLines + "\n\n" - + "### Page provenance\n\n" - + "- segmentCount: " + (report.pageProvenance?.summary?.segmentCount ?? 0) + "\n" - + "- controlSegmentCount: " + (report.pageProvenance?.summary?.controlSegmentCount ?? 0) + "\n\n" - + provenanceLines + "\n\n" - + "### Page performance: same-origin API Resource Timing\n\n" - + "- budgetMs: " + (report.pagePerformance?.summary?.budgetMs ?? sameOriginApiBudgetMs) + "\n" - + "- sameOriginApiPathCount: " + (report.pagePerformance?.summary?.sameOriginApiPathCount ?? 0) + "\n" - + "- sameOriginApiSampleCount: " + (report.pagePerformance?.summary?.sameOriginApiSampleCount ?? 0) + "\n" - + "- longLivedStreamPathCount: " + (report.pagePerformance?.summary?.longLivedStreamPathCount ?? 0) + "\n" - + "- longLivedStreamSampleCount: " + (report.pagePerformance?.summary?.longLivedStreamSampleCount ?? 0) + "\n" - + "- longLivedStreamOpenOverFiveSecondPathCount: " + (report.pagePerformance?.summary?.longLivedStreamOpenOverFiveSecondPathCount ?? 0) + "\n" - + "- longLivedStreamOpenOverFiveSecondSampleCount: " + (report.pagePerformance?.summary?.longLivedStreamOpenOverFiveSecondSampleCount ?? 0) + "\n" - + "- longLivedStreamLifetimeOverFiveSecondSampleCount: " + (report.pagePerformance?.summary?.longLivedStreamLifetimeOverFiveSecondSampleCount ?? 0) + "\n" - + "- slowPathCount: " + (report.pagePerformance?.summary?.slowPathCount ?? 0) + "\n" - + "- slowSampleCount: " + (report.pagePerformance?.summary?.slowSampleCount ?? 0) + "\n" - + "- worstP95Ms: " + (report.pagePerformance?.summary?.worstP95Ms ?? "-") + "\n\n" - + performanceLines + "\n\n" - + "### Page performance: long-lived streams\n\n" - + "- policy: SSE/long-lived stream lifetime is not ordinary API load latency; only stream open latency is compared with the YAML usability budget, while disconnects remain runtime alerts.\n\n" - + streamPerformanceLines + "\n\n" - + "### Prompt network\n\n" + promptNetworkLines + "\n\n" - + "### Runtime alerts\n\n" - + "- httpErrorCount: " + (alertSummary.httpErrorCount ?? 0) + "\n" - + "- requestFailedCount: " + (alertSummary.requestFailedCount ?? 0) + "\n" - + "- domDiagnosticSampleCount: " + (alertSummary.domDiagnosticSampleCount ?? 0) + "\n" - + "- consoleAlertCount: " + (alertSummary.consoleAlertCount ?? 0) + "\n" - + "- pageErrorCount: " + (alertSummary.pageErrorCount ?? 0) + "\n\n" - + "#### HTTP errors\n\n" + httpAlertLines + "\n\n" - + "#### Request failed\n\n" + requestFailedLines + "\n\n" - + "#### DOM diagnostics\n\n" + domDiagnosticLines + "\n\n" - + "#### Console alerts\n\n" + consoleAlertLines + "\n\n" - + "#### Console alert groups\n\n" + consoleAlertGroupLines + "\n\n" - + "### Turn timing table\n\n" - + turnTimingTable + "\n\n" - + "### Aggregate timeline\n\n" - + metricLines + "\n\n" - + "## Command timeline\n\n" + commandLines + "\n\n" - + "## State transitions\n\n" + transitionLines + "\n"; -} - -async function fileMeta(file) { - const [buffer, stats] = await Promise.all([readFile(file), stat(file)]); - return { byteCount: stats.size, sha256: "sha256:" + createHash("sha256").update(buffer).digest("hex") }; -} - -function sha256(value) { - return "sha256:" + createHash("sha256").update(String(value)).digest("hex"); -} - -function urlPath(value) { - try { - const url = new URL(String(value || "http://invalid.local/")); - return url.pathname; - } catch { - return "-"; - } -} - -function compactLocation(value) { - if (!value || typeof value !== "object") return null; - return { urlPath: urlPath(value.url), lineNumber: value.lineNumber ?? null, columnNumber: value.columnNumber ?? null }; -} - -function limitText(value, limit) { - const text = String(value ?? ""); - if (text.length <= limit) return text; - return text.slice(0, Math.max(0, limit - 1)) + "…"; -} -`; +` + nodeWebObserveAnalyzerTimingSource(); } diff --git a/scripts/src/hwlab-node-web-observe-analyzer-timing-source.ts b/scripts/src/hwlab-node-web-observe-analyzer-timing-source.ts new file mode 100644 index 00000000..51d378be --- /dev/null +++ b/scripts/src/hwlab-node-web-observe-analyzer-timing-source.ts @@ -0,0 +1,2400 @@ +// SPEC: PJ2026-01040111 long-running Workbench observation. +// Responsibility: Timing, trace-order, Code Agent card, and report-rendering source for the offline web-probe observe analyzer. +export function nodeWebObserveAnalyzerTimingSource(): string { + return String.raw`function buildTurnTimingTable(samples, timeline) { + const columns = []; + const registry = new Map(); + const promptAssignmentByKey = new Map(); + const rows = []; + for (let index = 0; index < samples.length; index += 1) { + const sample = samples[index]; + const timelineItem = timeline[index] || {}; + const cells = {}; + const rawMetrics = turnMetricItems(sample, timelineItem); + const domIndexes = rawMetrics.map((item) => Number(item.domIndex)).filter(Number.isFinite); + const maxDomIndex = domIndexes.length > 0 ? Math.max(...domIndexes) : null; + for (const rawMetric of rawMetrics) { + const scopedKey = turnTimingScopedMetricKey(rawMetric, sample); + const samplePromptIndex = Number(timelineItem.promptIndex ?? 0); + const evidencePromptIndex = inferTurnMetricPromptIndex(rawMetric, samplePromptIndex, maxDomIndex); + if (evidencePromptIndex !== null) { + const existingPromptIndex = promptAssignmentByKey.get(scopedKey); + if (existingPromptIndex === undefined || evidencePromptIndex > existingPromptIndex) promptAssignmentByKey.set(scopedKey, evidencePromptIndex); + } + const assignedPromptIndex = promptAssignmentByKey.get(scopedKey) ?? null; + const metric = { ...rawMetric, key: scopedKey, baseKey: rawMetric.key, promptIndex: assignedPromptIndex, samplePromptIndex, pageEpoch: Number(sample?.pageEpoch ?? rawMetric.pageEpoch ?? 0) || 0 }; + let column = registry.get(metric.key); + if (!column) { + column = { + id: "T" + String(columns.length + 1), + label: "T" + String(columns.length + 1), + keyHash: sha256(metric.key), + source: metric.source, + firstSeq: sample.seq ?? null, + firstTs: sample.ts ?? null, + lastSeq: sample.seq ?? null, + lastTs: sample.ts ?? null, + promptIndex: metric.promptIndex ?? null, + lastPromptIndex: metric.promptIndex ?? null, + traceId: metric.traceId ?? null, + messageId: metric.messageId ?? null, + domIndex: metric.domIndex ?? null, + pageRole: metric.pageRole ?? sample.pageRole ?? null, + pageId: metric.pageId ?? sample.pageId ?? null, + pageEpoch: metric.pageEpoch ?? null + }; + registry.set(metric.key, column); + columns.push(column); + } else { + column.lastSeq = sample.seq ?? null; + column.lastTs = sample.ts ?? null; + if (column.source !== "turn" && metric.source === "turn") column.source = "turn"; + if (metric.promptIndex && column.promptIndex !== metric.promptIndex) column.promptIndex = metric.promptIndex; + column.lastPromptIndex = metric.promptIndex ?? column.lastPromptIndex ?? null; + if (!column.traceId && metric.traceId) column.traceId = metric.traceId; + if (!column.messageId && metric.messageId) column.messageId = metric.messageId; + } + cells[column.id] = { + totalElapsedSeconds: metric.totalElapsedSeconds, + recentUpdateSeconds: metric.recentUpdateSeconds, + status: metric.status ?? null, + promptIndex: metric.promptIndex ?? null, + source: metric.source, + pageRole: metric.pageRole ?? sample.pageRole ?? null, + pageId: metric.pageId ?? sample.pageId ?? null, + pageEpoch: metric.pageEpoch ?? null, + sampleGroupSeq: sample.sampleGroupSeq ?? null, + traceId: metric.traceId ?? null, + messageId: metric.messageId ?? null, + textHash: metric.textHash ?? null, + samplePromptIndex: metric.samplePromptIndex ?? null + }; + } + rows.push({ + ts: sample.ts ?? null, + seq: sample.seq ?? null, + sampleGroupSeq: sample.sampleGroupSeq ?? null, + pageRole: sample.pageRole ?? null, + pageId: sample.pageId ?? null, + pageEpoch: Number(sample?.pageEpoch ?? 0) || 0, + promptIndex: timelineItem.promptIndex ?? 0, + routeSessionId: sample.routeSessionId ?? null, + activeSessionId: sample.activeSessionId ?? null, + cells + }); + } + const timingEvents = detectTurnTimingNonMonotonic(columns, rows); + return { + columns, + rows, + nonMonotonic: timingEvents.anomalies, + elapsedZeroResets: timingEvents.elapsedZeroResets, + totalElapsedForwardJumps: timingEvents.totalElapsedForwardJumps, + terminalElapsedGrowth: timingEvents.terminalElapsedGrowth, + recentUpdateResets: timingEvents.recentUpdateResets, + recentUpdateSteps: timingEvents.recentUpdateSteps + }; +} + +function turnTimingScopedMetricKey(metric, sample) { + return [ + metric?.key ?? "unknown", + metric?.pageRole ?? sample?.pageRole ?? "unknown-role", + metric?.pageId ?? sample?.pageId ?? "unknown-page", + Number(sample?.pageEpoch ?? metric?.pageEpoch ?? 0) || 0 + ].join("|page:"); +} + +function inferTurnMetricPromptIndex(metric, samplePromptIndex, maxDomIndex) { + const promptIndex = Number(samplePromptIndex); + if (!Number.isFinite(promptIndex) || promptIndex <= 0) return null; + if (metric?.source === "aggregate") return promptIndex; + if (isActiveTurnStatus(metric?.status)) return promptIndex; + const domIndex = Number(metric?.domIndex); + if (!isTerminalTurnStatus(metric?.status) && Number.isFinite(domIndex) && Number.isFinite(maxDomIndex) && domIndex === maxDomIndex && metric?.recentUpdateSeconds !== null && metric?.recentUpdateSeconds !== undefined) return promptIndex; + return null; +} + +function detectTurnTimingNonMonotonic(columns, rows) { + const anomalies = []; + const elapsedZeroResets = []; + const totalElapsedForwardJumps = []; + const terminalElapsedGrowth = []; + const recentUpdateResets = []; + const recentUpdateSteps = []; + for (const column of columns) { + const previousByMetric = new Map(); + let previousTerminalTotal = null; + for (const row of rows) { + const cell = row.cells?.[column.id]; + if (!cell) continue; + for (const metric of ["totalElapsedSeconds", "recentUpdateSeconds"]) { + const value = cell[metric]; + if (value === null || value === undefined || !Number.isFinite(Number(value))) continue; + const current = Number(value); + const previous = previousByMetric.get(metric); + if (previous && metric === "totalElapsedSeconds" && current < previous.value) { + const anomaly = previous.value > 0 && current === 0 ? "zero-reset" : "decrease"; + const event = { + columnId: column.id, + columnLabel: column.label, + metric, + anomaly, + expectedPattern: anomaly === "zero-reset" ? "total-elapsed-should-not-return-to-zero" : "total-elapsed-monotonic", + fromSeq: previous.seq, + fromTs: previous.ts, + fromValue: previous.value, + toSeq: row.seq ?? null, + toTs: row.ts ?? null, + toValue: current, + delta: current - previous.value, + traceId: cell.traceId ?? column.traceId ?? null, + messageId: cell.messageId ?? column.messageId ?? null, + promptIndex: cell.promptIndex ?? null, + samplePromptIndex: row.promptIndex ?? null, + source: cell.source ?? column.source ?? null, + pageRole: cell.pageRole ?? column.pageRole ?? null, + pageId: cell.pageId ?? column.pageId ?? null, + pageEpoch: cell.pageEpoch ?? row.pageEpoch ?? column.pageEpoch ?? null, + valuesRedacted: true + }; + anomalies.push(event); + if (anomaly === "zero-reset") elapsedZeroResets.push(event); + } + if (previous && metric === "totalElapsedSeconds" && current > previous.value) { + const sampleDeltaSeconds = elapsedSecondsBetween(previous.ts, row.ts); + const delta = current - previous.value; + const allowedIncreaseSeconds = sampleDeltaSeconds + alertThresholds.turnTimingSampleSlackSeconds; + const terminalTransition = !isTerminalTurnStatus(previous.status) && isTerminalTurnStatus(cell.status); + if (delta > allowedIncreaseSeconds && !terminalTransition) { + totalElapsedForwardJumps.push({ + columnId: column.id, + columnLabel: column.label, + metric, + anomaly: "forward-jump", + expectedPattern: "total-elapsed-increase-should-match-browser-sample-interval", + fromSeq: previous.seq, + fromTs: previous.ts, + fromValue: previous.value, + fromStatus: previous.status ?? null, + toSeq: row.seq ?? null, + toTs: row.ts ?? null, + toValue: current, + toStatus: cell.status ?? null, + delta, + sampleDeltaSeconds, + allowedIncreaseSeconds, + excessiveIncreaseSeconds: Number((delta - allowedIncreaseSeconds).toFixed(3)), + traceId: cell.traceId ?? column.traceId ?? null, + messageId: cell.messageId ?? column.messageId ?? null, + promptIndex: cell.promptIndex ?? null, + samplePromptIndex: row.promptIndex ?? null, + source: cell.source ?? column.source ?? null, + pageRole: cell.pageRole ?? column.pageRole ?? null, + pageId: cell.pageId ?? column.pageId ?? null, + pageEpoch: cell.pageEpoch ?? row.pageEpoch ?? column.pageEpoch ?? null, + valuesRedacted: true + }); + } + } + if (metric === "totalElapsedSeconds" && isTerminalTurnStatus(cell.status)) { + if (previousTerminalTotal && current > previousTerminalTotal.value) { + terminalElapsedGrowth.push({ + columnId: column.id, + columnLabel: column.label, + metric, + anomaly: "terminal-growth", + expectedPattern: "terminal-total-elapsed-sealed", + fromSeq: previousTerminalTotal.seq, + fromTs: previousTerminalTotal.ts, + fromValue: previousTerminalTotal.value, + fromStatus: previousTerminalTotal.status, + toSeq: row.seq ?? null, + toTs: row.ts ?? null, + toValue: current, + toStatus: cell.status ?? null, + delta: current - previousTerminalTotal.value, + traceId: cell.traceId ?? column.traceId ?? null, + messageId: cell.messageId ?? column.messageId ?? null, + promptIndex: cell.promptIndex ?? null, + samplePromptIndex: row.promptIndex ?? null, + source: cell.source ?? column.source ?? null, + pageRole: cell.pageRole ?? column.pageRole ?? null, + pageId: cell.pageId ?? column.pageId ?? null, + pageEpoch: cell.pageEpoch ?? row.pageEpoch ?? column.pageEpoch ?? null, + valuesRedacted: true + }); + } + previousTerminalTotal = { value: current, seq: row.seq ?? null, ts: row.ts ?? null, status: cell.status ?? null }; + } + if (previous && metric === "recentUpdateSeconds") { + const elapsedMs = Date.parse(String(row.ts ?? "")) - Date.parse(String(previous.ts ?? "")); + const elapsedSeconds = Number.isFinite(elapsedMs) && elapsedMs >= 0 ? elapsedMs / 1000 : null; + const increase = current - previous.value; + const allowedIncrease = elapsedSeconds === null + ? alertThresholds.turnTimingSampleSlackSeconds + : Math.max(alertThresholds.turnTimingSampleSlackSeconds, elapsedSeconds + alertThresholds.turnTimingSampleSlackSeconds); + const excessiveIncrease = increase > allowedIncrease ? increase - allowedIncrease : 0; + recentUpdateSteps.push({ + columnId: column.id, + columnLabel: column.label, + metric: "recentUpdateSeconds", + event: increase < 0 ? "reset" : excessiveIncrease > 0 ? "jump" : "increase", + expectedPattern: "sawtooth-increase-or-reset", + fromSeq: previous.seq, + fromTs: previous.ts, + fromValue: previous.value, + toSeq: row.seq ?? null, + toTs: row.ts ?? null, + toValue: current, + delta: increase, + sampleDeltaSeconds: elapsedSeconds, + allowedIncreaseSeconds: allowedIncrease, + excessiveIncreaseSeconds: excessiveIncrease, + traceId: cell.traceId ?? column.traceId ?? null, + messageId: cell.messageId ?? column.messageId ?? null, + promptIndex: cell.promptIndex ?? null, + samplePromptIndex: row.promptIndex ?? null, + source: cell.source ?? column.source ?? null, + pageRole: cell.pageRole ?? column.pageRole ?? null, + pageId: cell.pageId ?? column.pageId ?? null, + pageEpoch: cell.pageEpoch ?? row.pageEpoch ?? column.pageEpoch ?? null, + valuesRedacted: true + }); + if (increase < 0) { + recentUpdateResets.push({ + columnId: column.id, + columnLabel: column.label, + metric: "recentUpdateSeconds", + event: "reset", + fromSeq: previous.seq, + fromTs: previous.ts, + fromValue: previous.value, + toSeq: row.seq ?? null, + toTs: row.ts ?? null, + toValue: current, + delta: increase, + sampleDeltaSeconds: elapsedSeconds, + traceId: cell.traceId ?? column.traceId ?? null, + messageId: cell.messageId ?? column.messageId ?? null, + promptIndex: cell.promptIndex ?? null, + samplePromptIndex: row.promptIndex ?? null, + source: cell.source ?? column.source ?? null, + pageRole: cell.pageRole ?? column.pageRole ?? null, + pageId: cell.pageId ?? column.pageId ?? null, + pageEpoch: cell.pageEpoch ?? row.pageEpoch ?? column.pageEpoch ?? null, + valuesRedacted: true + }); + } + if (excessiveIncrease > 0) { + anomalies.push({ + columnId: column.id, + columnLabel: column.label, + metric: "recentUpdateSeconds", + anomaly: "jump", + expectedPattern: "sawtooth-increase-or-reset", + fromSeq: previous.seq, + fromTs: previous.ts, + fromValue: previous.value, + toSeq: row.seq ?? null, + toTs: row.ts ?? null, + toValue: current, + delta: increase, + sampleDeltaSeconds: elapsedSeconds, + allowedIncreaseSeconds: allowedIncrease, + excessiveIncreaseSeconds: excessiveIncrease, + traceId: cell.traceId ?? column.traceId ?? null, + messageId: cell.messageId ?? column.messageId ?? null, + promptIndex: cell.promptIndex ?? null, + samplePromptIndex: row.promptIndex ?? null, + source: cell.source ?? column.source ?? null, + pageRole: cell.pageRole ?? column.pageRole ?? null, + pageId: cell.pageId ?? column.pageId ?? null, + pageEpoch: cell.pageEpoch ?? row.pageEpoch ?? column.pageEpoch ?? null, + valuesRedacted: true + }); + } + } + previousByMetric.set(metric, { value: current, seq: row.seq ?? null, ts: row.ts ?? null, status: cell.status ?? null }); + } + } + } + return { anomalies, elapsedZeroResets, totalElapsedForwardJumps, terminalElapsedGrowth, recentUpdateResets, recentUpdateSteps }; +} + +function elapsedSecondsBetween(fromTs, toTs) { + const from = Date.parse(fromTs); + const to = Date.parse(toTs); + if (!Number.isFinite(from) || !Number.isFinite(to) || to < from) return 0; + return Number(((to - from) / 1000).toFixed(3)); +} + +function maxPositiveDelta(items) { + const values = (Array.isArray(items) ? items : []) + .map((item) => Number(item.delta)) + .filter((value) => Number.isFinite(value) && value > 0); + return values.length > 0 ? Math.max(...values) : 0; +} + +function isTerminalTurnStatus(value) { + const status = String(value ?? "").trim().toLowerCase().replace(/_/gu, "-"); + return ["completed", "succeeded", "success", "failed", "error", "blocked", "timeout", "canceled", "cancelled", "stale", "done", "terminal", "thread-resume-failed"].includes(status); +} + + + +function buildTraceOrderMetrics(samples, timeline) { + const rows = []; + const orderAnomalies = []; + const completionNotLast = []; + const groups = new Map(); + for (let sampleIndex = 0; sampleIndex < samples.length; sampleIndex += 1) { + const sample = samples[sampleIndex] || {}; + const sampleRows = traceTimingRowsForSample(sample, timeline[sampleIndex] || {}); + for (const row of sampleRows) { + const normalized = { + ...row, + sampleIndex, + sampleSeq: sample.seq ?? null, + timestamp: sample.ts || sample.timestamp || sample.collectedAt || sample.time || null, + pageRole: row.pageRole || sample.pageRole || sample.role || sample.contextRole || null, + pageId: row.pageId || sample.pageId || sample.contextId || null, + sessionId: row.sessionId || sample.sessionId || sample.workbenchSessionId || null, + }; + rows.push(normalized); + const key = traceRowGroupKey(normalized); + if (!groups.has(key)) groups.set(key, []); + groups.get(key).push(normalized); + } + } + const slackSeconds = Math.max(1, Number(alertThresholds?.turnTimingSampleSlackSeconds || 0)); + for (const groupRows of groups.values()) { + const sorted = groupRows.slice().sort((a, b) => { + if (a.sampleIndex !== b.sampleIndex) return a.sampleIndex - b.sampleIndex; + return (a.rowIndex ?? 0) - (b.rowIndex ?? 0); + }); + let previous = null; + for (const row of sorted) { + if (previous) { + const reasons = []; + if (Number.isFinite(previous.totalSeconds) && Number.isFinite(row.totalSeconds) && row.totalSeconds + slackSeconds < previous.totalSeconds) { + reasons.push('total-decreased'); + } + if (Number.isFinite(previous.projectedSeq) && Number.isFinite(row.projectedSeq) && row.projectedSeq < previous.projectedSeq) { + reasons.push('projected-seq-decreased'); + } + if (Number.isFinite(previous.clockSeconds) && Number.isFinite(row.clockSeconds)) { + const diff = previous.clockSeconds - row.clockSeconds; + if (diff > slackSeconds && diff < 43200) reasons.push('clock-decreased'); + } + if (Number.isFinite(previous.timestampMs) && Number.isFinite(row.timestampMs) && row.timestampMs + slackSeconds * 1000 < previous.timestampMs) { + reasons.push('timestamp-decreased'); + } + if (reasons.length) { + orderAnomalies.push({ + sampleIndex: row.sampleIndex, + sampleSeq: row.sampleSeq, + timestamp: row.timestamp, + pageRole: row.pageRole, + pageId: row.pageId, + sessionId: row.sessionId, + traceId: row.traceId || previous.traceId || null, + previousRowIndex: previous.rowIndex, + currentRowIndex: row.rowIndex, + reasons, + previousTotalSeconds: previous.totalSeconds, + currentTotalSeconds: row.totalSeconds, + previousProjectedSeq: previous.projectedSeq, + currentProjectedSeq: row.projectedSeq, + previousSourceSeq: previous.sourceSeq, + currentSourceSeq: row.sourceSeq, + previousEventSeq: previous.eventSeq, + currentEventSeq: row.eventSeq, + previousClockSeconds: previous.clockSeconds, + currentClockSeconds: row.clockSeconds, + previousTimestampMs: previous.timestampMs, + currentTimestampMs: row.timestampMs, + previousPreview: previous.preview, + currentPreview: row.preview, + }); + } + } + previous = row; + } + for (let index = 0; index < sorted.length; index += 1) { + const row = sorted[index]; + if (!row.isCompletion) continue; + const later = sorted.slice(index + 1).find((candidate) => { + if (candidate.isCompletion && candidate.preview === row.preview) return false; + return Number.isFinite(candidate.totalSeconds) || Number.isFinite(candidate.clockSeconds) || Number.isFinite(candidate.projectedSeq); + }); + if (later) { + completionNotLast.push({ + sampleIndex: row.sampleIndex, + sampleSeq: row.sampleSeq, + timestamp: row.timestamp, + pageRole: row.pageRole, + pageId: row.pageId, + sessionId: row.sessionId, + traceId: row.traceId || later.traceId || null, + completionRowIndex: row.rowIndex, + laterRowIndex: later.rowIndex, + completionTotalSeconds: row.totalSeconds, + laterTotalSeconds: later.totalSeconds, + completionProjectedSeq: row.projectedSeq, + laterProjectedSeq: later.projectedSeq, + completionSourceSeq: row.sourceSeq, + laterSourceSeq: later.sourceSeq, + completionEventSeq: row.eventSeq, + laterEventSeq: later.eventSeq, + completionPreview: row.preview, + laterPreview: later.preview, + }); + } + } + } + return { + summary: { + sampleCount: samples.length, + traceRowCount: rows.length, + orderAnomalyCount: orderAnomalies.length, + completionNotLastCount: completionNotLast.length, + }, + rows: rows.slice(0, 200), + orderAnomalies: orderAnomalies.slice(0, 100), + completionNotLast: completionNotLast.slice(0, 100), + }; +} + +function buildCodeAgentCardDurationUnderreportedMetrics(samples, timeline) { + const findings = []; + const slackSeconds = Math.max(5, Number(alertThresholds?.turnTimingSampleSlackSeconds || 0)); + for (let sampleIndex = 0; sampleIndex < samples.length; sampleIndex += 1) { + const sample = samples[sampleIndex] || {}; + const cards = codeAgentCardsForSample(sample); + if (!cards.length) continue; + const traceRows = traceTimingRowsForSample(sample, timeline[sampleIndex] || {}); + const terminalCards = cards.filter((card) => isCodeAgentCardTerminal(card)); + const sampleText = sampleVisibleText(sample, timeline[sampleIndex] || {}); + for (const card of terminalCards) { + const cardText = codeAgentCardText(card); + const parsedCardSeconds = parseTotalElapsedSeconds(cardText).filter(Number.isFinite); + const cardSeconds = Number.isFinite(Number(card.totalElapsedSeconds)) ? Number(card.totalElapsedSeconds) : parsedCardSeconds.length > 0 ? Math.max(...parsedCardSeconds) : NaN; + if (!Number.isFinite(cardSeconds)) continue; + const traceMatched = traceRows.filter((row) => traceRowMatchesCard(row, card, terminalCards.length)); + const traceEvidence = maxTraceDurationEvidence(traceMatched); + const textEvidence = maxSelfReportedDurationEvidence([card.text, card.preview, card.finalResponseText, card.runningRecordText, terminalCards.length === 1 ? sampleText : ''].filter(Boolean).join('\n')); + const evidences = [traceEvidence, textEvidence].filter((item) => item && item.exact === true && Number.isFinite(item.seconds)); + if (!evidences.length) continue; + const strongest = evidences.sort((a, b) => b.seconds - a.seconds)[0]; + const tolerance = Math.max(slackSeconds, Math.ceil(strongest.seconds * 0.05)); + if (strongest.seconds > cardSeconds + tolerance) { + findings.push({ + sampleIndex, + timestamp: sample.timestamp || sample.collectedAt || sample.time || null, + pageRole: card.pageRole || sample.pageRole || sample.role || sample.contextRole || null, + pageId: card.pageId || sample.pageId || sample.contextId || null, + sessionId: card.sessionId || sample.sessionId || sample.workbenchSessionId || null, + traceId: card.traceId || strongest.traceId || null, + status: card.status || card.state || card.phase || null, + cardTotalElapsedSeconds: cardSeconds, + expectedElapsedSeconds: strongest.seconds, + deltaSeconds: strongest.seconds - cardSeconds, + toleranceSeconds: tolerance, + evidenceKind: strongest.kind, + evidencePreview: strongest.preview, + cardPreview: card.preview || compactOneLine(cardText || ''), + }); + } + } + } + return findings.slice(0, 100); +} + +function buildCodeAgentCardDurationMismatchMetrics(samples, timeline) { + const findings = []; + const slackSeconds = Math.max(5, Number(alertThresholds?.turnTimingSampleSlackSeconds || 0)); + for (let sampleIndex = 0; sampleIndex < samples.length; sampleIndex += 1) { + const sample = samples[sampleIndex] || {}; + const cards = codeAgentCardsForSample(sample); + if (!cards.length) continue; + const traceRows = traceTimingRowsForSample(sample, timeline[sampleIndex] || {}); + const terminalCards = cards.filter((card) => isCodeAgentCardTerminal(card)); + const sampleText = sampleVisibleText(sample, timeline[sampleIndex] || {}); + for (const card of terminalCards) { + const cardText = codeAgentCardText(card); + const parsedCardSeconds = parseTotalElapsedSeconds(cardText).filter(Number.isFinite); + const cardSeconds = Number.isFinite(Number(card.totalElapsedSeconds)) ? Number(card.totalElapsedSeconds) : parsedCardSeconds.length > 0 ? Math.max(...parsedCardSeconds) : NaN; + if (!Number.isFinite(cardSeconds)) continue; + const traceMatched = traceRows.filter((row) => traceRowMatchesCard(row, card, terminalCards.length)); + const traceEvidence = maxTraceDurationEvidence(traceMatched); + const textEvidence = maxSelfReportedDurationEvidence([card.text, card.preview, card.finalResponseText, card.runningRecordText, terminalCards.length === 1 ? sampleText : ''].filter(Boolean).join('\n')); + const evidences = [traceEvidence, textEvidence].filter((item) => item && item.exact === true && Number.isFinite(item.seconds)); + if (!evidences.length) continue; + const strongest = evidences.sort((a, b) => b.seconds - a.seconds)[0]; + const exactEvidence = strongest.exact === true || strongest.kind === 'trace-completion-total' || strongest.kind === 'final-response-duration'; + const tolerance = Math.max(slackSeconds, Math.ceil(strongest.seconds * 0.05)); + const signedDelta = Number((cardSeconds - strongest.seconds).toFixed(3)); + const absoluteDelta = Math.abs(signedDelta); + const underreported = strongest.seconds > cardSeconds + tolerance; + const overreported = exactEvidence && cardSeconds > strongest.seconds + tolerance; + if (!underreported && !overreported) continue; + findings.push({ + sampleIndex, + timestamp: sample.timestamp || sample.collectedAt || sample.time || sample.ts || null, + pageRole: card.pageRole || sample.pageRole || sample.role || sample.contextRole || null, + pageId: card.pageId || sample.pageId || sample.contextId || null, + sessionId: card.sessionId || sample.sessionId || sample.workbenchSessionId || null, + traceId: card.traceId || strongest.traceId || null, + status: card.status || card.state || card.phase || null, + direction: underreported ? 'underreported' : 'overreported', + cardTotalElapsedSeconds: cardSeconds, + expectedElapsedSeconds: strongest.seconds, + signedDeltaSeconds: signedDelta, + deltaSeconds: Number(absoluteDelta.toFixed(3)), + toleranceSeconds: tolerance, + evidenceKind: strongest.kind, + exactEvidence, + evidencePreview: strongest.preview, + cardPreview: card.preview || compactOneLine(cardText || ''), + }); + } + } + return findings.slice(0, 100); +} + +function traceTimingRowsForSample(sample, timelineItem) { + const rows = []; + const seen = new Set(); + const appendRowsFromText = (text, source, baseIndex, meta = {}) => { + for (const extracted of extractTraceRowsFromText(text, source, baseIndex, meta)) { + const key = [extracted.pageRole || '', extracted.pageId || '', extracted.rowIndex, extracted.preview].join('|'); + if (seen.has(key)) continue; + seen.add(key); + rows.push(extracted); + } + }; + for (const candidate of traceRowCandidateArrays(sample, timelineItem)) { + const array = Array.isArray(candidate.rows) ? candidate.rows : []; + array.forEach((item, index) => { + if (typeof item === 'string') { + appendRowsFromText(item, candidate.source, index, candidate.meta || {}); + return; + } + if (!item || typeof item !== 'object') return; + const text = objectText(item); + if (!text) return; + appendRowsFromText(text, candidate.source, Number.isFinite(Number(item.index)) ? Number(item.index) : index, { + ...(candidate.meta || {}), + pageRole: item.pageRole || item.role || candidate.meta?.pageRole || null, + pageId: item.pageId || item.contextId || candidate.meta?.pageId || null, + sessionId: item.sessionId || item.workbenchSessionId || candidate.meta?.sessionId || null, + traceId: item.traceId || item.trace_id || candidate.meta?.traceId || null, + projectedSeq: item.projectedSeq ?? item.projected_seq ?? item.projectedSequence ?? null, + sourceSeq: item.sourceSeq ?? item.source_seq ?? item.sourceSequence ?? null, + eventSeq: item.eventSeq ?? item.event_seq ?? item.sequence ?? null, + eventTimestamp: item.eventTimestamp ?? item.event_ts ?? item.timestamp ?? item.ts ?? null, + eventTimeText: item.eventTimeText ?? item.timeText ?? null, + eventKind: item.eventKind ?? item.kind ?? item.status ?? null, + }); + }); + } + if (!rows.length) { + appendRowsFromText(sampleVisibleText(sample, timelineItem), 'visible-text', 0, { + pageRole: sample.pageRole || sample.role || sample.contextRole || null, + pageId: sample.pageId || sample.contextId || null, + sessionId: sample.sessionId || sample.workbenchSessionId || null, + }); + } + rows.sort((a, b) => (a.rowIndex ?? 0) - (b.rowIndex ?? 0)); + return rows; +} + +function extractTraceRowsFromText(text, source, baseIndex, meta = {}) { + const result = []; + const normalized = String(text || '').replace(/\r/g, '\n'); + if (!normalized.trim()) return result; + const lines = normalized.split('\n').map((line) => line.trim()).filter(Boolean); + for (let index = 0; index < lines.length; index += 1) { + const line = lines[index]; + if (!traceLineLooksRelevant(line)) continue; + const nextLine = index + 1 < lines.length && !/^\d{1,2}:\d{2}:\d{2}\b/.test(lines[index + 1]) ? lines[index + 1] : ''; + const preview = compactOneLine(nextLine ? line + ' ' + nextLine : line); + result.push(normalizeTraceTimingRow(preview, source, Number(baseIndex || 0) * 1000 + index, meta)); + } + return result; +} + +function normalizeTraceTimingRow(text, source, rowIndex, meta = {}) { + const preview = compactOneLine(text).slice(0, 240); + const projectedSeq = firstFiniteNumber(meta.projectedSeq, parseTraceRowProjectedSeq(preview)); + const sourceSeq = firstFiniteNumber(meta.sourceSeq); + const eventSeq = firstFiniteNumber(meta.eventSeq); + const timestampMs = parseTraceRowTimestampMs(meta.eventTimestamp || meta.eventTimeText || preview); + return { + source, + rowIndex, + preview, + pageRole: meta.pageRole || null, + pageId: meta.pageId || null, + sessionId: meta.sessionId || null, + traceId: meta.traceId || parseTraceRowTraceId(preview), + clockSeconds: parseTraceRowClockSeconds(preview) ?? parseTraceRowClockSeconds(meta.eventTimeText || ""), + timestampMs, + totalSeconds: parseTraceRowTotalSeconds(preview), + projectedSeq, + sourceSeq, + eventSeq, + eventTimestamp: meta.eventTimestamp || null, + eventTimeText: meta.eventTimeText || null, + eventKind: meta.eventKind || null, + isCompletion: traceRowIsTerminalCompletionText(preview, meta.eventKind || ""), + }; +} + +function traceRowIsTerminalCompletionText(preview, eventKind = "") { + const value = [preview, eventKind].map((item) => String(item || "")).join(" "); + if (/\bnon[-_ ]?terminal\b/i.test(value)) return false; + if (/轮次完成|turn\s+completed|completed\s+turn|backend[_: -]?turn[_: -]?finished/i.test(value)) return true; + return /\bterminal(?:[_: -]?status|Status)?\s*[=: -]\s*(?:completed|failed|cancelled|canceled|timeout)\b/i.test(value); +} + +function traceRowIsRoundCompletionText(preview, eventKind = "") { + const value = [preview, eventKind].map((item) => String(item || "")).join(" "); + if (/\bnon[-_ ]?terminal\b/i.test(value)) return false; + return /轮次完成|turn\s+completed|completed\s+turn|backend[_: -]?turn[_: -]?finished/i.test(value); +} + +function traceRowCandidateArrays(sample, timelineItem) { + const candidates = []; + const pushArray = (rows, source, meta = {}) => { + if (Array.isArray(rows) && rows.length) candidates.push({ rows, source, meta }); + }; + const directSources = [ + [sample?.traceRows, 'sample.traceRows'], + [sample?.eventRows, 'sample.eventRows'], + [sample?.activityRows, 'sample.activityRows'], + [sample?.timelineRows, 'sample.timelineRows'], + [sample?.dom?.traceRows, 'sample.dom.traceRows'], + [sample?.dom?.eventRows, 'sample.dom.eventRows'], + [sample?.dom?.activityRows, 'sample.dom.activityRows'], + [sample?.dom?.timelineRows, 'sample.dom.timelineRows'], + [timelineItem?.traceRows, 'timeline.traceRows'], + [timelineItem?.eventRows, 'timeline.eventRows'], + [timelineItem?.activityRows, 'timeline.activityRows'], + [timelineItem?.rows, 'timeline.rows'], + [timelineItem?.events, 'timeline.events'], + ]; + for (const [rows, source] of directSources) pushArray(rows, source, {}); + if (!candidates.length) collectNamedTraceArrays(sample, candidates, 'sample', 0); + if (!candidates.length) collectNamedTraceArrays(timelineItem, candidates, 'timeline', 0); + return candidates; +} + +function collectNamedTraceArrays(value, candidates, path, depth) { + if (!value || depth > 5) return; + if (Array.isArray(value)) { + const pathLooksTrace = /trace|timeline|activity|event|log|record/i.test(path); + const valueLooksTrace = value.slice(0, 5).some((item) => traceLineLooksRelevant(typeof item === 'string' ? item : objectText(item))); + if (pathLooksTrace || valueLooksTrace) candidates.push({ rows: value, source: path, meta: {} }); + return; + } + if (typeof value !== 'object') return; + for (const [key, child] of Object.entries(value)) { + if (!child) continue; + if (Array.isArray(child)) { + const childPath = path + '.' + key; + const pathLooksTrace = /trace|timeline|activity|event|log|record/i.test(key); + const valueLooksTrace = child.slice(0, 5).some((item) => traceLineLooksRelevant(typeof item === 'string' ? item : objectText(item))); + if (pathLooksTrace || valueLooksTrace) candidates.push({ rows: child, source: childPath, meta: {} }); + continue; + } + if (typeof child === 'object' && /dom|trace|timeline|activity|event|log|record|page|card|message|panel|diagnostic/i.test(key)) { + collectNamedTraceArrays(child, candidates, path + '.' + key, depth + 1); + } + } +} + +function traceLineLooksRelevant(text) { + const value = String(text || '').trim(); + if (!value) return false; + if (/^\d{1,2}:\d{2}:\d{2}\b/.test(value)) return true; + if (/\btotal=\d/.test(value)) return true; + if (/轮次完成(总耗时/.test(value)) return true; + if (/\bseq(?:uence)?[=:]\s*\d+/i.test(value)) return true; + return false; +} + +function parseTraceRowClockSeconds(text) { + const match = String(text || '').match(/^\s*(\d{1,2}):(\d{2}):(\d{2})\b/); + if (!match) return null; + return Number(match[1]) * 3600 + Number(match[2]) * 60 + Number(match[3]); +} + +function parseTraceRowTotalSeconds(text) { + const value = String(text || ''); + const totalMatch = value.match(/\btotal=([0-9:.]+)/i); + if (totalMatch) return parseTraceDurationSeconds(totalMatch[1]); + const completionMatch = value.match(/总耗时\s*([0-9:.]+)/); + if (completionMatch) return parseTraceDurationSeconds(completionMatch[1]); + return null; +} + +function parseTraceDurationSeconds(value) { + const text = String(value || '').trim(); + if (!text) return null; + const parts = text.split(':').map((part) => Number(part)); + if (parts.some((part) => !Number.isFinite(part))) return null; + if (parts.length === 3) return parts[0] * 3600 + parts[1] * 60 + parts[2]; + if (parts.length === 2) return parts[0] * 60 + parts[1]; + if (parts.length === 1) return parts[0]; + return null; +} + +function parseTraceRowProjectedSeq(text) { + const value = String(text || ''); + const match = value.match(/\b(?:projected[_-]?seq|seq(?:uence)?|event[_-]?seq)\s*[=:]\s*(\d+)/i); + return match ? Number(match[1]) : null; +} + +function parseTraceRowTimestampMs(value) { + const text = String(value || '').trim(); + if (!text) return null; + const parsed = Date.parse(text); + return Number.isFinite(parsed) ? parsed : null; +} + +function firstFiniteNumber(...values) { + for (const value of values) { + const numeric = Number(value); + if (Number.isFinite(numeric)) return numeric; + } + return null; +} + +function parseTraceRowTraceId(text) { + const match = String(text || '').match(/\b(?:trace_id=|traceId[:=]?\s*)(trc_[a-z0-9_-]+|[a-f0-9]{16,})\b/i); + return match ? match[1] : null; +} + +function traceRowGroupKey(row) { + const identity = row.traceId ? 'trace:' + row.traceId : 'sample:' + (row.sampleIndex ?? '-') + ':' + (row.source || 'unknown'); + return [row.pageRole || '', row.pageId || '', row.sessionId || '', row.source || '', row.sampleIndex ?? '', identity].join('|'); +} + +function traceRowMatchesCard(row, card, terminalCardCount) { + if (!row) return false; + if (card.traceId) return row.traceId === card.traceId; + if (row.traceId) return false; + if (row.sessionId && card.sessionId) return terminalCardCount === 1 && row.sessionId === card.sessionId; + if (terminalCardCount === 1) return true; + return false; +} + +function maxTraceDurationEvidence(rows) { + const finiteTotals = rows.map((row) => Number(row.totalSeconds)).filter(Number.isFinite); + const finiteClocks = rows.map((row) => Number(row.clockSeconds)).filter(Number.isFinite); + const evidences = []; + if (finiteTotals.length) { + const maxTotal = Math.max(...finiteTotals); + const source = rows.find((row) => Number(row.totalSeconds) === maxTotal); + const exact = source?.isCompletion === true; + evidences.push({ kind: exact ? 'trace-completion-total' : 'trace-total', seconds: maxTotal, traceId: source?.traceId || null, preview: source?.preview || '', exact }); + } + if (finiteClocks.length >= 2) { + const minClock = Math.min(...finiteClocks); + const maxClock = Math.max(...finiteClocks); + const span = maxClock - minClock; + if (span >= 0 && span < 43200) evidences.push({ kind: 'trace-clock-span', seconds: span, traceId: null, preview: 'visible trace row clock span', exact: false }); + } + if (!evidences.length) return null; + evidences.sort((a, b) => b.seconds - a.seconds); + return evidences[0]; +} + +function maxSelfReportedDurationEvidence(text) { + const value = String(text || ''); + const lines = value.split(/\n+/).map((line) => line.trim()).filter(Boolean); + let best = null; + for (let index = 0; index < lines.length; index += 1) { + const line = lines[index]; + const previous = index > 0 ? lines[index - 1] : ''; + const next = index + 1 < lines.length ? lines[index + 1] : ''; + const candidateText = selfReportedDurationCandidateText(previous, line, next); + if (!candidateText) continue; + const seconds = parseSelfReportedRoundDurationSeconds(candidateText); + if (!Number.isFinite(seconds)) continue; + if (!best || seconds > best.seconds) { + best = { kind: 'final-response-duration', seconds, preview: compactOneLine(candidateText).slice(0, 240), exact: true }; + } + } + return best; +} + +function selfReportedDurationCandidateText(previous, line, next) { + const current = String(line || ''); + const before = String(previous || ''); + const after = String(next || ''); + const windowText = [before, current, after].filter(Boolean).join(' '); + const hasDurationKeyword = /耗时|用时|duration|elapsed/i.test(current); + const hasNearbyDurationHeading = /(?:本轮|整轮|全程|任务|round|turn)?\s*(?:耗时|用时|duration|elapsed)/i.test(before) + || /(?:本轮|整轮|全程|任务|round|turn)?\s*(?:耗时|用时|duration|elapsed)/i.test(after); + const hasDurationValue = /(?:约|大约|around|about)?\s*\d+(?:\.\d+)?\s*(?:小时|分钟|分|秒|hour|hours|hr|hrs|min|mins|minute|minutes|sec|secs|second|seconds)/i.test(windowText); + const hasRoundContext = /本轮|整轮|全程|从.+到|全部通过|smoke|benchmark|round|turn|completed|passed/i.test(windowText); + if (hasDurationKeyword && hasDurationValue) return current; + if (hasNearbyDurationHeading && hasDurationValue) return windowText; + if (hasRoundContext && hasDurationValue && /(?:约|大约|around|about)\s*\d|\d+(?:\.\d+)?\s*(?:分钟|小时|minute|hour)/i.test(current)) return windowText; + return ''; +} + +function parseSelfReportedRoundDurationSeconds(text) { + const value = String(text || ''); + const clock = value.match(/(?:耗时|用时|duration|elapsed)[^0-9]{0,24}(\d{1,2}:\d{2}:\d{2}|\d{1,2}:\d{2})/i); + if (clock) return parseTraceDurationSeconds(clock[1]); + const hour = value.match(/(?:约|大约|around|about)?\s*(\d+(?:\.\d+)?)\s*(?:小时|hour|hours|hr|hrs)/i); + const minute = value.match(/(?:约|大约|around|about)?\s*(\d+(?:\.\d+)?)\s*(?:分钟|分|min|mins|minute|minutes)/i); + const second = value.match(/(?:约|大约|around|about)?\s*(\d+(?:\.\d+)?)\s*(?:秒|sec|secs|second|seconds)/i); + let total = 0; + if (hour) total += Number(hour[1]) * 3600; + if (minute) total += Number(minute[1]) * 60; + if (second) total += Number(second[1]); + return total > 0 ? total : null; +} + +function sampleVisibleText(sample, timelineItem) { + const chunks = []; + for (const source of [sample?.visibleText, sample?.text, sample?.innerText, sample?.dom?.visibleText, sample?.dom?.text, timelineItem?.visibleText, timelineItem?.text, timelineItem?.message, timelineItem?.summary]) { + if (typeof source === 'string' && source.trim()) chunks.push(source); + } + return chunks.join('\n'); +} + +function objectText(value) { + if (!value || typeof value !== 'object') return typeof value === 'string' ? value : ''; + const keys = ['text', 'innerText', 'visibleText', 'label', 'title', 'summary', 'message', 'content', 'body', 'preview', 'description']; + const chunks = []; + for (const key of keys) { + const part = value[key]; + if (typeof part === 'string' && part.trim()) chunks.push(part); + } + return chunks.join('\n'); +} + +function compactOneLine(value) { + return String(value || '').replace(/\s+/g, ' ').trim(); +} + +function buildCodeAgentCardTimingMetrics(samples, timeline, turnTiming) { + const missingElapsed = []; + const missingRecentUpdate = []; + const cardRows = []; + for (let index = 0; index < (Array.isArray(samples) ? samples : []).length; index += 1) { + const sample = samples[index]; + const timelineItem = timeline[index] || {}; + for (const card of codeAgentCardsForSample(sample)) { + const text = codeAgentCardText(card); + const totalElapsedValues = parseTotalElapsedSeconds(card?.durationText).filter(Number.isFinite); + const recentUpdateValues = parseRecentUpdateSeconds(card?.activityText).filter(Number.isFinite); + const terminal = isCodeAgentCardTerminal(card); + const row = { + ...ref(sample), + promptIndex: timelineItem.promptIndex ?? 0, + source: card.source ?? "turn", + status: card.status ?? null, + messageId: card.messageId ?? null, + traceId: card.traceId ?? firstTraceId([text]), + sessionId: card.sessionId ?? sample.sessionId ?? sample.workbenchSessionId ?? sample.routeSessionId ?? sample.activeSessionId ?? null, + durationText: limitText(card.durationText, 120), + activityText: limitText(card.activityText, 120), + totalElapsedSeconds: totalElapsedValues.length > 0 ? Math.max(...totalElapsedValues) : null, + recentUpdateSeconds: recentUpdateValues.length > 0 ? Math.max(...recentUpdateValues) : null, + terminal, + textHash: card.textHash ?? sha256(text), + textPreview: limitText(text, 180), + valuesRedacted: true + }; + cardRows.push(row); + if (row.totalElapsedSeconds === null) missingElapsed.push(row); + if (!terminal && row.recentUpdateSeconds === null) missingRecentUpdate.push(row); + } + } + const roundCompletion = buildRoundCompletionMetrics(samples, timeline, turnTiming); + return { + summary: { + cardSampleCount: cardRows.length, + terminalCardSampleCount: cardRows.filter((item) => item.terminal).length, + runningCardSampleCount: cardRows.filter((item) => !item.terminal).length, + missingElapsedCount: missingElapsed.length, + missingRecentUpdateCount: missingRecentUpdate.length, + roundCompletionEventCount: roundCompletion.events.length, + roundCompletionElapsedMismatchCount: roundCompletion.elapsedMismatches.length, + roundCompletionFinalResponseMissingCount: roundCompletion.finalResponseMissing.length, + roundCompletionPostTimingChangeCount: roundCompletion.postCompletionTimingChanges.length, + roundCompletionPostRecentUpdateVisibleCount: roundCompletion.postCompletionRecentUpdateVisible.length, + elapsedMismatchToleranceSeconds: alertThresholds.turnTimingSampleSlackSeconds, + }, + cardSamples: cardRows.slice(0, 200), + missingElapsed: missingElapsed.slice(0, 200), + missingRecentUpdate: missingRecentUpdate.slice(0, 200), + roundCompletion, + valuesRedacted: true + }; +} + +function codeAgentCardsForSample(sample) { + const turnCards = (Array.isArray(sample?.turns) ? sample.turns : []) + .map((item) => ({ ...item, source: item?.source || "turn" })) + .filter(isCodeAgentCardLike); + if (turnCards.length > 0) return turnCards; + return (Array.isArray(sample?.messages) ? sample.messages : []) + .map((item) => ({ ...item, source: item?.source || "message" })) + .filter(isCodeAgentCardLike); +} + +function isCodeAgentCardLike(item) { + const text = codeAgentCardText(item); + const role = String(item?.dataRole || item?.role || "").toLowerCase(); + if (/agent|assistant/u.test(role)) return true; + return /Code Agent|运行记录|耗时|最近\s*(?:\d+|一|两|三)|轮次完成|trace_id=trc_/iu.test(text); +} + +function codeAgentCardText(item) { + return [ + item?.durationText, + item?.activityText, + item?.text, + item?.textPreview, + item?.status + ].map((value) => String(value || "")).filter((value) => value.trim().length > 0).join("\n"); +} + +function isCodeAgentCardTerminal(item) { + const status = String(item?.status ?? item?.state ?? item?.phase ?? "").trim().toLowerCase().replace(/_/gu, "-"); + if (isActiveTurnStatus(status)) return false; + if (isTerminalTurnStatus(status)) return true; + if (item?.terminal === true || item?.sealed === true) return true; + const text = codeAgentCardText(item); + return isTerminalTraceText(text) || /轮次完成|轮次失败|轮次取消|已记录|completed|failed|canceled|cancelled|blocked/iu.test(text); +} + +function isActiveTurnStatus(value) { + const status = String(value ?? "").trim().toLowerCase().replace(/_/gu, "-"); + return ["pending", "running", "queued", "admitted", "dispatching", "in-progress", "inprogress", "executing", "progress", "thinking", "working", "active", "streaming", "created", "started"].includes(status); +} + +function buildRoundCompletionMetrics(samples, timeline, turnTiming) { + const events = []; + const elapsedMismatchToleranceSeconds = Math.max(5, Number(alertThresholds.turnTimingSampleSlackSeconds || 0)); + for (let index = 0; index < (Array.isArray(samples) ? samples : []).length; index += 1) { + const sample = samples[index]; + const timelineItem = timeline[index] || {}; + for (const event of roundCompletionEventsForSample(sample, timelineItem)) events.push(event); + } + const completionEvents = dedupeRoundCompletionEvents(events); + const elapsedMismatches = []; + const finalResponseMissing = []; + const postCompletionTimingChanges = []; + const postCompletionRecentUpdateVisible = []; + for (const event of completionEvents) { + const sampleIndex = samples.findIndex((sample) => sample?.seq === event.seq && sample?.pageRole === event.pageRole && sample?.pageId === event.pageId); + const sameSample = sampleIndex >= 0 ? samples[sampleIndex] : null; + const sameTimelineItem = sampleIndex >= 0 ? timeline[sampleIndex] || {} : {}; + const cards = sameSample ? cardMetricItemsForCompletion(sameSample, sameTimelineItem, event) : []; + const bestCard = cards.filter((card) => Number.isFinite(Number(card.totalElapsedSeconds)))[0] || null; + if (Number.isFinite(Number(event.elapsedSeconds)) && bestCard) { + const delta = Math.abs(Number(bestCard.totalElapsedSeconds) - Number(event.elapsedSeconds)); + if (delta > elapsedMismatchToleranceSeconds) { + elapsedMismatches.push({ + ...eventRef(event), + cardTotalElapsedSeconds: Number(bestCard.totalElapsedSeconds), + completionElapsedSeconds: Number(event.elapsedSeconds), + deltaSeconds: Number(delta.toFixed(3)), + toleranceSeconds: elapsedMismatchToleranceSeconds, + cardTraceId: bestCard.traceId ?? null, + cardMessageId: bestCard.messageId ?? null, + valuesRedacted: true + }); + } + } + if (!(sameSample && sampleHasTerminalAgentResultCard(sameSample, event)) && !(sameSample && sampleHasVisibleFinalResponse(sameSample, event)) && !hasFinalResponseAfterCompletion(samples, timeline, event)) { + finalResponseMissing.push({ + ...eventRef(event), + completionElapsedSeconds: event.elapsedSeconds, + finalResponseProbe: sameSample ? terminalAgentResultProbe(sameSample, event) : { ok: false, reason: "sample-not-found" }, + valuesRedacted: true + }); + } + const postTiming = detectPostCompletionTimingChanges(turnTiming, event); + postCompletionTimingChanges.push(...postTiming.timingChanges); + postCompletionRecentUpdateVisible.push(...postTiming.recentUpdateVisible); + } + return { + events: completionEvents.slice(0, 200), + elapsedMismatches: dedupeRoundCompletionRows(elapsedMismatches).slice(0, 200), + finalResponseMissing: dedupeRoundCompletionRows(finalResponseMissing).slice(0, 200), + postCompletionTimingChanges: dedupeRoundCompletionRows(postCompletionTimingChanges).slice(0, 200), + postCompletionRecentUpdateVisible: dedupeRoundCompletionRows(postCompletionRecentUpdateVisible).slice(0, 200), + valuesRedacted: true + }; +} + +function roundCompletionEventsForSample(sample, timelineItem) { + const rows = []; + for (const item of traceTimingRowsForSample(sample, timelineItem)) { + const text = String(item?.preview || item?.text || item?.textPreview || ""); + if (!traceRowIsRoundCompletionText(text, item?.eventKind || "")) continue; + const elapsedValues = [ + Number(item?.totalSeconds), + ...parseTotalElapsedSeconds(text).filter(Number.isFinite) + ].filter(Number.isFinite); + const attributed = inferRoundCompletionCard(sample, text); + rows.push({ + ...ref(sample), + promptIndex: timelineItem.promptIndex ?? 0, + traceId: item?.traceId ?? firstTraceId([text]) ?? attributed?.traceId ?? null, + messageId: item?.messageId ?? attributed?.messageId ?? null, + attributed: Boolean(item?.traceId || item?.messageId || attributed?.traceId || attributed?.messageId), + traceRowIndex: item?.rowIndex ?? item?.index ?? null, + elapsedSeconds: elapsedValues.length > 0 ? Math.max(...elapsedValues) : null, + textHash: item?.textHash ?? sha256(text), + preview: limitText(text, 180), + valuesRedacted: true + }); + } + return rows; +} + +function inferRoundCompletionCard(sample, text) { + const cards = codeAgentCardsForSample(sample) + .filter((card) => isCodeAgentCardTerminal(card)) + .filter((card) => card?.traceId || card?.messageId); + const textHash = sha256(String(text || "")); + const direct = cards.filter((card) => { + const cardText = codeAgentCardText(card); + return card?.textHash === textHash || cardText.includes(String(text || "").slice(0, 80)) || /轮次完成/iu.test(cardText); + }); + const candidates = direct.length > 0 ? direct : cards; + if (candidates.length !== 1) return null; + const card = candidates[0]; + return { traceId: card.traceId ?? null, messageId: card.messageId ?? card.id ?? null }; +} + +function cardMetricItemsForCompletion(sample, timelineItem, event) { + const metrics = turnMetricItems(sample, timelineItem) + .filter((item) => item.promptIndex === event.promptIndex) + .filter((item) => item.pageRole === event.pageRole || !event.pageRole) + .filter((item) => item.pageId === event.pageId || !event.pageId); + if (!event.traceId && !event.messageId) { + const withElapsed = metrics.filter((item) => Number.isFinite(Number(item.totalElapsedSeconds))); + return withElapsed.length === 1 ? withElapsed : []; + } + return metrics + .filter((item) => !event.traceId || !item.traceId || item.traceId === event.traceId) + .filter((item) => !event.messageId || !item.messageId || item.messageId === event.messageId) + .sort((left, right) => { + const leftTraceMatch = event.traceId && left.traceId === event.traceId ? 0 : 1; + const rightTraceMatch = event.traceId && right.traceId === event.traceId ? 0 : 1; + const leftMessageMatch = event.messageId && left.messageId === event.messageId ? 0 : 1; + const rightMessageMatch = event.messageId && right.messageId === event.messageId ? 0 : 1; + return leftTraceMatch - rightTraceMatch || leftMessageMatch - rightMessageMatch || String(left.source || "").localeCompare(String(right.source || "")); + }); +} + +function hasFinalResponseAfterCompletion(samples, timeline, event) { + if (!event.traceId && !event.messageId && !event.promptIndex) return true; + const eventTsMs = Date.parse(String(event.ts || "")); + for (let index = 0; index < (Array.isArray(samples) ? samples : []).length; index += 1) { + const sample = samples[index]; + const tsMs = Date.parse(String(sample?.ts || "")); + if (Number.isFinite(eventTsMs) && Number.isFinite(tsMs) && tsMs < eventTsMs) continue; + if (sample?.pageRole !== event.pageRole) continue; + const sampleSession = sample?.routeSessionId || sample?.activeSessionId || null; + const eventSession = event.routeSessionId || event.activeSessionId || null; + if (sampleSession && eventSession && sampleSession !== eventSession) continue; + const promptIndex = timeline[index]?.promptIndex ?? 0; + if (!event.traceId && !event.messageId && event.promptIndex && promptIndex !== event.promptIndex) continue; + if (sampleHasTerminalAgentResultCard(sample, event)) return true; + if (sampleHasVisibleFinalResponse(sample, event)) return true; + } + return false; +} + +function sampleHasTerminalAgentResultCard(sample, event = {}) { + return terminalAgentResultProbe(sample, event).ok === true; +} + +function terminalAgentResultProbe(sample, event = {}) { + const decisions = []; + for (const item of codeAgentCardsForSample(sample)) { + const text = normalizedText(codeAgentCardText(item)); + const traceMatched = Boolean(event.traceId && item?.traceId && item.traceId === event.traceId); + const decision = { + source: item?.source || null, + role: item?.role ?? null, + dataRole: item?.dataRole ?? null, + status: item?.status ?? null, + traceId: item?.traceId ?? null, + messageId: item?.messageId ?? null, + textBytes: Buffer.byteLength(text), + valuesRedacted: true + }; + if (event.traceId && item?.traceId && item.traceId !== event.traceId) { + decisions.push({ ...decision, decision: "skip-trace" }); + continue; + } + if (!traceMatched && event.messageId && item?.messageId && item.messageId !== event.messageId) { + decisions.push({ ...decision, decision: "skip-message" }); + continue; + } + if (!isAssistantFinalResponseCandidate(item, item?.source || "turn")) { + decisions.push({ ...decision, decision: "skip-role" }); + continue; + } + if (!isCodeAgentCardTerminal(item)) { + decisions.push({ ...decision, decision: "skip-non-terminal" }); + continue; + } + if (text.length < 24) { + decisions.push({ ...decision, decision: "skip-short" }); + continue; + } + decisions.push({ ...decision, decision: "accept-terminal-agent-card" }); + return { ok: true, decisions: decisions.slice(-8), valuesRedacted: true }; + } + return { ok: false, decisions: decisions.slice(-8), valuesRedacted: true }; +} + +function sampleHasVisibleFinalResponse(sample, event = {}) { + for (const [groupName, group] of [["messages", sample?.messages], ["turns", sample?.turns]]) { + if (!Array.isArray(group)) continue; + for (const item of group) { + const traceMatched = Boolean(event.traceId && item?.traceId && item.traceId === event.traceId); + if (event.traceId && item?.traceId && item.traceId !== event.traceId) continue; + if (!traceMatched && event.messageId && item?.messageId && item.messageId !== event.messageId) continue; + if (!isAssistantFinalResponseCandidate(item, groupName)) continue; + const text = normalizedText([item?.text, item?.textPreview].filter(Boolean).join(" ")); + if (text.length < 24) continue; + if (groupName === "messages" && isTerminalTurnStatus(item?.status)) return true; + if (isDiagnosticText(text)) continue; + if (isFinalResultText(text)) return true; + if (/运行记录/iu.test(text) && /(?:已完成|完成|新增|实现|验证|测试|结果|README|文件|summary)/iu.test(text)) return true; + } + } + return false; +} + +function normalizedDomRole(item) { + return String(item?.dataRole ?? item?.role ?? item?.ariaRole ?? "") + .trim() + .toLowerCase() + .replace(/[_\s]+/gu, "-"); +} + +function isAssistantFinalResponseCandidate(item, groupName) { + const role = normalizedDomRole(item); + if (/agent|assistant|code-agent|bot/iu.test(role)) return true; + if (/user|human|client|prompt/iu.test(role)) return false; + if (groupName === "turns") return isCodeAgentCardLike(item); + const text = codeAgentCardText(item); + if (isTerminalTurnStatus(item?.status) && /Code Agent|运行记录|assistant|agent/iu.test(text)) return true; + return false; +} + +function detectPostCompletionTimingChanges(turnTiming, event) { + const timingChanges = []; + const recentUpdateVisible = []; + if (!event.traceId && !event.messageId) return { timingChanges, recentUpdateVisible }; + const rows = Array.isArray(turnTiming?.rows) ? turnTiming.rows : []; + const columns = Array.isArray(turnTiming?.columns) ? turnTiming.columns : []; + const eventTsMs = Date.parse(String(event.ts || "")); + for (const column of columns) { + if (column.pageRole && event.pageRole && column.pageRole !== event.pageRole) continue; + if (event.traceId && column.traceId && column.traceId !== event.traceId) continue; + if (event.messageId && column.messageId && column.messageId !== event.messageId) continue; + if (event.promptIndex && column.promptIndex && column.promptIndex !== event.promptIndex && column.lastPromptIndex !== event.promptIndex) continue; + let previousTotal = null; + let previousRecent = null; + for (const row of rows) { + const rowTsMs = Date.parse(String(row.ts || "")); + if (Number.isFinite(eventTsMs) && Number.isFinite(rowTsMs) && rowTsMs < eventTsMs) continue; + if (row.pageRole && event.pageRole && row.pageRole !== event.pageRole) continue; + const cell = row.cells?.[column.id]; + if (!cell) continue; + if (event.traceId && cell.traceId && cell.traceId !== event.traceId) continue; + if (event.messageId && cell.messageId && cell.messageId !== event.messageId) continue; + const total = cell.totalElapsedSeconds === null || cell.totalElapsedSeconds === undefined ? NaN : Number(cell.totalElapsedSeconds); + if (Number.isFinite(total)) { + if (previousTotal && Math.abs(total - previousTotal.value) > alertThresholds.turnTimingSampleSlackSeconds) { + timingChanges.push({ + ...eventRef(event), + columnId: column.id, + columnLabel: column.label, + metric: "totalElapsedSeconds", + fromSeq: previousTotal.seq, + fromTs: previousTotal.ts, + fromValue: previousTotal.value, + toSeq: row.seq ?? null, + toTs: row.ts ?? null, + toValue: total, + delta: Number((total - previousTotal.value).toFixed(3)), + toleranceSeconds: alertThresholds.turnTimingSampleSlackSeconds, + traceId: cell.traceId ?? column.traceId ?? event.traceId ?? null, + messageId: cell.messageId ?? column.messageId ?? null, + valuesRedacted: true + }); + } + previousTotal = { value: total, seq: row.seq ?? null, ts: row.ts ?? null }; + } + const recent = cell.recentUpdateSeconds === null || cell.recentUpdateSeconds === undefined ? NaN : Number(cell.recentUpdateSeconds); + if (Number.isFinite(recent)) { + recentUpdateVisible.push({ + ...eventRef(event), + columnId: column.id, + columnLabel: column.label, + metric: "recentUpdateSeconds", + seq: row.seq ?? null, + ts: row.ts ?? null, + value: recent, + traceId: cell.traceId ?? column.traceId ?? event.traceId ?? null, + messageId: cell.messageId ?? column.messageId ?? null, + valuesRedacted: true + }); + if (previousRecent && recent !== previousRecent.value) { + timingChanges.push({ + ...eventRef(event), + columnId: column.id, + columnLabel: column.label, + metric: "recentUpdateSeconds", + fromSeq: previousRecent.seq, + fromTs: previousRecent.ts, + fromValue: previousRecent.value, + toSeq: row.seq ?? null, + toTs: row.ts ?? null, + toValue: recent, + delta: Number((recent - previousRecent.value).toFixed(3)), + traceId: cell.traceId ?? column.traceId ?? event.traceId ?? null, + messageId: cell.messageId ?? column.messageId ?? null, + valuesRedacted: true + }); + } + previousRecent = { value: recent, seq: row.seq ?? null, ts: row.ts ?? null }; + } + } + } + return { timingChanges, recentUpdateVisible }; +} + +function dedupeRoundCompletionEvents(rows) { + const result = []; + const seen = new Set(); + for (const row of Array.isArray(rows) ? rows : []) { + const key = [ + row?.pageRole ?? "", + row?.pageId ?? "", + row?.promptIndex ?? "", + row?.traceId ?? "", + row?.messageId ?? "", + row?.textHash ?? "", + row?.elapsedSeconds ?? "" + ].join("|"); + if (seen.has(key)) continue; + seen.add(key); + result.push(row); + } + return result; +} + +function eventRef(event) { + return { + seq: event?.seq ?? null, + sampleGroupSeq: event?.sampleGroupSeq ?? null, + ts: event?.ts ?? null, + pageRole: event?.pageRole ?? null, + pageId: event?.pageId ?? null, + routeSessionId: event?.routeSessionId ?? null, + activeSessionId: event?.activeSessionId ?? null, + promptIndex: event?.promptIndex ?? null, + traceId: event?.traceId ?? null, + messageId: event?.messageId ?? null, + }; +} + +function dedupeRoundCompletionRows(rows) { + const result = []; + const seen = new Set(); + for (const row of Array.isArray(rows) ? rows : []) { + const key = [ + row?.seq ?? row?.fromSeq ?? "", + row?.toSeq ?? "", + row?.pageRole ?? "", + row?.promptIndex ?? "", + row?.traceId ?? "", + row?.metric ?? "", + row?.textHash ?? "", + row?.columnId ?? "" + ].join("|"); + if (seen.has(key)) continue; + seen.add(key); + result.push(row); + } + return result; +} + +function turnMetricItems(sample, timelineItem) { + const promptIndex = timelineItem.promptIndex ?? 0; + const pageRole = sample?.pageRole || "control"; + const pageId = sample?.pageId || "unknown-page"; + const sessionKey = pageRole + ":" + pageId + ":" + (sample?.routeSessionId || sample?.activeSessionId || "unknown-session"); + const roundKey = String(promptIndex); + const items = []; + if (Array.isArray(sample?.turns) && sample.turns.length > 0) { + for (const turn of sample.turns) { + const texts = turnTexts(turn); + const totalElapsedValues = texts.flatMap(parseTotalElapsedSeconds).filter(Number.isFinite); + const recentUpdateValues = texts.flatMap(parseRecentUpdateSeconds).filter(Number.isFinite); + const traceId = turn.traceId || firstTraceId(texts); + const messageId = turn.messageId || null; + const turnId = turn.turnId || traceId || null; + const stableId = traceId || messageId || turnId || null; + const domIndex = Number.isFinite(Number(turn.index)) ? Number(turn.index) : items.length; + const key = stableId + ? "timing:" + sessionKey + ":id-" + stableId + : "turn:" + sessionKey + ":round-" + roundKey + ":dom-index-" + String(domIndex); + items.push({ + key, + source: "turn", + pageRole, + pageId, + promptIndex, + traceId, + messageId, + turnId, + domIndex, + status: turn.status ?? null, + totalElapsedSeconds: totalElapsedValues.length > 0 ? Math.max(...totalElapsedValues) : null, + recentUpdateSeconds: recentUpdateValues.length > 0 ? Math.max(...recentUpdateValues) : null, + textHash: turn.textHash || sha256(texts.join("\n")) + }); + } + return items; + } + if (Array.isArray(sample?.messages) && sample.messages.length > 0) { + for (const message of sample.messages) { + const text = [message?.durationText, message?.activityText].map((value) => String(value || "")).filter((value) => value.trim().length > 0).join("\n"); + const totalElapsedValues = parseTotalElapsedSeconds(text).filter(Number.isFinite); + const recentUpdateValues = parseRecentUpdateSeconds(text).filter(Number.isFinite); + if (totalElapsedValues.length === 0 && recentUpdateValues.length === 0) continue; + const domIndex = Number.isFinite(Number(message.index)) ? Number(message.index) : items.length; + const traceId = message.traceId || firstTraceId([text]); + const messageId = message.messageId || null; + const stableId = traceId || messageId || message.turnId || null; + items.push({ + key: stableId + ? "timing:" + sessionKey + ":id-" + stableId + : "message:" + sessionKey + ":round-" + roundKey + ":dom-index-" + String(domIndex), + source: "message", + pageRole, + pageId, + promptIndex, + traceId, + messageId, + turnId: message.turnId || traceId || null, + domIndex, + status: message.status ?? null, + totalElapsedSeconds: totalElapsedValues.length > 0 ? Math.max(...totalElapsedValues) : null, + recentUpdateSeconds: recentUpdateValues.length > 0 ? Math.max(...recentUpdateValues) : null, + textHash: message.textHash || sha256(text) + }); + } + if (items.length > 0) return items; + } + if (timelineItem.totalElapsedSeconds !== null || timelineItem.recentUpdateSeconds !== null) { + return [{ + key: "aggregate:" + sessionKey + ":round-" + roundKey, + source: "aggregate", + pageRole, + pageId, + promptIndex, + traceId: null, + messageId: null, + domIndex: null, + status: null, + totalElapsedSeconds: timelineItem.totalElapsedSeconds ?? null, + recentUpdateSeconds: timelineItem.recentUpdateSeconds ?? null, + textHash: timelineItem.textDigest ?? null + }]; + } + return []; +} + +function turnTexts(turn) { + return [ + turn?.durationText, + turn?.activityText + ].map((value) => String(value || "")).filter((value) => value.trim().length > 0); +} + +function firstTraceId(texts) { + for (const text of texts) { + const match = String(text || "").match(/\btrc_[A-Za-z0-9_-]+\b/u); + if (match) return match[0]; + } + return null; +} + +function buildRoundMetricSummaries(timeline, promptCommands, timing = {}) { + const rounds = []; + const timingColumns = Array.isArray(timing.columns) ? timing.columns : []; + const timingRows = Array.isArray(timing.rows) ? timing.rows : []; + const nonMonotonic = Array.isArray(timing.nonMonotonic) ? timing.nonMonotonic : []; + const totalElapsedForwardJumps = Array.isArray(timing.totalElapsedForwardJumps) ? timing.totalElapsedForwardJumps : []; + const elapsedZeroResets = Array.isArray(timing.elapsedZeroResets) ? timing.elapsedZeroResets : []; + const terminalElapsedGrowth = Array.isArray(timing.terminalElapsedGrowth) ? timing.terminalElapsedGrowth : []; + const recentUpdateResets = Array.isArray(timing.recentUpdateResets) ? timing.recentUpdateResets : []; + const recentUpdateSteps = Array.isArray(timing.recentUpdateSteps) ? timing.recentUpdateSteps : []; + for (let index = 0; index < promptCommands.length; index += 1) { + const promptIndex = index + 1; + const items = timeline.filter((item) => item.promptIndex === promptIndex); + const aggregateTotalElapsed = items.map((item) => item.totalElapsedSeconds).filter((value) => value !== null); + const aggregateRecentUpdate = items.map((item) => item.recentUpdateSeconds).filter((value) => value !== null); + const promptTurnTiming = roundPromptTurnTimingValues(timingRows, timingColumns, promptIndex, { + firstSeq: items[0]?.seq ?? null, + firstSampleAt: items[0]?.ts ?? null + }); + const totalElapsed = promptTurnTiming.totalElapsed.length > 0 ? promptTurnTiming.totalElapsed : aggregateTotalElapsed; + const recentUpdate = promptTurnTiming.recentUpdate.length > 0 ? promptTurnTiming.recentUpdate : aggregateRecentUpdate; + const loadingCounts = items.map((item) => Number(item.loadingCount ?? 0)).filter(Number.isFinite); + const loadingOwners = new Set(); + for (const item of items) { + for (const owner of Array.isArray(item.loadingOwners) ? item.loadingOwners : []) { + if (owner?.ownerKey) loadingOwners.add(owner.ownerKey); + } + } + const timingAnomalies = nonMonotonic.filter((item) => item.promptIndex === promptIndex); + const timingForwardJumps = totalElapsedForwardJumps.filter((item) => item.promptIndex === promptIndex); + const timingZeroResets = elapsedZeroResets.filter((item) => item.promptIndex === promptIndex); + const timingTerminalGrowth = terminalElapsedGrowth.filter((item) => item.promptIndex === promptIndex); + const timingResets = recentUpdateResets.filter((item) => item.promptIndex === promptIndex); + const timingSteps = recentUpdateSteps.filter((item) => item.promptIndex === promptIndex); + const terminalGrowthDeltas = timingTerminalGrowth.map((item) => Number(item.delta)).filter((value) => Number.isFinite(value) && value > 0); + const timingStepDeltas = timingSteps.map((item) => Number(item.delta)).filter((value) => Number.isFinite(value) && value >= 0); + const timingStepExcess = timingSteps.map((item) => Number(item.excessiveIncreaseSeconds)).filter((value) => Number.isFinite(value) && value > 0); + rounds.push({ + promptIndex, + promptCommandId: promptCommands[index].commandId, + promptTextHash: promptCommands[index].textHash, + promptTextBytes: promptCommands[index].textBytes, + promptCompletedAt: promptCommands[index].ts, + sampleCount: items.length, + firstSeq: items[0]?.seq ?? null, + lastSeq: items[items.length - 1]?.seq ?? null, + firstSampleAt: items[0]?.ts ?? null, + lastSampleAt: items[items.length - 1]?.ts ?? null, + withTotalElapsed: totalElapsed.length, + withRecentUpdate: recentUpdate.length, + loadingSamples: loadingCounts.filter((value) => value > 0).length, + maxLoadingCount: loadingCounts.length > 0 ? Math.max(...loadingCounts) : 0, + loadingOwnerCount: loadingOwners.size, + maxTotalElapsedSeconds: totalElapsed.length > 0 ? Math.max(...totalElapsed) : null, + lastTotalElapsedSeconds: lastNonNull(totalElapsed), + maxRecentUpdateSeconds: recentUpdate.length > 0 ? Math.max(...recentUpdate) : null, + lastRecentUpdateSeconds: lastNonNull(recentUpdate), + diagnosticSamples: items.filter((item) => item.diagnosticSeen).length, + terminalSamples: items.filter((item) => item.terminalSeen).length, + finalTextSamples: items.filter((item) => item.finalResultTextSeen).length, + turnTimingNonMonotonicCount: timingAnomalies.length, + turnTimingTotalElapsedDecreaseCount: timingAnomalies.filter((item) => item.metric === "totalElapsedSeconds").length, + turnTimingTotalElapsedZeroResetCount: timingZeroResets.length, + turnTimingTotalElapsedForwardJumpCount: timingForwardJumps.length, + turnTimingTotalElapsedForwardJumpMaxSeconds: maxPositiveDelta(timingForwardJumps), + turnTimingTerminalElapsedGrowthCount: timingTerminalGrowth.length, + turnTimingTerminalElapsedGrowthMaxSeconds: terminalGrowthDeltas.length > 0 ? Math.max(...terminalGrowthDeltas) : 0, + turnTimingRecentUpdateJumpCount: timingAnomalies.filter((item) => item.metric === "recentUpdateSeconds" && item.anomaly === "jump").length, + turnTimingRecentUpdateSawtoothJumpCount: timingAnomalies.filter((item) => item.metric === "recentUpdateSeconds" && item.anomaly === "jump").length, + turnTimingRecentUpdateStepCount: timingSteps.length, + turnTimingRecentUpdateMaxIncreaseSeconds: timingStepDeltas.length > 0 ? Math.max(...timingStepDeltas) : null, + turnTimingRecentUpdateMaxExcessSeconds: timingStepExcess.length > 0 ? Math.max(...timingStepExcess) : 0, + turnTimingRecentUpdateResetCount: timingResets.length, + turnTimingRecentUpdateDecreaseCount: timingResets.length + }); + } + return rounds; +} + +function roundPromptTurnTimingValues(rows, columns, promptIndex, round = {}) { + const roundFirstSeq = Number(round.firstSeq); + const roundFirstMs = Date.parse(String(round.firstSampleAt ?? "")); + const seqSlack = 3; + const timeSlackMs = 3000; + const promptColumnIds = new Set(columns + .filter((column) => { + if (Number(column?.promptIndex) === Number(promptIndex)) return true; + const firstSeq = Number(column?.firstSeq); + if (Number.isFinite(roundFirstSeq) && Number.isFinite(firstSeq) && firstSeq >= roundFirstSeq - seqSlack) return true; + const firstMs = Date.parse(String(column?.firstTs ?? "")); + return Number.isFinite(roundFirstMs) && Number.isFinite(firstMs) && firstMs >= roundFirstMs - timeSlackMs; + }) + .map((column) => String(column?.id || "")) + .filter(Boolean)); + if (promptColumnIds.size === 0) return { totalElapsed: [], recentUpdate: [] }; + const totalElapsed = []; + const recentUpdate = []; + for (const row of rows) { + if (Number(row?.promptIndex ?? 0) !== Number(promptIndex)) continue; + const cells = row?.cells && typeof row.cells === "object" ? row.cells : {}; + for (const [columnId, cell] of Object.entries(cells)) { + if (!promptColumnIds.has(columnId)) continue; + const total = Number(cell?.totalElapsedSeconds); + if (Number.isFinite(total)) totalElapsed.push(total); + const recent = Number(cell?.recentUpdateSeconds); + if (Number.isFinite(recent)) recentUpdate.push(recent); + } + } + return { totalElapsed, recentUpdate }; +} + +function sampleTexts(sample) { + const rows = []; + for (const group of [sample?.messages, sample?.traceRows, sample?.diagnostics]) { + if (!Array.isArray(group)) continue; + for (const item of group) { + const text = String(item?.textPreview || ""); + if (text.trim()) rows.push(text); + } + } + return rows; +} + +function sampleTurnTimingTexts(sample) { + const rows = []; + for (const group of [sample?.turns, sample?.messages]) { + if (!Array.isArray(group)) continue; + for (const item of group) { + for (const value of [item?.durationText, item?.activityText]) { + const text = String(value || ""); + if (text.trim()) rows.push(text); + } + } + } + return rows; +} + +function parseTotalElapsedSeconds(text) { + const values = []; + for (const match of String(text || "").matchAll(/(?:总耗时|耗时)\s*[=::]?\s*(\d{1,2}):(\d{2}):(\d{2})/giu)) { + values.push(Number(match[1]) * 3600 + Number(match[2]) * 60 + Number(match[3])); + } + for (const match of String(text || "").matchAll(/(?:总耗时|耗时)\s*[=::]?\s*(\d{1,2}):(\d{2})(?!:)/giu)) { + values.push(Number(match[1]) * 60 + Number(match[2])); + } + for (const match of String(text || "").matchAll(/(?:总耗时|耗时)\s*[=::]?\s*(?:(\d+)\s*天)?\s*(?:(\d+)\s*小时)?\s*(?:(\d+)\s*(?:分钟|分))?\s*(?:(\d+)\s*秒)?/giu)) { + const days = Number(match[1] || 0); + const hours = Number(match[2] || 0); + const minutes = Number(match[3] || 0); + const seconds = Number(match[4] || 0); + if (days || hours || minutes || seconds || /(?:天|小时|分钟|分|秒)/u.test(match[0] || "")) values.push(days * 86400 + hours * 3600 + minutes * 60 + seconds); + } + return values; +} + +function lastNonNull(values) { + for (let index = values.length - 1; index >= 0; index -= 1) if (values[index] !== null && values[index] !== undefined) return values[index]; + return null; +} + +function parseRecentUpdateSeconds(text) { + const values = []; + for (const match of String(text || "").matchAll(/最近\s*(?:(\d+)\s*天)?\s*(?:(\d+)\s*小时)?\s*(?:(\d+)\s*(?:分钟|分))?\s*(?:(\d+)\s*秒)?\s*前/giu)) { + const days = Number(match[1] || 0); + const hours = Number(match[2] || 0); + const minutes = Number(match[3] || 0); + const seconds = Number(match[4] || 0); + values.push(days * 86400 + hours * 3600 + minutes * 60 + seconds); + } + return values; +} + +function latestPromptIndex(promptTimes, tsMs) { + let index = 0; + for (let i = 0; i < promptTimes.length; i += 1) { + if (promptTimes[i] <= tsMs) index = i + 1; + else break; + } + return index; +} + +function promptIndexForTs(promptTimes, ts) { + const tsMs = Date.parse(ts); + return Number.isFinite(tsMs) ? latestPromptIndex(promptTimes, tsMs) : 0; +} + +function buildTransitions(samples) { + const rows = []; + let last = null; + for (const sample of samples) { + const digest = digestSample(sample); + if (digest !== last) { + rows.push({ seq: sample.seq, ts: sample.ts, url: sample.url, routeSessionId: sample.routeSessionId || null, activeSessionId: sample.activeSessionId || null, messageCount: Array.isArray(sample.messages) ? sample.messages.length : 0, traceRowCount: Array.isArray(sample.traceRows) ? sample.traceRows.length : 0, digest }); + last = digest; + } + } + return rows.slice(0, 200); +} + +function detectFinalFlicker(samples) { + const flickers = []; + const lastNonEmptyByScope = new Map(); + for (const sample of samples) { + const scope = finalFlickerScope(sample); + if (!scope) continue; + const messageText = Array.isArray(sample.messages) ? sample.messages.map((item) => item.textPreview || "").join("\n") : ""; + const nonEmpty = messageText.trim().length > 0; + const finalLike = /轮次完成|已记录|已完成第\d+轮|final response|terminal result/iu.test(messageText); + const diagnosticLike = /temporarily|timeout|无法连接|暂时|error|failed|超时/iu.test(messageText); + const lastNonEmpty = lastNonEmptyByScope.get(scope); + if (nonEmpty && finalLike && !diagnosticLike) lastNonEmptyByScope.set(scope, { sample, messageText }); + if (lastNonEmpty && nonEmpty && diagnosticLike) flickers.push({ scope, from: ref(lastNonEmpty.sample), to: ref(sample) }); + if (lastNonEmpty && !nonEmpty) flickers.push({ scope, from: ref(lastNonEmpty.sample), to: ref(sample), reason: "text-disappeared" }); + } + return flickers; +} + +function finalFlickerScope(sample) { + const pathname = samplePathname(sample); + const sessionId = sample?.routeSessionId || sample?.activeSessionId || workbenchSessionIdFromPath(pathname); + if (!sessionId) return null; + if (!pathname.startsWith("/workbench/sessions/" + sessionId)) return null; + return (sample?.pageRole || "control") + ":" + sessionId; +} + +function samplePathname(sample) { + const raw = String(sample?.path || sample?.url || "").trim(); + if (!raw) return ""; + try { + return new URL(raw, "http://hwlab.local").pathname || raw; + } catch { + return raw.split(/[?#]/u, 1)[0] || raw; + } +} + +function workbenchSessionIdFromPath(pathname) { + const match = String(pathname || "").match(/^\/workbench\/sessions\/([^/?#]+)/u); + return match ? match[1] : null; +} + +function detectTerminalZeroElapsed(samples) { + const rows = []; + for (const sample of samples) { + const turns = Array.isArray(sample?.turns) ? sample.turns : []; + const messages = Array.isArray(sample?.messages) ? sample.messages : []; + for (const item of [...turns, ...messages]) { + const text = [item?.durationText, item?.activityText, item?.status, item?.textPreview, item?.text].filter(Boolean).join("\n"); + if (!/(?:Code Agent|运行记录|completed|failed|canceled|blocked)/iu.test(text)) continue; + if (!/(?:completed|failed|canceled|blocked|已完成|失败|取消)/iu.test(text)) continue; + const timingText = [item?.durationText, item?.activityText].filter(Boolean).join("\n"); + const elapsedValues = parseTotalElapsedSeconds(timingText); + if (!elapsedValues.includes(0)) continue; + rows.push({ + ...ref(sample), + status: item?.status ?? null, + messageId: item?.messageId ?? null, + traceId: item?.traceId ?? null, + durationText: item?.durationText ?? null, + activityText: item?.activityText ?? null, + textPreview: limitText(item?.textPreview || item?.text || "", 180) + }); + } + } + return rows; +} + +function detectCrossPageProjectionDiffs(samples) { + const groups = new Map(); + for (const sample of samples) { + const key = sample?.sampleGroupSeq ?? sample?.seq; + if (key === null || key === undefined) continue; + const group = groups.get(key) || {}; + if (sample?.pageRole === "control") group.control = sample; + else if (sample?.pageRole === "observer") group.observer = sample; + groups.set(key, group); + } + const rows = []; + for (const [sampleGroupSeq, group] of groups.entries()) { + const control = group.control; + const observer = group.observer; + if (!control || !observer) continue; + const controlTraceIds = visibleTraceIds(control); + const observerTraceIds = visibleTraceIds(observer); + const missingInObserver = [...controlTraceIds].filter((item) => !observerTraceIds.has(item)); + const controlMessages = Array.isArray(control.messages) ? control.messages.length : 0; + const observerMessages = Array.isArray(observer.messages) ? observer.messages.length : 0; + const controlTraceRows = Array.isArray(control.traceRows) ? control.traceRows.length : 0; + const observerTraceRows = Array.isArray(observer.traceRows) ? observer.traceRows.length : 0; + const controlZero = detectTerminalZeroElapsed([control]).length > 0; + const observerZero = detectTerminalZeroElapsed([observer]).length > 0; + const sameSession = (control.routeSessionId || control.activeSessionId || null) && (control.routeSessionId || control.activeSessionId || null) === (observer.routeSessionId || observer.activeSessionId || null); + const controlMessageDigest = digestMessageTexts(control); + const observerMessageDigest = digestMessageTexts(observer); + const messageTextDigestDiff = controlMessageDigest !== observerMessageDigest; + const projectionDivergent = sameSession && (Math.abs(controlMessages - observerMessages) > 0 || controlZero !== observerZero); + const traceVisibilityDivergent = sameSession && !projectionDivergent && (missingInObserver.length > 0 || Math.abs(controlTraceRows - observerTraceRows) > 0); + if (!projectionDivergent && !traceVisibilityDivergent) continue; + rows.push({ + sampleGroupSeq, + diffKind: traceVisibilityDivergent ? "trace-visibility" : "projection", + control: ref(control), + observer: ref(observer), + controlTraceIds: [...controlTraceIds].slice(0, 8), + observerTraceIds: [...observerTraceIds].slice(0, 8), + missingTraceIdsInObserver: missingInObserver.slice(0, 8), + controlMessageCount: controlMessages, + observerMessageCount: observerMessages, + controlTraceRowCount: controlTraceRows, + observerTraceRowCount: observerTraceRows, + terminalZeroElapsedDiff: controlZero !== observerZero, + messageTextDigestDiff, + controlMessageDigest, + observerMessageDigest + }); + } + return rows; +} + +function mergeCrossPageDiffRows(...groups) { + const rows = []; + const seen = new Set(); + for (const group of groups) { + if (!Array.isArray(group)) continue; + for (const row of group) { + const key = [ + row?.diffKind || "projection", + row?.control?.seq ?? null, + row?.observer?.seq ?? null, + row?.controlMessageCount ?? null, + row?.observerMessageCount ?? null, + row?.controlTraceRowCount ?? null, + row?.observerTraceRowCount ?? null + ].join(":"); + if (seen.has(key)) continue; + seen.add(key); + rows.push(row); + } + } + return rows; +} + +function annotateCrossPageDiffTiming(rows) { + const groups = new Map(); + for (const row of Array.isArray(rows) ? rows : []) { + const controlAt = Date.parse(String(row?.control?.ts || "")); + const observerAt = Date.parse(String(row?.observer?.ts || "")); + const timestamps = [controlAt, observerAt].filter(Number.isFinite); + const startMs = timestamps.length > 0 ? Math.min(...timestamps) : null; + const endMs = timestamps.length > 0 ? Math.max(...timestamps) : null; + const sessionId = row?.control?.routeSessionId || row?.control?.activeSessionId || row?.observer?.routeSessionId || row?.observer?.activeSessionId || "unknown-session"; + const key = [row?.diffKind || "projection", sessionId].join(":"); + const group = groups.get(key) || { rows: [], firstMs: null, lastMs: null }; + const annotated = { + ...row, + sampleStartAt: startMs === null ? null : new Date(startMs).toISOString(), + sampleEndAt: endMs === null ? null : new Date(endMs).toISOString(), + pairSkewMs: startMs === null || endMs === null ? null : endMs - startMs, + }; + group.rows.push(annotated); + if (startMs !== null && (group.firstMs === null || startMs < group.firstMs)) group.firstMs = startMs; + if (endMs !== null && (group.lastMs === null || endMs > group.lastMs)) group.lastMs = endMs; + groups.set(key, group); + } + const result = []; + for (const group of groups.values()) { + const sortedRows = group.rows.slice().sort((a, b) => Number(Date.parse(String(a.sampleStartAt || ""))) - Number(Date.parse(String(b.sampleStartAt || "")))); + const segments = []; + const splitGapMs = Math.max(1000, Number(alertThresholds.crossPageProjectionDivergenceRedMs || alertThresholds.visibleLoadingSlowMs || 10_000)); + for (const row of sortedRows) { + const startMs = Date.parse(String(row.sampleStartAt || "")); + const endMs = Date.parse(String(row.sampleEndAt || row.sampleStartAt || "")); + const last = segments.at(-1); + const lastEndMs = last && last.lastMs !== null ? last.lastMs : null; + if (!last || (Number.isFinite(startMs) && lastEndMs !== null && startMs - lastEndMs > splitGapMs)) { + segments.push({ rows: [row], firstMs: Number.isFinite(startMs) ? startMs : null, lastMs: Number.isFinite(endMs) ? endMs : Number.isFinite(startMs) ? startMs : null }); + continue; + } + last.rows.push(row); + if (Number.isFinite(startMs) && (last.firstMs === null || startMs < last.firstMs)) last.firstMs = startMs; + const effectiveEndMs = Number.isFinite(endMs) ? endMs : Number.isFinite(startMs) ? startMs : null; + if (effectiveEndMs !== null && (last.lastMs === null || effectiveEndMs > last.lastMs)) last.lastMs = effectiveEndMs; + } + for (let segmentIndex = 0; segmentIndex < segments.length; segmentIndex += 1) { + const segment = segments[segmentIndex]; + const observedSpanMs = segment.firstMs === null || segment.lastMs === null ? null : segment.lastMs - segment.firstMs; + for (const row of segment.rows) { + result.push({ + ...row, + segmentIndex, + observedFirstAt: segment.firstMs === null ? null : new Date(segment.firstMs).toISOString(), + observedLastAt: segment.lastMs === null ? null : new Date(segment.lastMs).toISOString(), + observedSpanMs, + }); + } + } + } + return result; +} + +function detectAdjacentCrossPageProjectionDiffs(samples) { + const rows = []; + const ordered = (Array.isArray(samples) ? samples : []).slice().sort((a, b) => Number(a?.seq ?? 0) - Number(b?.seq ?? 0)); + for (let i = 1; i < ordered.length; i += 1) { + const a = ordered[i - 1]; + const b = ordered[i]; + const roles = new Set([a?.pageRole, b?.pageRole]); + if (!roles.has("control") || !roles.has("observer")) continue; + const control = a?.pageRole === "control" ? a : b; + const observer = a?.pageRole === "observer" ? a : b; + const controlSession = control.routeSessionId || control.activeSessionId || null; + const observerSession = observer.routeSessionId || observer.activeSessionId || null; + if (!controlSession || controlSession !== observerSession) continue; + const controlAt = Date.parse(String(control.ts || "")); + const observerAt = Date.parse(String(observer.ts || "")); + if (Number.isFinite(controlAt) && Number.isFinite(observerAt) && Math.abs(controlAt - observerAt) > 1500) continue; + const controlMessages = Array.isArray(control.messages) ? control.messages.length : 0; + const observerMessages = Array.isArray(observer.messages) ? observer.messages.length : 0; + const controlTraceRows = Array.isArray(control.traceRows) ? control.traceRows.length : 0; + const observerTraceRows = Array.isArray(observer.traceRows) ? observer.traceRows.length : 0; + const controlZero = detectTerminalZeroElapsed([control]).length > 0; + const observerZero = detectTerminalZeroElapsed([observer]).length > 0; + const missingInObserver = [...visibleTraceIds(control)].filter((item) => !visibleTraceIds(observer).has(item)); + const controlTraceIds = visibleTraceIds(control); + const observerTraceIds = visibleTraceIds(observer); + const controlMessageDigest = digestMessageTexts(control); + const observerMessageDigest = digestMessageTexts(observer); + const messageTextDigestDiff = controlMessageDigest !== observerMessageDigest; + const projectionDivergent = controlMessages !== observerMessages || controlZero !== observerZero; + const traceVisibilityDivergent = !projectionDivergent && (missingInObserver.length > 0 || controlTraceRows !== observerTraceRows); + if (!projectionDivergent && !traceVisibilityDivergent) continue; + rows.push({ + sampleGroupSeq: control.sampleGroupSeq ?? observer.sampleGroupSeq ?? null, + adjacentPair: true, + diffKind: traceVisibilityDivergent ? "trace-visibility" : "projection", + control: ref(control), + observer: ref(observer), + controlTraceIds: [...controlTraceIds].slice(0, 8), + observerTraceIds: [...observerTraceIds].slice(0, 8), + missingTraceIdsInObserver: missingInObserver.slice(0, 8), + controlMessageCount: controlMessages, + observerMessageCount: observerMessages, + controlTraceRowCount: controlTraceRows, + observerTraceRowCount: observerTraceRows, + terminalZeroElapsedDiff: controlZero !== observerZero, + messageTextDigestDiff, + controlMessageDigest, + observerMessageDigest + }); + } + return rows; +} + +function detectTraceMessageDuplication(samples) { + const rows = []; + for (const sample of samples) { + const finalText = normalizedText((Array.isArray(sample?.messages) ? sample.messages : []).map((item) => item?.textPreview || item?.text || "").join("\n")); + if (finalText.length < 40) continue; + const traceRows = Array.isArray(sample?.traceRows) ? sample.traceRows : []; + for (const row of traceRows) { + const rowTextRaw = String(row?.textPreview || row?.text || ""); + if (!/(?:助手消息|assistant\s+message|assistant)/iu.test(rowTextRaw)) continue; + const rowText = normalizedText(rowTextRaw); + if (rowText.length < 24) continue; + const overlap = longestSharedSubstringLength(finalText, rowText); + if (overlap < 40 && overlap < Math.min(rowText.length, finalText.length) * 0.55) continue; + rows.push({ ...ref(sample), traceId: row?.traceId ?? null, rowIndex: row?.index ?? null, rowTextPreview: limitText(rowTextRaw, 180) }); + } + } + return rows; +} + +function detectTurnTraceIdMissing(samples) { + const rows = []; + for (const sample of samples) { + for (const item of Array.isArray(sample?.turns) ? sample.turns : []) { + const text = [item?.status, item?.durationText, item?.activityText, item?.textPreview, item?.text].filter(Boolean).join("\n"); + if (!/(?:Code Agent|运行记录|耗时|最近)/iu.test(text)) continue; + if (item?.traceId) continue; + rows.push({ ...ref(sample), status: item?.status ?? null, messageId: item?.messageId ?? null, textPreview: limitText(item?.textPreview || item?.text || "", 180) }); + } + } + return rows; +} + +function visibleTraceIds(sample) { + const ids = new Set(); + for (const group of [sample?.turns, sample?.messages, sample?.traceRows]) { + if (!Array.isArray(group)) continue; + for (const item of group) if (item?.traceId) ids.add(String(item.traceId)); + } + return ids; +} + +function digestMessageTexts(sample) { + return sha256((Array.isArray(sample?.messages) ? sample.messages : []).map((item) => item?.textHash || normalizedText(item?.textPreview || item?.text || "")).join("|")); +} + +function normalizedText(value) { + return String(value || "").replace(/\s+/gu, " ").trim(); +} + +function longestSharedSubstringLength(a, b) { + if (!a || !b) return 0; + const left = a.length <= b.length ? a : b; + const right = a.length <= b.length ? b : a; + const max = Math.min(left.length, 280); + let best = 0; + for (let start = 0; start < max; start += 1) { + for (let end = Math.min(max, start + 160); end > start + best; end -= 1) { + if (right.includes(left.slice(start, end))) { + best = end - start; + break; + } + } + } + return best; +} + +function digestSample(sample) { + const messages = Array.isArray(sample.messages) ? sample.messages.map((item) => stableDigestItem(item, ["dataRole", "role", "status", "sessionId", "messageId", "traceId", "turnId"])).join("|") : ""; + const trace = Array.isArray(sample.traceRows) ? sample.traceRows.map((item) => stableDigestItem(item, ["status", "sessionId", "messageId", "traceId", "turnId", "projectedSeq", "sourceSeq", "eventSeq", "eventKind"])).join("|") : ""; + const diagnostics = Array.isArray(sample.diagnostics) ? sample.diagnostics.map((item) => stableDigestItem(item, ["className", "status", "sessionId", "messageId", "traceId", "turnId", "diagnosticCode"])).join("|") : ""; + return sha256((sample.routeSessionId || "") + "|" + (sample.activeSessionId || "") + "|" + messages + "|" + trace + "|" + diagnostics); +} + +function samplePageKey(sample) { + return String(sample?.pageRole || "control") + ":" + String(sample?.pageId || "default"); +} + +function stableDigestItem(item, fields) { + if (!item || typeof item !== "object") return ""; + const identity = fields.map((field) => String(item?.[field] ?? "")).join(":"); + return identity + ":" + stableVisibleDigestText(item?.textPreview || item?.text || item?.textHash || ""); +} + +function stableVisibleDigestText(value) { + return normalizedText(value) + .replace(/\btotal=\d{1,2}:\d{2}:\d{2}\b/giu, "total=") + .replace(/(?:总耗时|耗时)\s*[=::]?\s*(?:(?:\d+\s*天)?\s*(?:\d+\s*小时)?\s*(?:\d+\s*(?:分钟|分))?\s*(?:\d+\s*秒)?|\d{1,2}:\d{2}(?::\d{2})?)/giu, "耗时 ") + .replace(/最近\s*(?:(?:\d+\s*天)?\s*(?:\d+\s*小时)?\s*(?:\d+\s*(?:分钟|分))?\s*(?:\d+\s*秒)?)\s*前/giu, "最近 前"); +} + +function nearCommand(sample, commandTimes, windowMs) { + const ts = Date.parse(sample.ts); + return Number.isFinite(ts) && commandTimes.some((item) => Math.abs(ts - item) <= windowMs); +} + +function sampleRefs(samples, pick) { + const seen = new Set(); + const refs = []; + for (const sample of samples) { + const value = pick(sample); + if (!value || seen.has(value)) continue; + seen.add(value); + refs.push({ ...ref(sample), value }); + } + return refs.slice(0, 20); +} + +function ref(sample) { + if (!sample) return null; + return { seq: sample.seq ?? null, sampleGroupSeq: sample.sampleGroupSeq ?? null, ts: sample.ts ?? null, pageRole: sample.pageRole ?? null, pageId: sample.pageId ?? null, url: sample.url ?? null, routeSessionId: sample.routeSessionId ?? null, activeSessionId: sample.activeSessionId ?? null }; +} + +async function artifactSummary(artifacts) { + const items = artifacts.slice(-30).map((item) => ({ kind: item.kind, reason: item.reason, sampleSeq: item.sampleSeq, path: item.path, sha256: item.sha256, byteCount: item.byteCount })); + return { count: artifacts.length, latest: items }; +} + +function compactManifest(value) { + if (!value) return null; + return { jobId: value.jobId, stateDir: value.stateDir, baseUrl: value.baseUrl, targetPath: value.targetPath, startedAt: value.startedAt, status: value.status, pageAuthority: value.pageAuthority ?? null, sampling: value.sampling, pageProvenance: value.pageProvenance ?? null, safety: value.safety }; +} + +function compactHeartbeat(value) { + if (!value) return null; + return { jobId: value.jobId, pid: value.pid, status: value.status, sampleSeq: value.sampleSeq, commandSeq: value.commandSeq, pageId: value.pageId ?? null, observerPageId: value.observerPageId ?? null, currentUrl: value.currentUrl, observerUrl: value.observerUrl ?? null, observerRefreshIntervalMs: value.observerRefreshIntervalMs ?? null, lastObserverRefreshAt: value.lastObserverRefreshAt ?? null, pageProvenance: value.pageProvenance ?? null, updatedAt: value.updatedAt, uptimeMs: value.uptimeMs }; +} + +function renderTurnTimingTable(sampleMetrics) { + const columns = Array.isArray(sampleMetrics?.turnColumns) ? sampleMetrics.turnColumns : []; + const rows = Array.isArray(sampleMetrics?.turnTimingTable) ? sampleMetrics.turnTimingTable : []; + const disclosure = sampleMetrics?.turnTimingTableDisclosure || null; + if (columns.length === 0 || rows.length === 0) return "- 无 turn 时间表。"; + const header = ["时间戳"]; + for (const column of columns) { + const columnLabel = formatTurnColumnDisplayLabel(column); + header.push(columnLabel + " 总耗时(s)"); + header.push(columnLabel + " 最近更新(s)"); + } + const lines = []; + lines.push("| " + header.map(escapeMarkdownCell).join(" | ") + " |"); + lines.push("| " + header.map(() => "---").join(" | ") + " |"); + for (const row of rows) { + const cells = [row.ts || "-"]; + for (const column of columns) { + const cell = row.cells?.[column.id] || {}; + cells.push(formatMetricCell(cell.totalElapsedSeconds)); + cells.push(formatMetricCell(cell.recentUpdateSeconds)); + } + lines.push("| " + cells.map(escapeMarkdownCell).join(" | ") + " |"); + } + const columnLines = columns.map((column) => "- " + formatTurnColumnDisplayLabel(column) + ": pageRole=" + (column.pageRole || "-") + " pageId=" + (column.pageId || "-") + " source=" + (column.source || "-") + " prompt=" + (column.promptIndex ?? "-") + " lastPrompt=" + (column.lastPromptIndex ?? "-") + " firstSeq=" + (column.firstSeq ?? "-") + " lastSeq=" + (column.lastSeq ?? "-") + " traceId=" + (column.traceId || "-") + " messageId=" + (column.messageId || "-")).join("\n"); + const nonMonotonic = Array.isArray(sampleMetrics?.turnTimingNonMonotonic) ? sampleMetrics.turnTimingNonMonotonic : []; + const nonMonotonicLines = nonMonotonic.length > 0 + ? nonMonotonic.slice(0, 80).map((item) => "- " + formatTurnEventDisplayLabel(item) + " " + item.metric + (item.anomaly ? " " + item.anomaly : "") + " " + (item.fromValue ?? "-") + " -> " + (item.toValue ?? "-") + " delta=" + (item.delta ?? "-") + " sampleDelta=" + (item.sampleDeltaSeconds ?? "-") + " allowed=" + (item.allowedIncreaseSeconds ?? "-") + " seq " + (item.fromSeq ?? "-") + " -> " + (item.toSeq ?? "-") + " ts " + (item.fromTs || "-") + " -> " + (item.toTs || "-") + " traceId=" + (item.traceId || "-")).join("\n") + : "- 未观察到总耗时下降或最近更新异常跳增。"; + const terminalGrowth = Array.isArray(sampleMetrics?.turnTimingTerminalElapsedGrowth) ? sampleMetrics.turnTimingTerminalElapsedGrowth : []; + const terminalGrowthLines = terminalGrowth.length > 0 + ? terminalGrowth.slice(0, 80).map((item) => "- " + formatTurnEventDisplayLabel(item) + " terminal totalElapsed growth " + (item.fromValue ?? "-") + " -> " + (item.toValue ?? "-") + " delta=" + (item.delta ?? "-") + " status " + (item.fromStatus || "-") + " -> " + (item.toStatus || "-") + " seq " + (item.fromSeq ?? "-") + " -> " + (item.toSeq ?? "-") + " ts " + (item.fromTs || "-") + " -> " + (item.toTs || "-") + " traceId=" + (item.traceId || "-")).join("\n") + : "- 未观察到 terminal 后总耗时增长。"; + const elapsedZeroResets = Array.isArray(sampleMetrics?.turnTimingElapsedZeroResets) ? sampleMetrics.turnTimingElapsedZeroResets : []; + const elapsedZeroResetLines = elapsedZeroResets.length > 0 + ? elapsedZeroResets.slice(0, 80).map((item) => "- " + formatTurnEventDisplayLabel(item) + " totalElapsed zero-reset " + (item.fromValue ?? "-") + " -> " + (item.toValue ?? "-") + " delta=" + (item.delta ?? "-") + " seq " + (item.fromSeq ?? "-") + " -> " + (item.toSeq ?? "-") + " ts " + (item.fromTs || "-") + " -> " + (item.toTs || "-") + " traceId=" + (item.traceId || "-")).join("\n") + : "- 未观察到总耗时从非零跳回 0 秒。"; + const totalElapsedForwardJumps = Array.isArray(sampleMetrics?.turnTimingTotalElapsedForwardJumps) ? sampleMetrics.turnTimingTotalElapsedForwardJumps : []; + const totalElapsedForwardJumpLines = totalElapsedForwardJumps.length > 0 + ? totalElapsedForwardJumps.slice(0, 80).map((item) => "- " + formatTurnEventDisplayLabel(item) + " totalElapsed forward-jump " + (item.fromValue ?? "-") + " -> " + (item.toValue ?? "-") + " delta=" + (item.delta ?? "-") + " sampleDelta=" + (item.sampleDeltaSeconds ?? "-") + " allowed=" + (item.allowedIncreaseSeconds ?? "-") + " seq " + (item.fromSeq ?? "-") + " -> " + (item.toSeq ?? "-") + " ts " + (item.fromTs || "-") + " -> " + (item.toTs || "-") + " traceId=" + (item.traceId || "-")).join("\n") + : "- 未观察到总耗时超出采样间隔的异常前跳。"; + const sawtoothJumps = Array.isArray(sampleMetrics?.turnTimingRecentUpdateSawtoothJumps) + ? sampleMetrics.turnTimingRecentUpdateSawtoothJumps + : nonMonotonic.filter((item) => item.metric === "recentUpdateSeconds" && item.anomaly === "jump"); + const sawtoothJumpLines = sawtoothJumps.length > 0 + ? sawtoothJumps.slice(0, 80).map((item) => "- " + formatTurnEventDisplayLabel(item) + " recentUpdate sawtooth-jump " + (item.fromValue ?? "-") + " -> " + (item.toValue ?? "-") + " delta=" + (item.delta ?? "-") + " sampleDelta=" + (item.sampleDeltaSeconds ?? "-") + " allowed=" + (item.allowedIncreaseSeconds ?? "-") + " seq " + (item.fromSeq ?? "-") + " -> " + (item.toSeq ?? "-") + " ts " + (item.fromTs || "-") + " -> " + (item.toTs || "-") + " traceId=" + (item.traceId || "-")).join("\n") + : "- 未观察到最近更新三角波异常跳增。"; + const recentUpdateSteps = Array.isArray(sampleMetrics?.turnTimingRecentUpdateLargestSteps) + ? sampleMetrics.turnTimingRecentUpdateLargestSteps + : Array.isArray(sampleMetrics?.turnTimingRecentUpdateSteps) + ? sampleMetrics.turnTimingRecentUpdateSteps.filter((item) => Number.isFinite(Number(item.delta))).slice().sort((a, b) => Number(b.delta) - Number(a.delta)).slice(0, 200) + : []; + const stepLines = recentUpdateSteps.length > 0 + ? recentUpdateSteps.slice(0, 80).map((item) => "- " + formatTurnEventDisplayLabel(item) + " recentUpdate step " + (item.fromValue ?? "-") + " -> " + (item.toValue ?? "-") + " delta=" + (item.delta ?? "-") + " sampleDelta=" + (item.sampleDeltaSeconds ?? "-") + " allowed=" + (item.allowedIncreaseSeconds ?? "-") + " excess=" + (item.excessiveIncreaseSeconds ?? 0) + " event=" + (item.event || "-") + " seq " + (item.fromSeq ?? "-") + " -> " + (item.toSeq ?? "-") + " ts " + (item.fromTs || "-") + " -> " + (item.toTs || "-") + " traceId=" + (item.traceId || "-")).join("\n") + : "- 未观察到最近更新相邻采样 step。"; + const recentUpdateResets = Array.isArray(sampleMetrics?.turnTimingRecentUpdateResets) ? sampleMetrics.turnTimingRecentUpdateResets : []; + const resetLines = recentUpdateResets.length > 0 + ? recentUpdateResets.slice(0, 80).map((item) => "- " + formatTurnEventDisplayLabel(item) + " reset " + (item.fromValue ?? "-") + " -> " + (item.toValue ?? "-") + " delta=" + (item.delta ?? "-") + " sampleDelta=" + (item.sampleDeltaSeconds ?? "-") + " seq " + (item.fromSeq ?? "-") + " -> " + (item.toSeq ?? "-") + " ts " + (item.fromTs || "-") + " -> " + (item.toTs || "-") + " traceId=" + (item.traceId || "-")).join("\n") + : "- 未观察到最近更新归零/回落。"; + const disclosureLine = disclosure?.truncated + ? "表格披露:已按 head/tail 有界输出,totalRows=" + disclosure.totalRows + " includedRows=" + disclosure.includedRows + " omittedRows=" + disclosure.omittedRows + ";异常计数在截断前基于全量采样计算。" + : "表格披露:完整输出当前分析窗口的采样点。"; + return disclosureLine + "\n\n" + lines.join("\n") + "\n\n列说明:\n" + columnLines + "\n\n异常事件(仅报表暴露,不做下游 repair;最近更新按三角波模型检测异常跳增):\n" + nonMonotonicLines + "\n\nTerminal 后总耗时增长事件(终态 turn 的总耗时应 sealed,不应继续增长):\n" + terminalGrowthLines + "\n\n总耗时归零跳变事件(例如已显示真实耗时后又变成 0 秒):\n" + elapsedZeroResetLines + "\n\n总耗时异常前跳事件(预期按采样间隔近似递增;例如 14 秒 -> 1137 秒应被列入这里):\n" + totalElapsedForwardJumpLines + "\n\n最近更新 sawtooth jump 事件(预期每秒增长约 1,遇到新活动归零;例如 1 秒 -> 1 分 4 秒应被列入这里):\n" + sawtoothJumpLines + "\n\n最近更新相邻采样 step(按 delta 降序;用于人工识别一秒跳几十秒/一分钟的瞬态):\n" + stepLines + "\n\n最近更新 reset 事件(预期三角波归零,不计为异常):\n" + resetLines; +} + +function formatTurnColumnDisplayLabel(column) { + const base = String(column?.label || column?.id || "-"); + const role = String(column?.pageRole || "unknown"); + const pageId = compactPageId(column?.pageId); + return pageId ? base + "@" + role + "/" + pageId : base + "@" + role; +} + +function formatTurnEventDisplayLabel(item) { + const base = String(item?.columnLabel || item?.columnId || "-"); + const role = String(item?.pageRole || "unknown"); + const pageId = compactPageId(item?.pageId); + return pageId ? base + "@" + role + "/" + pageId : base + "@" + role; +} + +function compactPageId(value) { + if (value === null || value === undefined || value === "") return ""; + const text = String(value); + if (text.length <= 18) return text; + return text.slice(0, 12) + ".." + text.slice(-4); +} + +function formatMetricCell(value) { + if (value === null || value === undefined || !Number.isFinite(Number(value))) return "-"; + return String(Number(value)); +} + +function escapeMarkdownCell(value) { + return String(value ?? "-").replace(/\|/gu, "\\|"); +} + +function renderMarkdown(report) { + const findingLines = report.findings.length === 0 ? "- 无红灯项。" : report.findings.map((item) => "- " + item.severity + ": " + item.id + " - " + item.summary).join("\n"); + const commandLines = report.commandTimeline.length === 0 ? "- 无控制命令。" : report.commandTimeline.map((item) => "- " + item.ts + " " + item.phase + " " + item.type + " " + item.commandId + " " + (item.afterUrl || "")).join("\n"); + const commandFailureLines = Array.isArray(report.commandFailures) && report.commandFailures.length > 0 + ? report.commandFailures.slice(0, 80).map((item) => "- " + (item.ts || "-") + " type=" + (item.type || "-") + " commandId=" + (item.commandId || "-") + " durationMs=" + (item.durationMs ?? "-") + " sampleSeq=" + (item.sampleSeq ?? "-") + " path=" + (item.beforePath || "-") + "->" + (item.afterPath || "-") + " message=" + escapeMarkdownCell(item.message || item.failureKind || item.name || "-")).join("\n") + : "- 无失败控制命令。"; + const transitionLines = report.transitions.length === 0 ? "- 无状态变化。" : report.transitions.slice(0, 80).map((item) => "- #" + item.seq + " " + item.ts + " messages=" + item.messageCount + " traceRows=" + item.traceRowCount + " route=" + (item.routeSessionId || "-") + " active=" + (item.activeSessionId || "-")).join("\n"); + const metricSummary = report.sampleMetrics?.summary || {}; + const loading = report.sampleMetrics?.loading || {}; + const loadingSummary = loading.summary || {}; + const sessionRailTitles = report.sampleMetrics?.sessionRailTitles || {}; + const sessionRailTitleSummary = sessionRailTitles.summary || {}; + const codeAgentCardTiming = report.sampleMetrics?.codeAgentCardTiming || {}; + const codeAgentCardTimingSummary = codeAgentCardTiming.summary || {}; + const roundCompletion = codeAgentCardTiming.roundCompletion || {}; + const traceOrder = report.sampleMetrics?.traceOrder || {}; + const traceOrderSummary = traceOrder.summary || {}; + const alertSummary = report.runtimeAlerts?.summary || {}; + const httpAlertLines = Array.isArray(report.runtimeAlerts?.networkHttpErrorsByPath) && report.runtimeAlerts.networkHttpErrorsByPath.length > 0 + ? report.runtimeAlerts.networkHttpErrorsByPath.slice(0, 40).map((item) => "- HTTP " + (item.status ?? "-") + " " + item.method + " " + item.urlPath + " count=" + item.count + " prompts=" + (item.promptIndexes?.join(",") || "-") + " first=" + (item.firstAt || "-") + " last=" + (item.lastAt || "-")).join("\n") + : "- 无 HTTP 错误。"; + const requestFailedLines = Array.isArray(report.runtimeAlerts?.networkRequestFailedByPath) && report.runtimeAlerts.networkRequestFailedByPath.length > 0 + ? report.runtimeAlerts.networkRequestFailedByPath.slice(0, 40).map((item) => "- requestfailed " + item.method + " " + item.urlPath + " count=" + item.count + " failure=" + (item.failureKinds?.slice(0, 4).join(",") || "-") + " prompts=" + (item.promptIndexes?.join(",") || "-") + " first=" + (item.firstAt || "-") + " last=" + (item.lastAt || "-")).join("\n") + : "- 无 requestfailed。"; + const domDiagnosticLines = Array.isArray(report.runtimeAlerts?.domDiagnostics) && report.runtimeAlerts.domDiagnostics.length > 0 + ? report.runtimeAlerts.domDiagnostics.slice(0, 40).map((item) => "- #" + (item.seq ?? "-") + " " + (item.ts || "-") + " prompt=" + (item.promptIndex ?? "-") + " source=" + (item.source || "-") + " code=" + (item.diagnosticCode || "-") + " traceId=" + (item.traceId || "-") + " http=" + (item.httpStatus ?? "-") + " idle=" + (item.idleSeconds ?? "-") + " waitingFor=" + (item.waitingFor || "-") + " lastEventLabel=" + (item.lastEventLabel || "-") + " textHash=" + (item.textHash || "-") + " preview=" + escapeMarkdownCell(item.preview || "")).join("\n") + : "- 无 DOM 诊断文本。"; + const consoleAlertLines = Array.isArray(report.runtimeAlerts?.consoleAlerts) && report.runtimeAlerts.consoleAlerts.length > 0 + ? report.runtimeAlerts.consoleAlerts.slice(0, 40).map((item) => "- " + (item.ts || "-") + " prompt=" + (item.promptIndex ?? "-") + " type=" + (item.type || "-") + " status=" + (item.status ?? "-") + " path=" + (item.urlPath || "-") + " traceId=" + (item.traceId || "-") + " textHash=" + (item.textHash || "-") + " preview=" + escapeMarkdownCell(item.preview || "")).join("\n") + : "- 无 console warning/error。"; + const consoleAlertGroupLines = Array.isArray(report.runtimeAlerts?.consoleAlertsByPath) && report.runtimeAlerts.consoleAlertsByPath.length > 0 + ? report.runtimeAlerts.consoleAlertsByPath.slice(0, 40).map((item) => "- console " + (item.type || "-") + " status=" + (item.status ?? "-") + " path=" + (item.urlPath || "-") + " count=" + item.count + " prompts=" + (item.promptIndexes?.join(",") || "-") + " traces=" + (item.traceIds?.slice(0, 6).join(",") || "-")).join("\n") + : "- 无 console 分组。"; + const promptNetworkLines = Array.isArray(report.promptNetwork?.rounds) && report.promptNetwork.rounds.length > 0 + ? report.promptNetwork.rounds.map((item) => "- round " + item.promptIndex + " promptHash=" + (item.promptTextHash || "-") + " chatPostOk=" + String(item.chatPostOk) + " modes=" + (Array.isArray(item.submitModes) && item.submitModes.length > 0 ? item.submitModes.join(",") : "-") + " failure=" + (item.failureKind || "-") + " statuses=" + (Array.isArray(item.responseStatuses) && item.responseStatuses.length > 0 ? item.responseStatuses.join(",") : "-") + " firstChat=" + (item.firstChatEventAt || "-") + " lastChat=" + (item.lastChatEventAt || "-")).join("\n") + : "- 无 prompt 网络记录。"; + const roundLines = Array.isArray(report.sampleMetrics?.rounds) && report.sampleMetrics.rounds.length > 0 + ? report.sampleMetrics.rounds.map((item) => "- round " + item.promptIndex + " promptHash=" + (item.promptTextHash || "-") + " samples=" + item.sampleCount + " loadingSamples=" + (item.loadingSamples ?? 0) + " maxLoading=" + (item.maxLoadingCount ?? 0) + " loadingOwners=" + (item.loadingOwnerCount ?? 0) + " totalMax=" + (item.maxTotalElapsedSeconds ?? "-") + " totalLast=" + (item.lastTotalElapsedSeconds ?? "-") + " recentMax=" + (item.maxRecentUpdateSeconds ?? "-") + " recentLast=" + (item.lastRecentUpdateSeconds ?? "-") + " totalDecrease=" + (item.turnTimingTotalElapsedDecreaseCount ?? 0) + " totalForwardJump=" + (item.turnTimingTotalElapsedForwardJumpCount ?? 0) + " totalForwardJumpMax=" + (item.turnTimingTotalElapsedForwardJumpMaxSeconds ?? 0) + " terminalGrowth=" + (item.turnTimingTerminalElapsedGrowthCount ?? 0) + " terminalGrowthMax=" + (item.turnTimingTerminalElapsedGrowthMaxSeconds ?? 0) + " recentJump=" + (item.turnTimingRecentUpdateJumpCount ?? 0) + " recentSawtoothJump=" + (item.turnTimingRecentUpdateSawtoothJumpCount ?? item.turnTimingRecentUpdateJumpCount ?? 0) + " recentStep=" + (item.turnTimingRecentUpdateStepCount ?? 0) + " recentMaxIncrease=" + (item.turnTimingRecentUpdateMaxIncreaseSeconds ?? "-") + " recentMaxExcess=" + (item.turnTimingRecentUpdateMaxExcessSeconds ?? 0) + " recentReset=" + (item.turnTimingRecentUpdateResetCount ?? 0) + " diagnostics=" + item.diagnosticSamples + " terminal=" + item.terminalSamples + " finalText=" + item.finalTextSamples).join("\n") + : "- 无轮次指标。"; + const cardMissingElapsedLines = Array.isArray(codeAgentCardTiming.missingElapsed) && codeAgentCardTiming.missingElapsed.length > 0 + ? codeAgentCardTiming.missingElapsed.slice(0, 80).map((item) => "- #" + (item.seq ?? "-") + " " + (item.ts || "-") + " prompt=" + (item.promptIndex ?? "-") + " role=" + (item.pageRole || "-") + " status=" + (item.status || "-") + " traceId=" + (item.traceId || "-") + " messageId=" + (item.messageId || "-") + " preview=" + escapeMarkdownCell(item.textPreview || "")).join("\n") + : "- 未观察到 Code Agent 卡片缺少耗时。"; + const cardMissingRecentLines = Array.isArray(codeAgentCardTiming.missingRecentUpdate) && codeAgentCardTiming.missingRecentUpdate.length > 0 + ? codeAgentCardTiming.missingRecentUpdate.slice(0, 80).map((item) => "- #" + (item.seq ?? "-") + " " + (item.ts || "-") + " prompt=" + (item.promptIndex ?? "-") + " role=" + (item.pageRole || "-") + " status=" + (item.status || "-") + " traceId=" + (item.traceId || "-") + " messageId=" + (item.messageId || "-") + " total=" + (item.totalElapsedSeconds ?? "-") + " preview=" + escapeMarkdownCell(item.textPreview || "")).join("\n") + : "- 未观察到未终态 Code Agent 卡片缺少最近更新。"; + const roundCompletionLines = Array.isArray(roundCompletion.events) && roundCompletion.events.length > 0 + ? roundCompletion.events.slice(0, 80).map((item) => "- #" + (item.seq ?? "-") + " " + (item.ts || "-") + " prompt=" + (item.promptIndex ?? "-") + " role=" + (item.pageRole || "-") + " traceId=" + (item.traceId || "-") + " completionElapsed=" + (item.elapsedSeconds ?? "-") + " preview=" + escapeMarkdownCell(item.preview || "")).join("\n") + : "- 未观察到“轮次完成(总耗时 ...)”trace 行。"; + const roundCompletionMismatchLines = Array.isArray(roundCompletion.elapsedMismatches) && roundCompletion.elapsedMismatches.length > 0 + ? roundCompletion.elapsedMismatches.slice(0, 80).map((item) => "- #" + (item.seq ?? "-") + " " + (item.ts || "-") + " prompt=" + (item.promptIndex ?? "-") + " traceId=" + (item.traceId || "-") + " completion=" + (item.completionElapsedSeconds ?? "-") + " card=" + (item.cardTotalElapsedSeconds ?? "-") + " delta=" + (item.deltaSeconds ?? "-") + " tolerance=" + (item.toleranceSeconds ?? "-")).join("\n") + : "- 未观察到轮次完成耗时与卡片耗时不一致。"; + const roundCompletionFinalMissingLines = Array.isArray(roundCompletion.finalResponseMissing) && roundCompletion.finalResponseMissing.length > 0 + ? roundCompletion.finalResponseMissing.slice(0, 80).map((item) => "- #" + (item.seq ?? "-") + " " + (item.ts || "-") + " prompt=" + (item.promptIndex ?? "-") + " role=" + (item.pageRole || "-") + " traceId=" + (item.traceId || "-") + " completionElapsed=" + (item.completionElapsedSeconds ?? "-")).join("\n") + : "- 未观察到轮次完成后 final response 缺失。"; + const roundCompletionPostTimingLines = Array.isArray(roundCompletion.postCompletionTimingChanges) && roundCompletion.postCompletionTimingChanges.length > 0 + ? roundCompletion.postCompletionTimingChanges.slice(0, 80).map((item) => "- " + (item.columnLabel || item.columnId || "-") + " " + (item.metric || "-") + " " + (item.fromValue ?? "-") + " -> " + (item.toValue ?? "-") + " delta=" + (item.delta ?? "-") + " completionSeq=" + (item.seq ?? "-") + " seq " + (item.fromSeq ?? "-") + " -> " + (item.toSeq ?? "-") + " traceId=" + (item.traceId || "-")).join("\n") + : "- 未观察到轮次完成后耗时/最近更新继续变化。"; + const durationUnderreportedLines = Array.isArray(codeAgentCardTiming.durationUnderreported) && codeAgentCardTiming.durationUnderreported.length > 0 + ? codeAgentCardTiming.durationUnderreported.slice(0, 80).map((item) => "- sample=" + (item.sampleIndex ?? "-") + " " + (item.timestamp || "-") + " role=" + (item.pageRole || "-") + " status=" + (item.status || "-") + " traceId=" + (item.traceId || "-") + " card=" + (item.cardTotalElapsedSeconds ?? "-") + "s expected=" + (item.expectedElapsedSeconds ?? "-") + "s delta=" + (item.deltaSeconds ?? "-") + "s evidence=" + (item.evidenceKind || "-") + " preview=" + escapeMarkdownCell(item.evidencePreview || item.cardPreview || "")).join("\n") + : "- 未观察到 Code Agent 卡片耗时低于 trace/final-response 证据。"; + const durationMismatchLines = Array.isArray(codeAgentCardTiming.durationMismatches) && codeAgentCardTiming.durationMismatches.length > 0 + ? codeAgentCardTiming.durationMismatches.slice(0, 80).map((item) => "- sample=" + (item.sampleIndex ?? "-") + " " + (item.timestamp || "-") + " role=" + (item.pageRole || "-") + " status=" + (item.status || "-") + " traceId=" + (item.traceId || "-") + " direction=" + (item.direction || "-") + " card=" + (item.cardTotalElapsedSeconds ?? "-") + "s expected=" + (item.expectedElapsedSeconds ?? "-") + "s signedDelta=" + (item.signedDeltaSeconds ?? "-") + "s delta=" + (item.deltaSeconds ?? "-") + "s tolerance=" + (item.toleranceSeconds ?? "-") + "s evidence=" + (item.evidenceKind || "-") + " exact=" + String(item.exactEvidence === true) + " preview=" + escapeMarkdownCell(item.evidencePreview || item.cardPreview || "")).join("\n") + : "- 未观察到 Code Agent 卡片耗时与 completion/final-response 封口证据不一致。"; + const traceOrderAnomalyLines = Array.isArray(traceOrder.orderAnomalies) && traceOrder.orderAnomalies.length > 0 + ? traceOrder.orderAnomalies.slice(0, 80).map((item) => "- sample=" + (item.sampleIndex ?? "-") + " " + (item.timestamp || "-") + " role=" + (item.pageRole || "-") + " traceId=" + (item.traceId || "-") + " rows=" + (item.previousRowIndex ?? "-") + "->" + (item.currentRowIndex ?? "-") + " reasons=" + (Array.isArray(item.reasons) ? item.reasons.join(",") : "-") + " total=" + (item.previousTotalSeconds ?? "-") + "->" + (item.currentTotalSeconds ?? "-") + " clock=" + (item.previousClockSeconds ?? "-") + "->" + (item.currentClockSeconds ?? "-") + " preview=" + escapeMarkdownCell((item.previousPreview || "") + " / " + (item.currentPreview || ""))).join("\n") + : "- 未观察到可见 trace 行顺序非单调。"; + const traceCompletionNotLastLines = Array.isArray(traceOrder.completionNotLast) && traceOrder.completionNotLast.length > 0 + ? traceOrder.completionNotLast.slice(0, 80).map((item) => "- sample=" + (item.sampleIndex ?? "-") + " " + (item.timestamp || "-") + " role=" + (item.pageRole || "-") + " traceId=" + (item.traceId || "-") + " rows=" + (item.completionRowIndex ?? "-") + "->" + (item.laterRowIndex ?? "-") + " total=" + (item.completionTotalSeconds ?? "-") + "->" + (item.laterTotalSeconds ?? "-") + " completion=" + escapeMarkdownCell(item.completionPreview || "") + " later=" + escapeMarkdownCell(item.laterPreview || "")).join("\n") + : "- 未观察到 completion 行后还有同 trace 后续行。"; + const loadingSegmentLines = Array.isArray(loading.segments) && loading.segments.length > 0 + ? loading.segments.slice(0, 80).map((item) => "- observedDuration=" + (item.durationSeconds ?? 0) + "s upperBound=" + (item.upperBoundSeconds ?? item.durationSeconds ?? 0) + "s endedGap=" + (item.endedGapSeconds ?? "-") + "s samples=" + (item.sampleCount ?? 0) + " countMax=" + (item.maxCount ?? 0) + " owners=" + (item.ownerCount ?? 0) + " seq=" + (item.firstSeq ?? "-") + ".." + (item.lastSeq ?? "-") + " ts=" + (item.firstAt || "-") + ".." + (item.lastAt || "-") + " endedAt=" + (item.endedAt || (item.ongoing ? "ongoing" : "-")) + " ownerLabels=" + ((Array.isArray(item.owners) ? item.owners : []).slice(0, 6).map((owner) => (owner.ownerKind || "-") + ":" + (owner.ownerLabel || "-") + "x" + (owner.count ?? 0)).join(",") || "-")).join("\n") + : "- 未观察到“加载中”可见区间。"; + const loadingOwnerLines = Array.isArray(loading.owners) && loading.owners.length > 0 + ? loading.owners.slice(0, 80).map((item) => "- " + (item.ownerKind || "-") + " " + escapeMarkdownCell(item.ownerLabel || item.ownerKey || "-") + " traceId=" + (item.ownerTraceId || "-") + " messageId=" + (item.ownerMessageId || "-") + " sessionId=" + (item.ownerSessionId || "-") + " samples=" + (item.sampleCount ?? 0) + " occurrences=" + (item.occurrenceCount ?? 0) + " maxCount=" + (item.maxSimultaneousCount ?? 0) + " longest=" + (item.longestContinuousSeconds ?? 0) + "s seq=" + (item.firstSeq ?? "-") + ".." + (item.lastSeq ?? "-") + " prompts=" + (Array.isArray(item.promptIndexes) ? item.promptIndexes.join(",") : "-")).join("\n") + : "- 未观察到“加载中”归属。"; + const loadingTimelineLines = Array.isArray(loading.timeline) && loading.timeline.length > 0 + ? loading.timeline.slice(0, 160).map((item) => "- #" + (item.seq ?? "-") + " " + (item.ts || "-") + " prompt=" + (item.promptIndex ?? "-") + " loadingCount=" + (item.loadingCount ?? 0) + " ownerCount=" + (item.ownerCount ?? 0) + " owners=" + ((Array.isArray(item.owners) ? item.owners : []).slice(0, 6).map((owner) => (owner.ownerKind || "-") + ":" + (owner.ownerLabel || "-") + " trace=" + (owner.ownerTraceId || "-") + "x" + (owner.count ?? 0)).join(",") || "-")).join("\n") + : "- 未观察到“加载中”采样点。"; + const sessionRailTitleSampleLines = Array.isArray(sessionRailTitles.samples) && sessionRailTitles.samples.length > 0 + ? sessionRailTitles.samples.slice(0, 80).map((item) => "- #" + (item.seq ?? "-") + " " + (item.ts || "-") + " role=" + (item.pageRole || "-") + " visible=" + (item.visibleCount ?? 0) + " fallback=" + (item.fallbackTitleCount ?? 0) + " ratio=" + (item.fallbackTitleRatio ?? 0) + " examples=" + ((Array.isArray(item.examples) ? item.examples : []).slice(0, 4).map((example) => escapeMarkdownCell(example.titlePreview || example.titleHash || "-")).join(",") || "-")).join("\n") + : "- 未观察到超过一半 fallback 的 session 列表采样点。"; + const sessionRailTitleExampleLines = Array.isArray(sessionRailTitles.examples) && sessionRailTitles.examples.length > 0 + ? sessionRailTitles.examples.slice(0, 80).map((item) => "- firstSeq=" + (item.firstSeq ?? "-") + " role=" + (item.pageRole || "-") + " active=" + String(item.active === true) + " sessionPrefix=" + (item.sessionIdPrefix || "-") + " titleHash=" + (item.titleHash || "-") + " preview=" + escapeMarkdownCell(item.titlePreview || "")).join("\n") + : "- 无 fallback 标题示例。"; + const provenanceLines = Array.isArray(report.pageProvenance?.segments) && report.pageProvenance.segments.length > 0 + ? report.pageProvenance.segments.slice(0, 40).map((item) => "- fingerprint=" + (item.assetFingerprint || "-") + " samples=" + item.sampleCount + " seq=" + (item.firstSeq ?? "-") + ".." + (item.lastSeq ?? "-") + " ts=" + (item.firstAt || "-") + ".." + (item.lastAt || "-") + " scripts=" + (item.scriptCount ?? "-") + " styles=" + (item.stylesheetCount ?? "-") + " urlPaths=" + (Array.isArray(item.urlPaths) ? item.urlPaths.slice(0, 4).join(",") : "-")).join("\n") + : "- 无页面 provenance segment。"; + const ordinaryPerformanceItems = Array.isArray(report.pagePerformance?.sameOriginApiByPath) ? report.pagePerformance.sameOriginApiByPath.filter((item) => item.isLongLivedStream !== true) : []; + const streamPerformanceItems = Array.isArray(report.pagePerformance?.sameOriginApiByPath) ? report.pagePerformance.sameOriginApiByPath.filter((item) => item.isLongLivedStream === true) : []; + const sameOriginApiBudgetMs = Number(report.alertThresholds?.sameOriginApiSlowMs ?? report.pagePerformance?.summary?.budgetMs); + const streamOpenBudgetMs = Number(report.alertThresholds?.longLivedStreamOpenSlowMs); + const performanceLines = ordinaryPerformanceItems.length > 0 + ? ordinaryPerformanceItems.slice(0, 80).map((item) => "- " + item.path + " kind=" + (item.routeKind || "same-origin-api") + " budgetMetric=" + (item.budgetMetric || "durationMs") + " samples=" + item.sampleCount + " p50=" + (item.p50Ms ?? "-") + "ms p75=" + (item.p75Ms ?? "-") + "ms p95=" + (item.p95Ms ?? "-") + "ms max=" + (item.maxMs ?? "-") + "ms >budget=" + (item.overBudgetCount ?? item.overFiveSecondCount ?? 0) + " budgetMs=" + (item.budgetMs ?? sameOriginApiBudgetMs) + " legacy>5s=" + (item.overFiveSecondCount ?? 0) + " window=" + (item.firstAt || "-") + ".." + (item.lastAt || "-")).join("\n") + : "- 无同源 API Resource Timing 样本。"; + const streamPerformanceLines = streamPerformanceItems.length > 0 + ? streamPerformanceItems.slice(0, 80).map((item) => "- " + item.path + " kind=" + (item.routeKind || "same-origin-api-stream") + " samples=" + item.sampleCount + " streamOpenP50=" + (item.streamOpenP50Ms ?? "-") + "ms streamOpenP75=" + (item.streamOpenP75Ms ?? "-") + "ms streamOpenP95=" + (item.streamOpenP95Ms ?? "-") + "ms streamOpenMax=" + (item.streamOpenMaxMs ?? "-") + "ms streamOpen>budget=" + (item.streamOpenOverBudgetCount ?? item.streamOpenOverFiveSecondCount ?? 0) + " streamOpenBudgetMs=" + (item.streamOpenBudgetMs ?? streamOpenBudgetMs) + " streamOpenLegacy>5s=" + (item.streamOpenOverFiveSecondCount ?? 0) + " streamLifetime>5s=" + (item.streamLifetimeOverFiveSecondCount ?? 0) + " lifetimeMax=" + (item.maxMs ?? "-") + "ms window=" + (item.firstAt || "-") + ".." + (item.lastAt || "-")).join("\n") + : "- 无同源长连接 Resource Timing 样本。"; + const metricLines = Array.isArray(report.sampleMetrics?.timeline) && report.sampleMetrics.timeline.length > 0 + ? report.sampleMetrics.timeline.slice(0, 120).map((item) => "- #" + item.seq + " " + item.ts + " prompt=" + item.promptIndex + " loadingCount=" + (item.loadingCount ?? 0) + " loadingOwners=" + (item.loadingOwnerCount ?? 0) + " totalElapsedSeconds=" + (item.totalElapsedSeconds ?? "-") + " recentUpdateSeconds=" + (item.recentUpdateSeconds ?? "-") + " terminal=" + item.terminalSeen + " finalText=" + item.finalResultTextSeen + " diagnostic=" + item.diagnosticSeen).join("\n") + : "- 无采样指标。"; + const turnTimingTable = renderTurnTimingTable(report.sampleMetrics); + return "# web-probe observe analysis\n\n" + + "- stateDir: " + report.stateDir + "\n" + + "- generatedAt: " + report.generatedAt + "\n" + + "- samples: " + report.counts.samples + "\n" + + "- control: " + report.counts.control + "\n" + + "- network: " + report.counts.network + "\n" + + "- console: " + (report.counts.console ?? 0) + "\n" + + "- errors: " + report.counts.errors + "\n\n" + + "## Findings\n\n" + findingLines + "\n\n" + + "## Command failures\n\n" + commandFailureLines + "\n\n" + + "## Sample metrics\n\n" + + "- sampleCount: " + (metricSummary.sampleCount ?? 0) + "\n" + + "- withTotalElapsed: " + (metricSummary.withTotalElapsed ?? 0) + "\n" + + "- withRecentUpdate: " + (metricSummary.withRecentUpdate ?? 0) + "\n" + + "- diagnostics: " + (metricSummary.diagnostics ?? 0) + "\n" + + "- loadingSamples: " + (metricSummary.loadingSampleCount ?? 0) + "\n" + + "- loadingMaxCount: " + (metricSummary.loadingMaxCount ?? 0) + "\n" + + "- loadingMaxOwnerCount: " + (metricSummary.loadingMaxOwnerCount ?? 0) + "\n" + + "- loadingOwnerCount: " + (metricSummary.loadingOwnerCount ?? 0) + "\n" + + "- loadingLongestContinuousSeconds: " + (metricSummary.loadingLongestContinuousSeconds ?? 0) + "\n" + + "- loadingCurrentContinuousSeconds: " + (metricSummary.loadingCurrentContinuousSeconds ?? 0) + "\n" + + "- loadingOverFiveSecondSegmentCount: " + (metricSummary.loadingOverFiveSecondSegmentCount ?? 0) + "\n" + + "- sessionRailFallbackMajoritySampleCount: " + (metricSummary.sessionRailFallbackMajoritySampleCount ?? 0) + "\n" + + "- sessionRailFallbackMaxRatio: " + (metricSummary.sessionRailFallbackMaxRatio ?? 0) + "\n" + + "- sessionRailFallbackMaxCount: " + (metricSummary.sessionRailFallbackMaxCount ?? 0) + "\n" + + "- promptSegments: " + (metricSummary.promptSegments ?? 0) + "\n\n" + + "- turnColumns: " + (metricSummary.turnColumns ?? 0) + "\n" + + "- turnTimingRows: " + (metricSummary.turnTimingRows ?? 0) + "\n" + + "- turnTimingNonMonotonicCount: " + (metricSummary.turnTimingNonMonotonicCount ?? 0) + "\n" + + "- turnTimingTotalElapsedDecreaseCount: " + (metricSummary.turnTimingTotalElapsedDecreaseCount ?? 0) + "\n" + + "- turnTimingTotalElapsedForwardJumpCount: " + (metricSummary.turnTimingTotalElapsedForwardJumpCount ?? 0) + "\n" + + "- turnTimingTotalElapsedForwardJumpMaxSeconds: " + (metricSummary.turnTimingTotalElapsedForwardJumpMaxSeconds ?? 0) + "\n" + + "- turnTimingTerminalElapsedGrowthCount: " + (metricSummary.turnTimingTerminalElapsedGrowthCount ?? 0) + "\n" + + "- turnTimingTerminalElapsedGrowthMaxSeconds: " + (metricSummary.turnTimingTerminalElapsedGrowthMaxSeconds ?? 0) + "\n" + + "- turnTimingRecentUpdateJumpCount: " + (metricSummary.turnTimingRecentUpdateJumpCount ?? 0) + "\n" + + "- turnTimingRecentUpdateSawtoothJumpCount: " + (metricSummary.turnTimingRecentUpdateSawtoothJumpCount ?? metricSummary.turnTimingRecentUpdateJumpCount ?? 0) + "\n" + + "- turnTimingRecentUpdateStepCount: " + (metricSummary.turnTimingRecentUpdateStepCount ?? 0) + "\n" + + "- turnTimingRecentUpdateMaxIncreaseSeconds: " + (metricSummary.turnTimingRecentUpdateMaxIncreaseSeconds ?? "-") + "\n" + + "- turnTimingRecentUpdateMaxExcessSeconds: " + (metricSummary.turnTimingRecentUpdateMaxExcessSeconds ?? 0) + "\n" + + "- turnTimingRecentUpdateResetCount: " + (metricSummary.turnTimingRecentUpdateResetCount ?? 0) + "\n\n" + + "- codeAgentCardSampleCount: " + (metricSummary.codeAgentCardSampleCount ?? 0) + "\n" + + "- codeAgentCardMissingElapsedCount: " + (metricSummary.codeAgentCardMissingElapsedCount ?? 0) + "\n" + + "- codeAgentCardMissingRecentUpdateCount: " + (metricSummary.codeAgentCardMissingRecentUpdateCount ?? 0) + "\n" + + "- codeAgentCardDurationUnderreportedCount: " + (metricSummary.codeAgentCardDurationUnderreportedCount ?? 0) + "\n" + + "- codeAgentCardDurationMismatchCount: " + (metricSummary.codeAgentCardDurationMismatchCount ?? 0) + "\n" + + "- traceRowCount: " + (metricSummary.traceRowCount ?? 0) + "\n" + + "- traceRowOrderAnomalyCount: " + (metricSummary.traceRowOrderAnomalyCount ?? 0) + "\n" + + "- traceRowCompletionNotLastCount: " + (metricSummary.traceRowCompletionNotLastCount ?? 0) + "\n" + + "- roundCompletionEventCount: " + (metricSummary.roundCompletionEventCount ?? 0) + "\n" + + "- roundCompletionElapsedMismatchCount: " + (metricSummary.roundCompletionElapsedMismatchCount ?? 0) + "\n" + + "- roundCompletionFinalResponseMissingCount: " + (metricSummary.roundCompletionFinalResponseMissingCount ?? 0) + "\n" + + "- roundCompletionPostTimingChangeCount: " + (metricSummary.roundCompletionPostTimingChangeCount ?? 0) + "\n\n" + + "### Rounds\n\n" + roundLines + "\n\n" + + "### Code Agent card timing display\n\n" + + "- cardSampleCount: " + (codeAgentCardTimingSummary.cardSampleCount ?? 0) + "\n" + + "- runningCardSampleCount: " + (codeAgentCardTimingSummary.runningCardSampleCount ?? 0) + "\n" + + "- terminalCardSampleCount: " + (codeAgentCardTimingSummary.terminalCardSampleCount ?? 0) + "\n" + + "- missingElapsedCount: " + (codeAgentCardTimingSummary.missingElapsedCount ?? 0) + "\n" + + "- missingRecentUpdateCount: " + (codeAgentCardTimingSummary.missingRecentUpdateCount ?? 0) + "\n" + + "- durationUnderreportedCount: " + (codeAgentCardTimingSummary.durationUnderreportedCount ?? 0) + "\n" + + "- durationMismatchCount: " + (codeAgentCardTimingSummary.durationMismatchCount ?? 0) + "\n" + + "- policy: Code Agent 卡片无论终态/非终态都必须显示耗时;非终态必须显示最近更新。该 analyzer 只报告采样到的页面表现,不做下游 repair。\n\n" + + "#### Missing elapsed samples\n\n" + cardMissingElapsedLines + "\n\n" + + "#### Missing recent update samples\n\n" + cardMissingRecentLines + "\n\n" + + "#### Duration underreported samples\n\n" + durationUnderreportedLines + "\n\n" + + "#### Duration mismatch samples\n\n" + durationMismatchLines + "\n\n" + + "### Trace row visual order\n\n" + + "- traceRowCount: " + (traceOrderSummary.traceRowCount ?? 0) + "\n" + + "- orderAnomalyCount: " + (traceOrderSummary.orderAnomalyCount ?? 0) + "\n" + + "- completionNotLastCount: " + (traceOrderSummary.completionNotLastCount ?? 0) + "\n" + + "- policy: 可见 trace 行在同一 trace 内必须按 total/时钟/projected seq 单调展示;completion 行不得出现在同 trace 后续行之前。\n\n" + + "#### Trace order anomalies\n\n" + traceOrderAnomalyLines + "\n\n" + + "#### Completion row not last samples\n\n" + traceCompletionNotLastLines + "\n\n" + + "### Round completion consistency\n\n" + + "- completionEventCount: " + (codeAgentCardTimingSummary.roundCompletionEventCount ?? 0) + "\n" + + "- elapsedMismatchCount: " + (codeAgentCardTimingSummary.roundCompletionElapsedMismatchCount ?? 0) + "\n" + + "- finalResponseMissingCount: " + (codeAgentCardTimingSummary.roundCompletionFinalResponseMissingCount ?? 0) + "\n" + + "- postTimingChangeCount: " + (codeAgentCardTimingSummary.roundCompletionPostTimingChangeCount ?? 0) + "\n" + + "- postRecentUpdateVisibleCount: " + (codeAgentCardTimingSummary.roundCompletionPostRecentUpdateVisibleCount ?? 0) + "\n" + + "- elapsedMismatchToleranceSeconds: " + (codeAgentCardTimingSummary.elapsedMismatchToleranceSeconds ?? "-") + "\n" + + "- policy: 轮次完成(总耗时 ...) 的耗时必须与卡片总耗时一致;完成后 final response 必须可见,耗时/最近更新不得继续跳变。\n\n" + + "#### Round completion events\n\n" + roundCompletionLines + "\n\n" + + "#### Completion elapsed mismatches\n\n" + roundCompletionMismatchLines + "\n\n" + + "#### Final response missing after completion\n\n" + roundCompletionFinalMissingLines + "\n\n" + + "#### Post-completion timing changes\n\n" + roundCompletionPostTimingLines + "\n\n" + + "### Loading visibility: visible 加载中\n\n" + + "- sampleCount: " + (loadingSummary.sampleCount ?? 0) + "\n" + + "- loadingSampleCount: " + (loadingSummary.loadingSampleCount ?? 0) + "\n" + + "- maxSimultaneousCount: " + (loadingSummary.maxSimultaneousCount ?? 0) + "\n" + + "- maxSimultaneousOwnerCount: " + (loadingSummary.maxSimultaneousOwnerCount ?? 0) + "\n" + + "- concurrentLoadingSampleCount: " + (loadingSummary.concurrentLoadingSampleCount ?? 0) + "\n" + + "- ownerCount: " + (loadingSummary.ownerCount ?? 0) + "\n" + + "- segmentCount: " + (loadingSummary.segmentCount ?? 0) + "\n" + + "- overFiveSecondSegmentCount: " + (loadingSummary.overFiveSecondSegmentCount ?? 0) + "\n" + + "- longestContinuousSeconds: " + (loadingSummary.longestContinuousSeconds ?? 0) + "\n" + + "- currentContinuousSeconds: " + (loadingSummary.currentContinuousSeconds ?? 0) + "\n" + + "- budgetSeconds: " + (loadingSummary.budgetSeconds ?? (Number.isFinite(Number(report.alertThresholds?.visibleLoadingSlowMs)) ? Number(report.alertThresholds.visibleLoadingSlowMs) / 1000 : "unconfigured")) + "\n" + + "- policy: 该指标只能证明用户真实看到“加载中”的持续时间;修复必须降低真实请求/投影/渲染耗时,禁止提前展示未加载完内容来压低该指标。\n\n" + + "#### Loading segments\n\n" + loadingSegmentLines + "\n\n" + + "#### Loading owners\n\n" + loadingOwnerLines + "\n\n" + + "#### Loading sample timeline\n\n" + loadingTimelineLines + "\n\n" + + "### Session rail titles\n\n" + + "- sampleCount: " + (sessionRailTitleSummary.sampleCount ?? 0) + "\n" + + "- visibleSampleCount: " + (sessionRailTitleSummary.visibleSampleCount ?? 0) + "\n" + + "- fallbackSampleCount: " + (sessionRailTitleSummary.fallbackSampleCount ?? 0) + "\n" + + "- majorityFallbackSampleCount: " + (sessionRailTitleSummary.majorityFallbackSampleCount ?? 0) + "\n" + + "- maxFallbackRatio: " + (sessionRailTitleSummary.maxFallbackRatio ?? 0) + "\n" + + "- maxVisibleCount: " + (sessionRailTitleSummary.maxVisibleCount ?? 0) + "\n" + + "- maxFallbackTitleCount: " + (sessionRailTitleSummary.maxFallbackTitleCount ?? 0) + "\n" + + "- policy: 可见 session 列表中 'Session ses_...' fallback 标题超过一半必须报警;修复应让上游 session list projection 直接携带名称,不能靠点击详情后下游修补。\n\n" + + "#### Session rail fallback samples\n\n" + sessionRailTitleSampleLines + "\n\n" + + "#### Session rail fallback examples\n\n" + sessionRailTitleExampleLines + "\n\n" + + "### Page provenance\n\n" + + "- segmentCount: " + (report.pageProvenance?.summary?.segmentCount ?? 0) + "\n" + + "- controlSegmentCount: " + (report.pageProvenance?.summary?.controlSegmentCount ?? 0) + "\n\n" + + provenanceLines + "\n\n" + + "### Page performance: same-origin API Resource Timing\n\n" + + "- budgetMs: " + (report.pagePerformance?.summary?.budgetMs ?? sameOriginApiBudgetMs) + "\n" + + "- sameOriginApiPathCount: " + (report.pagePerformance?.summary?.sameOriginApiPathCount ?? 0) + "\n" + + "- sameOriginApiSampleCount: " + (report.pagePerformance?.summary?.sameOriginApiSampleCount ?? 0) + "\n" + + "- longLivedStreamPathCount: " + (report.pagePerformance?.summary?.longLivedStreamPathCount ?? 0) + "\n" + + "- longLivedStreamSampleCount: " + (report.pagePerformance?.summary?.longLivedStreamSampleCount ?? 0) + "\n" + + "- longLivedStreamOpenOverFiveSecondPathCount: " + (report.pagePerformance?.summary?.longLivedStreamOpenOverFiveSecondPathCount ?? 0) + "\n" + + "- longLivedStreamOpenOverFiveSecondSampleCount: " + (report.pagePerformance?.summary?.longLivedStreamOpenOverFiveSecondSampleCount ?? 0) + "\n" + + "- longLivedStreamLifetimeOverFiveSecondSampleCount: " + (report.pagePerformance?.summary?.longLivedStreamLifetimeOverFiveSecondSampleCount ?? 0) + "\n" + + "- slowPathCount: " + (report.pagePerformance?.summary?.slowPathCount ?? 0) + "\n" + + "- slowSampleCount: " + (report.pagePerformance?.summary?.slowSampleCount ?? 0) + "\n" + + "- worstP95Ms: " + (report.pagePerformance?.summary?.worstP95Ms ?? "-") + "\n\n" + + performanceLines + "\n\n" + + "### Page performance: long-lived streams\n\n" + + "- policy: SSE/long-lived stream lifetime is not ordinary API load latency; only stream open latency is compared with the YAML usability budget, while disconnects remain runtime alerts.\n\n" + + streamPerformanceLines + "\n\n" + + "### Prompt network\n\n" + promptNetworkLines + "\n\n" + + "### Runtime alerts\n\n" + + "- httpErrorCount: " + (alertSummary.httpErrorCount ?? 0) + "\n" + + "- requestFailedCount: " + (alertSummary.requestFailedCount ?? 0) + "\n" + + "- domDiagnosticSampleCount: " + (alertSummary.domDiagnosticSampleCount ?? 0) + "\n" + + "- consoleAlertCount: " + (alertSummary.consoleAlertCount ?? 0) + "\n" + + "- pageErrorCount: " + (alertSummary.pageErrorCount ?? 0) + "\n\n" + + "#### HTTP errors\n\n" + httpAlertLines + "\n\n" + + "#### Request failed\n\n" + requestFailedLines + "\n\n" + + "#### DOM diagnostics\n\n" + domDiagnosticLines + "\n\n" + + "#### Console alerts\n\n" + consoleAlertLines + "\n\n" + + "#### Console alert groups\n\n" + consoleAlertGroupLines + "\n\n" + + "### Turn timing table\n\n" + + turnTimingTable + "\n\n" + + "### Aggregate timeline\n\n" + + metricLines + "\n\n" + + "## Command timeline\n\n" + commandLines + "\n\n" + + "## State transitions\n\n" + transitionLines + "\n"; +} + +async function fileMeta(file) { + const [buffer, stats] = await Promise.all([readFile(file), stat(file)]); + return { byteCount: stats.size, sha256: "sha256:" + createHash("sha256").update(buffer).digest("hex") }; +} + +function sha256(value) { + return "sha256:" + createHash("sha256").update(String(value)).digest("hex"); +} + +function urlPath(value) { + try { + const url = new URL(String(value || "http://invalid.local/")); + return url.pathname; + } catch { + return "-"; + } +} + +function compactLocation(value) { + if (!value || typeof value !== "object") return null; + return { urlPath: urlPath(value.url), lineNumber: value.lineNumber ?? null, columnNumber: value.columnNumber ?? null }; +} + +function limitText(value, limit) { + const text = String(value ?? ""); + if (text.length <= limit) return text; + return text.slice(0, Math.max(0, limit - 1)) + "…"; +} +`; +} diff --git a/scripts/src/hwlab-node-web-observe-collect.ts b/scripts/src/hwlab-node-web-observe-collect.ts index fc93ebbf..84a9580e 100644 --- a/scripts/src/hwlab-node-web-observe-collect.ts +++ b/scripts/src/hwlab-node-web-observe-collect.ts @@ -38,12 +38,15 @@ if(view==='files'){ const samples=readJsonl('samples.jsonl'); const control=readJsonl('control.jsonl'); const manifest=readJson('manifest.json')||{}; +const report=readJson('analysis/report.json')||{}; function unique(values){return Array.from(new Set(values.filter(Boolean)));} +function numOrNull(value){const n=Number(value); return Number.isFinite(n)?n:null} +function promptIndexOf(value){const n=Number(value); return Number.isInteger(n)&&n>0?n:null} function tsMs(value){const ms=Date.parse(String(value||'')); return Number.isFinite(ms)?ms:null} function promptCommands(){ const map=new Map(); for(const item of control){ - if((item.type!=='sendPrompt'&&item.type!=='steer')||(item.phase!=='started'&&item.phase!=='completed'&&item.phase!=='failed')) continue; + if(item.type!=='sendPrompt'||(item.phase!=='started'&&item.phase!=='completed'&&item.phase!=='failed')) continue; const id=item.commandId||item.seq||String(map.size+1); const existing=map.get(id)||{}; map.set(id,{...existing,...item,input:{...(existing.input||{}),...(item.input||{})},firstTs:existing.firstTs||item.ts,lastTs:item.ts}); @@ -59,14 +62,95 @@ function controlsFor(index,prompts){ return control.filter((item)=>{const ms=tsMs(item.ts); return ms!==null&&ms>=start&&ms({group:'turn',item})):[]), + ...(Array.isArray(sample.traceRows)?sample.traceRows.map((item)=>({group:'traceRow',item})):[]), + ...(Array.isArray(sample.messages)?sample.messages.map((item)=>({group:'message',item})):[]), + ]; +} +function traceIdStats(items){ + const map=new Map(); for(const sample of items){ - for(const turn of Array.isArray(sample.turns)?sample.turns:[]) ids.push(turn.traceId); - for(const row of Array.isArray(sample.traceRows)?sample.traceRows:[]) ids.push(row.traceId||firstTraceId(textOf(row))); - for(const message of Array.isArray(sample.messages)?sample.messages:[]) ids.push(message.traceId||firstTraceId(textOf(message))); + const seq=numOrNull(sample.seq); + for(const entry of entryGroups(sample)){ + const id=itemTraceId(entry.item); if(!id) continue; + const current=map.get(id)||{traceId:id,count:0,firstSeq:null,lastSeq:null,terminalCount:0}; + current.count+=1; + current.firstSeq=current.firstSeq===null?seq:seq===null?current.firstSeq:Math.min(current.firstSeq,seq); + current.lastSeq=current.lastSeq===null?seq:seq===null?current.lastSeq:Math.max(current.lastSeq,seq); + if(terminalText(textOf(entry.item))||String(entry.item?.status||'').match(/complete|fail|cancel|terminal/iu)) current.terminalCount+=1; + map.set(id,current); + } } - return unique(ids); + return Array.from(map.values()).sort((a,b)=>(a.firstSeq??0)-(b.firstSeq??0)||String(a.traceId).localeCompare(String(b.traceId))); +} +function traceIdsFromSamples(items){return traceIdStats(items).map((item)=>item.traceId)} +function collectReportTurnColumns(value,out=[]){ + if(!value||typeof value!=='object') return out; + if(Array.isArray(value)){for(const item of value) collectReportTurnColumns(item,out); return out;} + if(Array.isArray(value.turnColumns)){ + for(const item of value.turnColumns){ + if(!item||typeof item!=='object') continue; + const traceId=String(item.traceId||'').trim(); + if(!traceId) continue; + out.push({...item,traceId,promptIndex:promptIndexOf(item.promptIndex),lastPromptIndex:promptIndexOf(item.lastPromptIndex),firstSeq:numOrNull(item.firstSeq),lastSeq:numOrNull(item.lastSeq)}); + } + } + for(const key of ['sampleMetrics','analysis','analysisWindow','recentWindow','windows','recent','archive','summary']){ + if(value[key]&&value[key]!==value) collectReportTurnColumns(value[key],out); + } + return out; +} +const reportTurnColumns=(()=>{const seen=new Set(); const rows=[]; for(const item of collectReportTurnColumns(report)){const key=[item.traceId,item.promptIndex,item.lastPromptIndex,item.firstSeq,item.lastSeq,item.source,item.pageRole,item.messageId].join('|'); if(seen.has(key)) continue; seen.add(key); rows.push(item);} return rows.sort((a,b)=>(a.firstSeq??0)-(b.firstSeq??0)||(a.promptIndex??9999)-(b.promptIndex??9999));})(); +function columnOverlapsSegment(column,segment){ + if(segment.length===0) return true; + const first=numOrNull(segment[0]?.seq); const last=numOrNull(segment[segment.length-1]?.seq); + if(first===null||last===null||column.firstSeq===null||column.lastSeq===null) return true; + return column.lastSeq>=first&&column.firstSeq<=last; +} +function reportColumnsForPrompt(index,segment){ + const promptIndex=index+1; + const segmentIds=new Set(traceIdsFromSamples(segment)); + return reportTurnColumns + .filter((column)=>(column.promptIndex===promptIndex||column.lastPromptIndex===promptIndex)&&columnOverlapsSegment(column,segment)) + .filter((column)=>segmentIds.size===0||segmentIds.has(column.traceId)||column.source==='turn') + .sort((a,b)=>(a.firstSeq??0)-(b.firstSeq??0)||(a.lastSeq??0)-(b.lastSeq??0)||String(a.label||'').localeCompare(String(b.label||''))); +} +function choosePrimaryTraceId(index,segment){ + const columns=reportColumnsForPrompt(index,segment); + const promptIndex=index+1; + const promptColumns=columns.filter((column)=>column.promptIndex===promptIndex); + const preferred=promptColumns.length>0?promptColumns:columns; + if(preferred.length>0) return {traceId:preferred[0].traceId,columns}; + const stats=traceIdStats(segment).sort((a,b)=>(b.lastSeq??0)-(a.lastSeq??0)||(b.firstSeq??0)-(a.firstSeq??0)); + return {traceId:stats[0]?.traceId||null,columns:[]}; +} +function samplesForTrace(items,traceId,maxSeq=null){ + if(!traceId) return items; + return items.filter((sample)=>{ + const seq=numOrNull(sample.seq); + return (maxSeq===null||seq===null||seq<=maxSeq)&&traceIdsFromSamples([sample]).includes(traceId); + }); +} +function latestSampleForTrace(items,traceId,maxSeq=null){ + return samplesForTrace(items,traceId,maxSeq).slice(-1)[0]||null; +} +function traceEntries(items,traceId){ + const entries=[]; + for(const sample of items){ + for(const entry of entryGroups(sample)){ + const text=textOf(entry.item); + const id=itemTraceId(entry.item); + if(!traceId||id===traceId||text.includes(traceId)) entries.push({...entry,sample,text}); + } + } + return entries; +} +function textsFor(items,traceId=null){ + const entries=traceId?traceEntries(items,traceId):items.flatMap((sample)=>entryGroups(sample).map((entry)=>({...entry,sample,text:textOf(entry.item)}))); + return entries.length>0?entries.map((entry)=>entry.text):[]; } function parseElapsed(text){ const values=[]; const raw=String(text||''); @@ -81,10 +165,11 @@ function parseElapsed(text){ function parseRecent(text){const m=String(text||'').match(/最近\\s*(\\d+)\\s*(秒|分钟|分|小时)前/u); if(!m)return null; const n=Number(m[1]); return m[2]==='小时'?n*3600:m[2]==='秒'?n:n*60} function fmtDuration(seconds){if(seconds===null||seconds===undefined||!Number.isFinite(Number(seconds)))return '-'; const value=Math.max(0,Math.round(Number(seconds))); const h=Math.floor(value/3600), m=Math.floor((value%3600)/60), s=value%60; return (h>0?String(h).padStart(2,'0')+':':'')+String(m).padStart(2,'0')+':'+String(s).padStart(2,'0')} function terminalText(text){return /轮次完成|轮次失败|轮次取消|已记录|final response|sealed final response|turn completed|turn failed|turn canceled|completed|failed|canceled|cancelled|terminal/iu.test(String(text||''))} -function statusFor(items){ - const texts=items.flatMap((sample)=>[...(Array.isArray(sample.turns)?sample.turns:[]),...(Array.isArray(sample.traceRows)?sample.traceRows:[]),...(Array.isArray(sample.messages)?sample.messages:[])].map(textOf)); +function statusFor(items,traceId=null){ + const texts=textsFor(items,traceId); const joined=texts.join('\\n'); - const lastTurn=[].concat(...items.map((sample)=>Array.isArray(sample.turns)?sample.turns:[])).slice(-1)[0]||null; + const matchedEntries=traceId?traceEntries(items,traceId):items.flatMap((sample)=>entryGroups(sample).map((entry)=>({...entry,sample,text:textOf(entry.item)}))); + const lastTurn=matchedEntries.filter((entry)=>entry.group==='turn').slice(-1)[0]?.item||null; const raw=String(lastTurn?.status||'').toLowerCase(); if(/cancel/iu.test(joined)||raw.includes('cancel'))return 'canceled'; if(/failed|error|agentrun:error/iu.test(joined)||raw.includes('fail'))return 'failed'; @@ -101,15 +186,39 @@ function userMessageFor(items,prompt){ } return {preview:short(prompt?.input?.textPreview||'',90)||'(hash only)',textHash:hash,textBytes:prompt?.input?.textBytes??null}; } -function finalResponseFor(items,traceId){ - const turns=[].concat(...items.map((sample)=>Array.isArray(sample.turns)?sample.turns:[])); - const candidates=(traceId?turns.filter((turn)=>turn.traceId===traceId):turns).slice().reverse(); - for(const turn of candidates){ - const status=String(turn.status||'').toLowerCase(); const text=textOf(turn); - if(status.includes('running')&&!terminalText(text))continue; - if(!terminalText(text)&&!status.match(/complete|fail|cancel|block/u))continue; +function cleanFinalResponseText(text){ + const raw=String(text||'').trim(); + if(!raw) return ''; + if(/Code Agent\\s*耗时[\\s\\S]*运行记录/iu.test(raw)) return ''; + if(/^sent\\s+当前 AgentRun 请求已取消/u.test(raw)) return ''; + if(/^(admitted|run|ok|error)\\s+/iu.test(raw)) return ''; + if(/^(AgentRun|Code Agent turn was durably admitted|commandExecution|runner-job|command -v|node --version|npm --version|cd |mkdir -p|npm install|npx )/iu.test(raw)) return ''; + if(/(?:^|\\s)轮次(完成|失败|取消)(?总耗时/iu.test(raw)) return ''; + if(/^(轮次完成|轮次失败|轮次取消|已记录)$/u.test(raw.replace(/\\s+/gu,' ').trim())) return ''; + return raw; +} +function entryRole(entry){return String(entry.item?.role||entry.item?.authorRole||entry.item?.messageRole||entry.item?.speaker||'').toLowerCase()} +function sameAsUserPrompt(entry,text,user){ + if(!user) return false; + if(user.textHash&&entry.item?.textHash===user.textHash) return true; + const userPrefix=short(user.preview||'',48).replace(/\\s+/gu,' ').trim().slice(0,32); + const textPrefix=String(text||'').replace(/\\s+/gu,' ').trim().slice(0,32); + return userPrefix.length>=18&&textPrefix===userPrefix; +} +function finalResponseFor(items,traceId,user=null){ + const candidates=(traceId?traceEntries(items,traceId):items.flatMap((sample)=>entryGroups(sample).map((entry)=>({...entry,sample,text:textOf(entry.item)})))).slice().reverse(); + for(const entry of candidates){ + const role=entryRole(entry); + const entryStatus=String(entry.item?.status||''); + if(entry.group==='message'&&!/assistant|agent|system/u.test(role)) continue; + if(/用户|user/iu.test(role)||/用户|user/iu.test(entryStatus)) continue; + const status=String(entry.item?.status||'').toLowerCase(); const text=cleanFinalResponseText(entry.text); + if(!text) continue; + if(/^第\\d+轮\\/\\d+[::]?\\s*请/u.test(text)||/^请(执行|帮|回答|修复|调查|用|不要|直接)/u.test(text)) continue; + if(sameAsUserPrompt(entry,text,user)) continue; + if(status.includes('running')&&!terminalText(entry.text)&&entry.group==='turn')continue; const preview=short(text,180); - return preview?{preview,textHash:turn.textHash||sha(text),textBytes:turn.textBytes??Buffer.byteLength(text),empty:false}:{preview:'(空内容)',textHash:null,textBytes:0,empty:true}; + if(preview)return {preview,textHash:entry.item?.textHash||sha(text),textBytes:entry.item?.textBytes??Buffer.byteLength(text),empty:false}; } return {preview:'(空内容)',textHash:null,textBytes:0,empty:true}; } @@ -117,14 +226,16 @@ function turnSummaryRows(){ const prompts=promptCommands(); if(prompts.length===0){ const allTraceIds=traceIdsFromSamples(samples); - return [{round:0,commandId:null,commandType:null,userPreview:'(无 sendPrompt/steer control 记录)',userHash:null,userBytes:null,traceId:allTraceIds[0]||null,status:statusFor(samples),elapsedSeconds:null,recentUpdateSeconds:null,marks:'-',firstSeq:samples[0]?.seq??null,lastSeq:samples[samples.length-1]?.seq??null,lastTs:samples[samples.length-1]?.ts??null,finalResponse:finalResponseFor(samples,allTraceIds[0]||null),valuesRedacted:true}]; + return [{round:0,commandId:null,commandType:null,userPreview:'(无 sendPrompt control 记录)',userHash:null,userBytes:null,traceId:allTraceIds[0]||null,status:statusFor(samples,allTraceIds[0]||null),elapsedSeconds:null,recentUpdateSeconds:null,marks:'-',firstSeq:samples[0]?.seq??null,lastSeq:samples[samples.length-1]?.seq??null,lastTs:samples[samples.length-1]?.ts??null,finalResponse:finalResponseFor(samples,allTraceIds[0]||null),valuesRedacted:true}]; } return prompts.map((prompt,index)=>{ - const segment=segmentFor(index,prompts); const segmentControls=controlsFor(index,prompts); const ids=traceIdsFromSamples(segment); const traceId=ids[0]||null; const user=userMessageFor(segment,prompt); - const texts=segment.flatMap((sample)=>[...(Array.isArray(sample.turns)?sample.turns:[]),...(Array.isArray(sample.traceRows)?sample.traceRows:[]),...(Array.isArray(sample.messages)?sample.messages:[])].map(textOf)); + const segment=segmentFor(index,prompts); const segmentControls=controlsFor(index,prompts); const selected=choosePrimaryTraceId(index,segment); const traceId=selected.traceId; const user=userMessageFor(segment,prompt); + const primaryColumn=selected.columns.find((column)=>column.traceId===traceId)||null; + const traceSample=latestSampleForTrace(segment,traceId,primaryColumn?.lastSeq??null); + const texts=textsFor(segment,traceId); const elapsedValues=texts.map(parseElapsed).filter((value)=>value!==null); const recentValues=texts.map(parseRecent).filter((value)=>value!==null); - const marks=unique([prompt.type==='steer'?'steer':null,...segmentControls.map((item)=>item.type==='cancel'?'cancel':item.type==='steer'?'steer':null)]).join(',')||'-'; - return {round:index+1,commandId:prompt.commandId||null,commandType:prompt.type||null,userPreview:user.preview,userHash:user.textHash,userBytes:user.textBytes,traceId,status:statusFor(segment),elapsedSeconds:elapsedValues.length?Math.max(...elapsedValues):null,recentUpdateSeconds:recentValues.length?Math.max(...recentValues):null,marks,firstSeq:segment[0]?.seq??null,lastSeq:segment[segment.length-1]?.seq??null,lastTs:segment[segment.length-1]?.ts??null,finalResponse:finalResponseFor(segment,traceId),valuesRedacted:true}; + const marks=unique(segmentControls.map((item)=>item.type==='cancel'?'cancel':item.type==='steer'?'steer':null)).join(',')||'-'; + return {round:index+1,commandId:prompt.commandId||null,commandType:prompt.type||null,userPreview:user.preview,userHash:user.textHash,userBytes:user.textBytes,traceId,traceIds:unique([traceId,...selected.columns.map((column)=>column.traceId),...traceIdsFromSamples(segment)]).slice(0,8),status:statusFor(segment,traceId),elapsedSeconds:elapsedValues.length?Math.max(...elapsedValues):null,recentUpdateSeconds:recentValues.length?Math.max(...recentValues):null,marks,firstSeq:segment[0]?.seq??primaryColumn?.firstSeq??null,lastSeq:traceSample?.seq??primaryColumn?.lastSeq??segment[segment.length-1]?.seq??null,lastTs:traceSample?.ts??segment[segment.length-1]?.ts??null,finalResponse:finalResponseFor(segment,traceId,user),valuesRedacted:true}; }); } function pad(value,width){const text=short(value,width); return text+Array(Math.max(0,width-text.length)+1).join(' ')} @@ -137,18 +248,19 @@ function renderTurnSummary(rows){ function selectSample(rows){ if(requestedSampleSeq!==null){const exact=samples.find((sample)=>Number(sample.seq)===requestedSampleSeq); if(exact)return exact;} if(requestedTimestamp){const target=tsMs(requestedTimestamp); if(target!==null){const before=samples.filter((sample)=>{const ms=tsMs(sample.ts); return ms!==null&&ms<=target}).slice(-1)[0]; if(before)return before;}} + if(requestedTurn!==null&&rows[requestedTurn-1]?.traceId){const row=rows[requestedTurn-1]; const byTrace=latestSampleForTrace(samples,row.traceId,numOrNull(row.lastSeq)); if(byTrace)return byTrace;} if(requestedTurn!==null&&rows[requestedTurn-1]?.lastSeq!==null){const byTurn=samples.find((sample)=>Number(sample.seq)===Number(rows[requestedTurn-1].lastSeq)); if(byTurn)return byTurn;} if(requestedTraceId){const byTrace=samples.filter((sample)=>traceIdsFromSamples([sample]).includes(requestedTraceId)).slice(-1)[0]; if(byTrace)return byTrace;} return samples[samples.length-1]||null; } function renderTraceFrame(sample,rows){ if(!sample)return {ok:false,renderedText:'TRACE_FRAME_BLOCKER\\nno sample matched --sample-seq/--timestamp/--trace-id/--turn',blocker:'sample-not-found'}; - const sampleTraceIds=traceIdsFromSamples([sample]); const traceId=requestedTraceId||sampleTraceIds[0]||rows.find((row)=>row.lastSeq===sample.seq)?.traceId||null; + const sampleTraceIds=traceIdsFromSamples([sample]); const traceId=requestedTraceId||(requestedTurn!==null?rows[requestedTurn-1]?.traceId:null)||rows.find((row)=>row.lastSeq===sample.seq)?.traceId||sampleTraceIds[0]||null; const traceRows=(Array.isArray(sample.traceRows)?sample.traceRows:[]).filter((row)=>!traceId||row.traceId===traceId||!row.traceId||textOf(row).includes(traceId)); const turns=(Array.isArray(sample.turns)?sample.turns:[]).filter((turn)=>!traceId||turn.traceId===traceId||textOf(turn).includes(traceId)); const texts=[...turns.map(textOf),...traceRows.map(textOf)]; const elapsed=Math.max(-1,...texts.map(parseElapsed).filter((value)=>value!==null)); const recent=Math.max(-1,...texts.map(parseRecent).filter((value)=>value!==null)); - const status=statusFor([sample]); const finalResponse=finalResponseFor([sample],traceId); + const status=statusFor([sample],traceId); const finalResponse=finalResponseFor([sample],traceId); const visibleTraceRows=traceRows.slice(-24); const rowLines=visibleTraceRows.map((row,index)=>{const text=textOf(row); return short((row.status?row.status+' ':'')+text,180)||('row#'+index+' '+(row.textHash||'-'));}); if(traceRows.length>visibleTraceRows.length) rowLines.unshift('(已省略 '+(traceRows.length-visibleTraceRows.length)+' 条较早 trace rows;需要原始数据请看 samples.jsonl)');