fix(web-probe): compare visible digests per observer page (#766)
Co-authored-by: Codex <codex@noreply.local>
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user