Files
pikasTech-unidesk/scripts/src/hwlab-node-web-observe-analyzer-source.ts
T
2026-06-30 01:22:46 +00:00

4899 lines
264 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// SPEC: PJ2026-01040111 long-running Workbench observation.
// Responsibility: Source string for the offline web-probe observe analyzer.
import { nodeWebObserveAnalyzerTimingSource } from "./hwlab-node-web-observe-analyzer-timing-source";
export function nodeWebObserveAnalyzerSource(): string {
return String.raw`#!/usr/bin/env node
import { createHash } from "node:crypto";
import { createReadStream } from "node:fs";
import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
import path from "node:path";
import { createInterface } from "node:readline";
const stateDir = path.resolve(process.env.UNIDESK_WEB_OBSERVE_STATE_DIR || process.argv[2] || ".state/web-observe/manual");
const archivePrefix = safeArchivePrefix(process.env.UNIDESK_WEB_OBSERVE_ANALYZE_ARCHIVE_PREFIX || "");
const analyzeTailSamples = (() => {
const raw = process.env.UNIDESK_WEB_OBSERVE_ANALYZE_TAIL_SAMPLES;
if (raw === undefined || raw === null || String(raw).trim() === "") return 360;
const parsed = Number(raw);
return Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : 360;
})();
const alertThresholds = parseAlertThresholds(process.env.UNIDESK_WEB_OBSERVE_ALERT_THRESHOLDS_JSON);
const projectManagementConfig = parseProjectManagementConfig(process.env.UNIDESK_WEB_OBSERVE_PROJECT_MANAGEMENT_JSON);
const dataDir = archivePrefix ? path.join(stateDir, "archive") : stateDir;
const dataFile = (name) => path.join(dataDir, archivePrefix ? archivePrefix + "-" + name : name);
const analysisDir = path.join(stateDir, "analysis");
const reportJsonPath = path.join(analysisDir, "report.json");
const reportMdPath = path.join(analysisDir, "report.md");
const jsonlReadIssues = [];
const sourceSamples = await readJsonl(dataFile("samples.jsonl"), { compact: compactSampleForAnalysis, tail: analyzeTailSamples });
const relatedJsonlTailLimit = analyzeTailSamples > 0 ? Math.max(1200, analyzeTailSamples * 50) : 0;
const smallJsonlTailLimit = analyzeTailSamples > 0 ? Math.max(500, analyzeTailSamples * 8) : 0;
const sourceSampleWindow = sampleTimeWindow(sourceSamples, 60_000);
const sourceControlAll = await readJsonl(dataFile("control.jsonl"), { tail: relatedJsonlTailLimit });
const analysisFocus = analysisFocusFromControl(sourceControlAll);
const sourceControl = filterRowsByTimeWindow(sourceControlAll, sourceSampleWindow);
const samples = applyAnalysisFocus(sourceSamples, analysisFocus);
const sampleWindow = sampleTimeWindow(samples, 60_000);
const controlWindow = analysisControlWindow(sampleWindow, analysisFocus, 1000);
const control = applyAnalysisFocus(filterRowsByTimeWindow(sourceControlAll, controlWindow), analysisFocus, 1000);
const sourceNetworkAll = await readJsonl(dataFile("network.jsonl"), { tail: relatedJsonlTailLimit });
const network = applyAnalysisFocus(filterRowsByTimeWindow(sourceNetworkAll, sampleWindow), analysisFocus);
const promptNetworkRows = applyAnalysisFocus(filterRowsByTimeWindow(sourceNetworkAll, controlWindow), analysisFocus);
const consoleEvents = applyAnalysisFocus(filterRowsByTimeWindow(await readJsonl(dataFile("console.jsonl"), { tail: smallJsonlTailLimit }), sampleWindow), analysisFocus);
const errors = applyAnalysisFocus(filterRowsByTimeWindow(await readJsonl(dataFile("errors.jsonl"), { tail: smallJsonlTailLimit }), sampleWindow), analysisFocus);
const artifacts = applyAnalysisFocus(filterRowsByTimeWindow(await readJsonl(dataFile("artifacts.jsonl"), { tail: smallJsonlTailLimit }), sampleWindow), analysisFocus);
const manifest = await readJson(path.join(stateDir, "manifest.json"));
const heartbeat = await readJson(path.join(stateDir, "heartbeat.json"));
const commandState = await readCommandState(stateDir);
await mkdir(analysisDir, { recursive: true, mode: 0o700 });
const transitions = buildTransitions(samples);
const sampleMetrics = buildSampleMetrics(samples, control);
const pageProvenance = buildPageProvenanceReport(samples, control, manifest);
const pagePerformance = buildPagePerformanceReport(samples, manifest);
const promptNetwork = buildPromptNetworkReport(control, promptNetworkRows);
const runtimeAlerts = buildRuntimeAlerts(samples, control, network, consoleEvents, errors);
const apiDomLag = buildApiDomLagReport(samples, network);
const projectManagement = buildProjectManagementReport(samples, control, network, pagePerformance, projectManagementConfig);
const runnerErrors = errors.slice(-8).map((item) => {
const details = item.error?.details && typeof item.error.details === "object" ? item.error.details : {};
const attempts = Array.isArray(item.error?.attempts) ? item.error.attempts : Array.isArray(details.attempts) ? details.attempts : [];
const lastAttempt = attempts.length > 0 ? attempts[attempts.length - 1] : null;
const readiness = lastAttempt?.readiness || lastAttempt?.readinessBeforeClick || details.readinessBeforeClick || details.readinessAfterWait || item.error?.navigationReadiness || null;
const readinessSnapshot = readiness?.snapshot || readiness;
return {
ts: item.ts ?? null,
type: item.type ?? null,
commandId: item.commandId ?? null,
sampleSeq: item.sampleSeq ?? null,
message: limitText(item.error?.message ?? item.message ?? "", 240),
retry: item.error?.auth?.lastRetryLabel ?? null,
retryExhausted: item.error?.auth?.retryExhausted === true,
lastError: limitText(item.error?.auth?.lastError ?? "", 160),
attemptCount: attempts.length,
lastFailureKind: lastAttempt?.failureKind ?? null,
lastReadinessReason: readiness?.reason ?? null,
lastReadiness: readinessSnapshot ? {
reason: readiness?.reason ?? readinessSnapshot.reason ?? null,
path: readinessSnapshot.path ?? null,
readyState: readinessSnapshot.readyState ?? null,
workbenchShellVisible: readinessSnapshot.workbenchShellVisible === true,
sessionCreatePresent: readinessSnapshot.sessionCreatePresent === true,
sessionCreateVisible: readinessSnapshot.sessionCreateVisible === true,
sessionRailPresent: readinessSnapshot.sessionRailPresent === true,
sessionRailCollapsed: readinessSnapshot.sessionRailCollapsed ?? null,
sessionCollapseTogglePresent: readinessSnapshot.sessionCollapseTogglePresent === true,
sessionCollapseToggleVisible: readinessSnapshot.sessionCollapseToggleVisible === true,
sessionCollapseToggleExpanded: readinessSnapshot.sessionCollapseToggleExpanded ?? null,
commandInputPresent: readinessSnapshot.commandInputPresent === true,
activeTabPresent: readinessSnapshot.activeTabPresent === true,
warningPresent: readinessSnapshot.warningPresent === true,
loginVisible: readinessSnapshot.loginVisible === true,
bodyTextHash: readinessSnapshot.bodyTextHash ?? null,
valuesRedacted: true
} : null,
navigationAttempts: attempts.slice(-5).map((attempt) => ({
attempt: attempt?.attempt ?? null,
ok: attempt?.ok === true,
failureKind: attempt?.failureKind ?? null,
message: limitText(attempt?.message ?? "", 160),
readinessReason: attempt?.readiness?.reason ?? null,
valuesRedacted: true
})),
};
});
const commandFailures = summarizeCommandFailures(control);
const toolFindings = buildToolFindings({ manifest, heartbeat, commandState });
const findings = [...toolFindings, ...buildProjectManagementFindings(projectManagement), ...buildFindings(samples, control, network, errors, sampleMetrics, promptNetwork, runtimeAlerts, pagePerformance, pageProvenance, commandFailures, manifest, apiDomLag)];
if (jsonlReadIssues.length > 0) findings.unshift({ id: "jsonl-read-issues", severity: "red", summary: "observer analyzer hit JSONL read/parse issues", count: jsonlReadIssues.length, issues: jsonlReadIssues.slice(0, 20) });
const recentWindow = buildRecentAnalysisWindow({ samples, control, network, consoleEvents, errors, manifest });
const commandTimeline = control.filter((item) => item.phase === "completed" || item.phase === "failed").map((item) => ({ ts: item.ts, phase: item.phase, commandId: item.commandId, type: item.type, input: item.input, afterUrl: item.afterUrl }));
const report = {
ok: findings.filter((item) => item.severity === "red").length === 0,
command: "web-probe-observe analyze",
generatedAt: new Date().toISOString(),
stateDir,
jsonlScope: { mode: archivePrefix ? "archive" : "current", archivePrefix: archivePrefix || null, dataDir, analyzeTailSamples, sourceSampleCount: sourceSamples.length, effectiveSampleCount: samples.length, sourceControlCount: sourceControlAll.length, sampleWindow, focus: analysisFocus, valuesRedacted: true },
alertThresholds,
manifest: compactManifest(manifest),
heartbeat: compactHeartbeat(heartbeat),
counts: { samples: samples.length, control: control.length, network: network.length, console: consoleEvents.length, errors: errors.length, artifacts: artifacts.length },
commandTimeline,
transitions,
sampleMetrics,
pageProvenance,
pagePerformance,
projectManagement,
promptNetwork,
runtimeAlerts,
apiDomLag,
runnerErrors,
commandFailures,
commandState,
toolFindings,
findings,
windows: { recent: recentWindow },
readIssues: jsonlReadIssues,
artifactSummary: await artifactSummary(artifacts),
safety: { offlineOnly: true, browserDriven: false, apiFetch: false, valuesRedacted: true },
};
await writeFile(reportJsonPath, JSON.stringify(report, null, 2) + "\n", { mode: 0o600 });
await writeFile(reportMdPath, renderMarkdown(report), { mode: 0o600 });
const [jsonMeta, mdMeta] = await Promise.all([fileMeta(reportJsonPath), fileMeta(reportMdPath)]);
console.log(JSON.stringify({
ok: true,
command: "web-probe-observe analyze",
stateDir,
jsonlScope: report.jsonlScope,
reportJsonPath,
reportJsonSha256: jsonMeta.sha256,
reportMdPath,
reportMdSha256: mdMeta.sha256,
counts: report.counts,
archiveSummary: {
sampleMetrics: sampleMetrics.summary,
pagePerformance: pagePerformance.summary,
projectManagement: projectManagement.summary,
runtimeAlerts: runtimeAlerts.summary,
apiDomLag: apiDomLag.summary,
findingCount: findings.length,
redFindingCount: findings.filter((item) => item.severity === "red").length,
redFindings: prioritizeFindings(findings.filter((item) => item.severity === "red")).slice(0, 12).map((item) => ({ kind: item.id ?? item.kind ?? item.code, severity: item.severity, count: item.count ?? item.sampleCount ?? null, summary: String(item.summary ?? item.message ?? "").slice(0, 180) })),
},
analysisWindow: recentWindow.summary,
jsonlReadIssues: jsonlReadIssues.slice(0, 3).map((item) => ({ file: item.file, line: item.line ?? null, error: String(item.error ?? "").slice(0, 160) })),
readIssues: jsonlReadIssues.slice(0, 3).map((item) => ({ file: item.file, line: item.line ?? null, error: String(item.error ?? "").slice(0, 160) })),
sampleMetrics: {
...recentWindow.sampleMetrics.summary,
rounds: recentWindow.sampleMetrics.rounds.slice(-8).map((item) => ({ promptIndex: item.promptIndex, promptTextHash: item.promptTextHash, sampleCount: item.sampleCount, firstSeq: item.firstSeq, lastSeq: item.lastSeq, lastTotalElapsedSeconds: item.lastTotalElapsedSeconds, lastRecentUpdateSeconds: item.lastRecentUpdateSeconds, loadingSamples: item.loadingSamples, maxLoadingCount: item.maxLoadingCount, loadingOwnerCount: item.loadingOwnerCount, diagnosticSamples: item.diagnosticSamples, terminalSamples: item.terminalSamples, finalTextSamples: item.finalTextSamples, turnTimingTotalElapsedZeroResetCount: item.turnTimingTotalElapsedZeroResetCount, turnTimingTotalElapsedForwardJumpCount: item.turnTimingTotalElapsedForwardJumpCount, turnTimingTotalElapsedForwardJumpMaxSeconds: item.turnTimingTotalElapsedForwardJumpMaxSeconds, turnTimingRecentUpdateJumpCount: item.turnTimingRecentUpdateJumpCount, turnTimingRecentUpdateMaxIncreaseSeconds: item.turnTimingRecentUpdateMaxIncreaseSeconds })),
turnColumns: recentWindow.sampleMetrics.turnColumns.slice(-12).map((item) => ({ label: item.label, source: item.source, pageRole: item.pageRole ?? null, pageId: item.pageId ?? null, pageEpoch: item.pageEpoch ?? null, promptIndex: item.promptIndex, lastPromptIndex: item.lastPromptIndex, firstSeq: item.firstSeq, lastSeq: item.lastSeq, traceId: item.traceId, messageId: item.messageId })),
loading: compactLoadingMetricsForOutput(recentWindow.sampleMetrics.loading),
sessionRailTitles: compactSessionRailTitleMetricsForOutput(recentWindow.sampleMetrics.sessionRailTitles),
},
pageProvenance: recentWindow.pageProvenance.summary,
pagePerformance: recentWindow.pagePerformance.summary,
projectManagement: compactProjectManagementForOutput(projectManagement),
promptNetwork: recentWindow.promptNetwork.summary,
runtimeAlerts: recentWindow.runtimeAlerts.summary,
apiDomLag: compactApiDomLagForOutput(apiDomLag),
runnerErrors,
commandFailures: commandFailures.slice(-8),
commandState,
toolFindings: toolFindings.slice(0, 8).map((item) => ({ kind: item.id, severity: item.severity, count: item.count ?? null, summary: String(item.summary ?? "").slice(0, 180) })),
httpErrorGroups: recentWindow.runtimeAlerts.networkHttpErrorsByPath.slice(0, 8).map((item) => ({
method: item.method ?? null,
status: item.status ?? null,
path: item.urlPath ?? null,
count: item.count,
firstAt: item.firstAt ?? null,
lastAt: item.lastAt ?? null,
promptIndexes: Array.isArray(item.promptIndexes) ? item.promptIndexes.slice(0, 6) : [],
failureKinds: Array.isArray(item.failureKinds) ? item.failureKinds.slice(0, 4) : [],
})),
requestFailedGroups: recentWindow.runtimeAlerts.networkRequestFailedByPath.slice(0, 8).map((item) => ({
method: item.method ?? null,
status: item.status ?? null,
path: item.urlPath ?? null,
count: item.count,
firstAt: item.firstAt ?? null,
lastAt: item.lastAt ?? null,
promptIndexes: Array.isArray(item.promptIndexes) ? item.promptIndexes.slice(0, 6) : [],
failureKinds: Array.isArray(item.failureKinds) ? item.failureKinds.slice(0, 4) : [],
})),
domDiagnosticGroups: recentWindow.runtimeAlerts.domDiagnosticsByFingerprint.slice(0, 5).map((item) => ({
diagnosticCode: item.diagnosticCode ?? null,
count: item.count,
firstAt: item.firstAt,
lastAt: item.lastAt,
promptIndexes: Array.isArray(item.promptIndexes) ? item.promptIndexes.slice(0, 6) : [],
traceIds: Array.isArray(item.traceIds) ? item.traceIds.slice(0, 4) : [],
text: String(item.preview ?? item.normalizedPreview ?? item.text ?? "").slice(0, 160),
})),
domDiagnosticSamples: recentWindow.runtimeAlerts.domDiagnostics.slice(-8).map((item) => ({
seq: item.seq ?? null,
ts: item.ts ?? null,
promptIndex: item.promptIndex ?? null,
source: item.source ?? null,
diagnosticCode: item.diagnosticCode ?? null,
traceId: item.traceId ?? null,
httpStatus: item.httpStatus ?? null,
idleSeconds: item.idleSeconds ?? null,
waitingFor: item.waitingFor ?? null,
lastEventLabel: item.lastEventLabel ?? null,
text: String(item.preview ?? item.text ?? "").slice(0, 180),
})),
consoleAlertGroups: recentWindow.runtimeAlerts.consoleAlertsByPath.slice(0, 8).map((item) => ({
type: item.type ?? null,
status: item.status ?? null,
path: item.urlPath ?? null,
count: item.count,
firstAt: item.firstAt ?? null,
lastAt: item.lastAt ?? null,
promptIndexes: Array.isArray(item.promptIndexes) ? item.promptIndexes.slice(0, 6) : [],
traceIds: Array.isArray(item.traceIds) ? item.traceIds.slice(0, 4) : [],
})),
consoleAlertSamples: recentWindow.runtimeAlerts.consoleAlerts.slice(-8).map((item) => ({
ts: item.ts ?? null,
promptIndex: item.promptIndex ?? null,
type: item.type ?? null,
status: item.status ?? null,
path: item.urlPath ?? null,
traceId: item.traceId ?? null,
text: String(item.preview ?? item.text ?? "").slice(0, 180),
})),
turnTimingRecentUpdateJumps: recentWindow.sampleMetrics.turnTimingRecentUpdateSawtoothJumps.slice(0, 8).map((item) => ({
columnLabel: item.columnLabel ?? item.columnId ?? null,
pageRole: item.pageRole ?? null,
pageId: item.pageId ?? null,
pageEpoch: item.pageEpoch ?? null,
promptIndex: item.promptIndex ?? null,
traceId: item.traceId ?? null,
fromSeq: item.fromSeq ?? null,
toSeq: item.toSeq ?? null,
fromTs: item.fromTs ?? null,
toTs: item.toTs ?? null,
fromValue: item.fromValue ?? null,
toValue: item.toValue ?? null,
delta: item.delta ?? null,
sampleDeltaSeconds: item.sampleDeltaSeconds ?? null,
allowedIncreaseSeconds: item.allowedIncreaseSeconds ?? null,
excessiveIncreaseSeconds: item.excessiveIncreaseSeconds ?? null,
timingSourceOfTruth: item.timingSourceOfTruth ?? null,
timingStatus: item.timingStatus ?? null,
})),
turnTimingElapsedZeroResets: recentWindow.sampleMetrics.turnTimingElapsedZeroResets.slice(0, 8).map((item) => ({
columnLabel: item.columnLabel ?? item.columnId ?? null,
pageRole: item.pageRole ?? null,
pageId: item.pageId ?? null,
pageEpoch: item.pageEpoch ?? null,
promptIndex: item.promptIndex ?? null,
traceId: item.traceId ?? null,
fromSeq: item.fromSeq ?? null,
toSeq: item.toSeq ?? null,
fromTs: item.fromTs ?? null,
toTs: item.toTs ?? null,
fromValue: item.fromValue ?? null,
toValue: item.toValue ?? null,
delta: item.delta ?? null,
anomaly: item.anomaly ?? null,
timingSourceOfTruth: item.timingSourceOfTruth ?? null,
timingStatus: item.timingStatus ?? null,
})),
turnTimingTotalElapsedForwardJumps: recentWindow.sampleMetrics.turnTimingTotalElapsedForwardJumps.slice(0, 8).map((item) => ({
columnLabel: item.columnLabel ?? item.columnId ?? null,
pageRole: item.pageRole ?? null,
pageId: item.pageId ?? null,
pageEpoch: item.pageEpoch ?? null,
promptIndex: item.promptIndex ?? null,
traceId: item.traceId ?? null,
fromSeq: item.fromSeq ?? null,
toSeq: item.toSeq ?? null,
fromTs: item.fromTs ?? null,
toTs: item.toTs ?? null,
fromValue: item.fromValue ?? null,
toValue: item.toValue ?? null,
delta: item.delta ?? null,
sampleDeltaSeconds: item.sampleDeltaSeconds ?? null,
allowedIncreaseSeconds: item.allowedIncreaseSeconds ?? null,
excessiveIncreaseSeconds: item.excessiveIncreaseSeconds ?? null,
anomaly: item.anomaly ?? null,
timingSourceOfTruth: item.timingSourceOfTruth ?? null,
timingStatus: item.timingStatus ?? null,
})),
pagePerformanceSlowApi: recentWindow.pagePerformance.sameOriginApiByPath.filter((item) => item.isLongLivedStream !== true && Number(item.overBudgetCount ?? item.overFiveSecondCount ?? 0) > 0).slice(0, 5).map((item) => ({ path: item.path, route: item.route, sampleCount: item.sampleCount, p95Ms: item.p95Ms, maxMs: item.maxMs, overBudgetCount: item.overBudgetCount, budgetMs: item.budgetMs, overFiveSecondCount: item.overFiveSecondCount, slowSamples: item.slowSamples })),
archivePagePerformanceSlowApi: pagePerformance.sameOriginApiByPath.filter((item) => item.isLongLivedStream !== true && Number(item.overBudgetCount ?? item.overFiveSecondCount ?? 0) > 0).slice(0, 8).map((item) => ({ path: item.path, route: item.route, sampleCount: item.sampleCount, p95Ms: item.p95Ms, maxMs: item.maxMs, overBudgetCount: item.overBudgetCount, budgetMs: item.budgetMs, overFiveSecondCount: item.overFiveSecondCount, slowSamples: item.slowSamples })),
pagePerformancePartialApi: recentWindow.pagePerformance.sameOriginApiByPath.filter((item) => item.isLongLivedStream !== true && Number(item.partialOverBudgetCount ?? item.partialOverFiveSecondCount ?? 0) > 0).slice(0, 5).map((item) => ({ path: item.path, route: item.route, sampleCount: item.sampleCount, completeTimingSampleCount: item.completeTimingSampleCount, partialTimingSampleCount: item.partialTimingSampleCount, partialOverBudgetCount: item.partialOverBudgetCount, budgetMs: item.partialBudgetMs, partialOverFiveSecondCount: item.partialOverFiveSecondCount, partialSamples: item.partialSamples })),
pagePerformanceSseStreams: recentWindow.pagePerformance.sameOriginApiByPath.filter((item) => item.isLongLivedStream === true).slice(0, 5).map((item) => ({ path: item.path, route: item.route, sampleCount: item.sampleCount, streamOpenSampleCount: item.streamOpenSampleCount, streamOpenP95Ms: item.streamOpenP95Ms, streamOpenMaxMs: item.streamOpenMaxMs, streamOpenOverBudgetCount: item.streamOpenOverBudgetCount, streamOpenBudgetMs: item.streamOpenBudgetMs, streamOpenOverFiveSecondCount: item.streamOpenOverFiveSecondCount, streamLifetimeOverFiveSecondCount: item.streamLifetimeOverFiveSecondCount, slowSamples: item.slowSamples })),
findings: prioritizeFindings(recentWindow.findings).slice(0, 8).map((item) => ({ kind: item.id ?? item.kind ?? item.code, severity: item.severity, count: item.count ?? item.sampleCount ?? null, timingSourceOfTruth: item.timingSourceOfTruth ?? null, timingStatus: item.timingStatus ?? null, summary: String(item.summary ?? item.message ?? "").slice(0, 180) })),
traceOrderAnomalies: (recentWindow.sampleMetrics?.traceOrder?.orderAnomalies || []).slice(0, 8).map((item) => ({
sampleSeq: item.sampleSeq ?? null,
sampleIndex: item.sampleIndex ?? null,
timestamp: item.timestamp ?? null,
pageRole: item.pageRole ?? null,
traceId: item.traceId ?? null,
reasons: item.reasons ?? [],
previousRowIndex: item.previousRowIndex ?? null,
currentRowIndex: item.currentRowIndex ?? null,
previousTotalSeconds: item.previousTotalSeconds ?? null,
currentTotalSeconds: item.currentTotalSeconds ?? null,
previousProjectedSeq: item.previousProjectedSeq ?? null,
currentProjectedSeq: item.currentProjectedSeq ?? null,
previousSourceSeq: item.previousSourceSeq ?? null,
currentSourceSeq: item.currentSourceSeq ?? null,
previousEventSeq: item.previousEventSeq ?? null,
currentEventSeq: item.currentEventSeq ?? null,
previousPreview: String(item.previousPreview || "").slice(0, 120),
currentPreview: String(item.currentPreview || "").slice(0, 120),
})),
traceCompletionNotLast: (recentWindow.sampleMetrics?.traceOrder?.completionNotLast || []).slice(0, 8).map((item) => ({
sampleSeq: item.sampleSeq ?? null,
sampleIndex: item.sampleIndex ?? null,
timestamp: item.timestamp ?? null,
pageRole: item.pageRole ?? null,
traceId: item.traceId ?? null,
completionRowIndex: item.completionRowIndex ?? null,
laterRowIndex: item.laterRowIndex ?? null,
completionTotalSeconds: item.completionTotalSeconds ?? null,
laterTotalSeconds: item.laterTotalSeconds ?? null,
completionProjectedSeq: item.completionProjectedSeq ?? null,
laterProjectedSeq: item.laterProjectedSeq ?? null,
completionPreview: String(item.completionPreview || "").slice(0, 120),
laterPreview: String(item.laterPreview || "").slice(0, 120),
})),
roundCompletionElapsedMismatches: (recentWindow.sampleMetrics?.codeAgentCardTiming?.roundCompletion?.elapsedMismatches || []).slice(0, 8).map((item) => ({
seq: item.seq ?? null,
ts: item.ts ?? null,
pageRole: item.pageRole ?? null,
promptIndex: item.promptIndex ?? null,
traceId: item.traceId ?? null,
completionElapsedSeconds: item.completionElapsedSeconds ?? null,
cardTotalElapsedSeconds: item.cardTotalElapsedSeconds ?? null,
deltaSeconds: item.deltaSeconds ?? null,
toleranceSeconds: item.toleranceSeconds ?? null,
timingSourceOfTruth: item.timingSourceOfTruth ?? null,
timingStatus: item.timingStatus ?? null,
})),
codeAgentCardDurationUnderreported: (recentWindow.sampleMetrics?.codeAgentCardTiming?.durationUnderreported || []).slice(0, 8).map((item) => ({
sampleIndex: item.sampleIndex ?? null,
timestamp: item.timestamp ?? null,
pageRole: item.pageRole ?? null,
traceId: item.traceId ?? null,
cardTotalElapsedSeconds: item.cardTotalElapsedSeconds ?? null,
expectedElapsedSeconds: item.expectedElapsedSeconds ?? null,
deltaSeconds: item.deltaSeconds ?? null,
evidenceKind: item.evidenceKind ?? null,
timingSourceOfTruth: item.timingSourceOfTruth ?? null,
timingStatus: item.timingStatus ?? null,
evidencePreview: String(item.evidencePreview || "").slice(0, 120),
})),
codeAgentCardDurationMismatches: (recentWindow.sampleMetrics?.codeAgentCardTiming?.durationMismatches || []).slice(0, 8).map((item) => ({
sampleIndex: item.sampleIndex ?? null,
timestamp: item.timestamp ?? null,
pageRole: item.pageRole ?? null,
traceId: item.traceId ?? null,
direction: item.direction ?? null,
cardTotalElapsedSeconds: item.cardTotalElapsedSeconds ?? null,
expectedElapsedSeconds: item.expectedElapsedSeconds ?? null,
signedDeltaSeconds: item.signedDeltaSeconds ?? null,
deltaSeconds: item.deltaSeconds ?? null,
evidenceKind: item.evidenceKind ?? null,
exactEvidence: item.exactEvidence === true,
timingSourceOfTruth: item.timingSourceOfTruth ?? null,
timingStatus: item.timingStatus ?? null,
evidencePreview: String(item.evidencePreview || "").slice(0, 120),
})),
valuesRedacted: true,
}));
async function readJson(file) {
try { return JSON.parse(await readFile(file, "utf8")); } catch { return null; }
}
async function readCommandState(rootDir) {
const buckets = {};
for (const bucket of ["pending", "processing", "done", "failed", "abandoned"]) {
buckets[bucket] = await readCommandBucket(path.join(rootDir, "commands", bucket), bucket);
}
return {
pendingCount: buckets.pending.count,
processingCount: buckets.processing.count,
doneCount: buckets.done.count,
failedCount: buckets.failed.count,
abandonedCount: buckets.abandoned.count,
pending: buckets.pending.items,
processing: buckets.processing.items,
failed: buckets.failed.items.slice(0, 12),
abandoned: buckets.abandoned.items.slice(0, 20),
summary: {
backlogCount: buckets.pending.count + buckets.processing.count,
oldestPendingAgeSeconds: buckets.pending.oldestAgeSeconds,
oldestProcessingAgeSeconds: buckets.processing.oldestAgeSeconds,
valuesRedacted: true
},
valuesRedacted: true
};
}
async function readCommandBucket(dir, bucket) {
let names = [];
try {
names = (await readdir(dir)).filter((name) => name.endsWith(".json")).sort();
} catch (error) {
if (!error || error.code !== "ENOENT") jsonlReadIssues.push({ file: path.basename(dir), kind: "command-dir-read-error", code: error && error.code ? String(error.code) : null, errorMessage: limitText(error && error.message ? error.message : String(error), 240) });
return { bucket, count: 0, oldestAgeSeconds: null, items: [] };
}
const items = [];
for (const name of names.slice(0, 200)) {
const file = path.join(dir, name);
const parsed = await readJson(file) || {};
let meta = null;
try { meta = await stat(file); } catch {}
const timestamp = parsed.createdAt || parsed.abandonedAt || parsed.completedAt || parsed.failedAt || (meta ? meta.mtime.toISOString() : null);
const timestampMs = Date.parse(String(timestamp || ""));
items.push({
bucket,
id: parsed.id || parsed.commandId || name.replace(/[.]json$/u, ""),
type: parsed.type || parsed.command?.type || null,
createdAt: parsed.createdAt || null,
completedAt: parsed.completedAt || null,
failedAt: parsed.failedAt || null,
abandonedAt: parsed.abandonedAt || null,
reason: parsed.reason || parsed.error?.message || null,
ageSeconds: Number.isFinite(timestampMs) ? Math.max(0, Math.round((Date.now() - timestampMs) / 1000)) : null,
mtime: meta ? meta.mtime.toISOString() : null,
valuesRedacted: true
});
}
const ages = items.map((item) => item.ageSeconds).filter((value) => Number.isFinite(value));
return { bucket, count: names.length, oldestAgeSeconds: ages.length > 0 ? Math.max(...ages) : null, items };
}
function buildToolFindings({ manifest, heartbeat, commandState }) {
const findings = [];
const diagnostics = heartbeatDiagnostics(manifest, heartbeat);
if (diagnostics.heartbeatStale) {
findings.push({
id: "tool-runner-heartbeat-stale",
severity: "red",
summary: "web-probe observe runner heartbeat is stale; treat this as observer tooling failure, not Workbench behavior",
count: 1,
diagnostics,
valuesRedacted: true
});
}
if ((commandState?.pendingCount ?? 0) > 0 || (commandState?.processingCount ?? 0) > 0) {
findings.push({
id: "tool-pending-commands-unconsumed",
severity: "red",
summary: "web-probe observe has pending/processing control commands that were not consumed by the runner",
count: (commandState?.pendingCount ?? 0) + (commandState?.processingCount ?? 0),
pending: (commandState?.pending ?? []).slice(0, 12),
processing: (commandState?.processing ?? []).slice(0, 12),
valuesRedacted: true
});
}
if ((commandState?.abandonedCount ?? 0) > 0) {
findings.push({
id: "tool-commands-abandoned",
severity: "info",
summary: "web-probe observe force-stop abandoned queued commands; do not interpret these as Workbench command failures",
count: commandState.abandonedCount,
commands: (commandState.abandoned ?? []).slice(0, 20),
valuesRedacted: true
});
}
if (heartbeat?.forceStop || manifest?.forceStop) {
findings.push({
id: "tool-runner-force-stopped",
severity: "info",
summary: "web-probe observe runner was stopped by the CLI outside the command queue",
count: 1,
forceStop: heartbeat?.forceStop || manifest?.forceStop,
valuesRedacted: true
});
}
return findings;
}
function heartbeatDiagnostics(manifest, heartbeat) {
const status = String(heartbeat?.status || manifest?.status || "");
const terminal = /^(completed|failed|force-stopped|stopped|abandoned)$/u.test(status);
const sampleIntervalMs = Number(manifest?.sampling?.sampleIntervalMs) || 5000;
const staleAfterMs = Math.max(60000, sampleIntervalMs * 3);
const updatedAt = heartbeat?.updatedAt || heartbeat?.lastSampleAt || null;
const updatedMs = Date.parse(String(updatedAt || ""));
const ageSeconds = Number.isFinite(updatedMs) ? Math.max(0, Math.round((Date.now() - updatedMs) / 1000)) : null;
const heartbeatStale = !terminal && (!Number.isFinite(updatedMs) || Date.now() - updatedMs > staleAfterMs);
return {
status: status || null,
terminal,
updatedAt,
heartbeatAgeSeconds: ageSeconds,
heartbeatStale,
heartbeatStaleAfterSeconds: Math.round(staleAfterMs / 1000),
sampleSeq: heartbeat?.sampleSeq ?? null,
commandSeq: heartbeat?.commandSeq ?? null,
activeCommandId: heartbeat?.activeCommandId ?? null,
valuesRedacted: true
};
}
function safeArchivePrefix(value) {
const text = String(value || "").trim();
if (!text) return "";
if (!/^[A-Za-z0-9_.-]+$/u.test(text) || text.includes("..")) throw new Error("unsafe archive prefix: " + text);
return text;
}
function positiveNumber(value, fallback) {
const parsed = Number(value);
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
}
function requiredPositiveThreshold(raw, key) {
const parsed = Number(raw?.[key]);
if (!Number.isFinite(parsed) || parsed <= 0) {
throw new Error("UNIDESK_WEB_OBSERVE_ALERT_THRESHOLDS_JSON requires positive " + key + "; configure config/hwlab-node-lanes.yaml webProbe.alertThresholds");
}
return parsed;
}
function parseAlertThresholds(value) {
if (!value) {
throw new Error("UNIDESK_WEB_OBSERVE_ALERT_THRESHOLDS_JSON is required; configure config/hwlab-node-lanes.yaml webProbe.alertThresholds for the selected node/lane");
}
const raw = (() => {
try { return JSON.parse(value); } catch (error) { throw new Error("UNIDESK_WEB_OBSERVE_ALERT_THRESHOLDS_JSON is invalid JSON: " + (error instanceof Error ? error.message : String(error))); }
})();
const sessionRailFallbackRatio = requiredPositiveThreshold(raw, "sessionRailFallbackRatio");
if (sessionRailFallbackRatio > 1) {
throw new Error("UNIDESK_WEB_OBSERVE_ALERT_THRESHOLDS_JSON sessionRailFallbackRatio must be <= 1");
}
return {
sameOriginApiSlowMs: requiredPositiveThreshold(raw, "sameOriginApiSlowMs"),
partialApiSlowMs: requiredPositiveThreshold(raw, "partialApiSlowMs"),
longLivedStreamOpenSlowMs: requiredPositiveThreshold(raw, "longLivedStreamOpenSlowMs"),
visibleLoadingSlowMs: requiredPositiveThreshold(raw, "visibleLoadingSlowMs"),
turnTimingSampleSlackSeconds: requiredPositiveThreshold(raw, "turnTimingSampleSlackSeconds"),
turnElapsedSevereTimeoutSeconds: requiredPositiveThreshold(raw, "turnElapsedSevereTimeoutSeconds"),
domEvaluateTimeoutRedCount: requiredPositiveThreshold(raw, "domEvaluateTimeoutRedCount"),
domEvaluateTimeoutRedWindowMs: requiredPositiveThreshold(raw, "domEvaluateTimeoutRedWindowMs"),
screenshotTimeoutRedCount: requiredPositiveThreshold(raw, "screenshotTimeoutRedCount"),
pageErrorRedCount: requiredPositiveThreshold(raw, "pageErrorRedCount"),
uncommandedStateChangeCommandWindowMs: requiredPositiveThreshold(raw, "uncommandedStateChangeCommandWindowMs"),
scrollJumpCommandWindowMs: requiredPositiveThreshold(raw, "scrollJumpCommandWindowMs"),
scrollJumpFromY: requiredPositiveThreshold(raw, "scrollJumpFromY"),
scrollJumpToY: requiredPositiveThreshold(raw, "scrollJumpToY"),
sessionRailFallbackRatio,
crossPageProjectionDivergenceRedMs: positiveNumber(raw.crossPageProjectionDivergenceRedMs, requiredPositiveThreshold(raw, "visibleLoadingSlowMs")),
source: "yaml-env",
};
}
function parseProjectManagementConfig(value) {
if (!value || value === "null") {
return {
enabled: false,
targetPaths: [],
readinessSelectors: [],
naturalApiPathPrefixes: [],
commandAllowlist: [],
launchRoute: "",
slowApiBudgetMs: 0,
source: "yaml-env",
valuesRedacted: true
};
}
const raw = (() => {
try { return JSON.parse(value); } catch (error) { throw new Error("UNIDESK_WEB_OBSERVE_PROJECT_MANAGEMENT_JSON is invalid JSON: " + (error instanceof Error ? error.message : String(error))); }
})();
if (raw?.enabled !== true && raw?.enabled !== false) throw new Error("UNIDESK_WEB_OBSERVE_PROJECT_MANAGEMENT_JSON requires boolean enabled");
if (raw.enabled !== true) return { enabled: false, targetPaths: [], readinessSelectors: [], naturalApiPathPrefixes: [], commandAllowlist: [], launchRoute: "", slowApiBudgetMs: 0, source: "yaml-env", valuesRedacted: true };
const stringList = (key) => {
const list = raw?.[key];
if (!Array.isArray(list) || list.some((item) => typeof item !== "string" || item.length === 0)) throw new Error("UNIDESK_WEB_OBSERVE_PROJECT_MANAGEMENT_JSON requires string[] " + key + "; configure config/hwlab-node-lanes.yaml webProbe.projectManagement");
return list;
};
const slowApiBudgetMs = Number(raw?.slowApiBudgetMs);
if (!Number.isFinite(slowApiBudgetMs) || slowApiBudgetMs <= 0) throw new Error("UNIDESK_WEB_OBSERVE_PROJECT_MANAGEMENT_JSON requires positive slowApiBudgetMs");
const launchRoute = String(raw.launchRoute || "");
if (!launchRoute.startsWith("/")) throw new Error("UNIDESK_WEB_OBSERVE_PROJECT_MANAGEMENT_JSON launchRoute must be an absolute path");
return {
enabled: true,
targetPaths: stringList("targetPaths"),
readinessSelectors: stringList("readinessSelectors"),
naturalApiPathPrefixes: stringList("naturalApiPathPrefixes"),
commandAllowlist: stringList("commandAllowlist"),
launchRoute,
slowApiBudgetMs,
source: "yaml-env",
valuesRedacted: true
};
}
async function readJsonl(file, options = {}) {
const tailLimit = Number.isFinite(Number(options.tail)) && Number(options.tail) > 0 ? Math.floor(Number(options.tail)) : 0;
if (tailLimit > 0) return readJsonlTail(file, tailLimit, options);
const rows = [];
try {
const input = createReadStream(file, { encoding: "utf8" });
const lines = createInterface({ input, crlfDelay: Infinity });
let lineNo = 0;
for await (const rawLine of lines) {
lineNo += 1;
const line = String(rawLine || "").trim();
if (!line) continue;
try {
const parsed = JSON.parse(line);
rows.push(typeof options.compact === "function" ? options.compact(parsed) : parsed);
} catch (error) {
const item = { parseError: true, lineNo, rawHash: sha256(line), errorMessage: limitText(error && error.message ? error.message : String(error), 240) };
rows.push(item);
if (jsonlReadIssues.length < 50) jsonlReadIssues.push({ file: path.basename(file), kind: "parse-error", lineNo, rawHash: item.rawHash, errorMessage: item.errorMessage });
}
}
return rows;
} catch (error) {
if (error && error.code === "ENOENT") return [];
if (jsonlReadIssues.length < 50) jsonlReadIssues.push({ file: path.basename(file), kind: "read-error", code: error && error.code ? String(error.code) : null, errorMessage: limitText(error && error.message ? error.message : String(error), 240) });
return [];
}
}
async function readJsonlTail(file, limit, options = {}) {
try {
const lines = await readTailLines(file, limit);
const rows = [];
let lineNo = 0;
for (const rawLine of lines) {
lineNo += 1;
const line = String(rawLine || "").trim();
if (!line) continue;
try {
const parsed = JSON.parse(line);
rows.push(typeof options.compact === "function" ? options.compact(parsed) : parsed);
} catch (error) {
const item = { parseError: true, lineNo, tail: true, rawHash: sha256(line), errorMessage: limitText(error && error.message ? error.message : String(error), 240) };
rows.push(item);
if (jsonlReadIssues.length < 50) jsonlReadIssues.push({ file: path.basename(file), kind: "parse-error", lineNo, tail: true, rawHash: item.rawHash, errorMessage: item.errorMessage });
}
}
return rows;
} catch (error) {
if (error && error.code === "ENOENT") return [];
if (jsonlReadIssues.length < 50) jsonlReadIssues.push({ file: path.basename(file), kind: "read-error", tail: true, code: error && error.code ? String(error.code) : null, errorMessage: limitText(error && error.message ? error.message : String(error), 240) });
return [];
}
}
async function readTailLines(file, limit) {
const info = await stat(file);
if (!info.size || limit <= 0) return [];
const chunkSize = 64 * 1024;
const maxBytes = Math.max(32 * 1024 * 1024, Math.min(256 * 1024 * 1024, limit * 512 * 1024));
const chunks = [];
let position = info.size;
let readBytes = 0;
let newlineCount = 0;
while (position > 0 && newlineCount <= limit && readBytes < maxBytes) {
const readSize = Math.min(chunkSize, position, maxBytes - readBytes);
position -= readSize;
const chunk = await readFileSlice(file, position, readSize);
chunks.unshift(chunk);
readBytes += chunk.length;
for (let index = 0; index < chunk.length; index += 1) {
if (chunk[index] === 10) newlineCount += 1;
}
}
if (position > 0 && newlineCount <= limit && jsonlReadIssues.length < 50) {
jsonlReadIssues.push({ file: path.basename(file), kind: "tail-scan-truncated", limit, readBytes, fileBytes: info.size });
}
const text = Buffer.concat(chunks).toString("utf8");
let lines = text.split(/\r?\n/u);
if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
if (position > 0 && lines.length > 0) lines.shift();
return lines.slice(-limit);
}
async function readFileSlice(file, start, length) {
const chunks = [];
await new Promise((resolve, reject) => {
const stream = createReadStream(file, { start, end: start + length - 1 });
stream.on("data", (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
stream.on("error", reject);
stream.on("end", resolve);
});
return Buffer.concat(chunks);
}
function sampleTimeWindow(samples, paddingMs) {
const times = samples
.map((item) => Date.parse(String(item && item.ts || "")))
.filter((value) => Number.isFinite(value));
if (times.length === 0) return { startMs: null, endMs: null, startAt: null, endAt: null, paddingMs };
const startMs = Math.max(0, Math.min(...times) - Math.max(0, paddingMs || 0));
const endMs = Math.max(...times) + Math.max(0, paddingMs || 0);
return { startMs, endMs, startAt: new Date(startMs).toISOString(), endAt: new Date(endMs).toISOString(), paddingMs };
}
function filterRowsByTimeWindow(rows, window) {
if (!window || !Number.isFinite(window.startMs) || !Number.isFinite(window.endMs)) return rows;
return rows.filter((item) => {
const value = item && (item.ts || item.observedAt || item.startedAt || item.finishedAt || item.createdAt || item.updatedAt);
const ms = Date.parse(String(value || ""));
if (!Number.isFinite(ms)) return true;
return ms >= window.startMs && ms <= window.endMs;
});
}
function analysisFocusFromControl(control) {
const completedNewSessions = (Array.isArray(control) ? control : [])
.filter((item) => item?.type === "newSession" && item?.phase === "completed" && Number.isFinite(Date.parse(String(item.ts || ""))))
.sort((a, b) => Date.parse(String(a.ts || "")) - Date.parse(String(b.ts || "")));
const latest = completedNewSessions.at(-1) ?? null;
if (!latest) return { mode: "all", reason: "no-new-session-command", startAt: null, startMs: null, commandId: null, valuesRedacted: true };
const startMs = Date.parse(String(latest.ts));
return {
mode: "after-new-session",
reason: "latest-completed-new-session",
startAt: new Date(startMs).toISOString(),
startMs,
commandId: latest.commandId ?? null,
valuesRedacted: true
};
}
function applyAnalysisFocus(rows, focus, graceMs = 0) {
if (!focus || !Number.isFinite(focus.startMs)) return rows;
const minMs = focus.startMs - Math.max(0, Number(graceMs) || 0);
return (Array.isArray(rows) ? rows : []).filter((item) => {
const value = item && (item.ts || item.observedAt || item.startedAt || item.finishedAt || item.createdAt || item.updatedAt);
const ms = Date.parse(String(value || ""));
return Number.isFinite(ms) ? ms >= minMs : true;
});
}
function analysisControlWindow(sampleWindow, focus, graceMs = 0) {
if (!sampleWindow || !Number.isFinite(sampleWindow.endMs)) return sampleWindow;
if (!focus || !Number.isFinite(focus.startMs)) return sampleWindow;
const startMs = Math.max(0, Math.min(Number(sampleWindow.startMs ?? focus.startMs), focus.startMs - Math.max(0, Number(graceMs) || 0)));
return {
...sampleWindow,
startMs,
startAt: new Date(startMs).toISOString()
};
}
function compactSampleForAnalysis(sample) {
if (!sample || typeof sample !== "object") return sample;
return {
seq: sample.seq ?? null,
ts: sample.ts ?? null,
reason: sample.reason ?? null,
sampleGroupSeq: sample.sampleGroupSeq ?? null,
pageId: sample.pageId ?? null,
pageRole: sample.pageRole ?? null,
commandId: sample.commandId ?? null,
observerInitiated: sample.observerInitiated ?? null,
url: sample.url ?? null,
path: sample.path ?? null,
title: sample.title ?? null,
routeSessionId: sample.routeSessionId ?? null,
activeSessionId: sample.activeSessionId ?? null,
messages: compactDomItems(sample.messages),
traceRows: compactDomItems(sample.traceRows),
loadings: compactLoadingItems(sample.loadings),
sessionRail: compactSessionRail(sample.sessionRail),
turns: compactDomItems(sample.turns),
diagnostics: compactDomItems(sample.diagnostics),
composer: compactComposer(sample.composer),
projectManagement: compactProjectManagementSample(sample.projectManagement),
pageProvenance: compactSamplePageProvenance(sample.pageProvenance),
performance: compactPerformanceItems(sample.performance)
};
}
function compactProjectManagementSample(value) {
if (!value || typeof value !== "object") return null;
return {
pageKind: value.pageKind ?? null,
configuredPath: value.configuredPath === true,
rootVisible: value.rootVisible === true,
mdtodoVisible: value.mdtodoVisible === true,
sourceCount: value.sourceCount ?? null,
fileCount: value.fileCount ?? null,
taskCount: value.taskCount ?? null,
taskRefMissingCount: value.taskRefMissingCount ?? null,
selectedSourceId: value.selectedSourceId ?? null,
selectedFileRef: value.selectedFileRef ?? null,
selectedTaskRef: value.selectedTaskRef ?? null,
selectedTaskStatus: value.selectedTaskStatus ?? null,
sourceSelectVisible: value.sourceSelectVisible === true,
fileSelectVisible: value.fileSelectVisible === true,
sourceConfigVisible: value.sourceConfigVisible === true,
taskEditorVisible: value.taskEditorVisible === true,
taskBodyVisible: value.taskBodyVisible === true,
taskBody: value.taskBody ?? null,
reportLinkCount: value.reportLinkCount ?? 0,
reportPreviewVisible: value.reportPreviewVisible === true,
reportPreview: value.reportPreview ?? null,
reportFullscreenVisible: value.reportFullscreenVisible === true,
newTaskDraftVisible: value.newTaskDraftVisible === true,
taskStatusCounts: value.taskStatusCounts && typeof value.taskStatusCounts === "object" ? value.taskStatusCounts : {},
launchButtonVisible: value.launchButtonVisible === true,
launchButtonEnabled: value.launchButtonEnabled === true,
launchButtonText: value.launchButtonText ?? null,
blockerCount: value.blockerCount ?? 0,
blockers: Array.isArray(value.blockers) ? value.blockers.slice(0, 6) : [],
paneGaps: Array.isArray(value.paneGaps) ? value.paneGaps.slice(0, 8).map((item) => ({
name: item?.name ?? null,
visible: item?.visible === true,
widthPx: item?.widthPx ?? null,
heightPx: item?.heightPx ?? null,
bottomGapPx: item?.bottomGapPx ?? null,
bottomGapRatio: item?.bottomGapRatio ?? null,
contentNodeCount: item?.contentNodeCount ?? null,
valuesRedacted: true
})) : [],
workbenchLinkCount: value.workbenchLinkCount ?? 0,
valuesRedacted: true
};
}
function compactSessionRail(value) {
if (!value || typeof value !== "object") return null;
const items = Array.isArray(value.items) ? value.items.slice(0, 80).map((item) => ({
index: item?.index ?? null,
tag: item?.tag ?? null,
testId: item?.testId ?? null,
role: item?.role ?? null,
active: item?.active === true,
sessionIdPrefix: item?.sessionIdPrefix ?? (item?.sessionId ? String(item.sessionId).slice(0, 12) : null),
fallbackTitle: item?.fallbackTitle === true,
titlePreview: limitText(String(item?.titlePreview || item?.titleText || ""), 180),
titleHash: item?.titleHash ?? sha256(String(item?.titlePreview || item?.titleText || "")),
titleBytes: item?.titleBytes ?? null,
})) : [];
const fallbackItems = Array.isArray(value.fallbackItems)
? value.fallbackItems.slice(0, 20).map((item) => ({
index: item?.index ?? null,
active: item?.active === true,
sessionIdPrefix: item?.sessionIdPrefix ?? (item?.sessionId ? String(item.sessionId).slice(0, 12) : null),
titlePreview: limitText(String(item?.titlePreview || item?.titleText || ""), 180),
titleHash: item?.titleHash ?? sha256(String(item?.titlePreview || item?.titleText || "")),
}))
: items.filter((item) => item.fallbackTitle).slice(0, 20);
const visibleCount = Number(value.visibleCount ?? items.length);
const fallbackTitleCount = Number(value.fallbackTitleCount ?? fallbackItems.length);
return {
visibleCount: Number.isFinite(visibleCount) ? visibleCount : items.length,
fallbackTitleCount: Number.isFinite(fallbackTitleCount) ? fallbackTitleCount : fallbackItems.length,
fallbackTitleRatio: Number.isFinite(Number(value.fallbackTitleRatio)) ? Number(value.fallbackTitleRatio) : (items.length > 0 ? Number((fallbackItems.length / items.length).toFixed(4)) : 0),
items,
fallbackItems,
valuesRedacted: true
};
}
function compactComposer(value) {
if (!value || typeof value !== "object") return null;
return {
inputPresent: value.inputPresent === true,
inputDisabled: value.inputDisabled === true,
warningPresent: value.warningPresent === true,
submitPresent: value.submitPresent === true,
submitDisabled: value.submitDisabled === true,
submitAction: value.submitAction ?? null,
activeStatus: value.activeStatus ?? null,
valuesRedacted: true
};
}
function compactLoadingItems(items) {
if (!Array.isArray(items)) return [];
return items.map((item) => {
if (!item || typeof item !== "object") return item;
const rawText = String(item.text ?? item.textPreview ?? "");
return {
index: item.index ?? null,
tag: item.tag ?? null,
testId: item.testId ?? null,
role: item.role ?? null,
ownerKind: item.ownerKind ?? null,
ownerKey: item.ownerKey ?? null,
ownerLabel: item.ownerLabel ?? null,
owner: item.owner && typeof item.owner === "object" ? {
tag: item.owner.tag ?? null,
testId: item.owner.testId ?? null,
role: item.owner.role ?? null,
id: item.owner.id ?? null,
className: item.owner.className ?? null,
status: item.owner.status ?? null,
sessionId: item.owner.sessionId ?? null,
messageId: item.owner.messageId ?? null,
traceId: item.owner.traceId ?? null,
ariaLabel: item.owner.ariaLabel ?? null,
} : null,
text: limitText(rawText, 400),
textPreview: limitText(String(item.textPreview ?? rawText), 240),
textHash: item.textHash ?? sha256(rawText),
textBytes: item.textBytes ?? Buffer.byteLength(rawText),
ownerTextHash: item.ownerTextHash ?? null,
ownerTextPreview: item.ownerTextPreview ? limitText(item.ownerTextPreview, 240) : null,
};
});
}
function compactDomItems(items) {
if (!Array.isArray(items)) return [];
return items.map(compactDomItem);
}
function compactDomItem(item) {
if (!item || typeof item !== "object") return item;
const rawText = String(item.text ?? item.textPreview ?? "");
const preview = String(item.textPreview ?? limitText(rawText, 240));
return {
index: item.index ?? null,
tag: item.tag ?? null,
testId: item.testId ?? null,
role: item.role ?? null,
dataRole: item.dataRole ?? null,
status: item.status ?? null,
sessionId: item.sessionId ?? null,
messageId: item.messageId ?? null,
traceId: item.traceId ?? null,
turnId: item.turnId ?? null,
projectedSeq: Number.isFinite(Number(item.projectedSeq)) ? Number(item.projectedSeq) : null,
sourceSeq: Number.isFinite(Number(item.sourceSeq)) ? Number(item.sourceSeq) : null,
eventSeq: Number.isFinite(Number(item.eventSeq)) ? Number(item.eventSeq) : null,
eventTimestamp: item.eventTimestamp ?? null,
eventTimeText: item.eventTimeText ?? null,
eventKind: item.eventKind ?? null,
durationText: item.durationText ?? null,
activityText: item.activityText ?? null,
className: item.className ?? null,
diagnosticCode: item.diagnosticCode ?? null,
source: item.source ?? null,
sources: Array.isArray(item.sources) ? item.sources.slice(0, 8) : undefined,
text: limitText(rawText, 4000),
textPreview: limitText(preview, 600),
textHash: item.textHash ?? sha256(rawText),
textBytes: item.textBytes ?? Buffer.byteLength(rawText)
};
}
function compactPerformanceItems(items) {
if (!Array.isArray(items)) return [];
return items.map((item) => ({
name: item?.name ?? null,
initiatorType: item?.initiatorType ?? null,
startTime: item?.startTime ?? null,
duration: item?.duration ?? null,
workerStart: item?.workerStart ?? null,
redirectStart: item?.redirectStart ?? null,
redirectEnd: item?.redirectEnd ?? null,
fetchStart: item?.fetchStart ?? null,
domainLookupStart: item?.domainLookupStart ?? null,
domainLookupEnd: item?.domainLookupEnd ?? null,
connectStart: item?.connectStart ?? null,
connectEnd: item?.connectEnd ?? null,
secureConnectionStart: item?.secureConnectionStart ?? null,
requestStart: item?.requestStart ?? null,
responseStart: item?.responseStart ?? null,
responseEnd: item?.responseEnd ?? null,
transferSize: item?.transferSize ?? null,
encodedBodySize: item?.encodedBodySize ?? null,
decodedBodySize: item?.decodedBodySize ?? null,
nextHopProtocol: item?.nextHopProtocol ?? null,
responseStatus: Number.isFinite(Number(item?.responseStatus)) ? Number(item.responseStatus) : null
}));
}
function compactLoadingMetricsForOutput(value) {
if (!value || typeof value !== "object") return null;
return {
summary: value.summary ?? null,
longestSegments: Array.isArray(value.segments) ? value.segments.slice(0, 8) : [],
owners: Array.isArray(value.owners) ? value.owners.slice(0, 8) : [],
timeline: Array.isArray(value.timeline) ? value.timeline.slice(-12) : [],
valuesRedacted: true
};
}
function compactSessionRailTitleMetricsForOutput(value) {
if (!value || typeof value !== "object") return null;
return {
summary: value.summary ?? null,
samples: Array.isArray(value.samples) ? value.samples.slice(0, 12) : [],
examples: Array.isArray(value.examples) ? value.examples.slice(0, 12) : [],
timeline: Array.isArray(value.timeline) ? value.timeline.slice(-12) : [],
valuesRedacted: true
};
}
function compactSamplePageProvenance(value) {
if (!value || typeof value !== "object") return null;
return {
pageLoadSeq: value.pageLoadSeq ?? null,
reason: value.reason ?? null,
observedAt: value.observedAt ?? null,
urlPath: value.urlPath ?? null,
documentReadyState: value.documentReadyState ?? null,
timeOrigin: value.timeOrigin ?? null,
httpStatus: value.httpStatus ?? null,
assetFingerprint: value.assetFingerprint ?? null,
scriptCount: value.scriptCount ?? null,
stylesheetCount: value.stylesheetCount ?? null,
metaCount: value.metaCount ?? null,
scripts: Array.isArray(value.scripts) ? value.scripts.slice(0, 20) : [],
stylesheets: Array.isArray(value.stylesheets) ? value.stylesheets.slice(0, 20) : [],
error: value.error ?? null,
valuesRedacted: true
};
}
function summarizeCommandFailures(control) {
return control.filter((item) => item?.phase === "failed").map((item) => {
const detail = item?.detail && typeof item.detail === "object" ? item.detail : {};
const error = detail?.error && typeof detail.error === "object" ? detail.error : detail;
return {
ts: item.ts ?? null,
commandId: item.commandId ?? null,
type: item.type ?? item.input?.type ?? null,
source: item.source ?? null,
durationMs: detail.durationMs ?? null,
beforePath: urlPath(detail.beforeUrl || item.beforeUrl),
afterPath: urlPath(detail.afterUrl || item.afterUrl),
name: error?.name ?? null,
message: limitText(error?.message ?? detail?.message ?? "", 240),
failureKind: error?.failureKind ?? detail?.failureKind ?? null,
failureSampleOk: detail?.failureSample?.ok === true,
sampleSeq: detail?.failureSample?.sampleSeq ?? null,
valuesRedacted: true
};
});
}
function buildProjectManagementReport(samples, control, network, pagePerformance, config) {
const enabled = config?.enabled === true;
const targetPathSamples = (samples || []).filter((sample) => enabled && config.targetPaths.some((target) => String(sample?.path || "").startsWith(target)));
const projectSamples = (samples || []).filter((sample) => sample?.projectManagement && typeof sample.projectManagement === "object");
const latest = projectSamples[projectSamples.length - 1] || null;
const latestProject = latest?.projectManagement || null;
const pageKindCounts = countBy(projectSamples.map((sample) => sample.projectManagement?.pageKind).filter(Boolean));
const latestTaskStatusCounts = latestProject?.taskStatusCounts && typeof latestProject.taskStatusCounts === "object" ? latestProject.taskStatusCounts : {};
const commandRows = projectManagementCommandRows(control, config);
const launchCommands = commandRows.filter((item) => item.type === "launchWorkbenchFromTask" || item.type === "launchWorkbenchFromMdtodo");
const launchSuccess = launchCommands.filter((item) => item.phase === "completed" && Number(item.launchStatus ?? 0) >= 200 && Number(item.launchStatus ?? 0) < 300);
const launchFailed = launchCommands.filter((item) => item.phase === "failed" || Number(item.launchStatus ?? 200) >= 400);
const projectApiEvents = projectManagementNetworkRows(network, config);
const projectApiResponses = projectApiEvents.filter((item) => item.type === "response");
const projectApiFailures = projectApiResponses.filter((item) => Number(item.status ?? 0) >= 400);
const projectApiFailedRequests = projectApiEvents.filter((item) => item.type === "requestfailed");
const projectApiByPath = groupProjectApiEvents(projectApiEvents);
const projectApiPerformance = projectManagementPerformanceRows(pagePerformance, config);
const slowProjectApiPerformance = projectApiPerformance.filter((item) => Number(item.overBudgetCount ?? 0) > 0 || Number(item.p95Ms ?? 0) > Number(config?.slowApiBudgetMs ?? 0));
const selectedTaskSamples = projectSamples.filter((sample) => sample.projectManagement?.selectedTaskRef?.hash);
const launchEnabledSamples = projectSamples.filter((sample) => sample.projectManagement?.launchButtonEnabled === true);
const launchVisibleSamples = projectSamples.filter((sample) => sample.projectManagement?.launchButtonVisible === true);
const mdtodoSamples = projectSamples.filter((sample) => sample.projectManagement?.pageKind === "project-management-mdtodo");
const selectedFileLabelBadSamples = projectSamples.filter((sample) => sample.projectManagement?.selectedFileLabel && sample.projectManagement?.selectedFileLabelLooksDirect === false);
const suspiciousFileLabelSamples = projectSamples.filter((sample) => Number(sample.projectManagement?.fileOptionSuspiciousLabelCount ?? 0) > 0);
const bodyVisibleSamples = selectedTaskSamples.filter((sample) => sample.projectManagement?.taskBodyVisible === true && Number(sample.projectManagement?.taskBody?.textBytes ?? 0) > 0);
const reportLinkSamples = projectSamples.filter((sample) => Number(sample.projectManagement?.reportLinkCount ?? 0) > 0);
const reportPreviewSamples = projectSamples.filter((sample) => sample.projectManagement?.reportPreviewVisible === true && Number(sample.projectManagement?.reportPreview?.textBytes ?? 0) > 0);
const reportFullscreenSamples = projectSamples.filter((sample) => sample.projectManagement?.reportFullscreenVisible === true);
const hwpodBlockerSamples = projectManagementHwpodBlockerRows(projectSamples);
const projectionReportSamples = projectManagementProjectionReportRows(projectSamples);
const hwpodApiFailures = projectManagementHwpodApiFailureRows(projectApiFailures);
const paneGapRows = projectManagementPaneGapRows(projectSamples);
const severePaneGapSamples = paneGapRows.actionable;
const ignoredPaneGapSamples = paneGapRows.ignored;
const previewCommands = commandRows.filter((item) => item.type === "openMdtodoReportPreview" || item.type === "toggleMdtodoReportFullscreen");
const launchNonEmpty = launchSuccess.filter((item) => item.chatObserved === true && (Number(item.workbenchMessageCount ?? 0) > 0 || Number(item.workbenchTraceRowCount ?? 0) > 0));
const launchEmpty = launchSuccess.filter((item) => item.chatObserved !== true || (Number(item.workbenchMessageCount ?? 0) === 0 && Number(item.workbenchTraceRowCount ?? 0) === 0));
const minMdtodoTaskCount = minNumber(mdtodoSamples.map((sample) => sample.projectManagement?.taskCount));
const maxMdtodoTaskCount = maxNumber(mdtodoSamples.map((sample) => sample.projectManagement?.taskCount));
return {
enabled,
config: config || null,
summary: {
enabled,
targetPathSampleCount: targetPathSamples.length,
projectSampleCount: projectSamples.length,
mdtodoSampleCount: mdtodoSamples.length,
pageKindCounts,
latestPageKind: latestProject?.pageKind ?? null,
latestPath: latest?.path ?? null,
latestSeq: latest?.seq ?? null,
latestTs: latest?.ts ?? null,
latestSourceCount: latestProject?.sourceCount ?? null,
latestFileCount: latestProject?.fileCount ?? null,
latestTaskCount: latestProject?.taskCount ?? null,
maxSourceCount: maxNumber(projectSamples.map((sample) => sample.projectManagement?.sourceCount)),
maxFileCount: maxNumber(projectSamples.map((sample) => sample.projectManagement?.fileCount)),
maxTaskCount: maxNumber(projectSamples.map((sample) => sample.projectManagement?.taskCount)),
taskRefMissingMax: maxNumber(projectSamples.map((sample) => sample.projectManagement?.taskRefMissingCount)),
latestSelectedTaskRefHash: latestProject?.selectedTaskRef?.hash ?? null,
latestSelectedTaskRefPreview: latestProject?.selectedTaskRef?.preview ?? null,
latestSelectedFileLabelPreview: latestProject?.selectedFileLabel?.textPreview ?? null,
latestSelectedFileLabelLooksDirect: latestProject?.selectedFileLabelLooksDirect ?? null,
selectedFileLabelBadSampleCount: selectedFileLabelBadSamples.length,
fileOptionSuspiciousLabelSampleCount: suspiciousFileLabelSamples.length,
maxFileOptionSuspiciousLabelCount: maxNumber(projectSamples.map((sample) => sample.projectManagement?.fileOptionSuspiciousLabelCount)),
latestSelectedTaskStatus: latestProject?.selectedTaskStatus ?? null,
latestTaskStatusCounts,
selectedTaskBodyVisibleSamples: bodyVisibleSamples.length,
reportLinkVisibleSamples: reportLinkSamples.length,
maxReportLinkCount: maxNumber(projectSamples.map((sample) => sample.projectManagement?.reportLinkCount)),
reportPreviewVisibleSamples: reportPreviewSamples.length,
reportFullscreenVisibleSamples: reportFullscreenSamples.length,
hwpodBlockerSampleCount: hwpodBlockerSamples.length,
projectionReportSampleCount: projectionReportSamples.length,
hwpodApiFailureCount: hwpodApiFailures.length,
severePaneGapSampleCount: severePaneGapSamples.length,
ignoredPaneGapSampleCount: ignoredPaneGapSamples.length,
maxPaneBottomGapPx: maxNumber(severePaneGapSamples.map((item) => item.maxBottomGapPx)),
maxPaneBottomGapRatio: maxNumber(severePaneGapSamples.map((item) => item.maxBottomGapRatio)),
launchButtonVisibleSamples: launchVisibleSamples.length,
launchButtonEnabledSamples: launchEnabledSamples.length,
launchButtonDisabledSamples: Math.max(0, launchVisibleSamples.length - launchEnabledSamples.length),
latestWorkbenchLinkCount: latestProject?.workbenchLinkCount ?? null,
maxWorkbenchLinkCount: maxNumber(projectSamples.map((sample) => sample.projectManagement?.workbenchLinkCount)),
maxBlockerCount: maxNumber(projectSamples.map((sample) => sample.projectManagement?.blockerCount)),
selectedTaskSampleCount: selectedTaskSamples.length,
projectCommandCount: commandRows.length,
launchCommandCount: launchCommands.length,
launchSuccessCount: launchSuccess.length,
launchFailureCount: launchFailed.length,
launchNonEmptyCount: launchNonEmpty.length,
launchEmptyCount: launchEmpty.length,
launchWithOtelTraceHeaderCount: launchSuccess.filter((item) => item.otelTraceId).length,
reportPreviewCommandCount: previewCommands.length,
mdtodoTaskCountMin: minMdtodoTaskCount,
mdtodoTaskCountMax: maxMdtodoTaskCount,
projectApiEventCount: projectApiEvents.length,
projectApiResponseCount: projectApiResponses.length,
projectApiFailureCount: projectApiFailures.length,
projectApiRequestFailedCount: projectApiFailedRequests.length,
projectApiSlowPathCount: slowProjectApiPerformance.length,
slowApiBudgetMs: config?.slowApiBudgetMs ?? null,
valuesRedacted: true
},
latest: latestProject,
samples: projectSamples.slice(-80).map((sample) => ({
seq: sample.seq ?? null,
ts: sample.ts ?? null,
pageRole: sample.pageRole ?? null,
path: sample.path ?? null,
pageKind: sample.projectManagement?.pageKind ?? null,
sourceCount: sample.projectManagement?.sourceCount ?? null,
fileCount: sample.projectManagement?.fileCount ?? null,
taskCount: sample.projectManagement?.taskCount ?? null,
taskRefMissingCount: sample.projectManagement?.taskRefMissingCount ?? null,
selectedTaskRefHash: sample.projectManagement?.selectedTaskRef?.hash ?? null,
selectedFileLabelPreview: sample.projectManagement?.selectedFileLabel?.textPreview ?? null,
selectedFileLabelLooksDirect: sample.projectManagement?.selectedFileLabelLooksDirect ?? null,
fileOptionSuspiciousLabelCount: sample.projectManagement?.fileOptionSuspiciousLabelCount ?? 0,
selectedTaskStatus: sample.projectManagement?.selectedTaskStatus ?? null,
taskBodyVisible: sample.projectManagement?.taskBodyVisible === true,
taskBodyBytes: sample.projectManagement?.taskBody?.textBytes ?? 0,
reportLinkCount: sample.projectManagement?.reportLinkCount ?? 0,
reportPreviewVisible: sample.projectManagement?.reportPreviewVisible === true,
reportPreviewBytes: sample.projectManagement?.reportPreview?.textBytes ?? 0,
reportFullscreenVisible: sample.projectManagement?.reportFullscreenVisible === true,
paneGaps: Array.isArray(sample.projectManagement?.paneGaps) ? sample.projectManagement.paneGaps.slice(0, 4) : [],
launchButtonVisible: sample.projectManagement?.launchButtonVisible === true,
launchButtonEnabled: sample.projectManagement?.launchButtonEnabled === true,
blockerCount: sample.projectManagement?.blockerCount ?? 0,
workbenchLinkCount: sample.projectManagement?.workbenchLinkCount ?? 0,
valuesRedacted: true
})),
targetPathWithoutProjectSummary: targetPathSamples.filter((sample) => !sample.projectManagement).slice(0, 20).map(ref),
commands: commandRows,
launchCommands,
projectApiByPath,
projectApiFailures: projectApiFailures.slice(0, 40),
projectApiRequestFailed: projectApiFailedRequests.slice(0, 40),
hwpodBlockerSamples: hwpodBlockerSamples.slice(0, 40),
projectionReportSamples: projectionReportSamples.slice(0, 40),
hwpodApiFailures: hwpodApiFailures.slice(0, 40),
severePaneGapSamples: severePaneGapSamples.slice(0, 40),
ignoredPaneGapSamples: ignoredPaneGapSamples.slice(0, 40),
projectApiPerformance,
slowProjectApiPerformance,
valuesRedacted: true
};
}
function compactProjectManagementForOutput(report) {
if (!report || typeof report !== "object") return null;
const compactCommand = (item) => ({
ts: item?.ts ?? null,
phase: item?.phase ?? null,
type: item?.type ?? null,
commandId: item?.commandId ?? null,
afterPath: item?.afterPath ?? null,
launchStatus: item?.launchStatus ?? null,
sessionId: item?.sessionId ?? null,
workbenchUrl: item?.workbenchUrl ?? null,
otelTraceId: item?.otelTraceId ?? null,
chatObserved: item?.chatObserved ?? null,
chatStatus: item?.chatStatus ?? null,
chatTraceId: item?.chatTraceId ?? null,
workbenchMessageCount: item?.workbenchMessageCount ?? null,
workbenchTraceRowCount: item?.workbenchTraceRowCount ?? null,
contractVersion: item?.contractVersion ?? null,
selectedTaskRefHash: item?.selectedTaskRefHash ?? null,
errorMessageHash: item?.errorMessageHash ?? null,
message: item?.message ? limitText(item.message, 180) : null,
valuesRedacted: true
});
const compactApiGroup = (item) => ({
method: item?.method ?? null,
path: item?.path ?? item?.urlPath ?? null,
status: item?.status ?? null,
type: item?.type ?? null,
count: item?.count ?? item?.sampleCount ?? null,
firstAt: item?.firstAt ?? null,
lastAt: item?.lastAt ?? null,
failureKinds: Array.isArray(item?.failureKinds) ? item.failureKinds.slice(0, 4) : [],
valuesRedacted: true
});
const compactSlowSample = (item) => ({
ts: item?.ts ?? null,
seq: item?.seq ?? null,
path: item?.path ?? item?.rawPath ?? null,
durationMs: item?.durationMs ?? null,
requestToResponseStartMs: item?.requestToResponseStartMs ?? item?.streamOpenMs ?? null,
responseTransferMs: item?.responseTransferMs ?? null,
timingStatus: item?.timingStatus ?? null,
initiatorType: item?.initiatorType ?? null,
nextHopProtocol: item?.nextHopProtocol ?? null,
serverTimingNames: Array.isArray(item?.serverTimingNames) ? item.serverTimingNames.slice(0, 4) : [],
otelTraceId: item?.otelTraceId ?? null,
valuesRedacted: true
});
const compactPerformance = (item) => ({
path: item?.path ?? item?.route ?? null,
sampleCount: item?.sampleCount ?? null,
p95Ms: item?.p95Ms ?? item?.p95 ?? null,
maxMs: item?.maxMs ?? item?.max ?? null,
budgetMs: item?.projectSlowBudgetMs ?? item?.budgetMs ?? report.summary?.slowApiBudgetMs ?? null,
overBudgetCount: item?.overBudgetCount ?? item?.overFiveSecondCount ?? null,
slowSamples: Array.isArray(item?.slowSamples) ? item.slowSamples.slice(0, 3).map(compactSlowSample) : [],
valuesRedacted: true
});
const compactSample = (item) => ({
seq: item?.seq ?? null,
ts: item?.ts ?? null,
pageRole: item?.pageRole ?? null,
path: item?.path ?? null,
selectedTaskRefHash: item?.selectedTaskRefHash ?? null,
selectedFileLabelPreview: item?.selectedFileLabelPreview ?? null,
pageKind: item?.pageKind ?? null,
reason: item?.reason ?? null,
severePaneCount: item?.severePaneCount ?? null,
maxBottomGapPx: item?.maxBottomGapPx ?? null,
maxBottomGapRatio: item?.maxBottomGapRatio ?? null,
paneGaps: Array.isArray(item?.paneGaps) ? item.paneGaps.slice(0, 4) : undefined,
valuesRedacted: true
});
return {
summary: report.summary ?? null,
samples: Array.isArray(report.samples) ? report.samples.slice(-8) : [],
commands: Array.isArray(report.commands) ? report.commands.slice(-8).map(compactCommand) : [],
launchCommands: Array.isArray(report.launchCommands) ? report.launchCommands.slice(-8).map(compactCommand) : [],
projectApiByPath: Array.isArray(report.projectApiByPath) ? report.projectApiByPath.slice(0, 8).map(compactApiGroup) : [],
hwpodBlockerSamples: Array.isArray(report.hwpodBlockerSamples) ? report.hwpodBlockerSamples.slice(0, 8).map(compactSample) : [],
projectionReportSamples: Array.isArray(report.projectionReportSamples) ? report.projectionReportSamples.slice(0, 8).map(compactSample) : [],
hwpodApiFailures: Array.isArray(report.hwpodApiFailures) ? report.hwpodApiFailures.slice(0, 8).map(compactApiGroup) : [],
severePaneGapSamples: Array.isArray(report.severePaneGapSamples) ? report.severePaneGapSamples.slice(0, 8).map(compactSample) : [],
ignoredPaneGapSamples: Array.isArray(report.ignoredPaneGapSamples) ? report.ignoredPaneGapSamples.slice(0, 8).map(compactSample) : [],
projectApiPerformance: Array.isArray(report.projectApiPerformance) ? report.projectApiPerformance.slice(0, 8).map(compactPerformance) : [],
slowProjectApiPerformance: Array.isArray(report.slowProjectApiPerformance) ? report.slowProjectApiPerformance.slice(0, 8).map(compactPerformance) : [],
valuesRedacted: true
};
}
function projectManagementCommandRows(control, config) {
const allowed = new Set(config?.commandAllowlist || []);
const mdtodoCommandTypes = new Set(["gotoProjectMdtodo", "openMdtodoSourceConfig", "configureMdtodoHwpodSource", "probeMdtodoSource", "reindexMdtodoSource", "expandMdtodoTask", "openMdtodoReportPreview", "toggleMdtodoReportFullscreen", "editMdtodoTaskInline", "editMdtodoTaskTitle", "editMdtodoTaskBody", "toggleMdtodoTaskStatus", "addMdtodoRootTask", "addMdtodoSubTask", "continueMdtodoTask", "deleteMdtodoTask", "launchWorkbenchFromMdtodo"]);
return (control || [])
.filter((item) => allowed.has(item?.type) || mdtodoCommandTypes.has(item?.type) || String(item?.type || "").startsWith("selectMdtodo") || item?.type === "selectProjectSource" || item?.type === "launchWorkbenchFromTask")
.filter((item) => item.phase === "completed" || item.phase === "failed")
.map((item) => {
const detail = item.detail && typeof item.detail === "object" ? item.detail : {};
const error = detail.error && typeof detail.error === "object" ? detail.error : {};
return {
ts: item.ts ?? null,
phase: item.phase ?? null,
type: item.type ?? null,
commandId: item.commandId ?? null,
afterPath: urlPath(item.afterUrl),
launchStatus: detail.launchStatus ?? error.details?.launchStatus ?? null,
sessionId: detail.sessionId ?? error.details?.sessionId ?? null,
workbenchUrl: detail.workbenchUrl ?? error.details?.workbenchUrl ?? null,
otelTraceId: detail.otelTraceId ?? error.details?.otelTraceId ?? null,
chatObserved: detail.chatObserved ?? error.details?.chatObserved ?? null,
chatStatus: detail.chatStatus ?? error.details?.chatStatus ?? null,
chatSessionId: detail.chatSessionId ?? error.details?.chatSessionId ?? null,
chatTraceId: detail.chatTraceId ?? error.details?.chatTraceId ?? null,
chatOtelTraceId: detail.chatOtelTraceId ?? error.details?.chatOtelTraceId ?? null,
workbenchMessageCount: detail.workbenchSnapshot?.messageCount ?? error.details?.workbenchSnapshot?.messageCount ?? null,
workbenchTraceRowCount: detail.workbenchSnapshot?.traceRowCount ?? error.details?.workbenchSnapshot?.traceRowCount ?? null,
workbenchComposerReady: detail.workbenchSnapshot?.composerReady ?? error.details?.workbenchSnapshot?.composerReady ?? null,
contractVersion: detail.contractVersion ?? error.details?.contractVersion ?? null,
selectedTaskRefHash: detail.selectedTask?.hash ?? detail.projectBeforeClick?.selectedTaskRef?.hash ?? null,
errorName: error.name ?? null,
errorMessageHash: error.message ? sha256(error.message) : null,
message: error.message ? limitText(error.message, 180) : null,
valuesRedacted: true
};
});
}
function projectManagementNetworkRows(network, config) {
const prefixes = config?.naturalApiPathPrefixes || [];
return (network || [])
.filter((item) => item?.observerInitiated !== true)
.map((item) => ({
ts: item.ts ?? null,
type: item.type ?? null,
method: String(item.method || "GET").toUpperCase(),
status: Number.isFinite(Number(item.status)) ? Number(item.status) : null,
path: urlPath(item.url),
failureKind: item.failure ? limitText(item.failure, 120) : null,
valuesRedacted: true
}))
.filter((item) => prefixes.some((prefix) => String(item.path || "").startsWith(prefix)));
}
function groupProjectApiEvents(events) {
const groups = new Map();
for (const item of events || []) {
const key = [item.method, item.path, item.status ?? "-", item.type].join(" ");
const existing = groups.get(key) || { method: item.method, path: item.path, status: item.status, type: item.type, count: 0, firstAt: item.ts, lastAt: item.ts, failureKinds: [], valuesRedacted: true };
existing.count += 1;
existing.lastAt = item.ts;
if (item.failureKind && !existing.failureKinds.includes(item.failureKind)) existing.failureKinds.push(item.failureKind);
groups.set(key, existing);
}
return Array.from(groups.values()).sort((a, b) => b.count - a.count || String(a.path).localeCompare(String(b.path)));
}
function projectManagementPerformanceRows(pagePerformance, config) {
const prefixes = config?.naturalApiPathPrefixes || [];
const rows = Array.isArray(pagePerformance?.sameOriginApiByPath) ? pagePerformance.sameOriginApiByPath : [];
return rows
.filter((item) => prefixes.some((prefix) => String(item?.path || "").startsWith(prefix)))
.map((item) => ({ ...item, projectSlowBudgetMs: config?.slowApiBudgetMs ?? null }));
}
function projectManagementDigestText(value) {
if (!value || typeof value !== "object") return "";
return String(value.textPreview ?? value.preview ?? value.text ?? "").trim();
}
function projectManagementSampleRef(sample) {
return {
seq: sample?.seq ?? null,
ts: sample?.ts ?? null,
pageRole: sample?.pageRole ?? null,
path: sample?.path ?? null,
pageKind: sample?.projectManagement?.pageKind ?? null,
selectedTaskRefHash: sample?.projectManagement?.selectedTaskRef?.hash ?? null,
selectedFileLabelPreview: sample?.projectManagement?.selectedFileLabel?.textPreview ?? null,
valuesRedacted: true
};
}
function projectManagementHwpodBlockerRows(projectSamples) {
const pattern = /(?:no outbound WebSocket hwpod-node|HWLAB_HWPOD_NODE_OPS_URL|hwpod-node-ops contract)/iu;
const rows = [];
for (const sample of projectSamples || []) {
const blockers = Array.isArray(sample?.projectManagement?.blockers) ? sample.projectManagement.blockers : [];
const matched = blockers
.filter((item) => pattern.test(projectManagementDigestText(item)))
.map((item) => ({
index: item?.index ?? null,
testId: item?.testId ?? null,
role: item?.role ?? null,
textHash: item?.textHash ?? null,
textPreview: item?.textPreview ?? null,
valuesRedacted: true
}));
if (matched.length > 0) rows.push({ ...projectManagementSampleRef(sample), blockers: matched.slice(0, 4), valuesRedacted: true });
}
return rows;
}
function projectManagementProjectionReportRows(projectSamples) {
const pattern = /(?:报告索引待刷新|projection-only|任务投影确认存在报告链接)/iu;
return (projectSamples || [])
.filter((sample) => pattern.test(projectManagementDigestText(sample?.projectManagement?.reportPreview)))
.map((sample) => ({
...projectManagementSampleRef(sample),
reportPreviewHash: sample?.projectManagement?.reportPreview?.textHash ?? null,
reportPreviewPreview: sample?.projectManagement?.reportPreview?.textPreview ?? null,
reportPreviewBytes: sample?.projectManagement?.reportPreview?.textBytes ?? null,
valuesRedacted: true
}));
}
function projectManagementHwpodApiFailureRows(projectApiFailures) {
const pattern = /^\/v1\/project-management\/mdtodo\/(?:task-detail|report-preview)\b/u;
return (projectApiFailures || [])
.filter((item) => pattern.test(String(item?.path || "")) && Number(item?.status ?? 0) >= 500)
.map((item) => ({
ts: item?.ts ?? null,
method: item?.method ?? null,
path: item?.path ?? null,
status: item?.status ?? null,
type: item?.type ?? null,
failureKind: item?.failureKind ?? null,
valuesRedacted: true
}));
}
function projectManagementPaneGapRows(projectSamples) {
const actionable = [];
const ignored = [];
for (const sample of projectSamples || []) {
const paneGaps = Array.isArray(sample?.projectManagement?.paneGaps) ? sample.projectManagement.paneGaps : [];
const severeGaps = paneGaps
.filter((item) => item?.visible === true)
.filter((item) => {
const bottomGapPx = Number(item?.bottomGapPx ?? 0);
const bottomGapRatio = Number(item?.bottomGapRatio ?? 0);
const heightPx = Number(item?.heightPx ?? 0);
return heightPx >= 120 && bottomGapPx >= 180 && bottomGapRatio >= 0.28;
})
.map((item) => ({
name: item?.name ?? null,
widthPx: item?.widthPx ?? null,
heightPx: item?.heightPx ?? null,
bottomGapPx: item?.bottomGapPx ?? null,
bottomGapRatio: item?.bottomGapRatio ?? null,
contentNodeCount: item?.contentNodeCount ?? null,
valuesRedacted: true
}));
if (severeGaps.length === 0) continue;
const maxGapPx = maxNumber(severeGaps.map((item) => item.bottomGapPx));
const maxGapRatio = maxNumber(severeGaps.map((item) => item.bottomGapRatio));
const multiPane = severeGaps.length >= 2;
const singleExtreme = maxGapPx >= 240 && maxGapRatio >= 0.45;
if (!multiPane && !singleExtreme) continue;
const selectedTaskRefHash = sample?.projectManagement?.selectedTaskRef?.hash ?? null;
const isMdtodo = sample?.projectManagement?.pageKind === "project-management-mdtodo";
const isInitialEmptyDetail = isMdtodo && !selectedTaskRefHash;
const row = {
...projectManagementSampleRef(sample),
severePaneCount: severeGaps.length,
maxBottomGapPx: maxGapPx,
maxBottomGapRatio: maxGapRatio,
paneGaps: severeGaps.slice(0, 4),
valuesRedacted: true
};
if (isInitialEmptyDetail) {
ignored.push({
...row,
reason: "mdtodo-initial-empty-detail-no-selected-task",
valuesRedacted: true
});
continue;
}
actionable.push(row);
}
return { actionable, ignored, valuesRedacted: true };
}
function buildProjectManagementFindings(report) {
if (!report?.enabled) return [];
const findings = [];
const summary = report.summary || {};
if (Number(summary.targetPathSampleCount ?? 0) > 0 && Number(summary.projectSampleCount ?? 0) === 0) {
findings.push({ id: "project-management-route-not-ready", severity: "red", summary: "project management target path was sampled but no project-management DOM summary was detected", count: summary.targetPathSampleCount, samples: report.targetPathWithoutProjectSummary, valuesRedacted: true });
}
if (Number(summary.taskRefMissingMax ?? 0) > 0) {
findings.push({ id: "mdtodo-taskref-missing", severity: "red", summary: "mdtodo task rows were visible without stable data-task-ref; Workbench launch must bind by opaque public task id", count: summary.taskRefMissingMax, samples: report.samples.filter((item) => Number(item.taskRefMissingCount ?? 0) > 0).slice(0, 20), valuesRedacted: true });
}
if (Number(summary.mdtodoSampleCount ?? 0) > 0 && Number(summary.latestTaskCount ?? 0) > 0 && Number(summary.launchButtonEnabledSamples ?? 0) === 0) {
findings.push({ id: "workbench-launch-button-unavailable", severity: "red", summary: "mdtodo tasks were sampled but the Workbench launch button was never enabled", count: summary.mdtodoSampleCount, latest: report.latest, valuesRedacted: true });
}
if (Number(summary.selectedFileLabelBadSampleCount ?? 0) > 0 || summary.latestSelectedFileLabelLooksDirect === false) {
findings.push({ id: "mdtodo-file-label-not-filename", severity: "red", summary: "MDTODO file dropdown selected label is not a direct markdown filename", count: summary.selectedFileLabelBadSampleCount, latestSelectedFileLabelPreview: summary.latestSelectedFileLabelPreview, samples: report.samples.filter((item) => item.selectedFileLabelLooksDirect === false).slice(0, 12), valuesRedacted: true });
}
if (Number(summary.maxFileOptionSuspiciousLabelCount ?? 0) > 0) {
findings.push({ id: "mdtodo-nondirect-files-visible", severity: "red", summary: "MDTODO file dropdown includes non-direct or report-like markdown labels; docs/MDTODO discovery must be direct files only", count: summary.maxFileOptionSuspiciousLabelCount, samples: report.samples.filter((item) => Number(item.fileOptionSuspiciousLabelCount ?? 0) > 0).slice(0, 12), valuesRedacted: true });
}
if (Number(summary.selectedTaskSampleCount ?? 0) > 0 && Number(summary.selectedTaskBodyVisibleSamples ?? 0) === 0) {
findings.push({ id: "mdtodo-task-body-not-visible", severity: "red", summary: "selected MDTODO task was sampled but no rendered task body was visible", count: summary.selectedTaskSampleCount, samples: report.samples.filter((item) => item.selectedTaskRefHash).slice(-12), valuesRedacted: true });
}
if (Number(summary.hwpodBlockerSampleCount ?? 0) > 0) {
findings.push({ id: "mdtodo-hwpod-node-disconnected", severity: "red", summary: "MDTODO surfaced the hwpod-node disconnected / HWLAB_HWPOD_NODE_OPS_URL fallback blocker", count: summary.hwpodBlockerSampleCount, samples: report.hwpodBlockerSamples.slice(0, 12), valuesRedacted: true });
}
if (Number(summary.projectionReportSampleCount ?? 0) > 0) {
findings.push({ id: "mdtodo-report-projection-only", severity: "red", summary: "MDTODO report preview is projection-only instead of opening the full markdown report from the HWPOD source", count: summary.projectionReportSampleCount, samples: report.projectionReportSamples.slice(0, 12), valuesRedacted: true });
}
if (Number(summary.maxReportLinkCount ?? 0) > 0 && Number(summary.reportPreviewVisibleSamples ?? 0) === 0) {
const severity = Number(summary.reportPreviewCommandCount ?? 0) > 0 ? "red" : "amber";
findings.push({ id: "mdtodo-report-preview-missing", severity, summary: "MDTODO report links were visible but no markdown report preview was sampled", count: summary.maxReportLinkCount, previewCommandCount: summary.reportPreviewCommandCount, samples: report.samples.filter((item) => Number(item.reportLinkCount ?? 0) > 0).slice(-12), valuesRedacted: true });
}
if (Number(summary.reportPreviewCommandCount ?? 0) > 0 && Number(summary.reportFullscreenVisibleSamples ?? 0) === 0 && report.commands.some((item) => item.type === "toggleMdtodoReportFullscreen" && item.phase === "completed")) {
findings.push({ id: "mdtodo-report-fullscreen-missing", severity: "red", summary: "toggleMdtodoReportFullscreen command completed but fullscreen report dialog was never sampled", count: summary.reportPreviewCommandCount, commands: report.commands.filter((item) => item.type === "toggleMdtodoReportFullscreen").slice(-8), valuesRedacted: true });
}
if (Number(summary.launchEmptyCount ?? 0) > 0) {
findings.push({ id: "mdtodo-workbench-launch-empty", severity: "red", summary: "MDTODO Workbench launch created a session without observing agent chat or visible message/trace content", count: summary.launchEmptyCount, commands: report.launchCommands.filter((item) => item.chatObserved !== true || (Number(item.workbenchMessageCount ?? 0) === 0 && Number(item.workbenchTraceRowCount ?? 0) === 0)).slice(0, 12), valuesRedacted: true });
}
if (Number(summary.mdtodoTaskCountMin ?? 0) > 0 && Number(summary.mdtodoTaskCountMax ?? 0) > 0 && (Number(summary.mdtodoTaskCountMax) - Number(summary.mdtodoTaskCountMin) >= 10 || Number(summary.mdtodoTaskCountMax) / Math.max(1, Number(summary.mdtodoTaskCountMin)) >= 2)) {
findings.push({ id: "mdtodo-task-count-diverged", severity: "amber", summary: "MDTODO task count varied sharply during observation; compare control commands and observer samples for projection divergence", minTaskCount: summary.mdtodoTaskCountMin, maxTaskCount: summary.mdtodoTaskCountMax, samples: report.samples.slice(-20), valuesRedacted: true });
}
if (Number(summary.severePaneGapSampleCount ?? 0) > 0) {
findings.push({ id: "mdtodo-pane-bottom-gap", severity: "red", summary: "MDTODO task tree, main detail, or report sidebar left large unused bottom gaps in actionable selected-task samples", count: summary.severePaneGapSampleCount, ignoredInitialEmptyDetailCount: summary.ignoredPaneGapSampleCount, maxBottomGapPx: summary.maxPaneBottomGapPx, maxBottomGapRatio: summary.maxPaneBottomGapRatio, samples: report.severePaneGapSamples.slice(0, 12), valuesRedacted: true });
}
if (Number(summary.hwpodApiFailureCount ?? 0) > 0) {
findings.push({ id: "project-management-hwpod-api-failed", severity: "red", summary: "HWPOD-backed MDTODO task detail or report preview API returned a server error during natural page use", count: summary.hwpodApiFailureCount, failures: report.hwpodApiFailures.slice(0, 12), valuesRedacted: true });
}
if (Number(summary.projectApiFailureCount ?? 0) > 0 || Number(summary.projectApiRequestFailedCount ?? 0) > 0) {
findings.push({ id: "project-management-api-failed", severity: "amber", summary: "natural project-management or Workbench launch API requests failed during observation", count: Number(summary.projectApiFailureCount ?? 0) + Number(summary.projectApiRequestFailedCount ?? 0), groups: report.projectApiByPath.slice(0, 12), valuesRedacted: true });
}
if (Number(summary.projectApiSlowPathCount ?? 0) > 0) {
findings.push({ id: "project-management-api-slow", severity: "red", summary: "project-management API resource timing exceeded YAML projectManagement.slowApiBudgetMs", count: summary.projectApiSlowPathCount, budgetMs: summary.slowApiBudgetMs, groups: report.slowProjectApiPerformance.slice(0, 12), valuesRedacted: true });
}
if (Number(summary.launchFailureCount ?? 0) > 0) {
findings.push({ id: "mdtodo-workbench-launch-failed", severity: "red", summary: "MDTODO Workbench launch command failed or returned an HTTP error", count: summary.launchFailureCount, commands: report.launchCommands.filter((item) => item.phase === "failed" || Number(item.launchStatus ?? 200) >= 400).slice(0, 12), valuesRedacted: true });
}
if (Number(summary.launchSuccessCount ?? 0) > 0 && Number(summary.launchWithOtelTraceHeaderCount ?? 0) === 0) {
findings.push({ id: "mdtodo-workbench-launch-otel-trace-missing", severity: "amber", summary: "Workbench launch succeeded but no x-hwlab-otel-trace-id header was captured for Tempo drill-down", count: summary.launchSuccessCount, commands: report.launchCommands.slice(0, 12), valuesRedacted: true });
}
return findings;
}
function countBy(values) {
const out = {};
for (const value of values || []) out[value] = (out[value] || 0) + 1;
return out;
}
function maxNumber(values) {
const numeric = (values || []).map((value) => Number(value)).filter(Number.isFinite);
return numeric.length > 0 ? Math.max(...numeric) : 0;
}
function minNumber(values) {
const numeric = (values || []).map((value) => Number(value)).filter(Number.isFinite);
return numeric.length > 0 ? Math.min(...numeric) : 0;
}
function buildSessionInvariantFindings(control, manifest = {}) {
const findings = [];
for (const row of control || []) {
if (row?.type !== "assertSessionInvariant" || row?.phase !== "completed") continue;
const detail = objectValue(row.detail);
const messageOrder = objectValue(detail.messageOrder);
if (messageOrder.userClustered !== true) continue;
const afterRound = numberOrNull(detail.afterRound ?? row.input?.afterRound);
const consecutiveUserMessageCount = numberOrNull(messageOrder.consecutiveUserMessageCount) ?? 0;
const sentinelRange = stringOrNull(messageOrder.sentinelRange) ?? stringOrNull(detail.expectedSentinelRange);
const traceIds = arrayStrings(messageOrder.traceIds).slice(0, 12);
const findingId = stringOrNull(detail.findingId) ?? "workbench-message-order-user-clustered-after-navigation";
const severity = stringOrNull(detail.severity) ?? "amber";
const rootCause = "session_message_role_clustered";
const rootCauseStatus = "confirmed-from-controlled-refresh-dom;check-otel-session_messages_read-role-sequence";
const rootCauseConfidence = "medium";
const nextAction = "Use OTel session_messages_read/session detail for the same canarySessionId and traceIds. Compare roleSequencePrefix and adjacentSameRoleCount; if the read model is already clustered, fix Workbench projection/read-model timeline ordering before changing renderer code.";
findings.push({
id: findingId,
severity,
summary: "message-order root cause visible: controlled refresh/switch-back afterRound=" + (afterRound ?? "-") + " left consecutive user message cards without interleaved assistant/code-agent terminal cards" + (sentinelRange ? " (" + sentinelRange + ")" : ""),
rootCause,
rootCauseStatus,
rootCauseConfidence,
nextAction,
count: Math.max(1, consecutiveUserMessageCount),
blocking: detail.blocking === true ? true : false,
afterRound,
canarySessionId: stringOrNull(detail.canarySessionId),
routeSessionId: stringOrNull(detail.routeSessionId),
activeSessionId: stringOrNull(detail.activeSessionId),
consecutiveUserMessageCount,
sentinelRange,
sampleSeq: numberOrNull(detail.sampleSeq),
traceIds,
pageRole: stringOrNull(detail.pageRole) ?? "control",
pageId: stringOrNull(detail.pageId),
observerId: stringOrNull(manifest.jobId),
stateDir: stringOrNull(manifest.stateDir),
commandId: stringOrNull(row.commandId),
commandTs: stringOrNull(row.ts),
evidence: {
afterRound,
consecutiveUserMessageCount,
sentinelRange,
sampleSeq: numberOrNull(detail.sampleSeq),
traceIds,
canarySessionId: stringOrNull(detail.canarySessionId),
routeSessionId: stringOrNull(detail.routeSessionId),
activeSessionId: stringOrNull(detail.activeSessionId),
valuesRedacted: true,
},
messageOrder: {
sequence: Array.isArray(messageOrder.sequence) ? messageOrder.sequence.slice(-20) : [],
clusters: Array.isArray(messageOrder.clusters) ? messageOrder.clusters.slice(0, 8) : [],
valuesRedacted: true,
},
valuesRedacted: true,
});
}
return findings;
}
function buildControlledNavigationRootCauseFindings(control, manifest = {}) {
const commands = [];
for (const row of control || []) {
if (row?.phase !== "completed") continue;
if (row?.type !== "refreshCurrentSession" && row?.type !== "switchAwayAndBack") continue;
const detail = objectValue(row.detail);
const navigation = objectValue(detail.navigation);
const readiness = objectValue(navigation.readiness);
const snapshot = objectValue(readiness.snapshot);
const pageProvenance = objectValue(navigation.pageProvenance);
const blankShell = snapshot.workbenchShellVisible === false
&& snapshot.sessionRailPresent === false
&& snapshot.commandInputPresent === false
&& snapshot.bodyTextHash === "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
const shellOrComposerMissing = snapshot.workbenchShellVisible === false
|| snapshot.sessionRailPresent === false
|| snapshot.commandInputPresent === false;
const degraded = navigation.degraded === true
|| readiness.ok === false
|| detail.routeOk === false
|| blankShell
|| (detail.activeOk === false && shellOrComposerMissing)
|| (detail.composerReady === false && snapshot.commandInputPresent === false);
if (!degraded) continue;
const rootCause = stringOrNull(navigation.degradedReason)
?? (readiness.ok === false ? stringOrNull(readiness.reason) : null)
?? (detail.routeOk === false ? "route-session-not-hydrated" : null)
?? (blankShell ? "workbench-blank-shell-after-navigation" : null)
?? (detail.activeOk === false && shellOrComposerMissing ? "active-session-not-hydrated" : null)
?? (detail.composerReady === false && snapshot.commandInputPresent === false ? "composer-not-ready" : null)
?? "controlled-navigation-degraded";
commands.push({
commandId: stringOrNull(row.commandId),
type: stringOrNull(row.type),
commandTs: stringOrNull(row.ts),
afterRound: numberOrNull(detail.afterRound ?? row.input?.afterRound),
rootCause,
blocking: true,
canarySessionId: stringOrNull(detail.canarySessionId),
routeSessionId: stringOrNull(detail.routeSessionId),
activeSessionId: stringOrNull(detail.activeSessionId),
routeOk: detail.routeOk === true,
activeOk: detail.activeOk === true,
composerReady: detail.composerReady === true,
navigation: {
httpStatus: numberOrNull(navigation.httpStatus),
degraded: navigation.degraded === true,
degradedReason: stringOrNull(navigation.degradedReason),
beforePath: urlPath(navigation.beforeUrl),
afterPath: urlPath(navigation.afterUrl),
valuesRedacted: true,
},
readiness: {
ok: readiness.ok === true,
reason: stringOrNull(readiness.reason),
durationMs: numberOrNull(readiness.durationMs),
path: stringOrNull(snapshot.path),
readyState: stringOrNull(snapshot.readyState),
workbenchShellVisible: snapshot.workbenchShellVisible === true,
sessionCreatePresent: snapshot.sessionCreatePresent === true,
sessionRailPresent: snapshot.sessionRailPresent === true,
commandInputPresent: snapshot.commandInputPresent === true,
activeTabPresent: snapshot.activeTabPresent === true,
loginVisible: snapshot.loginVisible === true,
blankShell,
bodyTextHash: stringOrNull(snapshot.bodyTextHash),
valuesRedacted: true,
},
pageProvenance: {
pageLoadSeq: numberOrNull(pageProvenance.pageLoadSeq),
reason: stringOrNull(pageProvenance.reason),
observedAt: stringOrNull(pageProvenance.observedAt),
urlPath: stringOrNull(pageProvenance.urlPath),
documentReadyState: stringOrNull(pageProvenance.documentReadyState),
timeOrigin: numberOrNull(pageProvenance.timeOrigin),
httpStatus: numberOrNull(pageProvenance.httpStatus),
assetFingerprint: stringOrNull(pageProvenance.assetFingerprint),
scriptCount: numberOrNull(pageProvenance.scriptCount),
stylesheetCount: numberOrNull(pageProvenance.stylesheetCount),
scripts: arrayStrings(pageProvenance.scripts).slice(0, 8),
stylesheets: arrayStrings(pageProvenance.stylesheets).slice(0, 8),
valuesRedacted: true,
},
observer: {
ok: detail.observer?.ok === true,
pageRole: stringOrNull(detail.observer?.pageRole),
pageId: stringOrNull(detail.observer?.pageId),
changed: detail.observer?.changed === true,
valuesRedacted: true,
},
observerId: stringOrNull(manifest.jobId),
stateDir: stringOrNull(manifest.stateDir),
valuesRedacted: true,
});
}
if (commands.length === 0) return [];
return [{
id: "workbench-controlled-navigation-degraded-root-cause",
severity: "red",
summary: "controlled Workbench refresh/switch completed degraded; route may be correct but app shell, active session, or composer was not ready, so later Code Agent turns cannot continue",
count: commands.length,
blocking: true,
rootCauses: Array.from(new Set(commands.map((item) => item.rootCause))).slice(0, 12),
commands: commands.slice(0, 20),
next: "Investigate the first degraded command, then correlate browser requestfailed/static asset failures and Workbench hydration state before changing Code Agent/provider logic.",
valuesRedacted: true,
}];
}
function sessionInvariantNavigationWindows(control) {
const started = new Map();
const windows = [];
for (const row of control || []) {
if (row?.type !== "switchAwayAndBack" && row?.type !== "refreshCurrentSession") continue;
const commandId = stringOrNull(row.commandId) ?? String(row.seq ?? "");
if (row.phase === "started") {
started.set(commandId, row);
continue;
}
if (row.phase !== "completed") continue;
const detail = objectValue(row.detail);
const canarySessionId = stringOrNull(detail.canarySessionId);
const alternateSessionId = stringOrNull(detail.alternateSessionId);
const startRow = started.get(commandId);
const startMs = timestampMs(startRow?.ts ?? row.ts);
const endMs = timestampMs(row.ts);
if (!canarySessionId || !Number.isFinite(startMs) || !Number.isFinite(endMs)) continue;
windows.push({
commandId,
afterRound: numberOrNull(detail.afterRound ?? row.input?.afterRound),
startMs: Math.max(0, startMs - 1000),
endMs: endMs + 5000,
startAt: new Date(startMs).toISOString(),
endAt: new Date(endMs).toISOString(),
canarySessionId,
alternateSessionId,
routeOk: detail.routeOk === true,
activeOk: detail.activeOk === true,
valuesRedacted: true,
});
}
return windows;
}
function sessionInvariantCanarySessionIds(control) {
const ids = new Set();
for (const row of control || []) {
const detail = objectValue(row?.detail);
if (row?.type === "newSession" && row?.phase === "completed") {
const sessionId = stringOrNull(detail.sessionId)
?? stringOrNull(detail.result?.sessionId)
?? stringOrNull(detail.createSession?.createdSessionId);
if (sessionId) ids.add(sessionId);
}
const canarySessionId = stringOrNull(detail.canarySessionId);
if (canarySessionId) ids.add(canarySessionId);
}
return ids;
}
function sessionChangeSamplesOutsideControlledNavigation(samples, key, windows) {
const canaryIds = new Set((windows || []).map((item) => item.canarySessionId).filter(Boolean));
if (canaryIds.size === 0) return samples.filter((item) => item?.[key]);
return (samples || []).filter((sample) => {
const value = stringOrNull(sample?.[key]);
if (!value) return false;
if (canaryIds.has(value)) return false;
return !sampleInControlledNavigationWindow(sample, windows);
});
}
function sampleInControlledNavigationWindow(sample, windows) {
const ms = timestampMs(sample?.ts);
if (!Number.isFinite(ms)) return false;
return (windows || []).some((window) => ms >= window.startMs && ms <= window.endMs);
}
function sampleRefInControlledNavigationSessionWindow(sample, windows) {
const ms = timestampMs(sample?.ts);
if (!Number.isFinite(ms)) return false;
if (!sampleRefMatchesControlledNavigationSession(sample, windows)) return false;
return (windows || []).some((window) => ms >= window.startMs && ms <= window.endMs);
}
function sampleRefMatchesControlledNavigationSession(sample, windows) {
const routeSessionId = stringOrNull(sample?.routeSessionId);
const activeSessionId = stringOrNull(sample?.activeSessionId);
return (windows || []).some((window) => {
const expected = [window.canarySessionId, window.alternateSessionId].filter(Boolean);
return expected.some((sessionId) => sessionId === routeSessionId || sessionId === activeSessionId);
});
}
function isBlankHydrationProjectionSample(sample) {
if (!sample) return false;
const messageCount = Array.isArray(sample.messages) ? sample.messages.length : Number(sample.messageCount ?? 0);
const traceRowCount = Array.isArray(sample.traceRows) ? sample.traceRows.length : Number(sample.traceRowCount ?? 0);
return !stringOrNull(sample.activeSessionId)
&& Number(messageCount) === 0
&& Number(traceRowCount) === 0;
}
function controlledNavigationHydrationCrossPageDiff(row, windows, sampleBySeq) {
if (row?.diffKind !== "projection") return false;
if (!sampleRefMatchesControlledNavigationSession(row.control, windows) || !sampleRefMatchesControlledNavigationSession(row.observer, windows)) return false;
const control = sampleBySeq.get(Number(row?.control?.seq));
const observer = sampleBySeq.get(Number(row?.observer?.seq));
return isBlankHydrationProjectionSample(control) || isBlankHydrationProjectionSample(observer);
}
function crossPageDiffHasWorkbenchAppShellNotReady(row, sampleBySeq) {
const control = sampleBySeq.get(Number(row?.control?.seq));
const observer = sampleBySeq.get(Number(row?.observer?.seq));
return workbenchSampleAppShellNotReady(control) || workbenchSampleAppShellNotReady(observer);
}
function workbenchSampleAppShellNotReady(sample) {
if (!sample || !isWorkbenchPathSample(sample)) return false;
const routeSessionId = stringOrNull(sample.routeSessionId) || workbenchSessionIdFromPath(samplePathname(sample));
if (!routeSessionId) return false;
const messageCount = Array.isArray(sample.messages) ? sample.messages.length : Number(sample.messageCount ?? 0);
const traceRowCount = Array.isArray(sample.traceRows) ? sample.traceRows.length : Number(sample.traceRowCount ?? 0);
const turnCount = Array.isArray(sample.turns) ? sample.turns.length : Number(sample.turnCount ?? 0);
const loadingCount = Array.isArray(sample.loadings) ? sample.loadings.length : 0;
const diagnosticCount = Array.isArray(sample.diagnostics) ? sample.diagnostics.length : 0;
const railVisibleCount = Number(sample?.sessionRail?.visibleCount ?? 0);
const composer = objectValue(sample.composer);
const composerPresent = composer.inputPresent === true || composer.submitPresent === true;
const provenance = objectValue(sample.pageProvenance);
const hasWorkbenchAssets = Number(provenance.scriptCount ?? 0) > 0 || Number(provenance.stylesheetCount ?? 0) > 0;
return !stringOrNull(sample.activeSessionId)
&& Number(messageCount) === 0
&& Number(traceRowCount) === 0
&& Number(turnCount) === 0
&& Number(loadingCount) === 0
&& Number(diagnosticCount) === 0
&& Number(railVisibleCount) === 0
&& !composerPresent
&& hasWorkbenchAssets;
}
function detectWorkbenchAppShellNotReady(samples) {
const rows = (Array.isArray(samples) ? samples : [])
.filter(workbenchSampleAppShellNotReady)
.map((sample) => workbenchAppShellNotReadyRef(sample));
return annotateWorkbenchAppShellNotReadyTiming(rows);
}
function annotateWorkbenchAppShellNotReadyTiming(rows) {
const groups = new Map();
const splitGapMs = Math.max(1000, Number(alertThresholds.crossPageProjectionDivergenceRedMs || alertThresholds.visibleLoadingSlowMs || 10_000));
for (const row of rows.slice().sort((a, b) => timestampMs(a.ts) - timestampMs(b.ts))) {
const ms = timestampMs(row.ts);
const key = [row.pageRole || "control", row.pageId || "default", row.routeSessionId || row.url || ""].join(":");
const group = groups.get(key) || [];
let segment = group.at(-1);
if (!segment || !Number.isFinite(ms) || !Number.isFinite(segment.lastMs) || ms - segment.lastMs > splitGapMs) {
segment = { rows: [], firstMs: Number.isFinite(ms) ? ms : null, lastMs: Number.isFinite(ms) ? ms : null };
group.push(segment);
}
segment.rows.push(row);
if (Number.isFinite(ms)) {
if (segment.firstMs === null || ms < segment.firstMs) segment.firstMs = ms;
if (segment.lastMs === null || ms > segment.lastMs) segment.lastMs = ms;
}
groups.set(key, group);
}
const result = [];
for (const group of groups.values()) {
for (let segmentIndex = 0; segmentIndex < group.length; segmentIndex += 1) {
const segment = group[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 workbenchAppShellNotReadyRef(sample) {
const provenance = objectValue(sample?.pageProvenance);
const performance = workbenchAssetPerformanceSummary(sample);
return {
...ref(sample),
title: stringOrNull(sample?.title),
messageCount: Array.isArray(sample?.messages) ? sample.messages.length : 0,
turnCount: Array.isArray(sample?.turns) ? sample.turns.length : 0,
traceRowCount: Array.isArray(sample?.traceRows) ? sample.traceRows.length : 0,
sessionRailVisibleCount: Number(sample?.sessionRail?.visibleCount ?? 0),
composerInputPresent: sample?.composer?.inputPresent === true,
composerSubmitPresent: sample?.composer?.submitPresent === true,
pageProvenance: {
documentReadyState: stringOrNull(provenance.documentReadyState),
pageLoadSeq: numberOrNull(provenance.pageLoadSeq),
timeOrigin: numberOrNull(provenance.timeOrigin),
assetFingerprint: stringOrNull(provenance.assetFingerprint),
scriptCount: numberOrNull(provenance.scriptCount),
stylesheetCount: numberOrNull(provenance.stylesheetCount),
scripts: arrayStrings(provenance.scripts).slice(0, 8),
stylesheets: arrayStrings(provenance.stylesheets).slice(0, 8),
valuesRedacted: true
},
assetPerformance: performance,
valuesRedacted: true
};
}
function workbenchAssetPerformanceSummary(sample) {
const assets = (Array.isArray(sample?.performance) ? sample.performance : [])
.filter((entry) => /^(script|link|css)$/iu.test(String(entry?.initiatorType || "")) || /\.(?:js|css)$/iu.test(String(entry?.name || "")))
.map((entry) => ({
path: urlPath(entry?.name),
initiatorType: entry?.initiatorType ?? null,
duration: numberOrNull(entry?.duration),
responseStatus: numberOrNull(entry?.responseStatus),
transferSize: numberOrNull(entry?.transferSize),
encodedBodySize: numberOrNull(entry?.encodedBodySize),
decodedBodySize: numberOrNull(entry?.decodedBodySize),
nextHopProtocol: entry?.nextHopProtocol ?? null,
valuesRedacted: true
}));
const responseStatusCounts = {};
for (const item of assets) {
const key = item.responseStatus === null ? "unknown" : String(item.responseStatus);
responseStatusCounts[key] = (responseStatusCounts[key] || 0) + 1;
}
return {
assetCount: assets.length,
zeroStatusCount: assets.filter((item) => item.responseStatus === 0).length,
missingStatusCount: assets.filter((item) => item.responseStatus === null).length,
responseStatusCounts,
assets: assets.slice(0, 12),
valuesRedacted: true
};
}
function objectValue(value) {
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
}
function stringOrNull(value) {
return typeof value === "string" && value.length > 0 ? value : null;
}
function numberOrNull(value) {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
}
function arrayStrings(value) {
return Array.isArray(value) ? value.map((item) => String(item || "")).filter(Boolean) : [];
}
function timestampMs(value) {
const parsed = Date.parse(String(value || ""));
return Number.isFinite(parsed) ? parsed : NaN;
}
function buildApiDomLagReport(samples, network) {
const windowMs = 30_000;
const budgetMs = Number.isFinite(Number(alertThresholds.sameOriginApiSlowMs)) ? Number(alertThresholds.sameOriginApiSlowMs) : 10_000;
const sampleRows = (Array.isArray(samples) ? samples : [])
.map((sample) => {
const tsMs = timestampMs(sample?.ts);
return {
sample,
tsMs,
pageKey: samplePageKey(sample),
digest: digestSample(sample),
sessionIds: new Set([sample?.routeSessionId, sample?.activeSessionId].filter(Boolean).map(String)),
traceIds: sampleTraceIds(sample)
};
})
.filter((item) => Number.isFinite(item.tsMs))
.sort((a, b) => a.tsMs - b.tsMs);
const samplesByPage = new Map();
for (const row of sampleRows) {
const rows = samplesByPage.get(row.pageKey) || [];
rows.push(row);
samplesByPage.set(row.pageKey, rows);
}
const naturalApiResponses = (Array.isArray(network) ? network : [])
.filter((item) => item?.observerInitiated !== true && item?.type === "response" && isApiLikePath(urlPath(item?.url)));
const telemetryExcluded = [];
const nonStateRelevant = [];
const stateRelevantResponses = [];
for (const item of naturalApiResponses) {
const event = compactApiDomLagResponseEvent(item);
if (!Number.isFinite(event.tsMs)) {
nonStateRelevant.push(event);
continue;
}
if (isApiDomLagTelemetryPath(event.path)) telemetryExcluded.push(event);
else if (!isApiDomLagStateRelevantPath(event.path)) nonStateRelevant.push(event);
else stateRelevantResponses.push(event);
}
const candidates = [];
for (const event of stateRelevantResponses) {
const pageSamples = samplesByPage.get(event.pageKey) || [];
const before = lastSampleAtOrBefore(pageSamples, event.tsMs, event);
const firstAfter = firstSampleAfter(pageSamples, event.tsMs, event.tsMs + windowMs, event);
const baselineDigest = before?.digest ?? null;
const change = firstSampleAfter(pageSamples, event.tsMs, event.tsMs + windowMs, event, (row) => !baselineDigest || row.digest !== baselineDigest);
candidates.push({
...event,
windowMs,
budgetMs,
firstSampleDeltaMs: firstAfter ? Math.max(0, Math.round(firstAfter.tsMs - event.tsMs)) : null,
domChangeDeltaMs: change ? Math.max(0, Math.round(change.tsMs - event.tsMs)) : null,
overBudget: change ? (change.tsMs - event.tsMs) > budgetMs : false,
domChanged: Boolean(change),
noDomChangeWithinWindow: !change,
beforeSample: compactApiDomLagSample(before),
firstAfterSample: compactApiDomLagSample(firstAfter),
changeSample: compactApiDomLagSample(change),
confidence: apiDomLagConfidence(event.path),
valuesRedacted: true
});
}
const changedDeltas = candidates.map((item) => nullableNumber(item.domChangeDeltaMs)).filter(Number.isFinite).sort((a, b) => a - b);
const groups = groupApiDomLagCandidates(candidates);
const overBudget = candidates.filter((item) => item.overBudget === true);
return {
summary: {
windowMs,
budgetMs,
naturalApiResponseCount: naturalApiResponses.length,
telemetryExcludedCount: telemetryExcluded.length,
nonStateRelevantResponseCount: nonStateRelevant.length,
stateRelevantResponseCount: stateRelevantResponses.length,
candidateCount: candidates.length,
domChangedCount: changedDeltas.length,
noDomChangeWithinWindowCount: candidates.filter((item) => item.noDomChangeWithinWindow === true).length,
lowConfidenceStreamOpenCount: candidates.filter((item) => item.confidence === "low-stream-open-only").length,
overBudgetCount: overBudget.length,
p50DomChangeDeltaMs: percentile(changedDeltas, 50),
p95DomChangeDeltaMs: percentile(changedDeltas, 95),
maxDomChangeDeltaMs: changedDeltas.length > 0 ? Math.max(...changedDeltas) : null,
groupCount: groups.length,
valuesRedacted: true
},
groups,
worstCandidates: candidates
.filter((item) => Number.isFinite(nullableNumber(item.domChangeDeltaMs)))
.sort((a, b) => nullableNumber(b.domChangeDeltaMs) - nullableNumber(a.domChangeDeltaMs))
.slice(0, 20),
recentCandidates: candidates.slice(-40),
telemetryExcluded: telemetryExcluded.slice(0, 20),
nonStateRelevant: nonStateRelevant.slice(0, 20),
valuesRedacted: true
};
}
function compactApiDomLagResponseEvent(item) {
const parsed = parseApiDomLagUrl(item?.url);
const tsMs = timestampMs(item?.ts);
return {
ts: item?.ts ?? null,
tsMs,
pageRole: item?.pageRole ?? null,
pageId: item?.pageId ?? null,
pageKey: String(item?.pageRole || "control") + ":" + String(item?.pageId || "default"),
commandId: item?.commandId ?? null,
method: String(item?.method || "GET").toUpperCase(),
status: Number.isFinite(Number(item?.status)) ? Number(item.status) : null,
path: parsed.path,
rawPath: parsed.rawPath,
queryKeys: parsed.queryKeys,
sessionId: parsed.sessionId,
traceId: parsed.traceId,
urlHash: item?.url ? sha256(item.url) : null,
routeKind: apiDomLagRouteKind(parsed.path),
valuesRedacted: true
};
}
function parseApiDomLagUrl(value) {
try {
const parsed = new URL(String(value || "http://invalid.local/"));
const rawPath = parsed.pathname || "-";
const queryKeys = Array.from(parsed.searchParams.keys()).sort().slice(0, 12);
const sessionId = parsed.searchParams.get("sessionId") || parsed.searchParams.get("includeSessionId") || firstIdInText(parsed.pathname + " " + parsed.search, /\bses_[A-Za-z0-9_-]+\b/u);
const traceId = parsed.searchParams.get("traceId") || firstIdInText(parsed.pathname + " " + parsed.search, /\btrc_[A-Za-z0-9_-]+\b/u);
return {
rawPath,
path: normalizeApiPath(rawPath),
queryKeys,
sessionId,
traceId
};
} catch {
const rawPath = urlPath(value);
return {
rawPath,
path: normalizeApiPath(rawPath),
queryKeys: [],
sessionId: firstIdInText(String(value || ""), /\bses_[A-Za-z0-9_-]+\b/u),
traceId: firstIdInText(String(value || ""), /\btrc_[A-Za-z0-9_-]+\b/u)
};
}
}
function firstIdInText(text, pattern) {
const match = String(text || "").match(pattern);
return match ? match[0] : null;
}
function nullableNumber(value) {
if (value === null || value === undefined || value === "") return NaN;
const numeric = Number(value);
return Number.isFinite(numeric) ? numeric : NaN;
}
function isApiDomLagTelemetryPath(path) {
const value = String(path || "");
return value === "/v1/web-performance" || value === "/v1/health" || value === "/health";
}
function isApiDomLagStateRelevantPath(path) {
const value = String(path || "");
return value.startsWith("/auth/") || value.startsWith("/v1/workbench/") || value === "/v1/agent/chat" || value === "/v1/agent/chat/steer";
}
function apiDomLagRouteKind(path) {
const value = String(path || "");
if (value === "/v1/workbench/events") return "workbench-events-stream";
if (value.startsWith("/v1/workbench/sessions")) return "workbench-sessions";
if (value.startsWith("/v1/workbench/traces")) return "workbench-traces";
if (value.startsWith("/v1/workbench/turns")) return "workbench-turns";
if (value === "/v1/agent/chat" || value === "/v1/agent/chat/steer") return "agent-chat-submit";
if (value.startsWith("/auth/")) return "auth";
return "state-api";
}
function apiDomLagConfidence(path) {
return String(path || "") === "/v1/workbench/events" ? "low-stream-open-only" : "medium-response-to-dom";
}
function sampleTraceIds(sample) {
const ids = new Set();
for (const group of [sample?.messages, sample?.traceRows, sample?.turns, sample?.diagnostics]) {
if (!Array.isArray(group)) continue;
for (const item of group) if (item?.traceId) ids.add(String(item.traceId));
}
return ids;
}
function lastSampleAtOrBefore(rows, tsMs, event) {
let result = null;
for (const row of rows) {
if (row.tsMs > tsMs) break;
if (apiDomLagSampleMatchesEvent(row, event)) result = row;
}
return result;
}
function firstSampleAfter(rows, startMs, endMs, event, predicate = null) {
for (const row of rows) {
if (row.tsMs < startMs) continue;
if (row.tsMs > endMs) break;
if (!apiDomLagSampleMatchesEvent(row, event)) continue;
if (typeof predicate === "function" && !predicate(row)) continue;
return row;
}
return null;
}
function apiDomLagSampleMatchesEvent(row, event) {
if (!row || !event) return false;
if (event.sessionId && !row.sessionIds.has(String(event.sessionId))) return false;
if (event.traceId && row.traceIds.size > 0 && !row.traceIds.has(String(event.traceId))) return false;
return true;
}
function compactApiDomLagSample(row) {
if (!row) return null;
const sample = row.sample || {};
return {
seq: sample.seq ?? null,
ts: sample.ts ?? null,
pageRole: sample.pageRole ?? null,
pageId: sample.pageId ?? null,
routeSessionId: sample.routeSessionId ?? null,
activeSessionId: sample.activeSessionId ?? null,
messageCount: Array.isArray(sample.messages) ? sample.messages.length : null,
traceRowCount: Array.isArray(sample.traceRows) ? sample.traceRows.length : null,
diagnosticCount: Array.isArray(sample.diagnostics) ? sample.diagnostics.length : null,
valuesRedacted: true
};
}
function groupApiDomLagCandidates(candidates) {
const groups = new Map();
for (const item of candidates || []) {
const key = [item.method || "-", item.path || "-", item.status ?? "-", item.confidence || "-"].join(" ");
const group = groups.get(key) || {
method: item.method ?? null,
path: item.path ?? "-",
routeKind: item.routeKind ?? null,
status: item.status ?? null,
confidence: item.confidence ?? null,
count: 0,
domChangedCount: 0,
noDomChangeWithinWindowCount: 0,
overBudgetCount: 0,
firstAt: item.ts ?? null,
lastAt: item.ts ?? null,
deltas: [],
examples: []
};
group.count += 1;
group.firstAt = minIso(group.firstAt, item.ts ?? null);
group.lastAt = maxIso(group.lastAt, item.ts ?? null);
if (item.domChanged === true && Number.isFinite(Number(item.domChangeDeltaMs))) {
group.domChangedCount += 1;
group.deltas.push(Number(item.domChangeDeltaMs));
}
if (item.noDomChangeWithinWindow === true) group.noDomChangeWithinWindowCount += 1;
if (item.overBudget === true) group.overBudgetCount += 1;
if (group.examples.length < 6) {
group.examples.push({
ts: item.ts ?? null,
sessionId: item.sessionId ?? null,
traceId: item.traceId ?? null,
domChangeDeltaMs: item.domChangeDeltaMs ?? null,
firstSampleDeltaMs: item.firstSampleDeltaMs ?? null,
changeSeq: item.changeSample?.seq ?? null,
beforeSeq: item.beforeSample?.seq ?? null,
valuesRedacted: true
});
}
groups.set(key, group);
}
return Array.from(groups.values())
.map((item) => {
const deltas = item.deltas.slice().sort((a, b) => a - b);
return {
method: item.method,
path: item.path,
routeKind: item.routeKind,
status: item.status,
confidence: item.confidence,
count: item.count,
domChangedCount: item.domChangedCount,
noDomChangeWithinWindowCount: item.noDomChangeWithinWindowCount,
overBudgetCount: item.overBudgetCount,
p50DomChangeDeltaMs: percentile(deltas, 50),
p95DomChangeDeltaMs: percentile(deltas, 95),
maxDomChangeDeltaMs: deltas.length > 0 ? Math.max(...deltas) : null,
firstAt: item.firstAt,
lastAt: item.lastAt,
examples: item.examples,
valuesRedacted: true
};
})
.sort((a, b) => Number(b.maxDomChangeDeltaMs ?? -1) - Number(a.maxDomChangeDeltaMs ?? -1) || b.count - a.count || String(a.path).localeCompare(String(b.path)));
}
function detectTraceEventsPageReadIssues(network) {
const events = (Array.isArray(network) ? network : [])
.filter((item) => item?.observerInitiated !== true && (item?.type === "response" || item?.type === "requestfailed"))
.map(compactTraceEventsPageReadEvent)
.filter((item) => item !== null);
const http404 = events.filter((item) => item.type === "response" && Number(item.status) === 404);
const httpErrors = events.filter((item) => item.type === "response" && Number(item.status) >= 400 && Number(item.status) !== 404);
const requestFailed = events.filter((item) => item.type === "requestfailed");
return {
events,
http404,
httpErrors,
requestFailed,
summary: traceEventsPageReadIssueSummary(events),
valuesRedacted: true
};
}
function compactTraceEventsPageReadEvent(item) {
const parsed = parseTraceEventsPageReadUrl(item?.url);
if (!parsed.match) return null;
const failureText = item?.failureKind ?? item?.failure ?? item?.errorText ?? null;
return {
ts: item?.ts ?? null,
pageRole: item?.pageRole ?? null,
pageId: item?.pageId ?? null,
commandId: item?.commandId ?? null,
method: String(item?.method || "GET").toUpperCase(),
type: item?.type ?? null,
status: Number.isFinite(Number(item?.status)) ? Number(item.status) : null,
path: "/v1/workbench/traces/:traceId/events",
rawPath: parsed.rawPath,
traceId: parsed.traceId,
afterProjectedSeq: parsed.afterProjectedSeq,
sinceSeq: parsed.sinceSeq,
limit: parsed.limit,
tail: parsed.tail,
queryKeys: parsed.queryKeys,
failureKind: failureText ? limitText(String(failureText), 120) : null,
urlHash: item?.url ? sha256(item.url) : null,
valuesRedacted: true
};
}
function parseTraceEventsPageReadUrl(value) {
const fallback = {
match: false,
rawPath: urlPath(value),
traceId: firstIdInText(String(value || ""), /\btrc_[A-Za-z0-9_-]+\b/u),
afterProjectedSeq: null,
sinceSeq: null,
limit: null,
tail: null,
queryKeys: [],
};
try {
const parsed = new URL(String(value || ""), "http://invalid.local/");
const rawPath = parsed.pathname || "-";
const match = rawPath.match(/^\/v1\/workbench\/traces\/([^/]+)\/events$/u);
return {
match: Boolean(match),
rawPath,
traceId: match ? decodeURIComponent(match[1]) : fallback.traceId,
afterProjectedSeq: numericSearchParam(parsed.searchParams, "afterProjectedSeq"),
sinceSeq: numericSearchParam(parsed.searchParams, "sinceSeq") ?? numericSearchParam(parsed.searchParams, "afterSeq"),
limit: numericSearchParam(parsed.searchParams, "limit"),
tail: numericSearchParam(parsed.searchParams, "tail"),
queryKeys: Array.from(parsed.searchParams.keys()).sort().slice(0, 12),
};
} catch {
return fallback;
}
}
function numericSearchParam(searchParams, key) {
const raw = searchParams?.get?.(key);
if (raw === null || raw === undefined || raw === "") return null;
const parsed = Number(raw);
return Number.isFinite(parsed) ? parsed : null;
}
function traceEventsPageReadIssueSummary(events) {
const items = Array.isArray(events) ? events : [];
const statuses = uniqueSorted(items.map((item) => item.status).filter((item) => item !== null && item !== undefined).map(String));
const traceIds = uniqueSorted(items.map((item) => item.traceId).filter(Boolean).map(String)).slice(0, 8);
const afterProjectedSeqs = uniqueSorted(items.map((item) => item.afterProjectedSeq).filter((item) => item !== null && item !== undefined).map(String)).slice(0, 8);
const sinceSeqs = uniqueSorted(items.map((item) => item.sinceSeq).filter((item) => item !== null && item !== undefined).map(String)).slice(0, 8);
const failureKinds = uniqueSorted(items.map((item) => item.failureKind).filter(Boolean).map(String)).slice(0, 6);
return {
eventCount: items.length,
responseErrorCount: items.filter((item) => item.type === "response" && Number(item.status) >= 400).length,
http404Count: items.filter((item) => item.type === "response" && Number(item.status) === 404).length,
requestFailedCount: items.filter((item) => item.type === "requestfailed").length,
statuses,
traceIds,
afterProjectedSeqs,
sinceSeqs,
failureKinds,
firstAt: items.reduce((value, item) => minIso(value, item.ts ?? null), null),
lastAt: items.reduce((value, item) => maxIso(value, item.ts ?? null), null),
rootCauseVisibility: "browser network rows identify trace-events page-read path; OTel trace_events_read should confirm backend paging fields",
valuesRedacted: true
};
}
function uniqueSorted(values) {
return Array.from(new Set((values || []).filter((item) => item !== null && item !== undefined).map(String))).sort();
}
function compactApiDomLagForOutput(report) {
if (!report || typeof report !== "object") return null;
return {
summary: report.summary ?? null,
groups: Array.isArray(report.groups) ? report.groups.slice(0, 8) : [],
worstCandidates: Array.isArray(report.worstCandidates) ? report.worstCandidates.slice(0, 8) : [],
recentCandidates: Array.isArray(report.recentCandidates) ? report.recentCandidates.slice(-8) : [],
valuesRedacted: true
};
}
function detectWorkbenchInPlaceProjectionLag(samples, network, control = []) {
const canarySessionIds = sessionInvariantCanarySessionIds(control);
const terminalTraceMissing = detectWorkbenchTerminalTraceMissing(samples, canarySessionIds);
const terminalApiDomLag = detectWorkbenchTerminalApiDomLag(samples, network, canarySessionIds);
return {
terminalTraceMissing,
terminalApiDomLag,
valuesRedacted: true
};
}
function detectWorkbenchTerminalTraceMissing(samples, canarySessionIds = new Set()) {
const budgetMs = Number.isFinite(Number(alertThresholds.sameOriginApiSlowMs)) ? Number(alertThresholds.sameOriginApiSlowMs) : 10_000;
const rows = [];
const open = new Map();
const sortedSamples = (Array.isArray(samples) ? samples : [])
.filter(isWorkbenchPathSample)
.filter((sample) => workbenchSampleMatchesCanarySessions(sample, canarySessionIds))
.slice()
.sort((a, b) => Date.parse(a?.ts || "") - Date.parse(b?.ts || ""));
const closeSegment = (key, lastSample = null) => {
const segment = open.get(key);
if (!segment) return;
open.delete(key);
const firstMs = Date.parse(segment.first.ts || "");
const lastMs = Date.parse((lastSample ?? segment.last).ts || "");
const observedMs = Number.isFinite(firstMs) && Number.isFinite(lastMs) && lastMs >= firstMs ? Math.round(lastMs - firstMs) : 0;
if (observedMs <= budgetMs) return;
rows.push({
...ref(segment.first),
lastSeq: segment.last.seq ?? null,
lastAt: segment.last.ts ?? null,
traceId: segment.traceId,
messageCount: Array.isArray(segment.last.messages) ? segment.last.messages.length : 0,
turnCount: Array.isArray(segment.last.turns) ? segment.last.turns.length : 0,
traceRowCount: 0,
sampleCount: segment.sampleCount,
observedMs,
budgetMs,
finalMessageVisible: workbenchFinalMessageVisible(segment.last, segment.traceId),
terminalTurnVisible: workbenchTerminalTurnVisible(segment.last, segment.traceId),
detail: "terminal turn/message stayed visible for this trace while the same in-place Workbench page had zero trace rows beyond the configured budget",
valuesRedacted: true
});
};
for (const sample of sortedSamples) {
if (!isWorkbenchPathSample(sample)) continue;
const tsMs = Date.parse(sample?.ts || "");
if (!Number.isFinite(tsMs)) continue;
const terminalTraceIds = workbenchTerminalTraceIdsFromDom(sample);
const presentKeys = new Set();
for (const traceId of terminalTraceIds) {
const key = [samplePageKey(sample), sample?.routeSessionId ?? "", sample?.activeSessionId ?? "", traceId].join("|");
presentKeys.add(key);
const traceRows = workbenchTraceRowsForTrace(sample, traceId);
if (traceRows.length > 0 || workbenchSampleHasTerminalProjection(sample, { traceIds: [traceId] })) {
closeSegment(key, sample);
continue;
}
const existing = open.get(key);
if (existing) {
existing.last = sample;
existing.sampleCount += 1;
} else {
open.set(key, { first: sample, last: sample, traceId, sampleCount: 1 });
}
}
for (const [key, segment] of Array.from(open.entries())) {
if (samplePageKey(sample) === samplePageKey(segment.first) && !presentKeys.has(key)) closeSegment(key, sample);
}
}
for (const key of Array.from(open.keys())) closeSegment(key);
return rows;
}
function detectWorkbenchTerminalApiDomLag(samples, network, canarySessionIds = new Set()) {
const windowMs = 30_000;
const budgetMs = Number.isFinite(Number(alertThresholds.sameOriginApiSlowMs)) ? Number(alertThresholds.sameOriginApiSlowMs) : 10_000;
const sampleRows = (Array.isArray(samples) ? samples : [])
.filter(isWorkbenchPathSample)
.filter((sample) => workbenchSampleMatchesCanarySessions(sample, canarySessionIds))
.map((sample) => ({ sample, tsMs: Date.parse(sample?.ts || ""), pageKey: samplePageKey(sample) }))
.filter((item) => Number.isFinite(item.tsMs))
.sort((a, b) => a.tsMs - b.tsMs);
const rowsByPage = new Map();
for (const row of sampleRows) {
const rows = rowsByPage.get(row.pageKey) || [];
rows.push(row);
rowsByPage.set(row.pageKey, rows);
}
const terminalEvents = (Array.isArray(network) ? network : [])
.map(compactWorkbenchTerminalApiEvent)
.filter((item) => workbenchTerminalEventMatchesCanarySessions(item, canarySessionIds))
.filter((item) => item !== null);
const overBudget = [];
for (const event of terminalEvents) {
const pageSamples = rowsByPage.get(event.pageKey) || [];
if (pageSamples.length === 0 || event.tsMs < pageSamples[0].tsMs) continue;
const alreadyVisible = lastWorkbenchSampleAtOrBefore(pageSamples, event.tsMs, event, (row) => workbenchSampleHasTerminalProjection(row.sample, event));
if (alreadyVisible) continue;
const firstAfter = firstWorkbenchSampleAfter(pageSamples, event.tsMs, event.tsMs + windowMs, event);
const firstMiss = firstWorkbenchSampleAfter(pageSamples, event.tsMs, event.tsMs + budgetMs, event, (row) => !workbenchSampleHasTerminalProjection(row.sample, event));
const resolved = firstWorkbenchSampleAfter(pageSamples, event.tsMs, event.tsMs + windowMs, event, (row) => workbenchSampleHasTerminalProjection(row.sample, event));
const deltaMs = resolved ? Math.max(0, Math.round(resolved.tsMs - event.tsMs)) : null;
const unresolved = !resolved;
const exceedsBudget = unresolved || (Number.isFinite(deltaMs) && deltaMs > budgetMs);
if (!firstMiss) continue;
if (!exceedsBudget) continue;
overBudget.push({
ts: event.ts,
pageRole: event.pageRole,
pageId: event.pageId,
routeKind: event.routeKind,
method: event.method,
path: event.path,
traceIds: event.traceIds.slice(0, 6),
sessionIds: event.sessionIds.slice(0, 4),
terminalEvidenceCount: event.terminalEvidenceCount,
traceEventLikeCount: event.traceEventLikeCount,
finalTextFieldCount: event.finalTextFieldCount,
budgetMs,
windowMs,
resolvedDeltaMs: deltaMs,
unresolvedWithinWindow: unresolved,
firstAfterSample: compactWorkbenchProjectionSample(firstAfter?.sample, event),
firstMissSample: compactWorkbenchProjectionSample(firstMiss?.sample, event),
resolvedSample: compactWorkbenchProjectionSample(resolved?.sample, event),
valuesRedacted: true
});
}
return {
summary: {
terminalEventCount: terminalEvents.length,
overBudgetCount: overBudget.length,
budgetMs,
windowMs,
valuesRedacted: true
},
overBudget,
terminalEvents: terminalEvents.slice(-20),
valuesRedacted: true
};
}
function workbenchSampleMatchesCanarySessions(sample, canarySessionIds) {
if (!(canarySessionIds instanceof Set) || canarySessionIds.size === 0) return true;
const sessionIds = [sample?.routeSessionId, sample?.activeSessionId].filter(Boolean).map(String);
return sessionIds.some((id) => canarySessionIds.has(id));
}
function workbenchTerminalEventMatchesCanarySessions(event, canarySessionIds) {
if (!event) return false;
if (!(canarySessionIds instanceof Set) || canarySessionIds.size === 0) return true;
const eventSessionIds = Array.isArray(event.sessionIds) ? event.sessionIds.map(String) : [];
return eventSessionIds.some((id) => canarySessionIds.has(id));
}
function compactWorkbenchTerminalApiEvent(item) {
if (!item || item.type !== "response" || item.observerInitiated === true) return null;
const summary = objectValue(item.bodySummary);
if (!summary || Number(summary.terminalEvidenceCount ?? 0) <= 0) return null;
const parsed = parseApiDomLagUrl(item.url);
if (!String(parsed.path || "").startsWith("/v1/workbench/") && parsed.path !== "/v1/agent/chat" && parsed.path !== "/v1/agent/chat/steer") return null;
const routeKind = summary.pathKind ?? apiDomLagRouteKind(parsed.path);
if (!isReliableWorkbenchTerminalApiEvent(summary, routeKind)) return null;
const terminalTraceIds = uniqueSorted(Array.isArray(summary.terminalTraceIds) ? summary.terminalTraceIds : []).slice(0, 12);
if (terminalTraceIds.length === 0) return null;
const tsMs = Date.parse(item.ts || "");
if (!Number.isFinite(tsMs)) return null;
return {
ts: item.ts,
tsMs,
pageRole: item.pageRole ?? null,
pageId: item.pageId ?? null,
pageKey: String(item.pageRole || "control") + ":" + String(item.pageId || "default"),
method: String(item.method || "GET").toUpperCase(),
path: parsed.path,
routeKind,
traceIds: terminalTraceIds,
observedTraceIds: uniqueSorted([...(Array.isArray(summary.traceIds) ? summary.traceIds : []), parsed.traceId].filter(Boolean)).slice(0, 12),
sessionIds: uniqueSorted([...(Array.isArray(summary.sessionIds) ? summary.sessionIds : []), parsed.sessionId].filter(Boolean)).slice(0, 12),
terminalEvidenceCount: Number(summary.terminalEvidenceCount ?? 0),
terminalStatusCount: Number(summary.terminalStatusCount ?? 0),
terminalTextCount: Number(summary.terminalTextCount ?? 0),
traceEventLikeCount: Number(summary.traceEventLikeCount ?? 0),
finalTextFieldCount: Number(summary.finalTextFieldCount ?? 0),
valuesRedacted: true
};
}
function isReliableWorkbenchTerminalApiEvent(summary, routeKind) {
if (!summary || routeKind !== "workbench-turn") return false;
if (Number(summary.terminalEvidenceCount ?? 0) <= 0) return false;
return Number(summary.runningStatusCount ?? 0) <= 0;
}
function lastWorkbenchSampleAtOrBefore(rows, tsMs, event, predicate = null) {
let result = null;
for (const row of rows || []) {
if (row.tsMs > tsMs) break;
if (!workbenchSampleMatchesTerminalEvent(row.sample, event)) continue;
if (typeof predicate === "function" && !predicate(row)) continue;
result = row;
}
return result;
}
function firstWorkbenchSampleAfter(rows, startMs, endMs, event, predicate = null) {
for (const row of rows || []) {
if (row.tsMs < startMs) continue;
if (row.tsMs > endMs) break;
if (!workbenchSampleMatchesTerminalEvent(row.sample, event)) continue;
if (typeof predicate === "function" && !predicate(row)) continue;
return row;
}
return null;
}
function workbenchSampleMatchesTerminalEvent(sample, event) {
if (!sample || !event) return false;
if (event.sessionIds.length > 0) {
const sessionIds = new Set([sample.routeSessionId, sample.activeSessionId].filter(Boolean).map(String));
if (!event.sessionIds.some((id) => sessionIds.has(id))) return false;
}
if (event.traceIds.length > 0) {
const traces = sampleTraceIds(sample);
if (traces.size === 0) return false;
if (!event.traceIds.some((id) => traces.has(id))) return false;
}
return true;
}
function workbenchSampleHasTerminalProjection(sample, event) {
const traceIds = event.traceIds.length > 0 ? event.traceIds : Array.from(sampleTraceIds(sample));
if (traceIds.length === 0) return false;
return traceIds.some((traceId) => workbenchFinalMessageVisible(sample, traceId) || workbenchTerminalTurnVisible(sample, traceId));
}
function compactWorkbenchProjectionSample(sample, event = null) {
if (!sample) return null;
const eventTraceIds = event?.traceIds && event.traceIds.length > 0 ? event.traceIds : Array.from(sampleTraceIds(sample));
const visibleTraceIds = eventTraceIds.slice(0, 6);
return {
...ref(sample),
messageCount: Array.isArray(sample.messages) ? sample.messages.length : 0,
turnCount: Array.isArray(sample.turns) ? sample.turns.length : 0,
traceRowCount: Array.isArray(sample.traceRows) ? sample.traceRows.length : 0,
traceIds: visibleTraceIds,
finalMessageVisible: visibleTraceIds.some((traceId) => workbenchFinalMessageVisible(sample, traceId)),
terminalTurnVisible: visibleTraceIds.some((traceId) => workbenchTerminalTurnVisible(sample, traceId)),
valuesRedacted: true
};
}
function isWorkbenchPathSample(sample) {
return /\/workbench(?:\/|$)/u.test(String(sample?.path || sample?.url || ""));
}
function workbenchTerminalTraceIdsFromDom(sample) {
const ids = new Set();
for (const groupName of ["turns", "messages"]) {
const group = Array.isArray(sample?.[groupName]) ? sample[groupName] : [];
for (const item of group) {
const traceId = stringOrNull(item?.traceId);
if (!traceId) continue;
if (workbenchDomItemIsTerminal(item)) ids.add(traceId);
}
}
return Array.from(ids).sort();
}
function workbenchTraceRowsForTrace(sample, traceId) {
const rows = Array.isArray(sample?.traceRows) ? sample.traceRows : [];
return rows.filter((item) => !traceId || item?.traceId === traceId);
}
function workbenchFinalMessageVisible(sample, traceId) {
const messages = Array.isArray(sample?.messages) ? sample.messages : [];
return messages.some((item) => (!traceId || item?.traceId === traceId) && workbenchDomItemLooksFinal(item));
}
function workbenchTerminalTurnVisible(sample, traceId) {
const turns = Array.isArray(sample?.turns) ? sample.turns : [];
return turns.some((item) => (!traceId || item?.traceId === traceId) && workbenchDomItemIsTerminal(item));
}
function workbenchDomItemIsTerminal(item) {
const status = String(item?.status || "").toLowerCase();
if (/^(completed|complete|succeeded|success|failed|failure|error|canceled|cancelled|done)$/u.test(status)) return true;
return isTerminalTraceText([item?.status, item?.textPreview, item?.text, item?.durationText, item?.activityText].filter(Boolean).join(" "));
}
function workbenchDomItemLooksFinal(item) {
const text = [item?.status, item?.textPreview, item?.text].filter(Boolean).join(" ");
return workbenchDomItemIsTerminal(item) && isFinalResultText(text);
}
function promptCommandHasAuthoritativeSubmitSideEffect(control, promptRound) {
const commandId = stringOrNull(promptRound?.promptCommandId);
if (!commandId) return false;
const row = (control || []).find((item) => item?.type === "sendPrompt" && item?.phase === "completed" && item?.commandId === commandId);
const detail = objectValue(row?.detail);
const chatSubmit = objectValue(detail.chatSubmit);
const sideEffect = objectValue(chatSubmit.sideEffect);
return chatSubmit.sideEffectObserved === true
|| sideEffect.submitted === true
|| Number(sideEffect.messageCountDelta ?? 0) > 0;
}
function buildFindings(samples, control, network, errors, sampleMetrics, promptNetwork, runtimeAlerts, pagePerformance, pageProvenance, commandFailures = [], manifest = {}, apiDomLag = null) {
const findings = [];
const effectiveApiDomLag = apiDomLag || buildApiDomLagReport(samples, network);
if (commandFailures.length > 0) findings.push({ id: "observer-command-failed", severity: "red", summary: "observer control commands failed; analyze must surface command failure instead of hiding it in command artifacts", count: commandFailures.length, commands: commandFailures.slice(0, 20) });
findings.push(...buildFrontendFreezeFindings(errors, control));
findings.push(...buildControlledNavigationRootCauseFindings(control, manifest));
findings.push(...buildSessionInvariantFindings(control, manifest));
const commandTimes = control
.filter((item) => item.phase === "completed" || item.phase === "started" || item.type === "observer-periodic-refresh")
.map((item) => Date.parse(item.ts))
.filter(Number.isFinite);
const controlledNavigationWindows = sessionInvariantNavigationWindows(control);
const routeSessions = new Set(samples.map((item) => item.routeSessionId).filter(Boolean));
const activeSessions = new Set(samples.map((item) => item.activeSessionId).filter(Boolean));
const routeSessionUnexpected = sessionChangeSamplesOutsideControlledNavigation(samples, "routeSessionId", controlledNavigationWindows);
const activeSessionUnexpected = sessionChangeSamplesOutsideControlledNavigation(samples, "activeSessionId", controlledNavigationWindows);
if (routeSessions.size > 1 && routeSessionUnexpected.length > 0) findings.push({ id: "session-route-changed", severity: "amber", summary: "route session changed outside controlled session-invariance navigation windows", routeSessionCount: routeSessions.size, samples: sampleRefs(routeSessionUnexpected, (item) => item.routeSessionId) });
if (activeSessions.size > 1 && activeSessionUnexpected.length > 0) findings.push({ id: "active-session-changed", severity: "amber", summary: "active session changed outside controlled session-invariance navigation windows", activeSessionCount: activeSessions.size, samples: sampleRefs(activeSessionUnexpected, (item) => item.activeSessionId) });
const mismatches = samples.filter((item) => item.routeSessionId && item.activeSessionId && item.routeSessionId !== item.activeSessionId && !sampleInControlledNavigationWindow(item, controlledNavigationWindows));
if (mismatches.length > 0) findings.push({ id: "route-active-session-mismatch", severity: "red", summary: "routeSessionId and activeSessionId diverged", count: mismatches.length, samples: mismatches.slice(0, 10).map(ref) });
const uncommandedChanges = [];
const commandedPromptSeqs = new Set((sampleMetrics?.timeline ?? []).filter((item) => Number(item?.promptIndex) > 0).map((item) => item.seq).filter((seq) => seq !== null && seq !== undefined));
const previousDigestByPage = new Map();
for (const sample of samples) {
const pageKey = samplePageKey(sample);
const previous = previousDigestByPage.get(pageKey);
const next = digestSample(sample);
if (previous && previous.digest !== next && !commandedPromptSeqs.has(sample?.seq) && !nearCommand(sample, commandTimes, alertThresholds.uncommandedStateChangeCommandWindowMs)) {
uncommandedChanges.push({
...ref(sample),
previousSeq: previous.sample?.seq ?? null,
previousTs: previous.sample?.timestamp ?? previous.sample?.ts ?? null,
fromDigest: previous.digest,
toDigest: next,
fromMessageCount: previous.sample?.messageCount ?? previous.sample?.messages?.length ?? null,
toMessageCount: sample?.messageCount ?? sample?.messages?.length ?? null,
fromTraceRowCount: Array.isArray(previous.sample?.traceRows) ? previous.sample.traceRows.length : previous.sample?.traceRowCount ?? null,
toTraceRowCount: Array.isArray(sample?.traceRows) ? sample.traceRows.length : sample?.traceRowCount ?? null,
fromPath: previous.sample?.path ?? previous.sample?.urlPath ?? null,
toPath: sample?.path ?? sample?.urlPath ?? null,
detail: "visible digest changed without nearby command",
});
}
previousDigestByPage.set(pageKey, { digest: next, sample });
}
if (uncommandedChanges.length > 0) findings.push({ id: "uncommanded-visible-state-change", severity: "amber", summary: "visible message/trace digest changed without a nearby command", count: uncommandedChanges.length, samples: uncommandedChanges.slice(0, 20) });
const finalFlicker = detectFinalFlicker(samples);
if (finalFlicker.length > 0) findings.push({ id: "final-response-flicker", severity: "red", summary: "message text digest disappeared or switched to diagnostic-like text after non-empty final text", count: finalFlicker.length, samples: finalFlicker.slice(0, 20) });
const terminalZeroElapsed = detectTerminalZeroElapsed(samples);
if (terminalZeroElapsed.length > 0) findings.push({ id: "turn-terminal-zero-elapsed", severity: "amber", summary: "terminal Code Agent card displayed 耗时 0 秒; terminal duration issue is a non-blocking timing alert", count: terminalZeroElapsed.length, samples: terminalZeroElapsed.slice(0, 20) });
const cardTiming = sampleMetrics?.codeAgentCardTiming || {};
const cardTimingSummary = cardTiming.summary || {};
if (Number(cardTimingSummary.missingElapsedCount ?? 0) > 0) findings.push({ id: "code-agent-card-elapsed-missing", severity: "amber", summary: "visible Code Agent card did not display total elapsed time; elapsed visibility is a non-blocking timing alert", count: cardTimingSummary.missingElapsedCount, samples: (cardTiming.missingElapsed || []).slice(0, 20) });
if (Number(cardTimingSummary.missingRecentUpdateCount ?? 0) > 0) findings.push({ id: "code-agent-card-running-recent-update-missing", severity: "amber", summary: "non-terminal Code Agent card did not display 最近更新; recent-update visibility is a non-blocking timing alert", count: cardTimingSummary.missingRecentUpdateCount, samples: (cardTiming.missingRecentUpdate || []).slice(0, 20) });
const roundCompletion = cardTiming.roundCompletion || {};
if (Number(cardTimingSummary.durationUnderreportedCount ?? 0) > 0) {
findings.push({
id: "code-agent-card-duration-underreported",
severity: "amber",
summary: "completed Code Agent card total elapsed is shorter than trace/final-response duration evidence; timing mismatch is a non-blocking alert",
timingSourceOfTruth: "trace-completion-total-or-final-response-duration",
timingStatus: timingStatusFromRows(cardTiming.durationUnderreported, "business-turn-completed"),
timingAlert: true,
count: cardTimingSummary.durationUnderreportedCount,
samples: (cardTiming.durationUnderreported || []).slice(0, 20),
});
}
if (Number(cardTimingSummary.durationMismatchCount ?? 0) > 0) {
findings.push({
id: "code-agent-card-duration-mismatch",
severity: "amber",
summary: "completed Code Agent card total elapsed does not match sealed completion/final-response timing evidence; timing mismatch is a non-blocking alert",
timingSourceOfTruth: "trace-completion-total-or-final-response-duration",
timingStatus: timingStatusFromRows(cardTiming.durationMismatches, "business-turn-completed"),
timingAlert: true,
count: cardTimingSummary.durationMismatchCount,
samples: (cardTiming.durationMismatches || []).slice(0, 20),
});
}
const traceOrder = sampleMetrics?.traceOrder || {};
const traceOrderSummary = traceOrder.summary || {};
if (Number(traceOrderSummary.orderAnomalyCount ?? 0) > 0) {
findings.push({
id: "trace-row-order-nonmonotonic",
severity: "red",
summary: "visible trace rows are not monotonic by total time, clock time, or projected sequence in DOM order",
count: traceOrderSummary.orderAnomalyCount,
samples: (traceOrder.orderAnomalies || []).slice(0, 20),
});
}
if (Number(traceOrderSummary.completionNotLastCount ?? 0) > 0) {
findings.push({
id: "trace-completion-row-not-last",
severity: "red",
summary: "visible trace shows a completion row before later trace rows for the same trace",
count: traceOrderSummary.completionNotLastCount,
samples: (traceOrder.completionNotLast || []).slice(0, 20),
});
}
if (Number(cardTimingSummary.roundCompletionElapsedMismatchCount ?? 0) > 0) findings.push({ id: "round-completion-elapsed-mismatch", severity: "amber", summary: "Trace row 轮次完成(总耗时 ... does not match the visible Code Agent card total elapsed time within YAML timing slack; timing mismatch is a non-blocking alert", timingSourceOfTruth: "trace-round-completion-total", timingStatus: timingStatusFromRows(roundCompletion.elapsedMismatches, "business-turn-completed"), timingAlert: true, count: cardTimingSummary.roundCompletionElapsedMismatchCount, toleranceSeconds: cardTimingSummary.elapsedMismatchToleranceSeconds, samples: (roundCompletion.elapsedMismatches || []).slice(0, 20) });
if (Number(cardTimingSummary.roundCompletionFinalResponseMissingCount ?? 0) > 0) findings.push({ id: "round-completion-final-response-missing", severity: "red", summary: "Trace row showed 轮次完成, but no final response was visible in the Code Agent card afterward", count: cardTimingSummary.roundCompletionFinalResponseMissingCount, samples: (roundCompletion.finalResponseMissing || []).slice(0, 20) });
if (Number(cardTimingSummary.roundCompletionPostTimingChangeCount ?? 0) > 0) findings.push({ id: "round-completion-post-timing-change", severity: "amber", summary: "After 轮次完成, card total elapsed or 最近更新 continued changing; terminal timing alert is non-blocking", count: cardTimingSummary.roundCompletionPostTimingChangeCount, samples: (roundCompletion.postCompletionTimingChanges || []).slice(0, 20) });
if (Number(cardTimingSummary.roundCompletionPostRecentUpdateVisibleCount ?? 0) > 0) findings.push({ id: "round-completion-recent-update-still-visible", severity: "info", summary: "最近更新 was still visible after 轮次完成; inspect whether terminal cards should hide activity age or keep it sealed", count: cardTimingSummary.roundCompletionPostRecentUpdateVisibleCount, samples: (roundCompletion.postCompletionRecentUpdateVisible || []).slice(0, 20) });
const scrollJumps = [];
for (let i = 1; i < samples.length; i += 1) {
const prevY = Number(samples[i - 1]?.scroll?.y ?? 0);
const nextY = Number(samples[i]?.scroll?.y ?? 0);
if (prevY > alertThresholds.scrollJumpFromY && nextY < alertThresholds.scrollJumpToY && !nearCommand(samples[i], commandTimes, alertThresholds.scrollJumpCommandWindowMs)) scrollJumps.push({ from: ref(samples[i - 1]), to: ref(samples[i]) });
}
if (scrollJumps.length > 0) findings.push({ id: "scroll-jump-top", severity: "amber", summary: "scroll position jumped near top without nearby command", count: scrollJumps.length, samples: scrollJumps.slice(0, 10) });
const traceTerminal = samples.some((item) => Array.isArray(item.traceRows) && item.traceRows.some((row) => isTerminalTraceText((row.status || "") + " " + (row.textPreview || ""))));
const traceSeen = samples.some((item) => Array.isArray(item.traceRows) && item.traceRows.length > 0);
if (traceSeen && !traceTerminal) findings.push({ id: "trace-without-terminal", severity: "amber", summary: "trace rows were visible but no terminal status was sampled", firstTraceSample: ref(samples.find((item) => Array.isArray(item.traceRows) && item.traceRows.length > 0)) });
const workbenchInPlaceProjectionLag = detectWorkbenchInPlaceProjectionLag(samples, network, control);
if (workbenchInPlaceProjectionLag.terminalTraceMissing.length > 0) findings.push({
id: "workbench-terminal-trace-not-hydrated-in-place",
severity: "red",
summary: "Workbench rendered a terminal turn/message in-place while the same trace still had no visible run-record trace rows",
count: workbenchInPlaceProjectionLag.terminalTraceMissing.length,
samples: workbenchInPlaceProjectionLag.terminalTraceMissing.slice(0, 20),
rootCause: "workbench_trace_projection_not_hydrated_in_place",
rootCauseStatus: "confirmed-from-dom-samples",
rootCauseConfidence: "high",
valuesRedacted: true
});
if (workbenchInPlaceProjectionLag.terminalApiDomLag.overBudget.length > 0) findings.push({
id: "workbench-terminal-api-dom-not-refreshed-in-place",
severity: "red",
summary: "Workbench REST returned terminal/final trace evidence but the same in-place page did not render terminal message plus trace rows within the configured budget",
count: workbenchInPlaceProjectionLag.terminalApiDomLag.overBudget.length,
budgetMs: workbenchInPlaceProjectionLag.terminalApiDomLag.summary.budgetMs,
windowMs: workbenchInPlaceProjectionLag.terminalApiDomLag.summary.windowMs,
samples: workbenchInPlaceProjectionLag.terminalApiDomLag.overBudget.slice(0, 20),
rootCause: "workbench_rest_terminal_projection_dom_lag",
rootCauseStatus: "confirmed-from-network-body-summary-and-dom-samples",
rootCauseConfidence: "high",
valuesRedacted: true
});
const promptFailures = Array.isArray(promptNetwork?.rounds) ? promptNetwork.rounds.filter((item) => item.chatPostOk === false && !promptCommandHasAuthoritativeSubmitSideEffect(control, item)) : [];
if (promptFailures.length > 0) findings.push({ id: "prompt-chat-submit-failed", severity: "red", summary: "sendPrompt command had no successful /v1/agent/chat or /v1/agent/chat/steer POST response in the sampling window", count: promptFailures.length, rounds: promptFailures.slice(0, 10) });
const promptSteerRounds = Array.isArray(promptNetwork?.rounds) ? promptNetwork.rounds.filter((item) => item.steerUsed === true) : [];
if (promptSteerRounds.length > 0) findings.push({ id: "prompt-routed-to-steer", severity: "amber", summary: "sendPrompt was submitted through /v1/agent/chat/steer; verify the previous turn was truly in-flight and not an unsealed terminal failure", count: promptSteerRounds.length, rounds: promptSteerRounds.slice(0, 10) });
const elapsedZeroResets = Array.isArray(sampleMetrics?.turnTimingElapsedZeroResets) ? sampleMetrics.turnTimingElapsedZeroResets : [];
if (elapsedZeroResets.length > 0) findings.push({ id: "turn-timing-total-elapsed-zero-reset", severity: "amber", summary: "Code Agent total elapsed jumped from a non-zero value back to 0 seconds; timing reset is a non-blocking alert", timingSourceOfTruth: "dom-card-total-elapsed-sequence", timingStatus: timingStatusFromRows(elapsedZeroResets), timingAlert: true, count: elapsedZeroResets.length, samples: elapsedZeroResets.slice(0, 20) });
const elapsedDecreases = Array.isArray(sampleMetrics?.turnTimingNonMonotonic)
? sampleMetrics.turnTimingNonMonotonic.filter((item) => item.metric === "totalElapsedSeconds" && item.anomaly !== "zero-reset")
: [];
if (elapsedDecreases.length > 0) findings.push({ id: "turn-timing-total-elapsed-decrease", severity: "amber", summary: "Code Agent total elapsed decreased between adjacent samples; timing decrease is a non-blocking alert", timingSourceOfTruth: "dom-card-total-elapsed-sequence", timingStatus: timingStatusFromRows(elapsedDecreases), timingAlert: true, count: elapsedDecreases.length, samples: elapsedDecreases.slice(0, 20) });
const elapsedForwardJumps = Array.isArray(sampleMetrics?.turnTimingTotalElapsedForwardJumps) ? sampleMetrics.turnTimingTotalElapsedForwardJumps : [];
if (elapsedForwardJumps.length > 0) findings.push({ id: "turn-timing-total-elapsed-forward-jump", severity: "amber", summary: "Code Agent total elapsed jumped forward faster than browser sample interval; timing jump is a non-blocking alert", timingSourceOfTruth: "dom-card-total-elapsed-sequence", timingStatus: timingStatusFromRows(elapsedForwardJumps), timingAlert: true, count: elapsedForwardJumps.length, samples: elapsedForwardJumps.slice(0, 20) });
const terminalElapsedGrowth = Array.isArray(sampleMetrics?.turnTimingTerminalElapsedGrowth) ? sampleMetrics.turnTimingTerminalElapsedGrowth : [];
if (terminalElapsedGrowth.length > 0) findings.push({ id: "turn-timing-terminal-elapsed-growth", severity: "amber", summary: "terminal Code Agent card total elapsed changed after terminal status; terminal timing alert is non-blocking", timingSourceOfTruth: "terminal-card-total-elapsed-seal", timingStatus: timingStatusFromRows(terminalElapsedGrowth, "business-turn-completed"), timingAlert: true, count: terminalElapsedGrowth.length, samples: terminalElapsedGrowth.slice(0, 20) });
const recentUpdateSawtoothJumps = Array.isArray(sampleMetrics?.turnTimingRecentUpdateSawtoothJumps)
? sampleMetrics.turnTimingRecentUpdateSawtoothJumps
: Array.isArray(sampleMetrics?.turnTimingNonMonotonic)
? sampleMetrics.turnTimingNonMonotonic.filter((item) => item.metric === "recentUpdateSeconds" && item.anomaly === "jump")
: [];
if (recentUpdateSawtoothJumps.length > 0) findings.push({ id: "turn-timing-recent-update-sawtooth-jump", severity: "amber", summary: "最近更新 value jumped faster than sample interval; expected sawtooth increase-or-reset", count: recentUpdateSawtoothJumps.length, samples: recentUpdateSawtoothJumps.slice(0, 20) });
const severeTimeoutRounds = Array.isArray(sampleMetrics?.rounds) ? sampleMetrics.rounds.filter((item) => Number(item.maxTotalElapsedSeconds) > alertThresholds.turnElapsedSevereTimeoutSeconds) : [];
const severeTimeoutSamples = Array.isArray(sampleMetrics?.timeline) ? sampleMetrics.timeline.filter((item) => Number(item.totalElapsedSeconds) > alertThresholds.turnElapsedSevereTimeoutSeconds) : [];
if (severeTimeoutRounds.length > 0 || severeTimeoutSamples.length > 0) findings.push({ id: "turn-elapsed-severe-timeout", severity: "amber", summary: "turn total elapsed exceeded the YAML-configured elapsed alert threshold; timing is a non-blocking alert unless the turn fails to complete or breaks multi-round continuity", timingSourceOfTruth: "dom-card-total-elapsed-yaml-threshold", timingStatus: timingStatusFromRows([...severeTimeoutRounds, ...severeTimeoutSamples], "observer-timeout"), timingAlert: true, thresholdSeconds: alertThresholds.turnElapsedSevereTimeoutSeconds, count: Math.max(severeTimeoutRounds.length, severeTimeoutSamples.length), rounds: severeTimeoutRounds.slice(0, 20), samples: severeTimeoutSamples.slice(0, 20) });
const loadingSummary = sampleMetrics?.loading?.summary || {};
const visibleLoadingSlowSeconds = alertThresholds.visibleLoadingSlowMs / 1000;
if (Number(loadingSummary.longestContinuousSeconds ?? 0) > visibleLoadingSlowSeconds) findings.push({ id: "page-loading-visible-over-budget", severity: "red", summary: "visible 加载中 stayed on screen longer than configured YAML budget; fix real loading latency instead of revealing incomplete content early", count: loadingSummary.overBudgetSegmentCount ?? loadingSummary.overFiveSecondSegmentCount ?? 1, longestContinuousSeconds: loadingSummary.longestContinuousSeconds, budgetSeconds: visibleLoadingSlowSeconds, segments: sampleMetrics.loading.segments.slice(0, 20), owners: sampleMetrics.loading.owners.slice(0, 20) });
if (Number(loadingSummary.maxSimultaneousCount ?? 0) > 1) findings.push({ id: "page-loading-concurrent", severity: "info", summary: "multiple 加载中 indicators were visible in the same sampled DOM point", count: loadingSummary.concurrentLoadingSampleCount ?? 0, maxSimultaneousCount: loadingSummary.maxSimultaneousCount, owners: sampleMetrics.loading.owners.slice(0, 20) });
const sessionRailTitleSummary = sampleMetrics?.sessionRailTitles?.summary || {};
if (Number(sessionRailTitleSummary.overThresholdSampleCount ?? sessionRailTitleSummary.majorityFallbackSampleCount ?? 0) > 0) findings.push({
id: "session-rail-title-fallback-root-cause",
severity: "red",
summary: "INV-02 root cause visible: session rail is rendering fallback Session ses_* titles, so list projection/read model or rail binding is missing stable title/preview data before DOM render",
rootCause: "session_title_fallback_from_facts",
rootCauseStatus: "confirmed-from-dom-session-rail",
rootCauseConfidence: "high",
nextAction: "Check OTel session_list_read fallbackTitleCount/fallbackTitleRatio and emptyPreviewCount for the same run; fix session list projection/read model title/preview before changing DOM fallback text.",
count: sessionRailTitleSummary.overThresholdSampleCount ?? sessionRailTitleSummary.majorityFallbackSampleCount,
thresholdRatio: alertThresholds.sessionRailFallbackRatio,
maxFallbackRatio: sessionRailTitleSummary.maxFallbackRatio,
maxFallbackTitleCount: sessionRailTitleSummary.maxFallbackTitleCount,
evidence: {
thresholdRatio: alertThresholds.sessionRailFallbackRatio,
maxFallbackRatio: sessionRailTitleSummary.maxFallbackRatio,
maxFallbackTitleCount: sessionRailTitleSummary.maxFallbackTitleCount,
overThresholdSampleCount: sessionRailTitleSummary.overThresholdSampleCount ?? null,
majorityFallbackSampleCount: sessionRailTitleSummary.majorityFallbackSampleCount ?? null,
valuesRedacted: true
},
samples: sampleMetrics.sessionRailTitles.samples.slice(0, 20),
examples: sampleMetrics.sessionRailTitles.examples.slice(0, 20),
valuesRedacted: true
});
const traceEventsPageReadIssues = detectTraceEventsPageReadIssues(network);
if (traceEventsPageReadIssues.http404.length > 0) findings.push({
id: "trace-events-page-read-404-root-cause",
severity: "red",
summary: "INV-07 root cause visible: /v1/workbench/traces/:traceId/events returned HTTP 404 for a trace event page read, so the failure is in the trace-events API paging/read-model contract before DOM rendering",
rootCause: "trace_events_paging_contract_mismatch",
rootCauseStatus: "confirmed-from-browser-network",
rootCauseConfidence: "high",
nextAction: "Use OTel trace_events_read for the same trace to compare sinceSeq/afterProjectedSeq, returnedEvents, range, totalEvents, hasMore and fullTraceLoaded; fix backend paging contract or add missing instrumentation before changing renderer behavior.",
count: traceEventsPageReadIssues.http404.length,
evidence: traceEventsPageReadIssues.summary,
samples: traceEventsPageReadIssues.http404.slice(0, 20),
valuesRedacted: true
});
if (traceEventsPageReadIssues.httpErrors.length > 0) findings.push({
id: "trace-events-page-read-http-error-root-cause",
severity: "red",
summary: "trace events page read returned HTTP error status; monitor can localize this to the trace-events API instead of a generic Workbench render failure",
rootCause: "trace-events-api-page-read-http-error",
rootCauseStatus: "confirmed-from-browser-network",
rootCauseConfidence: "high",
nextAction: "Drill down by traceId and afterProjectedSeq, then compare with OTel trace_events_read paging fields.",
count: traceEventsPageReadIssues.httpErrors.length,
evidence: traceEventsPageReadIssues.summary,
samples: traceEventsPageReadIssues.httpErrors.slice(0, 20),
valuesRedacted: true
});
if (traceEventsPageReadIssues.requestFailed.length > 0) findings.push({
id: "trace-events-page-read-requestfailed-root-cause",
severity: "amber",
summary: "trace events page read failed at browser/network level; monitor localized the failure path, but HTTP status is unavailable so OTel/API instrumentation is needed to confirm the backend root cause",
rootCause: "trace-events-api-page-read-network-failed",
rootCauseStatus: "network-signal-needs-otel-confirmation",
rootCauseConfidence: "medium",
nextAction: "Check whether this happened during observer refresh/navigation; if not, query OTel by /v1/workbench/traces/:traceId/events and add route span fields when status/afterProjectedSeq are missing.",
count: traceEventsPageReadIssues.requestFailed.length,
evidence: traceEventsPageReadIssues.summary,
samples: traceEventsPageReadIssues.requestFailed.slice(0, 20),
valuesRedacted: true
});
if ((runtimeAlerts?.summary?.httpErrorCount ?? 0) > 0) findings.push({ id: "runtime-http-errors", severity: "amber", summary: "natural page requests returned HTTP error status during observation", count: runtimeAlerts.summary.httpErrorCount, groups: runtimeAlerts.networkHttpErrorsByPath.slice(0, 12) });
if ((runtimeAlerts?.summary?.significantRequestFailedCount ?? runtimeAlerts?.summary?.requestFailedCount ?? 0) > 0) findings.push({ id: "runtime-requestfailed", severity: "amber", summary: "browser requestfailed events were captured during observation", count: runtimeAlerts.summary.significantRequestFailedCount ?? runtimeAlerts.summary.requestFailedCount, groups: (runtimeAlerts.networkSignificantRequestFailedByPath ?? runtimeAlerts.networkRequestFailedByPath).slice(0, 12) });
if ((runtimeAlerts?.summary?.domDiagnosticSampleCount ?? 0) > 0) findings.push({ id: "runtime-dom-diagnostics", severity: "amber", summary: "diagnostic/error/warning-like text was visible in sampled DOM", count: runtimeAlerts.summary.domDiagnosticSampleCount, groupCount: runtimeAlerts.summary.domDiagnosticGroupCount ?? 0, groups: runtimeAlerts.domDiagnosticsByText.slice(0, 12), samples: runtimeAlerts.domDiagnostics.slice(0, 12) });
if ((runtimeAlerts?.summary?.executionErrorCount ?? 0) > 0) findings.push({ id: "runtime-execution-errors", severity: "red", summary: "Workbench rendered execution failure/error rows during observation", count: runtimeAlerts.summary.executionErrorCount, groups: runtimeAlerts.runtimeExecutionErrorsByCode.slice(0, 12) });
if ((runtimeAlerts?.summary?.significantConsoleAlertCount ?? runtimeAlerts?.summary?.consoleAlertCount ?? 0) > 0) findings.push({ id: "runtime-console-alerts", severity: "amber", summary: "browser console warning/error entries were captured during observation", count: runtimeAlerts.summary.significantConsoleAlertCount ?? runtimeAlerts.summary.consoleAlertCount, groups: (runtimeAlerts.significantConsoleAlertsByPath ?? runtimeAlerts.consoleAlertsByPath).slice(0, 12) });
const crossPageDiffs = mergeCrossPageDiffRows(
detectCrossPageProjectionDiffs(samples),
detectAdjacentCrossPageProjectionDiffs(samples)
);
const crossPageProjectionDiffs = crossPageDiffs.filter((item) => item.diffKind !== "trace-visibility");
const crossPageTraceVisibilityDiffs = crossPageDiffs.filter((item) => item.diffKind === "trace-visibility");
const crossPageProjectionBudgetMs = alertThresholds.crossPageProjectionDivergenceRedMs;
const sampleBySeq = new Map(samples.map((item) => [Number(item?.seq), item]).filter(([seq]) => Number.isFinite(seq)));
const appShellNotReadyRows = detectWorkbenchAppShellNotReady(samples);
const persistentAppShellNotReadyRows = appShellNotReadyRows.filter((item) => Number(item.observedSpanMs ?? 0) > crossPageProjectionBudgetMs);
const transientAppShellNotReadyRows = appShellNotReadyRows.filter((item) => Number(item.observedSpanMs ?? 0) <= crossPageProjectionBudgetMs);
if (persistentAppShellNotReadyRows.length > 0) findings.push({
id: "workbench-app-shell-not-ready",
severity: "red",
summary: "Workbench route document loaded but the app shell never mounted or hydrated within the configured budget; treat this as page/runtime startup evidence, not a Workbench projection divergence",
count: persistentAppShellNotReadyRows.length,
budgetMs: crossPageProjectionBudgetMs,
samples: persistentAppShellNotReadyRows.slice(0, 20),
rootCause: "workbench_page_app_shell_not_ready",
rootCauseStatus: "confirmed-from-dom-and-asset-provenance",
rootCauseConfidence: "high",
valuesRedacted: true
});
if (transientAppShellNotReadyRows.length > 0) findings.push({ id: "workbench-app-shell-transient-not-ready", severity: "info", summary: "Workbench route briefly had a document and assets but no mounted app shell; retained as startup context", count: transientAppShellNotReadyRows.length, budgetMs: crossPageProjectionBudgetMs, samples: transientAppShellNotReadyRows.slice(0, 20) });
const timedCrossPageProjectionDiffs = annotateCrossPageDiffTiming(crossPageProjectionDiffs);
const controlledNavigationHydrationProjectionDiffs = timedCrossPageProjectionDiffs.filter((item) => controlledNavigationHydrationCrossPageDiff(item, controlledNavigationWindows, sampleBySeq));
const appShellNotReadyProjectionDiffs = timedCrossPageProjectionDiffs.filter((item) => !controlledNavigationHydrationCrossPageDiff(item, controlledNavigationWindows, sampleBySeq) && crossPageDiffHasWorkbenchAppShellNotReady(item, sampleBySeq));
const evaluatedCrossPageProjectionDiffs = timedCrossPageProjectionDiffs.filter((item) => !controlledNavigationHydrationCrossPageDiff(item, controlledNavigationWindows, sampleBySeq) && !crossPageDiffHasWorkbenchAppShellNotReady(item, sampleBySeq));
const persistentCrossPageProjectionDiffs = evaluatedCrossPageProjectionDiffs.filter((item) => Number(item.observedSpanMs ?? 0) > crossPageProjectionBudgetMs);
const transientCrossPageProjectionDiffs = evaluatedCrossPageProjectionDiffs.filter((item) => Number(item.observedSpanMs ?? 0) <= crossPageProjectionBudgetMs);
if (persistentCrossPageProjectionDiffs.length > 0) findings.push({ id: "cross-page-projection-divergence", severity: "red", summary: "control and observer pages saw different projection state for the same sampled session beyond the configured budget", count: persistentCrossPageProjectionDiffs.length, budgetMs: crossPageProjectionBudgetMs, samples: persistentCrossPageProjectionDiffs.slice(0, 20) });
if (transientCrossPageProjectionDiffs.length > 0) findings.push({ id: "cross-page-projection-transient-divergence", severity: "info", summary: "control and observer pages briefly differed near a sampled transition; retained as transient evidence but not treated as persistent projection failure", count: transientCrossPageProjectionDiffs.length, budgetMs: crossPageProjectionBudgetMs, samples: transientCrossPageProjectionDiffs.slice(0, 20) });
if (controlledNavigationHydrationProjectionDiffs.length > 0) findings.push({ id: "cross-page-projection-controlled-navigation-hydration", severity: "info", summary: "control and observer pages differed while a non-blocking session-invariance navigation command still had an unhydrated blank page; retained as context but not treated as a red projection blocker", count: controlledNavigationHydrationProjectionDiffs.length, budgetMs: crossPageProjectionBudgetMs, samples: controlledNavigationHydrationProjectionDiffs.slice(0, 20) });
if (appShellNotReadyProjectionDiffs.length > 0) findings.push({ id: "cross-page-projection-app-shell-not-ready", severity: "info", summary: "cross-page projection differences were explained by a page whose Workbench app shell was not mounted; see workbench-app-shell-not-ready for the blocking root cause", count: appShellNotReadyProjectionDiffs.length, budgetMs: crossPageProjectionBudgetMs, samples: appShellNotReadyProjectionDiffs.slice(0, 20) });
if (crossPageTraceVisibilityDiffs.length > 0) findings.push({ id: "cross-page-trace-visibility-divergence", severity: "info", summary: "control and observer pages differed only in visible trace row count; this is local disclosure/hydration visibility, not session/message projection divergence", count: crossPageTraceVisibilityDiffs.length, samples: crossPageTraceVisibilityDiffs.slice(0, 20) });
const traceMessageDuplicates = detectTraceMessageDuplication(samples);
if (traceMessageDuplicates.length > 0) findings.push({ id: "trace-assistant-message-duplicates-final-response", severity: "amber", summary: "trace-frame rendered duplicate visible assistant final rows; the fixed Final Response renderer summary block is excluded", count: traceMessageDuplicates.length, finalResponseSummaryBlockCounted: false, traceFrameSource: "traceRows-only", samples: traceMessageDuplicates.slice(0, 20) });
const turnTraceMissing = detectTurnTraceIdMissing(samples);
if (turnTraceMissing.length > 0) findings.push({ id: "turn-trace-id-missing", severity: "red", summary: "Code Agent turn/card was visible without a trace id, so historical trace hydration cannot be reliable", count: turnTraceMissing.length, samples: turnTraceMissing.slice(0, 20) });
const pagePerformanceItems = Array.isArray(pagePerformance?.sameOriginApiByPath) ? pagePerformance.sameOriginApiByPath : [];
const slowApi = pagePerformanceItems.filter((item) => item.isLongLivedStream !== true && Number(item.overBudgetCount ?? item.overFiveSecondCount ?? 0) > 0);
if (slowApi.length > 0) findings.push({ id: "page-performance-slow-same-origin-api", severity: "red", summary: "same-origin API resource timing exceeded configured YAML usability budget", count: slowApi.length, budgetMs: alertThresholds.sameOriginApiSlowMs, groups: slowApi.slice(0, 20) });
const longLivedStreams = pagePerformanceItems.filter((item) => item.isLongLivedStream);
const slowStreamOpen = longLivedStreams.filter((item) => Number(item.streamOpenOverBudgetCount ?? item.streamOpenOverFiveSecondCount ?? 0) > 0);
if (slowStreamOpen.length > 0) findings.push({ id: "page-performance-slow-long-lived-stream-open", severity: "red", summary: "long-lived stream open latency exceeded configured YAML usability budget; stream lifetime is still reported separately", count: slowStreamOpen.length, budgetMs: alertThresholds.longLivedStreamOpenSlowMs, groups: slowStreamOpen.slice(0, 20) });
if (longLivedStreams.length > 0) findings.push({ id: "page-performance-long-lived-streams", severity: "info", summary: "same-origin long-lived streams are reported separately; lifetime is not treated as API load latency", count: longLivedStreams.length, groups: longLivedStreams.slice(0, 20) });
if ((pageProvenance?.summary?.segmentCount ?? 0) > 1) findings.push({ id: "page-provenance-segments", severity: "info", summary: "observer crossed page asset provenance segments; interpret runtime findings by segment", segmentCount: pageProvenance.summary.segmentCount, segments: pageProvenance.segments.slice(0, 20) });
const naturalApi = network.filter((item) => item.observerInitiated === false && item.type === "response" && /\/v1\/|\/auth\//u.test(String(item.url || "")));
const apiDomLagSummary = effectiveApiDomLag?.summary || {};
findings.push({ id: "natural-api-dom-lag-baseline", severity: "info", summary: "natural API responses and DOM samples are available for API-to-DOM lag correlation", naturalApiResponses: naturalApi.length, sampleCount: samples.length, apiDomLag: apiDomLagSummary });
findings.push({
id: "natural-api-dom-lag-candidates",
severity: Number(apiDomLagSummary.overBudgetCount ?? 0) > 0 ? "amber" : "info",
summary: "state-relevant natural API responses were correlated to the first subsequent DOM digest change; over-budget lag is a non-blocking investigation alert",
count: apiDomLagSummary.candidateCount ?? 0,
domChangedCount: apiDomLagSummary.domChangedCount ?? 0,
noDomChangeWithinWindowCount: apiDomLagSummary.noDomChangeWithinWindowCount ?? 0,
overBudgetCount: apiDomLagSummary.overBudgetCount ?? 0,
budgetMs: apiDomLagSummary.budgetMs ?? null,
p95DomChangeDeltaMs: apiDomLagSummary.p95DomChangeDeltaMs ?? null,
maxDomChangeDeltaMs: apiDomLagSummary.maxDomChangeDeltaMs ?? null,
groups: Array.isArray(effectiveApiDomLag?.groups) ? effectiveApiDomLag.groups.slice(0, 12) : [],
});
if (errors.length > 0) findings.push({ id: "browser-console-or-page-errors", severity: "amber", summary: "pageerror/runner errors were captured", count: errors.length, first: errors.slice(0, 5) });
if (samples.length === 0) findings.push({ id: "no-samples", severity: "red", summary: "observer produced no samples" });
return findings;
}
function buildFrontendFreezeFindings(errors, control) {
const findings = [];
const promptTimes = (control || [])
.filter((item) => item.type === "sendPrompt" && item.phase === "completed")
.map((item) => Date.parse(item.ts))
.filter(Number.isFinite)
.sort((a, b) => a - b);
const stopWindows = stopCommandWindows(control);
const events = (errors || [])
.map((item) => frontendFreezeErrorEvent(item, promptTimes))
.filter((item) => item && !errorInsideStopWindow(item, stopWindows));
const domEvents = events.filter((item) => item.kind === "dom-evaluate-timeout");
const controlDomBurst = firstBurst(
domEvents.filter((item) => item.pageRole === "control" || item.pageRole === null),
alertThresholds.domEvaluateTimeoutRedCount,
alertThresholds.domEvaluateTimeoutRedWindowMs,
);
if (controlDomBurst) findings.push(frontendFreezeBurstFinding({
id: "frontend-control-dom-evaluate-timeout-red",
summary: "control page DOM evaluation timed out repeatedly; treat the browser page as frozen and keep the sentinel red instead of refreshing or falling back",
burst: controlDomBurst,
thresholdCount: alertThresholds.domEvaluateTimeoutRedCount,
windowMs: alertThresholds.domEvaluateTimeoutRedWindowMs,
pageRole: "control",
}));
const observerDomBurst = firstBurst(
domEvents.filter((item) => item.pageRole === "observer"),
alertThresholds.domEvaluateTimeoutRedCount,
alertThresholds.domEvaluateTimeoutRedWindowMs,
);
if (observerDomBurst) findings.push(frontendFreezeBurstFinding({
id: "frontend-observer-dom-evaluate-timeout-red",
summary: "observer page DOM evaluation timed out repeatedly; the observer page is frozen and later periodic refresh evidence must not clear this run",
burst: observerDomBurst,
thresholdCount: alertThresholds.domEvaluateTimeoutRedCount,
windowMs: alertThresholds.domEvaluateTimeoutRedWindowMs,
pageRole: "observer",
}));
const screenshotBurst = firstBurst(
events.filter((item) => item.kind === "screenshot-timeout"),
alertThresholds.screenshotTimeoutRedCount,
alertThresholds.domEvaluateTimeoutRedWindowMs,
);
if (screenshotBurst) findings.push(frontendFreezeBurstFinding({
id: "frontend-screenshot-timeout-red",
summary: "browser screenshot capture timed out repeatedly; this is freeze evidence and the sentinel must stay red until investigated",
burst: screenshotBurst,
thresholdCount: alertThresholds.screenshotTimeoutRedCount,
windowMs: alertThresholds.domEvaluateTimeoutRedWindowMs,
pageRole: null,
}));
const pageErrors = events.filter((item) => item.kind === "page-error");
const pageErrorBurst = firstBurst(pageErrors, alertThresholds.pageErrorRedCount, alertThresholds.domEvaluateTimeoutRedWindowMs);
if (pageErrorBurst) findings.push(frontendFreezeBurstFinding({
id: "frontend-page-error-red",
summary: "browser pageerror entries exceeded the YAML threshold; page runtime exceptions are blocking when repeated in the observation window",
burst: pageErrorBurst,
thresholdCount: alertThresholds.pageErrorRedCount,
windowMs: alertThresholds.domEvaluateTimeoutRedWindowMs,
pageRole: null,
}));
return findings;
}
function frontendFreezeErrorEvent(item, promptTimes) {
const details = objectValue(item?.error?.details);
const message = String(item?.error?.message ?? item?.message ?? item?.error ?? "");
const type = String(item?.type || "");
const tsMs = Date.parse(String(item?.ts || ""));
if (!Number.isFinite(tsMs)) return null;
const kind = classifyFrontendFreezeError(type, message);
if (!kind) return null;
return {
ts: item.ts ?? null,
tsMs,
promptIndex: promptIndexForTs(promptTimes, item.ts),
kind,
type: item.type ?? null,
pageRole: stringOrNull(item?.pageRole) ?? stringOrNull(details.pageRole) ?? pageRoleFromErrorType(type),
pageId: stringOrNull(item?.pageId) ?? stringOrNull(details.pageId),
routeSessionId: stringOrNull(item?.routeSessionId) ?? stringOrNull(details.routeSessionId),
activeSessionId: stringOrNull(item?.activeSessionId) ?? stringOrNull(details.activeSessionId),
commandId: stringOrNull(item?.commandId) ?? stringOrNull(details.commandId),
sampleSeq: numberOrNull(item?.sampleSeq ?? details.sampleSeq),
timeoutMs: timeoutMsFromMessage(message),
messageHash: message ? sha256(message) : null,
preview: limitText(message, 240),
valuesRedacted: true,
};
}
function pageRoleFromErrorType(type) {
const value = String(type || "");
if (/^control-/iu.test(value)) return "control";
if (/^observer-/iu.test(value)) return "observer";
return null;
}
function classifyFrontendFreezeError(type, message) {
const value = String(message || "");
if (/sampleOnePage\s+DOM\s+evaluate\s+exceeded/iu.test(value) && /(?:control|observer)-sample-error/iu.test(type)) return "dom-evaluate-timeout";
if (/screenshot|captureScreenshot|page\.screenshot/iu.test(type + " " + value) && /timeout|timed\s*out|exceeded/iu.test(value)) return "screenshot-timeout";
if (/pageerror|uncaught|unhandledrejection/iu.test(type) || /^(?:Error|TypeError|ReferenceError|RangeError|SyntaxError):/u.test(value)) return "page-error";
return null;
}
function firstBurst(events, thresholdCount, windowMs) {
const count = Math.max(1, Math.floor(Number(thresholdCount || 0)));
const budgetMs = Math.max(1, Number(windowMs || 0));
const sorted = (events || []).filter((item) => Number.isFinite(item?.tsMs)).sort((a, b) => a.tsMs - b.tsMs);
if (sorted.length < count) return null;
for (let start = 0; start <= sorted.length - count; start += 1) {
const end = start + count - 1;
if (sorted[end].tsMs - sorted[start].tsMs <= budgetMs) return sorted.slice(start, end + 1);
}
return null;
}
function frontendFreezeBurstFinding({ id, summary, burst, thresholdCount, windowMs, pageRole }) {
const first = burst[0];
const last = burst[burst.length - 1];
const pageIds = uniqueStrings(burst.map((item) => item.pageId));
const routeSessionIds = uniqueStrings(burst.map((item) => item.routeSessionId));
const activeSessionIds = uniqueStrings(burst.map((item) => item.activeSessionId));
return {
id,
severity: "red",
summary,
count: burst.length,
thresholdCount,
windowMs,
firstAt: first?.ts ?? null,
lastAt: last?.ts ?? null,
pageRole,
pageIds,
routeSessionIds,
activeSessionIds,
timeoutMsMax: maxPresentNumber(burst.map((item) => item.timeoutMs)),
rootCause: "frontend_page_freeze_or_runtime_exception",
rootCauseStatus: "confirmed-from-browser-observer-errors",
rootCauseConfidence: "high",
fallbackAllowed: false,
observerRefreshMayNotClear: true,
nextAction: "Keep this run red; do not auto-refresh, fallback, or mark healthy until OTel/browser evidence explains why the page stopped responding.",
events: burst.map((item) => ({
ts: item.ts,
promptIndex: item.promptIndex,
type: item.type,
pageRole: item.pageRole,
pageId: item.pageId,
routeSessionId: item.routeSessionId,
activeSessionId: item.activeSessionId,
commandId: item.commandId,
sampleSeq: item.sampleSeq,
timeoutMs: item.timeoutMs,
messageHash: item.messageHash,
preview: item.preview,
valuesRedacted: true,
})),
valuesRedacted: true,
};
}
function stopCommandWindows(control) {
return (control || [])
.filter((item) => /^(?:stop|forceStop|cancel|close)$/iu.test(String(item?.type || item?.command || "")))
.map((item) => {
const tsMs = Date.parse(String(item?.ts || ""));
return Number.isFinite(tsMs) ? { fromMs: tsMs - 1000, toMs: tsMs + 10000 } : null;
})
.filter(Boolean);
}
function errorInsideStopWindow(event, windows) {
return (windows || []).some((window) => event.tsMs >= window.fromMs && event.tsMs <= window.toMs);
}
function timeoutMsFromMessage(value) {
const match = String(value || "").match(/\b(?:exceeded|timeout|timed\s*out\s*after)\s+(\d{2,})\s*ms\b/iu)
|| String(value || "").match(/\b(\d{2,})\s*ms\b/iu);
return match ? Number(match[1]) : null;
}
function uniqueStrings(values) {
return Array.from(new Set((values || []).filter((item) => typeof item === "string" && item.length > 0))).slice(0, 12);
}
function maxPresentNumber(values) {
const numbers = (values || []).filter((item) => item !== null && item !== undefined && Number.isFinite(Number(item))).map((item) => Number(item));
return numbers.length > 0 ? Math.max(...numbers) : null;
}
function buildRecentAnalysisWindow({ samples, control, network, consoleEvents, errors, manifest }) {
const latestSampleMs = latestTimestampMs(samples);
const windowMs = 5 * 60 * 1000;
const fromMs = Number.isFinite(latestSampleMs) ? latestSampleMs - windowMs : Number.NEGATIVE_INFINITY;
const toMs = Number.isFinite(latestSampleMs) ? latestSampleMs : Number.POSITIVE_INFINITY;
const inWindow = (item) => {
const tsMs = Date.parse(item?.ts);
return Number.isFinite(tsMs) && tsMs >= fromMs && tsMs <= toMs;
};
const windowSamples = samples.filter(inWindow);
const windowControl = control.filter(inWindow);
const windowNetwork = network.filter(inWindow);
const windowConsole = consoleEvents.filter(inWindow);
const windowErrors = errors.filter(inWindow);
const sampleMetrics = buildSampleMetrics(windowSamples, control);
const pageProvenance = buildPageProvenanceReport(windowSamples, windowControl, manifest);
const pagePerformance = buildPagePerformanceReport(windowSamples, manifest);
const promptNetwork = buildPromptNetworkReport(windowControl, windowNetwork);
const runtimeAlerts = buildRuntimeAlerts(windowSamples, control, windowNetwork, windowConsole, windowErrors);
const apiDomLag = buildApiDomLagReport(windowSamples, windowNetwork);
const findings = buildFindings(windowSamples, control, windowNetwork, windowErrors, sampleMetrics, promptNetwork, runtimeAlerts, pagePerformance, pageProvenance, [], {}, apiDomLag);
return {
summary: {
name: "recent-5m",
windowMs,
fromAt: Number.isFinite(fromMs) ? new Date(fromMs).toISOString() : null,
toAt: Number.isFinite(toMs) ? new Date(toMs).toISOString() : null,
samples: windowSamples.length,
control: windowControl.length,
network: windowNetwork.length,
console: windowConsole.length,
errors: windowErrors.length,
valuesRedacted: true
},
sampleMetrics,
pageProvenance,
pagePerformance,
promptNetwork,
runtimeAlerts,
apiDomLag,
findings,
valuesRedacted: true
};
}
function latestTimestampMs(items) {
let latest = Number.NEGATIVE_INFINITY;
for (const item of items || []) {
const tsMs = Date.parse(item?.ts);
if (Number.isFinite(tsMs) && tsMs > latest) latest = tsMs;
}
return latest;
}
function buildPageProvenanceReport(samples, control, manifest) {
const groups = new Map();
for (const sample of samples) {
const provenance = sample?.pageProvenance;
if (!provenance) continue;
const key = provenance.assetFingerprint || "unknown";
const group = groups.get(key) || {
assetFingerprint: provenance.assetFingerprint || null,
pageLoadSeqs: [],
sampleCount: 0,
firstSeq: sample.seq ?? null,
lastSeq: sample.seq ?? null,
firstAt: sample.ts ?? null,
lastAt: sample.ts ?? null,
urlPaths: [],
scriptCount: provenance.scriptCount ?? null,
stylesheetCount: provenance.stylesheetCount ?? null,
metaCount: provenance.metaCount ?? null,
scripts: Array.isArray(provenance.scripts) ? provenance.scripts.slice(0, 12) : [],
stylesheets: Array.isArray(provenance.stylesheets) ? provenance.stylesheets.slice(0, 12) : [],
valuesRedacted: true
};
group.sampleCount += 1;
group.lastSeq = sample.seq ?? null;
group.lastAt = sample.ts ?? null;
if (provenance.pageLoadSeq !== null && provenance.pageLoadSeq !== undefined && !group.pageLoadSeqs.includes(provenance.pageLoadSeq)) group.pageLoadSeqs.push(provenance.pageLoadSeq);
if (provenance.urlPath && !group.urlPaths.includes(provenance.urlPath)) group.urlPaths.push(provenance.urlPath);
groups.set(key, group);
}
const segments = Array.from(groups.values()).sort((a, b) => Number(a.firstSeq ?? 0) - Number(b.firstSeq ?? 0));
const controlSegments = control
.filter((item) => item.type === "page-provenance" || item?.pageProvenance)
.map((item) => ({
ts: item.ts ?? null,
reason: item.reason ?? item.detail?.reason ?? null,
httpStatus: item.httpStatus ?? item.detail?.httpStatus ?? null,
pageProvenance: item.pageProvenance ?? item.detail?.pageProvenance ?? null,
}))
.slice(0, 80);
return {
summary: {
segmentCount: segments.length,
sampleCount: segments.reduce((sum, item) => sum + item.sampleCount, 0),
manifestFingerprint: manifest?.pageProvenance?.assetFingerprint ?? null,
controlSegmentCount: controlSegments.length
},
segments,
controlSegments,
valuesRedacted: true
};
}
function buildPagePerformanceReport(samples, manifest) {
const base = manifest?.baseUrl || "http://invalid.local";
const seen = new Set();
const groups = new Map();
const sampleTimes = samples.map((sample) => Date.parse(sample?.ts || "")).filter(Number.isFinite);
const windowStartMs = sampleTimes.length > 0 ? Math.min(...sampleTimes) : null;
const windowEndMs = sampleTimes.length > 0 ? Math.max(...sampleTimes) : null;
for (const sample of samples) {
const entries = Array.isArray(sample?.performance) ? sample.performance : [];
for (const entry of entries) {
const durationMs = Number(entry?.duration);
if (!Number.isFinite(durationMs) || durationMs < 0) continue;
const entryCompletedMs = performanceEntryCompletedEpochMs(sample, entry);
if (windowStartMs !== null && entryCompletedMs !== null && entryCompletedMs < windowStartMs) continue;
if (windowEndMs !== null && entryCompletedMs !== null && entryCompletedMs > windowEndMs + 1000) continue;
const entryTs = entryCompletedMs === null ? (sample.ts ?? null) : new Date(entryCompletedMs).toISOString();
const parsed = parsePerformanceUrl(entry?.name, base);
if (!parsed.sameOrigin || !isApiLikePath(parsed.path)) continue;
const normalizedPath = normalizeApiPath(parsed.path);
const routeKind = classifyApiPerformanceRoute(normalizedPath, entry);
const isLongLivedStream = routeKind === "same-origin-api-stream";
const streamOpenMs = streamOpenLatencyMs(entry);
const timingStatus = resourceTimingPhaseStatus(entry);
const dedupeKey = [parsed.path, entry.initiatorType || "", sample?.pageProvenance?.pageLoadSeq ?? "", sample?.pageProvenance?.timeOrigin ?? "", entry.startTime ?? "", Math.round(durationMs)].join("|");
if (seen.has(dedupeKey)) continue;
seen.add(dedupeKey);
const group = groups.get(normalizedPath) || {
routeKind,
path: normalizedPath,
isLongLivedStream,
budgetMetric: isLongLivedStream ? "streamOpenMs" : "durationMs",
rawPathSamples: [],
sampleCount: 0,
completeTimingSampleCount: 0,
partialTimingSampleCount: 0,
durationsMs: [],
streamOpenDurationsMs: [],
overFiveSecondCount: 0,
overBudgetCount: 0,
partialOverFiveSecondCount: 0,
partialOverBudgetCount: 0,
streamLifetimeOverFiveSecondCount: 0,
streamOpenOverFiveSecondCount: 0,
streamOpenOverBudgetCount: 0,
firstAt: entryTs,
lastAt: entryTs,
firstSeq: sample.seq ?? null,
lastSeq: sample.seq ?? null,
initiatorTypes: [],
pageAssetFingerprints: [],
slowSamples: [],
partialSamples: [],
valuesRedacted: true
};
group.sampleCount += 1;
const partialOrdinaryTiming = !isLongLivedStream && timingStatus.status !== "complete";
let overBudget = false;
if (partialOrdinaryTiming) {
group.partialTimingSampleCount += 1;
if (durationMs > 5000) group.partialOverFiveSecondCount += 1;
if (durationMs > alertThresholds.partialApiSlowMs) {
group.partialOverBudgetCount += 1;
if (group.partialSamples.length < 80) group.partialSamples.push(compactPagePerformanceSlowSample({ sample, entry, entryTs, normalizedPath, rawPath: parsed.path, durationMs, streamOpenMs }));
}
} else {
group.completeTimingSampleCount += 1;
group.durationsMs.push(durationMs);
if (isLongLivedStream) {
if (durationMs > 5000) group.streamLifetimeOverFiveSecondCount += 1;
if (streamOpenMs !== null) {
group.streamOpenDurationsMs.push(streamOpenMs);
if (streamOpenMs > 5000) {
group.streamOpenOverFiveSecondCount += 1;
group.overFiveSecondCount += 1;
}
if (streamOpenMs > alertThresholds.longLivedStreamOpenSlowMs) {
group.streamOpenOverBudgetCount += 1;
group.overBudgetCount += 1;
overBudget = true;
}
}
} else {
if (durationMs > 5000) group.overFiveSecondCount += 1;
if (durationMs > alertThresholds.sameOriginApiSlowMs) {
group.overBudgetCount += 1;
overBudget = true;
}
}
}
if (overBudget && group.slowSamples.length < 80) group.slowSamples.push(compactPagePerformanceSlowSample({ sample, entry, entryTs, normalizedPath, rawPath: parsed.path, durationMs, streamOpenMs }));
group.lastAt = entryTs;
group.lastSeq = sample.seq ?? null;
if (parsed.path && !group.rawPathSamples.includes(parsed.path)) group.rawPathSamples.push(parsed.path);
if (entry.initiatorType && !group.initiatorTypes.includes(entry.initiatorType)) group.initiatorTypes.push(entry.initiatorType);
const assetFingerprint = sample?.pageProvenance?.assetFingerprint;
if (assetFingerprint && !group.pageAssetFingerprints.includes(assetFingerprint)) group.pageAssetFingerprints.push(assetFingerprint);
groups.set(normalizedPath, group);
}
}
const sameOriginApiByPath = Array.from(groups.values()).map((group) => {
const durations = group.durationsMs.slice().sort((a, b) => a - b);
const streamOpenDurations = group.streamOpenDurationsMs.slice().sort((a, b) => a - b);
return {
routeKind: group.routeKind,
path: group.path,
isLongLivedStream: group.isLongLivedStream === true,
budgetMetric: group.budgetMetric,
sampleCount: group.sampleCount,
budgetMs: group.isLongLivedStream === true ? alertThresholds.longLivedStreamOpenSlowMs : alertThresholds.sameOriginApiSlowMs,
partialBudgetMs: alertThresholds.partialApiSlowMs,
streamOpenBudgetMs: alertThresholds.longLivedStreamOpenSlowMs,
completeTimingSampleCount: group.completeTimingSampleCount,
partialTimingSampleCount: group.partialTimingSampleCount,
p50Ms: percentile(durations, 50),
p75Ms: percentile(durations, 75),
p95Ms: percentile(durations, 95),
maxMs: durations.length > 0 ? durations[durations.length - 1] : null,
streamOpenSampleCount: streamOpenDurations.length,
streamOpenP50Ms: percentile(streamOpenDurations, 50),
streamOpenP75Ms: percentile(streamOpenDurations, 75),
streamOpenP95Ms: percentile(streamOpenDurations, 95),
streamOpenMaxMs: streamOpenDurations.length > 0 ? streamOpenDurations[streamOpenDurations.length - 1] : null,
streamOpenOverFiveSecondCount: group.streamOpenOverFiveSecondCount,
streamOpenOverBudgetCount: group.streamOpenOverBudgetCount,
streamLifetimeOverFiveSecondCount: group.streamLifetimeOverFiveSecondCount,
overFiveSecondCount: group.overFiveSecondCount,
overBudgetCount: group.overBudgetCount,
partialOverFiveSecondCount: group.partialOverFiveSecondCount,
partialOverBudgetCount: group.partialOverBudgetCount,
overFiveSecondRatio: group.sampleCount > 0 ? Number((group.overFiveSecondCount / group.sampleCount).toFixed(3)) : 0,
overBudgetRatio: group.sampleCount > 0 ? Number((group.overBudgetCount / group.sampleCount).toFixed(3)) : 0,
firstAt: group.firstAt,
lastAt: group.lastAt,
firstSeq: group.firstSeq,
lastSeq: group.lastSeq,
initiatorTypes: group.initiatorTypes,
rawPathSamples: group.rawPathSamples.slice(0, 8),
pageAssetFingerprints: group.pageAssetFingerprints.slice(0, 8),
slowSamples: group.slowSamples
.slice()
.sort((a, b) => Number(b.durationMs ?? 0) - Number(a.durationMs ?? 0))
.slice(0, 12),
partialSamples: group.partialSamples
.slice()
.sort((a, b) => Number(b.durationMs ?? 0) - Number(a.durationMs ?? 0))
.slice(0, 12),
valuesRedacted: true
};
}).sort((a, b) => (Number(b.overBudgetCount ?? b.overFiveSecondCount ?? 0) - Number(a.overBudgetCount ?? a.overFiveSecondCount ?? 0)) || (Number(b.p95Ms ?? 0) - Number(a.p95Ms ?? 0)) || a.path.localeCompare(b.path));
const longLivedStreams = sameOriginApiByPath.filter((item) => item.isLongLivedStream);
const ordinaryApi = sameOriginApiByPath.filter((item) => item.isLongLivedStream !== true);
const slow = ordinaryApi.filter((item) => Number(item.overBudgetCount ?? item.overFiveSecondCount ?? 0) > 0);
const slowFiveSecond = ordinaryApi.filter((item) => Number(item.overFiveSecondCount ?? 0) > 0);
const partialSlow = ordinaryApi.filter((item) => Number(item.partialOverBudgetCount ?? item.partialOverFiveSecondCount ?? 0) > 0);
const partialFiveSecond = ordinaryApi.filter((item) => Number(item.partialOverFiveSecondCount ?? 0) > 0);
const slowStreamOpen = longLivedStreams.filter((item) => Number(item.streamOpenOverBudgetCount ?? item.streamOpenOverFiveSecondCount ?? 0) > 0);
const slowStreamOpenFiveSecond = longLivedStreams.filter((item) => Number(item.streamOpenOverFiveSecondCount ?? 0) > 0);
const budgetP95Values = sameOriginApiByPath
.map((item) => Number(item.isLongLivedStream ? (item.streamOpenP95Ms ?? 0) : (item.p95Ms ?? 0)))
.filter((value) => Number.isFinite(value));
return {
summary: {
budgetMs: alertThresholds.sameOriginApiSlowMs,
alertThresholds,
sameOriginApiPathCount: sameOriginApiByPath.length,
sameOriginApiSampleCount: sameOriginApiByPath.reduce((sum, item) => sum + item.sampleCount, 0),
longLivedStreamPathCount: longLivedStreams.length,
longLivedStreamSampleCount: longLivedStreams.reduce((sum, item) => sum + item.sampleCount, 0),
longLivedStreamOpenOverFiveSecondPathCount: slowStreamOpenFiveSecond.length,
longLivedStreamOpenOverFiveSecondSampleCount: slowStreamOpenFiveSecond.reduce((sum, item) => sum + Number(item.streamOpenOverFiveSecondCount ?? 0), 0),
longLivedStreamOpenOverBudgetPathCount: slowStreamOpen.length,
longLivedStreamOpenOverBudgetSampleCount: slowStreamOpen.reduce((sum, item) => sum + Number(item.streamOpenOverBudgetCount ?? item.streamOpenOverFiveSecondCount ?? 0), 0),
longLivedStreamLifetimeOverFiveSecondSampleCount: longLivedStreams.reduce((sum, item) => sum + Number(item.streamLifetimeOverFiveSecondCount ?? 0), 0),
slowPathCount: slow.length,
slowSampleCount: slow.reduce((sum, item) => sum + Number(item.overBudgetCount ?? item.overFiveSecondCount ?? 0), 0),
overFiveSecondPathCount: slowFiveSecond.length,
overFiveSecondSampleCount: slowFiveSecond.reduce((sum, item) => sum + Number(item.overFiveSecondCount ?? 0), 0),
partialTimingSampleCount: ordinaryApi.reduce((sum, item) => sum + Number(item.partialTimingSampleCount ?? 0), 0),
partialOverFiveSecondPathCount: partialFiveSecond.length,
partialOverFiveSecondSampleCount: partialFiveSecond.reduce((sum, item) => sum + Number(item.partialOverFiveSecondCount ?? 0), 0),
partialOverBudgetPathCount: partialSlow.length,
partialOverBudgetSampleCount: partialSlow.reduce((sum, item) => sum + Number(item.partialOverBudgetCount ?? item.partialOverFiveSecondCount ?? 0), 0),
worstP95Ms: budgetP95Values.length > 0 ? Math.max(...budgetP95Values) : null,
valuesRedacted: true
},
sameOriginApiByPath,
valuesRedacted: true
};
}
function performanceEntryCompletedEpochMs(sample, entry) {
const origin = Number(sample?.pageProvenance?.timeOrigin);
const responseEnd = Number(entry?.responseEnd);
const startTime = Number(entry?.startTime);
const offset = Number.isFinite(responseEnd) && responseEnd > 0 ? responseEnd : startTime;
if (Number.isFinite(origin) && origin > 0 && Number.isFinite(offset) && offset >= 0) return Math.round(origin + offset);
const sampleTs = Date.parse(sample?.ts || "");
return Number.isFinite(sampleTs) ? sampleTs : null;
}
function compactPagePerformanceSlowSample({ sample, entry, entryTs, normalizedPath, rawPath, durationMs, streamOpenMs }) {
const timingStatus = resourceTimingPhaseStatus(entry);
const serverTiming = compactServerTiming(entry?.serverTiming);
return {
ts: entryTs ?? sample?.ts ?? null,
sampleTs: sample?.ts ?? null,
seq: sample?.seq ?? null,
path: normalizedPath ?? null,
rawPath: rawPath ?? null,
initiatorType: entry?.initiatorType ?? null,
durationMs: roundFinite(durationMs),
startTimeMs: roundFinite(entry?.startTime),
fetchStartMs: roundFinite(entry?.fetchStart),
requestStartMs: roundFinite(entry?.requestStart),
responseStartMs: roundFinite(entry?.responseStart),
responseEndMs: roundFinite(entry?.responseEnd),
streamOpenMs: roundFinite(streamOpenMs),
dnsMs: phaseDeltaMs(entry, "domainLookupEnd", "domainLookupStart"),
tcpMs: phaseDeltaMs(entry, "connectEnd", "connectStart"),
tlsStartMs: roundFinite(entry?.secureConnectionStart),
requestToResponseStartMs: phaseDeltaMs(entry, "responseStart", "requestStart"),
responseTransferMs: phaseDeltaMs(entry, "responseEnd", "responseStart"),
timingStatus: timingStatus.status,
invalidTimingPhases: timingStatus.invalidPhases,
partialTimingPhases: timingStatus.partialPhases,
transferSize: Number.isFinite(Number(entry?.transferSize)) ? Number(entry.transferSize) : null,
encodedBodySize: Number.isFinite(Number(entry?.encodedBodySize)) ? Number(entry.encodedBodySize) : null,
decodedBodySize: Number.isFinite(Number(entry?.decodedBodySize)) ? Number(entry.decodedBodySize) : null,
nextHopProtocol: entry?.nextHopProtocol ?? null,
serverTiming,
serverTimingNames: serverTiming.map((item) => item.name).filter(Boolean).slice(0, 8),
otelTraceId: extractOtelTraceIdFromServerTiming(serverTiming),
valuesRedacted: true
};
}
function phaseDeltaMs(entry, endKey, startKey) {
const end = Number(entry?.[endKey]);
const start = Number(entry?.[startKey]);
if (!Number.isFinite(end) || !Number.isFinite(start) || end <= 0 || start <= 0 || end < start) return null;
return Math.round(end - start);
}
function resourceTimingPhaseStatus(entry) {
const pairs = [
["requestToResponseStart", "requestStart", "responseStart"],
["responseTransfer", "responseStart", "responseEnd"],
];
const invalidPhases = [];
const partialPhases = [];
for (const [label, startKey, endKey] of pairs) {
const start = Number(entry?.[startKey]);
const end = Number(entry?.[endKey]);
if (!Number.isFinite(start) || !Number.isFinite(end) || start <= 0 || end <= 0) {
partialPhases.push(label);
} else if (end < start) {
invalidPhases.push(label);
}
}
return {
status: invalidPhases.length > 0 ? "invalid" : (partialPhases.length > 0 ? "partial" : "complete"),
invalidPhases,
partialPhases,
};
}
function compactServerTiming(value) {
const items = Array.isArray(value) ? value : [];
return items.slice(0, 8).map((item) => ({
name: truncate(String(item?.name || ""), 80),
duration: Number.isFinite(Number(item?.duration)) ? Math.round(Number(item.duration)) : null,
description: truncate(String(item?.description || ""), 120),
})).filter((item) => item.name || item.description || item.duration !== null);
}
function extractOtelTraceIdFromServerTiming(items) {
const text = (Array.isArray(items) ? items : []).map((item) => [item.name, item.description].filter(Boolean).join(" ")).join(" ");
const match = text.match(/\b[0-9a-f]{32}\b/iu);
return match ? match[0].toLowerCase() : null;
}
function roundFinite(value) {
const numeric = Number(value);
return Number.isFinite(numeric) ? Math.round(numeric) : null;
}
function classifyApiPerformanceRoute(normalizedPath, entry = {}) {
if (normalizedPath === "/v1/workbench/events") return "same-origin-api-stream";
if (String(entry?.initiatorType ?? "").toLowerCase() === "eventsource") return "same-origin-api-stream";
return "same-origin-api";
}
function streamOpenLatencyMs(entry = {}) {
const responseStart = Number(entry?.responseStart);
const startTime = Number(entry?.startTime);
if (!Number.isFinite(responseStart) || responseStart <= 0) return null;
if (!Number.isFinite(startTime) || startTime < 0) return Math.max(0, responseStart);
if (responseStart < startTime) return null;
return Math.max(0, responseStart - startTime);
}
function parsePerformanceUrl(value, base) {
try {
const url = new URL(String(value || ""), base);
const origin = new URL(String(base || "http://invalid.local")).origin;
return { sameOrigin: url.origin === origin, path: url.pathname };
} catch {
return { sameOrigin: false, path: "-" };
}
}
function isApiLikePath(path) {
return /^\/(?:v1(?:\/|$)|auth(?:\/|$)|health(?:\/|$))/u.test(String(path || ""));
}
function normalizeApiPath(path) {
return String(path || "-")
.replace(/\/v1\/workbench\/sessions\/ses_[^/]+/gu, "/v1/workbench/sessions/:id")
.replace(/\/v1\/workbench\/turns\/trc_[^/]+/gu, "/v1/workbench/turns/:traceId")
.replace(/\/v1\/workbench\/traces\/trc_[^/]+/gu, "/v1/workbench/traces/:traceId")
.replace(/\/v1\/workbench\/sessions\/[0-9a-f-]{12,}/giu, "/v1/workbench/sessions/:id")
.replace(/\/v1\/[^/]+\/[0-9a-f-]{16,}(?=\/|$)/giu, (match) => match.replace(/\/[0-9a-f-]{16,}$/iu, "/:id"));
}
function percentile(sortedValues, percentileValue) {
if (!Array.isArray(sortedValues) || sortedValues.length === 0) return null;
if (sortedValues.length === 1) return Math.round(sortedValues[0]);
const rank = (percentileValue / 100) * (sortedValues.length - 1);
const lower = Math.floor(rank);
const upper = Math.ceil(rank);
if (lower === upper) return Math.round(sortedValues[lower]);
const weight = rank - lower;
return Math.round(sortedValues[lower] * (1 - weight) + sortedValues[upper] * weight);
}
function buildPromptNetworkReport(control, network) {
const promptsById = new Map();
for (const item of control) {
if (item?.type !== "sendPrompt" || !item.commandId) continue;
const existing = promptsById.get(item.commandId) || {
commandId: item.commandId,
promptIndex: promptsById.size + 1,
promptTextHash: item.input?.textHash ?? null,
promptTextBytes: item.input?.textBytes ?? null,
startedAt: null,
completedAt: null,
failedAt: null,
phase: null
};
if (!existing.promptTextHash && item.input?.textHash) existing.promptTextHash = item.input.textHash;
if (!existing.promptTextBytes && item.input?.textBytes) existing.promptTextBytes = item.input.textBytes;
if (item.phase === "started") existing.startedAt = item.ts ?? existing.startedAt;
if (item.phase === "completed") existing.completedAt = item.ts ?? existing.completedAt;
if (item.phase === "failed") existing.failedAt = item.ts ?? existing.failedAt;
existing.phase = item.phase ?? existing.phase;
promptsById.set(item.commandId, existing);
}
const prompts = Array.from(promptsById.values()).sort((a, b) => Date.parse(a.startedAt || a.completedAt || a.failedAt || "") - Date.parse(b.startedAt || b.completedAt || b.failedAt || ""));
prompts.forEach((item, index) => { item.promptIndex = index + 1; });
const chatEvents = network
.filter((item) => String(item?.method || "").toUpperCase() === "POST" && promptSubmitModeForUrl(item?.url) !== null)
.map((item) => {
const failureText = item.failureKind ?? item.failure ?? item.errorText ?? null;
const urlPathValue = urlPath(item.url);
return {
ts: item.ts ?? null,
tsMs: Date.parse(item.ts),
type: item.type ?? null,
status: Number.isFinite(Number(item.status)) ? Number(item.status) : null,
commandId: item.commandId ?? null,
urlPath: urlPathValue,
submitMode: promptSubmitModeForUrl(item.url),
failureKind: failureText ? String(failureText) : null,
errorTextHash: failureText ? sha256(failureText) : null
};
})
.filter((item) => Number.isFinite(item.tsMs))
.sort((a, b) => a.tsMs - b.tsMs);
const rounds = prompts.map((prompt) => {
const startMs = Date.parse(prompt.startedAt || prompt.completedAt || prompt.failedAt || "");
const endAnchorMs = Date.parse(prompt.completedAt || prompt.failedAt || prompt.startedAt || "");
const fromMs = Number.isFinite(startMs) ? startMs - 3000 : Number.NEGATIVE_INFINITY;
const toMs = Number.isFinite(endAnchorMs) ? endAnchorMs + 30000 : Number.POSITIVE_INFINITY;
const events = chatEvents.filter((event) => {
if (event.commandId && prompt.commandId && event.commandId === prompt.commandId) return true;
return event.tsMs >= fromMs && event.tsMs <= toMs;
});
const responses = events.filter((event) => event.type === "response");
const failures = events.filter((event) => event.type === "requestfailed");
const responseStatuses = responses.map((event) => event.status).filter((status) => status !== null);
const submitModes = Array.from(new Set(events.map((event) => event.submitMode).filter(Boolean))).sort();
const chatPostOk = responseStatuses.some((status) => status >= 200 && status < 300);
const failureKind = chatPostOk
? null
: failures.length > 0
? "requestfailed"
: responseStatuses.length === 0
? "missing-response"
: "http-status";
return {
promptIndex: prompt.promptIndex,
promptCommandId: prompt.commandId,
promptTextHash: prompt.promptTextHash,
promptTextBytes: prompt.promptTextBytes,
startedAt: prompt.startedAt,
completedAt: prompt.completedAt,
failedAt: prompt.failedAt,
chatPostOk,
failureKind,
requestCount: events.filter((event) => event.type === "request").length,
responseCount: responses.length,
requestFailedCount: failures.length,
responseStatuses,
submitModes,
steerUsed: submitModes.includes("steer"),
firstChatEventAt: events[0]?.ts ?? null,
lastChatEventAt: events[events.length - 1]?.ts ?? null,
events: events.slice(0, 12).map((event) => ({ ts: event.ts, type: event.type, status: event.status, urlPath: event.urlPath, submitMode: event.submitMode, failureKind: event.failureKind, errorTextHash: event.errorTextHash }))
};
});
return {
summary: {
promptCount: rounds.length,
chatPostOk: rounds.filter((item) => item.chatPostOk === true).length,
chatPostFailed: rounds.filter((item) => item.chatPostOk === false).length,
chatPostMissing: rounds.filter((item) => item.failureKind === "missing-response").length
},
rounds
};
}
function promptSubmitModeForUrl(value) {
const pathValue = urlPath(value);
if (pathValue === "/v1/agent/chat") return "chat";
if (pathValue === "/v1/agent/chat/steer") return "steer";
return null;
}
function parseDomDiagnosticSummary(text) {
const value = String(text || "");
const traceMatch = value.match(/\b(?:trace_id=)?(trc_[A-Za-z0-9_-]+|[a-f0-9]{16,64})\b/iu);
const httpStatusMatch = value.match(/\bHTTP\s+([1-5][0-9]{2})\b/iu);
const idleMatch = value.match(/\bidle\s+(\d+)s\b/iu);
const waitingForMatch = value.match(/\bwaitingFor=([^\s;,)]+)/iu);
const lastEventLabelMatch = value.match(/\blastEventLabel=([^\s;,)]+)/iu);
const diagnosticCode = httpStatusMatch
? "http-" + httpStatusMatch[1]
: /turn\s*超过|无新活动/iu.test(value)
? "turn-idle-no-activity"
: /Failed to fetch/iu.test(value)
? "failed-to-fetch"
: "diagnostic";
return {
diagnosticCode,
traceId: traceMatch?.[1] || null,
httpStatus: httpStatusMatch ? Number(httpStatusMatch[1]) : null,
idleSeconds: idleMatch ? Number(idleMatch[1]) : null,
waitingFor: waitingForMatch?.[1] || null,
lastEventLabel: lastEventLabelMatch?.[1] || null
};
}
function isDomDiagnosticSampleText(text) {
const value = String(text || "").replace(/\s+/g, " ").trim();
if (!value) return false;
const strongDiagnostic = [
/\bHTTP\s+[45][0-9]{2}\b(?:[\s\S]{0,120}\btrace_id=|\b)/iu,
/\btrace_id=(?:trc_[A-Za-z0-9_-]+|[a-f0-9]{16,64})\b/iu,
/workbench\s+turn\s*超过\s*\d+ms\s*无新活动/iu,
/\bturn\s*超过\b[\s\S]{0,120}\b无新活动\b/iu,
/\bprojection-resume:sync-failed\b/iu,
/\bAgentRun\s+GET\b[\s\S]*\/result\b[\s\S]*timed out after\s+\d+ms\b/iu,
/\bFailed to fetch\b/iu,
/\bFailed to load resource\b[\s\S]{0,180}\bstatus of\s+[45][0-9]{2}\b/iu,
/\bserver responded with a status of\s+[45][0-9]{2}\b/iu,
/Code Agent\b[\s\S]{0,120}(?:无法连接上游|请求已结束)/iu
].some((pattern) => pattern.test(value));
if (!strongDiagnostic) return false;
const looksLikeToolStdout = /\b(?:stdout|stderr):/iu.test(value)
&& /(?:\becho\s+["']?===|\bnode\s+|\.tspy\b|tspy\/|===\s*[A-Za-z0-9_.-]+\s*===)/iu.test(value);
if (!looksLikeToolStdout) return true;
return /\b(?:trace_id=|HTTP\s+[45][0-9]{2}|workbench\s+turn\s*超过|projection-resume:sync-failed|Failed to fetch|Failed to load resource|server responded with a status of\s+[45][0-9]{2}|Code Agent\b[\s\S]{0,120}(?:无法连接上游|请求已结束))\b/iu.test(value);
}
function buildRuntimeAlerts(samples, control, network, consoleEvents, errors) {
const promptTimes = control
.filter((item) => item.type === "sendPrompt" && item.phase === "completed")
.map((item) => Date.parse(item.ts))
.filter(Number.isFinite)
.sort((a, b) => a - b);
const observerRefreshTimes = control
.filter((item) => item.type === "observer-periodic-refresh")
.map((item) => Date.parse(item.ts))
.filter(Number.isFinite)
.sort((a, b) => a - b);
const naturalNetwork = network.filter((item) => item?.observerInitiated !== true);
const httpErrors = naturalNetwork
.filter((item) => item?.type === "response" && Number(item.status) >= 400)
.map((item) => networkAlertEvent(item, promptTimes));
const requestFailed = naturalNetwork
.filter((item) => item?.type === "requestfailed")
.map((item) => networkAlertEvent(item, promptTimes));
const significantRequestFailed = requestFailed.filter(
(item) => !isBenignLongLivedStreamClosureAlert(item) && !isObserverRefreshClosureAlert(item, observerRefreshTimes),
);
const domDiagnostics = [];
const executionErrors = [];
const baselineExecutionErrors = [];
const canarySessionIds = sessionInvariantCanarySessionIds(control);
const firstPromptMs = promptTimes.length > 0 ? promptTimes[0] : Infinity;
const firstSeenExecutionErrorMs = new Map();
for (const sample of samples) {
const tsMs = Date.parse(sample?.ts);
const promptIndex = Number.isFinite(tsMs) ? latestPromptIndex(promptTimes, tsMs) : 0;
if (Array.isArray(sample?.diagnostics)) {
for (const diagnostic of sample.diagnostics.slice(0, 12)) {
const text = diagnostic?.textPreview || diagnostic?.text || "";
if (!String(text).trim()) continue;
const parsedDiagnostic = parseDomDiagnosticSummary(text);
domDiagnostics.push({
seq: sample.seq ?? null,
ts: sample.ts ?? null,
promptIndex,
source: "diagnostic-node",
className: diagnostic.className ?? null,
diagnosticCode: diagnostic.diagnosticCode ?? parsedDiagnostic.diagnosticCode,
traceId: diagnostic.traceId ?? parsedDiagnostic.traceId,
httpStatus: diagnostic.httpStatus ?? parsedDiagnostic.httpStatus,
idleSeconds: diagnostic.idleSeconds ?? parsedDiagnostic.idleSeconds,
waitingFor: diagnostic.waitingFor ?? parsedDiagnostic.waitingFor,
lastEventLabel: diagnostic.lastEventLabel ?? parsedDiagnostic.lastEventLabel,
compact: diagnostic.compact ?? null,
expanded: diagnostic.expanded ?? null,
routeSessionId: sample.routeSessionId ?? null,
activeSessionId: sample.activeSessionId ?? null,
textHash: diagnostic.textHash || sha256(text),
preview: limitText(text, 260)
});
}
}
const texts = sampleTexts(sample).filter(isDomDiagnosticSampleText);
for (const text of texts.slice(0, 4)) {
const parsedDiagnostic = parseDomDiagnosticSummary(text);
domDiagnostics.push({
seq: sample.seq ?? null,
ts: sample.ts ?? null,
promptIndex,
source: "sample-text",
diagnosticCode: parsedDiagnostic.diagnosticCode,
traceId: parsedDiagnostic.traceId,
httpStatus: parsedDiagnostic.httpStatus,
idleSeconds: parsedDiagnostic.idleSeconds,
waitingFor: parsedDiagnostic.waitingFor,
lastEventLabel: parsedDiagnostic.lastEventLabel,
routeSessionId: sample.routeSessionId ?? null,
activeSessionId: sample.activeSessionId ?? null,
textHash: sha256(text),
preview: limitText(text, 220)
});
}
const seenExecutionErrors = new Set();
for (const candidate of sampleExecutionErrorCandidates(sample)) {
const parsed = parseExecutionErrorText(candidate.text);
if (!parsed) continue;
const textHash = sha256(candidate.text);
const dedupeKey = [candidate.source, candidate.traceId || "-", parsed.backend || "-", parsed.code || "-", parsed.status || "-", textHash].join("|");
if (seenExecutionErrors.has(dedupeKey)) continue;
seenExecutionErrors.add(dedupeKey);
const firstSeenMs = firstSeenExecutionErrorMs.has(dedupeKey) ? firstSeenExecutionErrorMs.get(dedupeKey) : tsMs;
if (!firstSeenExecutionErrorMs.has(dedupeKey) && Number.isFinite(tsMs)) firstSeenExecutionErrorMs.set(dedupeKey, tsMs);
const baseline = Number.isFinite(firstSeenMs) && firstSeenMs < firstPromptMs;
const event = {
seq: sample.seq ?? null,
ts: sample.ts ?? null,
promptIndex,
baseline,
firstSeenAt: Number.isFinite(firstSeenMs) ? new Date(firstSeenMs).toISOString() : null,
source: candidate.source,
backend: parsed.backend,
status: parsed.status,
code: parsed.code,
rawCode: parsed.rawCode,
totalSeconds: parsed.totalSeconds,
traceId: candidate.traceId || parsed.traceId || null,
messageId: candidate.messageId || null,
routeSessionId: sample.routeSessionId ?? null,
activeSessionId: sample.activeSessionId ?? null,
textHash,
preview: limitText(candidate.text, 260)
};
const eventSessions = [event.routeSessionId, event.activeSessionId].filter(Boolean);
const nonCanarySession = canarySessionIds.size > 0 && !eventSessions.some((sessionId) => canarySessionIds.has(sessionId));
if (baseline || nonCanarySession) baselineExecutionErrors.push({ ...event, baseline: true, baselineReason: nonCanarySession ? "non-canary-session" : "pre-first-prompt" });
else executionErrors.push(event);
domDiagnostics.push({
seq: sample.seq ?? null,
ts: sample.ts ?? null,
promptIndex,
source: "execution-row",
diagnosticCode: parsed.rawCode || parsed.code || "execution-error",
traceId: candidate.traceId || parsed.traceId || null,
routeSessionId: sample.routeSessionId ?? null,
activeSessionId: sample.activeSessionId ?? null,
textHash,
preview: limitText(candidate.text, 220)
});
}
}
const consoleAlerts = consoleEvents
.filter((item) => /error|warning|warn|assert/iu.test(String(item?.type || "")) || isDiagnosticText(item?.text))
.map((item) => consoleAlertEvent(item, promptTimes));
const significantConsoleAlerts = consoleAlerts.filter((item) => !isBenignLongLivedStreamClosureAlert(item) && !isObserverRefreshClosureAlert(item, observerRefreshTimes));
const pageErrors = errors.map((item) => ({
ts: item.ts ?? null,
promptIndex: promptIndexForTs(promptTimes, item.ts),
type: item.type ?? null,
pageRole: item.pageRole ?? item.error?.details?.pageRole ?? null,
pageId: item.pageId ?? item.error?.details?.pageId ?? null,
routeSessionId: item.routeSessionId ?? item.error?.details?.routeSessionId ?? null,
activeSessionId: item.activeSessionId ?? item.error?.details?.activeSessionId ?? null,
commandId: item.commandId ?? item.error?.details?.commandId ?? null,
sampleSeq: item.sampleSeq ?? item.error?.details?.sampleSeq ?? null,
timeoutMs: timeoutMsFromMessage(item.error?.message || item.message || item.error || ""),
errorName: item.error?.name ?? item.name ?? null,
messageHash: item.error?.message ? sha256(item.error.message) : item.message ? sha256(item.message) : null,
preview: limitText(item.error?.message || item.message || item.error || "", 220)
}));
return {
summary: {
httpErrorCount: httpErrors.length,
requestFailedCount: requestFailed.length,
significantRequestFailedCount: significantRequestFailed.length,
benignLongLivedStreamClosureCount: requestFailed.length - significantRequestFailed.length,
domDiagnosticSampleCount: domDiagnostics.length,
domDiagnosticGroupCount: groupDomDiagnostics(domDiagnostics).length,
executionErrorCount: executionErrors.length,
baselineExecutionErrorCount: baselineExecutionErrors.length,
consoleAlertCount: consoleAlerts.length,
significantConsoleAlertCount: significantConsoleAlerts.length,
pageErrorCount: pageErrors.length,
networkErrorGroupCount: groupNetworkAlerts(httpErrors).length,
requestFailedGroupCount: groupNetworkAlerts(requestFailed).length,
significantRequestFailedGroupCount: groupNetworkAlerts(significantRequestFailed).length,
executionErrorGroupCount: groupExecutionErrors(executionErrors).length,
baselineExecutionErrorGroupCount: groupExecutionErrors(baselineExecutionErrors).length,
consoleAlertGroupCount: groupConsoleAlerts(consoleAlerts).length,
significantConsoleAlertGroupCount: groupConsoleAlerts(significantConsoleAlerts).length
},
networkHttpErrorsByPath: groupNetworkAlerts(httpErrors),
networkRequestFailedByPath: groupNetworkAlerts(requestFailed),
networkSignificantRequestFailedByPath: groupNetworkAlerts(significantRequestFailed),
domDiagnostics: domDiagnostics.slice(-80),
domDiagnosticsByText: groupDomDiagnostics(domDiagnostics),
domDiagnosticsByFingerprint: groupDomDiagnostics(domDiagnostics).slice(0, 80),
runtimeExecutionErrors: executionErrors.slice(0, 120),
runtimeExecutionErrorsByCode: groupExecutionErrors(executionErrors),
baselineRuntimeExecutionErrors: baselineExecutionErrors.slice(0, 80),
baselineRuntimeExecutionErrorsByCode: groupExecutionErrors(baselineExecutionErrors),
consoleAlerts: consoleAlerts.slice(0, 80),
consoleAlertsByPath: groupConsoleAlerts(consoleAlerts),
significantConsoleAlerts: significantConsoleAlerts.slice(0, 80),
significantConsoleAlertsByPath: groupConsoleAlerts(significantConsoleAlerts),
pageErrors: pageErrors.slice(0, 40)
};
}
function groupDomDiagnostics(events) {
const groups = new Map();
for (const item of events || []) {
const preview = String(item?.preview || "").trim();
if (!isReportableDomDiagnostic(item, preview)) continue;
const normalizedPreview = normalizeDiagnosticPreview(preview);
const key = [
item?.diagnosticCode || "",
normalizedPreview
].join("|");
const existing = groups.get(key) || {
source: item?.source || null,
sources: new Set(),
diagnosticCode: item?.diagnosticCode || null,
textHash: item?.textHash || null,
normalizedPreview,
preview,
count: 0,
firstAt: item?.ts || null,
lastAt: item?.ts || null,
promptIndexes: new Set(),
traceIds: new Set(),
sampleSeqs: []
};
if (item?.source) existing.sources.add(String(item.source));
existing.count += 1;
existing.firstAt = minIso(existing.firstAt, item?.ts || null);
existing.lastAt = maxIso(existing.lastAt, item?.ts || null);
if (Number.isFinite(Number(item?.promptIndex))) existing.promptIndexes.add(Number(item.promptIndex));
for (const traceId of extractDiagnosticTraceIds(item, preview)) existing.traceIds.add(traceId);
if (existing.sampleSeqs.length < 12 && item?.seq !== undefined && item?.seq !== null) existing.sampleSeqs.push(item.seq);
groups.set(key, existing);
}
return Array.from(groups.values())
.map((item) => ({
source: item.source,
sources: Array.from(item.sources).sort(),
diagnosticCode: item.diagnosticCode,
textHash: item.textHash,
normalizedPreview: item.normalizedPreview,
preview: item.preview,
count: item.count,
firstAt: item.firstAt,
lastAt: item.lastAt,
promptIndexes: Array.from(item.promptIndexes).sort((a, b) => a - b),
traceIds: Array.from(item.traceIds).sort(),
sampleSeqs: item.sampleSeqs
}))
.sort((a, b) => (b.count - a.count) || String(a.firstAt || "").localeCompare(String(b.firstAt || "")));
}
function isReportableDomDiagnostic(item, preview) {
if (item?.source === "diagnostic-node" || item?.source === "execution-row") return true;
return /trace_id=|HTTP\s+\d{3}\b|Failed to load resource|ERR_[A-Z_]+|provider-unavailable|AgentRun error|超过\s*\d+\s*ms\s*无新活动|代理暂时无法连接上游|Trace 更新超时|加载失败/iu.test(String(preview || ""));
}
function normalizeDiagnosticPreview(text) {
return String(text || "")
.replace(/trace_id=[A-Za-z0-9_-]+/gu, "trace_id=:traceId")
.replace(/\btrc_[A-Za-z0-9_-]+\b/gu, "trc_:traceId")
.replace(/\bses_[A-Za-z0-9_-]+\b/gu, "ses_:sessionId")
.replace(/\brun_[A-Za-z0-9_-]+\b/gu, "run_:runId")
.replace(/\bcmd_[A-Za-z0-9_-]+\b/gu, "cmd_:commandId")
.replace(/[!]+$/gu, "")
.replace(/\s+/gu, " ")
.trim();
}
function extractDiagnosticTraceIds(item, preview) {
const ids = new Set();
if (item?.traceId) ids.add(String(item.traceId));
const text = String(preview || "");
for (const match of text.matchAll(/\btrc_[A-Za-z0-9_-]+\b/gu)) ids.add(match[0]);
for (const match of text.matchAll(/trace_id=([A-Za-z0-9_-]+)/gu)) ids.add(match[1]);
return ids;
}
function minIso(a, b) {
if (!a) return b || null;
if (!b) return a || null;
return Date.parse(a) <= Date.parse(b) ? a : b;
}
function maxIso(a, b) {
if (!a) return b || null;
if (!b) return a || null;
return Date.parse(a) >= Date.parse(b) ? a : b;
}
function sampleExecutionErrorCandidates(sample) {
const candidates = [];
const add = (source, items) => {
if (!Array.isArray(items)) return;
for (const item of items) {
const text = String(item?.textPreview || item?.text || item?.preview || "").trim();
if (!text) continue;
if (!parseExecutionErrorText(text)) continue;
candidates.push({
source,
text,
traceId: item?.traceId ?? null,
messageId: item?.messageId ?? null,
status: item?.status ?? null
});
}
};
add("diagnostic-node", sample?.diagnostics);
add("message", sample?.messages);
add("trace-row", sample?.traceRows);
add("turn", sample?.turns);
const specific = candidates.filter((candidate) => {
const parsed = parseExecutionErrorText(candidate.text);
return parsed && parsed.code !== "error";
});
return specific.length > 0 ? specific : candidates;
}
function parseExecutionErrorText(text) {
const value = String(text || "");
const agentRunCodeMatch = value.match(/\bagentrun:error:([A-Za-z0-9_.:-]+)/u);
const agentRunText = /\bAgentRun\s+error\b|\bagentrun:error:/iu.test(value);
const providerUnavailable = /\bprovider[-_\s]*unavailable\b/iu.test(value);
if (!agentRunCodeMatch && !agentRunText && !providerUnavailable) return null;
const statusMatch = value.match(/\b(fail(?:ed)?|error|blocked|cancel(?:ed)?)\b/iu);
const traceMatch = value.match(/\btrc_[A-Za-z0-9_-]+\b/u);
const totalMatch = value.match(/\btotal\s*=\s*([0-9]{1,2}:[0-9]{2}(?::[0-9]{2})?)\b/iu)
|| value.match(/总耗时\s*[:]?\s*([0-9]{1,2}:[0-9]{2}(?::[0-9]{2})?)/iu);
const agentRunCode = cleanExecutionCode(agentRunCodeMatch?.[1] || "");
const rawCode = agentRunCode ? "agentrun:error:" + agentRunCode : providerUnavailable ? "provider-unavailable" : "agentrun:error";
return {
backend: agentRunText || agentRunCodeMatch ? "agentrun" : "unknown",
status: normalizeExecutionStatus(statusMatch?.[1] || "error"),
code: agentRunCode || (providerUnavailable ? "provider-unavailable" : "error"),
rawCode,
totalSeconds: totalMatch ? parseClockDurationSeconds(totalMatch[1]) : null,
traceId: traceMatch?.[0] || null
};
}
function cleanExecutionCode(code) {
const value = String(code || "").replace(/(?:AgentRun|Error|Failed).*$/u, "").replace(/[^A-Za-z0-9_.:-].*$/u, "");
return value || null;
}
function normalizeExecutionStatus(status) {
const value = String(status || "").toLowerCase();
if (value === "failed") return "fail";
if (value === "cancelled" || value === "canceled") return "cancel";
return value || "error";
}
function parseClockDurationSeconds(value) {
const parts = String(value || "").split(":").map((part) => Number(part));
if (parts.length === 2 && parts.every(Number.isFinite)) return parts[0] * 60 + parts[1];
if (parts.length === 3 && parts.every(Number.isFinite)) return parts[0] * 3600 + parts[1] * 60 + parts[2];
return null;
}
function groupExecutionErrors(events) {
const groups = new Map();
for (const event of events) {
const key = [event.backend || "-", event.status || "-", event.code || "-"].join(" ");
const group = groups.get(key) || {
backend: event.backend ?? null,
status: event.status ?? null,
code: event.code ?? null,
rawCode: event.rawCode ?? null,
count: 0,
firstAt: event.ts,
lastAt: event.ts,
promptIndexes: [],
traceIds: [],
sources: []
};
group.count += 1;
group.lastAt = event.ts;
if (event.promptIndex && !group.promptIndexes.includes(event.promptIndex)) group.promptIndexes.push(event.promptIndex);
if (event.traceId && !group.traceIds.includes(event.traceId)) group.traceIds.push(event.traceId);
if (event.source && !group.sources.includes(event.source)) group.sources.push(event.source);
groups.set(key, group);
}
return Array.from(groups.values()).sort((a, b) => b.count - a.count || String(a.code).localeCompare(String(b.code)));
}
function consoleAlertEvent(item, promptTimes) {
const text = String(item?.text || "");
const statusMatch = text.match(/\bstatus\s+of\s+([1-5][0-9]{2})\b/iu) || text.match(/\bHTTP\s+([1-5][0-9]{2})\b/iu);
const location = compactLocation(item.location);
const traceMatch = (location?.urlPath || text).match(/\btrc_[A-Za-z0-9_-]+\b/u);
return {
ts: item.ts ?? null,
promptIndex: promptIndexForTs(promptTimes, item.ts),
type: item.type ?? null,
status: statusMatch ? Number(statusMatch[1]) : null,
urlPath: location?.urlPath || "-",
traceId: traceMatch?.[0] || null,
textHash: item.text ? sha256(item.text) : null,
preview: limitText(text, 220),
location
};
}
function groupConsoleAlerts(events) {
const groups = new Map();
for (const event of events) {
const key = [event.type || "-", event.status ?? "-", event.urlPath || "-"].join(" ");
const group = groups.get(key) || {
type: event.type ?? null,
status: event.status ?? null,
urlPath: event.urlPath || "-",
count: 0,
firstAt: event.ts,
lastAt: event.ts,
promptIndexes: [],
traceIds: []
};
group.count += 1;
group.lastAt = event.ts;
if (event.promptIndex && !group.promptIndexes.includes(event.promptIndex)) group.promptIndexes.push(event.promptIndex);
if (event.traceId && !group.traceIds.includes(event.traceId)) group.traceIds.push(event.traceId);
groups.set(key, group);
}
return Array.from(groups.values()).sort((a, b) => b.count - a.count || String(a.urlPath).localeCompare(String(b.urlPath)));
}
function isBenignLongLivedStreamClosureAlert(event) {
if (event?.urlPath !== "/v1/workbench/events") return false;
if (event.status !== null && event.status !== undefined && Number(event.status) > 0) return false;
const text = String(event.failureKind || event.errorPreview || event.preview || "");
return /ERR_NETWORK_CHANGED|ERR_ABORTED|net::ERR_NETWORK_CHANGED|net::ERR_ABORTED|aborted|network changed/iu.test(text);
}
function isObserverRefreshClosureAlert(event, observerRefreshTimes) {
const urlPath = String(event?.urlPath || "");
if (!["/v1/workbench/events", "/v1/web-performance"].includes(urlPath) && !/^\/v1\/workbench\/traces\/[^/]+\/events$/u.test(urlPath)) return false;
if (event.status !== null && event.status !== undefined && Number(event.status) > 0) return false;
const text = String(event.failureKind || event.errorPreview || event.preview || "");
if (!/ERR_NETWORK_CHANGED|ERR_ABORTED|ERR_INCOMPLETE_CHUNKED_ENCODING|ERR_INVALID_CHUNKED_ENCODING|net::ERR_NETWORK_CHANGED|net::ERR_ABORTED|aborted|network changed|incomplete chunked|invalid chunked/iu.test(text)) return false;
const ts = Date.parse(String(event.ts || ""));
return Number.isFinite(ts) && observerRefreshTimes.some((refreshTs) => Math.abs(ts - refreshTs) <= 8000);
}
function networkAlertEvent(item, promptTimes) {
const failureText = item.failureKind ?? item.failure ?? item.errorText ?? null;
return {
ts: item.ts ?? null,
promptIndex: promptIndexForTs(promptTimes, item.ts),
method: String(item.method || "GET").toUpperCase(),
status: Number.isFinite(Number(item.status)) ? Number(item.status) : null,
type: item.type ?? null,
urlPath: urlPath(item.url),
urlHash: item.url ? sha256(item.url) : null,
failureKind: failureText ? String(failureText) : null,
errorTextHash: failureText ? sha256(failureText) : null,
errorPreview: failureText ? limitText(failureText, 160) : null
};
}
function groupNetworkAlerts(events) {
const groups = new Map();
for (const event of events) {
const key = [event.method, event.urlPath, event.status ?? "-", event.type].join(" ");
const group = groups.get(key) || {
method: event.method,
urlPath: event.urlPath,
status: event.status,
type: event.type,
count: 0,
firstAt: event.ts,
lastAt: event.ts,
promptIndexes: [],
failureKinds: [],
errorTextHashes: []
};
group.count += 1;
group.lastAt = event.ts;
if (event.promptIndex && !group.promptIndexes.includes(event.promptIndex)) group.promptIndexes.push(event.promptIndex);
if (event.failureKind && !group.failureKinds.includes(event.failureKind)) group.failureKinds.push(event.failureKind);
if (event.errorTextHash && !group.errorTextHashes.includes(event.errorTextHash)) group.errorTextHashes.push(event.errorTextHash);
groups.set(key, group);
}
return Array.from(groups.values()).sort((a, b) => b.count - a.count || String(a.urlPath).localeCompare(String(b.urlPath)));
}
function isDiagnosticText(text) {
const value = String(text || "");
return /Failed to (?:fetch|load resource)|request failed|net::ERR_[A-Z0-9_:-]+|server responded with a status of [45][0-9]{2}|HTTP\s+[45][0-9]{2}\b|trace_id=|workbench turn\s*超过|turn\s*超过|无新活动|idle\s+\d+s|waitingFor=|lastEventLabel=|无法连接上游|代理暂时无法连接上游|provider-unavailable|agentrun:error|AgentRun error|projection-resume|sync-failed|durable projection store|realtime-gap|Trace 更新超时|加载失败|请求失败|请求已失败/iu.test(value);
}
function prioritizeFindings(findings) {
const items = Array.isArray(findings) ? findings : [];
const severityRank = (severity) => {
const value = String(severity || "").toLowerCase();
if (value === "red") return 0;
if (value === "amber" || value === "warning") return 1;
if (value === "info") return 3;
return 2;
};
const kindRank = (item) => {
const id = String(item?.id ?? item?.kind ?? item?.code ?? "");
if (id.startsWith("project-management-") || id.startsWith("mdtodo-") || id === "workbench-launch-button-unavailable") return 0;
if (id === "page-performance-slow-same-origin-api") return 0;
if (id === "session-rail-title-fallback-majority") return 0.5;
if (id.startsWith("workbench-terminal-")) return 0.6;
if (id.startsWith("code-agent-card-")) return 0.8;
if (id.startsWith("round-completion-")) return 0.9;
if (id.startsWith("turn-timing-total-elapsed")) return 1;
if (id.startsWith("turn-timing-terminal-elapsed")) return 1.1;
if (id.startsWith("turn-timing-recent-update")) return 2;
if (id.includes("runtime-execution") || id.includes("prompt-chat-submit-failed")) return 3;
return 10;
};
return items.slice().sort((left, right) => {
const kindDelta = kindRank(left) - kindRank(right);
if (kindDelta !== 0) return kindDelta;
return severityRank(left?.severity ?? left?.level) - severityRank(right?.severity ?? right?.level);
});
}
function isTerminalTraceText(text) {
return /轮次完成|轮次失败|轮次取消|已记录|已完成第\d+轮|final response|sealed final response|turn completed|turn failed|turn canceled|terminal result|\bcompleted\b|\bfailed\b|\bcanceled\b|\bcancelled\b|\bterminal\b|\bdone\b/iu.test(String(text || ""));
}
function isFinalResultText(text) {
return /已完成第\d+轮|已按第\d+轮完成|final response|sealed final response|最终结果|已完成[:]|smoke\s*测试结果|benchmark|PVC\/workspace|修改文件|Results:/iu.test(String(text || ""));
}
function buildSampleMetrics(samples, control) {
const promptCommands = buildSendPromptCommandTimeline(control);
const promptTimes = promptCommands.map((item) => item.tsMs);
const timeline = samples.map((sample) => {
const texts = sampleTexts(sample);
const timingTexts = sampleTurnTimingTexts(sample);
const tsMs = Date.parse(sample.ts);
const promptIndex = Number.isFinite(tsMs) ? latestPromptIndex(promptTimes, tsMs) : 0;
const totalElapsedValues = timingTexts.flatMap(parseTotalElapsedSeconds).filter(Number.isFinite);
const recentUpdateValues = timingTexts.flatMap(parseRecentUpdateSeconds).filter(Number.isFinite);
const diagnosticTexts = texts.filter(isDiagnosticText).slice(0, 5);
const terminalTexts = texts.filter(isTerminalTraceText).slice(0, 5);
const finalResultTexts = texts.filter(isFinalResultText).slice(0, 5);
const loadings = Array.isArray(sample.loadings) ? sample.loadings : [];
const loadingOwners = uniqueLoadingOwners(loadings);
return {
seq: sample.seq ?? null,
ts: sample.ts ?? null,
routeSessionId: sample.routeSessionId ?? null,
activeSessionId: sample.activeSessionId ?? null,
promptIndex,
messageCount: Array.isArray(sample.messages) ? sample.messages.length : 0,
traceRowCount: Array.isArray(sample.traceRows) ? sample.traceRows.length : 0,
loadingCount: loadings.length,
loadingOwnerCount: loadingOwners.length,
loadingOwners: loadingOwners.map((item) => ({ ownerKey: item.ownerKey, ownerKind: item.ownerKind, ownerLabel: item.ownerLabel, count: item.count })).slice(0, 12),
sessionRailVisibleCount: Number(sample?.sessionRail?.visibleCount ?? 0),
sessionRailFallbackTitleCount: Number(sample?.sessionRail?.fallbackTitleCount ?? 0),
sessionRailFallbackTitleRatio: Number(sample?.sessionRail?.fallbackTitleRatio ?? 0),
totalElapsedSeconds: totalElapsedValues.length > 0 ? Math.max(...totalElapsedValues) : null,
recentUpdateSeconds: recentUpdateValues.length > 0 ? Math.max(...recentUpdateValues) : null,
terminalSeen: terminalTexts.length > 0,
finalResultTextSeen: finalResultTexts.length > 0,
diagnosticSeen: diagnosticTexts.length > 0,
diagnosticTextHashes: diagnosticTexts.map(sha256).slice(0, 5),
textDigest: digestSample(sample)
};
});
const turnTiming = buildTurnTimingTable(samples, timeline);
const traceOrder = buildTraceOrderMetrics(samples, timeline);
const codeAgentCardTiming = buildCodeAgentCardTimingMetrics(samples, timeline, turnTiming);
const codeAgentCardDurationUnderreported = buildCodeAgentCardDurationUnderreportedMetrics(samples, timeline);
const codeAgentCardDurationMismatches = buildCodeAgentCardDurationMismatchMetrics(samples, timeline);
if (codeAgentCardTiming && codeAgentCardTiming.summary) {
codeAgentCardTiming.summary.durationUnderreportedCount = codeAgentCardDurationUnderreported.length;
codeAgentCardTiming.summary.durationMismatchCount = codeAgentCardDurationMismatches.length;
codeAgentCardTiming.durationUnderreported = codeAgentCardDurationUnderreported;
codeAgentCardTiming.durationMismatches = codeAgentCardDurationMismatches;
}
const turnCells = turnTiming.rows.flatMap((row) => Object.values(row.cells || {}));
const turnTimingNonMonotonic = Array.isArray(turnTiming.nonMonotonic) ? turnTiming.nonMonotonic : [];
const turnTimingElapsedZeroResets = Array.isArray(turnTiming.elapsedZeroResets) ? turnTiming.elapsedZeroResets : [];
const turnTimingTotalElapsedForwardJumps = Array.isArray(turnTiming.totalElapsedForwardJumps) ? turnTiming.totalElapsedForwardJumps : [];
const turnTimingRecentUpdateSawtoothJumps = turnTimingNonMonotonic.filter((item) => item.metric === "recentUpdateSeconds" && item.anomaly === "jump");
const turnTimingTerminalElapsedGrowth = Array.isArray(turnTiming.terminalElapsedGrowth) ? turnTiming.terminalElapsedGrowth : [];
const turnTimingRecentUpdateResets = Array.isArray(turnTiming.recentUpdateResets) ? turnTiming.recentUpdateResets : [];
const turnTimingRecentUpdateSteps = Array.isArray(turnTiming.recentUpdateSteps) ? turnTiming.recentUpdateSteps : [];
const turnTimingTerminalElapsedGrowthDeltas = turnTimingTerminalElapsedGrowth
.map((item) => Number(item.delta))
.filter((value) => Number.isFinite(value) && value > 0);
const turnTimingRecentUpdateLargestSteps = turnTimingRecentUpdateSteps
.filter((item) => Number.isFinite(Number(item.delta)))
.slice()
.sort((a, b) => Number(b.delta) - Number(a.delta))
.slice(0, 200);
const turnTimingRecentUpdatePositiveSteps = turnTimingRecentUpdateSteps
.map((item) => Number(item.delta))
.filter((value) => Number.isFinite(value) && value >= 0);
const turnTimingRecentUpdateExcessSteps = turnTimingRecentUpdateSteps
.map((item) => Number(item.excessiveIncreaseSeconds))
.filter((value) => Number.isFinite(value) && value > 0);
const withTotal = timeline.filter((item) => item.totalElapsedSeconds !== null).length;
const withRecent = timeline.filter((item) => item.recentUpdateSeconds !== null).length;
const diagnostics = timeline.filter((item) => item.diagnosticSeen).length;
const loading = buildLoadingMetrics(samples, timeline);
const sessionRailTitles = buildSessionRailTitleMetrics(samples, timeline);
const reportTurnTimingRows = boundedTurnTimingRowsForReport(turnTiming.rows);
const reportTimeline = boundedRowsForReport(timeline);
const rounds = buildRoundMetricSummaries(timeline, promptCommands, {
columns: turnTiming.columns,
rows: turnTiming.rows,
nonMonotonic: turnTimingNonMonotonic,
elapsedZeroResets: turnTimingElapsedZeroResets,
totalElapsedForwardJumps: turnTimingTotalElapsedForwardJumps,
terminalElapsedGrowth: turnTimingTerminalElapsedGrowth,
recentUpdateResets: turnTimingRecentUpdateResets,
recentUpdateSteps: turnTimingRecentUpdateSteps
});
const recentUpdateJumpCount = turnTimingRecentUpdateSawtoothJumps.length;
return {
summary: {
sampleCount: timeline.length,
withTotalElapsed: withTotal,
withRecentUpdate: withRecent,
diagnostics,
loadingSampleCount: loading.summary.loadingSampleCount,
loadingMaxCount: loading.summary.maxSimultaneousCount,
loadingMaxOwnerCount: loading.summary.maxSimultaneousOwnerCount,
loadingOwnerCount: loading.summary.ownerCount,
loadingConcurrentSampleCount: loading.summary.concurrentLoadingSampleCount,
loadingLongestContinuousSeconds: loading.summary.longestContinuousSeconds,
loadingCurrentContinuousSeconds: loading.summary.currentContinuousSeconds,
loadingOverFiveSecondSegmentCount: loading.summary.overFiveSecondSegmentCount,
loadingOverBudgetSegmentCount: loading.summary.overBudgetSegmentCount,
sessionRailSampleCount: sessionRailTitles.summary.sampleCount,
sessionRailVisibleSampleCount: sessionRailTitles.summary.visibleSampleCount,
sessionRailFallbackMajoritySampleCount: sessionRailTitles.summary.majorityFallbackSampleCount,
sessionRailFallbackMaxRatio: sessionRailTitles.summary.maxFallbackRatio,
sessionRailFallbackMaxVisibleCount: sessionRailTitles.summary.maxVisibleCount,
sessionRailFallbackMaxCount: sessionRailTitles.summary.maxFallbackTitleCount,
promptSegments: Math.max(0, promptTimes.length),
rounds: rounds.length,
turnColumns: turnTiming.columns.length,
turnTimingRows: turnTiming.rows.length,
turnCellsWithTotalElapsed: turnCells.filter((item) => item.totalElapsedSeconds !== null).length,
turnCellsWithRecentUpdate: turnCells.filter((item) => item.recentUpdateSeconds !== null).length,
turnTimingNonMonotonicCount: turnTimingNonMonotonic.length,
turnTimingTotalElapsedDecreaseCount: turnTimingNonMonotonic.filter((item) => item.metric === "totalElapsedSeconds").length,
turnTimingTotalElapsedZeroResetCount: turnTimingElapsedZeroResets.length,
turnTimingTotalElapsedForwardJumpCount: turnTimingTotalElapsedForwardJumps.length,
turnTimingTotalElapsedForwardJumpMaxSeconds: maxPositiveDelta(turnTimingTotalElapsedForwardJumps),
turnTimingTerminalElapsedGrowthCount: turnTimingTerminalElapsedGrowth.length,
turnTimingTerminalElapsedGrowthMaxSeconds: turnTimingTerminalElapsedGrowthDeltas.length > 0 ? Math.max(...turnTimingTerminalElapsedGrowthDeltas) : 0,
turnTimingRecentUpdateJumpCount: recentUpdateJumpCount,
turnTimingRecentUpdateSawtoothJumpCount: recentUpdateJumpCount,
turnTimingRecentUpdateStepCount: turnTimingRecentUpdateSteps.length,
turnTimingRecentUpdateMaxIncreaseSeconds: turnTimingRecentUpdatePositiveSteps.length > 0 ? Math.max(...turnTimingRecentUpdatePositiveSteps) : null,
turnTimingRecentUpdateMaxExcessSeconds: turnTimingRecentUpdateExcessSteps.length > 0 ? Math.max(...turnTimingRecentUpdateExcessSteps) : 0,
turnTimingRecentUpdateResetCount: turnTimingRecentUpdateResets.length,
turnTimingRecentUpdateDecreaseCount: turnTimingRecentUpdateResets.length,
codeAgentCardSampleCount: codeAgentCardTiming.summary.cardSampleCount,
codeAgentCardMissingElapsedCount: codeAgentCardTiming.summary.missingElapsedCount,
codeAgentCardMissingRecentUpdateCount: codeAgentCardTiming.summary.missingRecentUpdateCount,
roundCompletionEventCount: codeAgentCardTiming.summary.roundCompletionEventCount,
roundCompletionElapsedMismatchCount: codeAgentCardTiming.summary.roundCompletionElapsedMismatchCount,
roundCompletionFinalResponseMissingCount: codeAgentCardTiming.summary.roundCompletionFinalResponseMissingCount,
roundCompletionPostTimingChangeCount: codeAgentCardTiming.summary.roundCompletionPostTimingChangeCount,
codeAgentCardDurationUnderreportedCount: codeAgentCardTiming.summary.durationUnderreportedCount,
codeAgentCardDurationMismatchCount: codeAgentCardTiming.summary.durationMismatchCount,
traceRowCount: traceOrder.summary.traceRowCount,
traceRowOrderAnomalyCount: traceOrder.summary.orderAnomalyCount,
traceRowCompletionNotLastCount: traceOrder.summary.completionNotLastCount,
roundsWithTurnTimingNonMonotonic: rounds.filter((item) => item.turnTimingNonMonotonicCount > 0).length,
roundsWithTurnTimingTotalElapsedForwardJumps: rounds.filter((item) => item.turnTimingTotalElapsedForwardJumpCount > 0).length,
roundsWithTerminalElapsedGrowth: rounds.filter((item) => item.turnTimingTerminalElapsedGrowthCount > 0).length,
roundsWithRecentUpdateJumps: rounds.filter((item) => item.turnTimingRecentUpdateJumpCount > 0).length
},
loading,
sessionRailTitles,
codeAgentCardTiming,
traceOrder,
rounds,
turnColumns: turnTiming.columns,
turnTimingTable: reportTurnTimingRows.rows,
turnTimingTableDisclosure: reportTurnTimingRows.disclosure,
turnTimingNonMonotonic,
turnTimingElapsedZeroResets,
turnTimingTotalElapsedForwardJumps,
turnTimingTerminalElapsedGrowth,
turnTimingRecentUpdateSawtoothJumps,
turnTimingRecentUpdateSteps,
turnTimingRecentUpdateLargestSteps,
turnTimingRecentUpdateResets,
timeline: reportTimeline.rows,
timelineDisclosure: reportTimeline.disclosure
};
}
function boundedRowsForReport(rows) {
const sourceRows = Array.isArray(rows) ? rows : [];
const maxRows = 1200;
const headRows = 120;
if (sourceRows.length <= maxRows) return { rows: sourceRows, disclosure: { truncated: false, totalRows: sourceRows.length, includedRows: sourceRows.length, omittedRows: 0, headRows: sourceRows.length, tailRows: 0 } };
const tailRows = Math.max(0, maxRows - headRows);
return {
rows: [...sourceRows.slice(0, headRows), ...sourceRows.slice(-tailRows)],
disclosure: {
truncated: true,
totalRows: sourceRows.length,
includedRows: maxRows,
omittedRows: Math.max(0, sourceRows.length - maxRows),
headRows,
tailRows,
policy: "report-bounded-head-tail; summary metrics are computed before truncation",
valuesRedacted: true
}
};
}
function buildSendPromptCommandTimeline(control) {
const byCommand = new Map();
let ordinal = 0;
for (const item of Array.isArray(control) ? control : []) {
if (item?.type !== "sendPrompt") continue;
const commandId = item.commandId ? String(item.commandId) : "sendPrompt-" + String(ordinal++);
const existing = byCommand.get(commandId) || {
commandId,
startedAt: null,
startedTsMs: null,
completedAt: null,
completedTsMs: null,
failedAt: null,
failedTsMs: null,
textHash: null,
textBytes: null,
};
if (!existing.textHash && item.input?.textHash) existing.textHash = item.input.textHash;
if (!existing.textBytes && item.input?.textBytes) existing.textBytes = item.input.textBytes;
const tsMs = Date.parse(item.ts);
if (item.phase === "started" && Number.isFinite(tsMs)) {
existing.startedAt = item.ts ?? existing.startedAt;
existing.startedTsMs = tsMs;
} else if (item.phase === "completed" && Number.isFinite(tsMs)) {
existing.completedAt = item.ts ?? existing.completedAt;
existing.completedTsMs = tsMs;
} else if (item.phase === "failed" && Number.isFinite(tsMs)) {
existing.failedAt = item.ts ?? existing.failedAt;
existing.failedTsMs = tsMs;
}
byCommand.set(commandId, existing);
}
return Array.from(byCommand.values())
.map((item) => {
const tsMs = Number.isFinite(item.startedTsMs) ? item.startedTsMs : Number.isFinite(item.completedTsMs) ? item.completedTsMs : item.failedTsMs;
const ts = Number.isFinite(item.startedTsMs) ? item.startedAt : Number.isFinite(item.completedTsMs) ? item.completedAt : item.failedAt;
return { ...item, ts, tsMs };
})
.filter((item) => Number.isFinite(item.tsMs))
.sort((a, b) => a.tsMs - b.tsMs)
.map((item, index) => ({ ...item, promptIndex: index + 1 }));
}
function buildSessionRailTitleMetrics(samples, timeline) {
const rows = [];
const examplesByHash = new Map();
for (let index = 0; index < (Array.isArray(samples) ? samples : []).length; index += 1) {
const sample = samples[index];
const rail = sample?.sessionRail && typeof sample.sessionRail === "object" ? sample.sessionRail : null;
if (!rail) continue;
const visibleCount = Number(rail.visibleCount ?? 0);
const fallbackTitleCount = Number(rail.fallbackTitleCount ?? 0);
const safeVisibleCount = Number.isFinite(visibleCount) && visibleCount > 0 ? visibleCount : 0;
const safeFallbackTitleCount = Number.isFinite(fallbackTitleCount) && fallbackTitleCount > 0 ? fallbackTitleCount : 0;
const fallbackTitleRatio = safeVisibleCount > 0 ? Number((safeFallbackTitleCount / safeVisibleCount).toFixed(4)) : 0;
const fallbackItems = Array.isArray(rail.fallbackItems) ? rail.fallbackItems : [];
for (const item of fallbackItems) {
const hash = String(item?.titleHash || item?.titlePreview || item?.sessionIdPrefix || "").trim();
if (!hash || examplesByHash.has(hash)) continue;
examplesByHash.set(hash, {
titleHash: item?.titleHash ?? null,
titlePreview: limitText(String(item?.titlePreview || ""), 160),
sessionIdPrefix: item?.sessionIdPrefix ?? null,
active: item?.active === true,
firstSeq: sample?.seq ?? null,
firstAt: sample?.ts ?? null,
pageRole: sample?.pageRole ?? null,
});
}
rows.push({
...ref(sample),
promptIndex: timeline[index]?.promptIndex ?? 0,
visibleCount: safeVisibleCount,
fallbackTitleCount: safeFallbackTitleCount,
fallbackTitleRatio,
majorityFallback: safeVisibleCount > 0 && safeFallbackTitleCount > safeVisibleCount / 2,
overThreshold: safeVisibleCount > 0 && fallbackTitleRatio > alertThresholds.sessionRailFallbackRatio,
examples: fallbackItems.slice(0, 5).map((item) => ({
titleHash: item?.titleHash ?? null,
titlePreview: limitText(String(item?.titlePreview || ""), 160),
sessionIdPrefix: item?.sessionIdPrefix ?? null,
active: item?.active === true,
})),
});
}
const visibleRows = rows.filter((item) => item.visibleCount > 0);
const majorityRows = rows.filter((item) => item.majorityFallback);
const overThresholdRows = rows.filter((item) => item.overThreshold);
const fallbackRows = rows.filter((item) => item.fallbackTitleCount > 0);
const maxFallbackRatio = rows.length > 0 ? Math.max(...rows.map((item) => Number(item.fallbackTitleRatio) || 0)) : 0;
const maxVisibleCount = rows.length > 0 ? Math.max(...rows.map((item) => Number(item.visibleCount) || 0)) : 0;
const maxFallbackTitleCount = rows.length > 0 ? Math.max(...rows.map((item) => Number(item.fallbackTitleCount) || 0)) : 0;
return {
summary: {
sampleCount: rows.length,
visibleSampleCount: visibleRows.length,
fallbackSampleCount: fallbackRows.length,
majorityFallbackSampleCount: majorityRows.length,
overThresholdSampleCount: overThresholdRows.length,
thresholdRatio: alertThresholds.sessionRailFallbackRatio,
maxFallbackRatio,
maxVisibleCount,
maxFallbackTitleCount,
},
samples: majorityRows.slice(0, 80),
examples: Array.from(examplesByHash.values()).slice(0, 80),
timeline: rows.slice(-200),
valuesRedacted: true
};
}
function uniqueLoadingOwners(loadings) {
const groups = new Map();
for (let index = 0; index < (Array.isArray(loadings) ? loadings : []).length; index += 1) {
const item = loadings[index];
const ownerKey = loadingOwnerKey(item, index);
const ownerIdentity = loadingOwnerIdentity(item);
const existing = groups.get(ownerKey) || {
ownerKey,
ownerKind: item?.ownerKind ?? "unknown",
ownerLabel: loadingOwnerLabel(item, ownerKey),
...ownerIdentity,
count: 0,
textHashes: []
};
existing.count += 1;
if (item?.textHash && !existing.textHashes.includes(item.textHash)) existing.textHashes.push(item.textHash);
for (const key of ["ownerSessionId", "ownerMessageId", "ownerTraceId"]) {
if (!existing[key] && ownerIdentity[key]) existing[key] = ownerIdentity[key];
}
groups.set(ownerKey, existing);
}
return Array.from(groups.values()).sort((a, b) => b.count - a.count || String(a.ownerLabel).localeCompare(String(b.ownerLabel)));
}
function loadingOwnerIdentity(item) {
const owner = item?.owner && typeof item.owner === "object" ? item.owner : {};
return {
ownerSessionId: owner.sessionId ?? null,
ownerMessageId: owner.messageId ?? null,
ownerTraceId: owner.traceId ?? null,
};
}
function loadingOwnerKey(item, index = 0) {
const key = String(item?.ownerKey || "").trim();
if (key) return key.slice(0, 240);
const owner = item?.owner && typeof item.owner === "object" ? item.owner : {};
return [
item?.ownerKind || "unknown",
owner.testId || item?.testId || owner.id || owner.role || owner.className || item?.role || item?.tag || "node",
owner.sessionId || owner.messageId || owner.traceId || item?.textHash || String(index)
].filter(Boolean).join(":").slice(0, 240);
}
function loadingOwnerLabel(item, fallback) {
return limitText(String(item?.ownerLabel || item?.owner?.ariaLabel || item?.owner?.testId || item?.owner?.className || fallback || "unknown"), 160);
}
function buildLoadingMetrics(samples, timeline) {
const events = samples.map((sample, index) => {
const tsMs = Date.parse(sample?.ts);
const loadings = Array.isArray(sample?.loadings) ? sample.loadings : [];
const owners = uniqueLoadingOwners(loadings);
return {
seq: sample?.seq ?? null,
ts: sample?.ts ?? null,
tsMs,
promptIndex: timeline[index]?.promptIndex ?? 0,
routeSessionId: sample?.routeSessionId ?? null,
activeSessionId: sample?.activeSessionId ?? null,
loadingCount: loadings.length,
ownerCount: owners.length,
owners,
ownerKeys: owners.map((item) => item.ownerKey),
ownerLabels: owners.map((item) => item.ownerLabel).slice(0, 8)
};
}).filter((item) => Number.isFinite(item.tsMs));
const continuityThresholdMs = loadingContinuityThresholdMs(events);
const segments = buildLoadingSegments(events, continuityThresholdMs, (event) => event.loadingCount, (event) => event.owners)
.sort((a, b) => Number(b.durationSeconds ?? 0) - Number(a.durationSeconds ?? 0) || Number(b.maxCount ?? 0) - Number(a.maxCount ?? 0));
const ownerMap = new Map();
for (const event of events) {
for (const owner of event.owners) {
const existing = ownerMap.get(owner.ownerKey) || {
ownerKey: owner.ownerKey,
ownerKind: owner.ownerKind,
ownerLabel: owner.ownerLabel,
ownerSessionId: owner.ownerSessionId ?? null,
ownerMessageId: owner.ownerMessageId ?? null,
ownerTraceId: owner.ownerTraceId ?? null,
sampleCount: 0,
occurrenceCount: 0,
maxSimultaneousCount: 0,
firstAt: event.ts,
lastAt: event.ts,
firstSeq: event.seq,
lastSeq: event.seq,
promptIndexes: new Set(),
events: []
};
existing.sampleCount += 1;
existing.occurrenceCount += owner.count;
existing.maxSimultaneousCount = Math.max(existing.maxSimultaneousCount, owner.count);
existing.lastAt = event.ts;
existing.lastSeq = event.seq;
for (const key of ["ownerSessionId", "ownerMessageId", "ownerTraceId"]) {
if (!existing[key] && owner[key]) existing[key] = owner[key];
}
if (Number.isFinite(Number(event.promptIndex))) existing.promptIndexes.add(Number(event.promptIndex));
existing.events.push({ ...event, loadingCount: owner.count, owners: [owner] });
ownerMap.set(owner.ownerKey, existing);
}
}
const owners = Array.from(ownerMap.values()).map((owner) => {
const ownerSegments = buildLoadingSegments(owner.events, continuityThresholdMs, (event) => event.loadingCount, (event) => event.owners);
const longest = ownerSegments.reduce((max, item) => Math.max(max, Number(item.durationSeconds ?? 0)), 0);
return {
ownerKey: owner.ownerKey,
ownerKind: owner.ownerKind,
ownerLabel: owner.ownerLabel,
ownerSessionId: owner.ownerSessionId ?? null,
ownerMessageId: owner.ownerMessageId ?? null,
ownerTraceId: owner.ownerTraceId ?? null,
sampleCount: owner.sampleCount,
occurrenceCount: owner.occurrenceCount,
maxSimultaneousCount: owner.maxSimultaneousCount,
longestContinuousSeconds: longest,
firstAt: owner.firstAt,
lastAt: owner.lastAt,
firstSeq: owner.firstSeq,
lastSeq: owner.lastSeq,
promptIndexes: Array.from(owner.promptIndexes).sort((a, b) => a - b),
segments: ownerSegments.sort((a, b) => Number(b.durationSeconds ?? 0) - Number(a.durationSeconds ?? 0)).slice(0, 8),
valuesRedacted: true
};
}).sort((a, b) => Number(b.longestContinuousSeconds ?? 0) - Number(a.longestContinuousSeconds ?? 0) || Number(b.occurrenceCount ?? 0) - Number(a.occurrenceCount ?? 0));
const latest = events[events.length - 1] || null;
const currentSegment = latest && latest.loadingCount > 0
? segments.find((segment) => segment.ongoing === true && segment.lastSeq === latest.seq) || null
: null;
const timelineRows = events
.filter((event, index) => event.loadingCount > 0 || (index > 0 && events[index - 1]?.loadingCount > 0))
.slice(0, 500)
.map((event) => ({
seq: event.seq,
ts: event.ts,
promptIndex: event.promptIndex,
loadingCount: event.loadingCount,
ownerCount: event.ownerCount,
owners: event.owners.map((owner) => ({ ownerKind: owner.ownerKind, ownerLabel: owner.ownerLabel, ownerSessionId: owner.ownerSessionId ?? null, ownerMessageId: owner.ownerMessageId ?? null, ownerTraceId: owner.ownerTraceId ?? null, count: owner.count })).slice(0, 8)
}));
return {
summary: {
sampleCount: events.length,
loadingSampleCount: events.filter((event) => event.loadingCount > 0).length,
maxSimultaneousCount: events.reduce((max, event) => Math.max(max, event.loadingCount), 0),
maxSimultaneousOwnerCount: events.reduce((max, event) => Math.max(max, event.ownerCount), 0),
concurrentLoadingSampleCount: events.filter((event) => event.loadingCount > 1).length,
ownerCount: owners.length,
segmentCount: segments.length,
overFiveSecondSegmentCount: segments.filter((segment) => Number(segment.durationSeconds ?? 0) > 5).length,
overBudgetSegmentCount: segments.filter((segment) => Number(segment.durationSeconds ?? 0) > alertThresholds.visibleLoadingSlowMs / 1000).length,
budgetSeconds: alertThresholds.visibleLoadingSlowMs / 1000,
longestContinuousSeconds: segments.length > 0 ? Number(segments[0].durationSeconds ?? 0) : 0,
currentContinuousSeconds: currentSegment ? Number(currentSegment.durationSeconds ?? 0) : 0,
continuityThresholdMs,
latestLoadingCount: latest?.loadingCount ?? 0,
latestOwnerCount: latest?.ownerCount ?? 0,
valuesRedacted: true
},
segments: segments.slice(0, 80),
owners: owners.slice(0, 80),
timeline: timelineRows,
valuesRedacted: true
};
}
function loadingContinuityThresholdMs(events) {
const deltas = [];
for (let index = 1; index < events.length; index += 1) {
const delta = events[index].tsMs - events[index - 1].tsMs;
if (Number.isFinite(delta) && delta > 0) deltas.push(delta);
}
if (deltas.length === 0) return 5000;
const sorted = deltas.slice().sort((a, b) => a - b);
const median = sorted[Math.floor(sorted.length / 2)];
return Math.min(15000, Math.max(1500, Math.round(median * 2.5)));
}
function buildLoadingSegments(events, continuityThresholdMs, countForEvent, ownersForEvent) {
const segments = [];
let segment = null;
let previousTsMs = null;
for (const event of events) {
const count = Number(countForEvent(event) ?? 0);
const gapOk = previousTsMs === null || !Number.isFinite(event.tsMs) || event.tsMs - previousTsMs <= continuityThresholdMs;
if (count > 0) {
if (!segment || !gapOk) {
if (segment) segments.push(finalizeLoadingSegment(segment, null));
segment = {
firstAt: event.ts,
lastAt: event.ts,
firstSeq: event.seq,
lastSeq: event.seq,
promptIndexes: new Set(),
ownerKeys: new Set(),
ownerLabels: new Map(),
sampleCount: 0,
maxCount: 0,
ongoing: true
};
}
segment.lastAt = event.ts;
segment.lastSeq = event.seq;
segment.sampleCount += 1;
segment.maxCount = Math.max(segment.maxCount, count);
if (Number.isFinite(Number(event.promptIndex))) segment.promptIndexes.add(Number(event.promptIndex));
for (const owner of ownersForEvent(event) || []) {
if (!owner?.ownerKey) continue;
segment.ownerKeys.add(owner.ownerKey);
if (!segment.ownerLabels.has(owner.ownerKey)) segment.ownerLabels.set(owner.ownerKey, { ownerKey: owner.ownerKey, ownerKind: owner.ownerKind, ownerLabel: owner.ownerLabel, count: 0 });
const label = segment.ownerLabels.get(owner.ownerKey);
label.count += owner.count ?? 1;
}
} else if (segment) {
segment.ongoing = false;
segment.endedAt = event.ts;
segment.endSeq = event.seq;
segments.push(finalizeLoadingSegment(segment, event));
segment = null;
}
previousTsMs = event.tsMs;
}
if (segment) segments.push(finalizeLoadingSegment(segment, null));
return segments;
}
function finalizeLoadingSegment(segment, absentEvent) {
const startMs = Date.parse(segment.firstAt || "");
const lastMs = Date.parse(segment.lastAt || "");
const absentMs = Date.parse(absentEvent?.ts || "");
const durationSeconds = Number.isFinite(startMs) && Number.isFinite(lastMs) && lastMs >= startMs ? Number(((lastMs - startMs) / 1000).toFixed(3)) : 0;
const upperBoundSeconds = Number.isFinite(startMs) && Number.isFinite(absentMs) && absentMs >= startMs ? Number(((absentMs - startMs) / 1000).toFixed(3)) : durationSeconds;
const endedGapSeconds = Number.isFinite(lastMs) && Number.isFinite(absentMs) && absentMs >= lastMs ? Number(((absentMs - lastMs) / 1000).toFixed(3)) : null;
return {
firstAt: segment.firstAt,
lastAt: segment.lastAt,
endedAt: absentEvent?.ts ?? null,
firstSeq: segment.firstSeq,
lastSeq: segment.lastSeq,
endSeq: absentEvent?.seq ?? null,
durationSeconds,
upperBoundSeconds,
endedGapSeconds,
sampleCount: segment.sampleCount,
maxCount: segment.maxCount,
ownerCount: segment.ownerKeys.size,
owners: Array.from(segment.ownerLabels.values()).sort((a, b) => b.count - a.count || String(a.ownerLabel).localeCompare(String(b.ownerLabel))).slice(0, 12),
promptIndexes: Array.from(segment.promptIndexes).sort((a, b) => a - b),
ongoing: absentEvent ? false : segment.ongoing === true,
valuesRedacted: true
};
}
function boundedTurnTimingRowsForReport(rows) {
const sourceRows = Array.isArray(rows) ? rows : [];
const maxRows = 1200;
const headRows = 120;
if (sourceRows.length <= maxRows) return { rows: sourceRows, disclosure: { truncated: false, totalRows: sourceRows.length, includedRows: sourceRows.length, omittedRows: 0, headRows: sourceRows.length, tailRows: 0 } };
const tailRows = Math.max(0, maxRows - headRows);
return {
rows: [...sourceRows.slice(0, headRows), ...sourceRows.slice(-tailRows)],
disclosure: {
truncated: true,
totalRows: sourceRows.length,
includedRows: maxRows,
omittedRows: Math.max(0, sourceRows.length - maxRows),
headRows,
tailRows,
policy: "report-bounded-head-tail; full anomaly counters are computed before truncation",
valuesRedacted: true
}
};
}
` + nodeWebObserveAnalyzerTimingSource();
}