From 2b89905a7395e1b7a6ce4213c54efef3103a1061 Mon Sep 17 00:00:00 2001 From: Lyon <88232613+pikasTech@users.noreply.github.com> Date: Tue, 23 Jun 2026 15:31:08 +0800 Subject: [PATCH] feat(web-probe): detect trace order and card timing drift (#737) Co-authored-by: Codex --- scripts/src/hwlab-node-impl.ts | 2 +- .../hwlab-node-web-observe-runner-source.ts | 889 ++++++++++++++++++ 2 files changed, 890 insertions(+), 1 deletion(-) diff --git a/scripts/src/hwlab-node-impl.ts b/scripts/src/hwlab-node-impl.ts index 6260658c..7415c5aa 100644 --- a/scripts/src/hwlab-node-impl.ts +++ b/scripts/src/hwlab-node-impl.ts @@ -2966,7 +2966,7 @@ function withNodeRuntimeTriggerRendered(result: Record, scoped: const warning = record(result.warning ?? pipelineWait.warning); const postFlush = record(result.postFlush); const gitMirror = record(result.gitMirror); - const gitMirrorSummary = record(gitMirror.statusSummary ?? gitMirror.afterSummary ?? gitMirror.beforeSummary ?? gitMirror.summary); + const gitMirrorSummary = record(postFlush.afterSummary ?? postFlush.beforeSummary ?? gitMirror.statusSummary ?? gitMirror.afterSummary ?? gitMirror.beforeSummary ?? gitMirror.summary); const refresh = record(result.refresh); const create = record(result.create); const next = record(result.next); diff --git a/scripts/src/hwlab-node-web-observe-runner-source.ts b/scripts/src/hwlab-node-web-observe-runner-source.ts index ef085bf4..9543d4a3 100644 --- a/scripts/src/hwlab-node-web-observe-runner-source.ts +++ b/scripts/src/hwlab-node-web-observe-runner-source.ts @@ -1600,11 +1600,14 @@ async function sampleOnePage(targetPage, { reason, groupSeq, pageRole, targetPag tag: element.tagName.toLowerCase(), testId: element.getAttribute("data-testid"), role: element.getAttribute("role"), + dataRole: element.getAttribute("data-role"), status: element.getAttribute("data-status") || element.getAttribute("aria-busy") || null, sessionId: element.getAttribute("data-session-id") || null, messageId: element.getAttribute("data-message-id") || element.getAttribute("id") || null, traceId: element.getAttribute("data-trace-id") || null, turnId: element.getAttribute("data-turn-id") || null, + durationText: trim(element.querySelector(".message-duration-meta")?.textContent || "", 120), + activityText: trim(element.querySelector(".message-activity-meta")?.textContent || "", 120), text: stableMessageText(element), rect: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) }, }; @@ -2449,6 +2452,7 @@ function compactDomItem(item) { tag: item.tag ?? null, testId: item.testId ?? null, role: item.role ?? null, + dataRole: item.dataRole ?? null, status: item.status ?? null, sessionId: item.sessionId ?? null, messageId: item.messageId ?? null, @@ -2580,6 +2584,44 @@ function buildFindings(samples, control, network, errors, sampleMetrics, promptN if (finalFlicker.length > 0) findings.push({ id: "final-response-flicker", severity: "red", summary: "message text digest disappeared or switched to diagnostic-like text after non-empty final text", count: finalFlicker.length, samples: finalFlicker.slice(0, 20) }); const terminalZeroElapsed = detectTerminalZeroElapsed(samples); if (terminalZeroElapsed.length > 0) findings.push({ id: "turn-terminal-zero-elapsed", severity: "red", summary: "terminal Code Agent card displayed 耗时 0 秒; terminal duration must come from durable timing projection, not a missing/zero display fallback", count: terminalZeroElapsed.length, samples: terminalZeroElapsed.slice(0, 20) }); + const cardTiming = sampleMetrics?.codeAgentCardTiming || {}; + const cardTimingSummary = cardTiming.summary || {}; + if (Number(cardTimingSummary.missingElapsedCount ?? 0) > 0) findings.push({ id: "code-agent-card-elapsed-missing", severity: "red", summary: "visible Code Agent card did not display total elapsed time; elapsed must be visible for running and terminal cards", count: cardTimingSummary.missingElapsedCount, samples: (cardTiming.missingElapsed || []).slice(0, 20) }); + if (Number(cardTimingSummary.missingRecentUpdateCount ?? 0) > 0) findings.push({ id: "code-agent-card-running-recent-update-missing", severity: "red", summary: "non-terminal Code Agent card did not display 最近更新", count: cardTimingSummary.missingRecentUpdateCount, samples: (cardTiming.missingRecentUpdate || []).slice(0, 20) }); + const roundCompletion = cardTiming.roundCompletion || {}; + if (Number(cardTimingSummary.durationUnderreportedCount ?? 0) > 0) { + findings.push({ + id: "code-agent-card-duration-underreported", + severity: "red", + summary: "completed Code Agent card total elapsed is shorter than trace/final-response duration evidence", + count: cardTimingSummary.durationUnderreportedCount, + samples: (cardTiming.durationUnderreported || []).slice(0, 20), + }); + } + const traceOrder = sampleMetrics?.traceOrder || {}; + const traceOrderSummary = traceOrder.summary || {}; + if (Number(traceOrderSummary.orderAnomalyCount ?? 0) > 0) { + findings.push({ + id: "trace-row-order-nonmonotonic", + severity: "red", + summary: "visible trace rows are not monotonic by total time, clock time, or projected sequence in DOM order", + count: traceOrderSummary.orderAnomalyCount, + samples: (traceOrder.orderAnomalies || []).slice(0, 20), + }); + } + if (Number(traceOrderSummary.completionNotLastCount ?? 0) > 0) { + findings.push({ + id: "trace-completion-row-not-last", + severity: "red", + summary: "visible trace shows a completion row before later trace rows for the same trace", + count: traceOrderSummary.completionNotLastCount, + samples: (traceOrder.completionNotLast || []).slice(0, 20), + }); + } + if (Number(cardTimingSummary.roundCompletionElapsedMismatchCount ?? 0) > 0) findings.push({ id: "round-completion-elapsed-mismatch", severity: "red", summary: "Trace row 轮次完成(总耗时 ...) does not match the visible Code Agent card total elapsed time within YAML timing slack", count: cardTimingSummary.roundCompletionElapsedMismatchCount, toleranceSeconds: cardTimingSummary.elapsedMismatchToleranceSeconds, samples: (roundCompletion.elapsedMismatches || []).slice(0, 20) }); + if (Number(cardTimingSummary.roundCompletionFinalResponseMissingCount ?? 0) > 0) findings.push({ id: "round-completion-final-response-missing", severity: "red", summary: "Trace row showed 轮次完成, but no final response was visible in the Code Agent card afterward", count: cardTimingSummary.roundCompletionFinalResponseMissingCount, samples: (roundCompletion.finalResponseMissing || []).slice(0, 20) }); + if (Number(cardTimingSummary.roundCompletionPostTimingChangeCount ?? 0) > 0) findings.push({ id: "round-completion-post-timing-change", severity: "red", summary: "After 轮次完成, card total elapsed or 最近更新 continued changing; terminal timing should be sealed", count: cardTimingSummary.roundCompletionPostTimingChangeCount, samples: (roundCompletion.postCompletionTimingChanges || []).slice(0, 20) }); + if (Number(cardTimingSummary.roundCompletionPostRecentUpdateVisibleCount ?? 0) > 0) findings.push({ id: "round-completion-recent-update-still-visible", severity: "info", summary: "最近更新 was still visible after 轮次完成; inspect whether terminal cards should hide activity age or keep it sealed", count: cardTimingSummary.roundCompletionPostRecentUpdateVisibleCount, samples: (roundCompletion.postCompletionRecentUpdateVisible || []).slice(0, 20) }); const scrollJumps = []; for (let i = 1; i < samples.length; i += 1) { const prevY = Number(samples[i - 1]?.scroll?.y ?? 0); @@ -3673,6 +3715,8 @@ function prioritizeFindings(findings) { const id = String(item?.id ?? item?.kind ?? item?.code ?? ""); if (id === "page-performance-slow-same-origin-api") return 0; if (id === "session-rail-title-fallback-majority") return 0.5; + if (id.startsWith("code-agent-card-")) return 0.8; + if (id.startsWith("round-completion-")) return 0.9; if (id.startsWith("turn-timing-total-elapsed")) return 1; if (id.startsWith("turn-timing-recent-update")) return 2; if (id.includes("runtime-execution") || id.includes("prompt-chat-submit-failed")) return 3; @@ -3736,6 +3780,13 @@ function buildSampleMetrics(samples, control) { }; }); const turnTiming = buildTurnTimingTable(samples, timeline); + const traceOrder = buildTraceOrderMetrics(samples, timeline); + const codeAgentCardTiming = buildCodeAgentCardTimingMetrics(samples, timeline, turnTiming); + const codeAgentCardDurationUnderreported = buildCodeAgentCardDurationUnderreportedMetrics(samples, timeline); + if (codeAgentCardTiming && codeAgentCardTiming.summary) { + codeAgentCardTiming.summary.durationUnderreportedCount = codeAgentCardDurationUnderreported.length; + codeAgentCardTiming.durationUnderreported = codeAgentCardDurationUnderreported; + } const turnCells = turnTiming.rows.flatMap((row) => Object.values(row.cells || {})); const turnTimingNonMonotonic = Array.isArray(turnTiming.nonMonotonic) ? turnTiming.nonMonotonic : []; const turnTimingElapsedZeroResets = Array.isArray(turnTiming.elapsedZeroResets) ? turnTiming.elapsedZeroResets : []; @@ -3813,6 +3864,17 @@ function buildSampleMetrics(samples, control) { turnTimingRecentUpdateMaxExcessSeconds: turnTimingRecentUpdateExcessSteps.length > 0 ? Math.max(...turnTimingRecentUpdateExcessSteps) : 0, turnTimingRecentUpdateResetCount: turnTimingRecentUpdateResets.length, turnTimingRecentUpdateDecreaseCount: turnTimingRecentUpdateResets.length, + codeAgentCardSampleCount: codeAgentCardTiming.summary.cardSampleCount, + codeAgentCardMissingElapsedCount: codeAgentCardTiming.summary.missingElapsedCount, + codeAgentCardMissingRecentUpdateCount: codeAgentCardTiming.summary.missingRecentUpdateCount, + roundCompletionEventCount: codeAgentCardTiming.summary.roundCompletionEventCount, + roundCompletionElapsedMismatchCount: codeAgentCardTiming.summary.roundCompletionElapsedMismatchCount, + roundCompletionFinalResponseMissingCount: codeAgentCardTiming.summary.roundCompletionFinalResponseMissingCount, + roundCompletionPostTimingChangeCount: codeAgentCardTiming.summary.roundCompletionPostTimingChangeCount, + codeAgentCardDurationUnderreportedCount: codeAgentCardTiming.summary.durationUnderreportedCount, + traceRowCount: traceOrder.summary.traceRowCount, + traceRowOrderAnomalyCount: traceOrder.summary.orderAnomalyCount, + traceRowCompletionNotLastCount: traceOrder.summary.completionNotLastCount, roundsWithTurnTimingNonMonotonic: rounds.filter((item) => item.turnTimingNonMonotonicCount > 0).length, roundsWithTurnTimingTotalElapsedForwardJumps: rounds.filter((item) => item.turnTimingTotalElapsedForwardJumpCount > 0).length, roundsWithTerminalElapsedGrowth: rounds.filter((item) => item.turnTimingTerminalElapsedGrowthCount > 0).length, @@ -3820,6 +3882,8 @@ function buildSampleMetrics(samples, control) { }, loading, sessionRailTitles, + codeAgentCardTiming, + traceOrder, rounds, turnColumns: turnTiming.columns, turnTimingTable: turnTiming.rows, @@ -4421,6 +4485,758 @@ function isTerminalTurnStatus(value) { return ["completed", "failed", "blocked", "timeout", "canceled", "cancelled", "stale", "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, + timestamp: 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 (reasons.length) { + orderAnomalies.push({ + sampleIndex: row.sampleIndex, + 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, + previousClockSeconds: previous.clockSeconds, + currentClockSeconds: row.clockSeconds, + 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, + 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, + 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.length ? traceMatched : traceRows); + 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 && 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 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, + }); + }); + } + 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); + return { + source, + rowIndex, + preview, + pageRole: meta.pageRole || null, + pageId: meta.pageId || null, + sessionId: meta.sessionId || null, + traceId: meta.traceId || parseTraceRowTraceId(preview), + clockSeconds: parseTraceRowClockSeconds(preview), + totalSeconds: parseTraceRowTotalSeconds(preview), + projectedSeq: parseTraceRowProjectedSeq(preview), + isCompletion: /轮次完成|turn\s+completed|completed\s+turn/i.test(preview), + }; +} + +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 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) { + return [row.pageRole || '', row.pageId || '', row.sessionId || '', row.traceId || 'unknown'].join('|'); +} + +function traceRowMatchesCard(row, card, terminalCardCount) { + if (!row) return false; + if (row.traceId && card.traceId) return row.traceId === card.traceId; + if (row.sessionId && card.sessionId) return 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); + evidences.push({ kind: 'trace-total', seconds: maxTotal, traceId: source?.traceId || null, preview: source?.preview || '' }); + } + 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' }); + } + 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 (const line of lines) { + if (!/耗时|用时|duration|elapsed/i.test(line)) continue; + const seconds = parseSelfReportedRoundDurationSeconds(line); + if (!Number.isFinite(seconds)) continue; + if (!best || seconds > best.seconds) { + best = { kind: 'final-response-duration', seconds, preview: compactOneLine(line).slice(0, 240) }; + } + } + return best; +} + +function parseSelfReportedRoundDurationSeconds(text) { + const value = String(text || ''); + const clock = value.match(/(\d{1,2}:\d{2}:\d{2}|\d{1,2}:\d{2})/); + 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(text).filter(Number.isFinite); + const recentUpdateValues = parseRecentUpdateSeconds(text).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]), + 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) { + if (isTerminalTurnStatus(item?.status)) return true; + const text = codeAgentCardText(item); + return isTerminalTraceText(text) || /轮次完成|轮次失败|轮次取消|已记录|completed|failed|canceled|cancelled|blocked/iu.test(text); +} + +function buildRoundCompletionMetrics(samples, timeline, turnTiming) { + const events = []; + 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 elapsedMismatches = []; + const finalResponseMissing = []; + const postCompletionTimingChanges = []; + const postCompletionRecentUpdateVisible = []; + for (const event of events) { + 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 > alertThresholds.turnTimingSampleSlackSeconds) { + elapsedMismatches.push({ + ...eventRef(event), + cardTotalElapsedSeconds: Number(bestCard.totalElapsedSeconds), + completionElapsedSeconds: Number(event.elapsedSeconds), + deltaSeconds: Number(delta.toFixed(3)), + toleranceSeconds: alertThresholds.turnTimingSampleSlackSeconds, + cardTraceId: bestCard.traceId ?? null, + cardMessageId: bestCard.messageId ?? null, + valuesRedacted: true + }); + } + } + if (!hasFinalResponseAfterCompletion(samples, timeline, event)) { + finalResponseMissing.push({ + ...eventRef(event), + completionElapsedSeconds: event.elapsedSeconds, + valuesRedacted: true + }); + } + const postTiming = detectPostCompletionTimingChanges(turnTiming, event); + postCompletionTimingChanges.push(...postTiming.timingChanges); + postCompletionRecentUpdateVisible.push(...postTiming.recentUpdateVisible); + } + return { + events: dedupeRoundCompletionRows(events).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 Array.isArray(sample?.traceRows) ? sample.traceRows : []) { + const text = String(item?.text || item?.textPreview || ""); + if (!/轮次完成/iu.test(text)) continue; + const elapsedValues = parseTotalElapsedSeconds(text).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?.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) { + if (!event.traceId && !event.messageId) return []; + return turnMetricItems(sample, timelineItem) + .filter((item) => item.promptIndex === event.promptIndex) + .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) 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.promptIndex && promptIndex !== event.promptIndex) continue; + if (sampleHasVisibleFinalResponse(sample, event)) return true; + } + return false; +} + +function sampleHasVisibleFinalResponse(sample, event = {}) { + for (const group of [sample?.messages, sample?.turns]) { + if (!Array.isArray(group)) continue; + for (const item of group) { + if (event.traceId && item?.traceId && item.traceId !== event.traceId) continue; + if (event.messageId && item?.messageId && item.messageId !== event.messageId) continue; + const text = normalizedText([item?.text, item?.textPreview].filter(Boolean).join(" ")); + if (text.length < 24) continue; + if (isDiagnosticText(text)) continue; + if (isFinalResultText(text)) return true; + if (/运行记录/iu.test(text) && /(?:已完成|完成|新增|实现|验证|测试|结果|README|文件|summary)/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 = 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 = 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 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, + }; +} + +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"; @@ -5067,6 +5883,11 @@ function renderMarkdown(report) { 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") @@ -5089,6 +5910,33 @@ function renderMarkdown(report) { 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 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") : "- 未观察到“加载中”可见区间。"; @@ -5161,7 +6009,48 @@ function renderMarkdown(report) { + "- 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" + + "- 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" + + "- 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" + + "### 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"