Merge pull request #1381 from pikasTech/fix/1373-workbench-triad-sentinel

feat: add Workbench triad sentinel check
This commit is contained in:
Lyon
2026-07-01 15:22:51 +08:00
committed by GitHub
5 changed files with 276 additions and 5 deletions
@@ -785,3 +785,11 @@ sentinel:
actionZh: 查看详情后处理。
blocking: true
order: 960
- code: WBC-097
id: workbench-turn-state-triad-inconsistent
level: error
titleZh: Workbench 状态三元组不一致
summaryZh: 会话栏状态、对话卡片状态和最终回复正文没有收敛到同一个运行或完成状态。
actionZh: 查看详情后处理。
blocking: true
order: 970
@@ -1296,6 +1296,7 @@ function displayFindingCode(code) {
const normalized = String(code || "").toLowerCase();
const labels = {
"quick-verify-no-business-turn": "quick verify 未触达业务 turn",
"workbench-turn-state-triad-inconsistent": "Workbench 状态三元组不一致",
"session-rail-title-fallback-root-cause": "INV-02 会话标题 fallback 根因",
"trace-events-page-read-404-root-cause": "INV-07 trace events 404 根因",
"trace-events-page-read-http-error-root-cause": "trace events HTTP 错误根因",
@@ -1315,6 +1316,7 @@ function displayFindingSummary(code, summary) {
const normalized = String(code || "").toLowerCase();
const summaries = {
"quick-verify-no-business-turn": "quick verify 没有形成 sendPrompt、session、trace rows 或 Final Response;不能把公开 dashboard 200 当作 HWLAB 恢复证据。",
"workbench-turn-state-triad-inconsistent": "会话栏状态、turn 卡片状态和 Final Response 正文没有收敛到 completed/completed/present 或 running/running/absent。",
"session-rail-title-fallback-root-cause": "会话列表可见 Session ses_* fallback 标题;根因定位到 session list projection/read model 或 rail 数据绑定缺稳定 title/preview。",
"trace-events-page-read-404-root-cause": "trace events page read 返回 404;根因定位到 /v1/workbench/traces/:traceId/events API 分页/read-model 合约,早于 DOM 渲染。",
"trace-events-page-read-http-error-root-cause": "trace events page read 返回 HTTP 错误;根因定位到 trace-events API 路径,不是通用前端渲染失败。",
@@ -1336,6 +1338,7 @@ function findingNextAction(code) {
const normalized = String(code || "").toLowerCase();
const actions = {
"quick-verify-no-business-turn": "下一步: 打开该 run 的 turn-summary/trace-frame,并用 CLI 对照命令确认没有业务 turn。",
"workbench-turn-state-triad-inconsistent": "下一步: 查看 samples 中的 railStatus/cardStatus/finalResponseTextBytes 和 collectorMissing,再修 Workbench terminal projection seal。",
"session-rail-title-fallback-root-cause": "下一步: 查同 run 的 OTel session_list_read fallbackTitleCount/fallbackTitleRatio,修 session list projection/read model title/preview。",
"trace-events-page-read-404-root-cause": "下一步: 查同 trace 的 OTel trace_events_read sinceSeq/range/hasMore/fullTraceLoaded,修 trace-events 分页合约或 instrumentation。",
"trace-events-page-read-http-error-root-cause": "下一步: 按 traceId/afterProjectedSeq 对齐 OTel trace_events_read 和 HTTP route span。",
@@ -176,6 +176,7 @@ console.log(JSON.stringify({
turnColumns: recentWindow.sampleMetrics.turnColumns.slice(-12).map((item) => ({ label: item.label, source: item.source, pageRole: item.pageRole ?? null, pageId: item.pageId ?? null, pageEpoch: item.pageEpoch ?? null, promptIndex: item.promptIndex, lastPromptIndex: item.lastPromptIndex, firstSeq: item.firstSeq, lastSeq: item.lastSeq, traceId: item.traceId, messageId: item.messageId })),
loading: compactLoadingMetricsForOutput(recentWindow.sampleMetrics.loading),
sessionRailTitles: compactSessionRailTitleMetricsForOutput(recentWindow.sampleMetrics.sessionRailTitles),
workbenchTurnStateTriad: compactWorkbenchTurnStateTriadForOutput(recentWindow.sampleMetrics.workbenchTurnStateTriad),
},
pageProvenance: recentWindow.pageProvenance.summary,
pagePerformance: recentWindow.pagePerformance.summary,
@@ -943,6 +944,11 @@ function compactSessionRail(value) {
testId: item?.testId ?? null,
role: item?.role ?? null,
active: item?.active === true,
status: item?.status ?? null,
dataStatus: item?.dataStatus ?? null,
running: item?.running === true,
dataRunning: item?.dataRunning ?? null,
ariaBusy: item?.ariaBusy ?? null,
sessionIdPrefix: item?.sessionIdPrefix ?? (item?.sessionId ? String(item.sessionId).slice(0, 12) : null),
fallbackTitle: item?.fallbackTitle === true,
titlePreview: limitText(String(item?.titlePreview || item?.titleText || ""), 180),
@@ -964,6 +970,7 @@ function compactSessionRail(value) {
visibleCount: Number.isFinite(visibleCount) ? visibleCount : items.length,
fallbackTitleCount: Number.isFinite(fallbackTitleCount) ? fallbackTitleCount : fallbackItems.length,
fallbackTitleRatio: Number.isFinite(Number(value.fallbackTitleRatio)) ? Number(value.fallbackTitleRatio) : (items.length > 0 ? Number((fallbackItems.length / items.length).toFixed(4)) : 0),
activeItem: items.find((item) => item.active) || null,
items,
fallbackItems,
valuesRedacted: true
@@ -1028,6 +1035,8 @@ function compactDomItem(item) {
if (!item || typeof item !== "object") return item;
const rawText = String(item.text ?? item.textPreview ?? "");
const preview = String(item.textPreview ?? limitText(rawText, 240));
const hasBodyTextPresent = Object.prototype.hasOwnProperty.call(item, "bodyTextPresent");
const hasFinalResponsePresent = Object.prototype.hasOwnProperty.call(item, "finalResponsePresent");
return {
index: item.index ?? null,
tag: item.tag ?? null,
@@ -1047,6 +1056,18 @@ function compactDomItem(item) {
eventKind: item.eventKind ?? null,
durationText: item.durationText ?? null,
activityText: item.activityText ?? null,
bodyTextSource: item.bodyTextSource ?? null,
bodyTextCandidateCount: Number.isFinite(Number(item.bodyTextCandidateCount)) ? Number(item.bodyTextCandidateCount) : null,
bodyTextPresent: hasBodyTextPresent ? item.bodyTextPresent === true : null,
bodyTextPreview: item.bodyTextPreview ? limitText(item.bodyTextPreview, 600) : undefined,
bodyTextHash: item.bodyTextHash ?? null,
bodyTextBytes: Number.isFinite(Number(item.bodyTextBytes)) ? Number(item.bodyTextBytes) : null,
finalResponsePresent: hasFinalResponsePresent ? item.finalResponsePresent === true : null,
finalResponseTextSource: item.finalResponseTextSource ?? null,
finalResponseCandidateCount: Number.isFinite(Number(item.finalResponseCandidateCount)) ? Number(item.finalResponseCandidateCount) : null,
finalResponseTextPreview: item.finalResponseTextPreview ? limitText(item.finalResponseTextPreview, 600) : undefined,
finalResponseTextHash: item.finalResponseTextHash ?? null,
finalResponseTextBytes: Number.isFinite(Number(item.finalResponseTextBytes)) ? Number(item.finalResponseTextBytes) : null,
className: item.className ?? null,
diagnosticCode: item.diagnosticCode ?? null,
source: item.source ?? null,
@@ -1107,6 +1128,17 @@ function compactSessionRailTitleMetricsForOutput(value) {
};
}
function compactWorkbenchTurnStateTriadForOutput(value) {
if (!value || typeof value !== "object") return null;
return {
summary: value.summary ?? null,
invalidFullTriads: Array.isArray(value.invalidFullTriads) ? value.invalidFullTriads.slice(0, 8) : [],
cardFinalResponseMismatches: Array.isArray(value.cardFinalResponseMismatches) ? value.cardFinalResponseMismatches.slice(0, 8) : [],
collectorMissingRows: Array.isArray(value.collectorMissingRows) ? value.collectorMissingRows.slice(0, 8) : [],
valuesRedacted: true
};
}
function compactSamplePageProvenance(value) {
if (!value || typeof value !== "object") return null;
return {
@@ -2807,6 +2839,163 @@ function workbenchDomItemLooksFinal(item) {
return workbenchDomItemIsTerminal(item) && isFinalResultText(text);
}
function buildWorkbenchTurnStateTriadMetrics(samples, timeline = []) {
const rows = [];
const sourceSamples = Array.isArray(samples) ? samples : [];
for (let index = 0; index < sourceSamples.length; index += 1) {
const sample = sourceSamples[index];
if (!isWorkbenchPathSample(sample)) continue;
const turns = Array.isArray(sample?.turns) ? sample.turns.filter((turn) => String(turn?.role || turn?.dataRole || "agent") === "agent") : [];
if (turns.length === 0) continue;
const rail = activeSessionRailItemForSample(sample);
for (const turn of turns.slice(-6)) {
const row = workbenchTurnStateTriadRow(sample, turn, rail, timeline[index]?.promptIndex ?? 0);
if (row) rows.push(row);
}
}
const invalidFullTriads = rows.filter((row) => row.invalid === true && row.fullTriad === true);
const cardFinalResponseMismatches = rows.filter((row) => row.cardFinalResponseMismatch === true || row.legacyCardFinalResponseMismatch === true);
const collectorMissingRows = rows.filter((row) => Array.isArray(row.collectorMissing) && row.collectorMissing.length > 0);
const invalidRowKeys = new Set([...invalidFullTriads, ...cardFinalResponseMismatches].map((row) => row.rowKey));
const collectorMissingFields = uniqueSorted(collectorMissingRows.flatMap((row) => row.collectorMissing || []));
return {
summary: {
sampleCount: sourceSamples.length,
rowCount: rows.length,
fullTriadRowCount: rows.filter((row) => row.fullTriad === true).length,
invalidRowCount: invalidRowKeys.size,
invalidFullTriadCount: invalidFullTriads.length,
cardFinalResponseMismatchCount: cardFinalResponseMismatches.length,
collectorMissingRowCount: collectorMissingRows.length,
collectorMissingFields,
valuesRedacted: true
},
invalidFullTriads: invalidFullTriads.slice(0, 120),
cardFinalResponseMismatches: cardFinalResponseMismatches.slice(0, 120),
collectorMissingRows: collectorMissingRows.slice(0, 120),
timeline: rows.slice(-300),
valuesRedacted: true
};
}
function workbenchTurnStateTriadRow(sample, turn, rail, promptIndex) {
const collectorMissing = [];
const railRawStatus = firstString(rail?.status, rail?.dataStatus);
const railRunning = rail?.running === true || String(rail?.dataRunning || rail?.ariaBusy || "").toLowerCase() === "true";
const railStatus = normalizeWorkbenchTriadStatus(railRawStatus, railRunning);
if (!rail) collectorMissing.push("sessionRail.activeItem");
if (!railRawStatus && railRunning !== true) collectorMissing.push("sessionRail.status");
const cardRawStatus = firstString(turn?.status);
const cardStatus = normalizeWorkbenchTriadStatus(cardRawStatus, false) || (workbenchDomItemIsTerminal(turn) ? "completed" : null);
if (!cardRawStatus && !cardStatus) collectorMissing.push("turn.status");
const finalPresence = finalResponsePresenceFromTurn(turn);
if (finalPresence.known !== true) collectorMissing.push("turn.finalResponseTextBytes");
const fullTriad = Boolean(railStatus && cardStatus && finalPresence.known === true);
const finalResponsePresent = finalPresence.known === true ? finalPresence.present : null;
const tupleAllowed = fullTriad
? (railStatus === "completed" && cardStatus === "completed" && finalResponsePresent === true)
|| (railStatus === "running" && cardStatus === "running" && finalResponsePresent === false)
: null;
const cardFinalResponseMismatch = cardStatus === "completed" && finalPresence.known === true && finalResponsePresent !== true;
const legacyCardFinalResponseMismatch = cardStatus === "completed" && finalPresence.known !== true && !workbenchDomItemLooksFinal(turn);
const railCardMismatch = Boolean(railStatus && cardStatus && railStatus !== cardStatus);
const invalid = tupleAllowed === false || cardFinalResponseMismatch || legacyCardFinalResponseMismatch || railCardMismatch;
const traceId = firstString(turn?.traceId, turn?.turnId);
const messageId = firstString(turn?.messageId);
const rowKey = [
sample?.seq ?? "-",
sample?.pageRole ?? "control",
sample?.pageId ?? "default",
sample?.routeSessionId ?? sample?.activeSessionId ?? rail?.sessionIdPrefix ?? "-",
traceId ?? messageId ?? turn?.index ?? "-"
].join("|");
return {
...ref(sample),
rowKey,
promptIndex,
sessionIdPrefix: sessionPrefixForSample(sample, rail),
traceId,
messageId,
turnId: firstString(turn?.turnId),
turnIndex: turn?.index ?? null,
railStatus,
railStatusRaw: railRawStatus ?? null,
railRunning: railRunning === true,
railSource: rail ? "sessionRail.activeItem" : null,
cardStatus,
cardStatusRaw: cardRawStatus ?? null,
finalResponsePresent,
finalResponseTextBytes: turn?.finalResponseTextBytes !== null && turn?.finalResponseTextBytes !== undefined && Number.isFinite(Number(turn.finalResponseTextBytes)) ? Number(turn.finalResponseTextBytes) : null,
finalResponseSource: turn?.finalResponseTextSource ?? null,
finalResponseTextHash: turn?.finalResponseTextHash ?? null,
finalResponseTextPreview: turn?.finalResponseTextPreview ? limitText(turn.finalResponseTextPreview, 160) : null,
fullTriad,
tupleAllowed,
invalid,
cardFinalResponseMismatch,
legacyCardFinalResponseMismatch,
railCardMismatch,
collectorMissing,
nearestCommandId: sample?.commandId ?? null,
relatedChecks: ["WBC-001", "WBC-003", "WBC-011", "WBC-022"],
valuesRedacted: true
};
}
function activeSessionRailItemForSample(sample) {
const rail = sample?.sessionRail && typeof sample.sessionRail === "object" ? sample.sessionRail : null;
if (!rail) return null;
const items = Array.isArray(rail.items) ? rail.items : [];
if (rail.activeItem) return rail.activeItem;
const routeSessionId = String(sample?.routeSessionId || "");
const activeSessionId = String(sample?.activeSessionId || "");
return items.find((item) => item?.active === true)
|| items.find((item) => sessionRailItemMatchesSession(item, activeSessionId))
|| items.find((item) => sessionRailItemMatchesSession(item, routeSessionId))
|| null;
}
function sessionRailItemMatchesSession(item, sessionId) {
const id = String(sessionId || "");
const prefix = String(item?.sessionIdPrefix || item?.sessionId || "");
return Boolean(id && prefix && (id === prefix || id.startsWith(prefix) || prefix.startsWith(id)));
}
function sessionPrefixForSample(sample, rail) {
const id = firstString(sample?.activeSessionId, sample?.routeSessionId, rail?.sessionIdPrefix, rail?.sessionId);
return id ? String(id).slice(0, 12) : null;
}
function finalResponsePresenceFromTurn(turn) {
if (!turn || typeof turn !== "object") return { known: false, present: null };
if (turn.finalResponseTextBytes !== null && turn.finalResponseTextBytes !== undefined && Number.isFinite(Number(turn.finalResponseTextBytes))) {
const bytes = Number(turn.finalResponseTextBytes);
return { known: true, present: bytes > 0 };
}
if (Object.prototype.hasOwnProperty.call(turn, "finalResponsePresent") && turn.finalResponsePresent !== null && turn.finalResponsePresent !== undefined) {
return { known: true, present: turn.finalResponsePresent === true };
}
return { known: false, present: null };
}
function normalizeWorkbenchTriadStatus(status, running = false) {
const value = String(status || "").trim().toLowerCase().replace(/_/gu, "-");
if (running === true) return "running";
if (!value) return null;
if (/^(completed|complete|succeeded|success|finished|done|terminal|sealed)$/u.test(value)) return "completed";
if (/^(pending|running|active|busy|admitted|dispatching|executing|streaming|processing|queued|in-progress|creating)$/u.test(value)) return "running";
return null;
}
function firstString(...values) {
for (const value of values) {
if (typeof value !== "string") continue;
const text = value.trim();
if (text) return text;
}
return null;
}
function promptCommandHasAuthoritativeSubmitSideEffect(control, promptRound) {
const commandId = stringOrNull(promptRound?.promptCommandId);
if (!commandId) return false;
@@ -2958,6 +3147,33 @@ function buildFindings(samples, control, network, errors, sampleMetrics, promptN
rootCauseConfidence: "high",
valuesRedacted: true
});
const turnStateTriad = sampleMetrics?.workbenchTurnStateTriad || {};
const turnStateTriadSummary = turnStateTriad.summary || {};
const turnStateTriadRows = [
...(Array.isArray(turnStateTriad.invalidFullTriads) ? turnStateTriad.invalidFullTriads : []),
...(Array.isArray(turnStateTriad.cardFinalResponseMismatches) ? turnStateTriad.cardFinalResponseMismatches : [])
];
if (Number(turnStateTriadSummary.invalidRowCount ?? 0) > 0) findings.push({
id: "workbench-turn-state-triad-inconsistent",
severity: "red",
summary: "Workbench session rail status, turn card status, and Final Response body presence diverged from the allowed running/running/absent or completed/completed/present tuples",
count: turnStateTriadSummary.invalidRowCount,
fullTriadCount: turnStateTriadSummary.fullTriadRowCount,
invalidFullTriadCount: turnStateTriadSummary.invalidFullTriadCount,
cardFinalResponseMismatchCount: turnStateTriadSummary.cardFinalResponseMismatchCount,
legacyCollectorMissingCount: turnStateTriadSummary.collectorMissingRowCount,
collectorMissingFields: Array.isArray(turnStateTriadSummary.collectorMissingFields) ? turnStateTriadSummary.collectorMissingFields : [],
allowedTuples: [
{ railStatus: "completed", cardStatus: "completed", finalResponsePresent: true },
{ railStatus: "running", cardStatus: "running", finalResponsePresent: false }
],
samples: turnStateTriadRows.slice(0, 20),
collectorMissingSamples: Array.isArray(turnStateTriad.collectorMissingRows) ? turnStateTriad.collectorMissingRows.slice(0, 10) : [],
rootCause: "workbench_projection_state_triad_not_sealed",
rootCauseStatus: "confirmed-from-dom-samples",
rootCauseConfidence: "high",
valuesRedacted: true
});
const promptFailures = Array.isArray(promptNetwork?.rounds) ? promptNetwork.rounds.filter((item) => item.chatPostOk === false && !promptCommandHasAuthoritativeSubmitSideEffect(control, item)) : [];
if (promptFailures.length > 0) findings.push({ id: "prompt-chat-submit-failed", severity: "red", summary: "sendPrompt command had no successful /v1/agent/chat or /v1/agent/chat/steer POST response in the sampling window", count: promptFailures.length, rounds: promptFailures.slice(0, 10) });
const promptSteerRounds = Array.isArray(promptNetwork?.rounds) ? promptNetwork.rounds.filter((item) => item.steerUsed === true) : [];
@@ -4875,6 +5091,7 @@ function prioritizeFindings(findings) {
if (id.startsWith("project-management-") || id.startsWith("mdtodo-") || id === "workbench-launch-button-unavailable") return 0;
if (id === "page-performance-slow-same-origin-api") return 0;
if (id === "session-rail-title-fallback-majority") return 0.5;
if (id === "workbench-turn-state-triad-inconsistent") return 0.55;
if (id.startsWith("workbench-terminal-")) return 0.6;
if (id.startsWith("code-agent-card-")) return 0.8;
if (id.startsWith("round-completion-")) return 0.9;
@@ -4975,6 +5192,7 @@ function buildSampleMetrics(samples, control) {
const diagnostics = timeline.filter((item) => item.diagnosticSeen).length;
const loading = buildLoadingMetrics(samples, timeline);
const sessionRailTitles = buildSessionRailTitleMetrics(samples, timeline);
const workbenchTurnStateTriad = buildWorkbenchTurnStateTriadMetrics(samples, timeline);
const reportTurnTimingRows = boundedTurnTimingRowsForReport(turnTiming.rows);
const reportTimeline = boundedRowsForReport(timeline);
const rounds = buildRoundMetricSummaries(timeline, promptCommands, {
@@ -5009,6 +5227,11 @@ function buildSampleMetrics(samples, control) {
sessionRailFallbackMaxRatio: sessionRailTitles.summary.maxFallbackRatio,
sessionRailFallbackMaxVisibleCount: sessionRailTitles.summary.maxVisibleCount,
sessionRailFallbackMaxCount: sessionRailTitles.summary.maxFallbackTitleCount,
workbenchTurnStateTriadRows: workbenchTurnStateTriad.summary.rowCount,
workbenchTurnStateTriadInvalidRows: workbenchTurnStateTriad.summary.invalidRowCount,
workbenchTurnStateTriadFullInvalidRows: workbenchTurnStateTriad.summary.invalidFullTriadCount,
workbenchTurnStateCardFinalResponseMismatchRows: workbenchTurnStateTriad.summary.cardFinalResponseMismatchCount,
workbenchTurnStateCollectorMissingRows: workbenchTurnStateTriad.summary.collectorMissingRowCount,
promptSegments: Math.max(0, promptTimes.length),
rounds: rounds.length,
turnColumns: turnTiming.columns.length,
@@ -5048,6 +5271,7 @@ function buildSampleMetrics(samples, control) {
},
loading,
sessionRailTitles,
workbenchTurnStateTriad,
codeAgentCardTiming,
traceOrder,
rounds,
@@ -4580,12 +4580,20 @@ async function sampleOnePage(targetPage, { reason, groupSeq, pageRole, targetPag
seen.add(key);
const rect = element.getBoundingClientRect();
const fallbackTitle = looksLikeSessionTitleFallback(titleText, sessionId);
const dataStatus = element.getAttribute("data-status") || element.getAttribute("data-state") || null;
const dataRunning = element.getAttribute("data-running") || element.getAttribute("data-busy") || null;
const ariaBusy = element.getAttribute("aria-busy") || null;
items.push({
index: items.length,
tag: element.tagName.toLowerCase(),
testId: element.getAttribute("data-testid"),
role: element.getAttribute("role"),
active: element.getAttribute("data-active") === "true" || element.getAttribute("aria-selected") === "true",
status: dataStatus || ariaBusy || null,
dataStatus,
running: dataRunning === "true" || ariaBusy === "true" || element.classList.contains("is-running") || element.classList.contains("running"),
dataRunning,
ariaBusy,
sessionId,
sessionIdPrefix: sessionId ? String(sessionId).slice(0, 12) : null,
titleText,
@@ -4601,6 +4609,7 @@ async function sampleOnePage(targetPage, { reason, groupSeq, pageRole, targetPag
visibleCount,
fallbackTitleCount,
fallbackTitleRatio: visibleCount > 0 ? Number((fallbackTitleCount / visibleCount).toFixed(4)) : 0,
activeItem: visibleItems.find((item) => item.active) || null,
items: visibleItems.slice(0, 60),
fallbackItems: fallbackItems.slice(0, 12),
};
@@ -4637,7 +4646,7 @@ async function sampleOnePage(targetPage, { reason, groupSeq, pageRole, targetPag
const title = element.getAttribute("title") || "";
return tag === "button" || role === "button" || //u.test(aria) || //u.test(title);
};
const stableMessageText = (element) => {
const messageBodyTextDetail = (element) => {
const bodySelectors = [
".message-markdown.message-text",
".message-text",
@@ -4651,10 +4660,21 @@ async function sampleOnePage(targetPage, { reason, groupSeq, pageRole, targetPag
for (const candidate of Array.from(element.querySelectorAll(selector))) {
if (!visible(candidate)) continue;
const text = trim(candidate.textContent || "", 1200);
if (text && !parts.includes(text)) parts.push(text);
if (text && !parts.some((part) => part.text === text)) parts.push({ text, selector });
}
}
if (parts.length > 0) return parts.join(" ");
if (parts.length > 0) {
return {
text: parts.map((part) => part.text).join(" "),
source: parts[0]?.selector || "body-selector",
candidateCount: parts.length
};
}
return { text: "", source: null, candidateCount: 0 };
};
const stableMessageText = (element) => {
const body = messageBodyTextDetail(element);
if (body.text) return body.text;
const clone = element.cloneNode(true);
for (const selector of [
".message-duration-meta",
@@ -4715,6 +4735,7 @@ async function sampleOnePage(targetPage, { reason, groupSeq, pageRole, targetPag
const summarize = (selector, limit) => summarizeElements(Array.from(document.querySelectorAll(selector)), limit);
const summarizeMessages = (selector, limit) => Array.from(document.querySelectorAll(selector)).filter(visible).slice(-limit).map((element, index) => {
const rect = element.getBoundingClientRect();
const body = messageBodyTextDetail(element);
return {
index,
tag: element.tagName.toLowerCase(),
@@ -4728,6 +4749,9 @@ async function sampleOnePage(targetPage, { reason, groupSeq, pageRole, targetPag
turnId: element.getAttribute("data-turn-id") || null,
durationText: trim(element.querySelector(".message-duration-meta")?.textContent || "", 120),
activityText: trim(element.querySelector(".message-activity-meta")?.textContent || "", 120),
bodyText: body.text,
bodyTextSource: body.source,
bodyTextCandidateCount: body.candidateCount,
text: stableMessageText(element),
rect: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) },
};
@@ -4914,6 +4938,7 @@ async function sampleOnePage(targetPage, { reason, groupSeq, pageRole, targetPag
const turns = Array.from(document.querySelectorAll('article.message-card[data-role="agent"], .message-card[data-role="agent"], article[data-role="agent"]')).filter(visible).map((element, index) => {
const rect = element.getBoundingClientRect();
const text = textHashInput(element);
const finalResponse = messageBodyTextDetail(element);
const traceElement = element.matches("[data-trace-id]") ? element : element.querySelector("[data-trace-id]");
const traceMatch = text.match(/\btrc_[A-Za-z0-9_-]+\b/u);
const durationText = trim(element.querySelector(".message-duration-meta")?.textContent || "", 120);
@@ -4929,6 +4954,10 @@ async function sampleOnePage(targetPage, { reason, groupSeq, pageRole, targetPag
turnId: element.getAttribute("data-turn-id") || directTraceId,
durationText,
activityText,
finalResponseText: finalResponse.text,
finalResponseTextSource: finalResponse.source,
finalResponseCandidateCount: finalResponse.candidateCount,
finalResponsePresent: Boolean(finalResponse.text && finalResponse.text.trim()),
text,
rect: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) },
};
@@ -5014,12 +5043,12 @@ async function sampleOnePage(targetPage, { reason, groupSeq, pageRole, targetPag
function digestDom(dom, pageRole = "control") {
if (dom && dom.error) return dom;
const messages = Array.isArray(dom.messages) ? dom.messages.map((item) => ({ ...item, textHash: sha256Text(item.text || ""), textPreview: truncate(item.text || "", 160), textBytes: Buffer.byteLength(item.text || "") })) : [];
const messages = Array.isArray(dom.messages) ? dom.messages.map((item) => ({ ...item, textHash: sha256Text(item.text || ""), textPreview: truncate(item.text || "", 160), textBytes: Buffer.byteLength(item.text || ""), bodyTextHash: sha256Text(item.bodyText || ""), bodyTextPreview: truncate(item.bodyText || "", 160), bodyTextBytes: Buffer.byteLength(item.bodyText || ""), bodyTextPresent: Boolean(String(item.bodyText || "").trim()) })) : [];
const traceRows = Array.isArray(dom.traceRows) ? dom.traceRows.map((item) => ({ ...item, textHash: sha256Text(item.text || ""), textPreview: truncate(item.text || "", 160), textBytes: Buffer.byteLength(item.text || "") })) : [];
const loadings = Array.isArray(dom.loadings) ? dom.loadings.map((item) => ({ ...item, textHash: sha256Text(item.text || ""), textPreview: truncate(item.text || "", 160), textBytes: Buffer.byteLength(item.text || ""), ownerTextHash: sha256Text(item.ownerText || ""), ownerTextPreview: truncate(item.ownerText || "", 160) })) : [];
const sessionRail = digestSessionRail(dom.sessionRail);
const diagnostics = Array.isArray(dom.diagnostics) ? dom.diagnostics.map((item) => ({ ...item, textHash: sha256Text(item.text || ""), textPreview: truncate(item.text || "", 260), textBytes: Buffer.byteLength(item.text || "") })) : [];
const turns = Array.isArray(dom.turns) ? dom.turns.map((item) => ({ ...item, textHash: sha256Text(item.text || ""), textPreview: truncate(item.text || "", 200), textBytes: Buffer.byteLength(item.text || "") })) : [];
const turns = Array.isArray(dom.turns) ? dom.turns.map((item) => ({ ...item, textHash: sha256Text(item.text || ""), textPreview: truncate(item.text || "", 200), textBytes: Buffer.byteLength(item.text || ""), finalResponseTextHash: sha256Text(item.finalResponseText || ""), finalResponseTextPreview: truncate(item.finalResponseText || "", 200), finalResponseTextBytes: Buffer.byteLength(item.finalResponseText || ""), finalResponsePresent: Boolean(String(item.finalResponseText || "").trim()) })) : [];
const projectManagementSample = digestProjectManagement(dom.projectManagement);
const pageProvenance = normalizePageProvenance(dom.pageProvenance, { reason: "sample", pageLoadSeq: currentPageProvenance?.pageLoadSeq ?? pageLoadSeq });
if (pageRole === "control") currentPageProvenance = pageProvenance;
@@ -5112,6 +5141,11 @@ function digestSessionRail(value) {
testId: item?.testId ?? null,
role: item?.role ?? null,
active: item?.active === true,
status: item?.status ?? null,
dataStatus: item?.dataStatus ?? null,
running: item?.running === true,
dataRunning: item?.dataRunning ?? null,
ariaBusy: item?.ariaBusy ?? null,
sessionId: item?.sessionId ?? null,
sessionIdPrefix: item?.sessionIdPrefix ?? (item?.sessionId ? String(item.sessionId).slice(0, 12) : null),
fallbackTitle: item?.fallbackTitle === true,
@@ -5128,6 +5162,7 @@ function digestSessionRail(value) {
visibleCount: Number.isFinite(visibleCount) ? visibleCount : items.length,
fallbackTitleCount: Number.isFinite(fallbackTitleCount) ? fallbackTitleCount : fallbackItems.length,
fallbackTitleRatio: Number.isFinite(Number(value.fallbackTitleRatio)) ? Number(value.fallbackTitleRatio) : (items.length > 0 ? Number((fallbackItems.length / items.length).toFixed(4)) : 0),
activeItem: items.find((item) => item.active) || null,
items,
fallbackItems,
valuesRedacted: true,
@@ -1515,6 +1515,7 @@ function isQuickVerifyBlockingFinding(item: Record<string, unknown>): boolean {
"quick-verify-observer-start-failed",
"quick-verify-account-secret-missing",
"prompt-chat-submit-failed",
"workbench-turn-state-triad-inconsistent",
"route-active-session-mismatch",
"final-response-flicker",
"round-completion-final-response-missing",