2540 lines
151 KiB
TypeScript
2540 lines
151 KiB
TypeScript
// SPEC: PJ2026-01040111 long-running Workbench observation.
|
||
// Responsibility: Timing, trace-order, Code Agent card, and report-rendering source for the offline web-probe observe analyzer.
|
||
export function nodeWebObserveAnalyzerTimingSource(): string {
|
||
return String.raw`function buildTurnTimingTable(samples, timeline) {
|
||
const columns = [];
|
||
const registry = new Map();
|
||
const promptAssignmentByKey = new Map();
|
||
const rows = [];
|
||
for (let index = 0; index < samples.length; index += 1) {
|
||
const sample = samples[index];
|
||
const timelineItem = timeline[index] || {};
|
||
const cells = {};
|
||
const rawMetrics = turnMetricItems(sample, timelineItem);
|
||
const domIndexes = rawMetrics.map((item) => Number(item.domIndex)).filter(Number.isFinite);
|
||
const maxDomIndex = domIndexes.length > 0 ? Math.max(...domIndexes) : null;
|
||
for (const rawMetric of rawMetrics) {
|
||
const scopedKey = turnTimingScopedMetricKey(rawMetric, sample);
|
||
const samplePromptIndex = Number(timelineItem.promptIndex ?? 0);
|
||
const evidencePromptIndex = inferTurnMetricPromptIndex(rawMetric, samplePromptIndex, maxDomIndex);
|
||
if (evidencePromptIndex !== null) {
|
||
const existingPromptIndex = promptAssignmentByKey.get(scopedKey);
|
||
if (existingPromptIndex === undefined || evidencePromptIndex > existingPromptIndex) promptAssignmentByKey.set(scopedKey, evidencePromptIndex);
|
||
}
|
||
const assignedPromptIndex = promptAssignmentByKey.get(scopedKey) ?? null;
|
||
const metric = { ...rawMetric, key: scopedKey, baseKey: rawMetric.key, promptIndex: assignedPromptIndex, samplePromptIndex, pageEpoch: Number(sample?.pageEpoch ?? rawMetric.pageEpoch ?? 0) || 0 };
|
||
let column = registry.get(metric.key);
|
||
if (!column) {
|
||
column = {
|
||
id: "T" + String(columns.length + 1),
|
||
label: "T" + String(columns.length + 1),
|
||
keyHash: sha256(metric.key),
|
||
source: metric.source,
|
||
firstSeq: sample.seq ?? null,
|
||
firstTs: sample.ts ?? null,
|
||
lastSeq: sample.seq ?? null,
|
||
lastTs: sample.ts ?? null,
|
||
promptIndex: metric.promptIndex ?? null,
|
||
lastPromptIndex: metric.promptIndex ?? null,
|
||
traceId: metric.traceId ?? null,
|
||
messageId: metric.messageId ?? null,
|
||
domIndex: metric.domIndex ?? null,
|
||
pageRole: metric.pageRole ?? sample.pageRole ?? null,
|
||
pageId: metric.pageId ?? sample.pageId ?? null,
|
||
pageEpoch: metric.pageEpoch ?? null
|
||
};
|
||
registry.set(metric.key, column);
|
||
columns.push(column);
|
||
} else {
|
||
column.lastSeq = sample.seq ?? null;
|
||
column.lastTs = sample.ts ?? null;
|
||
if (column.source !== "turn" && metric.source === "turn") column.source = "turn";
|
||
if (metric.promptIndex && column.promptIndex !== metric.promptIndex) column.promptIndex = metric.promptIndex;
|
||
column.lastPromptIndex = metric.promptIndex ?? column.lastPromptIndex ?? null;
|
||
if (!column.traceId && metric.traceId) column.traceId = metric.traceId;
|
||
if (!column.messageId && metric.messageId) column.messageId = metric.messageId;
|
||
}
|
||
cells[column.id] = {
|
||
totalElapsedSeconds: metric.totalElapsedSeconds,
|
||
recentUpdateSeconds: metric.recentUpdateSeconds,
|
||
status: metric.status ?? null,
|
||
promptIndex: metric.promptIndex ?? null,
|
||
source: metric.source,
|
||
pageRole: metric.pageRole ?? sample.pageRole ?? null,
|
||
pageId: metric.pageId ?? sample.pageId ?? null,
|
||
pageEpoch: metric.pageEpoch ?? null,
|
||
sampleGroupSeq: sample.sampleGroupSeq ?? null,
|
||
traceId: metric.traceId ?? null,
|
||
messageId: metric.messageId ?? null,
|
||
textHash: metric.textHash ?? null,
|
||
samplePromptIndex: metric.samplePromptIndex ?? null
|
||
};
|
||
}
|
||
rows.push({
|
||
ts: sample.ts ?? null,
|
||
seq: sample.seq ?? null,
|
||
sampleGroupSeq: sample.sampleGroupSeq ?? null,
|
||
pageRole: sample.pageRole ?? null,
|
||
pageId: sample.pageId ?? null,
|
||
pageEpoch: Number(sample?.pageEpoch ?? 0) || 0,
|
||
promptIndex: timelineItem.promptIndex ?? 0,
|
||
routeSessionId: sample.routeSessionId ?? null,
|
||
activeSessionId: sample.activeSessionId ?? null,
|
||
cells
|
||
});
|
||
}
|
||
const timingEvents = detectTurnTimingNonMonotonic(columns, rows);
|
||
return {
|
||
columns,
|
||
rows,
|
||
nonMonotonic: timingEvents.anomalies,
|
||
elapsedZeroResets: timingEvents.elapsedZeroResets,
|
||
totalElapsedForwardJumps: timingEvents.totalElapsedForwardJumps,
|
||
terminalElapsedGrowth: timingEvents.terminalElapsedGrowth,
|
||
terminalElapsedCorrections: timingEvents.terminalElapsedCorrections,
|
||
recentUpdateResets: timingEvents.recentUpdateResets,
|
||
recentUpdateSteps: timingEvents.recentUpdateSteps
|
||
};
|
||
}
|
||
|
||
function turnTimingScopedMetricKey(metric, sample) {
|
||
return [
|
||
metric?.key ?? "unknown",
|
||
metric?.pageRole ?? sample?.pageRole ?? "unknown-role",
|
||
metric?.pageId ?? sample?.pageId ?? "unknown-page",
|
||
Number(sample?.pageEpoch ?? metric?.pageEpoch ?? 0) || 0
|
||
].join("|page:");
|
||
}
|
||
|
||
function inferTurnMetricPromptIndex(metric, samplePromptIndex, maxDomIndex) {
|
||
const promptIndex = Number(samplePromptIndex);
|
||
if (!Number.isFinite(promptIndex) || promptIndex <= 0) return null;
|
||
if (metric?.source === "aggregate") return promptIndex;
|
||
if (isActiveTurnStatus(metric?.status)) return promptIndex;
|
||
const domIndex = Number(metric?.domIndex);
|
||
if (!isTerminalTurnStatus(metric?.status) && Number.isFinite(domIndex) && Number.isFinite(maxDomIndex) && domIndex === maxDomIndex && metric?.recentUpdateSeconds !== null && metric?.recentUpdateSeconds !== undefined) return promptIndex;
|
||
return null;
|
||
}
|
||
|
||
function detectTurnTimingNonMonotonic(columns, rows) {
|
||
const anomalies = [];
|
||
const elapsedZeroResets = [];
|
||
const totalElapsedForwardJumps = [];
|
||
const terminalElapsedGrowth = [];
|
||
const terminalElapsedCorrections = [];
|
||
const recentUpdateResets = [];
|
||
const recentUpdateSteps = [];
|
||
for (const column of columns) {
|
||
const previousByMetric = new Map();
|
||
let previousTerminalTotal = null;
|
||
for (const row of rows) {
|
||
const cell = row.cells?.[column.id];
|
||
if (!cell) continue;
|
||
for (const metric of ["totalElapsedSeconds", "recentUpdateSeconds"]) {
|
||
const value = cell[metric];
|
||
if (value === null || value === undefined || !Number.isFinite(Number(value))) continue;
|
||
const current = Number(value);
|
||
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,
|
||
...timingFindingMeta("dom-card-total-elapsed-sequence", timingStatusFromTurnStatus(cell.status ?? previous.status), {
|
||
cardElapsedSource: cell.source ?? column.source ?? "dom-card",
|
||
}),
|
||
metric,
|
||
anomaly,
|
||
expectedPattern: anomaly === "zero-reset" ? "total-elapsed-should-not-return-to-zero" : "total-elapsed-monotonic",
|
||
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,
|
||
promptIndex: cell.promptIndex ?? null,
|
||
samplePromptIndex: row.promptIndex ?? null,
|
||
source: cell.source ?? column.source ?? null,
|
||
pageRole: cell.pageRole ?? column.pageRole ?? null,
|
||
pageId: cell.pageId ?? column.pageId ?? null,
|
||
pageEpoch: cell.pageEpoch ?? row.pageEpoch ?? column.pageEpoch ?? null,
|
||
valuesRedacted: true
|
||
};
|
||
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);
|
||
const delta = current - previous.value;
|
||
const allowedIncreaseSeconds = sampleDeltaSeconds + alertThresholds.turnTimingSampleSlackSeconds;
|
||
const terminalTransition = !isTerminalTurnStatus(previous.status) && isTerminalTurnStatus(cell.status);
|
||
if (delta > allowedIncreaseSeconds && !terminalTransition) {
|
||
totalElapsedForwardJumps.push({
|
||
columnId: column.id,
|
||
columnLabel: column.label,
|
||
...timingFindingMeta("dom-card-total-elapsed-sequence", timingStatusFromTurnStatus(cell.status ?? previous.status), {
|
||
cardElapsedSource: cell.source ?? column.source ?? "dom-card",
|
||
}),
|
||
metric,
|
||
anomaly: "forward-jump",
|
||
expectedPattern: "total-elapsed-increase-should-match-browser-sample-interval",
|
||
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,
|
||
sampleDeltaSeconds,
|
||
allowedIncreaseSeconds,
|
||
excessiveIncreaseSeconds: Number((delta - allowedIncreaseSeconds).toFixed(3)),
|
||
traceId: cell.traceId ?? column.traceId ?? null,
|
||
messageId: cell.messageId ?? column.messageId ?? null,
|
||
promptIndex: cell.promptIndex ?? null,
|
||
samplePromptIndex: row.promptIndex ?? null,
|
||
source: cell.source ?? column.source ?? null,
|
||
pageRole: cell.pageRole ?? column.pageRole ?? null,
|
||
pageId: cell.pageId ?? column.pageId ?? null,
|
||
pageEpoch: cell.pageEpoch ?? row.pageEpoch ?? column.pageEpoch ?? null,
|
||
valuesRedacted: true
|
||
});
|
||
}
|
||
}
|
||
if (metric === "totalElapsedSeconds" && isTerminalTurnStatus(cell.status)) {
|
||
if (previousTerminalTotal && current > previousTerminalTotal.value) {
|
||
terminalElapsedGrowth.push({
|
||
columnId: column.id,
|
||
columnLabel: column.label,
|
||
...timingFindingMeta("terminal-card-total-elapsed-seal", timingStatusFromTurnStatus(cell.status, "business-turn-completed"), {
|
||
cardElapsedSource: cell.source ?? column.source ?? "dom-card",
|
||
}),
|
||
metric,
|
||
anomaly: "terminal-growth",
|
||
expectedPattern: "terminal-total-elapsed-sealed",
|
||
fromSeq: previousTerminalTotal.seq,
|
||
fromTs: previousTerminalTotal.ts,
|
||
fromValue: previousTerminalTotal.value,
|
||
fromStatus: previousTerminalTotal.status,
|
||
toSeq: row.seq ?? null,
|
||
toTs: row.ts ?? null,
|
||
toValue: current,
|
||
toStatus: cell.status ?? null,
|
||
delta: current - previousTerminalTotal.value,
|
||
traceId: cell.traceId ?? column.traceId ?? null,
|
||
messageId: cell.messageId ?? column.messageId ?? null,
|
||
promptIndex: cell.promptIndex ?? null,
|
||
samplePromptIndex: row.promptIndex ?? null,
|
||
source: cell.source ?? column.source ?? null,
|
||
pageRole: cell.pageRole ?? column.pageRole ?? null,
|
||
pageId: cell.pageId ?? column.pageId ?? null,
|
||
pageEpoch: cell.pageEpoch ?? row.pageEpoch ?? column.pageEpoch ?? null,
|
||
valuesRedacted: true
|
||
});
|
||
}
|
||
previousTerminalTotal = { value: current, seq: row.seq ?? null, ts: row.ts ?? null, status: cell.status ?? null };
|
||
}
|
||
if (previous && metric === "recentUpdateSeconds") {
|
||
const elapsedMs = Date.parse(String(row.ts ?? "")) - Date.parse(String(previous.ts ?? ""));
|
||
const elapsedSeconds = Number.isFinite(elapsedMs) && elapsedMs >= 0 ? elapsedMs / 1000 : null;
|
||
const increase = current - previous.value;
|
||
const allowedIncrease = elapsedSeconds === null
|
||
? alertThresholds.turnTimingSampleSlackSeconds
|
||
: Math.max(alertThresholds.turnTimingSampleSlackSeconds, elapsedSeconds + alertThresholds.turnTimingSampleSlackSeconds);
|
||
const excessiveIncrease = increase > allowedIncrease ? increase - allowedIncrease : 0;
|
||
recentUpdateSteps.push({
|
||
columnId: column.id,
|
||
columnLabel: column.label,
|
||
metric: "recentUpdateSeconds",
|
||
event: increase < 0 ? "reset" : excessiveIncrease > 0 ? "jump" : "increase",
|
||
expectedPattern: "sawtooth-increase-or-reset",
|
||
fromSeq: previous.seq,
|
||
fromTs: previous.ts,
|
||
fromValue: previous.value,
|
||
toSeq: row.seq ?? null,
|
||
toTs: row.ts ?? null,
|
||
toValue: current,
|
||
delta: increase,
|
||
sampleDeltaSeconds: elapsedSeconds,
|
||
allowedIncreaseSeconds: allowedIncrease,
|
||
excessiveIncreaseSeconds: excessiveIncrease,
|
||
traceId: cell.traceId ?? column.traceId ?? null,
|
||
messageId: cell.messageId ?? column.messageId ?? null,
|
||
promptIndex: cell.promptIndex ?? null,
|
||
samplePromptIndex: row.promptIndex ?? null,
|
||
source: cell.source ?? column.source ?? null,
|
||
pageRole: cell.pageRole ?? column.pageRole ?? null,
|
||
pageId: cell.pageId ?? column.pageId ?? null,
|
||
pageEpoch: cell.pageEpoch ?? row.pageEpoch ?? column.pageEpoch ?? null,
|
||
valuesRedacted: true
|
||
});
|
||
if (increase < 0) {
|
||
recentUpdateResets.push({
|
||
columnId: column.id,
|
||
columnLabel: column.label,
|
||
metric: "recentUpdateSeconds",
|
||
event: "reset",
|
||
fromSeq: previous.seq,
|
||
fromTs: previous.ts,
|
||
fromValue: previous.value,
|
||
toSeq: row.seq ?? null,
|
||
toTs: row.ts ?? null,
|
||
toValue: current,
|
||
delta: increase,
|
||
sampleDeltaSeconds: elapsedSeconds,
|
||
traceId: cell.traceId ?? column.traceId ?? null,
|
||
messageId: cell.messageId ?? column.messageId ?? null,
|
||
promptIndex: cell.promptIndex ?? null,
|
||
samplePromptIndex: row.promptIndex ?? null,
|
||
source: cell.source ?? column.source ?? null,
|
||
pageRole: cell.pageRole ?? column.pageRole ?? null,
|
||
pageId: cell.pageId ?? column.pageId ?? null,
|
||
pageEpoch: cell.pageEpoch ?? row.pageEpoch ?? column.pageEpoch ?? null,
|
||
valuesRedacted: true
|
||
});
|
||
}
|
||
if (excessiveIncrease > 0) {
|
||
anomalies.push({
|
||
columnId: column.id,
|
||
columnLabel: column.label,
|
||
metric: "recentUpdateSeconds",
|
||
anomaly: "jump",
|
||
expectedPattern: "sawtooth-increase-or-reset",
|
||
fromSeq: previous.seq,
|
||
fromTs: previous.ts,
|
||
fromValue: previous.value,
|
||
toSeq: row.seq ?? null,
|
||
toTs: row.ts ?? null,
|
||
toValue: current,
|
||
delta: increase,
|
||
sampleDeltaSeconds: elapsedSeconds,
|
||
allowedIncreaseSeconds: allowedIncrease,
|
||
excessiveIncreaseSeconds: excessiveIncrease,
|
||
traceId: cell.traceId ?? column.traceId ?? null,
|
||
messageId: cell.messageId ?? column.messageId ?? null,
|
||
promptIndex: cell.promptIndex ?? null,
|
||
samplePromptIndex: row.promptIndex ?? null,
|
||
source: cell.source ?? column.source ?? null,
|
||
pageRole: cell.pageRole ?? column.pageRole ?? null,
|
||
pageId: cell.pageId ?? column.pageId ?? null,
|
||
pageEpoch: cell.pageEpoch ?? row.pageEpoch ?? column.pageEpoch ?? null,
|
||
valuesRedacted: true
|
||
});
|
||
}
|
||
}
|
||
previousByMetric.set(metric, { value: current, seq: row.seq ?? null, ts: row.ts ?? null, status: cell.status ?? null });
|
||
}
|
||
}
|
||
}
|
||
return { anomalies, elapsedZeroResets, totalElapsedForwardJumps, terminalElapsedGrowth, terminalElapsedCorrections, recentUpdateResets, recentUpdateSteps };
|
||
}
|
||
|
||
function elapsedSecondsBetween(fromTs, toTs) {
|
||
const from = Date.parse(fromTs);
|
||
const to = Date.parse(toTs);
|
||
if (!Number.isFinite(from) || !Number.isFinite(to) || to < from) return 0;
|
||
return Number(((to - from) / 1000).toFixed(3));
|
||
}
|
||
|
||
function maxPositiveDelta(items) {
|
||
const values = (Array.isArray(items) ? items : [])
|
||
.map((item) => Number(item.delta))
|
||
.filter((value) => Number.isFinite(value) && value > 0);
|
||
return values.length > 0 ? Math.max(...values) : 0;
|
||
}
|
||
|
||
function isTerminalTurnStatus(value) {
|
||
const status = String(value ?? "").trim().toLowerCase().replace(/_/gu, "-");
|
||
return ["completed", "succeeded", "success", "failed", "error", "blocked", "timeout", "canceled", "cancelled", "stale", "done", "terminal", "thread-resume-failed"].includes(status);
|
||
}
|
||
|
||
|
||
|
||
function buildTraceOrderMetrics(samples, timeline) {
|
||
const rows = [];
|
||
const orderAnomalies = [];
|
||
const completionNotLast = [];
|
||
const groups = new Map();
|
||
for (let sampleIndex = 0; sampleIndex < samples.length; sampleIndex += 1) {
|
||
const sample = samples[sampleIndex] || {};
|
||
const sampleRows = traceTimingRowsForSample(sample, timeline[sampleIndex] || {});
|
||
for (const row of sampleRows) {
|
||
const normalized = {
|
||
...row,
|
||
sampleIndex,
|
||
sampleSeq: sample.seq ?? null,
|
||
timestamp: sample.ts || sample.timestamp || sample.collectedAt || sample.time || null,
|
||
pageRole: row.pageRole || sample.pageRole || sample.role || sample.contextRole || null,
|
||
pageId: row.pageId || sample.pageId || sample.contextId || null,
|
||
sessionId: row.sessionId || sample.sessionId || sample.workbenchSessionId || null,
|
||
};
|
||
rows.push(normalized);
|
||
const key = traceRowGroupKey(normalized);
|
||
if (!groups.has(key)) groups.set(key, []);
|
||
groups.get(key).push(normalized);
|
||
}
|
||
}
|
||
const slackSeconds = Math.max(1, Number(alertThresholds?.turnTimingSampleSlackSeconds || 0));
|
||
for (const groupRows of groups.values()) {
|
||
const sorted = groupRows.slice().sort((a, b) => {
|
||
if (a.sampleIndex !== b.sampleIndex) return a.sampleIndex - b.sampleIndex;
|
||
return (a.rowIndex ?? 0) - (b.rowIndex ?? 0);
|
||
});
|
||
let previous = null;
|
||
for (const row of sorted) {
|
||
if (previous) {
|
||
const reasons = [];
|
||
if (Number.isFinite(previous.totalSeconds) && Number.isFinite(row.totalSeconds) && row.totalSeconds + slackSeconds < previous.totalSeconds) {
|
||
reasons.push('total-decreased');
|
||
}
|
||
if (Number.isFinite(previous.projectedSeq) && Number.isFinite(row.projectedSeq) && row.projectedSeq < previous.projectedSeq) {
|
||
reasons.push('projected-seq-decreased');
|
||
}
|
||
if (Number.isFinite(previous.clockSeconds) && Number.isFinite(row.clockSeconds)) {
|
||
const diff = previous.clockSeconds - row.clockSeconds;
|
||
if (diff > slackSeconds && diff < 43200) reasons.push('clock-decreased');
|
||
}
|
||
if (Number.isFinite(previous.timestampMs) && Number.isFinite(row.timestampMs) && row.timestampMs + slackSeconds * 1000 < previous.timestampMs) {
|
||
reasons.push('timestamp-decreased');
|
||
}
|
||
if (reasons.length) {
|
||
orderAnomalies.push({
|
||
sampleIndex: row.sampleIndex,
|
||
sampleSeq: row.sampleSeq,
|
||
timestamp: row.timestamp,
|
||
pageRole: row.pageRole,
|
||
pageId: row.pageId,
|
||
sessionId: row.sessionId,
|
||
traceId: row.traceId || previous.traceId || null,
|
||
previousRowIndex: previous.rowIndex,
|
||
currentRowIndex: row.rowIndex,
|
||
reasons,
|
||
previousTotalSeconds: previous.totalSeconds,
|
||
currentTotalSeconds: row.totalSeconds,
|
||
previousProjectedSeq: previous.projectedSeq,
|
||
currentProjectedSeq: row.projectedSeq,
|
||
previousSourceSeq: previous.sourceSeq,
|
||
currentSourceSeq: row.sourceSeq,
|
||
previousEventSeq: previous.eventSeq,
|
||
currentEventSeq: row.eventSeq,
|
||
previousClockSeconds: previous.clockSeconds,
|
||
currentClockSeconds: row.clockSeconds,
|
||
previousTimestampMs: previous.timestampMs,
|
||
currentTimestampMs: row.timestampMs,
|
||
previousPreview: previous.preview,
|
||
currentPreview: row.preview,
|
||
});
|
||
}
|
||
}
|
||
previous = row;
|
||
}
|
||
for (let index = 0; index < sorted.length; index += 1) {
|
||
const row = sorted[index];
|
||
if (!row.isCompletion) continue;
|
||
const later = sorted.slice(index + 1).find((candidate) => {
|
||
if (candidate.isCompletion && candidate.preview === row.preview) return false;
|
||
return Number.isFinite(candidate.totalSeconds) || Number.isFinite(candidate.clockSeconds) || Number.isFinite(candidate.projectedSeq);
|
||
});
|
||
if (later) {
|
||
completionNotLast.push({
|
||
sampleIndex: row.sampleIndex,
|
||
sampleSeq: row.sampleSeq,
|
||
timestamp: row.timestamp,
|
||
pageRole: row.pageRole,
|
||
pageId: row.pageId,
|
||
sessionId: row.sessionId,
|
||
traceId: row.traceId || later.traceId || null,
|
||
completionRowIndex: row.rowIndex,
|
||
laterRowIndex: later.rowIndex,
|
||
completionTotalSeconds: row.totalSeconds,
|
||
laterTotalSeconds: later.totalSeconds,
|
||
completionProjectedSeq: row.projectedSeq,
|
||
laterProjectedSeq: later.projectedSeq,
|
||
completionSourceSeq: row.sourceSeq,
|
||
laterSourceSeq: later.sourceSeq,
|
||
completionEventSeq: row.eventSeq,
|
||
laterEventSeq: later.eventSeq,
|
||
completionPreview: row.preview,
|
||
laterPreview: later.preview,
|
||
});
|
||
}
|
||
}
|
||
}
|
||
return {
|
||
summary: {
|
||
sampleCount: samples.length,
|
||
traceRowCount: rows.length,
|
||
orderAnomalyCount: orderAnomalies.length,
|
||
completionNotLastCount: completionNotLast.length,
|
||
},
|
||
rows: rows.slice(0, 200),
|
||
orderAnomalies: orderAnomalies.slice(0, 100),
|
||
completionNotLast: completionNotLast.slice(0, 100),
|
||
};
|
||
}
|
||
|
||
function buildCodeAgentCardDurationUnderreportedMetrics(samples, timeline) {
|
||
const findings = [];
|
||
const slackSeconds = Math.max(5, Number(alertThresholds?.turnTimingSampleSlackSeconds || 0));
|
||
for (let sampleIndex = 0; sampleIndex < samples.length; sampleIndex += 1) {
|
||
const sample = samples[sampleIndex] || {};
|
||
const cards = codeAgentCardsForSample(sample);
|
||
if (!cards.length) continue;
|
||
const traceRows = traceTimingRowsForSample(sample, timeline[sampleIndex] || {});
|
||
const terminalCards = cards.filter((card) => isCodeAgentCardTerminal(card));
|
||
const sampleText = sampleVisibleText(sample, timeline[sampleIndex] || {});
|
||
for (const card of terminalCards) {
|
||
const cardText = codeAgentCardText(card);
|
||
const parsedCardSeconds = parseTotalElapsedSeconds(cardText).filter(Number.isFinite);
|
||
const cardSeconds = Number.isFinite(Number(card.totalElapsedSeconds)) ? Number(card.totalElapsedSeconds) : parsedCardSeconds.length > 0 ? Math.max(...parsedCardSeconds) : NaN;
|
||
if (!Number.isFinite(cardSeconds)) continue;
|
||
const traceMatched = traceRows.filter((row) => traceRowMatchesCard(row, card, terminalCards.length));
|
||
const traceEvidence = maxTraceDurationEvidence(traceMatched);
|
||
const textEvidence = maxSelfReportedDurationEvidence([card.text, card.preview, card.finalResponseText, card.runningRecordText, terminalCards.length === 1 ? sampleText : ''].filter(Boolean).join('\n'));
|
||
const evidences = [traceEvidence, textEvidence].filter((item) => item && item.exact === true && Number.isFinite(item.seconds));
|
||
if (!evidences.length) continue;
|
||
const strongest = evidences.sort((a, b) => b.seconds - a.seconds)[0];
|
||
const tolerance = Math.max(slackSeconds, Math.ceil(strongest.seconds * 0.05));
|
||
if (strongest.seconds > cardSeconds + tolerance) {
|
||
findings.push({
|
||
sampleIndex,
|
||
...timingFindingMeta(strongest.kind, timingStatusFromTurnStatus(card.status || card.state || card.phase, "business-turn-completed"), {
|
||
cardElapsedSource: "dom-card-total-elapsed",
|
||
expectedElapsedSource: strongest.kind,
|
||
}),
|
||
timestamp: sample.timestamp || sample.collectedAt || sample.time || null,
|
||
pageRole: card.pageRole || sample.pageRole || sample.role || sample.contextRole || null,
|
||
pageId: card.pageId || sample.pageId || sample.contextId || null,
|
||
sessionId: card.sessionId || sample.sessionId || sample.workbenchSessionId || null,
|
||
traceId: card.traceId || strongest.traceId || null,
|
||
status: card.status || card.state || card.phase || null,
|
||
cardTotalElapsedSeconds: cardSeconds,
|
||
expectedElapsedSeconds: strongest.seconds,
|
||
deltaSeconds: strongest.seconds - cardSeconds,
|
||
toleranceSeconds: tolerance,
|
||
evidenceKind: strongest.kind,
|
||
evidencePreview: strongest.preview,
|
||
cardPreview: card.preview || compactOneLine(cardText || ''),
|
||
});
|
||
}
|
||
}
|
||
}
|
||
return findings.slice(0, 100);
|
||
}
|
||
|
||
function buildCodeAgentCardDurationMismatchMetrics(samples, timeline) {
|
||
const findings = [];
|
||
const slackSeconds = Math.max(5, Number(alertThresholds?.turnTimingSampleSlackSeconds || 0));
|
||
for (let sampleIndex = 0; sampleIndex < samples.length; sampleIndex += 1) {
|
||
const sample = samples[sampleIndex] || {};
|
||
const cards = codeAgentCardsForSample(sample);
|
||
if (!cards.length) continue;
|
||
const traceRows = traceTimingRowsForSample(sample, timeline[sampleIndex] || {});
|
||
const terminalCards = cards.filter((card) => isCodeAgentCardTerminal(card));
|
||
const sampleText = sampleVisibleText(sample, timeline[sampleIndex] || {});
|
||
for (const card of terminalCards) {
|
||
const cardText = codeAgentCardText(card);
|
||
const parsedCardSeconds = parseTotalElapsedSeconds(cardText).filter(Number.isFinite);
|
||
const cardSeconds = Number.isFinite(Number(card.totalElapsedSeconds)) ? Number(card.totalElapsedSeconds) : parsedCardSeconds.length > 0 ? Math.max(...parsedCardSeconds) : NaN;
|
||
if (!Number.isFinite(cardSeconds)) continue;
|
||
const traceMatched = traceRows.filter((row) => traceRowMatchesCard(row, card, terminalCards.length));
|
||
const traceEvidence = maxTraceDurationEvidence(traceMatched);
|
||
const textEvidence = maxSelfReportedDurationEvidence([card.text, card.preview, card.finalResponseText, card.runningRecordText, terminalCards.length === 1 ? sampleText : ''].filter(Boolean).join('\n'));
|
||
const evidences = [traceEvidence, textEvidence].filter((item) => item && item.exact === true && Number.isFinite(item.seconds));
|
||
if (!evidences.length) continue;
|
||
const strongest = evidences.sort((a, b) => b.seconds - a.seconds)[0];
|
||
const exactEvidence = strongest.exact === true || strongest.kind === 'trace-completion-total' || strongest.kind === 'final-response-duration';
|
||
const tolerance = Math.max(slackSeconds, Math.ceil(strongest.seconds * 0.05));
|
||
const signedDelta = Number((cardSeconds - strongest.seconds).toFixed(3));
|
||
const absoluteDelta = Math.abs(signedDelta);
|
||
const underreported = strongest.seconds > cardSeconds + tolerance;
|
||
const overreported = exactEvidence && cardSeconds > strongest.seconds + tolerance;
|
||
if (!underreported && !overreported) continue;
|
||
findings.push({
|
||
sampleIndex,
|
||
...timingFindingMeta(strongest.kind, timingStatusFromTurnStatus(card.status || card.state || card.phase, "business-turn-completed"), {
|
||
cardElapsedSource: "dom-card-total-elapsed",
|
||
expectedElapsedSource: strongest.kind,
|
||
}),
|
||
timestamp: sample.timestamp || sample.collectedAt || sample.time || sample.ts || null,
|
||
pageRole: card.pageRole || sample.pageRole || sample.role || sample.contextRole || null,
|
||
pageId: card.pageId || sample.pageId || sample.contextId || null,
|
||
sessionId: card.sessionId || sample.sessionId || sample.workbenchSessionId || null,
|
||
traceId: card.traceId || strongest.traceId || null,
|
||
status: card.status || card.state || card.phase || null,
|
||
direction: underreported ? 'underreported' : 'overreported',
|
||
cardTotalElapsedSeconds: cardSeconds,
|
||
expectedElapsedSeconds: strongest.seconds,
|
||
signedDeltaSeconds: signedDelta,
|
||
deltaSeconds: Number(absoluteDelta.toFixed(3)),
|
||
toleranceSeconds: tolerance,
|
||
evidenceKind: strongest.kind,
|
||
exactEvidence,
|
||
evidencePreview: strongest.preview,
|
||
cardPreview: card.preview || compactOneLine(cardText || ''),
|
||
});
|
||
}
|
||
}
|
||
return findings.slice(0, 100);
|
||
}
|
||
|
||
function traceTimingRowsForSample(sample, timelineItem) {
|
||
const rows = [];
|
||
const seen = new Set();
|
||
const appendRowsFromText = (text, source, baseIndex, meta = {}) => {
|
||
for (const extracted of extractTraceRowsFromText(text, source, baseIndex, meta)) {
|
||
const key = [extracted.pageRole || '', extracted.pageId || '', extracted.rowIndex, extracted.preview].join('|');
|
||
if (seen.has(key)) continue;
|
||
seen.add(key);
|
||
rows.push(extracted);
|
||
}
|
||
};
|
||
for (const candidate of traceRowCandidateArrays(sample, timelineItem)) {
|
||
const array = Array.isArray(candidate.rows) ? candidate.rows : [];
|
||
array.forEach((item, index) => {
|
||
if (typeof item === 'string') {
|
||
appendRowsFromText(item, candidate.source, index, candidate.meta || {});
|
||
return;
|
||
}
|
||
if (!item || typeof item !== 'object') return;
|
||
const text = objectText(item);
|
||
if (!text) return;
|
||
appendRowsFromText(text, candidate.source, Number.isFinite(Number(item.index)) ? Number(item.index) : index, {
|
||
...(candidate.meta || {}),
|
||
pageRole: item.pageRole || item.role || candidate.meta?.pageRole || null,
|
||
pageId: item.pageId || item.contextId || candidate.meta?.pageId || null,
|
||
sessionId: item.sessionId || item.workbenchSessionId || candidate.meta?.sessionId || null,
|
||
traceId: item.traceId || item.trace_id || candidate.meta?.traceId || null,
|
||
projectedSeq: item.projectedSeq ?? item.projected_seq ?? item.projectedSequence ?? null,
|
||
sourceSeq: item.sourceSeq ?? item.source_seq ?? item.sourceSequence ?? null,
|
||
eventSeq: item.eventSeq ?? item.event_seq ?? item.sequence ?? null,
|
||
eventTimestamp: item.eventTimestamp ?? item.event_ts ?? item.timestamp ?? item.ts ?? null,
|
||
eventTimeText: item.eventTimeText ?? item.timeText ?? null,
|
||
eventKind: item.eventKind ?? item.kind ?? item.status ?? null,
|
||
});
|
||
});
|
||
}
|
||
if (!rows.length) {
|
||
appendRowsFromText(sampleVisibleText(sample, timelineItem), 'visible-text', 0, {
|
||
pageRole: sample.pageRole || sample.role || sample.contextRole || null,
|
||
pageId: sample.pageId || sample.contextId || null,
|
||
sessionId: sample.sessionId || sample.workbenchSessionId || null,
|
||
});
|
||
}
|
||
rows.sort((a, b) => (a.rowIndex ?? 0) - (b.rowIndex ?? 0));
|
||
return rows;
|
||
}
|
||
|
||
function extractTraceRowsFromText(text, source, baseIndex, meta = {}) {
|
||
const result = [];
|
||
const normalized = String(text || '').replace(/\r/g, '\n');
|
||
if (!normalized.trim()) return result;
|
||
const lines = normalized.split('\n').map((line) => line.trim()).filter(Boolean);
|
||
for (let index = 0; index < lines.length; index += 1) {
|
||
const line = lines[index];
|
||
if (!traceLineLooksRelevant(line)) continue;
|
||
const nextLine = index + 1 < lines.length && !/^\d{1,2}:\d{2}:\d{2}\b/.test(lines[index + 1]) ? lines[index + 1] : '';
|
||
const preview = compactOneLine(nextLine ? line + ' ' + nextLine : line);
|
||
result.push(normalizeTraceTimingRow(preview, source, Number(baseIndex || 0) * 1000 + index, meta));
|
||
}
|
||
return result;
|
||
}
|
||
|
||
function normalizeTraceTimingRow(text, source, rowIndex, meta = {}) {
|
||
const preview = compactOneLine(text).slice(0, 240);
|
||
const projectedSeq = firstFiniteNumber(meta.projectedSeq, parseTraceRowProjectedSeq(preview));
|
||
const sourceSeq = firstFiniteNumber(meta.sourceSeq);
|
||
const eventSeq = firstFiniteNumber(meta.eventSeq);
|
||
const timestampMs = parseTraceRowTimestampMs(meta.eventTimestamp || meta.eventTimeText || preview);
|
||
return {
|
||
source,
|
||
rowIndex,
|
||
preview,
|
||
pageRole: meta.pageRole || null,
|
||
pageId: meta.pageId || null,
|
||
sessionId: meta.sessionId || null,
|
||
traceId: meta.traceId || parseTraceRowTraceId(preview),
|
||
clockSeconds: parseTraceRowClockSeconds(preview) ?? parseTraceRowClockSeconds(meta.eventTimeText || ""),
|
||
timestampMs,
|
||
totalSeconds: parseTraceRowTotalSeconds(preview),
|
||
projectedSeq,
|
||
sourceSeq,
|
||
eventSeq,
|
||
eventTimestamp: meta.eventTimestamp || null,
|
||
eventTimeText: meta.eventTimeText || null,
|
||
eventKind: meta.eventKind || null,
|
||
isCompletion: traceRowIsTerminalCompletionText(preview, meta.eventKind || ""),
|
||
};
|
||
}
|
||
|
||
function traceRowIsTerminalCompletionText(preview, eventKind = "") {
|
||
const value = [preview, eventKind].map((item) => String(item || "")).join(" ");
|
||
if (/\bnon[-_ ]?terminal\b/i.test(value)) return false;
|
||
if (/轮次完成|turn\s+completed|completed\s+turn|backend[_: -]?turn[_: -]?finished/i.test(value)) return true;
|
||
return /\bterminal(?:[_: -]?status|Status)?\s*[=: -]\s*(?:completed|failed|cancelled|canceled|timeout)\b/i.test(value);
|
||
}
|
||
|
||
function traceRowIsRoundCompletionText(preview, eventKind = "") {
|
||
const value = [preview, eventKind].map((item) => String(item || "")).join(" ");
|
||
if (/\bnon[-_ ]?terminal\b/i.test(value)) return false;
|
||
return /轮次完成|turn\s+completed|completed\s+turn|backend[_: -]?turn[_: -]?finished/i.test(value);
|
||
}
|
||
|
||
function traceRowCandidateArrays(sample, timelineItem) {
|
||
const candidates = [];
|
||
const pushArray = (rows, source, meta = {}) => {
|
||
if (Array.isArray(rows) && rows.length) candidates.push({ rows, source, meta });
|
||
};
|
||
const directSources = [
|
||
[sample?.traceRows, 'sample.traceRows'],
|
||
[sample?.eventRows, 'sample.eventRows'],
|
||
[sample?.activityRows, 'sample.activityRows'],
|
||
[sample?.timelineRows, 'sample.timelineRows'],
|
||
[sample?.dom?.traceRows, 'sample.dom.traceRows'],
|
||
[sample?.dom?.eventRows, 'sample.dom.eventRows'],
|
||
[sample?.dom?.activityRows, 'sample.dom.activityRows'],
|
||
[sample?.dom?.timelineRows, 'sample.dom.timelineRows'],
|
||
[timelineItem?.traceRows, 'timeline.traceRows'],
|
||
[timelineItem?.eventRows, 'timeline.eventRows'],
|
||
[timelineItem?.activityRows, 'timeline.activityRows'],
|
||
[timelineItem?.rows, 'timeline.rows'],
|
||
[timelineItem?.events, 'timeline.events'],
|
||
];
|
||
for (const [rows, source] of directSources) pushArray(rows, source, {});
|
||
if (!candidates.length) collectNamedTraceArrays(sample, candidates, 'sample', 0);
|
||
if (!candidates.length) collectNamedTraceArrays(timelineItem, candidates, 'timeline', 0);
|
||
return candidates;
|
||
}
|
||
|
||
function collectNamedTraceArrays(value, candidates, path, depth) {
|
||
if (!value || depth > 5) return;
|
||
if (Array.isArray(value)) {
|
||
const pathLooksTrace = /trace|timeline|activity|event|log|record/i.test(path);
|
||
const valueLooksTrace = value.slice(0, 5).some((item) => traceLineLooksRelevant(typeof item === 'string' ? item : objectText(item)));
|
||
if (pathLooksTrace || valueLooksTrace) candidates.push({ rows: value, source: path, meta: {} });
|
||
return;
|
||
}
|
||
if (typeof value !== 'object') return;
|
||
for (const [key, child] of Object.entries(value)) {
|
||
if (!child) continue;
|
||
if (Array.isArray(child)) {
|
||
const childPath = path + '.' + key;
|
||
const pathLooksTrace = /trace|timeline|activity|event|log|record/i.test(key);
|
||
const valueLooksTrace = child.slice(0, 5).some((item) => traceLineLooksRelevant(typeof item === 'string' ? item : objectText(item)));
|
||
if (pathLooksTrace || valueLooksTrace) candidates.push({ rows: child, source: childPath, meta: {} });
|
||
continue;
|
||
}
|
||
if (typeof child === 'object' && /dom|trace|timeline|activity|event|log|record|page|card|message|panel|diagnostic/i.test(key)) {
|
||
collectNamedTraceArrays(child, candidates, path + '.' + key, depth + 1);
|
||
}
|
||
}
|
||
}
|
||
|
||
function traceLineLooksRelevant(text) {
|
||
const value = String(text || '').trim();
|
||
if (!value) return false;
|
||
if (/^\d{1,2}:\d{2}:\d{2}\b/.test(value)) return true;
|
||
if (/\btotal=\d/.test(value)) return true;
|
||
if (/轮次完成(总耗时/.test(value)) return true;
|
||
if (/\bseq(?:uence)?[=:]\s*\d+/i.test(value)) return true;
|
||
return false;
|
||
}
|
||
|
||
function parseTraceRowClockSeconds(text) {
|
||
const match = String(text || '').match(/^\s*(\d{1,2}):(\d{2}):(\d{2})\b/);
|
||
if (!match) return null;
|
||
return Number(match[1]) * 3600 + Number(match[2]) * 60 + Number(match[3]);
|
||
}
|
||
|
||
function parseTraceRowTotalSeconds(text) {
|
||
const value = String(text || '');
|
||
const totalMatch = value.match(/\btotal=([0-9:.]+)/i);
|
||
if (totalMatch) return parseTraceDurationSeconds(totalMatch[1]);
|
||
const completionMatch = value.match(/总耗时\s*([0-9:.]+)/);
|
||
if (completionMatch) return parseTraceDurationSeconds(completionMatch[1]);
|
||
return null;
|
||
}
|
||
|
||
function parseTraceDurationSeconds(value) {
|
||
const text = String(value || '').trim();
|
||
if (!text) return null;
|
||
const parts = text.split(':').map((part) => Number(part));
|
||
if (parts.some((part) => !Number.isFinite(part))) return null;
|
||
if (parts.length === 3) return parts[0] * 3600 + parts[1] * 60 + parts[2];
|
||
if (parts.length === 2) return parts[0] * 60 + parts[1];
|
||
if (parts.length === 1) return parts[0];
|
||
return null;
|
||
}
|
||
|
||
function parseTraceRowProjectedSeq(text) {
|
||
const value = String(text || '');
|
||
const match = value.match(/\b(?:projected[_-]?seq|seq(?:uence)?|event[_-]?seq)\s*[=:]\s*(\d+)/i);
|
||
return match ? Number(match[1]) : null;
|
||
}
|
||
|
||
function parseTraceRowTimestampMs(value) {
|
||
const text = String(value || '').trim();
|
||
if (!text) return null;
|
||
const parsed = Date.parse(text);
|
||
return Number.isFinite(parsed) ? parsed : null;
|
||
}
|
||
|
||
function firstFiniteNumber(...values) {
|
||
for (const value of values) {
|
||
const numeric = Number(value);
|
||
if (Number.isFinite(numeric)) return numeric;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function parseTraceRowTraceId(text) {
|
||
const match = String(text || '').match(/\b(?:trace_id=|traceId[:=]?\s*)(trc_[a-z0-9_-]+|[a-f0-9]{16,})\b/i);
|
||
return match ? match[1] : null;
|
||
}
|
||
|
||
function traceRowGroupKey(row) {
|
||
const identity = row.traceId ? 'trace:' + row.traceId : 'sample:' + (row.sampleIndex ?? '-') + ':' + (row.source || 'unknown');
|
||
return [row.pageRole || '', row.pageId || '', row.sessionId || '', row.source || '', row.sampleIndex ?? '', identity].join('|');
|
||
}
|
||
|
||
function traceRowMatchesCard(row, card, terminalCardCount) {
|
||
if (!row) return false;
|
||
if (card.traceId) return row.traceId === card.traceId;
|
||
if (row.traceId) return false;
|
||
if (row.sessionId && card.sessionId) return terminalCardCount === 1 && row.sessionId === card.sessionId;
|
||
if (terminalCardCount === 1) return true;
|
||
return false;
|
||
}
|
||
|
||
function maxTraceDurationEvidence(rows) {
|
||
const finiteTotals = rows.map((row) => Number(row.totalSeconds)).filter(Number.isFinite);
|
||
const finiteClocks = rows.map((row) => Number(row.clockSeconds)).filter(Number.isFinite);
|
||
const evidences = [];
|
||
if (finiteTotals.length) {
|
||
const maxTotal = Math.max(...finiteTotals);
|
||
const source = rows.find((row) => Number(row.totalSeconds) === maxTotal);
|
||
const exact = source?.isCompletion === true;
|
||
evidences.push({ kind: exact ? 'trace-completion-total' : 'trace-total', seconds: maxTotal, traceId: source?.traceId || null, preview: source?.preview || '', exact });
|
||
}
|
||
if (finiteClocks.length >= 2) {
|
||
const minClock = Math.min(...finiteClocks);
|
||
const maxClock = Math.max(...finiteClocks);
|
||
const span = maxClock - minClock;
|
||
if (span >= 0 && span < 43200) evidences.push({ kind: 'trace-clock-span', seconds: span, traceId: null, preview: 'visible trace row clock span', exact: false });
|
||
}
|
||
if (!evidences.length) return null;
|
||
evidences.sort((a, b) => b.seconds - a.seconds);
|
||
return evidences[0];
|
||
}
|
||
|
||
function maxSelfReportedDurationEvidence(text) {
|
||
const value = String(text || '');
|
||
const lines = value.split(/\n+/).map((line) => line.trim()).filter(Boolean);
|
||
let best = null;
|
||
for (let index = 0; index < lines.length; index += 1) {
|
||
const line = lines[index];
|
||
const previous = index > 0 ? lines[index - 1] : '';
|
||
const next = index + 1 < lines.length ? lines[index + 1] : '';
|
||
const candidateText = selfReportedDurationCandidateText(previous, line, next);
|
||
if (!candidateText) continue;
|
||
const seconds = parseSelfReportedRoundDurationSeconds(candidateText);
|
||
if (!Number.isFinite(seconds)) continue;
|
||
if (!best || seconds > best.seconds) {
|
||
best = { kind: 'final-response-duration', seconds, preview: compactOneLine(candidateText).slice(0, 240), exact: true };
|
||
}
|
||
}
|
||
return best;
|
||
}
|
||
|
||
function selfReportedDurationCandidateText(previous, line, next) {
|
||
const current = String(line || '');
|
||
const before = String(previous || '');
|
||
const after = String(next || '');
|
||
const windowText = [before, current, after].filter(Boolean).join(' ');
|
||
const hasDurationKeyword = /耗时|用时|duration|elapsed/i.test(current);
|
||
const hasNearbyDurationHeading = /(?:本轮|整轮|全程|任务|round|turn)?\s*(?:耗时|用时|duration|elapsed)/i.test(before)
|
||
|| /(?:本轮|整轮|全程|任务|round|turn)?\s*(?:耗时|用时|duration|elapsed)/i.test(after);
|
||
const hasDurationValue = /(?:约|大约|around|about)?\s*\d+(?:\.\d+)?\s*(?:小时|分钟|分|秒|hour|hours|hr|hrs|min|mins|minute|minutes|sec|secs|second|seconds)/i.test(windowText);
|
||
const hasRoundContext = /本轮|整轮|全程|从.+到|全部通过|smoke|benchmark|round|turn|completed|passed/i.test(windowText);
|
||
if (hasDurationKeyword && hasDurationValue) return current;
|
||
if (hasNearbyDurationHeading && hasDurationValue) return windowText;
|
||
if (hasRoundContext && hasDurationValue && /(?:约|大约|around|about)\s*\d|\d+(?:\.\d+)?\s*(?:分钟|小时|minute|hour)/i.test(current)) return windowText;
|
||
return '';
|
||
}
|
||
|
||
function parseSelfReportedRoundDurationSeconds(text) {
|
||
const value = String(text || '');
|
||
const clock = value.match(/(?:耗时|用时|duration|elapsed)[^0-9]{0,24}(\d{1,2}:\d{2}:\d{2}|\d{1,2}:\d{2})/i);
|
||
if (clock) return parseTraceDurationSeconds(clock[1]);
|
||
const hour = value.match(/(?:约|大约|around|about)?\s*(\d+(?:\.\d+)?)\s*(?:小时|hour|hours|hr|hrs)/i);
|
||
const minute = value.match(/(?:约|大约|around|about)?\s*(\d+(?:\.\d+)?)\s*(?:分钟|分|min|mins|minute|minutes)/i);
|
||
const second = value.match(/(?:约|大约|around|about)?\s*(\d+(?:\.\d+)?)\s*(?:秒|sec|secs|second|seconds)/i);
|
||
let total = 0;
|
||
if (hour) total += Number(hour[1]) * 3600;
|
||
if (minute) total += Number(minute[1]) * 60;
|
||
if (second) total += Number(second[1]);
|
||
return total > 0 ? total : null;
|
||
}
|
||
|
||
function sampleVisibleText(sample, timelineItem) {
|
||
const chunks = [];
|
||
for (const source of [sample?.visibleText, sample?.text, sample?.innerText, sample?.dom?.visibleText, sample?.dom?.text, timelineItem?.visibleText, timelineItem?.text, timelineItem?.message, timelineItem?.summary]) {
|
||
if (typeof source === 'string' && source.trim()) chunks.push(source);
|
||
}
|
||
return chunks.join('\n');
|
||
}
|
||
|
||
function objectText(value) {
|
||
if (!value || typeof value !== 'object') return typeof value === 'string' ? value : '';
|
||
const keys = ['text', 'innerText', 'visibleText', 'label', 'title', 'summary', 'message', 'content', 'body', 'preview', 'description'];
|
||
const chunks = [];
|
||
for (const key of keys) {
|
||
const part = value[key];
|
||
if (typeof part === 'string' && part.trim()) chunks.push(part);
|
||
}
|
||
return chunks.join('\n');
|
||
}
|
||
|
||
function compactOneLine(value) {
|
||
return String(value || '').replace(/\s+/g, ' ').trim();
|
||
}
|
||
|
||
function buildCodeAgentCardTimingMetrics(samples, timeline, turnTiming) {
|
||
const missingElapsed = [];
|
||
const missingRecentUpdate = [];
|
||
const cardRows = [];
|
||
for (let index = 0; index < (Array.isArray(samples) ? samples : []).length; index += 1) {
|
||
const sample = samples[index];
|
||
const timelineItem = timeline[index] || {};
|
||
for (const card of codeAgentCardsForSample(sample)) {
|
||
const text = codeAgentCardText(card);
|
||
const totalElapsedValues = parseTotalElapsedSeconds(card?.durationText).filter(Number.isFinite);
|
||
const recentUpdateValues = parseRecentUpdateSeconds(card?.activityText).filter(Number.isFinite);
|
||
const terminal = isCodeAgentCardTerminal(card);
|
||
const row = {
|
||
...ref(sample),
|
||
promptIndex: timelineItem.promptIndex ?? 0,
|
||
source: card.source ?? "turn",
|
||
status: card.status ?? null,
|
||
messageId: card.messageId ?? null,
|
||
traceId: card.traceId ?? firstTraceId([text]),
|
||
sessionId: card.sessionId ?? sample.sessionId ?? sample.workbenchSessionId ?? sample.routeSessionId ?? sample.activeSessionId ?? null,
|
||
durationText: limitText(card.durationText, 120),
|
||
activityText: limitText(card.activityText, 120),
|
||
totalElapsedSeconds: totalElapsedValues.length > 0 ? Math.max(...totalElapsedValues) : null,
|
||
recentUpdateSeconds: recentUpdateValues.length > 0 ? Math.max(...recentUpdateValues) : null,
|
||
terminal,
|
||
textHash: card.textHash ?? sha256(text),
|
||
textPreview: limitText(text, 180),
|
||
valuesRedacted: true
|
||
};
|
||
cardRows.push(row);
|
||
if (row.totalElapsedSeconds === null) missingElapsed.push(row);
|
||
if (!terminal && row.recentUpdateSeconds === null) missingRecentUpdate.push(row);
|
||
}
|
||
}
|
||
const roundCompletion = buildRoundCompletionMetrics(samples, timeline, turnTiming);
|
||
return {
|
||
summary: {
|
||
cardSampleCount: cardRows.length,
|
||
terminalCardSampleCount: cardRows.filter((item) => item.terminal).length,
|
||
runningCardSampleCount: cardRows.filter((item) => !item.terminal).length,
|
||
missingElapsedCount: missingElapsed.length,
|
||
missingRecentUpdateCount: missingRecentUpdate.length,
|
||
roundCompletionEventCount: roundCompletion.events.length,
|
||
roundCompletionElapsedMismatchCount: roundCompletion.elapsedMismatches.length,
|
||
roundCompletionFinalResponseMissingCount: roundCompletion.finalResponseMissing.length,
|
||
roundCompletionPostTimingChangeCount: roundCompletion.postCompletionTimingChanges.length,
|
||
roundCompletionPostRecentUpdateVisibleCount: roundCompletion.postCompletionRecentUpdateVisible.length,
|
||
elapsedMismatchToleranceSeconds: alertThresholds.turnTimingSampleSlackSeconds,
|
||
},
|
||
cardSamples: cardRows.slice(0, 200),
|
||
missingElapsed: missingElapsed.slice(0, 200),
|
||
missingRecentUpdate: missingRecentUpdate.slice(0, 200),
|
||
roundCompletion,
|
||
valuesRedacted: true
|
||
};
|
||
}
|
||
|
||
function codeAgentCardsForSample(sample) {
|
||
const turnCards = (Array.isArray(sample?.turns) ? sample.turns : [])
|
||
.map((item) => ({ ...item, source: item?.source || "turn" }))
|
||
.filter(isCodeAgentCardLike);
|
||
if (turnCards.length > 0) return turnCards;
|
||
return (Array.isArray(sample?.messages) ? sample.messages : [])
|
||
.map((item) => ({ ...item, source: item?.source || "message" }))
|
||
.filter(isCodeAgentCardLike);
|
||
}
|
||
|
||
function isCodeAgentCardLike(item) {
|
||
const text = codeAgentCardText(item);
|
||
const role = String(item?.dataRole || item?.role || "").toLowerCase();
|
||
if (/agent|assistant/u.test(role)) return true;
|
||
return /Code Agent|运行记录|耗时|最近\s*(?:\d+|一|两|三)|轮次完成|trace_id=trc_/iu.test(text);
|
||
}
|
||
|
||
function codeAgentCardText(item) {
|
||
return [
|
||
item?.durationText,
|
||
item?.activityText,
|
||
item?.text,
|
||
item?.textPreview,
|
||
item?.status
|
||
].map((value) => String(value || "")).filter((value) => value.trim().length > 0).join("\n");
|
||
}
|
||
|
||
function isCodeAgentCardTerminal(item) {
|
||
const status = String(item?.status ?? item?.state ?? item?.phase ?? "").trim().toLowerCase().replace(/_/gu, "-");
|
||
if (isActiveTurnStatus(status)) return false;
|
||
if (isTerminalTurnStatus(status)) return true;
|
||
if (item?.terminal === true || item?.sealed === true) return true;
|
||
const text = codeAgentCardText(item);
|
||
return isTerminalTraceText(text) || /轮次完成|轮次失败|轮次取消|已记录|completed|failed|canceled|cancelled|blocked/iu.test(text);
|
||
}
|
||
|
||
function timingFindingMeta(sourceOfTruth, status, extra = {}) {
|
||
return {
|
||
timingSourceOfTruth: sourceOfTruth,
|
||
timingStatus: status || "non-blocking-timing-alert",
|
||
timingAlert: true,
|
||
blocking: false,
|
||
...extra,
|
||
};
|
||
}
|
||
|
||
function timingStatusFromTurnStatus(status, fallback = "observer-timeout") {
|
||
const normalized = String(status ?? "").trim().toLowerCase().replace(/_/gu, "-");
|
||
if (isSuccessfulTurnStatus(normalized)) return "business-turn-completed";
|
||
if (isTerminalTurnStatus(normalized)) return "scenario-incomplete";
|
||
if (isActiveTurnStatus(normalized)) return "observer-timeout";
|
||
return fallback;
|
||
}
|
||
|
||
function timingStatusFromRows(rows, fallback = "observer-timeout") {
|
||
const list = Array.isArray(rows) ? rows : [];
|
||
if (list.some((item) => isSuccessfulTurnStatus(item?.status ?? item?.toStatus ?? item?.fromStatus))) return "business-turn-completed";
|
||
if (list.some((item) => Number(item?.finalTextSamples ?? 0) > 0)) return "business-turn-completed";
|
||
if (list.some((item) => isTerminalTurnStatus(item?.status ?? item?.toStatus ?? item?.fromStatus))) return "scenario-incomplete";
|
||
if (list.some((item) => isActiveTurnStatus(item?.status ?? item?.toStatus ?? item?.fromStatus))) return "observer-timeout";
|
||
return fallback;
|
||
}
|
||
|
||
function isSuccessfulTurnStatus(status) {
|
||
const normalized = String(status ?? "").trim().toLowerCase().replace(/_/gu, "-");
|
||
return ["completed", "complete", "succeeded", "success", "done", "ok", "passed"].includes(normalized);
|
||
}
|
||
|
||
function isActiveTurnStatus(value) {
|
||
const status = String(value ?? "").trim().toLowerCase().replace(/_/gu, "-");
|
||
return ["pending", "running", "queued", "admitted", "dispatching", "in-progress", "inprogress", "executing", "progress", "thinking", "working", "active", "streaming", "created", "started"].includes(status);
|
||
}
|
||
|
||
function buildRoundCompletionMetrics(samples, timeline, turnTiming) {
|
||
const events = [];
|
||
const elapsedMismatchToleranceSeconds = Math.max(5, Number(alertThresholds.turnTimingSampleSlackSeconds || 0));
|
||
for (let index = 0; index < (Array.isArray(samples) ? samples : []).length; index += 1) {
|
||
const sample = samples[index];
|
||
const timelineItem = timeline[index] || {};
|
||
for (const event of roundCompletionEventsForSample(sample, timelineItem)) events.push(event);
|
||
}
|
||
const completionEvents = dedupeRoundCompletionEvents(events);
|
||
const elapsedMismatches = [];
|
||
const finalResponseMissing = [];
|
||
const postCompletionTimingChanges = [];
|
||
const postCompletionRecentUpdateVisible = [];
|
||
for (const event of completionEvents) {
|
||
const sampleIndex = samples.findIndex((sample) => sample?.seq === event.seq && sample?.pageRole === event.pageRole && sample?.pageId === event.pageId);
|
||
const sameSample = sampleIndex >= 0 ? samples[sampleIndex] : null;
|
||
const sameTimelineItem = sampleIndex >= 0 ? timeline[sampleIndex] || {} : {};
|
||
const cards = sameSample ? cardMetricItemsForCompletion(sameSample, sameTimelineItem, event) : [];
|
||
const bestCard = cards.filter((card) => Number.isFinite(Number(card.totalElapsedSeconds)))[0] || null;
|
||
if (Number.isFinite(Number(event.elapsedSeconds)) && bestCard) {
|
||
const delta = Math.abs(Number(bestCard.totalElapsedSeconds) - Number(event.elapsedSeconds));
|
||
if (delta > elapsedMismatchToleranceSeconds) {
|
||
elapsedMismatches.push({
|
||
...eventRef(event),
|
||
...timingFindingMeta("trace-round-completion-total", timingStatusFromTurnStatus(bestCard.status, "business-turn-completed"), {
|
||
cardElapsedSource: bestCard.source ?? "dom-card",
|
||
completionElapsedSource: "trace-round-completion",
|
||
}),
|
||
cardTotalElapsedSeconds: Number(bestCard.totalElapsedSeconds),
|
||
completionElapsedSeconds: Number(event.elapsedSeconds),
|
||
deltaSeconds: Number(delta.toFixed(3)),
|
||
toleranceSeconds: elapsedMismatchToleranceSeconds,
|
||
cardTraceId: bestCard.traceId ?? null,
|
||
cardMessageId: bestCard.messageId ?? null,
|
||
valuesRedacted: true
|
||
});
|
||
}
|
||
}
|
||
if (!(sameSample && sampleHasTerminalAgentResultCard(sameSample, event)) && !(sameSample && sampleHasVisibleFinalResponse(sameSample, event)) && !hasFinalResponseAfterCompletion(samples, timeline, event)) {
|
||
finalResponseMissing.push({
|
||
...eventRef(event),
|
||
completionElapsedSeconds: event.elapsedSeconds,
|
||
finalResponseProbe: sameSample ? terminalAgentResultProbe(sameSample, event) : { ok: false, reason: "sample-not-found" },
|
||
valuesRedacted: true
|
||
});
|
||
}
|
||
const postTiming = detectPostCompletionTimingChanges(turnTiming, event);
|
||
postCompletionTimingChanges.push(...postTiming.timingChanges);
|
||
postCompletionRecentUpdateVisible.push(...postTiming.recentUpdateVisible);
|
||
}
|
||
return {
|
||
events: completionEvents.slice(0, 200),
|
||
elapsedMismatches: dedupeRoundCompletionRows(elapsedMismatches).slice(0, 200),
|
||
finalResponseMissing: dedupeRoundCompletionRows(finalResponseMissing).slice(0, 200),
|
||
postCompletionTimingChanges: dedupeRoundCompletionRows(postCompletionTimingChanges).slice(0, 200),
|
||
postCompletionRecentUpdateVisible: dedupeRoundCompletionRows(postCompletionRecentUpdateVisible).slice(0, 200),
|
||
valuesRedacted: true
|
||
};
|
||
}
|
||
|
||
function roundCompletionEventsForSample(sample, timelineItem) {
|
||
const rows = [];
|
||
for (const item of traceTimingRowsForSample(sample, timelineItem)) {
|
||
const text = String(item?.preview || item?.text || item?.textPreview || "");
|
||
if (!traceRowIsRoundCompletionText(text, item?.eventKind || "")) continue;
|
||
const elapsedValues = [
|
||
Number(item?.totalSeconds),
|
||
...parseTotalElapsedSeconds(text).filter(Number.isFinite)
|
||
].filter(Number.isFinite);
|
||
const attributed = inferRoundCompletionCard(sample, text);
|
||
rows.push({
|
||
...ref(sample),
|
||
promptIndex: timelineItem.promptIndex ?? 0,
|
||
traceId: item?.traceId ?? firstTraceId([text]) ?? attributed?.traceId ?? null,
|
||
messageId: item?.messageId ?? attributed?.messageId ?? null,
|
||
attributed: Boolean(item?.traceId || item?.messageId || attributed?.traceId || attributed?.messageId),
|
||
traceRowIndex: item?.rowIndex ?? item?.index ?? null,
|
||
elapsedSeconds: elapsedValues.length > 0 ? Math.max(...elapsedValues) : null,
|
||
timingSourceOfTruth: "trace-round-completion-total",
|
||
timingStatus: "business-turn-completed",
|
||
textHash: item?.textHash ?? sha256(text),
|
||
preview: limitText(text, 180),
|
||
valuesRedacted: true
|
||
});
|
||
}
|
||
return rows;
|
||
}
|
||
|
||
function inferRoundCompletionCard(sample, text) {
|
||
const cards = codeAgentCardsForSample(sample)
|
||
.filter((card) => isCodeAgentCardTerminal(card))
|
||
.filter((card) => card?.traceId || card?.messageId);
|
||
const textHash = sha256(String(text || ""));
|
||
const direct = cards.filter((card) => {
|
||
const cardText = codeAgentCardText(card);
|
||
return card?.textHash === textHash || cardText.includes(String(text || "").slice(0, 80)) || /轮次完成/iu.test(cardText);
|
||
});
|
||
const candidates = direct.length > 0 ? direct : cards;
|
||
if (candidates.length !== 1) return null;
|
||
const card = candidates[0];
|
||
return { traceId: card.traceId ?? null, messageId: card.messageId ?? card.id ?? null };
|
||
}
|
||
|
||
function cardMetricItemsForCompletion(sample, timelineItem, event) {
|
||
const metrics = turnMetricItems(sample, timelineItem)
|
||
.filter((item) => item.promptIndex === event.promptIndex)
|
||
.filter((item) => item.pageRole === event.pageRole || !event.pageRole)
|
||
.filter((item) => item.pageId === event.pageId || !event.pageId);
|
||
if (!event.traceId && !event.messageId) {
|
||
const withElapsed = metrics.filter((item) => Number.isFinite(Number(item.totalElapsedSeconds)));
|
||
return withElapsed.length === 1 ? withElapsed : [];
|
||
}
|
||
return metrics
|
||
.filter((item) => !event.traceId || !item.traceId || item.traceId === event.traceId)
|
||
.filter((item) => !event.messageId || !item.messageId || item.messageId === event.messageId)
|
||
.sort((left, right) => {
|
||
const leftTraceMatch = event.traceId && left.traceId === event.traceId ? 0 : 1;
|
||
const rightTraceMatch = event.traceId && right.traceId === event.traceId ? 0 : 1;
|
||
const leftMessageMatch = event.messageId && left.messageId === event.messageId ? 0 : 1;
|
||
const rightMessageMatch = event.messageId && right.messageId === event.messageId ? 0 : 1;
|
||
return leftTraceMatch - rightTraceMatch || leftMessageMatch - rightMessageMatch || String(left.source || "").localeCompare(String(right.source || ""));
|
||
});
|
||
}
|
||
|
||
function hasFinalResponseAfterCompletion(samples, timeline, event) {
|
||
if (!event.traceId && !event.messageId && !event.promptIndex) return true;
|
||
const eventTsMs = Date.parse(String(event.ts || ""));
|
||
for (let index = 0; index < (Array.isArray(samples) ? samples : []).length; index += 1) {
|
||
const sample = samples[index];
|
||
const tsMs = Date.parse(String(sample?.ts || ""));
|
||
if (Number.isFinite(eventTsMs) && Number.isFinite(tsMs) && tsMs < eventTsMs) continue;
|
||
if (sample?.pageRole !== event.pageRole) continue;
|
||
const sampleSession = sample?.routeSessionId || sample?.activeSessionId || null;
|
||
const eventSession = event.routeSessionId || event.activeSessionId || null;
|
||
if (sampleSession && eventSession && sampleSession !== eventSession) continue;
|
||
const promptIndex = timeline[index]?.promptIndex ?? 0;
|
||
if (!event.traceId && !event.messageId && event.promptIndex && promptIndex !== event.promptIndex) continue;
|
||
if (sampleHasTerminalAgentResultCard(sample, event)) return true;
|
||
if (sampleHasVisibleFinalResponse(sample, event)) return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function sampleHasTerminalAgentResultCard(sample, event = {}) {
|
||
return terminalAgentResultProbe(sample, event).ok === true;
|
||
}
|
||
|
||
function terminalAgentResultProbe(sample, event = {}) {
|
||
const decisions = [];
|
||
for (const item of codeAgentCardsForSample(sample)) {
|
||
const text = normalizedText(codeAgentCardText(item));
|
||
const traceMatched = Boolean(event.traceId && item?.traceId && item.traceId === event.traceId);
|
||
const decision = {
|
||
source: item?.source || null,
|
||
role: item?.role ?? null,
|
||
dataRole: item?.dataRole ?? null,
|
||
status: item?.status ?? null,
|
||
traceId: item?.traceId ?? null,
|
||
messageId: item?.messageId ?? null,
|
||
textBytes: Buffer.byteLength(text),
|
||
valuesRedacted: true
|
||
};
|
||
if (event.traceId && item?.traceId && item.traceId !== event.traceId) {
|
||
decisions.push({ ...decision, decision: "skip-trace" });
|
||
continue;
|
||
}
|
||
if (!traceMatched && event.messageId && item?.messageId && item.messageId !== event.messageId) {
|
||
decisions.push({ ...decision, decision: "skip-message" });
|
||
continue;
|
||
}
|
||
if (!isAssistantFinalResponseCandidate(item, item?.source || "turn")) {
|
||
decisions.push({ ...decision, decision: "skip-role" });
|
||
continue;
|
||
}
|
||
if (!isCodeAgentCardTerminal(item)) {
|
||
decisions.push({ ...decision, decision: "skip-non-terminal" });
|
||
continue;
|
||
}
|
||
if (text.length < 24) {
|
||
decisions.push({ ...decision, decision: "skip-short" });
|
||
continue;
|
||
}
|
||
decisions.push({ ...decision, decision: "accept-terminal-agent-card" });
|
||
return { ok: true, decisions: decisions.slice(-8), valuesRedacted: true };
|
||
}
|
||
return { ok: false, decisions: decisions.slice(-8), valuesRedacted: true };
|
||
}
|
||
|
||
function sampleHasVisibleFinalResponse(sample, event = {}) {
|
||
for (const [groupName, group] of [["messages", sample?.messages], ["turns", sample?.turns]]) {
|
||
if (!Array.isArray(group)) continue;
|
||
for (const item of group) {
|
||
const traceMatched = Boolean(event.traceId && item?.traceId && item.traceId === event.traceId);
|
||
if (event.traceId && item?.traceId && item.traceId !== event.traceId) continue;
|
||
if (!traceMatched && event.messageId && item?.messageId && item.messageId !== event.messageId) continue;
|
||
if (!isAssistantFinalResponseCandidate(item, groupName)) continue;
|
||
const text = normalizedText([item?.text, item?.textPreview].filter(Boolean).join(" "));
|
||
if (text.length < 24) continue;
|
||
if (groupName === "messages" && isTerminalTurnStatus(item?.status)) return true;
|
||
if (isDiagnosticText(text)) continue;
|
||
if (isFinalResultText(text)) return true;
|
||
if (/运行记录/iu.test(text) && /(?:已完成|完成|新增|实现|验证|测试|结果|README|文件|summary)/iu.test(text)) return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function normalizedDomRole(item) {
|
||
return String(item?.dataRole ?? item?.role ?? item?.ariaRole ?? "")
|
||
.trim()
|
||
.toLowerCase()
|
||
.replace(/[_\s]+/gu, "-");
|
||
}
|
||
|
||
function isAssistantFinalResponseCandidate(item, groupName) {
|
||
const role = normalizedDomRole(item);
|
||
if (/agent|assistant|code-agent|bot/iu.test(role)) return true;
|
||
if (/user|human|client|prompt/iu.test(role)) return false;
|
||
if (groupName === "turns") return isCodeAgentCardLike(item);
|
||
const text = codeAgentCardText(item);
|
||
if (isTerminalTurnStatus(item?.status) && /Code Agent|运行记录|assistant|agent/iu.test(text)) return true;
|
||
return false;
|
||
}
|
||
|
||
function detectPostCompletionTimingChanges(turnTiming, event) {
|
||
const timingChanges = [];
|
||
const recentUpdateVisible = [];
|
||
if (!event.traceId && !event.messageId) return { timingChanges, recentUpdateVisible };
|
||
const rows = Array.isArray(turnTiming?.rows) ? turnTiming.rows : [];
|
||
const columns = Array.isArray(turnTiming?.columns) ? turnTiming.columns : [];
|
||
const eventTsMs = Date.parse(String(event.ts || ""));
|
||
for (const column of columns) {
|
||
if (column.pageRole && event.pageRole && column.pageRole !== event.pageRole) continue;
|
||
if (event.traceId && column.traceId && column.traceId !== event.traceId) continue;
|
||
if (event.messageId && column.messageId && column.messageId !== event.messageId) continue;
|
||
if (event.promptIndex && column.promptIndex && column.promptIndex !== event.promptIndex && column.lastPromptIndex !== event.promptIndex) continue;
|
||
let previousTotal = null;
|
||
let previousRecent = null;
|
||
for (const row of rows) {
|
||
const rowTsMs = Date.parse(String(row.ts || ""));
|
||
if (Number.isFinite(eventTsMs) && Number.isFinite(rowTsMs) && rowTsMs < eventTsMs) continue;
|
||
if (row.pageRole && event.pageRole && row.pageRole !== event.pageRole) continue;
|
||
const cell = row.cells?.[column.id];
|
||
if (!cell) continue;
|
||
if (event.traceId && cell.traceId && cell.traceId !== event.traceId) continue;
|
||
if (event.messageId && cell.messageId && cell.messageId !== event.messageId) continue;
|
||
const total = cell.totalElapsedSeconds === null || cell.totalElapsedSeconds === undefined ? NaN : Number(cell.totalElapsedSeconds);
|
||
if (Number.isFinite(total)) {
|
||
if (previousTotal && Math.abs(total - previousTotal.value) > alertThresholds.turnTimingSampleSlackSeconds) {
|
||
timingChanges.push({
|
||
...eventRef(event),
|
||
columnId: column.id,
|
||
columnLabel: column.label,
|
||
metric: "totalElapsedSeconds",
|
||
fromSeq: previousTotal.seq,
|
||
fromTs: previousTotal.ts,
|
||
fromValue: previousTotal.value,
|
||
toSeq: row.seq ?? null,
|
||
toTs: row.ts ?? null,
|
||
toValue: total,
|
||
delta: Number((total - previousTotal.value).toFixed(3)),
|
||
toleranceSeconds: alertThresholds.turnTimingSampleSlackSeconds,
|
||
traceId: cell.traceId ?? column.traceId ?? event.traceId ?? null,
|
||
messageId: cell.messageId ?? column.messageId ?? null,
|
||
valuesRedacted: true
|
||
});
|
||
}
|
||
previousTotal = { value: total, seq: row.seq ?? null, ts: row.ts ?? null };
|
||
}
|
||
const recent = cell.recentUpdateSeconds === null || cell.recentUpdateSeconds === undefined ? NaN : Number(cell.recentUpdateSeconds);
|
||
if (Number.isFinite(recent)) {
|
||
recentUpdateVisible.push({
|
||
...eventRef(event),
|
||
columnId: column.id,
|
||
columnLabel: column.label,
|
||
metric: "recentUpdateSeconds",
|
||
seq: row.seq ?? null,
|
||
ts: row.ts ?? null,
|
||
value: recent,
|
||
traceId: cell.traceId ?? column.traceId ?? event.traceId ?? null,
|
||
messageId: cell.messageId ?? column.messageId ?? null,
|
||
valuesRedacted: true
|
||
});
|
||
if (previousRecent && recent !== previousRecent.value) {
|
||
timingChanges.push({
|
||
...eventRef(event),
|
||
columnId: column.id,
|
||
columnLabel: column.label,
|
||
metric: "recentUpdateSeconds",
|
||
fromSeq: previousRecent.seq,
|
||
fromTs: previousRecent.ts,
|
||
fromValue: previousRecent.value,
|
||
toSeq: row.seq ?? null,
|
||
toTs: row.ts ?? null,
|
||
toValue: recent,
|
||
delta: Number((recent - previousRecent.value).toFixed(3)),
|
||
traceId: cell.traceId ?? column.traceId ?? event.traceId ?? null,
|
||
messageId: cell.messageId ?? column.messageId ?? null,
|
||
valuesRedacted: true
|
||
});
|
||
}
|
||
previousRecent = { value: recent, seq: row.seq ?? null, ts: row.ts ?? null };
|
||
}
|
||
}
|
||
}
|
||
return { timingChanges, recentUpdateVisible };
|
||
}
|
||
|
||
function dedupeRoundCompletionEvents(rows) {
|
||
const result = [];
|
||
const seen = new Set();
|
||
for (const row of Array.isArray(rows) ? rows : []) {
|
||
const key = [
|
||
row?.pageRole ?? "",
|
||
row?.pageId ?? "",
|
||
row?.promptIndex ?? "",
|
||
row?.traceId ?? "",
|
||
row?.messageId ?? "",
|
||
row?.textHash ?? "",
|
||
row?.elapsedSeconds ?? ""
|
||
].join("|");
|
||
if (seen.has(key)) continue;
|
||
seen.add(key);
|
||
result.push(row);
|
||
}
|
||
return result;
|
||
}
|
||
|
||
function eventRef(event) {
|
||
return {
|
||
seq: event?.seq ?? null,
|
||
sampleGroupSeq: event?.sampleGroupSeq ?? null,
|
||
ts: event?.ts ?? null,
|
||
pageRole: event?.pageRole ?? null,
|
||
pageId: event?.pageId ?? null,
|
||
routeSessionId: event?.routeSessionId ?? null,
|
||
activeSessionId: event?.activeSessionId ?? null,
|
||
promptIndex: event?.promptIndex ?? null,
|
||
traceId: event?.traceId ?? null,
|
||
messageId: event?.messageId ?? null,
|
||
};
|
||
}
|
||
|
||
function dedupeRoundCompletionRows(rows) {
|
||
const result = [];
|
||
const seen = new Set();
|
||
for (const row of Array.isArray(rows) ? rows : []) {
|
||
const key = [
|
||
row?.seq ?? row?.fromSeq ?? "",
|
||
row?.toSeq ?? "",
|
||
row?.pageRole ?? "",
|
||
row?.promptIndex ?? "",
|
||
row?.traceId ?? "",
|
||
row?.metric ?? "",
|
||
row?.textHash ?? "",
|
||
row?.columnId ?? ""
|
||
].join("|");
|
||
if (seen.has(key)) continue;
|
||
seen.add(key);
|
||
result.push(row);
|
||
}
|
||
return result;
|
||
}
|
||
|
||
function turnMetricItems(sample, timelineItem) {
|
||
const promptIndex = timelineItem.promptIndex ?? 0;
|
||
const pageRole = sample?.pageRole || "control";
|
||
const pageId = sample?.pageId || "unknown-page";
|
||
const sessionKey = pageRole + ":" + pageId + ":" + (sample?.routeSessionId || sample?.activeSessionId || "unknown-session");
|
||
const roundKey = String(promptIndex);
|
||
const items = [];
|
||
if (Array.isArray(sample?.turns) && sample.turns.length > 0) {
|
||
for (const turn of sample.turns) {
|
||
const texts = turnTexts(turn);
|
||
const totalElapsedValues = texts.flatMap(parseTotalElapsedSeconds).filter(Number.isFinite);
|
||
const recentUpdateValues = texts.flatMap(parseRecentUpdateSeconds).filter(Number.isFinite);
|
||
const traceId = turn.traceId || firstTraceId(texts);
|
||
const messageId = turn.messageId || null;
|
||
const turnId = turn.turnId || traceId || null;
|
||
const stableId = traceId || messageId || turnId || null;
|
||
const domIndex = Number.isFinite(Number(turn.index)) ? Number(turn.index) : items.length;
|
||
const key = stableId
|
||
? "timing:" + sessionKey + ":id-" + stableId
|
||
: "turn:" + sessionKey + ":round-" + roundKey + ":dom-index-" + String(domIndex);
|
||
items.push({
|
||
key,
|
||
source: "turn",
|
||
pageRole,
|
||
pageId,
|
||
promptIndex,
|
||
traceId,
|
||
messageId,
|
||
turnId,
|
||
domIndex,
|
||
status: turn.status ?? null,
|
||
totalElapsedSeconds: totalElapsedValues.length > 0 ? Math.max(...totalElapsedValues) : null,
|
||
recentUpdateSeconds: recentUpdateValues.length > 0 ? Math.max(...recentUpdateValues) : null,
|
||
textHash: turn.textHash || sha256(texts.join("\n"))
|
||
});
|
||
}
|
||
return items;
|
||
}
|
||
if (Array.isArray(sample?.messages) && sample.messages.length > 0) {
|
||
for (const message of sample.messages) {
|
||
const text = [message?.durationText, message?.activityText].map((value) => String(value || "")).filter((value) => value.trim().length > 0).join("\n");
|
||
const totalElapsedValues = parseTotalElapsedSeconds(text).filter(Number.isFinite);
|
||
const recentUpdateValues = parseRecentUpdateSeconds(text).filter(Number.isFinite);
|
||
if (totalElapsedValues.length === 0 && recentUpdateValues.length === 0) continue;
|
||
const domIndex = Number.isFinite(Number(message.index)) ? Number(message.index) : items.length;
|
||
const traceId = message.traceId || firstTraceId([text]);
|
||
const messageId = message.messageId || null;
|
||
const stableId = traceId || messageId || message.turnId || null;
|
||
items.push({
|
||
key: stableId
|
||
? "timing:" + sessionKey + ":id-" + stableId
|
||
: "message:" + sessionKey + ":round-" + roundKey + ":dom-index-" + String(domIndex),
|
||
source: "message",
|
||
pageRole,
|
||
pageId,
|
||
promptIndex,
|
||
traceId,
|
||
messageId,
|
||
turnId: message.turnId || traceId || null,
|
||
domIndex,
|
||
status: message.status ?? null,
|
||
totalElapsedSeconds: totalElapsedValues.length > 0 ? Math.max(...totalElapsedValues) : null,
|
||
recentUpdateSeconds: recentUpdateValues.length > 0 ? Math.max(...recentUpdateValues) : null,
|
||
textHash: message.textHash || sha256(text)
|
||
});
|
||
}
|
||
if (items.length > 0) return items;
|
||
}
|
||
if (timelineItem.totalElapsedSeconds !== null || timelineItem.recentUpdateSeconds !== null) {
|
||
return [{
|
||
key: "aggregate:" + sessionKey + ":round-" + roundKey,
|
||
source: "aggregate",
|
||
pageRole,
|
||
pageId,
|
||
promptIndex,
|
||
traceId: null,
|
||
messageId: null,
|
||
domIndex: null,
|
||
status: null,
|
||
totalElapsedSeconds: timelineItem.totalElapsedSeconds ?? null,
|
||
recentUpdateSeconds: timelineItem.recentUpdateSeconds ?? null,
|
||
textHash: timelineItem.textDigest ?? null
|
||
}];
|
||
}
|
||
return [];
|
||
}
|
||
|
||
function turnTexts(turn) {
|
||
return [
|
||
turn?.durationText,
|
||
turn?.activityText
|
||
].map((value) => String(value || "")).filter((value) => value.trim().length > 0);
|
||
}
|
||
|
||
function firstTraceId(texts) {
|
||
for (const text of texts) {
|
||
const match = String(text || "").match(/\btrc_[A-Za-z0-9_-]+\b/u);
|
||
if (match) return match[0];
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function buildRoundMetricSummaries(timeline, promptCommands, timing = {}) {
|
||
const rounds = [];
|
||
const timingColumns = Array.isArray(timing.columns) ? timing.columns : [];
|
||
const timingRows = Array.isArray(timing.rows) ? timing.rows : [];
|
||
const nonMonotonic = Array.isArray(timing.nonMonotonic) ? timing.nonMonotonic : [];
|
||
const totalElapsedForwardJumps = Array.isArray(timing.totalElapsedForwardJumps) ? timing.totalElapsedForwardJumps : [];
|
||
const elapsedZeroResets = Array.isArray(timing.elapsedZeroResets) ? timing.elapsedZeroResets : [];
|
||
const terminalElapsedGrowth = Array.isArray(timing.terminalElapsedGrowth) ? timing.terminalElapsedGrowth : [];
|
||
const recentUpdateResets = Array.isArray(timing.recentUpdateResets) ? timing.recentUpdateResets : [];
|
||
const recentUpdateSteps = Array.isArray(timing.recentUpdateSteps) ? timing.recentUpdateSteps : [];
|
||
for (let index = 0; index < promptCommands.length; index += 1) {
|
||
const promptIndex = index + 1;
|
||
const items = timeline.filter((item) => item.promptIndex === promptIndex);
|
||
const aggregateTotalElapsed = items.map((item) => item.totalElapsedSeconds).filter((value) => value !== null);
|
||
const aggregateRecentUpdate = items.map((item) => item.recentUpdateSeconds).filter((value) => value !== null);
|
||
const promptTurnTiming = roundPromptTurnTimingValues(timingRows, timingColumns, promptIndex, {
|
||
firstSeq: items[0]?.seq ?? null,
|
||
firstSampleAt: items[0]?.ts ?? null
|
||
});
|
||
const totalElapsed = promptTurnTiming.totalElapsed.length > 0 ? promptTurnTiming.totalElapsed : aggregateTotalElapsed;
|
||
const recentUpdate = promptTurnTiming.recentUpdate.length > 0 ? promptTurnTiming.recentUpdate : aggregateRecentUpdate;
|
||
const loadingCounts = items.map((item) => Number(item.loadingCount ?? 0)).filter(Number.isFinite);
|
||
const loadingOwners = new Set();
|
||
for (const item of items) {
|
||
for (const owner of Array.isArray(item.loadingOwners) ? item.loadingOwners : []) {
|
||
if (owner?.ownerKey) loadingOwners.add(owner.ownerKey);
|
||
}
|
||
}
|
||
const timingAnomalies = nonMonotonic.filter((item) => item.promptIndex === promptIndex);
|
||
const timingForwardJumps = totalElapsedForwardJumps.filter((item) => item.promptIndex === promptIndex);
|
||
const timingZeroResets = elapsedZeroResets.filter((item) => item.promptIndex === promptIndex);
|
||
const timingTerminalGrowth = terminalElapsedGrowth.filter((item) => item.promptIndex === promptIndex);
|
||
const timingResets = recentUpdateResets.filter((item) => item.promptIndex === promptIndex);
|
||
const timingSteps = recentUpdateSteps.filter((item) => item.promptIndex === promptIndex);
|
||
const terminalGrowthDeltas = timingTerminalGrowth.map((item) => Number(item.delta)).filter((value) => Number.isFinite(value) && value > 0);
|
||
const timingStepDeltas = timingSteps.map((item) => Number(item.delta)).filter((value) => Number.isFinite(value) && value >= 0);
|
||
const timingStepExcess = timingSteps.map((item) => Number(item.excessiveIncreaseSeconds)).filter((value) => Number.isFinite(value) && value > 0);
|
||
rounds.push({
|
||
promptIndex,
|
||
promptCommandId: promptCommands[index].commandId,
|
||
promptTextHash: promptCommands[index].textHash,
|
||
promptTextBytes: promptCommands[index].textBytes,
|
||
promptCompletedAt: promptCommands[index].ts,
|
||
sampleCount: items.length,
|
||
firstSeq: items[0]?.seq ?? null,
|
||
lastSeq: items[items.length - 1]?.seq ?? null,
|
||
firstSampleAt: items[0]?.ts ?? null,
|
||
lastSampleAt: items[items.length - 1]?.ts ?? null,
|
||
withTotalElapsed: totalElapsed.length,
|
||
withRecentUpdate: recentUpdate.length,
|
||
loadingSamples: loadingCounts.filter((value) => value > 0).length,
|
||
maxLoadingCount: loadingCounts.length > 0 ? Math.max(...loadingCounts) : 0,
|
||
loadingOwnerCount: loadingOwners.size,
|
||
maxTotalElapsedSeconds: totalElapsed.length > 0 ? Math.max(...totalElapsed) : null,
|
||
lastTotalElapsedSeconds: lastNonNull(totalElapsed),
|
||
maxRecentUpdateSeconds: recentUpdate.length > 0 ? Math.max(...recentUpdate) : null,
|
||
lastRecentUpdateSeconds: lastNonNull(recentUpdate),
|
||
diagnosticSamples: items.filter((item) => item.diagnosticSeen).length,
|
||
terminalSamples: items.filter((item) => item.terminalSeen).length,
|
||
finalTextSamples: items.filter((item) => item.finalResultTextSeen).length,
|
||
turnTimingNonMonotonicCount: timingAnomalies.length,
|
||
turnTimingTotalElapsedDecreaseCount: timingAnomalies.filter((item) => item.metric === "totalElapsedSeconds").length,
|
||
turnTimingTotalElapsedZeroResetCount: timingZeroResets.length,
|
||
turnTimingTotalElapsedForwardJumpCount: timingForwardJumps.length,
|
||
turnTimingTotalElapsedForwardJumpMaxSeconds: maxPositiveDelta(timingForwardJumps),
|
||
turnTimingTerminalElapsedGrowthCount: timingTerminalGrowth.length,
|
||
turnTimingTerminalElapsedGrowthMaxSeconds: terminalGrowthDeltas.length > 0 ? Math.max(...terminalGrowthDeltas) : 0,
|
||
turnTimingRecentUpdateJumpCount: timingAnomalies.filter((item) => item.metric === "recentUpdateSeconds" && item.anomaly === "jump").length,
|
||
turnTimingRecentUpdateSawtoothJumpCount: timingAnomalies.filter((item) => item.metric === "recentUpdateSeconds" && item.anomaly === "jump").length,
|
||
turnTimingRecentUpdateStepCount: timingSteps.length,
|
||
turnTimingRecentUpdateMaxIncreaseSeconds: timingStepDeltas.length > 0 ? Math.max(...timingStepDeltas) : null,
|
||
turnTimingRecentUpdateMaxExcessSeconds: timingStepExcess.length > 0 ? Math.max(...timingStepExcess) : 0,
|
||
turnTimingRecentUpdateResetCount: timingResets.length,
|
||
turnTimingRecentUpdateDecreaseCount: timingResets.length
|
||
});
|
||
}
|
||
return rounds;
|
||
}
|
||
|
||
function roundPromptTurnTimingValues(rows, columns, promptIndex, round = {}) {
|
||
const roundFirstSeq = Number(round.firstSeq);
|
||
const roundFirstMs = Date.parse(String(round.firstSampleAt ?? ""));
|
||
const seqSlack = 3;
|
||
const timeSlackMs = 3000;
|
||
const promptColumnIds = new Set(columns
|
||
.filter((column) => {
|
||
if (Number(column?.promptIndex) === Number(promptIndex)) return true;
|
||
const firstSeq = Number(column?.firstSeq);
|
||
if (Number.isFinite(roundFirstSeq) && Number.isFinite(firstSeq) && firstSeq >= roundFirstSeq - seqSlack) return true;
|
||
const firstMs = Date.parse(String(column?.firstTs ?? ""));
|
||
return Number.isFinite(roundFirstMs) && Number.isFinite(firstMs) && firstMs >= roundFirstMs - timeSlackMs;
|
||
})
|
||
.map((column) => String(column?.id || ""))
|
||
.filter(Boolean));
|
||
if (promptColumnIds.size === 0) return { totalElapsed: [], recentUpdate: [] };
|
||
const totalElapsed = [];
|
||
const recentUpdate = [];
|
||
for (const row of rows) {
|
||
if (Number(row?.promptIndex ?? 0) !== Number(promptIndex)) continue;
|
||
const cells = row?.cells && typeof row.cells === "object" ? row.cells : {};
|
||
for (const [columnId, cell] of Object.entries(cells)) {
|
||
if (!promptColumnIds.has(columnId)) continue;
|
||
const total = Number(cell?.totalElapsedSeconds);
|
||
if (Number.isFinite(total)) totalElapsed.push(total);
|
||
const recent = Number(cell?.recentUpdateSeconds);
|
||
if (Number.isFinite(recent)) recentUpdate.push(recent);
|
||
}
|
||
}
|
||
return { totalElapsed, recentUpdate };
|
||
}
|
||
|
||
function sampleTexts(sample) {
|
||
const rows = [];
|
||
for (const group of [sample?.messages, sample?.traceRows, sample?.diagnostics]) {
|
||
if (!Array.isArray(group)) continue;
|
||
for (const item of group) {
|
||
const text = String(item?.textPreview || "");
|
||
if (text.trim()) rows.push(text);
|
||
}
|
||
}
|
||
return rows;
|
||
}
|
||
|
||
function sampleTurnTimingTexts(sample) {
|
||
const rows = [];
|
||
for (const group of [sample?.turns, sample?.messages]) {
|
||
if (!Array.isArray(group)) continue;
|
||
for (const item of group) {
|
||
for (const value of [item?.durationText, item?.activityText]) {
|
||
const text = String(value || "");
|
||
if (text.trim()) rows.push(text);
|
||
}
|
||
}
|
||
}
|
||
return rows;
|
||
}
|
||
|
||
function parseTotalElapsedSeconds(text) {
|
||
const values = [];
|
||
for (const match of String(text || "").matchAll(/(?:总耗时|耗时)\s*[=::]?\s*(\d{1,2}):(\d{2}):(\d{2})/giu)) {
|
||
values.push(Number(match[1]) * 3600 + Number(match[2]) * 60 + Number(match[3]));
|
||
}
|
||
for (const match of String(text || "").matchAll(/(?:总耗时|耗时)\s*[=::]?\s*(\d{1,2}):(\d{2})(?!:)/giu)) {
|
||
values.push(Number(match[1]) * 60 + Number(match[2]));
|
||
}
|
||
for (const match of String(text || "").matchAll(/(?:总耗时|耗时)\s*[=::]?\s*(?:(\d+)\s*天)?\s*(?:(\d+)\s*小时)?\s*(?:(\d+)\s*(?:分钟|分))?\s*(?:(\d+)\s*秒)?/giu)) {
|
||
const days = Number(match[1] || 0);
|
||
const hours = Number(match[2] || 0);
|
||
const minutes = Number(match[3] || 0);
|
||
const seconds = Number(match[4] || 0);
|
||
if (days || hours || minutes || seconds || /(?:天|小时|分钟|分|秒)/u.test(match[0] || "")) values.push(days * 86400 + hours * 3600 + minutes * 60 + seconds);
|
||
}
|
||
return values;
|
||
}
|
||
|
||
function lastNonNull(values) {
|
||
for (let index = values.length - 1; index >= 0; index -= 1) if (values[index] !== null && values[index] !== undefined) return values[index];
|
||
return null;
|
||
}
|
||
|
||
function parseRecentUpdateSeconds(text) {
|
||
const values = [];
|
||
for (const match of String(text || "").matchAll(/最近\s*(?:(\d+)\s*天)?\s*(?:(\d+)\s*小时)?\s*(?:(\d+)\s*(?:分钟|分))?\s*(?:(\d+)\s*秒)?\s*前/giu)) {
|
||
const days = Number(match[1] || 0);
|
||
const hours = Number(match[2] || 0);
|
||
const minutes = Number(match[3] || 0);
|
||
const seconds = Number(match[4] || 0);
|
||
values.push(days * 86400 + hours * 3600 + minutes * 60 + seconds);
|
||
}
|
||
return values;
|
||
}
|
||
|
||
function latestPromptIndex(promptTimes, tsMs) {
|
||
let index = 0;
|
||
for (let i = 0; i < promptTimes.length; i += 1) {
|
||
if (promptTimes[i] <= tsMs) index = i + 1;
|
||
else break;
|
||
}
|
||
return index;
|
||
}
|
||
|
||
function promptIndexForTs(promptTimes, ts) {
|
||
const tsMs = Date.parse(ts);
|
||
return Number.isFinite(tsMs) ? latestPromptIndex(promptTimes, tsMs) : 0;
|
||
}
|
||
|
||
function buildTransitions(samples) {
|
||
const rows = [];
|
||
let last = null;
|
||
for (const sample of samples) {
|
||
const digest = digestSample(sample);
|
||
if (digest !== last) {
|
||
rows.push({ seq: sample.seq, ts: sample.ts, url: sample.url, routeSessionId: sample.routeSessionId || null, activeSessionId: sample.activeSessionId || null, messageCount: Array.isArray(sample.messages) ? sample.messages.length : 0, traceRowCount: Array.isArray(sample.traceRows) ? sample.traceRows.length : 0, digest });
|
||
last = digest;
|
||
}
|
||
}
|
||
return rows.slice(0, 200);
|
||
}
|
||
|
||
function detectFinalFlicker(samples) {
|
||
const flickers = [];
|
||
const lastNonEmptyByScope = new Map();
|
||
for (const sample of samples) {
|
||
const scope = finalFlickerScope(sample);
|
||
if (!scope) continue;
|
||
const messageText = Array.isArray(sample.messages) ? sample.messages.map((item) => item.textPreview || "").join("\n") : "";
|
||
const nonEmpty = messageText.trim().length > 0;
|
||
const finalLike = /轮次完成|已记录|已完成第\d+轮|final response|terminal result/iu.test(messageText);
|
||
const diagnosticLike = /temporarily|timeout|无法连接|暂时|error|failed|超时/iu.test(messageText);
|
||
const lastNonEmpty = lastNonEmptyByScope.get(scope);
|
||
if (nonEmpty && finalLike && !diagnosticLike) lastNonEmptyByScope.set(scope, { sample, messageText });
|
||
if (lastNonEmpty && nonEmpty && diagnosticLike) flickers.push({ scope, from: ref(lastNonEmpty.sample), to: ref(sample) });
|
||
if (lastNonEmpty && !nonEmpty) flickers.push({ scope, from: ref(lastNonEmpty.sample), to: ref(sample), reason: "text-disappeared" });
|
||
}
|
||
return flickers;
|
||
}
|
||
|
||
function finalFlickerScope(sample) {
|
||
const pathname = samplePathname(sample);
|
||
const sessionId = sample?.routeSessionId || sample?.activeSessionId || workbenchSessionIdFromPath(pathname);
|
||
if (!sessionId) return null;
|
||
if (!pathname.startsWith("/workbench/sessions/" + sessionId)) return null;
|
||
return (sample?.pageRole || "control") + ":" + sessionId;
|
||
}
|
||
|
||
function samplePathname(sample) {
|
||
const raw = String(sample?.path || sample?.url || "").trim();
|
||
if (!raw) return "";
|
||
try {
|
||
return new URL(raw, "http://hwlab.local").pathname || raw;
|
||
} catch {
|
||
return raw.split(/[?#]/u, 1)[0] || raw;
|
||
}
|
||
}
|
||
|
||
function workbenchSessionIdFromPath(pathname) {
|
||
const match = String(pathname || "").match(/^\/workbench\/sessions\/([^/?#]+)/u);
|
||
return match ? match[1] : null;
|
||
}
|
||
|
||
function detectTerminalZeroElapsed(samples) {
|
||
const rows = [];
|
||
for (const sample of samples) {
|
||
const turns = Array.isArray(sample?.turns) ? sample.turns : [];
|
||
const messages = Array.isArray(sample?.messages) ? sample.messages : [];
|
||
for (const item of [...turns, ...messages]) {
|
||
const text = [item?.durationText, item?.activityText, item?.status, item?.textPreview, item?.text].filter(Boolean).join("\n");
|
||
if (!/(?:Code Agent|运行记录|completed|failed|canceled|blocked)/iu.test(text)) continue;
|
||
if (!/(?:completed|failed|canceled|blocked|已完成|失败|取消)/iu.test(text)) continue;
|
||
const timingText = [item?.durationText, item?.activityText].filter(Boolean).join("\n");
|
||
const elapsedValues = parseTotalElapsedSeconds(timingText);
|
||
if (!elapsedValues.includes(0)) continue;
|
||
rows.push({
|
||
...ref(sample),
|
||
status: item?.status ?? null,
|
||
messageId: item?.messageId ?? null,
|
||
traceId: item?.traceId ?? null,
|
||
durationText: item?.durationText ?? null,
|
||
activityText: item?.activityText ?? null,
|
||
textPreview: limitText(item?.textPreview || item?.text || "", 180)
|
||
});
|
||
}
|
||
}
|
||
return rows;
|
||
}
|
||
|
||
function detectCrossPageProjectionDiffs(samples) {
|
||
const groups = new Map();
|
||
for (const sample of samples) {
|
||
const key = sample?.sampleGroupSeq ?? sample?.seq;
|
||
if (key === null || key === undefined) continue;
|
||
const group = groups.get(key) || {};
|
||
if (sample?.pageRole === "control") group.control = sample;
|
||
else if (sample?.pageRole === "observer") group.observer = sample;
|
||
groups.set(key, group);
|
||
}
|
||
const rows = [];
|
||
for (const [sampleGroupSeq, group] of groups.entries()) {
|
||
const control = group.control;
|
||
const observer = group.observer;
|
||
if (!control || !observer) continue;
|
||
const controlTraceIds = visibleTraceIds(control);
|
||
const observerTraceIds = visibleTraceIds(observer);
|
||
const missingInObserver = [...controlTraceIds].filter((item) => !observerTraceIds.has(item));
|
||
const controlMessages = Array.isArray(control.messages) ? control.messages.length : 0;
|
||
const observerMessages = Array.isArray(observer.messages) ? observer.messages.length : 0;
|
||
const controlTraceRows = Array.isArray(control.traceRows) ? control.traceRows.length : 0;
|
||
const observerTraceRows = Array.isArray(observer.traceRows) ? observer.traceRows.length : 0;
|
||
const controlZero = detectTerminalZeroElapsed([control]).length > 0;
|
||
const observerZero = detectTerminalZeroElapsed([observer]).length > 0;
|
||
const sameSession = (control.routeSessionId || control.activeSessionId || null) && (control.routeSessionId || control.activeSessionId || null) === (observer.routeSessionId || observer.activeSessionId || null);
|
||
const controlMessageDigest = digestMessageTexts(control);
|
||
const observerMessageDigest = digestMessageTexts(observer);
|
||
const messageTextDigestDiff = controlMessageDigest !== observerMessageDigest;
|
||
const projectionDivergent = sameSession && (Math.abs(controlMessages - observerMessages) > 0 || controlZero !== observerZero);
|
||
const traceVisibilityDivergent = sameSession && !projectionDivergent && (missingInObserver.length > 0 || Math.abs(controlTraceRows - observerTraceRows) > 0);
|
||
if (!projectionDivergent && !traceVisibilityDivergent) continue;
|
||
rows.push({
|
||
sampleGroupSeq,
|
||
diffKind: traceVisibilityDivergent ? "trace-visibility" : "projection",
|
||
control: ref(control),
|
||
observer: ref(observer),
|
||
controlTraceIds: [...controlTraceIds].slice(0, 8),
|
||
observerTraceIds: [...observerTraceIds].slice(0, 8),
|
||
missingTraceIdsInObserver: missingInObserver.slice(0, 8),
|
||
controlMessageCount: controlMessages,
|
||
observerMessageCount: observerMessages,
|
||
controlTraceRowCount: controlTraceRows,
|
||
observerTraceRowCount: observerTraceRows,
|
||
terminalZeroElapsedDiff: controlZero !== observerZero,
|
||
messageTextDigestDiff,
|
||
controlMessageDigest,
|
||
observerMessageDigest
|
||
});
|
||
}
|
||
return rows;
|
||
}
|
||
|
||
function mergeCrossPageDiffRows(...groups) {
|
||
const rows = [];
|
||
const seen = new Set();
|
||
for (const group of groups) {
|
||
if (!Array.isArray(group)) continue;
|
||
for (const row of group) {
|
||
const key = [
|
||
row?.diffKind || "projection",
|
||
row?.control?.seq ?? null,
|
||
row?.observer?.seq ?? null,
|
||
row?.controlMessageCount ?? null,
|
||
row?.observerMessageCount ?? null,
|
||
row?.controlTraceRowCount ?? null,
|
||
row?.observerTraceRowCount ?? null
|
||
].join(":");
|
||
if (seen.has(key)) continue;
|
||
seen.add(key);
|
||
rows.push(row);
|
||
}
|
||
}
|
||
return rows;
|
||
}
|
||
|
||
function annotateCrossPageDiffTiming(rows) {
|
||
const groups = new Map();
|
||
for (const row of Array.isArray(rows) ? rows : []) {
|
||
const controlAt = Date.parse(String(row?.control?.ts || ""));
|
||
const observerAt = Date.parse(String(row?.observer?.ts || ""));
|
||
const timestamps = [controlAt, observerAt].filter(Number.isFinite);
|
||
const startMs = timestamps.length > 0 ? Math.min(...timestamps) : null;
|
||
const endMs = timestamps.length > 0 ? Math.max(...timestamps) : null;
|
||
const sessionId = row?.control?.routeSessionId || row?.control?.activeSessionId || row?.observer?.routeSessionId || row?.observer?.activeSessionId || "unknown-session";
|
||
const key = [row?.diffKind || "projection", sessionId].join(":");
|
||
const group = groups.get(key) || { rows: [], firstMs: null, lastMs: null };
|
||
const annotated = {
|
||
...row,
|
||
sampleStartAt: startMs === null ? null : new Date(startMs).toISOString(),
|
||
sampleEndAt: endMs === null ? null : new Date(endMs).toISOString(),
|
||
pairSkewMs: startMs === null || endMs === null ? null : endMs - startMs,
|
||
};
|
||
group.rows.push(annotated);
|
||
if (startMs !== null && (group.firstMs === null || startMs < group.firstMs)) group.firstMs = startMs;
|
||
if (endMs !== null && (group.lastMs === null || endMs > group.lastMs)) group.lastMs = endMs;
|
||
groups.set(key, group);
|
||
}
|
||
const result = [];
|
||
for (const group of groups.values()) {
|
||
const sortedRows = group.rows.slice().sort((a, b) => Number(Date.parse(String(a.sampleStartAt || ""))) - Number(Date.parse(String(b.sampleStartAt || ""))));
|
||
const segments = [];
|
||
const splitGapMs = Math.max(1000, Number(alertThresholds.crossPageProjectionDivergenceRedMs || alertThresholds.visibleLoadingSlowMs || 10_000));
|
||
for (const row of sortedRows) {
|
||
const startMs = Date.parse(String(row.sampleStartAt || ""));
|
||
const endMs = Date.parse(String(row.sampleEndAt || row.sampleStartAt || ""));
|
||
const last = segments.at(-1);
|
||
const lastEndMs = last && last.lastMs !== null ? last.lastMs : null;
|
||
if (!last || (Number.isFinite(startMs) && lastEndMs !== null && startMs - lastEndMs > splitGapMs)) {
|
||
segments.push({ rows: [row], firstMs: Number.isFinite(startMs) ? startMs : null, lastMs: Number.isFinite(endMs) ? endMs : Number.isFinite(startMs) ? startMs : null });
|
||
continue;
|
||
}
|
||
last.rows.push(row);
|
||
if (Number.isFinite(startMs) && (last.firstMs === null || startMs < last.firstMs)) last.firstMs = startMs;
|
||
const effectiveEndMs = Number.isFinite(endMs) ? endMs : Number.isFinite(startMs) ? startMs : null;
|
||
if (effectiveEndMs !== null && (last.lastMs === null || effectiveEndMs > last.lastMs)) last.lastMs = effectiveEndMs;
|
||
}
|
||
for (let segmentIndex = 0; segmentIndex < segments.length; segmentIndex += 1) {
|
||
const segment = segments[segmentIndex];
|
||
const observedSpanMs = segment.firstMs === null || segment.lastMs === null ? null : segment.lastMs - segment.firstMs;
|
||
for (const row of segment.rows) {
|
||
result.push({
|
||
...row,
|
||
segmentIndex,
|
||
observedFirstAt: segment.firstMs === null ? null : new Date(segment.firstMs).toISOString(),
|
||
observedLastAt: segment.lastMs === null ? null : new Date(segment.lastMs).toISOString(),
|
||
observedSpanMs,
|
||
});
|
||
}
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
|
||
function detectAdjacentCrossPageProjectionDiffs(samples) {
|
||
const rows = [];
|
||
const ordered = (Array.isArray(samples) ? samples : []).slice().sort((a, b) => Number(a?.seq ?? 0) - Number(b?.seq ?? 0));
|
||
for (let i = 1; i < ordered.length; i += 1) {
|
||
const a = ordered[i - 1];
|
||
const b = ordered[i];
|
||
const roles = new Set([a?.pageRole, b?.pageRole]);
|
||
if (!roles.has("control") || !roles.has("observer")) continue;
|
||
const control = a?.pageRole === "control" ? a : b;
|
||
const observer = a?.pageRole === "observer" ? a : b;
|
||
const controlSession = control.routeSessionId || control.activeSessionId || null;
|
||
const observerSession = observer.routeSessionId || observer.activeSessionId || null;
|
||
if (!controlSession || controlSession !== observerSession) continue;
|
||
const controlAt = Date.parse(String(control.ts || ""));
|
||
const observerAt = Date.parse(String(observer.ts || ""));
|
||
if (Number.isFinite(controlAt) && Number.isFinite(observerAt) && Math.abs(controlAt - observerAt) > 1500) continue;
|
||
const controlMessages = Array.isArray(control.messages) ? control.messages.length : 0;
|
||
const observerMessages = Array.isArray(observer.messages) ? observer.messages.length : 0;
|
||
const controlTraceRows = Array.isArray(control.traceRows) ? control.traceRows.length : 0;
|
||
const observerTraceRows = Array.isArray(observer.traceRows) ? observer.traceRows.length : 0;
|
||
const controlZero = detectTerminalZeroElapsed([control]).length > 0;
|
||
const observerZero = detectTerminalZeroElapsed([observer]).length > 0;
|
||
const missingInObserver = [...visibleTraceIds(control)].filter((item) => !visibleTraceIds(observer).has(item));
|
||
const controlTraceIds = visibleTraceIds(control);
|
||
const observerTraceIds = visibleTraceIds(observer);
|
||
const controlMessageDigest = digestMessageTexts(control);
|
||
const observerMessageDigest = digestMessageTexts(observer);
|
||
const messageTextDigestDiff = controlMessageDigest !== observerMessageDigest;
|
||
const projectionDivergent = controlMessages !== observerMessages || controlZero !== observerZero;
|
||
const traceVisibilityDivergent = !projectionDivergent && (missingInObserver.length > 0 || controlTraceRows !== observerTraceRows);
|
||
if (!projectionDivergent && !traceVisibilityDivergent) continue;
|
||
rows.push({
|
||
sampleGroupSeq: control.sampleGroupSeq ?? observer.sampleGroupSeq ?? null,
|
||
adjacentPair: true,
|
||
diffKind: traceVisibilityDivergent ? "trace-visibility" : "projection",
|
||
control: ref(control),
|
||
observer: ref(observer),
|
||
controlTraceIds: [...controlTraceIds].slice(0, 8),
|
||
observerTraceIds: [...observerTraceIds].slice(0, 8),
|
||
missingTraceIdsInObserver: missingInObserver.slice(0, 8),
|
||
controlMessageCount: controlMessages,
|
||
observerMessageCount: observerMessages,
|
||
controlTraceRowCount: controlTraceRows,
|
||
observerTraceRowCount: observerTraceRows,
|
||
terminalZeroElapsedDiff: controlZero !== observerZero,
|
||
messageTextDigestDiff,
|
||
controlMessageDigest,
|
||
observerMessageDigest
|
||
});
|
||
}
|
||
return rows;
|
||
}
|
||
|
||
function detectTraceMessageDuplication(samples) {
|
||
const rows = [];
|
||
for (const sample of samples) {
|
||
const traceRows = Array.isArray(sample?.traceRows) ? sample.traceRows : [];
|
||
const groups = new Map();
|
||
for (let fallbackIndex = 0; fallbackIndex < traceRows.length; fallbackIndex += 1) {
|
||
const row = traceRows[fallbackIndex];
|
||
const rowTextRaw = String(row?.textPreview || row?.text || "");
|
||
if (!/(?:助手消息|assistant\s+message|assistant)/iu.test(rowTextRaw)) continue;
|
||
const rowText = normalizedText(rowTextRaw);
|
||
if (rowText.length < 24) continue;
|
||
const traceId = row?.traceId === undefined || row?.traceId === null ? "" : String(row.traceId);
|
||
const key = traceId + "\u0000" + rowText;
|
||
const group = groups.get(key) ?? { traceId: traceId || null, rowText, rowTextRaw, rows: [] };
|
||
group.rows.push({ row, fallbackIndex });
|
||
groups.set(key, group);
|
||
}
|
||
for (const group of groups.values()) {
|
||
if (group.rows.length < 2) continue;
|
||
rows.push({
|
||
...ref(sample),
|
||
traceId: group.traceId,
|
||
visibleAssistantFinalRowCount: group.rows.length,
|
||
rowIndexes: group.rows.map((item) => item.row?.index ?? item.fallbackIndex).slice(0, 12),
|
||
rowTextHash: sha256(group.rowText),
|
||
rowTextPreview: limitText(group.rowTextRaw, 180),
|
||
finalResponseSummaryBlockCounted: false,
|
||
traceFrameSource: "traceRows-only"
|
||
});
|
||
}
|
||
}
|
||
return rows;
|
||
}
|
||
|
||
function detectTurnTraceIdMissing(samples) {
|
||
const rows = [];
|
||
for (const sample of samples) {
|
||
for (const item of Array.isArray(sample?.turns) ? sample.turns : []) {
|
||
const text = [item?.status, item?.durationText, item?.activityText, item?.textPreview, item?.text].filter(Boolean).join("\n");
|
||
if (!/(?:Code Agent|运行记录|耗时|最近)/iu.test(text)) continue;
|
||
if (item?.traceId) continue;
|
||
rows.push({ ...ref(sample), status: item?.status ?? null, messageId: item?.messageId ?? null, textPreview: limitText(item?.textPreview || item?.text || "", 180) });
|
||
}
|
||
}
|
||
return rows;
|
||
}
|
||
|
||
function visibleTraceIds(sample) {
|
||
const ids = new Set();
|
||
for (const group of [sample?.turns, sample?.messages, sample?.traceRows]) {
|
||
if (!Array.isArray(group)) continue;
|
||
for (const item of group) if (item?.traceId) ids.add(String(item.traceId));
|
||
}
|
||
return ids;
|
||
}
|
||
|
||
function digestMessageTexts(sample) {
|
||
return sha256((Array.isArray(sample?.messages) ? sample.messages : []).map((item) => item?.textHash || normalizedText(item?.textPreview || item?.text || "")).join("|"));
|
||
}
|
||
|
||
function normalizedText(value) {
|
||
return String(value || "").replace(/\s+/gu, " ").trim();
|
||
}
|
||
|
||
function longestSharedSubstringLength(a, b) {
|
||
if (!a || !b) return 0;
|
||
const left = a.length <= b.length ? a : b;
|
||
const right = a.length <= b.length ? b : a;
|
||
const max = Math.min(left.length, 280);
|
||
let best = 0;
|
||
for (let start = 0; start < max; start += 1) {
|
||
for (let end = Math.min(max, start + 160); end > start + best; end -= 1) {
|
||
if (right.includes(left.slice(start, end))) {
|
||
best = end - start;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
return best;
|
||
}
|
||
|
||
function digestSample(sample) {
|
||
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);
|
||
}
|
||
|
||
function sampleRefs(samples, pick) {
|
||
const seen = new Set();
|
||
const refs = [];
|
||
for (const sample of samples) {
|
||
const value = pick(sample);
|
||
if (!value || seen.has(value)) continue;
|
||
seen.add(value);
|
||
refs.push({ ...ref(sample), value });
|
||
}
|
||
return refs.slice(0, 20);
|
||
}
|
||
|
||
function ref(sample) {
|
||
if (!sample) return null;
|
||
return { seq: sample.seq ?? null, sampleGroupSeq: sample.sampleGroupSeq ?? null, ts: sample.ts ?? null, pageRole: sample.pageRole ?? null, pageId: sample.pageId ?? null, url: sample.url ?? null, routeSessionId: sample.routeSessionId ?? null, activeSessionId: sample.activeSessionId ?? null };
|
||
}
|
||
|
||
async function artifactSummary(artifacts) {
|
||
const items = artifacts.slice(-30).map((item) => ({ kind: item.kind, reason: item.reason, sampleSeq: item.sampleSeq, path: item.path, sha256: item.sha256, byteCount: item.byteCount }));
|
||
return { count: artifacts.length, latest: items };
|
||
}
|
||
|
||
function compactManifest(value) {
|
||
if (!value) return null;
|
||
return { jobId: value.jobId, stateDir: value.stateDir, baseUrl: value.baseUrl, targetPath: value.targetPath, startedAt: value.startedAt, status: value.status, pageAuthority: value.pageAuthority ?? null, navigation: value.navigation ?? null, sampling: value.sampling, pageProvenance: value.pageProvenance ?? null, safety: value.safety };
|
||
}
|
||
|
||
function compactHeartbeat(value) {
|
||
if (!value) return null;
|
||
return { jobId: value.jobId, pid: value.pid, status: value.status, sampleSeq: value.sampleSeq, commandSeq: value.commandSeq, pageId: value.pageId ?? null, observerPageId: value.observerPageId ?? null, currentUrl: value.currentUrl, observerUrl: value.observerUrl ?? null, observerRefreshIntervalMs: value.observerRefreshIntervalMs ?? null, lastObserverRefreshAt: value.lastObserverRefreshAt ?? null, pageProvenance: value.pageProvenance ?? null, updatedAt: value.updatedAt, uptimeMs: value.uptimeMs };
|
||
}
|
||
|
||
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) {
|
||
const columnLabel = formatTurnColumnDisplayLabel(column);
|
||
header.push(columnLabel + " 总耗时(s)");
|
||
header.push(columnLabel + " 最近更新(s)");
|
||
}
|
||
const lines = [];
|
||
lines.push("| " + header.map(escapeMarkdownCell).join(" | ") + " |");
|
||
lines.push("| " + header.map(() => "---").join(" | ") + " |");
|
||
for (const row of rows) {
|
||
const cells = [row.ts || "-"];
|
||
for (const column of columns) {
|
||
const cell = row.cells?.[column.id] || {};
|
||
cells.push(formatMetricCell(cell.totalElapsedSeconds));
|
||
cells.push(formatMetricCell(cell.recentUpdateSeconds));
|
||
}
|
||
lines.push("| " + cells.map(escapeMarkdownCell).join(" | ") + " |");
|
||
}
|
||
const columnLines = columns.map((column) => "- " + formatTurnColumnDisplayLabel(column) + ": pageRole=" + (column.pageRole || "-") + " pageId=" + (column.pageId || "-") + " source=" + (column.source || "-") + " prompt=" + (column.promptIndex ?? "-") + " lastPrompt=" + (column.lastPromptIndex ?? "-") + " firstSeq=" + (column.firstSeq ?? "-") + " lastSeq=" + (column.lastSeq ?? "-") + " traceId=" + (column.traceId || "-") + " messageId=" + (column.messageId || "-")).join("\n");
|
||
const nonMonotonic = Array.isArray(sampleMetrics?.turnTimingNonMonotonic) ? sampleMetrics.turnTimingNonMonotonic : [];
|
||
const nonMonotonicLines = nonMonotonic.length > 0
|
||
? nonMonotonic.slice(0, 80).map((item) => "- " + formatTurnEventDisplayLabel(item) + " " + item.metric + (item.anomaly ? " " + item.anomaly : "") + " " + (item.fromValue ?? "-") + " -> " + (item.toValue ?? "-") + " delta=" + (item.delta ?? "-") + " sampleDelta=" + (item.sampleDeltaSeconds ?? "-") + " allowed=" + (item.allowedIncreaseSeconds ?? "-") + formatTimingMeta(item) + " seq " + (item.fromSeq ?? "-") + " -> " + (item.toSeq ?? "-") + " ts " + (item.fromTs || "-") + " -> " + (item.toTs || "-") + " traceId=" + (item.traceId || "-")).join("\n")
|
||
: "- 未观察到总耗时下降或最近更新异常跳增。";
|
||
const terminalGrowth = Array.isArray(sampleMetrics?.turnTimingTerminalElapsedGrowth) ? sampleMetrics.turnTimingTerminalElapsedGrowth : [];
|
||
const terminalGrowthLines = terminalGrowth.length > 0
|
||
? terminalGrowth.slice(0, 80).map((item) => "- " + formatTurnEventDisplayLabel(item) + " terminal totalElapsed growth " + (item.fromValue ?? "-") + " -> " + (item.toValue ?? "-") + " delta=" + (item.delta ?? "-") + " status " + (item.fromStatus || "-") + " -> " + (item.toStatus || "-") + formatTimingMeta(item) + " seq " + (item.fromSeq ?? "-") + " -> " + (item.toSeq ?? "-") + " ts " + (item.fromTs || "-") + " -> " + (item.toTs || "-") + " traceId=" + (item.traceId || "-")).join("\n")
|
||
: "- 未观察到 terminal 后总耗时增长。";
|
||
const elapsedZeroResets = Array.isArray(sampleMetrics?.turnTimingElapsedZeroResets) ? sampleMetrics.turnTimingElapsedZeroResets : [];
|
||
const elapsedZeroResetLines = elapsedZeroResets.length > 0
|
||
? elapsedZeroResets.slice(0, 80).map((item) => "- " + formatTurnEventDisplayLabel(item) + " totalElapsed zero-reset " + (item.fromValue ?? "-") + " -> " + (item.toValue ?? "-") + " delta=" + (item.delta ?? "-") + formatTimingMeta(item) + " seq " + (item.fromSeq ?? "-") + " -> " + (item.toSeq ?? "-") + " ts " + (item.fromTs || "-") + " -> " + (item.toTs || "-") + " traceId=" + (item.traceId || "-")).join("\n")
|
||
: "- 未观察到总耗时从非零跳回 0 秒。";
|
||
const totalElapsedForwardJumps = Array.isArray(sampleMetrics?.turnTimingTotalElapsedForwardJumps) ? sampleMetrics.turnTimingTotalElapsedForwardJumps : [];
|
||
const totalElapsedForwardJumpLines = totalElapsedForwardJumps.length > 0
|
||
? totalElapsedForwardJumps.slice(0, 80).map((item) => "- " + formatTurnEventDisplayLabel(item) + " totalElapsed forward-jump " + (item.fromValue ?? "-") + " -> " + (item.toValue ?? "-") + " delta=" + (item.delta ?? "-") + " sampleDelta=" + (item.sampleDeltaSeconds ?? "-") + " allowed=" + (item.allowedIncreaseSeconds ?? "-") + formatTimingMeta(item) + " seq " + (item.fromSeq ?? "-") + " -> " + (item.toSeq ?? "-") + " ts " + (item.fromTs || "-") + " -> " + (item.toTs || "-") + " traceId=" + (item.traceId || "-")).join("\n")
|
||
: "- 未观察到总耗时超出采样间隔的异常前跳。";
|
||
const sawtoothJumps = Array.isArray(sampleMetrics?.turnTimingRecentUpdateSawtoothJumps)
|
||
? sampleMetrics.turnTimingRecentUpdateSawtoothJumps
|
||
: nonMonotonic.filter((item) => item.metric === "recentUpdateSeconds" && item.anomaly === "jump");
|
||
const sawtoothJumpLines = sawtoothJumps.length > 0
|
||
? sawtoothJumps.slice(0, 80).map((item) => "- " + formatTurnEventDisplayLabel(item) + " recentUpdate sawtooth-jump " + (item.fromValue ?? "-") + " -> " + (item.toValue ?? "-") + " delta=" + (item.delta ?? "-") + " sampleDelta=" + (item.sampleDeltaSeconds ?? "-") + " allowed=" + (item.allowedIncreaseSeconds ?? "-") + " seq " + (item.fromSeq ?? "-") + " -> " + (item.toSeq ?? "-") + " ts " + (item.fromTs || "-") + " -> " + (item.toTs || "-") + " traceId=" + (item.traceId || "-")).join("\n")
|
||
: "- 未观察到最近更新三角波异常跳增。";
|
||
const recentUpdateSteps = Array.isArray(sampleMetrics?.turnTimingRecentUpdateLargestSteps)
|
||
? sampleMetrics.turnTimingRecentUpdateLargestSteps
|
||
: Array.isArray(sampleMetrics?.turnTimingRecentUpdateSteps)
|
||
? sampleMetrics.turnTimingRecentUpdateSteps.filter((item) => Number.isFinite(Number(item.delta))).slice().sort((a, b) => Number(b.delta) - Number(a.delta)).slice(0, 200)
|
||
: [];
|
||
const stepLines = recentUpdateSteps.length > 0
|
||
? recentUpdateSteps.slice(0, 80).map((item) => "- " + formatTurnEventDisplayLabel(item) + " recentUpdate step " + (item.fromValue ?? "-") + " -> " + (item.toValue ?? "-") + " delta=" + (item.delta ?? "-") + " sampleDelta=" + (item.sampleDeltaSeconds ?? "-") + " allowed=" + (item.allowedIncreaseSeconds ?? "-") + " excess=" + (item.excessiveIncreaseSeconds ?? 0) + " event=" + (item.event || "-") + " seq " + (item.fromSeq ?? "-") + " -> " + (item.toSeq ?? "-") + " ts " + (item.fromTs || "-") + " -> " + (item.toTs || "-") + " traceId=" + (item.traceId || "-")).join("\n")
|
||
: "- 未观察到最近更新相邻采样 step。";
|
||
const recentUpdateResets = Array.isArray(sampleMetrics?.turnTimingRecentUpdateResets) ? sampleMetrics.turnTimingRecentUpdateResets : [];
|
||
const resetLines = recentUpdateResets.length > 0
|
||
? recentUpdateResets.slice(0, 80).map((item) => "- " + formatTurnEventDisplayLabel(item) + " 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")
|
||
: "- 未观察到最近更新归零/回落。";
|
||
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时间源说明:totalElapsed 异常以 DOM Code Agent card 采样序列为观察源;completion/final-response 可用时作为 sealed source-of-truth;status=business-turn-completed 表示 timing 仅为非阻塞告警,status=observer-timeout 表示观察等待超预算,status=scenario-incomplete 表示业务回合失败或未完成。\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 formatTurnColumnDisplayLabel(column) {
|
||
const base = String(column?.label || column?.id || "-");
|
||
const role = String(column?.pageRole || "unknown");
|
||
const pageId = compactPageId(column?.pageId);
|
||
return pageId ? base + "@" + role + "/" + pageId : base + "@" + role;
|
||
}
|
||
|
||
function formatTurnEventDisplayLabel(item) {
|
||
const base = String(item?.columnLabel || item?.columnId || "-");
|
||
const role = String(item?.pageRole || "unknown");
|
||
const pageId = compactPageId(item?.pageId);
|
||
return pageId ? base + "@" + role + "/" + pageId : base + "@" + role;
|
||
}
|
||
|
||
function compactPageId(value) {
|
||
if (value === null || value === undefined || value === "") return "";
|
||
const text = String(value);
|
||
if (text.length <= 18) return text;
|
||
return text.slice(0, 12) + ".." + text.slice(-4);
|
||
}
|
||
|
||
function formatMetricCell(value) {
|
||
if (value === null || value === undefined || !Number.isFinite(Number(value))) return "-";
|
||
return String(Number(value));
|
||
}
|
||
|
||
function escapeMarkdownCell(value) {
|
||
return String(value ?? "-").replace(/\|/gu, "\\|");
|
||
}
|
||
|
||
function formatTimingMeta(item) {
|
||
const source = item?.timingSourceOfTruth || item?.expectedElapsedSource || item?.evidenceKind || "-";
|
||
const status = item?.timingStatus || "-";
|
||
return " source=" + source + " status=" + status;
|
||
}
|
||
|
||
function renderMarkdown(report) {
|
||
const findingLines = report.findings.length === 0 ? "- 无红灯项。" : report.findings.map((item) => "- " + item.severity + ": " + item.id + " - " + item.summary).join("\n");
|
||
const commandLines = report.commandTimeline.length === 0 ? "- 无控制命令。" : report.commandTimeline.map((item) => "- " + item.ts + " " + item.phase + " " + item.type + " " + item.commandId + " " + (item.afterUrl || "")).join("\n");
|
||
const commandFailureLines = Array.isArray(report.commandFailures) && report.commandFailures.length > 0
|
||
? report.commandFailures.slice(0, 80).map((item) => "- " + (item.ts || "-") + " type=" + (item.type || "-") + " commandId=" + (item.commandId || "-") + " durationMs=" + (item.durationMs ?? "-") + " sampleSeq=" + (item.sampleSeq ?? "-") + " path=" + (item.beforePath || "-") + "->" + (item.afterPath || "-") + " message=" + escapeMarkdownCell(item.message || item.failureKind || item.name || "-")).join("\n")
|
||
: "- 无失败控制命令。";
|
||
const transitionLines = report.transitions.length === 0 ? "- 无状态变化。" : report.transitions.slice(0, 80).map((item) => "- #" + item.seq + " " + item.ts + " messages=" + item.messageCount + " traceRows=" + item.traceRowCount + " route=" + (item.routeSessionId || "-") + " active=" + (item.activeSessionId || "-")).join("\n");
|
||
const metricSummary = report.sampleMetrics?.summary || {};
|
||
const loading = report.sampleMetrics?.loading || {};
|
||
const loadingSummary = loading.summary || {};
|
||
const sessionRailTitles = report.sampleMetrics?.sessionRailTitles || {};
|
||
const sessionRailTitleSummary = sessionRailTitles.summary || {};
|
||
const codeAgentCardTiming = report.sampleMetrics?.codeAgentCardTiming || {};
|
||
const codeAgentCardTimingSummary = codeAgentCardTiming.summary || {};
|
||
const roundCompletion = codeAgentCardTiming.roundCompletion || {};
|
||
const traceOrder = report.sampleMetrics?.traceOrder || {};
|
||
const traceOrderSummary = traceOrder.summary || {};
|
||
const alertSummary = report.runtimeAlerts?.summary || {};
|
||
const apiDomLag = report.apiDomLag || {};
|
||
const apiDomLagSummary = apiDomLag.summary || {};
|
||
const projectManagement = report.projectManagement || {};
|
||
const projectSummary = projectManagement.summary || {};
|
||
const projectCommandLines = Array.isArray(projectManagement.commands) && projectManagement.commands.length > 0
|
||
? projectManagement.commands.slice(0, 40).map((item) => "- " + (item.ts || "-") + " " + (item.phase || "-") + " " + (item.type || "-") + " command=" + (item.commandId || "-") + " status=" + (item.launchStatus ?? "-") + " session=" + (item.sessionId || "-") + " otel=" + (item.otelTraceId || "-") + " taskHash=" + (item.selectedTaskRefHash || "-")).join("\n")
|
||
: "- 无项目管理控制命令。";
|
||
const projectApiLines = Array.isArray(projectManagement.projectApiByPath) && projectManagement.projectApiByPath.length > 0
|
||
? projectManagement.projectApiByPath.slice(0, 40).map((item) => "- " + (item.method || "-") + " " + (item.path || "-") + " type=" + (item.type || "-") + " status=" + (item.status ?? "-") + " count=" + (item.count ?? 0) + " first=" + (item.firstAt || "-") + " last=" + (item.lastAt || "-")).join("\n")
|
||
: "- 无项目管理自然 API 记录。";
|
||
const projectSampleLines = Array.isArray(projectManagement.samples) && projectManagement.samples.length > 0
|
||
? projectManagement.samples.slice(-40).map((item) => "- #" + (item.seq ?? "-") + " " + (item.ts || "-") + " role=" + (item.pageRole || "-") + " kind=" + (item.pageKind || "-") + " src=" + (item.sourceCount ?? "-") + " files=" + (item.fileCount ?? "-") + " tasks=" + (item.taskCount ?? "-") + " selectedTask=" + (item.selectedTaskRefHash || "-") + " launchEnabled=" + String(item.launchButtonEnabled === true) + " links=" + (item.workbenchLinkCount ?? 0)).join("\n")
|
||
: "- 无项目管理 DOM 采样。";
|
||
const httpAlertLines = Array.isArray(report.runtimeAlerts?.networkHttpErrorsByPath) && report.runtimeAlerts.networkHttpErrorsByPath.length > 0
|
||
? report.runtimeAlerts.networkHttpErrorsByPath.slice(0, 40).map((item) => "- HTTP " + (item.status ?? "-") + " " + item.method + " " + item.urlPath + " count=" + item.count + " prompts=" + (item.promptIndexes?.join(",") || "-") + " first=" + (item.firstAt || "-") + " last=" + (item.lastAt || "-")).join("\n")
|
||
: "- 无 HTTP 错误。";
|
||
const requestFailedLines = Array.isArray(report.runtimeAlerts?.networkRequestFailedByPath) && report.runtimeAlerts.networkRequestFailedByPath.length > 0
|
||
? report.runtimeAlerts.networkRequestFailedByPath.slice(0, 40).map((item) => "- requestfailed " + item.method + " " + item.urlPath + " count=" + item.count + " failure=" + (item.failureKinds?.slice(0, 4).join(",") || "-") + " prompts=" + (item.promptIndexes?.join(",") || "-") + " first=" + (item.firstAt || "-") + " last=" + (item.lastAt || "-")).join("\n")
|
||
: "- 无 requestfailed。";
|
||
const domDiagnosticLines = Array.isArray(report.runtimeAlerts?.domDiagnostics) && report.runtimeAlerts.domDiagnostics.length > 0
|
||
? report.runtimeAlerts.domDiagnostics.slice(0, 40).map((item) => "- #" + (item.seq ?? "-") + " " + (item.ts || "-") + " prompt=" + (item.promptIndex ?? "-") + " source=" + (item.source || "-") + " code=" + (item.diagnosticCode || "-") + " traceId=" + (item.traceId || "-") + " http=" + (item.httpStatus ?? "-") + " idle=" + (item.idleSeconds ?? "-") + " waitingFor=" + (item.waitingFor || "-") + " lastEventLabel=" + (item.lastEventLabel || "-") + " textHash=" + (item.textHash || "-") + " preview=" + escapeMarkdownCell(item.preview || "")).join("\n")
|
||
: "- 无 DOM 诊断文本。";
|
||
const consoleAlertLines = Array.isArray(report.runtimeAlerts?.consoleAlerts) && report.runtimeAlerts.consoleAlerts.length > 0
|
||
? report.runtimeAlerts.consoleAlerts.slice(0, 40).map((item) => "- " + (item.ts || "-") + " prompt=" + (item.promptIndex ?? "-") + " type=" + (item.type || "-") + " status=" + (item.status ?? "-") + " path=" + (item.urlPath || "-") + " traceId=" + (item.traceId || "-") + " textHash=" + (item.textHash || "-") + " preview=" + escapeMarkdownCell(item.preview || "")).join("\n")
|
||
: "- 无 console warning/error。";
|
||
const consoleAlertGroupLines = Array.isArray(report.runtimeAlerts?.consoleAlertsByPath) && report.runtimeAlerts.consoleAlertsByPath.length > 0
|
||
? report.runtimeAlerts.consoleAlertsByPath.slice(0, 40).map((item) => "- console " + (item.type || "-") + " status=" + (item.status ?? "-") + " path=" + (item.urlPath || "-") + " count=" + item.count + " prompts=" + (item.promptIndexes?.join(",") || "-") + " traces=" + (item.traceIds?.slice(0, 6).join(",") || "-")).join("\n")
|
||
: "- 无 console 分组。";
|
||
const promptNetworkLines = Array.isArray(report.promptNetwork?.rounds) && report.promptNetwork.rounds.length > 0
|
||
? report.promptNetwork.rounds.map((item) => "- round " + item.promptIndex + " promptHash=" + (item.promptTextHash || "-") + " chatPostOk=" + String(item.chatPostOk) + " modes=" + (Array.isArray(item.submitModes) && item.submitModes.length > 0 ? item.submitModes.join(",") : "-") + " failure=" + (item.failureKind || "-") + " statuses=" + (Array.isArray(item.responseStatuses) && item.responseStatuses.length > 0 ? item.responseStatuses.join(",") : "-") + " firstChat=" + (item.firstChatEventAt || "-") + " lastChat=" + (item.lastChatEventAt || "-")).join("\n")
|
||
: "- 无 prompt 网络记录。";
|
||
const roundLines = Array.isArray(report.sampleMetrics?.rounds) && report.sampleMetrics.rounds.length > 0
|
||
? report.sampleMetrics.rounds.map((item) => "- round " + item.promptIndex + " promptHash=" + (item.promptTextHash || "-") + " samples=" + item.sampleCount + " loadingSamples=" + (item.loadingSamples ?? 0) + " maxLoading=" + (item.maxLoadingCount ?? 0) + " loadingOwners=" + (item.loadingOwnerCount ?? 0) + " totalMax=" + (item.maxTotalElapsedSeconds ?? "-") + " totalLast=" + (item.lastTotalElapsedSeconds ?? "-") + " recentMax=" + (item.maxRecentUpdateSeconds ?? "-") + " recentLast=" + (item.lastRecentUpdateSeconds ?? "-") + " totalDecrease=" + (item.turnTimingTotalElapsedDecreaseCount ?? 0) + " totalForwardJump=" + (item.turnTimingTotalElapsedForwardJumpCount ?? 0) + " totalForwardJumpMax=" + (item.turnTimingTotalElapsedForwardJumpMaxSeconds ?? 0) + " terminalGrowth=" + (item.turnTimingTerminalElapsedGrowthCount ?? 0) + " terminalGrowthMax=" + (item.turnTimingTerminalElapsedGrowthMaxSeconds ?? 0) + " recentJump=" + (item.turnTimingRecentUpdateJumpCount ?? 0) + " recentSawtoothJump=" + (item.turnTimingRecentUpdateSawtoothJumpCount ?? item.turnTimingRecentUpdateJumpCount ?? 0) + " recentStep=" + (item.turnTimingRecentUpdateStepCount ?? 0) + " recentMaxIncrease=" + (item.turnTimingRecentUpdateMaxIncreaseSeconds ?? "-") + " recentMaxExcess=" + (item.turnTimingRecentUpdateMaxExcessSeconds ?? 0) + " recentReset=" + (item.turnTimingRecentUpdateResetCount ?? 0) + " diagnostics=" + item.diagnosticSamples + " terminal=" + item.terminalSamples + " finalText=" + item.finalTextSamples).join("\n")
|
||
: "- 无轮次指标。";
|
||
const cardMissingElapsedLines = Array.isArray(codeAgentCardTiming.missingElapsed) && codeAgentCardTiming.missingElapsed.length > 0
|
||
? codeAgentCardTiming.missingElapsed.slice(0, 80).map((item) => "- #" + (item.seq ?? "-") + " " + (item.ts || "-") + " prompt=" + (item.promptIndex ?? "-") + " role=" + (item.pageRole || "-") + " status=" + (item.status || "-") + " traceId=" + (item.traceId || "-") + " messageId=" + (item.messageId || "-") + " preview=" + escapeMarkdownCell(item.textPreview || "")).join("\n")
|
||
: "- 未观察到 Code Agent 卡片缺少耗时。";
|
||
const cardMissingRecentLines = Array.isArray(codeAgentCardTiming.missingRecentUpdate) && codeAgentCardTiming.missingRecentUpdate.length > 0
|
||
? codeAgentCardTiming.missingRecentUpdate.slice(0, 80).map((item) => "- #" + (item.seq ?? "-") + " " + (item.ts || "-") + " prompt=" + (item.promptIndex ?? "-") + " role=" + (item.pageRole || "-") + " status=" + (item.status || "-") + " traceId=" + (item.traceId || "-") + " messageId=" + (item.messageId || "-") + " total=" + (item.totalElapsedSeconds ?? "-") + " preview=" + escapeMarkdownCell(item.textPreview || "")).join("\n")
|
||
: "- 未观察到未终态 Code Agent 卡片缺少最近更新。";
|
||
const roundCompletionLines = Array.isArray(roundCompletion.events) && roundCompletion.events.length > 0
|
||
? roundCompletion.events.slice(0, 80).map((item) => "- #" + (item.seq ?? "-") + " " + (item.ts || "-") + " prompt=" + (item.promptIndex ?? "-") + " role=" + (item.pageRole || "-") + " traceId=" + (item.traceId || "-") + " completionElapsed=" + (item.elapsedSeconds ?? "-") + " preview=" + escapeMarkdownCell(item.preview || "")).join("\n")
|
||
: "- 未观察到“轮次完成(总耗时 ...)”trace 行。";
|
||
const roundCompletionMismatchLines = Array.isArray(roundCompletion.elapsedMismatches) && roundCompletion.elapsedMismatches.length > 0
|
||
? roundCompletion.elapsedMismatches.slice(0, 80).map((item) => "- #" + (item.seq ?? "-") + " " + (item.ts || "-") + " prompt=" + (item.promptIndex ?? "-") + " traceId=" + (item.traceId || "-") + " completion=" + (item.completionElapsedSeconds ?? "-") + " card=" + (item.cardTotalElapsedSeconds ?? "-") + " delta=" + (item.deltaSeconds ?? "-") + " tolerance=" + (item.toleranceSeconds ?? "-") + formatTimingMeta(item)).join("\n")
|
||
: "- 未观察到轮次完成耗时与卡片耗时不一致。";
|
||
const roundCompletionFinalMissingLines = Array.isArray(roundCompletion.finalResponseMissing) && roundCompletion.finalResponseMissing.length > 0
|
||
? roundCompletion.finalResponseMissing.slice(0, 80).map((item) => "- #" + (item.seq ?? "-") + " " + (item.ts || "-") + " prompt=" + (item.promptIndex ?? "-") + " role=" + (item.pageRole || "-") + " traceId=" + (item.traceId || "-") + " completionElapsed=" + (item.completionElapsedSeconds ?? "-")).join("\n")
|
||
: "- 未观察到轮次完成后 final response 缺失。";
|
||
const roundCompletionPostTimingLines = Array.isArray(roundCompletion.postCompletionTimingChanges) && roundCompletion.postCompletionTimingChanges.length > 0
|
||
? roundCompletion.postCompletionTimingChanges.slice(0, 80).map((item) => "- " + (item.columnLabel || item.columnId || "-") + " " + (item.metric || "-") + " " + (item.fromValue ?? "-") + " -> " + (item.toValue ?? "-") + " delta=" + (item.delta ?? "-") + " completionSeq=" + (item.seq ?? "-") + " seq " + (item.fromSeq ?? "-") + " -> " + (item.toSeq ?? "-") + " traceId=" + (item.traceId || "-")).join("\n")
|
||
: "- 未观察到轮次完成后耗时/最近更新继续变化。";
|
||
const durationUnderreportedLines = Array.isArray(codeAgentCardTiming.durationUnderreported) && codeAgentCardTiming.durationUnderreported.length > 0
|
||
? codeAgentCardTiming.durationUnderreported.slice(0, 80).map((item) => "- sample=" + (item.sampleIndex ?? "-") + " " + (item.timestamp || "-") + " role=" + (item.pageRole || "-") + " status=" + (item.status || "-") + " traceId=" + (item.traceId || "-") + " card=" + (item.cardTotalElapsedSeconds ?? "-") + "s expected=" + (item.expectedElapsedSeconds ?? "-") + "s delta=" + (item.deltaSeconds ?? "-") + "s evidence=" + (item.evidenceKind || "-") + formatTimingMeta(item) + " preview=" + escapeMarkdownCell(item.evidencePreview || item.cardPreview || "")).join("\n")
|
||
: "- 未观察到 Code Agent 卡片耗时低于 trace/final-response 证据。";
|
||
const durationMismatchLines = Array.isArray(codeAgentCardTiming.durationMismatches) && codeAgentCardTiming.durationMismatches.length > 0
|
||
? codeAgentCardTiming.durationMismatches.slice(0, 80).map((item) => "- sample=" + (item.sampleIndex ?? "-") + " " + (item.timestamp || "-") + " role=" + (item.pageRole || "-") + " status=" + (item.status || "-") + " traceId=" + (item.traceId || "-") + " direction=" + (item.direction || "-") + " card=" + (item.cardTotalElapsedSeconds ?? "-") + "s expected=" + (item.expectedElapsedSeconds ?? "-") + "s signedDelta=" + (item.signedDeltaSeconds ?? "-") + "s delta=" + (item.deltaSeconds ?? "-") + "s tolerance=" + (item.toleranceSeconds ?? "-") + "s evidence=" + (item.evidenceKind || "-") + " exact=" + String(item.exactEvidence === true) + formatTimingMeta(item) + " preview=" + escapeMarkdownCell(item.evidencePreview || item.cardPreview || "")).join("\n")
|
||
: "- 未观察到 Code Agent 卡片耗时与 completion/final-response 封口证据不一致。";
|
||
const traceOrderAnomalyLines = Array.isArray(traceOrder.orderAnomalies) && traceOrder.orderAnomalies.length > 0
|
||
? traceOrder.orderAnomalies.slice(0, 80).map((item) => "- sample=" + (item.sampleIndex ?? "-") + " " + (item.timestamp || "-") + " role=" + (item.pageRole || "-") + " traceId=" + (item.traceId || "-") + " rows=" + (item.previousRowIndex ?? "-") + "->" + (item.currentRowIndex ?? "-") + " reasons=" + (Array.isArray(item.reasons) ? item.reasons.join(",") : "-") + " total=" + (item.previousTotalSeconds ?? "-") + "->" + (item.currentTotalSeconds ?? "-") + " clock=" + (item.previousClockSeconds ?? "-") + "->" + (item.currentClockSeconds ?? "-") + " preview=" + escapeMarkdownCell((item.previousPreview || "") + " / " + (item.currentPreview || ""))).join("\n")
|
||
: "- 未观察到可见 trace 行顺序非单调。";
|
||
const traceCompletionNotLastLines = Array.isArray(traceOrder.completionNotLast) && traceOrder.completionNotLast.length > 0
|
||
? traceOrder.completionNotLast.slice(0, 80).map((item) => "- sample=" + (item.sampleIndex ?? "-") + " " + (item.timestamp || "-") + " role=" + (item.pageRole || "-") + " traceId=" + (item.traceId || "-") + " rows=" + (item.completionRowIndex ?? "-") + "->" + (item.laterRowIndex ?? "-") + " total=" + (item.completionTotalSeconds ?? "-") + "->" + (item.laterTotalSeconds ?? "-") + " completion=" + escapeMarkdownCell(item.completionPreview || "") + " later=" + escapeMarkdownCell(item.laterPreview || "")).join("\n")
|
||
: "- 未观察到 completion 行后还有同 trace 后续行。";
|
||
const loadingSegmentLines = Array.isArray(loading.segments) && loading.segments.length > 0
|
||
? loading.segments.slice(0, 80).map((item) => "- observedDuration=" + (item.durationSeconds ?? 0) + "s upperBound=" + (item.upperBoundSeconds ?? item.durationSeconds ?? 0) + "s endedGap=" + (item.endedGapSeconds ?? "-") + "s samples=" + (item.sampleCount ?? 0) + " countMax=" + (item.maxCount ?? 0) + " owners=" + (item.ownerCount ?? 0) + " seq=" + (item.firstSeq ?? "-") + ".." + (item.lastSeq ?? "-") + " ts=" + (item.firstAt || "-") + ".." + (item.lastAt || "-") + " endedAt=" + (item.endedAt || (item.ongoing ? "ongoing" : "-")) + " ownerLabels=" + ((Array.isArray(item.owners) ? item.owners : []).slice(0, 6).map((owner) => (owner.ownerKind || "-") + ":" + (owner.ownerLabel || "-") + "x" + (owner.count ?? 0)).join(",") || "-")).join("\n")
|
||
: "- 未观察到“加载中”可见区间。";
|
||
const loadingOwnerLines = Array.isArray(loading.owners) && loading.owners.length > 0
|
||
? loading.owners.slice(0, 80).map((item) => "- " + (item.ownerKind || "-") + " " + escapeMarkdownCell(item.ownerLabel || item.ownerKey || "-") + " traceId=" + (item.ownerTraceId || "-") + " messageId=" + (item.ownerMessageId || "-") + " sessionId=" + (item.ownerSessionId || "-") + " samples=" + (item.sampleCount ?? 0) + " occurrences=" + (item.occurrenceCount ?? 0) + " maxCount=" + (item.maxSimultaneousCount ?? 0) + " longest=" + (item.longestContinuousSeconds ?? 0) + "s seq=" + (item.firstSeq ?? "-") + ".." + (item.lastSeq ?? "-") + " prompts=" + (Array.isArray(item.promptIndexes) ? item.promptIndexes.join(",") : "-")).join("\n")
|
||
: "- 未观察到“加载中”归属。";
|
||
const loadingTimelineLines = Array.isArray(loading.timeline) && loading.timeline.length > 0
|
||
? loading.timeline.slice(0, 160).map((item) => "- #" + (item.seq ?? "-") + " " + (item.ts || "-") + " prompt=" + (item.promptIndex ?? "-") + " loadingCount=" + (item.loadingCount ?? 0) + " ownerCount=" + (item.ownerCount ?? 0) + " owners=" + ((Array.isArray(item.owners) ? item.owners : []).slice(0, 6).map((owner) => (owner.ownerKind || "-") + ":" + (owner.ownerLabel || "-") + " trace=" + (owner.ownerTraceId || "-") + "x" + (owner.count ?? 0)).join(",") || "-")).join("\n")
|
||
: "- 未观察到“加载中”采样点。";
|
||
const sessionRailTitleSampleLines = Array.isArray(sessionRailTitles.samples) && sessionRailTitles.samples.length > 0
|
||
? sessionRailTitles.samples.slice(0, 80).map((item) => "- #" + (item.seq ?? "-") + " " + (item.ts || "-") + " role=" + (item.pageRole || "-") + " visible=" + (item.visibleCount ?? 0) + " fallback=" + (item.fallbackTitleCount ?? 0) + " ratio=" + (item.fallbackTitleRatio ?? 0) + " examples=" + ((Array.isArray(item.examples) ? item.examples : []).slice(0, 4).map((example) => escapeMarkdownCell(example.titlePreview || example.titleHash || "-")).join(",") || "-")).join("\n")
|
||
: "- 未观察到超过一半 fallback 的 session 列表采样点。";
|
||
const sessionRailTitleExampleLines = Array.isArray(sessionRailTitles.examples) && sessionRailTitles.examples.length > 0
|
||
? sessionRailTitles.examples.slice(0, 80).map((item) => "- firstSeq=" + (item.firstSeq ?? "-") + " role=" + (item.pageRole || "-") + " active=" + String(item.active === true) + " sessionPrefix=" + (item.sessionIdPrefix || "-") + " titleHash=" + (item.titleHash || "-") + " preview=" + escapeMarkdownCell(item.titlePreview || "")).join("\n")
|
||
: "- 无 fallback 标题示例。";
|
||
const provenanceLines = Array.isArray(report.pageProvenance?.segments) && report.pageProvenance.segments.length > 0
|
||
? report.pageProvenance.segments.slice(0, 40).map((item) => "- fingerprint=" + (item.assetFingerprint || "-") + " samples=" + item.sampleCount + " seq=" + (item.firstSeq ?? "-") + ".." + (item.lastSeq ?? "-") + " ts=" + (item.firstAt || "-") + ".." + (item.lastAt || "-") + " scripts=" + (item.scriptCount ?? "-") + " styles=" + (item.stylesheetCount ?? "-") + " urlPaths=" + (Array.isArray(item.urlPaths) ? item.urlPaths.slice(0, 4).join(",") : "-")).join("\n")
|
||
: "- 无页面 provenance segment。";
|
||
const ordinaryPerformanceItems = Array.isArray(report.pagePerformance?.sameOriginApiByPath) ? report.pagePerformance.sameOriginApiByPath.filter((item) => item.isLongLivedStream !== true) : [];
|
||
const streamPerformanceItems = Array.isArray(report.pagePerformance?.sameOriginApiByPath) ? report.pagePerformance.sameOriginApiByPath.filter((item) => item.isLongLivedStream === true) : [];
|
||
const sameOriginApiBudgetMs = Number(report.alertThresholds?.sameOriginApiSlowMs ?? report.pagePerformance?.summary?.budgetMs);
|
||
const streamOpenBudgetMs = Number(report.alertThresholds?.longLivedStreamOpenSlowMs);
|
||
const performanceLines = ordinaryPerformanceItems.length > 0
|
||
? ordinaryPerformanceItems.slice(0, 80).map((item) => "- " + item.path + " kind=" + (item.routeKind || "same-origin-api") + " budgetMetric=" + (item.budgetMetric || "durationMs") + " samples=" + item.sampleCount + " p50=" + (item.p50Ms ?? "-") + "ms p75=" + (item.p75Ms ?? "-") + "ms p95=" + (item.p95Ms ?? "-") + "ms max=" + (item.maxMs ?? "-") + "ms >budget=" + (item.overBudgetCount ?? item.overFiveSecondCount ?? 0) + " budgetMs=" + (item.budgetMs ?? sameOriginApiBudgetMs) + " legacy>5s=" + (item.overFiveSecondCount ?? 0) + " window=" + (item.firstAt || "-") + ".." + (item.lastAt || "-")).join("\n")
|
||
: "- 无同源 API Resource Timing 样本。";
|
||
const streamPerformanceLines = streamPerformanceItems.length > 0
|
||
? streamPerformanceItems.slice(0, 80).map((item) => "- " + item.path + " kind=" + (item.routeKind || "same-origin-api-stream") + " samples=" + item.sampleCount + " streamOpenP50=" + (item.streamOpenP50Ms ?? "-") + "ms streamOpenP75=" + (item.streamOpenP75Ms ?? "-") + "ms streamOpenP95=" + (item.streamOpenP95Ms ?? "-") + "ms streamOpenMax=" + (item.streamOpenMaxMs ?? "-") + "ms streamOpen>budget=" + (item.streamOpenOverBudgetCount ?? item.streamOpenOverFiveSecondCount ?? 0) + " streamOpenBudgetMs=" + (item.streamOpenBudgetMs ?? streamOpenBudgetMs) + " streamOpenLegacy>5s=" + (item.streamOpenOverFiveSecondCount ?? 0) + " streamLifetime>5s=" + (item.streamLifetimeOverFiveSecondCount ?? 0) + " lifetimeMax=" + (item.maxMs ?? "-") + "ms window=" + (item.firstAt || "-") + ".." + (item.lastAt || "-")).join("\n")
|
||
: "- 无同源长连接 Resource Timing 样本。";
|
||
const apiDomLagGroupLines = Array.isArray(apiDomLag.groups) && apiDomLag.groups.length > 0
|
||
? apiDomLag.groups.slice(0, 80).map((item) => "- " + (item.method || "-") + " " + (item.path || "-") + " status=" + (item.status ?? "-") + " kind=" + (item.routeKind || "-") + " confidence=" + (item.confidence || "-") + " count=" + (item.count ?? 0) + " changed=" + (item.domChangedCount ?? 0) + " noChange=" + (item.noDomChangeWithinWindowCount ?? 0) + " p50=" + (item.p50DomChangeDeltaMs ?? "-") + "ms p95=" + (item.p95DomChangeDeltaMs ?? "-") + "ms max=" + (item.maxDomChangeDeltaMs ?? "-") + "ms overBudget=" + (item.overBudgetCount ?? 0) + " window=" + (item.firstAt || "-") + ".." + (item.lastAt || "-")).join("\n")
|
||
: "- 无 API-DOM 候选分组。";
|
||
const apiDomLagWorstLines = Array.isArray(apiDomLag.worstCandidates) && apiDomLag.worstCandidates.length > 0
|
||
? apiDomLag.worstCandidates.slice(0, 40).map((item) => "- " + (item.ts || "-") + " " + (item.method || "-") + " " + (item.path || "-") + " status=" + (item.status ?? "-") + " confidence=" + (item.confidence || "-") + " domChange=" + (item.domChangeDeltaMs ?? "-") + "ms firstSample=" + (item.firstSampleDeltaMs ?? "-") + "ms session=" + (item.sessionId || "-") + " traceId=" + (item.traceId || "-") + " beforeSeq=" + (item.beforeSample?.seq ?? "-") + " changeSeq=" + (item.changeSample?.seq ?? "-")).join("\n")
|
||
: "- 无 API-DOM digest 变化候选。";
|
||
const metricLines = Array.isArray(report.sampleMetrics?.timeline) && report.sampleMetrics.timeline.length > 0
|
||
? report.sampleMetrics.timeline.slice(0, 120).map((item) => "- #" + item.seq + " " + item.ts + " prompt=" + item.promptIndex + " loadingCount=" + (item.loadingCount ?? 0) + " loadingOwners=" + (item.loadingOwnerCount ?? 0) + " totalElapsedSeconds=" + (item.totalElapsedSeconds ?? "-") + " recentUpdateSeconds=" + (item.recentUpdateSeconds ?? "-") + " terminal=" + item.terminalSeen + " finalText=" + item.finalResultTextSeen + " diagnostic=" + item.diagnosticSeen).join("\n")
|
||
: "- 无采样指标。";
|
||
const turnTimingTable = renderTurnTimingTable(report.sampleMetrics);
|
||
return "# web-probe observe analysis\n\n"
|
||
+ "- stateDir: " + report.stateDir + "\n"
|
||
+ "- generatedAt: " + report.generatedAt + "\n"
|
||
+ "- samples: " + report.counts.samples + "\n"
|
||
+ "- control: " + report.counts.control + "\n"
|
||
+ "- network: " + report.counts.network + "\n"
|
||
+ "- console: " + (report.counts.console ?? 0) + "\n"
|
||
+ "- errors: " + report.counts.errors + "\n\n"
|
||
+ "## Findings\n\n" + findingLines + "\n\n"
|
||
+ "## Project management\n\n"
|
||
+ "- enabled: " + String(projectSummary.enabled === true) + "\n"
|
||
+ "- projectSampleCount: " + (projectSummary.projectSampleCount ?? 0) + "\n"
|
||
+ "- mdtodoSampleCount: " + (projectSummary.mdtodoSampleCount ?? 0) + "\n"
|
||
+ "- latestPageKind: " + (projectSummary.latestPageKind || "-") + "\n"
|
||
+ "- latestPath: " + (projectSummary.latestPath || "-") + "\n"
|
||
+ "- latestCounts: source=" + (projectSummary.latestSourceCount ?? "-") + " file=" + (projectSummary.latestFileCount ?? "-") + " task=" + (projectSummary.latestTaskCount ?? "-") + "\n"
|
||
+ "- latestSelectedTaskRefHash: " + (projectSummary.latestSelectedTaskRefHash || "-") + "\n"
|
||
+ "- paneGap: actionable=" + (projectSummary.severePaneGapSampleCount ?? 0) + " ignoredInitialEmptyDetail=" + (projectSummary.ignoredPaneGapSampleCount ?? 0) + "\n"
|
||
+ "- launch: commands=" + (projectSummary.launchCommandCount ?? 0) + " success=" + (projectSummary.launchSuccessCount ?? 0) + " failure=" + (projectSummary.launchFailureCount ?? 0) + " otelTraceHeader=" + (projectSummary.launchWithOtelTraceHeaderCount ?? 0) + "\n"
|
||
+ "- projectApi: responses=" + (projectSummary.projectApiResponseCount ?? 0) + " failures=" + (projectSummary.projectApiFailureCount ?? 0) + " requestfailed=" + (projectSummary.projectApiRequestFailedCount ?? 0) + " slowPaths=" + (projectSummary.projectApiSlowPathCount ?? 0) + "\n\n"
|
||
+ "### Project samples\n\n" + projectSampleLines + "\n\n"
|
||
+ "### Project commands\n\n" + projectCommandLines + "\n\n"
|
||
+ "### Project API\n\n" + projectApiLines + "\n\n"
|
||
+ "## Command failures\n\n" + commandFailureLines + "\n\n"
|
||
+ "## Sample metrics\n\n"
|
||
+ "- sampleCount: " + (metricSummary.sampleCount ?? 0) + "\n"
|
||
+ "- withTotalElapsed: " + (metricSummary.withTotalElapsed ?? 0) + "\n"
|
||
+ "- withRecentUpdate: " + (metricSummary.withRecentUpdate ?? 0) + "\n"
|
||
+ "- diagnostics: " + (metricSummary.diagnostics ?? 0) + "\n"
|
||
+ "- loadingSamples: " + (metricSummary.loadingSampleCount ?? 0) + "\n"
|
||
+ "- loadingMaxCount: " + (metricSummary.loadingMaxCount ?? 0) + "\n"
|
||
+ "- loadingMaxOwnerCount: " + (metricSummary.loadingMaxOwnerCount ?? 0) + "\n"
|
||
+ "- loadingOwnerCount: " + (metricSummary.loadingOwnerCount ?? 0) + "\n"
|
||
+ "- loadingLongestContinuousSeconds: " + (metricSummary.loadingLongestContinuousSeconds ?? 0) + "\n"
|
||
+ "- loadingCurrentContinuousSeconds: " + (metricSummary.loadingCurrentContinuousSeconds ?? 0) + "\n"
|
||
+ "- loadingOverFiveSecondSegmentCount: " + (metricSummary.loadingOverFiveSecondSegmentCount ?? 0) + "\n"
|
||
+ "- sessionRailFallbackMajoritySampleCount: " + (metricSummary.sessionRailFallbackMajoritySampleCount ?? 0) + "\n"
|
||
+ "- sessionRailFallbackMaxRatio: " + (metricSummary.sessionRailFallbackMaxRatio ?? 0) + "\n"
|
||
+ "- sessionRailFallbackMaxCount: " + (metricSummary.sessionRailFallbackMaxCount ?? 0) + "\n"
|
||
+ "- promptSegments: " + (metricSummary.promptSegments ?? 0) + "\n\n"
|
||
+ "- turnColumns: " + (metricSummary.turnColumns ?? 0) + "\n"
|
||
+ "- turnTimingRows: " + (metricSummary.turnTimingRows ?? 0) + "\n"
|
||
+ "- turnTimingNonMonotonicCount: " + (metricSummary.turnTimingNonMonotonicCount ?? 0) + "\n"
|
||
+ "- turnTimingTotalElapsedDecreaseCount: " + (metricSummary.turnTimingTotalElapsedDecreaseCount ?? 0) + "\n"
|
||
+ "- turnTimingTotalElapsedForwardJumpCount: " + (metricSummary.turnTimingTotalElapsedForwardJumpCount ?? 0) + "\n"
|
||
+ "- turnTimingTotalElapsedForwardJumpMaxSeconds: " + (metricSummary.turnTimingTotalElapsedForwardJumpMaxSeconds ?? 0) + "\n"
|
||
+ "- turnTimingTerminalElapsedGrowthCount: " + (metricSummary.turnTimingTerminalElapsedGrowthCount ?? 0) + "\n"
|
||
+ "- turnTimingTerminalElapsedGrowthMaxSeconds: " + (metricSummary.turnTimingTerminalElapsedGrowthMaxSeconds ?? 0) + "\n"
|
||
+ "- turnTimingRecentUpdateJumpCount: " + (metricSummary.turnTimingRecentUpdateJumpCount ?? 0) + "\n"
|
||
+ "- turnTimingRecentUpdateSawtoothJumpCount: " + (metricSummary.turnTimingRecentUpdateSawtoothJumpCount ?? metricSummary.turnTimingRecentUpdateJumpCount ?? 0) + "\n"
|
||
+ "- turnTimingRecentUpdateStepCount: " + (metricSummary.turnTimingRecentUpdateStepCount ?? 0) + "\n"
|
||
+ "- turnTimingRecentUpdateMaxIncreaseSeconds: " + (metricSummary.turnTimingRecentUpdateMaxIncreaseSeconds ?? "-") + "\n"
|
||
+ "- turnTimingRecentUpdateMaxExcessSeconds: " + (metricSummary.turnTimingRecentUpdateMaxExcessSeconds ?? 0) + "\n"
|
||
+ "- turnTimingRecentUpdateResetCount: " + (metricSummary.turnTimingRecentUpdateResetCount ?? 0) + "\n\n"
|
||
+ "- codeAgentCardSampleCount: " + (metricSummary.codeAgentCardSampleCount ?? 0) + "\n"
|
||
+ "- codeAgentCardMissingElapsedCount: " + (metricSummary.codeAgentCardMissingElapsedCount ?? 0) + "\n"
|
||
+ "- codeAgentCardMissingRecentUpdateCount: " + (metricSummary.codeAgentCardMissingRecentUpdateCount ?? 0) + "\n"
|
||
+ "- codeAgentCardDurationUnderreportedCount: " + (metricSummary.codeAgentCardDurationUnderreportedCount ?? 0) + "\n"
|
||
+ "- codeAgentCardDurationMismatchCount: " + (metricSummary.codeAgentCardDurationMismatchCount ?? 0) + "\n"
|
||
+ "- traceRowCount: " + (metricSummary.traceRowCount ?? 0) + "\n"
|
||
+ "- traceRowOrderAnomalyCount: " + (metricSummary.traceRowOrderAnomalyCount ?? 0) + "\n"
|
||
+ "- traceRowCompletionNotLastCount: " + (metricSummary.traceRowCompletionNotLastCount ?? 0) + "\n"
|
||
+ "- roundCompletionEventCount: " + (metricSummary.roundCompletionEventCount ?? 0) + "\n"
|
||
+ "- roundCompletionElapsedMismatchCount: " + (metricSummary.roundCompletionElapsedMismatchCount ?? 0) + "\n"
|
||
+ "- roundCompletionFinalResponseMissingCount: " + (metricSummary.roundCompletionFinalResponseMissingCount ?? 0) + "\n"
|
||
+ "- roundCompletionPostTimingChangeCount: " + (metricSummary.roundCompletionPostTimingChangeCount ?? 0) + "\n\n"
|
||
+ "### Rounds\n\n" + roundLines + "\n\n"
|
||
+ "### Code Agent card timing display\n\n"
|
||
+ "- cardSampleCount: " + (codeAgentCardTimingSummary.cardSampleCount ?? 0) + "\n"
|
||
+ "- runningCardSampleCount: " + (codeAgentCardTimingSummary.runningCardSampleCount ?? 0) + "\n"
|
||
+ "- terminalCardSampleCount: " + (codeAgentCardTimingSummary.terminalCardSampleCount ?? 0) + "\n"
|
||
+ "- missingElapsedCount: " + (codeAgentCardTimingSummary.missingElapsedCount ?? 0) + "\n"
|
||
+ "- missingRecentUpdateCount: " + (codeAgentCardTimingSummary.missingRecentUpdateCount ?? 0) + "\n"
|
||
+ "- durationUnderreportedCount: " + (codeAgentCardTimingSummary.durationUnderreportedCount ?? 0) + "\n"
|
||
+ "- durationMismatchCount: " + (codeAgentCardTimingSummary.durationMismatchCount ?? 0) + "\n"
|
||
+ "- policy: Code Agent 卡片无论终态/非终态都必须显示耗时;非终态必须显示最近更新。completion/final-response 可用时是 sealed source-of-truth;DOM card elapsed 是显示源。该 analyzer 只报告采样到的页面表现,不做下游 repair。\n\n"
|
||
+ "#### Missing elapsed samples\n\n" + cardMissingElapsedLines + "\n\n"
|
||
+ "#### Missing recent update samples\n\n" + cardMissingRecentLines + "\n\n"
|
||
+ "#### Duration underreported samples\n\n" + durationUnderreportedLines + "\n\n"
|
||
+ "#### Duration mismatch samples\n\n" + durationMismatchLines + "\n\n"
|
||
+ "### Trace row visual order\n\n"
|
||
+ "- traceRowCount: " + (traceOrderSummary.traceRowCount ?? 0) + "\n"
|
||
+ "- orderAnomalyCount: " + (traceOrderSummary.orderAnomalyCount ?? 0) + "\n"
|
||
+ "- completionNotLastCount: " + (traceOrderSummary.completionNotLastCount ?? 0) + "\n"
|
||
+ "- policy: 可见 trace 行在同一 trace 内必须按 total/时钟/projected seq 单调展示;completion 行不得出现在同 trace 后续行之前。\n\n"
|
||
+ "#### Trace order anomalies\n\n" + traceOrderAnomalyLines + "\n\n"
|
||
+ "#### Completion row not last samples\n\n" + traceCompletionNotLastLines + "\n\n"
|
||
+ "### Round completion consistency\n\n"
|
||
+ "- completionEventCount: " + (codeAgentCardTimingSummary.roundCompletionEventCount ?? 0) + "\n"
|
||
+ "- elapsedMismatchCount: " + (codeAgentCardTimingSummary.roundCompletionElapsedMismatchCount ?? 0) + "\n"
|
||
+ "- finalResponseMissingCount: " + (codeAgentCardTimingSummary.roundCompletionFinalResponseMissingCount ?? 0) + "\n"
|
||
+ "- postTimingChangeCount: " + (codeAgentCardTimingSummary.roundCompletionPostTimingChangeCount ?? 0) + "\n"
|
||
+ "- postRecentUpdateVisibleCount: " + (codeAgentCardTimingSummary.roundCompletionPostRecentUpdateVisibleCount ?? 0) + "\n"
|
||
+ "- elapsedMismatchToleranceSeconds: " + (codeAgentCardTimingSummary.elapsedMismatchToleranceSeconds ?? "-") + "\n"
|
||
+ "- policy: 轮次完成(总耗时 ...) 是 trace sealed source-of-truth;卡片总耗时必须与它一致。完成后 final response 必须可见,耗时/最近更新不得继续跳变。\n\n"
|
||
+ "#### Round completion events\n\n" + roundCompletionLines + "\n\n"
|
||
+ "#### Completion elapsed mismatches\n\n" + roundCompletionMismatchLines + "\n\n"
|
||
+ "#### Final response missing after completion\n\n" + roundCompletionFinalMissingLines + "\n\n"
|
||
+ "#### Post-completion timing changes\n\n" + roundCompletionPostTimingLines + "\n\n"
|
||
+ "### Loading visibility: visible 加载中\n\n"
|
||
+ "- sampleCount: " + (loadingSummary.sampleCount ?? 0) + "\n"
|
||
+ "- loadingSampleCount: " + (loadingSummary.loadingSampleCount ?? 0) + "\n"
|
||
+ "- maxSimultaneousCount: " + (loadingSummary.maxSimultaneousCount ?? 0) + "\n"
|
||
+ "- maxSimultaneousOwnerCount: " + (loadingSummary.maxSimultaneousOwnerCount ?? 0) + "\n"
|
||
+ "- concurrentLoadingSampleCount: " + (loadingSummary.concurrentLoadingSampleCount ?? 0) + "\n"
|
||
+ "- ownerCount: " + (loadingSummary.ownerCount ?? 0) + "\n"
|
||
+ "- segmentCount: " + (loadingSummary.segmentCount ?? 0) + "\n"
|
||
+ "- overFiveSecondSegmentCount: " + (loadingSummary.overFiveSecondSegmentCount ?? 0) + "\n"
|
||
+ "- longestContinuousSeconds: " + (loadingSummary.longestContinuousSeconds ?? 0) + "\n"
|
||
+ "- currentContinuousSeconds: " + (loadingSummary.currentContinuousSeconds ?? 0) + "\n"
|
||
+ "- budgetSeconds: " + (loadingSummary.budgetSeconds ?? (Number.isFinite(Number(report.alertThresholds?.visibleLoadingSlowMs)) ? Number(report.alertThresholds.visibleLoadingSlowMs) / 1000 : "unconfigured")) + "\n"
|
||
+ "- policy: 该指标只能证明用户真实看到“加载中”的持续时间;修复必须降低真实请求/投影/渲染耗时,禁止提前展示未加载完内容来压低该指标。\n\n"
|
||
+ "#### Loading segments\n\n" + loadingSegmentLines + "\n\n"
|
||
+ "#### Loading owners\n\n" + loadingOwnerLines + "\n\n"
|
||
+ "#### Loading sample timeline\n\n" + loadingTimelineLines + "\n\n"
|
||
+ "### Session rail titles\n\n"
|
||
+ "- sampleCount: " + (sessionRailTitleSummary.sampleCount ?? 0) + "\n"
|
||
+ "- visibleSampleCount: " + (sessionRailTitleSummary.visibleSampleCount ?? 0) + "\n"
|
||
+ "- fallbackSampleCount: " + (sessionRailTitleSummary.fallbackSampleCount ?? 0) + "\n"
|
||
+ "- majorityFallbackSampleCount: " + (sessionRailTitleSummary.majorityFallbackSampleCount ?? 0) + "\n"
|
||
+ "- maxFallbackRatio: " + (sessionRailTitleSummary.maxFallbackRatio ?? 0) + "\n"
|
||
+ "- maxVisibleCount: " + (sessionRailTitleSummary.maxVisibleCount ?? 0) + "\n"
|
||
+ "- maxFallbackTitleCount: " + (sessionRailTitleSummary.maxFallbackTitleCount ?? 0) + "\n"
|
||
+ "- policy: 可见 session 列表中 'Session ses_...' fallback 标题超过一半必须报警;修复应让上游 session list projection 直接携带名称,不能靠点击详情后下游修补。\n\n"
|
||
+ "#### Session rail fallback samples\n\n" + sessionRailTitleSampleLines + "\n\n"
|
||
+ "#### Session rail fallback examples\n\n" + sessionRailTitleExampleLines + "\n\n"
|
||
+ "### Page provenance\n\n"
|
||
+ "- segmentCount: " + (report.pageProvenance?.summary?.segmentCount ?? 0) + "\n"
|
||
+ "- controlSegmentCount: " + (report.pageProvenance?.summary?.controlSegmentCount ?? 0) + "\n\n"
|
||
+ provenanceLines + "\n\n"
|
||
+ "### Page performance: same-origin API Resource Timing\n\n"
|
||
+ "- budgetMs: " + (report.pagePerformance?.summary?.budgetMs ?? sameOriginApiBudgetMs) + "\n"
|
||
+ "- sameOriginApiPathCount: " + (report.pagePerformance?.summary?.sameOriginApiPathCount ?? 0) + "\n"
|
||
+ "- sameOriginApiSampleCount: " + (report.pagePerformance?.summary?.sameOriginApiSampleCount ?? 0) + "\n"
|
||
+ "- longLivedStreamPathCount: " + (report.pagePerformance?.summary?.longLivedStreamPathCount ?? 0) + "\n"
|
||
+ "- longLivedStreamSampleCount: " + (report.pagePerformance?.summary?.longLivedStreamSampleCount ?? 0) + "\n"
|
||
+ "- longLivedStreamOpenOverFiveSecondPathCount: " + (report.pagePerformance?.summary?.longLivedStreamOpenOverFiveSecondPathCount ?? 0) + "\n"
|
||
+ "- longLivedStreamOpenOverFiveSecondSampleCount: " + (report.pagePerformance?.summary?.longLivedStreamOpenOverFiveSecondSampleCount ?? 0) + "\n"
|
||
+ "- longLivedStreamLifetimeOverFiveSecondSampleCount: " + (report.pagePerformance?.summary?.longLivedStreamLifetimeOverFiveSecondSampleCount ?? 0) + "\n"
|
||
+ "- slowPathCount: " + (report.pagePerformance?.summary?.slowPathCount ?? 0) + "\n"
|
||
+ "- slowSampleCount: " + (report.pagePerformance?.summary?.slowSampleCount ?? 0) + "\n"
|
||
+ "- worstP95Ms: " + (report.pagePerformance?.summary?.worstP95Ms ?? "-") + "\n\n"
|
||
+ performanceLines + "\n\n"
|
||
+ "### Page performance: long-lived streams\n\n"
|
||
+ "- policy: SSE/long-lived stream lifetime is not ordinary API load latency; only stream open latency is compared with the YAML usability budget, while disconnects remain runtime alerts.\n\n"
|
||
+ streamPerformanceLines + "\n\n"
|
||
+ "### Natural API to DOM lag candidates\n\n"
|
||
+ "- naturalApiResponseCount: " + (apiDomLagSummary.naturalApiResponseCount ?? 0) + "\n"
|
||
+ "- stateRelevantResponseCount: " + (apiDomLagSummary.stateRelevantResponseCount ?? 0) + "\n"
|
||
+ "- candidateCount: " + (apiDomLagSummary.candidateCount ?? 0) + "\n"
|
||
+ "- domChangedCount: " + (apiDomLagSummary.domChangedCount ?? 0) + "\n"
|
||
+ "- noDomChangeWithinWindowCount: " + (apiDomLagSummary.noDomChangeWithinWindowCount ?? 0) + "\n"
|
||
+ "- budgetMs: " + (apiDomLagSummary.budgetMs ?? "-") + "\n"
|
||
+ "- p95DomChangeDeltaMs: " + (apiDomLagSummary.p95DomChangeDeltaMs ?? "-") + "\n"
|
||
+ "- maxDomChangeDeltaMs: " + (apiDomLagSummary.maxDomChangeDeltaMs ?? "-") + "\n"
|
||
+ "- overBudgetCount: " + (apiDomLagSummary.overBudgetCount ?? 0) + "\n"
|
||
+ "- lowConfidenceStreamOpenCount: " + (apiDomLagSummary.lowConfidenceStreamOpenCount ?? 0) + "\n"
|
||
+ "- policy: 该指标是调查证据,不作为 Code Agent 阻塞项;/v1/workbench/events 只能代表 SSE stream-open 到后续 DOM 变化的低置信度候选。\n\n"
|
||
+ "#### API-DOM groups\n\n" + apiDomLagGroupLines + "\n\n"
|
||
+ "#### Worst API-DOM candidates\n\n" + apiDomLagWorstLines + "\n\n"
|
||
+ "### Prompt network\n\n" + promptNetworkLines + "\n\n"
|
||
+ "### Runtime alerts\n\n"
|
||
+ "- httpErrorCount: " + (alertSummary.httpErrorCount ?? 0) + "\n"
|
||
+ "- requestFailedCount: " + (alertSummary.requestFailedCount ?? 0) + "\n"
|
||
+ "- domDiagnosticSampleCount: " + (alertSummary.domDiagnosticSampleCount ?? 0) + "\n"
|
||
+ "- consoleAlertCount: " + (alertSummary.consoleAlertCount ?? 0) + "\n"
|
||
+ "- pageErrorCount: " + (alertSummary.pageErrorCount ?? 0) + "\n\n"
|
||
+ "#### HTTP errors\n\n" + httpAlertLines + "\n\n"
|
||
+ "#### Request failed\n\n" + requestFailedLines + "\n\n"
|
||
+ "#### DOM diagnostics\n\n" + domDiagnosticLines + "\n\n"
|
||
+ "#### Console alerts\n\n" + consoleAlertLines + "\n\n"
|
||
+ "#### Console alert groups\n\n" + consoleAlertGroupLines + "\n\n"
|
||
+ "### Turn timing table\n\n"
|
||
+ turnTimingTable + "\n\n"
|
||
+ "### Aggregate timeline\n\n"
|
||
+ metricLines + "\n\n"
|
||
+ "## Command timeline\n\n" + commandLines + "\n\n"
|
||
+ "## State transitions\n\n" + transitionLines + "\n";
|
||
}
|
||
|
||
async function fileMeta(file) {
|
||
const [buffer, stats] = await Promise.all([readFile(file), stat(file)]);
|
||
return { byteCount: stats.size, sha256: "sha256:" + createHash("sha256").update(buffer).digest("hex") };
|
||
}
|
||
|
||
function sha256(value) {
|
||
return "sha256:" + createHash("sha256").update(String(value)).digest("hex");
|
||
}
|
||
|
||
function urlPath(value) {
|
||
try {
|
||
const url = new URL(String(value || "http://invalid.local/"));
|
||
return url.pathname;
|
||
} catch {
|
||
return "-";
|
||
}
|
||
}
|
||
|
||
function compactLocation(value) {
|
||
if (!value || typeof value !== "object") return null;
|
||
return { urlPath: urlPath(value.url), lineNumber: value.lineNumber ?? null, columnNumber: value.columnNumber ?? null };
|
||
}
|
||
|
||
function limitText(value, limit) {
|
||
const text = String(value ?? "");
|
||
if (text.length <= limit) return text;
|
||
return text.slice(0, Math.max(0, limit - 1)) + "…";
|
||
}
|
||
`;
|
||
}
|