From 5fa5708fe11bb7e085b6b162976e696de692240f Mon Sep 17 00:00:00 2001 From: Lyon <88232613+pikasTech@users.noreply.github.com> Date: Fri, 26 Jun 2026 15:06:19 +0800 Subject: [PATCH] fix: avoid blocking sentinel on terminal timing correction (#992) Co-authored-by: Codex --- .../PJ2026-01060508-web-probe-sentinel.md | 2 ++ ...node-web-observe-analyzer-timing-source.ts | 22 ++++++++++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/project-management/PJ2026-01/specs/PJ2026-01060508-web-probe-sentinel.md b/project-management/PJ2026-01/specs/PJ2026-01060508-web-probe-sentinel.md index 6c39a473..8f1cc8c5 100644 --- a/project-management/PJ2026-01/specs/PJ2026-01060508-web-probe-sentinel.md +++ b/project-management/PJ2026-01/specs/PJ2026-01060508-web-probe-sentinel.md @@ -571,3 +571,5 @@ Dashboard 增强执行 issue 为 [#935](https://github.com/pikasTech/unidesk/iss P7 P6 收口必须区分 public dashboard validation 与 targetValidation quick verify:`monitor.pikapython.com` root/CSS/JS 200 只证明公开入口和静态 dashboard 资源可用;quick verify 的 `observe command`、采样样本、analyze report 和 red finding 仍是业务恢复判定的一部分。若 quick verify 因 `observe-command-newSession-failed`、`no-samples` 或等价采样器控制失败而 blocked,P6 issue 必须保持未关闭或显式拆出后续 blocker,不得只凭 public dashboard 200 关闭。 P8 哨兵恢复执行 issue 为 [#971](https://github.com/pikasTech/unidesk/issues/971)。P8 closeout 必须回写:SPEC P8 引用、120s warning 证据、`quick-verify-no-business-turn` 或等价业务触达证据、`browser-timeout` 分类修正、中文运维页面验证、`monitor.pikapython.com` 公网入口验证、k3s 内部 Service DNS quick verify 路径、D601/v03 用户入口 smoke 结果,以及仍未解除的真实业务 blocker 是否已单独拆出。 + +P8-P8 起,targetValidation 的 availability blocker 与 Workbench timing 架构治理必须分层。若同一 trace 在 terminal 前最后一帧仍为 running,随后 terminal commit 的 sealed `durationMs` 把可见 `totalElapsed` 小幅校正到更短值,且 drop 不超过 YAML `turnTimingSampleSlackSeconds`、final response 与完成行均已可见,则该现象作为 terminal-boundary timing correction 证据保留,不生成 `turn-timing-total-elapsed-decrease` red blocker。归零、running/running 下降、terminal 后增长、完成耗时与卡片耗时超出 slack、trace 乱序和完成行非最后仍保持 red;根因治理继续归 [HWLAB #2055](https://github.com/pikasTech/HWLAB/issues/2055) 和 [HWLAB #2125](https://github.com/pikasTech/HWLAB/issues/2125),不得在 UI、CLI renderer 或 analyzer 中做读侧 repair。 diff --git a/scripts/src/hwlab-node-web-observe-analyzer-timing-source.ts b/scripts/src/hwlab-node-web-observe-analyzer-timing-source.ts index 11881538..967fa0d2 100644 --- a/scripts/src/hwlab-node-web-observe-analyzer-timing-source.ts +++ b/scripts/src/hwlab-node-web-observe-analyzer-timing-source.ts @@ -91,6 +91,7 @@ export function nodeWebObserveAnalyzerTimingSource(): string { elapsedZeroResets: timingEvents.elapsedZeroResets, totalElapsedForwardJumps: timingEvents.totalElapsedForwardJumps, terminalElapsedGrowth: timingEvents.terminalElapsedGrowth, + terminalElapsedCorrections: timingEvents.terminalElapsedCorrections, recentUpdateResets: timingEvents.recentUpdateResets, recentUpdateSteps: timingEvents.recentUpdateSteps }; @@ -120,6 +121,7 @@ function detectTurnTimingNonMonotonic(columns, rows) { const elapsedZeroResets = []; const totalElapsedForwardJumps = []; const terminalElapsedGrowth = []; + const terminalElapsedCorrections = []; const recentUpdateResets = []; const recentUpdateSteps = []; for (const column of columns) { @@ -135,6 +137,9 @@ function detectTurnTimingNonMonotonic(columns, rows) { const previous = previousByMetric.get(metric); if (previous && metric === "totalElapsedSeconds" && current < previous.value) { const anomaly = previous.value > 0 && current === 0 ? "zero-reset" : "decrease"; + const terminalTransition = !isTerminalTurnStatus(previous.status) && isTerminalTurnStatus(cell.status); + const dropSeconds = previous.value - current; + const allowedDropSeconds = Math.max(1, Number(alertThresholds?.turnTimingSampleSlackSeconds || 0)); const event = { columnId: column.id, columnLabel: column.label, @@ -144,9 +149,11 @@ function detectTurnTimingNonMonotonic(columns, rows) { fromSeq: previous.seq, fromTs: previous.ts, fromValue: previous.value, + fromStatus: previous.status ?? null, toSeq: row.seq ?? null, toTs: row.ts ?? null, toValue: current, + toStatus: cell.status ?? null, delta: current - previous.value, traceId: cell.traceId ?? column.traceId ?? null, messageId: cell.messageId ?? column.messageId ?? null, @@ -158,8 +165,17 @@ function detectTurnTimingNonMonotonic(columns, rows) { pageEpoch: cell.pageEpoch ?? row.pageEpoch ?? column.pageEpoch ?? null, valuesRedacted: true }; - anomalies.push(event); - if (anomaly === "zero-reset") elapsedZeroResets.push(event); + if (terminalTransition && anomaly === "decrease" && dropSeconds <= allowedDropSeconds) { + terminalElapsedCorrections.push({ + ...event, + anomaly: "terminal-boundary-correction", + expectedPattern: "terminal-sealed-duration-may-correct-running-elapsed-within-slack", + allowedDropSeconds, + }); + } else { + anomalies.push(event); + if (anomaly === "zero-reset") elapsedZeroResets.push(event); + } } if (previous && metric === "totalElapsedSeconds" && current > previous.value) { const sampleDeltaSeconds = elapsedSecondsBetween(previous.ts, row.ts); @@ -319,7 +335,7 @@ function detectTurnTimingNonMonotonic(columns, rows) { } } } - return { anomalies, elapsedZeroResets, totalElapsedForwardJumps, terminalElapsedGrowth, recentUpdateResets, recentUpdateSteps }; + return { anomalies, elapsedZeroResets, totalElapsedForwardJumps, terminalElapsedGrowth, terminalElapsedCorrections, recentUpdateResets, recentUpdateSteps }; } function elapsedSecondsBetween(fromTs, toTs) {