diff --git a/scripts/src/hwlab-node-web-observe-runner-source.ts b/scripts/src/hwlab-node-web-observe-runner-source.ts index 35b9cccc..a3881025 100644 --- a/scripts/src/hwlab-node-web-observe-runner-source.ts +++ b/scripts/src/hwlab-node-web-observe-runner-source.ts @@ -1260,10 +1260,12 @@ function buildSampleMetrics(samples, control) { const turnTiming = buildTurnTimingTable(samples, timeline); const turnCells = turnTiming.rows.flatMap((row) => Object.values(row.cells || {})); const turnTimingNonMonotonic = Array.isArray(turnTiming.nonMonotonic) ? turnTiming.nonMonotonic : []; + const turnTimingRecentUpdateResets = Array.isArray(turnTiming.recentUpdateResets) ? turnTiming.recentUpdateResets : []; const withTotal = timeline.filter((item) => item.totalElapsedSeconds !== null).length; const withRecent = timeline.filter((item) => item.recentUpdateSeconds !== null).length; const diagnostics = timeline.filter((item) => item.diagnosticSeen).length; const rounds = buildRoundMetricSummaries(timeline, promptCommands); + const recentUpdateJumpCount = turnTimingNonMonotonic.filter((item) => item.metric === "recentUpdateSeconds" && item.anomaly === "jump").length; return { summary: { sampleCount: timeline.length, @@ -1278,12 +1280,15 @@ function buildSampleMetrics(samples, control) { turnCellsWithRecentUpdate: turnCells.filter((item) => item.recentUpdateSeconds !== null).length, turnTimingNonMonotonicCount: turnTimingNonMonotonic.length, turnTimingTotalElapsedDecreaseCount: turnTimingNonMonotonic.filter((item) => item.metric === "totalElapsedSeconds").length, - turnTimingRecentUpdateDecreaseCount: turnTimingNonMonotonic.filter((item) => item.metric === "recentUpdateSeconds").length + turnTimingRecentUpdateJumpCount: recentUpdateJumpCount, + turnTimingRecentUpdateResetCount: turnTimingRecentUpdateResets.length, + turnTimingRecentUpdateDecreaseCount: turnTimingRecentUpdateResets.length }, rounds, turnColumns: turnTiming.columns, turnTimingTable: turnTiming.rows, turnTimingNonMonotonic, + turnTimingRecentUpdateResets, timeline }; } @@ -1343,11 +1348,18 @@ function buildTurnTimingTable(samples, timeline) { cells }); } - return { columns, rows, nonMonotonic: detectTurnTimingNonMonotonic(columns, rows) }; + const timingEvents = detectTurnTimingNonMonotonic(columns, rows); + return { + columns, + rows, + nonMonotonic: timingEvents.anomalies, + recentUpdateResets: timingEvents.recentUpdateResets + }; } function detectTurnTimingNonMonotonic(columns, rows) { - const events = []; + const anomalies = []; + const recentUpdateResets = []; for (const column of columns) { const previousByMetric = new Map(); for (const row of rows) { @@ -1358,11 +1370,12 @@ function detectTurnTimingNonMonotonic(columns, rows) { if (value === null || value === undefined || !Number.isFinite(Number(value))) continue; const current = Number(value); const previous = previousByMetric.get(metric); - if (previous && current < previous.value) { - events.push({ + if (previous && metric === "totalElapsedSeconds" && current < previous.value) { + anomalies.push({ columnId: column.id, columnLabel: column.label, metric, + anomaly: "decrease", fromSeq: previous.seq, fromTs: previous.ts, fromValue: previous.value, @@ -1380,10 +1393,31 @@ function detectTurnTimingNonMonotonic(columns, rows) { if (previous && metric === "recentUpdateSeconds") { const elapsedMs = Date.parse(String(row.ts ?? "")) - Date.parse(String(previous.ts ?? "")); const elapsedSeconds = Number.isFinite(elapsedMs) && elapsedMs >= 0 ? elapsedMs / 1000 : null; - const allowedIncrease = elapsedSeconds === null ? 10 : Math.max(10, elapsedSeconds + 5); const increase = current - previous.value; + if (increase < 0) { + recentUpdateResets.push({ + columnId: column.id, + columnLabel: column.label, + metric: "recentUpdateSeconds", + event: "reset", + fromSeq: previous.seq, + fromTs: previous.ts, + fromValue: previous.value, + toSeq: row.seq ?? null, + toTs: row.ts ?? null, + toValue: current, + delta: increase, + sampleDeltaSeconds: elapsedSeconds, + traceId: cell.traceId ?? column.traceId ?? null, + messageId: cell.messageId ?? column.messageId ?? null, + promptIndex: cell.promptIndex ?? row.promptIndex ?? null, + source: cell.source ?? column.source ?? null, + valuesRedacted: true + }); + } + const allowedIncrease = elapsedSeconds === null ? 10 : Math.max(10, elapsedSeconds + 5); if (increase > allowedIncrease) { - events.push({ + anomalies.push({ columnId: column.id, columnLabel: column.label, metric: "recentUpdateSeconds", @@ -1409,7 +1443,7 @@ function detectTurnTimingNonMonotonic(columns, rows) { } } } - return events; + return { anomalies, recentUpdateResets }; } function turnMetricItems(sample, timelineItem) { @@ -1686,8 +1720,12 @@ function renderTurnTimingTable(sampleMetrics) { const nonMonotonic = Array.isArray(sampleMetrics?.turnTimingNonMonotonic) ? sampleMetrics.turnTimingNonMonotonic : []; const nonMonotonicLines = nonMonotonic.length > 0 ? nonMonotonic.slice(0, 80).map((item) => "- " + (item.columnLabel || item.columnId || "-") + " " + item.metric + (item.anomaly ? " " + item.anomaly : "") + " " + (item.fromValue ?? "-") + " -> " + (item.toValue ?? "-") + " delta=" + (item.delta ?? "-") + " sampleDelta=" + (item.sampleDeltaSeconds ?? "-") + " allowed=" + (item.allowedIncreaseSeconds ?? "-") + " seq " + (item.fromSeq ?? "-") + " -> " + (item.toSeq ?? "-") + " ts " + (item.fromTs || "-") + " -> " + (item.toTs || "-") + " traceId=" + (item.traceId || "-")).join("\n") - : "- 未观察到数值下降事件。"; - return lines.join("\n") + "\n\n列说明:\n" + columnLines + "\n\n非单调/跳增事件(仅报表暴露,不做下游 repair):\n" + nonMonotonicLines; + : "- 未观察到总耗时下降或最近更新异常跳增。"; + const recentUpdateResets = Array.isArray(sampleMetrics?.turnTimingRecentUpdateResets) ? sampleMetrics.turnTimingRecentUpdateResets : []; + 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\n最近更新 reset 事件(预期三角波归零,不计为异常):\n" + resetLines; } function formatMetricCell(value) { @@ -1745,6 +1783,12 @@ function renderMarkdown(report) { + "- withRecentUpdate: " + (metricSummary.withRecentUpdate ?? 0) + "\n" + "- diagnostics: " + (metricSummary.diagnostics ?? 0) + "\n" + "- promptSegments: " + (metricSummary.promptSegments ?? 0) + "\n\n" + + "- turnColumns: " + (metricSummary.turnColumns ?? 0) + "\n" + + "- turnTimingRows: " + (metricSummary.turnTimingRows ?? 0) + "\n" + + "- turnTimingNonMonotonicCount: " + (metricSummary.turnTimingNonMonotonicCount ?? 0) + "\n" + + "- turnTimingTotalElapsedDecreaseCount: " + (metricSummary.turnTimingTotalElapsedDecreaseCount ?? 0) + "\n" + + "- turnTimingRecentUpdateJumpCount: " + (metricSummary.turnTimingRecentUpdateJumpCount ?? 0) + "\n" + + "- turnTimingRecentUpdateResetCount: " + (metricSummary.turnTimingRecentUpdateResetCount ?? 0) + "\n\n" + "### Rounds\n\n" + roundLines + "\n\n" + "### Prompt network\n\n" + promptNetworkLines + "\n\n" + "### Runtime alerts\n\n"