fix(web-probe): compare visible digests per observer page (#766)

Co-authored-by: Codex <codex@noreply.local>
This commit is contained in:
Lyon
2026-06-24 02:33:42 +08:00
committed by GitHub
parent 8b42c15162
commit 9cc3c3e197
@@ -2690,7 +2690,10 @@ function summarizeCommandFailures(control) {
function buildFindings(samples, control, network, errors, sampleMetrics, promptNetwork, runtimeAlerts, pagePerformance, pageProvenance, commandFailures = []) {
const findings = [];
if (commandFailures.length > 0) findings.push({ id: "observer-command-failed", severity: "red", summary: "observer control commands failed; analyze must surface command failure instead of hiding it in command artifacts", count: commandFailures.length, commands: commandFailures.slice(0, 20) });
const commandTimes = control.filter((item) => item.phase === "completed" || item.phase === "started").map((item) => Date.parse(item.ts)).filter(Number.isFinite);
const commandTimes = control
.filter((item) => item.phase === "completed" || item.phase === "started" || item.type === "observer-periodic-refresh")
.map((item) => Date.parse(item.ts))
.filter(Number.isFinite);
const routeSessions = new Set(samples.map((item) => item.routeSessionId).filter(Boolean));
const activeSessions = new Set(samples.map((item) => item.activeSessionId).filter(Boolean));
if (routeSessions.size > 1) findings.push({ id: "session-route-changed", severity: "amber", summary: "route session changed during observation", routeSessionCount: routeSessions.size, samples: sampleRefs(samples, (item) => item.routeSessionId) });
@@ -2699,10 +2702,13 @@ function buildFindings(samples, control, network, errors, sampleMetrics, promptN
if (mismatches.length > 0) findings.push({ id: "route-active-session-mismatch", severity: "red", summary: "routeSessionId and activeSessionId diverged", count: mismatches.length, samples: mismatches.slice(0, 10).map(ref) });
const uncommandedChanges = [];
const commandedPromptSeqs = new Set((sampleMetrics?.timeline ?? []).filter((item) => Number(item?.promptIndex) > 0).map((item) => item.seq).filter((seq) => seq !== null && seq !== undefined));
for (let i = 1; i < samples.length; i += 1) {
const prev = digestSample(samples[i - 1]);
const next = digestSample(samples[i]);
if (prev !== next && !commandedPromptSeqs.has(samples[i]?.seq) && !nearCommand(samples[i], commandTimes, alertThresholds.uncommandedStateChangeCommandWindowMs)) uncommandedChanges.push(ref(samples[i]));
const previousDigestByPage = new Map();
for (const sample of samples) {
const pageKey = samplePageKey(sample);
const previous = previousDigestByPage.get(pageKey);
const next = digestSample(sample);
if (previous && previous.digest !== next && !commandedPromptSeqs.has(sample?.seq) && !nearCommand(sample, commandTimes, alertThresholds.uncommandedStateChangeCommandWindowMs)) uncommandedChanges.push(ref(sample));
previousDigestByPage.set(pageKey, { digest: next, sample });
}
if (uncommandedChanges.length > 0) findings.push({ id: "uncommanded-visible-state-change", severity: "amber", summary: "visible message/trace digest changed without a nearby command", count: uncommandedChanges.length, samples: uncommandedChanges.slice(0, 20) });
const finalFlicker = detectFinalFlicker(samples);
@@ -6225,12 +6231,29 @@ function longestSharedSubstringLength(a, b) {
}
function digestSample(sample) {
const messages = Array.isArray(sample.messages) ? sample.messages.map((item) => item.textHash || item.textPreview || "").join("|") : "";
const trace = Array.isArray(sample.traceRows) ? sample.traceRows.map((item) => (item.status || "") + ":" + (item.textHash || item.textPreview || "")).join("|") : "";
const diagnostics = Array.isArray(sample.diagnostics) ? sample.diagnostics.map((item) => (item.className || "") + ":" + (item.textHash || item.textPreview || "")).join("|") : "";
const messages = Array.isArray(sample.messages) ? sample.messages.map((item) => stableDigestItem(item, ["dataRole", "role", "status", "sessionId", "messageId", "traceId", "turnId"])).join("|") : "";
const trace = Array.isArray(sample.traceRows) ? sample.traceRows.map((item) => stableDigestItem(item, ["status", "sessionId", "messageId", "traceId", "turnId", "projectedSeq", "sourceSeq", "eventSeq", "eventKind"])).join("|") : "";
const diagnostics = Array.isArray(sample.diagnostics) ? sample.diagnostics.map((item) => stableDigestItem(item, ["className", "status", "sessionId", "messageId", "traceId", "turnId", "diagnosticCode"])).join("|") : "";
return sha256((sample.routeSessionId || "") + "|" + (sample.activeSessionId || "") + "|" + messages + "|" + trace + "|" + diagnostics);
}
function samplePageKey(sample) {
return String(sample?.pageRole || "control") + ":" + String(sample?.pageId || "default");
}
function stableDigestItem(item, fields) {
if (!item || typeof item !== "object") return "";
const identity = fields.map((field) => String(item?.[field] ?? "")).join(":");
return identity + ":" + stableVisibleDigestText(item?.textPreview || item?.text || item?.textHash || "");
}
function stableVisibleDigestText(value) {
return normalizedText(value)
.replace(/\btotal=\d{1,2}:\d{2}:\d{2}\b/giu, "total=<duration>")
.replace(/(?:|)\s*[=:]?\s*(?:(?:\d+\s*)?\s*(?:\d+\s*)?\s*(?:\d+\s*(?:|))?\s*(?:\d+\s*)?|\d{1,2}:\d{2}(?::\d{2})?)/giu, " <duration>")
.replace(/\s*(?:(?:\d+\s*)?\s*(?:\d+\s*)?\s*(?:\d+\s*(?:|))?\s*(?:\d+\s*)?)\s*/giu, " <duration>");
}
function nearCommand(sample, commandTimes, windowMs) {
const ts = Date.parse(sample.ts);
return Number.isFinite(ts) && commandTimes.some((item) => Math.abs(ts - item) <= windowMs);