diff --git a/scripts/src/hwlab-node-web-observe-runner-source.ts b/scripts/src/hwlab-node-web-observe-runner-source.ts index f5f4bb86..73b4ba6b 100644 --- a/scripts/src/hwlab-node-web-observe-runner-source.ts +++ b/scripts/src/hwlab-node-web-observe-runner-source.ts @@ -2638,6 +2638,8 @@ function buildFindings(samples, control, network, errors, sampleMetrics, promptN if (elapsedZeroResets.length > 0) findings.push({ id: "turn-timing-total-elapsed-zero-reset", severity: "red", summary: "Code Agent total elapsed jumped from a non-zero value back to 0 seconds", count: elapsedZeroResets.length, samples: elapsedZeroResets.slice(0, 20) }); const elapsedForwardJumps = Array.isArray(sampleMetrics?.turnTimingTotalElapsedForwardJumps) ? sampleMetrics.turnTimingTotalElapsedForwardJumps : []; if (elapsedForwardJumps.length > 0) findings.push({ id: "turn-timing-total-elapsed-forward-jump", severity: "red", summary: "Code Agent total elapsed jumped forward faster than browser sample interval", count: elapsedForwardJumps.length, samples: elapsedForwardJumps.slice(0, 20) }); + const terminalElapsedGrowth = Array.isArray(sampleMetrics?.turnTimingTerminalElapsedGrowth) ? sampleMetrics.turnTimingTerminalElapsedGrowth : []; + if (terminalElapsedGrowth.length > 0) findings.push({ id: "turn-timing-terminal-elapsed-growth", severity: "red", summary: "terminal Code Agent card total elapsed changed after terminal status; completed/failed/canceled timing must be sealed", count: terminalElapsedGrowth.length, samples: terminalElapsedGrowth.slice(0, 20) }); const recentUpdateSawtoothJumps = Array.isArray(sampleMetrics?.turnTimingRecentUpdateSawtoothJumps) ? sampleMetrics.turnTimingRecentUpdateSawtoothJumps : Array.isArray(sampleMetrics?.turnTimingNonMonotonic) @@ -3718,6 +3720,7 @@ function prioritizeFindings(findings) { 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-terminal-elapsed")) return 1.1; if (id.startsWith("turn-timing-recent-update")) return 2; if (id.includes("runtime-execution") || id.includes("prompt-chat-submit-failed")) return 3; return 10; @@ -4844,17 +4847,37 @@ 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); + 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(line).slice(0, 240) }; + best = { kind: 'final-response-duration', seconds, preview: compactOneLine(candidateText).slice(0, 240) }; } } 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);