From 9924212bd9ab7963e45a52ac3a26980dcda5737a Mon Sep 17 00:00:00 2001 From: Lyon <88232613+pikasTech@users.noreply.github.com> Date: Mon, 22 Jun 2026 13:59:55 +0800 Subject: [PATCH] fix: expose full-window web probe timing anomalies (#651) Co-authored-by: Codex --- scripts/src/hwlab-node-impl.ts | 99 +++++++++++++-- .../hwlab-node-web-observe-runner-source.ts | 115 +++++++++++++++++- 2 files changed, 200 insertions(+), 14 deletions(-) diff --git a/scripts/src/hwlab-node-impl.ts b/scripts/src/hwlab-node-impl.ts index e551f834..b5dc9c9d 100644 --- a/scripts/src/hwlab-node-impl.ts +++ b/scripts/src/hwlab-node-impl.ts @@ -7602,17 +7602,21 @@ function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOptions, spec "const takeHead = (value, limit) => Array.isArray(value) ? value.slice(0, limit) : [];", "const takeTail = (value, limit) => Array.isArray(value) ? value.slice(-limit) : [];", "const firstArray = (...values) => { for (const value of values) if (Array.isArray(value)) return value; return []; };", + "const mergeArrays = (...values) => { const out = []; const seen = new Set(); for (const value of values) { if (!Array.isArray(value)) continue; for (const item of value) { const key = JSON.stringify([item?.id ?? item?.kind ?? item?.code ?? item?.columnLabel ?? item?.traceId ?? null, item?.summary ?? item?.message ?? item?.fromSeq ?? null, item?.toSeq ?? null]); if (seen.has(key)) continue; seen.add(key); out.push(item); } } return out; };", "const clip = (value, limit = 160) => value === null || value === undefined ? null : String(value).slice(0, limit);", + "const findingRank = (item) => { const id = String(item?.id ?? item?.kind ?? item?.code ?? ''); if (id === 'page-performance-slow-same-origin-api') return 0; 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; return 10; };", + "const severityRank = (item) => { const severity = String(item?.severity ?? item?.level ?? '').toLowerCase(); if (severity === 'red') return 0; if (severity === 'amber' || severity === 'warning') return 1; if (severity === 'info') return 3; return 2; };", + "const sortFindings = (items) => (Array.isArray(items) ? items : []).slice().sort((a, b) => findingRank(a) - findingRank(b) || severityRank(a) - severityRank(b));", "const stdoutJson = readJson(stdoutPath);", "const reportJson = readJson(reportJsonPath);", "const source = (stdoutJson && stdoutJson.ok !== false ? stdoutJson : null) || reportJson || stdoutJson || null;", "const fullSource = reportJson || source;", "const objectOrNull = (value) => value && typeof value === 'object' && !Array.isArray(value) ? value : null;", - "const slimRound = (item) => { const v = objectOrNull(item) || {}; return { promptIndex: v.promptIndex ?? null, sampleCount: v.sampleCount ?? null, lastTotalElapsedSeconds: v.lastTotalElapsedSeconds ?? null, lastRecentUpdateSeconds: v.lastRecentUpdateSeconds ?? null, diagnosticSamples: v.diagnosticSamples ?? null, terminalSamples: v.terminalSamples ?? null, promptTextHash: clip(v.promptTextHash, 80) }; };", + "const slimRound = (item) => { const v = objectOrNull(item) || {}; return { promptIndex: v.promptIndex ?? null, sampleCount: v.sampleCount ?? null, lastTotalElapsedSeconds: v.lastTotalElapsedSeconds ?? null, lastRecentUpdateSeconds: v.lastRecentUpdateSeconds ?? null, diagnosticSamples: v.diagnosticSamples ?? null, terminalSamples: v.terminalSamples ?? null, turnTimingTotalElapsedForwardJumpCount: v.turnTimingTotalElapsedForwardJumpCount ?? null, turnTimingTotalElapsedForwardJumpMaxSeconds: v.turnTimingTotalElapsedForwardJumpMaxSeconds ?? null, promptTextHash: clip(v.promptTextHash, 80) }; };", "const slimTurnColumn = (item) => { const v = objectOrNull(item) || {}; return { label: clip(v.label, 24), promptIndex: v.promptIndex ?? null, lastPromptIndex: v.lastPromptIndex ?? null, traceId: clip(v.traceId, 48), firstSeq: v.firstSeq ?? null, lastSeq: v.lastSeq ?? null, source: clip(v.source, 48) }; };", "const slimSlowSample = (item) => { const v = objectOrNull(item) || {}; return { ts: v.ts ?? null, seq: v.seq ?? null, path: clip(v.path ?? v.rawPath, 96), initiatorType: clip(v.initiatorType, 24), durationMs: v.durationMs ?? null, requestToResponseStartMs: v.requestToResponseStartMs ?? v.streamOpenMs ?? null, responseTransferMs: v.responseTransferMs ?? null, nextHopProtocol: clip(v.nextHopProtocol, 24) }; };", "const slimSlowApi = (item) => { const v = objectOrNull(item) || {}; return { path: clip(v.path ?? v.route, 96), route: clip(v.route ?? v.path, 96), sampleCount: v.sampleCount ?? null, p95Ms: v.p95Ms ?? v.p95 ?? null, maxMs: v.maxMs ?? v.max ?? null, overFiveSecondCount: v.overFiveSecondCount ?? null, slowSamples: Array.isArray(v.slowSamples) ? v.slowSamples.slice(0, 3).map(slimSlowSample) : [] }; };", - "const slimFinding = (item) => { const v = objectOrNull(item) || {}; return { kind: clip(v.kind ?? v.code, 48), code: clip(v.code ?? v.kind, 48), severity: clip(v.severity ?? v.level, 24), level: clip(v.level ?? v.severity, 24), count: v.count ?? v.sampleCount ?? null, sampleCount: v.sampleCount ?? v.count ?? null, summary: clip(v.summary ?? v.message, 180), message: clip(v.message ?? v.summary, 180) }; };", + "const slimFinding = (item) => { const v = objectOrNull(item) || {}; return { kind: clip(v.kind ?? v.id ?? v.code, 48), code: clip(v.code ?? v.id ?? v.kind, 48), severity: clip(v.severity ?? v.level, 24), level: clip(v.level ?? v.severity, 24), count: v.count ?? v.sampleCount ?? null, sampleCount: v.sampleCount ?? v.count ?? null, summary: clip(v.summary ?? v.message, 180), message: clip(v.message ?? v.summary, 180) }; };", "const slimDomGroup = (item) => { const v = objectOrNull(item) || {}; return { count: v.count ?? null, firstAt: v.firstAt ?? null, lastAt: v.lastAt ?? null, text: clip(v.text ?? v.preview, 180) }; };", "const slimNetworkGroup = (item) => { const v = objectOrNull(item) || {}; return { count: v.count ?? null, method: clip(v.method, 12), status: v.status ?? null, path: clip(v.path, 96), firstAt: v.firstAt ?? null, lastAt: v.lastAt ?? null, promptIndexes: Array.isArray(v.promptIndexes) ? v.promptIndexes.slice(0, 6) : [], failureKinds: Array.isArray(v.failureKinds) ? v.failureKinds.slice(0, 4).map((x) => clip(x, 48)) : [] }; };", "const slimDomSample = (item) => { const v = objectOrNull(item) || {}; return { seq: v.seq ?? null, ts: v.ts ?? null, source: clip(v.source, 32), diagnosticCode: clip(v.diagnosticCode, 48), traceId: clip(v.traceId, 64), httpStatus: v.httpStatus ?? null, idleSeconds: v.idleSeconds ?? null, waitingFor: clip(v.waitingFor, 48), lastEventLabel: clip(v.lastEventLabel, 80), text: clip(v.text ?? v.preview, 180) }; };", @@ -7628,6 +7632,8 @@ function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOptions, spec " turnTimingRows: Array.isArray(srcMetrics.turnTimingRows) ? srcMetrics.turnTimingRows.length : (srcMetrics.turnTimingRows ?? null),", " turnTimingNonMonotonicCount: srcMetrics.turnTimingNonMonotonicCount ?? null,", " turnTimingTotalElapsedDecreaseCount: srcMetrics.turnTimingTotalElapsedDecreaseCount ?? null,", + " turnTimingTotalElapsedForwardJumpCount: srcMetrics.turnTimingTotalElapsedForwardJumpCount ?? null,", + " turnTimingTotalElapsedForwardJumpMaxSeconds: srcMetrics.turnTimingTotalElapsedForwardJumpMaxSeconds ?? null,", " turnTimingTerminalElapsedGrowthCount: srcMetrics.turnTimingTerminalElapsedGrowthCount ?? null,", " turnTimingRecentUpdateJumpCount: srcMetrics.turnTimingRecentUpdateJumpCount ?? null,", " turnTimingRecentUpdateSawtoothJumpCount: srcMetrics.turnTimingRecentUpdateSawtoothJumpCount ?? null,", @@ -7642,6 +7648,7 @@ function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOptions, spec "const fullRecentWindow = objectOrNull(fullSource?.windows?.recent);", "const fullPagePerformance = objectOrNull(fullRecentWindow?.pagePerformance) || objectOrNull(fullSource?.pagePerformance);", "const sourceSlowApi = Array.isArray(source?.pagePerformanceSlowApi) && source.pagePerformanceSlowApi.some((item) => Array.isArray(item?.slowSamples) && item.slowSamples.length > 0) ? source.pagePerformanceSlowApi : (Array.isArray(fullPagePerformance?.sameOriginApiByPath) ? fullPagePerformance.sameOriginApiByPath.filter((item) => Number(item?.overFiveSecondCount ?? 0) > 0) : []);", + "const combinedFindings = sortFindings(mergeArrays(source?.findings, fullSource?.findings));", "const srcPromptNetwork = objectOrNull(source?.promptNetwork);", "const promptNetwork = srcPromptNetwork ? { promptSegments: srcPromptNetwork.promptSegments ?? null } : null;", "const compact = source ? {", @@ -7654,7 +7661,7 @@ function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOptions, spec " pagePerformance,", " promptNetwork,", " pagePerformanceSlowApi: takeHead(sourceSlowApi, 4).map(slimSlowApi),", - " findings: takeHead(firstArray(source.findings, fullSource?.findings), 6).map(slimFinding),", + " findings: takeHead(combinedFindings, 8).map(slimFinding),", " httpErrorGroups: takeHead(firstArray(source.httpErrorGroups, fullSource?.httpErrorGroups), 4).map(slimNetworkGroup),", " requestFailedGroups: takeHead(firstArray(source.requestFailedGroups, fullSource?.requestFailedGroups), 5).map(slimNetworkGroup),", " domDiagnosticGroups: takeHead(firstArray(source.domDiagnosticGroups, fullSource?.domDiagnosticGroups), 3).map(slimDomGroup),", @@ -7662,8 +7669,9 @@ function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOptions, spec " consoleAlertGroups: takeHead(firstArray(source.consoleAlertGroups, fullSource?.consoleAlertGroups), 5).map(slimConsoleGroup),", " consoleAlertSamples: takeHead(firstArray(source.consoleAlertSamples, fullSource?.consoleAlertSamples), 5).map(slimConsoleSample),", " runnerErrors: takeTail(firstArray(source.runnerErrors, fullSource?.runnerErrors), 8).map(slimRunnerError),", - " turnTimingRecentUpdateJumps: takeHead(firstArray(source.turnTimingRecentUpdateJumps, srcMetrics?.turnTimingRecentUpdateJumps, fullSource?.turnTimingRecentUpdateJumps), 5).map(slimJump),", - " turnTimingElapsedZeroResets: takeHead(firstArray(source.turnTimingElapsedZeroResets, srcMetrics?.turnTimingElapsedZeroResets, fullSource?.turnTimingElapsedZeroResets), 5).map(slimJump),", + " turnTimingRecentUpdateJumps: takeHead(mergeArrays(source.turnTimingRecentUpdateJumps, srcMetrics?.turnTimingRecentUpdateJumps, fullSource?.sampleMetrics?.turnTimingRecentUpdateSawtoothJumps, fullSource?.turnTimingRecentUpdateJumps), 5).map(slimJump),", + " turnTimingElapsedZeroResets: takeHead(mergeArrays(source.turnTimingElapsedZeroResets, srcMetrics?.turnTimingElapsedZeroResets, fullSource?.sampleMetrics?.turnTimingElapsedZeroResets, fullSource?.turnTimingElapsedZeroResets), 5).map(slimJump),", + " turnTimingTotalElapsedForwardJumps: takeHead(mergeArrays(source.turnTimingTotalElapsedForwardJumps, srcMetrics?.turnTimingTotalElapsedForwardJumps, fullSource?.sampleMetrics?.turnTimingTotalElapsedForwardJumps, fullSource?.turnTimingTotalElapsedForwardJumps), 5).map(slimJump),", " reportJsonPath: source.reportJsonPath || reportJsonPath,", " reportJsonSha256: source.reportJsonSha256 || sha256(reportJsonPath),", " reportMdPath: source.reportMdPath || reportMdPath,", @@ -7688,7 +7696,47 @@ function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOptions, spec " stderrTail: tail(readText(stderrPath))", " }", "};", - "console.log(JSON.stringify(compact));", + "const compactStdoutLimitBytes = 8000;", + "const compactOutput = (value) => JSON.stringify(value);", + "let output = compactOutput(compact);", + "if (Buffer.byteLength(output, 'utf8') > compactStdoutLimitBytes) {", + " const minimal = {", + " ok: compact.ok,", + " counts: compact.counts ?? null,", + " analysisWindow: compact.analysisWindow ?? null,", + " archiveSummary: compact.archiveSummary ?? null,", + " sampleMetrics: compact.sampleMetrics ? {", + " rounds: Array.isArray(compact.sampleMetrics.rounds) ? compact.sampleMetrics.rounds : compact.sampleMetrics.rounds ?? null,", + " turnTimingRows: compact.sampleMetrics.turnTimingRows ?? null,", + " turnTimingNonMonotonicCount: compact.sampleMetrics.turnTimingNonMonotonicCount ?? null,", + " turnTimingTotalElapsedDecreaseCount: compact.sampleMetrics.turnTimingTotalElapsedDecreaseCount ?? null,", + " turnTimingTotalElapsedForwardJumpCount: compact.sampleMetrics.turnTimingTotalElapsedForwardJumpCount ?? null,", + " turnTimingTotalElapsedForwardJumpMaxSeconds: compact.sampleMetrics.turnTimingTotalElapsedForwardJumpMaxSeconds ?? null,", + " turnTimingTerminalElapsedGrowthCount: compact.sampleMetrics.turnTimingTerminalElapsedGrowthCount ?? null,", + " turnTimingRecentUpdateJumpCount: compact.sampleMetrics.turnTimingRecentUpdateJumpCount ?? null,", + " turnTimingRecentUpdateSawtoothJumpCount: compact.sampleMetrics.turnTimingRecentUpdateSawtoothJumpCount ?? null,", + " turnTimingRecentUpdateMaxIncreaseSeconds: compact.sampleMetrics.turnTimingRecentUpdateMaxIncreaseSeconds ?? null,", + " turnColumns: Array.isArray(compact.sampleMetrics.turnColumns) ? compact.sampleMetrics.turnColumns.slice(-6) : []", + " } : null,", + " runtimeAlerts: compact.runtimeAlerts ?? null,", + " pagePerformance: compact.pagePerformance ?? null,", + " promptNetwork: compact.promptNetwork ?? null,", + " findings: Array.isArray(compact.findings) ? compact.findings.slice(0, 4) : [],", + " pagePerformanceSlowApi: Array.isArray(compact.pagePerformanceSlowApi) ? compact.pagePerformanceSlowApi.slice(0, 3) : [],", + " runnerErrors: Array.isArray(compact.runnerErrors) ? compact.runnerErrors.slice(-4) : [],", + " turnTimingRecentUpdateJumps: Array.isArray(compact.turnTimingRecentUpdateJumps) ? compact.turnTimingRecentUpdateJumps.slice(0, 4) : [],", + " turnTimingElapsedZeroResets: Array.isArray(compact.turnTimingElapsedZeroResets) ? compact.turnTimingElapsedZeroResets.slice(0, 4) : [],", + " turnTimingTotalElapsedForwardJumps: Array.isArray(compact.turnTimingTotalElapsedForwardJumps) ? compact.turnTimingTotalElapsedForwardJumps.slice(0, 4) : [],", + " reportJsonPath: compact.reportJsonPath ?? reportJsonPath,", + " reportJsonSha256: compact.reportJsonSha256 ?? sha256(reportJsonPath),", + " reportMdPath: compact.reportMdPath ?? reportMdPath,", + " reportMdSha256: compact.reportMdSha256 ?? sha256(reportMdPath),", + " analyzer: { ...(compact.analyzer ?? {}), compactStdoutLimited: true, compactStdoutLimitBytes, originalCompactBytes: Buffer.byteLength(output, 'utf8') },", + " valuesRedacted: true", + " };", + " output = compactOutput(minimal);", + "}", + "console.log(output);", "UNIDESK_WEB_OBSERVE_ANALYZE_COMPACT", "exit \"$analyzer_exit\"", ].join("\n"); @@ -7725,6 +7773,7 @@ function renderWebObserveAnalyzeTable(value: Record): string { const analysisWindow = record(analysis?.analysisWindow); const archiveSummary = record(analysis?.archiveSummary); const sampleMetrics = record(analysis?.sampleMetrics); + const archiveMetrics = record(archiveSummary?.sampleMetrics); const runtimeAlerts = record(analysis?.runtimeAlerts); const pagePerformance = record(analysis?.pagePerformance); const promptNetwork = record(analysis?.promptNetwork); @@ -7743,6 +7792,7 @@ function renderWebObserveAnalyzeTable(value: Record): string { const runnerErrors = Array.isArray(analysis?.runnerErrors) ? analysis.runnerErrors.map(record).filter((item): item is Record => item !== null).slice(-8) : []; const recentUpdateJumps = Array.isArray(analysis?.turnTimingRecentUpdateJumps) ? analysis.turnTimingRecentUpdateJumps.map(record).filter((item): item is Record => item !== null).slice(0, 8) : []; const elapsedZeroResets = Array.isArray(analysis?.turnTimingElapsedZeroResets) ? analysis.turnTimingElapsedZeroResets.map(record).filter((item): item is Record => item !== null).slice(0, 8) : []; + const elapsedForwardJumps = Array.isArray(analysis?.turnTimingTotalElapsedForwardJumps) ? analysis.turnTimingTotalElapsedForwardJumps.map(record).filter((item): item is Record => item !== null).slice(0, 8) : []; const domDiagnosticRows = domDiagnostics.length > 0 ? domDiagnostics.map((item) => [ webObserveText(item.count), @@ -7801,6 +7851,17 @@ function renderWebObserveAnalyzeTable(value: Record): string { webObserveText(item.delta), webObserveShort(webObserveText(item.traceId), 24), ]); + const elapsedForwardJumpRows = elapsedForwardJumps.map((item) => [ + webObserveShort(webObserveText(item.columnLabel), 14), + webObserveText(item.promptIndex), + webObserveText(item.fromSeq), + webObserveText(item.toSeq), + `${webObserveText(item.fromValue)}->${webObserveText(item.toValue)}`, + webObserveText(item.delta), + webObserveText(item.sampleDeltaSeconds), + webObserveText(item.allowedIncreaseSeconds), + webObserveShort(webObserveText(item.traceId), 24), + ]); const httpErrorRows = httpErrorGroups.map((item) => [ webObserveText(item.count), webObserveText(item.method), @@ -7866,18 +7927,34 @@ function renderWebObserveAnalyzeTable(value: Record): string { ]), "", "Turn timing:", - webObserveTable(["ROUNDS", "TURNS", "ROWS", "NON_MONO", "ELAPSED_DEC", "ZERO_RESET", "TERMINAL_GROWTH", "RECENT_JUMP", "MAX_RECENT_STEP"], [[ + webObserveTable(["ROUNDS", "TURNS", "ROWS", "NON_MONO", "ELAPSED_DEC", "ZERO_RESET", "ELAPSED_JUMP", "TERMINAL_GROWTH", "RECENT_JUMP", "MAX_RECENT_STEP"], [[ webObserveText(roundCount), webObserveText(turnColumnCount), webObserveText(sampleMetrics?.turnTimingRows), webObserveText(sampleMetrics?.turnTimingNonMonotonicCount), webObserveText(sampleMetrics?.turnTimingTotalElapsedDecreaseCount), webObserveText(sampleMetrics?.turnTimingTotalElapsedZeroResetCount ?? elapsedZeroResets.length), + webObserveText(sampleMetrics?.turnTimingTotalElapsedForwardJumpCount ?? elapsedForwardJumps.length), webObserveText(sampleMetrics?.turnTimingTerminalElapsedGrowthCount), webObserveText(sampleMetrics?.turnTimingRecentUpdateJumpCount ?? sampleMetrics?.turnTimingRecentUpdateSawtoothJumpCount), webObserveText(sampleMetrics?.turnTimingRecentUpdateMaxIncreaseSeconds), ]]), "", + "Archive turn timing:", + webObserveTable(["SAMPLES", "ROUNDS", "TURNS", "ROWS", "NON_MONO", "ELAPSED_DEC", "ZERO_RESET", "ELAPSED_JUMP", "TERMINAL_GROWTH", "RECENT_JUMP", "MAX_RECENT_STEP"], [[ + webObserveText(archiveMetrics?.sampleCount), + webObserveText(archiveMetrics?.rounds), + webObserveText(archiveMetrics?.turnColumns), + webObserveText(archiveMetrics?.turnTimingRows), + webObserveText(archiveMetrics?.turnTimingNonMonotonicCount), + webObserveText(archiveMetrics?.turnTimingTotalElapsedDecreaseCount), + webObserveText(archiveMetrics?.turnTimingTotalElapsedZeroResetCount), + webObserveText(archiveMetrics?.turnTimingTotalElapsedForwardJumpCount), + webObserveText(archiveMetrics?.turnTimingTerminalElapsedGrowthCount), + webObserveText(archiveMetrics?.turnTimingRecentUpdateJumpCount ?? archiveMetrics?.turnTimingRecentUpdateSawtoothJumpCount), + webObserveText(archiveMetrics?.turnTimingRecentUpdateMaxIncreaseSeconds), + ]]), + "", "Rounds:", webObserveTable(["ROUND", "SAMPLES", "TOTAL_LAST", "RECENT_LAST", "DIAG", "TERMINAL", "PROMPT_HASH"], rounds.length > 0 ? rounds.map((item) => [ webObserveText(item.promptIndex), @@ -7905,6 +7982,9 @@ function renderWebObserveAnalyzeTable(value: Record): string { "Total elapsed zero resets:", webObserveTable(["TURN", "PROMPT", "FROM_SEQ", "TO_SEQ", "VALUE", "DELTA", "TRACE"], elapsedZeroResetRows.length > 0 ? elapsedZeroResetRows : [["-", "-", "-", "-", "-", "-", "-"]]), "", + "Total elapsed forward jumps:", + webObserveTable(["TURN", "PROMPT", "FROM_SEQ", "TO_SEQ", "VALUE", "DELTA", "SAMPLE_S", "ALLOWED", "TRACE"], elapsedForwardJumpRows.length > 0 ? elapsedForwardJumpRows : [["-", "-", "-", "-", "-", "-", "-", "-", "-"]]), + "", "Runtime alerts:", webObserveTable(["HTTP_ERR", "REQUEST_FAILED", "DOM_DIAG", "CONSOLE_ALERT", "PROMPT_SEGMENTS", "PROMPT_NETWORK"], [[ webObserveText(runtimeAlerts?.httpErrorCount), @@ -8372,6 +8452,8 @@ function renderWebObserveAnalyzeResult(result: Record): Record< [ ["nonMonotonic", sampleMetrics.turnTimingNonMonotonicCount], ["elapsedDecrease", sampleMetrics.turnTimingTotalElapsedDecreaseCount], + ["elapsedForwardJump", sampleMetrics.turnTimingTotalElapsedForwardJumpCount], + ["elapsedForwardJumpMaxSec", sampleMetrics.turnTimingTotalElapsedForwardJumpMaxSeconds], ["recentJump", sampleMetrics.turnTimingRecentUpdateJumpCount], ["maxRecentStepSec", sampleMetrics.turnTimingRecentUpdateMaxIncreaseSeconds], ], @@ -8380,7 +8462,7 @@ function renderWebObserveAnalyzeResult(result: Record): Record< rounds.length === 0 ? "ROUNDS\n-" : webObserveTable( - ["ROUND", "COMMAND", "SAMPLES", "TOTAL_MAX", "TOTAL_LAST", "RECENT_MAX", "RECENT_LAST", "DIAG", "TERM", "NONMONO", "JUMP"], + ["ROUND", "COMMAND", "SAMPLES", "TOTAL_MAX", "TOTAL_LAST", "RECENT_MAX", "RECENT_LAST", "DIAG", "TERM", "NONMONO", "ELAPSED_JUMP", "JUMP"], rounds.map((item) => [ item.promptIndex, item.promptCommandId, @@ -8392,6 +8474,7 @@ function renderWebObserveAnalyzeResult(result: Record): Record< item.diagnosticSamples, item.terminalSamples, item.turnTimingNonMonotonicCount, + item.turnTimingTotalElapsedForwardJumpCount, item.turnTimingRecentUpdateJumpCount, ]), ), diff --git a/scripts/src/hwlab-node-web-observe-runner-source.ts b/scripts/src/hwlab-node-web-observe-runner-source.ts index 89ddc2c0..db7fa334 100644 --- a/scripts/src/hwlab-node-web-observe-runner-source.ts +++ b/scripts/src/hwlab-node-web-observe-runner-source.ts @@ -1308,7 +1308,7 @@ console.log(JSON.stringify({ readIssues: jsonlReadIssues.slice(0, 3).map((item) => ({ file: item.file, line: item.line ?? null, error: String(item.error ?? "").slice(0, 160) })), sampleMetrics: { ...recentWindow.sampleMetrics.summary, - rounds: recentWindow.sampleMetrics.rounds.slice(-8).map((item) => ({ promptIndex: item.promptIndex, promptTextHash: item.promptTextHash, sampleCount: item.sampleCount, firstSeq: item.firstSeq, lastSeq: item.lastSeq, lastTotalElapsedSeconds: item.lastTotalElapsedSeconds, lastRecentUpdateSeconds: item.lastRecentUpdateSeconds, diagnosticSamples: item.diagnosticSamples, terminalSamples: item.terminalSamples, finalTextSamples: item.finalTextSamples, turnTimingTotalElapsedZeroResetCount: item.turnTimingTotalElapsedZeroResetCount, turnTimingRecentUpdateJumpCount: item.turnTimingRecentUpdateJumpCount, turnTimingRecentUpdateMaxIncreaseSeconds: item.turnTimingRecentUpdateMaxIncreaseSeconds })), + rounds: recentWindow.sampleMetrics.rounds.slice(-8).map((item) => ({ promptIndex: item.promptIndex, promptTextHash: item.promptTextHash, sampleCount: item.sampleCount, firstSeq: item.firstSeq, lastSeq: item.lastSeq, lastTotalElapsedSeconds: item.lastTotalElapsedSeconds, lastRecentUpdateSeconds: item.lastRecentUpdateSeconds, diagnosticSamples: item.diagnosticSamples, terminalSamples: item.terminalSamples, finalTextSamples: item.finalTextSamples, turnTimingTotalElapsedZeroResetCount: item.turnTimingTotalElapsedZeroResetCount, turnTimingTotalElapsedForwardJumpCount: item.turnTimingTotalElapsedForwardJumpCount, turnTimingTotalElapsedForwardJumpMaxSeconds: item.turnTimingTotalElapsedForwardJumpMaxSeconds, turnTimingRecentUpdateJumpCount: item.turnTimingRecentUpdateJumpCount, turnTimingRecentUpdateMaxIncreaseSeconds: item.turnTimingRecentUpdateMaxIncreaseSeconds })), turnColumns: recentWindow.sampleMetrics.turnColumns.slice(-12).map((item) => ({ label: item.label, source: item.source, promptIndex: item.promptIndex, lastPromptIndex: item.lastPromptIndex, firstSeq: item.firstSeq, lastSeq: item.lastSeq, traceId: item.traceId, messageId: item.messageId })), }, pageProvenance: recentWindow.pageProvenance.summary, @@ -1405,8 +1405,24 @@ console.log(JSON.stringify({ delta: item.delta ?? null, anomaly: item.anomaly ?? null, })), - pagePerformanceSlowApi: recentWindow.pagePerformance.sameOriginApiByPath.filter((item) => item.overFiveSecondCount > 0).slice(0, 5).map((item) => ({ path: item.path, route: item.route, sampleCount: item.sampleCount, p95Ms: item.p95Ms, maxMs: item.maxMs, overFiveSecondCount: item.overFiveSecondCount })), - findings: recentWindow.findings.slice(0, 8).map((item) => ({ kind: item.id ?? item.kind ?? item.code, severity: item.severity, count: item.count ?? item.sampleCount ?? null, summary: String(item.summary ?? item.message ?? "").slice(0, 180) })), + turnTimingTotalElapsedForwardJumps: recentWindow.sampleMetrics.turnTimingTotalElapsedForwardJumps.slice(0, 8).map((item) => ({ + columnLabel: item.columnLabel ?? item.columnId ?? null, + promptIndex: item.promptIndex ?? null, + traceId: item.traceId ?? null, + fromSeq: item.fromSeq ?? null, + toSeq: item.toSeq ?? null, + fromTs: item.fromTs ?? null, + toTs: item.toTs ?? null, + fromValue: item.fromValue ?? null, + toValue: item.toValue ?? null, + delta: item.delta ?? null, + sampleDeltaSeconds: item.sampleDeltaSeconds ?? null, + allowedIncreaseSeconds: item.allowedIncreaseSeconds ?? null, + excessiveIncreaseSeconds: item.excessiveIncreaseSeconds ?? null, + anomaly: item.anomaly ?? null, + })), + pagePerformanceSlowApi: recentWindow.pagePerformance.sameOriginApiByPath.filter((item) => item.overFiveSecondCount > 0).slice(0, 5).map((item) => ({ path: item.path, route: item.route, sampleCount: item.sampleCount, p95Ms: item.p95Ms, maxMs: item.maxMs, overFiveSecondCount: item.overFiveSecondCount, slowSamples: item.slowSamples })), + findings: prioritizeFindings(recentWindow.findings).slice(0, 8).map((item) => ({ kind: item.id ?? item.kind ?? item.code, severity: item.severity, count: item.count ?? item.sampleCount ?? null, summary: String(item.summary ?? item.message ?? "").slice(0, 180) })), valuesRedacted: true, })); @@ -1575,6 +1591,8 @@ function buildFindings(samples, control, network, errors, sampleMetrics, promptN if (promptFailures.length > 0) findings.push({ id: "prompt-chat-submit-failed", severity: "red", summary: "sendPrompt command had no successful /v1/agent/chat POST response in the sampling window", count: promptFailures.length, rounds: promptFailures.slice(0, 10) }); const elapsedZeroResets = Array.isArray(sampleMetrics?.turnTimingElapsedZeroResets) ? sampleMetrics.turnTimingElapsedZeroResets : []; 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 recentUpdateSawtoothJumps = Array.isArray(sampleMetrics?.turnTimingRecentUpdateSawtoothJumps) ? sampleMetrics.turnTimingRecentUpdateSawtoothJumps : Array.isArray(sampleMetrics?.turnTimingNonMonotonic) @@ -2496,6 +2514,30 @@ function isDiagnosticText(text) { return /Failed to (?:fetch|load resource)|request failed|net::ERR_[A-Z0-9_:-]+|server responded with a status of [45][0-9]{2}|HTTP\s+[45][0-9]{2}\b|trace_id=|workbench turn\s*超过|turn\s*超过|无新活动|idle\s+\d+s|waitingFor=|lastEventLabel=|无法连接上游|代理暂时无法连接上游|provider-unavailable|agentrun:error|AgentRun error|projection-resume|sync-failed|durable projection store|realtime-gap|Trace 更新超时|加载失败|请求失败|请求已失败/iu.test(value); } +function prioritizeFindings(findings) { + const items = Array.isArray(findings) ? findings : []; + const severityRank = (severity) => { + const value = String(severity || "").toLowerCase(); + if (value === "red") return 0; + if (value === "amber" || value === "warning") return 1; + if (value === "info") return 3; + return 2; + }; + const kindRank = (item) => { + const id = String(item?.id ?? item?.kind ?? item?.code ?? ""); + if (id === "page-performance-slow-same-origin-api") return 0; + 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; + return 10; + }; + return items.slice().sort((left, right) => { + const kindDelta = kindRank(left) - kindRank(right); + if (kindDelta !== 0) return kindDelta; + return severityRank(left?.severity ?? left?.level) - severityRank(right?.severity ?? right?.level); + }); +} + function isTerminalTraceText(text) { return /轮次完成|轮次失败|轮次取消|已记录|已完成第\d+轮|final response|sealed final response|turn completed|turn failed|turn canceled|terminal result|\bcompleted\b|\bfailed\b|\bcanceled\b|\bcancelled\b|\bterminal\b|\bdone\b/iu.test(String(text || "")); } @@ -2541,6 +2583,7 @@ function buildSampleMetrics(samples, control) { const turnCells = turnTiming.rows.flatMap((row) => Object.values(row.cells || {})); const turnTimingNonMonotonic = Array.isArray(turnTiming.nonMonotonic) ? turnTiming.nonMonotonic : []; const turnTimingElapsedZeroResets = Array.isArray(turnTiming.elapsedZeroResets) ? turnTiming.elapsedZeroResets : []; + const turnTimingTotalElapsedForwardJumps = Array.isArray(turnTiming.totalElapsedForwardJumps) ? turnTiming.totalElapsedForwardJumps : []; const turnTimingRecentUpdateSawtoothJumps = turnTimingNonMonotonic.filter((item) => item.metric === "recentUpdateSeconds" && item.anomaly === "jump"); const turnTimingTerminalElapsedGrowth = Array.isArray(turnTiming.terminalElapsedGrowth) ? turnTiming.terminalElapsedGrowth : []; const turnTimingRecentUpdateResets = Array.isArray(turnTiming.recentUpdateResets) ? turnTiming.recentUpdateResets : []; @@ -2565,6 +2608,7 @@ function buildSampleMetrics(samples, control) { const rounds = buildRoundMetricSummaries(timeline, promptCommands, { nonMonotonic: turnTimingNonMonotonic, elapsedZeroResets: turnTimingElapsedZeroResets, + totalElapsedForwardJumps: turnTimingTotalElapsedForwardJumps, terminalElapsedGrowth: turnTimingTerminalElapsedGrowth, recentUpdateResets: turnTimingRecentUpdateResets, recentUpdateSteps: turnTimingRecentUpdateSteps @@ -2585,6 +2629,8 @@ function buildSampleMetrics(samples, control) { turnTimingNonMonotonicCount: turnTimingNonMonotonic.length, turnTimingTotalElapsedDecreaseCount: turnTimingNonMonotonic.filter((item) => item.metric === "totalElapsedSeconds").length, turnTimingTotalElapsedZeroResetCount: turnTimingElapsedZeroResets.length, + turnTimingTotalElapsedForwardJumpCount: turnTimingTotalElapsedForwardJumps.length, + turnTimingTotalElapsedForwardJumpMaxSeconds: maxPositiveDelta(turnTimingTotalElapsedForwardJumps), turnTimingTerminalElapsedGrowthCount: turnTimingTerminalElapsedGrowth.length, turnTimingTerminalElapsedGrowthMaxSeconds: turnTimingTerminalElapsedGrowthDeltas.length > 0 ? Math.max(...turnTimingTerminalElapsedGrowthDeltas) : 0, turnTimingRecentUpdateJumpCount: recentUpdateJumpCount, @@ -2595,6 +2641,7 @@ function buildSampleMetrics(samples, control) { turnTimingRecentUpdateResetCount: turnTimingRecentUpdateResets.length, turnTimingRecentUpdateDecreaseCount: turnTimingRecentUpdateResets.length, roundsWithTurnTimingNonMonotonic: rounds.filter((item) => item.turnTimingNonMonotonicCount > 0).length, + roundsWithTurnTimingTotalElapsedForwardJumps: rounds.filter((item) => item.turnTimingTotalElapsedForwardJumpCount > 0).length, roundsWithTerminalElapsedGrowth: rounds.filter((item) => item.turnTimingTerminalElapsedGrowthCount > 0).length, roundsWithRecentUpdateJumps: rounds.filter((item) => item.turnTimingRecentUpdateJumpCount > 0).length }, @@ -2603,6 +2650,7 @@ function buildSampleMetrics(samples, control) { turnTimingTable: turnTiming.rows, turnTimingNonMonotonic, turnTimingElapsedZeroResets, + turnTimingTotalElapsedForwardJumps, turnTimingTerminalElapsedGrowth, turnTimingRecentUpdateSawtoothJumps, turnTimingRecentUpdateSteps, @@ -2673,6 +2721,7 @@ function buildTurnTimingTable(samples, timeline) { rows, nonMonotonic: timingEvents.anomalies, elapsedZeroResets: timingEvents.elapsedZeroResets, + totalElapsedForwardJumps: timingEvents.totalElapsedForwardJumps, terminalElapsedGrowth: timingEvents.terminalElapsedGrowth, recentUpdateResets: timingEvents.recentUpdateResets, recentUpdateSteps: timingEvents.recentUpdateSteps @@ -2682,6 +2731,7 @@ function buildTurnTimingTable(samples, timeline) { function detectTurnTimingNonMonotonic(columns, rows) { const anomalies = []; const elapsedZeroResets = []; + const totalElapsedForwardJumps = []; const terminalElapsedGrowth = []; const recentUpdateResets = []; const recentUpdateSteps = []; @@ -2720,6 +2770,35 @@ function detectTurnTimingNonMonotonic(columns, rows) { 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 + 3; + if (delta > allowedIncreaseSeconds) { + totalElapsedForwardJumps.push({ + columnId: column.id, + columnLabel: column.label, + metric, + anomaly: "forward-jump", + expectedPattern: "total-elapsed-increase-matches-browser-sample-interval", + fromSeq: previous.seq, + fromTs: previous.ts, + fromValue: previous.value, + toSeq: row.seq ?? null, + toTs: row.ts ?? null, + toValue: current, + delta, + sampleDeltaSeconds, + allowedIncreaseSeconds, + excessiveIncreaseSeconds: Number((delta - allowedIncreaseSeconds).toFixed(3)), + traceId: cell.traceId ?? column.traceId ?? null, + messageId: cell.messageId ?? column.messageId ?? null, + promptIndex: cell.promptIndex ?? row.promptIndex ?? null, + source: cell.source ?? column.source ?? null, + valuesRedacted: true + }); + } + } if (metric === "totalElapsedSeconds" && isTerminalTurnStatus(cell.status)) { if (previousTerminalTotal && current > previousTerminalTotal.value) { terminalElapsedGrowth.push({ @@ -2824,7 +2903,21 @@ function detectTurnTimingNonMonotonic(columns, rows) { } } } - return { anomalies, elapsedZeroResets, terminalElapsedGrowth, recentUpdateResets, recentUpdateSteps }; + 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) { @@ -2931,6 +3024,7 @@ function firstTraceId(texts) { function buildRoundMetricSummaries(timeline, promptCommands, timing = {}) { const rounds = []; 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 : []; @@ -2941,6 +3035,7 @@ function buildRoundMetricSummaries(timeline, promptCommands, timing = {}) { const totalElapsed = items.map((item) => item.totalElapsedSeconds).filter((value) => value !== null); const recentUpdate = items.map((item) => item.recentUpdateSeconds).filter((value) => value !== null); 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); @@ -2971,6 +3066,8 @@ function buildRoundMetricSummaries(timeline, promptCommands, timing = {}) { 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, @@ -3151,6 +3248,10 @@ function renderTurnTimingTable(sampleMetrics) { const elapsedZeroResetLines = elapsedZeroResets.length > 0 ? elapsedZeroResets.slice(0, 80).map((item) => "- " + (item.columnLabel || item.columnId || "-") + " 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) => "- " + (item.columnLabel || item.columnId || "-") + " 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"); @@ -3169,7 +3270,7 @@ function renderTurnTimingTable(sampleMetrics) { const resetLines = recentUpdateResets.length > 0 ? recentUpdateResets.slice(0, 80).map((item) => "- " + (item.columnLabel || item.columnId || "-") + " 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") : "- 未观察到最近更新归零/回落。"; - return 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最近更新 sawtooth jump 事件(预期每秒增长约 1,遇到新活动归零;例如 1 秒 -> 1 分 4 秒应被列入这里):\n" + sawtoothJumpLines + "\n\n最近更新相邻采样 step(按 delta 降序;用于人工识别一秒跳几十秒/一分钟的瞬态):\n" + stepLines + "\n\n最近更新 reset 事件(预期三角波归零,不计为异常):\n" + resetLines; + return 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 formatMetricCell(value) { @@ -3206,7 +3307,7 @@ function renderMarkdown(report) { ? report.promptNetwork.rounds.map((item) => "- round " + item.promptIndex + " promptHash=" + (item.promptTextHash || "-") + " chatPostOk=" + String(item.chatPostOk) + " 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 + " totalMax=" + (item.maxTotalElapsedSeconds ?? "-") + " totalLast=" + (item.lastTotalElapsedSeconds ?? "-") + " recentMax=" + (item.maxRecentUpdateSeconds ?? "-") + " recentLast=" + (item.lastRecentUpdateSeconds ?? "-") + " totalDecrease=" + (item.turnTimingTotalElapsedDecreaseCount ?? 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") + ? report.sampleMetrics.rounds.map((item) => "- round " + item.promptIndex + " promptHash=" + (item.promptTextHash || "-") + " samples=" + item.sampleCount + " 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 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") @@ -3237,6 +3338,8 @@ function renderMarkdown(report) { + "- 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"