fix(web-probe): harden observe analyze recovery (#780)

Co-authored-by: Codex <codex@noreply.local>
This commit is contained in:
Lyon
2026-06-24 04:54:45 +08:00
committed by GitHub
parent be9b6dce23
commit e6fe4ca053
2 changed files with 61 additions and 5 deletions
+2 -1
View File
@@ -8873,7 +8873,8 @@ function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOptions, spec
});
}
function recoverWebObserveAnalyzeTurnDetails(options: NodeWebProbeObserveOptions, spec: HwlabRuntimeLaneSpec, analysis: Record<string, unknown>): Record<string, unknown> {
function recoverWebObserveAnalyzeTurnDetails(options: NodeWebProbeObserveOptions, spec: HwlabRuntimeLaneSpec, analysis: Record<string, unknown> | null): Record<string, unknown> | null {
if (analysis === null || typeof analysis !== "object") return analysis;
const sampleMetrics = record(analysis.sampleMetrics);
const hasRounds = Array.isArray(sampleMetrics?.rounds) && sampleMetrics.rounds.length > 0;
const hasColumns = Array.isArray(sampleMetrics?.turnColumns) && sampleMetrics.turnColumns.length > 0;
@@ -2046,7 +2046,12 @@ import { createInterface } from "node:readline";
const stateDir = path.resolve(process.env.UNIDESK_WEB_OBSERVE_STATE_DIR || process.argv[2] || ".state/web-observe/manual");
const archivePrefix = safeArchivePrefix(process.env.UNIDESK_WEB_OBSERVE_ANALYZE_ARCHIVE_PREFIX || "");
const analyzeTailSamples = (() => { const parsed = Number(process.env.UNIDESK_WEB_OBSERVE_ANALYZE_TAIL_SAMPLES); return Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : 360; })();
const analyzeTailSamples = (() => {
const raw = process.env.UNIDESK_WEB_OBSERVE_ANALYZE_TAIL_SAMPLES;
if (raw === undefined || raw === null || String(raw).trim() === "") return 360;
const parsed = Number(raw);
return Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : 360;
})();
const alertThresholds = parseAlertThresholds(process.env.UNIDESK_WEB_OBSERVE_ALERT_THRESHOLDS_JSON);
const dataDir = archivePrefix ? path.join(stateDir, "archive") : stateDir;
const dataFile = (name) => path.join(dataDir, archivePrefix ? archivePrefix + "-" + name : name);
@@ -3978,6 +3983,8 @@ function buildSampleMetrics(samples, control) {
const diagnostics = timeline.filter((item) => item.diagnosticSeen).length;
const loading = buildLoadingMetrics(samples, timeline);
const sessionRailTitles = buildSessionRailTitleMetrics(samples, timeline);
const reportTurnTimingRows = boundedTurnTimingRowsForReport(turnTiming.rows);
const reportTimeline = boundedRowsForReport(timeline);
const rounds = buildRoundMetricSummaries(timeline, promptCommands, {
columns: turnTiming.columns,
rows: turnTiming.rows,
@@ -4053,7 +4060,8 @@ function buildSampleMetrics(samples, control) {
traceOrder,
rounds,
turnColumns: turnTiming.columns,
turnTimingTable: turnTiming.rows,
turnTimingTable: reportTurnTimingRows.rows,
turnTimingTableDisclosure: reportTurnTimingRows.disclosure,
turnTimingNonMonotonic,
turnTimingElapsedZeroResets,
turnTimingTotalElapsedForwardJumps,
@@ -4062,7 +4070,29 @@ function buildSampleMetrics(samples, control) {
turnTimingRecentUpdateSteps,
turnTimingRecentUpdateLargestSteps,
turnTimingRecentUpdateResets,
timeline
timeline: reportTimeline.rows,
timelineDisclosure: reportTimeline.disclosure
};
}
function boundedRowsForReport(rows) {
const sourceRows = Array.isArray(rows) ? rows : [];
const maxRows = 1200;
const headRows = 120;
if (sourceRows.length <= maxRows) return { rows: sourceRows, disclosure: { truncated: false, totalRows: sourceRows.length, includedRows: sourceRows.length, omittedRows: 0, headRows: sourceRows.length, tailRows: 0 } };
const tailRows = Math.max(0, maxRows - headRows);
return {
rows: [...sourceRows.slice(0, headRows), ...sourceRows.slice(-tailRows)],
disclosure: {
truncated: true,
totalRows: sourceRows.length,
includedRows: maxRows,
omittedRows: Math.max(0, sourceRows.length - maxRows),
headRows,
tailRows,
policy: "report-bounded-head-tail; summary metrics are computed before truncation",
valuesRedacted: true
}
};
}
@@ -4408,6 +4438,27 @@ function finalizeLoadingSegment(segment, absentEvent) {
};
}
function boundedTurnTimingRowsForReport(rows) {
const sourceRows = Array.isArray(rows) ? rows : [];
const maxRows = 1200;
const headRows = 120;
if (sourceRows.length <= maxRows) return { rows: sourceRows, disclosure: { truncated: false, totalRows: sourceRows.length, includedRows: sourceRows.length, omittedRows: 0, headRows: sourceRows.length, tailRows: 0 } };
const tailRows = Math.max(0, maxRows - headRows);
return {
rows: [...sourceRows.slice(0, headRows), ...sourceRows.slice(-tailRows)],
disclosure: {
truncated: true,
totalRows: sourceRows.length,
includedRows: maxRows,
omittedRows: Math.max(0, sourceRows.length - maxRows),
headRows,
tailRows,
policy: "report-bounded-head-tail; full anomaly counters are computed before truncation",
valuesRedacted: true
}
};
}
function buildTurnTimingTable(samples, timeline) {
const columns = [];
const registry = new Map();
@@ -6408,6 +6459,7 @@ function compactHeartbeat(value) {
function renderTurnTimingTable(sampleMetrics) {
const columns = Array.isArray(sampleMetrics?.turnColumns) ? sampleMetrics.turnColumns : [];
const rows = Array.isArray(sampleMetrics?.turnTimingTable) ? sampleMetrics.turnTimingTable : [];
const disclosure = sampleMetrics?.turnTimingTableDisclosure || null;
if (columns.length === 0 || rows.length === 0) return "- 无 turn 时间表。";
const header = ["时间戳"];
for (const column of columns) {
@@ -6461,7 +6513,10 @@ 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总耗时异常前跳事件(预期按采样间隔近似递增;例如 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;
const disclosureLine = disclosure?.truncated
? "表格披露:已按 head/tail 有界输出,totalRows=" + disclosure.totalRows + " includedRows=" + disclosure.includedRows + " omittedRows=" + disclosure.omittedRows + ";异常计数在截断前基于全量采样计算。"
: "表格披露:完整输出当前分析窗口的采样点。";
return disclosureLine + "\n\n" + lines.join("\n") + "\n\n列说明:\n" + columnLines + "\n\n异常事件(仅报表暴露,不做下游 repair;最近更新按三角波模型检测异常跳增):\n" + nonMonotonicLines + "\n\nTerminal 后总耗时增长事件(终态 turn 的总耗时应 sealed,不应继续增长):\n" + terminalGrowthLines + "\n\n总耗时归零跳变事件(例如已显示真实耗时后又变成 0 秒):\n" + elapsedZeroResetLines + "\n\n总耗时异常前跳事件(预期按采样间隔近似递增;例如 14 秒 -> 1137 秒应被列入这里):\n" + totalElapsedForwardJumpLines + "\n\n最近更新 sawtooth jump 事件(预期每秒增长约 1,遇到新活动归零;例如 1 秒 -> 1 分 4 秒应被列入这里):\n" + sawtoothJumpLines + "\n\n最近更新相邻采样 step(按 delta 降序;用于人工识别一秒跳几十秒/一分钟的瞬态):\n" + stepLines + "\n\n最近更新 reset 事件(预期三角波归零,不计为异常):\n" + resetLines;
}
function formatMetricCell(value) {