Files
pikasTech-unidesk/scripts/src/hwlab-node-web-observe-analyzer-source.ts
T
Codex 01ad090ec9 fix(web-probe): preserve taskBody/report fields in compactProjectManagementSample
The analyzer compact function dropped taskBodyVisible, taskBody,
reportLinkCount, reportPreviewVisible, reportPreview and
reportFullscreenVisible from project-management samples, causing
false mdtodo-task-body-not-visible and mdtodo-report-preview-missing
red findings even when the latest sample had taskBodyVisible=true.

Closes the analyzer/runner field mismatch surfaced by HWLAB#2216验收.
2026-06-26 19:51:43 +00:00

3349 lines
189 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 projectManagement = buildProjectManagementReport(samples, control, network, pagePerformance, projectManagementConfig);
const runnerErrors = errors.slice(-8).map((item) => {
const attempts = Array.isArray(item.error?.attempts) ? item.error.attempts : [];
const lastAttempt = attempts.length > 0 ? attempts[attempts.length - 1] : null;
const readiness = lastAttempt?.readiness || 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,
sessionCreateVisible: readinessSnapshot.sessionCreateVisible === true,
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)];
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,
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,
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,
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,
})),
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,
})),
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,
})),
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, 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,
})),
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,
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,
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"),
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,
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),
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) : [],
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 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
}));
}
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 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,
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,
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),
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
});
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) : [],
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 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.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.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";
findings.push({
id: findingId,
severity,
summary: "controlled refresh/switch-back afterRound=" + (afterRound ?? "-") + " left consecutive user message cards without interleaved assistant/code-agent terminal cards" + (sentinelRange ? " (" + sentinelRange + ")" : ""),
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),
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 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);
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 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 buildFindings(samples, control, network, errors, sampleMetrics, promptNetwork, runtimeAlerts, pagePerformance, pageProvenance, commandFailures = [], manifest = {}) {
const findings = [];
if (commandFailures.length > 0) findings.push({ id: "observer-command-failed", severity: "red", summary: "observer control commands failed; analyze must surface command failure instead of hiding it in command artifacts", count: commandFailures.length, commands: commandFailures.slice(0, 20) });
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",
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",
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", 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 promptFailures = Array.isArray(promptNetwork?.rounds) ? promptNetwork.rounds.filter((item) => item.chatPostOk === false) : [];
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", 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", 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", 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", 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", 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-over-threshold", severity: "red", summary: "visible session list rows exceeded configured YAML fallback-title ratio", count: sessionRailTitleSummary.overThresholdSampleCount ?? sessionRailTitleSummary.majorityFallbackSampleCount, thresholdRatio: alertThresholds.sessionRailFallbackRatio, maxFallbackRatio: sessionRailTitleSummary.maxFallbackRatio, maxFallbackTitleCount: sessionRailTitleSummary.maxFallbackTitleCount, samples: sampleMetrics.sessionRailTitles.samples.slice(0, 20), examples: sampleMetrics.sessionRailTitles.examples.slice(0, 20) });
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 timedCrossPageProjectionDiffs = annotateCrossPageDiffTiming(crossPageProjectionDiffs);
const controlledNavigationHydrationProjectionDiffs = timedCrossPageProjectionDiffs.filter((item) => controlledNavigationHydrationCrossPageDiff(item, controlledNavigationWindows, sampleBySeq));
const evaluatedCrossPageProjectionDiffs = timedCrossPageProjectionDiffs.filter((item) => !controlledNavigationHydrationCrossPageDiff(item, controlledNavigationWindows, 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 (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 || "")));
findings.push({ id: "natural-api-dom-lag-baseline", severity: "info", summary: "natural API responses and DOM samples are available for manual lag correlation", naturalApiResponses: naturalApi.length, sampleCount: samples.length });
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 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 findings = buildFindings(windowSamples, control, windowNetwork, windowErrors, sampleMetrics, promptNetwork, runtimeAlerts, pagePerformance, pageProvenance);
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,
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,
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("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();
}