Files
pikasTech-unidesk/scripts/src/hwlab-node-web-observe-runner-source.ts
T
2026-06-23 21:15:37 +08:00

6460 lines
355 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 长程观测 draft-2026-06-20-p0-passive-web-probe-observer.
// Responsibility: Source strings for the pure-client HWLAB web-probe observer and offline analyzer.
export function nodeWebObserveRunnerSource(): string {
return String.raw`#!/usr/bin/env node
import { createHash, randomBytes } from "node:crypto";
import { appendFile, mkdir, readFile, readdir, rename, stat, unlink, writeFile } from "node:fs/promises";
import path from "node:path";
import { pathToFileURL } from "node:url";
const specRef = "PJ2026-01040111 长程观测 draft-2026-06-20-p0-passive-web-probe-observer";
const startedAtMs = Date.now();
const startedAt = new Date(startedAtMs).toISOString();
const baseUrl = normalizeBaseUrl(process.env.HWLAB_WEB_BASE_URL);
const username = process.env.HWLAB_WEB_USER || "admin";
const password = process.env.HWLAB_WEB_PASS || "";
const stateDir = path.resolve(process.env.UNIDESK_WEB_OBSERVE_STATE_DIR || ".state/web-observe/manual");
const jobId = safeId(process.env.UNIDESK_WEB_OBSERVE_JOB_ID || "webobs-" + Date.now().toString(36) + "-" + randomBytes(3).toString("hex"));
const targetPath = process.env.UNIDESK_WEB_OBSERVE_TARGET_PATH || "/workbench";
const sampleIntervalMs = positiveInteger(process.env.UNIDESK_WEB_OBSERVE_SAMPLE_INTERVAL_MS, 5000);
const screenshotIntervalMs = positiveInteger(process.env.UNIDESK_WEB_OBSERVE_SCREENSHOT_INTERVAL_MS, 300000);
const maxSamples = positiveInteger(process.env.UNIDESK_WEB_OBSERVE_MAX_SAMPLES, 0);
const observerRefreshIntervalMs = positiveInteger(process.env.UNIDESK_WEB_OBSERVE_OBSERVER_REFRESH_INTERVAL_MS, 180000);
const viewport = parseViewport(process.env.UNIDESK_WEB_OBSERVE_VIEWPORT || "1440x900");
const browserProxyMode = parseBrowserProxyMode(process.env.UNIDESK_WEB_OBSERVE_BROWSER_PROXY_MODE || "auto");
const alertThresholds = parseAlertThresholds(process.env.UNIDESK_WEB_OBSERVE_ALERT_THRESHOLDS_JSON);
const playwrightProxy = proxyConfigFromEnv(baseUrl);
const chromiumLaunchOptions = chromiumLaunchOptionsForProxy(playwrightProxy);
const pageId = "control-" + randomBytes(4).toString("hex");
const observerPageId = "observer-" + randomBytes(4).toString("hex");
const dirs = {
commandsPending: path.join(stateDir, "commands", "pending"),
commandsProcessing: path.join(stateDir, "commands", "processing"),
commandsDone: path.join(stateDir, "commands", "done"),
commandsFailed: path.join(stateDir, "commands", "failed"),
screenshots: path.join(stateDir, "screenshots"),
analysis: path.join(stateDir, "analysis"),
archive: path.join(stateDir, "archive"),
};
const files = {
manifest: path.join(stateDir, "manifest.json"),
heartbeat: path.join(stateDir, "heartbeat.json"),
control: path.join(stateDir, "control.jsonl"),
samples: path.join(stateDir, "samples.jsonl"),
network: path.join(stateDir, "network.jsonl"),
console: path.join(stateDir, "console.jsonl"),
errors: path.join(stateDir, "errors.jsonl"),
artifacts: path.join(stateDir, "artifacts.jsonl"),
};
let browser;
let context;
let page;
let observerPage;
let lastObserverRefreshAtMs = Date.now();
let sampleSeq = 0;
let commandSeq = 0;
let artifactSeq = 0;
let activeCommandId = null;
let stopping = false;
let terminalStatus = "starting";
let lastScreenshotAtMs = 0;
let auth = null;
let pageLoadSeq = 0;
let currentPageProvenance = null;
const jsonlRotation = { stamp: compactFileTimestamp(startedAt), files: [] };
try {
if (!password) throw new Error("missing HWLAB_WEB_PASS");
await prepareDirs();
await rotateExistingJsonlArtifacts();
await writeManifest({ status: "starting" });
await writeHeartbeat({ status: "starting" });
if (jsonlRotation.files.length > 0) await appendJsonl(files.control, eventRecord("jsonl-rotated", { stamp: jsonlRotation.stamp, archiveDir: path.relative(stateDir, dirs.archive), files: jsonlRotation.files, valuesRedacted: true }));
const launcher = await import(pathToFileURL(path.resolve("scripts/src/browser-launcher.mjs")).href);
const { chromium } = await launcher.importPlaywright();
browser = await launcher.launchChromium(chromium, chromiumLaunchOptions);
context = await browser.newContext({ viewport, ...(playwrightProxy === null ? {} : { proxy: playwrightProxy }) });
page = await context.newPage();
attachPassiveListeners(page, "control", pageId);
auth = await runControlCommand({ id: "startup-login", type: "login", createdAt: startedAt, source: "startup" }, async () => authenticate(context, page));
await runControlCommand({ id: "startup-goto", type: "goto", path: targetPath, createdAt: new Date().toISOString(), source: "startup" }, async () => gotoTarget(targetPath));
observerPage = await context.newPage();
attachPassiveListeners(observerPage, "observer", observerPageId);
await runControlCommand({ id: "startup-observer-goto", type: "observerGoto", path: targetPath, createdAt: new Date().toISOString(), source: "startup" }, async () => {
const result = await syncObserverPageToControlSession("startup");
if (!result?.ok) {
await appendJsonl(files.control, eventRecord("observer-startup-degraded", {
reason: result?.failureKind || result?.reason || "observer-not-ready",
result: sanitize(result),
valuesRedacted: true,
}));
}
return result ?? { ok: false, reason: "observer-not-ready", pageRole: "observer", pageId: observerPageId, valuesRedacted: true };
});
terminalStatus = "running";
await writeManifest({ status: "running", auth: publicAuth(auth) });
await writeHeartbeat({ status: "running" });
while (!stopping) {
await drainOneCommand();
await samplePage("interval");
if (maxSamples > 0 && sampleSeq >= maxSamples) {
await appendJsonl(files.control, controlRecord({ id: "max-samples", type: "stop", source: "sampler" }, "completed", { reason: "max-samples", maxSamples }));
break;
}
await sleep(sampleIntervalMs);
}
terminalStatus = "completed";
await writeHeartbeat({ status: "completed" });
await writeManifest({ status: "completed", completedAt: new Date().toISOString() });
process.exitCode = 0;
} catch (error) {
terminalStatus = "failed";
await appendJsonl(files.errors, eventRecord("runner-error", { error: errorSummary(error) })).catch(() => {});
await writeHeartbeat({ status: "failed", error: errorSummary(error) }).catch(() => {});
await writeManifest({ status: "failed", error: errorSummary(error) }).catch(() => {});
process.exitCode = 2;
} finally {
if (browser) await browser.close().catch(() => {});
}
async function prepareDirs() {
await mkdir(stateDir, { recursive: true, mode: 0o700 });
await Promise.all(Object.values(dirs).map((dir) => mkdir(dir, { recursive: true, mode: 0o700 })));
}
async function rotateExistingJsonlArtifacts() {
for (const [key, file] of Object.entries(files)) {
if (!file.endsWith(".jsonl")) continue;
let meta = null;
try {
meta = await stat(file);
} catch (error) {
if (error?.code === "ENOENT") continue;
throw error;
}
if (!meta?.isFile()) continue;
const archiveFile = await uniqueArchiveFile(jsonlRotation.stamp + "-" + path.basename(file));
await rename(file, archiveFile);
jsonlRotation.files.push({ key, from: path.relative(stateDir, file), to: path.relative(stateDir, archiveFile), byteCount: meta.size });
}
}
async function uniqueArchiveFile(name) {
let candidate = path.join(dirs.archive, name);
const parsed = path.parse(name);
for (let index = 1; ; index += 1) {
try {
await stat(candidate);
candidate = path.join(dirs.archive, parsed.name + "-" + index + parsed.ext);
} catch (error) {
if (error?.code === "ENOENT") return candidate;
throw error;
}
}
}
function compactFileTimestamp(value) {
return new Date(value).toISOString().replace(/[-:]/gu, "").replace(/[.]\d{3}Z$/u, "Z");
}
async function writeManifest(extra = {}) {
const manifest = {
ok: extra.status !== "failed",
command: "web-probe-observe",
specRef,
jobId,
pid: process.pid,
stateDir,
baseUrl,
targetPath,
network: publicNetwork(playwrightProxy),
pageAuthority: { browser: "chromium", context: "shared-auth", pageMode: "dual-control-observer", controlPageId: pageId, observerPageId, continuityBreaksRecorded: true },
pageProvenance: compactPageProvenance(currentPageProvenance),
sampling: { mode: "passive", sampleIntervalMs, screenshotIntervalMs, maxSamples, observerRefreshIntervalMs, observerInitiatedDefault: false, responseBodyReadDefault: false },
alertThresholds,
jsonlRotation,
commandDirs: dirs,
artifacts: files,
safety: { pureClient: true, inboundApi: false, database: false, queueConsumer: false, k8sWorkload: false, valuesRedacted: true, secretValuesPrinted: false },
startedAt,
...extra,
};
await writeFile(files.manifest, JSON.stringify(manifest, null, 2) + "\n", { mode: 0o600 });
}
async function writeHeartbeat(extra = {}) {
const heartbeat = {
ok: terminalStatus !== "failed",
jobId,
pid: process.pid,
stateDir,
status: terminalStatus,
pageId,
observerPageId,
baseUrl,
currentUrl: currentPageUrl(),
observerUrl: pageUrl(observerPage),
observerRefreshIntervalMs,
lastObserverRefreshAt: Number.isFinite(lastObserverRefreshAtMs) ? new Date(lastObserverRefreshAtMs).toISOString() : null,
pageProvenance: compactPageProvenance(currentPageProvenance),
sampleSeq,
commandSeq,
activeCommandId,
updatedAt: new Date().toISOString(),
uptimeMs: Date.now() - startedAtMs,
...extra,
};
await writeFile(files.heartbeat, JSON.stringify(heartbeat, null, 2) + "\n", { mode: 0o600 });
}
function attachPassiveListeners(targetPage, pageRole = "control", targetPageId = pageId) {
targetPage.on("request", (request) => {
void appendJsonl(files.network, eventRecord("request", {
pageRole,
pageId: targetPageId,
observerInitiated: false,
commandId: activeCommandId,
method: request.method(),
url: safeUrl(request.url()),
resourceType: request.resourceType(),
frameUrl: safeFrameUrl(request.frame()),
}));
});
targetPage.on("response", (response) => {
const request = response.request();
void appendJsonl(files.network, eventRecord("response", {
pageRole,
pageId: targetPageId,
observerInitiated: false,
commandId: activeCommandId,
method: request.method(),
url: safeUrl(response.url()),
resourceType: request.resourceType(),
status: response.status(),
statusText: response.statusText(),
fromServiceWorker: response.fromServiceWorker(),
bodyRead: false,
}));
});
targetPage.on("requestfailed", (request) => {
void appendJsonl(files.network, eventRecord("requestfailed", {
pageRole,
pageId: targetPageId,
observerInitiated: false,
commandId: activeCommandId,
method: request.method(),
url: safeUrl(request.url()),
resourceType: request.resourceType(),
failure: request.failure()?.errorText ?? null,
}));
});
targetPage.on("console", (message) => {
void appendJsonl(files.console, eventRecord("console", { pageRole, pageId: targetPageId, type: message.type(), text: truncate(message.text(), 1000), location: message.location() }));
});
targetPage.on("pageerror", (error) => {
void appendJsonl(files.errors, eventRecord("pageerror", { pageRole, pageId: targetPageId, error: errorSummary(error) }));
});
targetPage.on("crash", () => {
void appendJsonl(files.errors, eventRecord("page-crash", { pageRole, pageId: targetPageId }));
});
targetPage.on("close", () => {
void appendJsonl(files.control, eventRecord("continuity-break", { pageRole, pageId: targetPageId, reason: "page-closed" }));
});
}
async function drainOneCommand() {
const entries = (await readdir(dirs.commandsPending).catch(() => [])).filter((name) => name.endsWith(".json")).sort();
const name = entries[0];
if (!name) return;
const pending = path.join(dirs.commandsPending, name);
const processing = path.join(dirs.commandsProcessing, name);
await rename(pending, processing).catch(() => null);
const raw = await readFile(processing, "utf8");
const command = JSON.parse(raw);
const id = safeId(command.id || name.replace(/[.]json$/u, ""));
command.id = id;
const stopCommandSampler = startCommandActiveSampler(command);
try {
const result = await processCommand(command);
const done = { ok: true, commandId: id, type: command.type, completedAt: new Date().toISOString(), result: sanitize(result) };
await writeFile(path.join(dirs.commandsDone, id + ".json"), JSON.stringify(done, null, 2) + "\n", { mode: 0o600 });
await appendJsonl(files.control, controlRecord(command, "completed", done.result));
await unlink(processing).catch(() => {});
} catch (error) {
const failureSample = await samplePage("command-failed", { refreshObserver: false, screenshot: false })
.then(() => ({ ok: true, sampleSeq, valuesRedacted: true }))
.catch((sampleError) => ({ ok: false, error: errorSummary(sampleError), valuesRedacted: true }));
const failed = { ok: false, commandId: id, type: command.type, failedAt: new Date().toISOString(), error: errorSummary(error), failureSample };
await writeFile(path.join(dirs.commandsFailed, id + ".json"), JSON.stringify(failed, null, 2) + "\n", { mode: 0o600 });
await appendJsonl(files.control, controlRecord(command, "failed", { error: failed.error, failureSample }));
await unlink(processing).catch(() => {});
} finally {
stopCommandSampler();
activeCommandId = null;
await writeHeartbeat({ status: terminalStatus });
}
}
function startCommandActiveSampler(command) {
const intervalMs = Math.max(1000, Number(sampleIntervalMs) || 5000);
let stopped = false;
let timer = null;
let inFlight = false;
const schedule = () => {
if (stopped) return;
timer = setTimeout(tick, intervalMs);
if (timer && typeof timer.unref === "function") timer.unref();
};
const tick = () => {
if (stopped) return;
if (inFlight) {
schedule();
return;
}
inFlight = true;
samplePage("command-active", { refreshObserver: false, screenshot: false })
.catch((error) => appendJsonl(files.errors, eventRecord("command-active-sample-error", { commandId: command.id, commandType: command.type, error: errorSummary(error) })))
.finally(() => {
inFlight = false;
schedule();
});
};
schedule();
return () => {
stopped = true;
if (timer) clearTimeout(timer);
};
}
async function processCommand(command) {
commandSeq += 1;
activeCommandId = command.id;
await writeHeartbeat({ status: "running", activeCommandId });
await appendJsonl(files.control, controlRecord(command, "started", commandInputSummary(command)));
switch (command.type) {
case "login": return authenticate(context, page);
case "preflight": return preflightSummary();
case "goto": return withObserverSync(await gotoTarget(command.path || command.url || targetPath), "goto");
case "newSession": return withObserverSync(await createSessionFromUi(), "newSession");
case "sendPrompt": return withObserverSync(await sendPrompt(String(command.text || "")), "sendPrompt");
case "selectProvider": return withObserverSync(await selectProvider(String(command.provider || command.value || command.text || "")), "selectProvider");
case "clickSession": return withObserverSync(await clickSession(String(command.sessionId || command.value || "")), "clickSession");
case "screenshot": return captureScreenshot(command.reason || "manual", command.imageType || "png");
case "mark": return { mark: truncate(command.label || command.text || "mark", 200), currentUrl: currentPageUrl(), pageId };
case "stop": stopping = true; return { stopping: true, currentUrl: currentPageUrl(), pageId };
default: throw new Error("unsupported observer command type: " + command.type);
}
}
async function withObserverSync(result, reason) {
return { ...result, observer: await syncObserverPageToControlSession(reason, result?.sessionId ?? null) };
}
async function syncObserverPageToControlSession(reason, explicitSessionId = null, options = {}) {
if (!observerPage || observerPage.isClosed()) return { ok: false, reason, pageRole: "observer", pageId: observerPageId, failureKind: "observer-page-unavailable" };
const forceRefresh = options?.forceRefresh === true;
const snapshot = await workbenchSessionSnapshot();
const sessionId = explicitSessionId || snapshot?.activeSessionId || snapshot?.routeSessionId || routeSessionIdFromUrl(currentPageUrl());
const target = sessionId ? "/workbench/sessions/" + encodeURIComponent(sessionId) : targetPath;
const targetUrl = new URL(target, baseUrl).toString();
const beforeUrl = pageUrl(observerPage);
const beforeSessionId = routeSessionIdFromUrl(beforeUrl);
if (sessionId && beforeSessionId === sessionId && !forceRefresh) return { ok: true, reason, changed: false, observerRoundTrip: false, sessionId, beforeUrl, afterUrl: beforeUrl, pageRole: "observer", pageId: observerPageId };
let status = null;
let statusText = null;
const response = await observerPage.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: 45000 }).catch((error) => ({ observerGotoError: errorSummary(error) }));
if (response?.observerGotoError) return { ok: false, reason, changed: false, sessionId: sessionId ?? null, targetPath: target, beforeUrl, afterUrl: pageUrl(observerPage), pageRole: "observer", pageId: observerPageId, error: response.observerGotoError, valuesRedacted: true };
status = typeof response?.status === "function" ? response.status() : null;
statusText = typeof response?.statusText === "function" ? response.statusText() : null;
const readiness = await waitForTargetPageReady(observerPage, targetUrl, { timeoutMs: 15000 });
if (!readiness.ok) {
lastObserverRefreshAtMs = Date.now();
return { ok: false, reason, changed: true, observerRoundTrip: forceRefresh, sessionId: sessionId ?? null, targetPath: target, beforeUrl, afterUrl: pageUrl(observerPage), pageRole: "observer", pageId: observerPageId, httpStatus: status, statusText, readiness, hydration: null, failureKind: readiness.reason || "observer-target-not-ready", valuesRedacted: true };
}
const hydration = await waitForWorkbenchSessionHydrated(observerPage, sessionId, { timeoutMs: 15000 });
lastObserverRefreshAtMs = Date.now();
return { ok: hydration.ok === true, reason, changed: true, observerRoundTrip: forceRefresh, sessionId: sessionId ?? null, targetPath: target, beforeUrl, afterUrl: pageUrl(observerPage), pageRole: "observer", pageId: observerPageId, httpStatus: status, statusText, readiness, hydration, failureKind: hydration.ok === true ? null : hydration.reason || "observer-session-hydration-failed", valuesRedacted: true };
}
async function waitForWorkbenchSessionHydrated(targetPage, sessionId, options = {}) {
const timeoutMs = Number.isFinite(Number(options.timeoutMs)) ? Number(options.timeoutMs) : 15000;
const started = Date.now();
const deadline = started + Math.max(1, timeoutMs);
let last = null;
while (Date.now() <= deadline) {
last = await workbenchSessionSnapshot(targetPage);
const expected = String(sessionId || "").trim();
const observedPath = safeUrlPath(last?.url || pageUrl(targetPage));
const routeOk = expected ? last?.routeSessionId === expected : isWorkbenchPathname(observedPath || "");
const activeOk = expected ? last?.activeSessionId === expected : isWorkbenchPathname(observedPath || "");
if (routeOk && activeOk) return { ok: true, durationMs: Date.now() - started, snapshot: last, valuesRedacted: true };
await targetPage.waitForTimeout(250).catch(() => {});
}
return { ok: false, durationMs: Date.now() - started, snapshot: last, reason: "observer-session-hydration-timeout", expectedSessionId: sessionId || null, valuesRedacted: true };
}
async function maybeRefreshObserverPage(reason) {
if (!observerPage || observerPage.isClosed()) return null;
if (!observerRefreshIntervalMs || observerRefreshIntervalMs <= 0) return null;
if (Date.now() - lastObserverRefreshAtMs < observerRefreshIntervalMs) return null;
const result = await syncObserverPageToControlSession("observer-periodic-refresh", null, { forceRefresh: true });
await appendJsonl(files.control, eventRecord("observer-periodic-refresh", {
pageRole: "observer",
pageId: observerPageId,
reason,
intervalMs: observerRefreshIntervalMs,
result,
valuesRedacted: true
}));
return result;
}
async function runControlCommand(command, fn) {
activeCommandId = command.id;
commandSeq += 1;
const beforeUrl = currentPageUrl();
const started = Date.now();
await appendJsonl(files.control, controlRecord(command, "started", { beforeUrl, input: commandInputSummary(command) }));
try {
const result = await fn();
await appendJsonl(files.control, controlRecord(command, "completed", { beforeUrl, afterUrl: currentPageUrl(), durationMs: Date.now() - started, result: sanitize(result) }));
return result;
} catch (error) {
let failurePageProvenance = null;
let failureScreenshot = null;
try {
failurePageProvenance = compactPageProvenance(await refreshPageProvenance("command-failed", null));
} catch (captureError) {
await appendJsonl(files.errors, eventRecord("failure-provenance-error", { commandId: command.id, pageRole: "control", pageId, error: errorSummary(captureError) }));
}
try {
failureScreenshot = await captureScreenshot("command-failed", "jpeg");
} catch (captureError) {
await appendJsonl(files.errors, eventRecord("failure-screenshot-error", { commandId: command.id, pageRole: "control", pageId, error: errorSummary(captureError) }));
}
await appendJsonl(files.control, controlRecord(command, "failed", { beforeUrl, afterUrl: currentPageUrl(), durationMs: Date.now() - started, error: errorSummary(error), failurePageProvenance, failureScreenshot }));
throw error;
} finally {
activeCommandId = null;
}
}
async function authenticate(browserContext, authPage) {
const loginUrl = new URL("/auth/login", baseUrl).toString();
let activeAuthPage = authPage && !authPage.isClosed() ? authPage : null;
let closeAuthPage = false;
if (!activeAuthPage) {
activeAuthPage = await browserContext.newPage();
closeAuthPage = true;
}
const attempts = [];
const maxAttempts = 5;
const initialDelayMs = 250;
const maxDelayMs = 5000;
try {
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
const retryDelayMs = attempt < maxAttempts ? Math.min(maxDelayMs, initialDelayMs * (2 ** (attempt - 1))) : 0;
const retryLabel = attempt + "/" + maxAttempts;
await writeHeartbeat({ status: terminalStatus, auth: { phase: "api-login", retryAttempt: attempt, retryMaxAttempts: maxAttempts, lastRetryLabel: retryLabel, retryDelayMs: 0, retryExhausted: false, valuesRedacted: true } }).catch(() => {});
try {
const response = await pageAuthLogin(activeAuthPage, loginUrl);
const cookieState = await readAuthCookieState(browserContext);
const retryable = isRetryableAuthStatus(response.status);
const item = {
attempt,
retryAttempt: attempt,
retryMaxAttempts: maxAttempts,
retryLabel,
retryDelayMs: retryable && attempt < maxAttempts ? retryDelayMs : 0,
method: "api",
status: response.status,
statusText: response.statusText,
retryable,
cookiePresent: cookieState.cookiePresent,
cookieNames: cookieState.cookieNames,
valuesRedacted: true,
};
attempts.push(item);
await writeHeartbeat({ status: terminalStatus, auth: { phase: "api-login", lastRetryLabel: item.retryLabel, retryAttempt: item.retryAttempt, retryMaxAttempts: item.retryMaxAttempts, retryDelayMs: item.retryDelayMs, lastStatus: item.status, lastStatusText: item.statusText, retryable: item.retryable, cookiePresent: item.cookiePresent, retryExhausted: false, valuesRedacted: true } }).catch(() => {});
if (response.ok && cookieState.cookiePresent) {
return {
ok: true,
method: "api",
loginPath: new URL(loginUrl).pathname,
status: response.status,
statusText: response.statusText,
cookiePresent: true,
cookieNames: cookieState.cookieNames,
attempts,
retryCount: attempt - 1,
retryMaxAttempts: maxAttempts,
lastRetryLabel: attempt + "/" + maxAttempts,
retryExhausted: false,
retryable: false,
valuesRedacted: true,
};
}
if (!retryable) break;
} catch (error) {
const retryable = isRetryableAuthError(error);
attempts.push({
attempt,
retryAttempt: attempt,
retryMaxAttempts: maxAttempts,
retryLabel,
retryDelayMs: retryable && attempt < maxAttempts ? retryDelayMs : 0,
method: "api",
status: 0,
statusText: "request-error",
retryable,
error: error && error.message ? truncate(error.message, 500) : truncate(String(error), 500),
cookiePresent: false,
cookieNames: [],
valuesRedacted: true,
});
const item = attempts[attempts.length - 1] || null;
await writeHeartbeat({ status: terminalStatus, auth: { phase: "api-login", lastRetryLabel: item?.retryLabel || retryLabel, retryAttempt: attempt, retryMaxAttempts: maxAttempts, retryDelayMs: item?.retryDelayMs ?? 0, lastStatus: item?.status ?? 0, lastStatusText: item?.statusText ?? "request-error", retryable, cookiePresent: false, retryExhausted: false, lastError: item?.error || null, valuesRedacted: true } }).catch(() => {});
if (!retryable) break;
}
if (attempt < maxAttempts && attempts[attempts.length - 1]?.retryable === true) await sleep(retryDelayMs);
}
} finally {
if (closeAuthPage && activeAuthPage && !activeAuthPage.isClosed()) await activeAuthPage.close().catch(() => {});
}
const cookieState = await readAuthCookieState(browserContext);
const last = attempts[attempts.length - 1] || null;
const retryable = attempts.some((attempt) => attempt && attempt.retryable === true);
const failure = {
ok: false,
method: "api",
loginPath: new URL(loginUrl).pathname,
status: typeof last?.status === "number" ? last.status : 0,
statusText: typeof last?.statusText === "string" ? last.statusText : "api-login-failed",
cookiePresent: cookieState.cookiePresent,
cookieNames: cookieState.cookieNames,
attempts,
retryCount: Math.max(0, attempts.length - 1),
retryMaxAttempts: maxAttempts,
lastRetryLabel: last?.retryLabel || null,
retryExhausted: retryable && attempts.length >= maxAttempts,
retryable,
lastError: last?.error || null,
valuesRedacted: true,
};
await writeHeartbeat({ status: terminalStatus, auth: { phase: "api-login", lastRetryLabel: failure.lastRetryLabel, retryAttempt: attempts.length, retryMaxAttempts: maxAttempts, retryDelayMs: 0, lastStatus: failure.status, lastStatusText: failure.statusText, retryable: failure.retryable, cookiePresent: failure.cookiePresent, retryExhausted: failure.retryExhausted, lastError: failure.lastError, valuesRedacted: true } }).catch(() => {});
const error = new Error(authFailureMessage(failure));
error.webProbeAuth = failure;
throw error;
}
async function pageAuthLogin(authPage, loginUrl) {
if (!authPage) throw new Error("auth page is not ready");
await authPage.goto(new URL("/assets/favicon.svg", baseUrl).toString(), { waitUntil: "domcontentloaded", timeout: 12000 });
return authPage.evaluate(async (input) => {
const response = await fetch(input.loginUrl, {
method: "POST",
headers: { accept: "application/json", "content-type": "application/json" },
body: JSON.stringify({ username: input.username, password: input.password }),
credentials: "include",
});
await response.text().catch(() => "");
return {
ok: response.ok,
status: response.status,
statusText: response.statusText || "",
};
}, { username, password, loginUrl });
}
function publicAuth(value) {
if (!value) return null;
return {
ok: value.ok === true,
method: value.method,
status: value.status,
cookiePresent: value.cookiePresent === true,
cookieNames: value.cookieNames || [],
retryCount: value.retryCount ?? null,
retryMaxAttempts: value.retryMaxAttempts ?? null,
lastRetryLabel: value.lastRetryLabel ?? null,
retryExhausted: value.retryExhausted === true,
valuesRedacted: true
};
}
async function readAuthCookieState(browserContext) {
const cookies = await browserContext.cookies(baseUrl);
const cookieNames = cookies.map((cookie) => cookie.name).sort();
return {
cookiePresent: cookieNames.includes("hwlab_session") || cookieNames.some((name) => /session|auth|token/iu.test(name)),
cookieNames: cookieNames.filter((name) => /session|auth|token/iu.test(name)),
};
}
function isRetryableAuthStatus(status) {
return status === 0 || status === 408 || status === 409 || status === 425 || status === 429 || status >= 500;
}
function isRetryableAuthError(error) {
const message = error && error.message ? String(error.message) : String(error || "");
return /AbortError|EAI_AGAIN|ETIMEDOUT|ECONNRESET|ECONNREFUSED|ECONNABORTED|socket hang up|ERR_NETWORK_CHANGED|fetch failed|failed to fetch|network|timeout|aborted/iu.test(message);
}
function authFailureMessage(failure) {
const last = Array.isArray(failure.attempts) && failure.attempts.length > 0 ? failure.attempts[failure.attempts.length - 1] : null;
const retry = failure.lastRetryLabel ? " retry=" + failure.lastRetryLabel : "";
const exhausted = failure.retryExhausted ? " exhausted=true" : "";
const status = last ? " status=" + (last.status ?? "-") + " " + (last.statusText ?? "") : "";
const error = last?.error ? " error=" + truncate(last.error, 160) : "";
return ("auth login failed:" + retry + exhausted + status + error).trim();
}
function proxyConfigFromEnv(targetBaseUrl) {
if (browserProxyMode === "direct") return null;
let target;
try {
target = new URL(targetBaseUrl);
} catch {
return null;
}
const noProxy = process.env.NO_PROXY || process.env.no_proxy || "";
if (noProxyMatches(target.hostname, noProxy)) return null;
const raw = target.protocol === "https:"
? process.env.HTTPS_PROXY || process.env.https_proxy || process.env.ALL_PROXY || process.env.all_proxy || process.env.HTTP_PROXY || process.env.http_proxy || ""
: process.env.HTTP_PROXY || process.env.http_proxy || process.env.ALL_PROXY || process.env.all_proxy || process.env.HTTPS_PROXY || process.env.https_proxy || "";
if (!raw) return null;
return { server: raw };
}
function chromiumLaunchOptionsForProxy(proxy) {
const base = { env: browserProcessEnvWithoutProxy() };
if (proxy === null) return { ...base, args: ["--no-proxy-server"] };
return { ...base, proxy };
}
function browserProcessEnvWithoutProxy() {
const blocked = new Set(["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "NO_PROXY", "http_proxy", "https_proxy", "all_proxy", "no_proxy"]);
const env = {};
for (const [key, value] of Object.entries(process.env)) {
if (!blocked.has(key) && value !== undefined) env[key] = value;
}
return env;
}
function noProxyMatches(hostname, rawList) {
const host = String(hostname || "").toLowerCase();
if (!host) return false;
return String(rawList || "").split(",").some((raw) => {
let item = raw.trim().toLowerCase();
if (!item) return false;
if (item === "*") return true;
item = item.replace(/^\*\./u, ".");
const portIndex = item.lastIndexOf(":");
if (portIndex > -1 && !item.includes("]")) item = item.slice(0, portIndex);
if (item.startsWith(".")) return host === item.slice(1) || host.endsWith(item);
return host === item;
});
}
function publicNetwork(proxy) {
return {
proxy: proxy === null ? { enabled: false, source: "env", valuesRedacted: true } : {
enabled: true,
source: "env",
server: publicProxyServer(proxy.server),
valuesRedacted: true,
},
browser: {
proxyMode: proxy === null ? "direct-no-proxy-server" : "explicit-playwright-proxy",
requestedProxyMode: browserProxyMode,
proxyEnvCleared: true,
valuesRedacted: true,
},
valuesRedacted: true,
};
}
function parseBrowserProxyMode(raw) {
if (raw === "auto" || raw === "direct") return raw;
return "auto";
}
function publicProxyServer(raw) {
try {
const parsed = new URL(String(raw || ""));
parsed.username = "";
parsed.password = "";
const value = parsed.toString();
if (parsed.pathname === "/" && parsed.search === "" && parsed.hash === "") return value.replace(/\/$/u, "");
return value;
} catch {
return String(raw || "").replace(/\/\/[^/@]+@/u, "//[redacted]@");
}
}
async function gotoTarget(rawTarget) {
const target = new URL(String(rawTarget || targetPath), baseUrl).toString();
const beforeUrl = currentPageUrl();
const attempts = [];
for (let attempt = 1; attempt <= 2; attempt += 1) {
try {
const response = await page.goto(target, { waitUntil: "domcontentloaded", timeout: 45000 });
await page.waitForTimeout(1000).catch(() => {});
const httpStatus = response ? response.status() : null;
const readiness = await waitForTargetPageReady(page, target, { timeoutMs: 15000 });
if (!readiness.ok) {
const pageProvenance = await refreshPageProvenance("goto-degraded", httpStatus).catch(() => null);
attempts.push({ attempt, ok: false, degraded: true, httpStatus, readiness, failureKind: readiness.reason || "workbench-app-not-ready" });
return { beforeUrl, afterUrl: currentPageUrl(), httpStatus, pageId, degraded: true, degradedReason: readiness.reason || "workbench-app-not-ready", pageProvenance: compactPageProvenance(pageProvenance), readiness, attempts };
}
const pageProvenance = await refreshPageProvenance("goto", httpStatus);
attempts.push({ attempt, ok: true, httpStatus, readiness });
return { beforeUrl, afterUrl: currentPageUrl(), httpStatus, pageId, pageProvenance: compactPageProvenance(pageProvenance), readiness, attempts };
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
attempts.push({ attempt, ok: false, failureKind: navigationFailureKind(message), message: redactErrorMessage(message), readiness: error?.navigationReadiness ?? null });
if (/workbench-app-not-ready/iu.test(message)) {
const lateReadiness = await waitForTargetPageReady(page, target, { timeoutMs: 5000 }).catch(() => null);
if (lateReadiness?.ok) {
const pageProvenance = await refreshPageProvenance("goto-late-ready", null);
attempts.push({ attempt, ok: true, lateReady: true, httpStatus: null, readiness: lateReadiness });
return { beforeUrl, afterUrl: currentPageUrl(), httpStatus: null, pageId, pageProvenance: compactPageProvenance(pageProvenance), readiness: lateReadiness, attempts };
}
}
if (attempt >= 2 || !isRetryableNavigationError(message)) {
throw Object.assign(new Error(message), { attempts, target });
}
if (!observerPage) {
await recreateAuthenticatedContextForNavigation("retryable-navigation-" + navigationFailureKind(message), attempt).catch((resetError) => appendJsonl(files.errors, eventRecord("navigation-context-reset-error", { commandId: activeCommandId, attempt, error: errorSummary(resetError) })));
} else {
await recreateControlPageForNavigation("retryable-navigation-" + navigationFailureKind(message), attempt).catch((resetError) => appendJsonl(files.errors, eventRecord("navigation-page-reset-error", { commandId: activeCommandId, attempt, error: errorSummary(resetError) })));
}
await page.waitForTimeout(1500 * attempt).catch(() => {});
}
}
return { beforeUrl, afterUrl: currentPageUrl(), httpStatus: null, pageId, attempts };
}
async function recreateControlPageForNavigation(reason, attempt) {
const before = currentPageUrl();
if (page && !page.isClosed()) await page.close().catch(() => {});
page = await context.newPage();
attachPassiveListeners(page, "control", pageId);
currentPageProvenance = null;
await appendJsonl(files.control, eventRecord("page-recreated", { reason, attempt, beforeUrl: before, afterUrl: currentPageUrl(), pageRole: "control", pageId, valuesRedacted: true }));
}
async function recreateAuthenticatedContextForNavigation(reason, attempt) {
const before = currentPageUrl();
if (page && !page.isClosed()) await page.close().catch(() => {});
if (observerPage && !observerPage.isClosed()) await observerPage.close().catch(() => {});
observerPage = null;
if (context) await context.close().catch(() => {});
context = await browser.newContext({ viewport, ...(playwrightProxy === null ? {} : { proxy: playwrightProxy }) });
auth = await authenticate(context);
page = await context.newPage();
attachPassiveListeners(page, "control", pageId);
currentPageProvenance = null;
await appendJsonl(files.control, eventRecord("context-recreated", { reason, attempt, beforeUrl: before, afterUrl: currentPageUrl(), pageRole: "control", pageId, auth: publicAuth(auth), valuesRedacted: true }));
}
async function refreshPageProvenance(reason, httpStatus = null) {
if (!page || page.isClosed()) return currentPageProvenance;
const observed = await page.evaluate(() => {
const assetPath = (raw) => {
if (!raw) return null;
try {
const url = new URL(raw, location.href);
const keys = Array.from(url.searchParams.keys()).sort();
return url.pathname + (keys.length > 0 ? "?keys=" + keys.join(",") : "");
} catch {
return null;
}
};
const meta = Array.from(document.querySelectorAll("meta[name], meta[property]")).map((element) => ({
key: String(element.getAttribute("name") || element.getAttribute("property") || "").slice(0, 120),
content: String(element.getAttribute("content") || "").slice(0, 200),
})).filter((item) => item.key).sort((a, b) => a.key.localeCompare(b.key));
const navigation = performance.getEntriesByType("navigation")[0] || null;
return {
url: location.href,
path: location.pathname,
title: document.title,
readyState: document.readyState,
timeOrigin: Math.round(performance.timeOrigin || 0),
navigationStartTime: navigation ? Math.round(navigation.startTime) : null,
scripts: Array.from(document.scripts).map((element) => assetPath(element.src)).filter(Boolean).sort(),
stylesheets: Array.from(document.querySelectorAll('link[rel~="stylesheet"][href]')).map((element) => assetPath(element.href)).filter(Boolean).sort(),
meta,
};
}).catch((error) => ({ error: errorSummary(error), url: currentPageUrl(), path: null, scripts: [], stylesheets: [], meta: [] }));
pageLoadSeq += 1;
currentPageProvenance = normalizePageProvenance(observed, { reason, httpStatus, pageLoadSeq });
await appendJsonl(files.control, eventRecord("page-provenance", { reason, httpStatus, pageProvenance: compactPageProvenance(currentPageProvenance) }));
return currentPageProvenance;
}
function normalizePageProvenance(value, options = {}) {
const scripts = Array.isArray(value?.scripts) ? value.scripts.map(String).filter(Boolean) : [];
const stylesheets = Array.isArray(value?.stylesheets) ? value.stylesheets.map(String).filter(Boolean) : [];
const meta = Array.isArray(value?.meta) ? value.meta.map((item) => ({
key: String(item?.key || "").slice(0, 120),
contentHash: sha256Text(String(item?.content || "")),
})).filter((item) => item.key) : [];
const fingerprintInput = JSON.stringify({ scripts, stylesheets, meta });
return {
pageLoadSeq: options.pageLoadSeq ?? pageLoadSeq,
reason: options.reason || "sample",
observedAt: new Date().toISOString(),
urlPath: safeUrlPath(value?.url || currentPageUrl()),
documentPath: value?.path || null,
titleHash: sha256Text(String(value?.title || "")),
documentReadyState: value?.readyState || null,
timeOrigin: Number.isFinite(Number(value?.timeOrigin)) ? Number(value.timeOrigin) : null,
navigationStartTime: Number.isFinite(Number(value?.navigationStartTime)) ? Number(value.navigationStartTime) : null,
httpStatus: options.httpStatus ?? null,
assetFingerprint: sha256Text(fingerprintInput),
scriptCount: scripts.length,
stylesheetCount: stylesheets.length,
metaCount: meta.length,
scripts: scripts.slice(0, 30),
stylesheets: stylesheets.slice(0, 30),
meta: meta.slice(0, 30),
error: value?.error || null,
valuesRedacted: true,
};
}
function compactPageProvenance(value) {
if (!value) 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 ?? 0,
stylesheetCount: value.stylesheetCount ?? 0,
metaCount: value.metaCount ?? 0,
scripts: Array.isArray(value.scripts) ? value.scripts.slice(0, 12) : [],
stylesheets: Array.isArray(value.stylesheets) ? value.stylesheets.slice(0, 12) : [],
error: value.error || null,
valuesRedacted: true,
};
}
function isRetryableNavigationError(message) {
return /net::ERR_NETWORK_CHANGED|net::ERR_ABORTED|net::ERR_CONNECTION_RESET|Navigation timeout|workbench-app-not-ready/iu.test(String(message || ""));
}
function navigationFailureKind(message) {
const text = String(message || "");
if (/net::ERR_NETWORK_CHANGED/iu.test(text)) return "net::ERR_NETWORK_CHANGED";
if (/net::ERR_ABORTED/iu.test(text)) return "net::ERR_ABORTED";
if (/net::ERR_CONNECTION_RESET/iu.test(text)) return "net::ERR_CONNECTION_RESET";
if (/Navigation timeout/iu.test(text)) return "navigation-timeout";
if (/workbench-app-not-ready/iu.test(text)) return "workbench-app-not-ready";
return "navigation-error";
}
function redactErrorMessage(message) {
return String(message || "")
.replace(/([?&](?:token|key|password|secret|authorization)=)[^&\s]+/giu, "$1[redacted]")
.replace(/(Bearer\s+)[A-Za-z0-9._~+/=-]+/gu, "$1[redacted]");
}
async function waitForTargetPageReady(targetPage, targetUrl, options = {}) {
const timeoutMs = Number.isFinite(Number(options.timeoutMs)) ? Math.max(1, Number(options.timeoutMs)) : 15000;
const targetPathname = safeUrlPath(targetUrl) || "";
if (!isWorkbenchPathname(targetPathname)) return { ok: true, reason: "not-workbench-route", valuesRedacted: true };
const started = Date.now();
await targetPage.waitForFunction(() => {
const visible = (element) => {
if (!element) return false;
const rect = element.getBoundingClientRect();
const style = window.getComputedStyle(element);
return rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none";
};
const workspace = document.querySelector("#workspace, .workbench-route");
const login = document.querySelector("form.login-card, .login-card, [data-testid='login']");
return Boolean(visible(workspace) || visible(login));
}, null, { timeout: timeoutMs }).catch(() => null);
const snapshot = await workbenchReadinessSnapshot(targetPage);
const ok = snapshot.workbenchShellVisible === true;
return {
ok,
reason: ok ? "workbench-ready" : snapshot.loginVisible ? "login-visible" : "workbench-app-not-ready",
durationMs: Date.now() - started,
snapshot,
valuesRedacted: true
};
}
async function workbenchReadinessSnapshot(targetPage) {
const snapshot = await targetPage.evaluate(() => {
const visible = (element) => {
if (!element) return false;
const rect = element.getBoundingClientRect();
const style = window.getComputedStyle(element);
return rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none";
};
return {
url: window.location.href,
path: window.location.pathname,
readyState: document.readyState,
workbenchShellVisible: visible(document.querySelector("#workspace, .workbench-route")),
sessionCreateVisible: visible(document.querySelector("#session-create")),
commandInputPresent: visible(document.querySelector("#command-input")),
activeTabPresent: visible(document.querySelector(".session-tab[data-active='true'], .session-tab[aria-selected='true']")),
warningPresent: visible(document.querySelector(".composer-warning")),
loginVisible: visible(document.querySelector("form.login-card, .login-card, [data-testid='login']")),
bodyTextPreview: String(document.body?.innerText || "").slice(0, 2000),
valuesRedacted: true
};
}).catch((error) => ({ error: errorSummary(error), valuesRedacted: true }));
if (snapshot && typeof snapshot.bodyTextPreview === "string") {
snapshot.bodyTextHash = sha256Text(snapshot.bodyTextPreview);
delete snapshot.bodyTextPreview;
}
return snapshot;
}
function isWorkbenchPathname(value) {
const pathname = String(value || "");
return pathname === "/workbench" || pathname === "/workspace" || pathname.startsWith("/workbench/") || pathname.startsWith("/workspace/");
}
async function createSessionFromUi() {
const beforeUrl = currentPageUrl();
const before = await workbenchSessionSnapshot();
const readinessBeforeClick = await workbenchReadinessSnapshot(page);
const create = page.locator("#session-create").first();
await create.waitFor({ state: "visible", timeout: 15000 });
const createButtonState = await create.evaluate((element) => {
const rect = element.getBoundingClientRect();
const style = window.getComputedStyle(element);
return {
tag: element.tagName.toLowerCase(),
id: element.id || null,
disabled: Boolean(element.disabled),
ariaDisabled: element.getAttribute("aria-disabled") || null,
visible: rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none",
rect: { width: Math.round(rect.width), height: Math.round(rect.height) },
valuesRedacted: true
};
}).catch((error) => ({ error: errorSummary(error), valuesRedacted: true }));
const createResponsePromise = page.waitForResponse((response) => {
const request = response.request();
if (request.method().toUpperCase() !== "POST") return false;
try {
return new URL(response.url()).pathname === "/v1/agent/sessions";
} catch {
return false;
}
}, { timeout: 45000 }).catch((error) => ({ waitError: errorSummary(error) }));
await create.click();
const createResponse = await createResponsePromise;
if (createResponse?.waitError) {
const error = new Error("newSession did not observe POST /v1/agent/sessions response after click: " + (createResponse.waitError.message || createResponse.waitError.name || "timeout"));
error.details = { beforeUrl, afterUrl: currentPageUrl(), before, readinessBeforeClick, createButtonState, waitError: createResponse.waitError, pageId, valuesRedacted: true };
throw error;
}
const createStatus = createResponse.status();
let createPayload = null;
let createPayloadError = null;
try {
createPayload = await createResponse.json();
} catch (error) {
createPayloadError = errorSummary(error);
}
const createdSessionId = sessionIdFromAgentSessionPayload(createPayload);
if (createStatus < 200 || createStatus >= 300 || !createdSessionId) {
const error = new Error("newSession did not receive an authoritative session id from POST /v1/agent/sessions");
error.details = { status: createStatus, statusText: createResponse.statusText(), responseParsed: createPayload !== null, responseParseError: createPayloadError, createdSessionId, valuesRedacted: true };
throw error;
}
await page.waitForFunction((expectedSessionId) => {
const activeTab = document.querySelector(".session-tab[data-active='true'], .session-tab[aria-selected='true']");
const sessionId = activeTab?.getAttribute("data-session-id") || "";
const routeMatch = window.location.pathname.match(/\/workbench\/sessions\/([^/]+)/u) || window.location.pathname.match(/\/workspace\/sessions\/([^/]+)/u);
const routeSessionId = routeMatch ? decodeURIComponent(routeMatch[1] || "") : "";
const warning = document.querySelector(".composer-warning")?.textContent?.trim() || "";
const input = document.querySelector("#command-input");
return Boolean(activeTab && sessionId === expectedSessionId && routeSessionId === expectedSessionId && input && !input.disabled && !warning);
}, createdSessionId, { timeout: 45000 }).catch(() => null);
const after = await workbenchSessionSnapshot();
const afterSessionId = after?.activeSessionId || after?.routeSessionId || "";
const ok = Boolean(afterSessionId === createdSessionId && after?.routeSessionId === createdSessionId && after?.composerReady);
if (!ok) {
const error = new Error("newSession did not select the authoritative newly created workbench session");
error.details = { beforeUrl, afterUrl: currentPageUrl(), before, after, createdSessionId, pageId, valuesRedacted: true };
throw error;
}
return {
beforeUrl,
afterUrl: currentPageUrl(),
ok,
before,
after,
sessionId: createdSessionId,
createSession: { status: createStatus, statusText: createResponse.statusText(), responseParsed: createPayload !== null, responseParseError: createPayloadError, createdSessionId, valuesRedacted: true },
pageId
};
}
function sessionIdFromAgentSessionPayload(payload) {
const direct = payload?.sessionId ?? payload?.id ?? payload?.session?.sessionId ?? payload?.session?.id ?? payload?.data?.sessionId ?? payload?.data?.id ?? payload?.data?.session?.sessionId ?? payload?.data?.session?.id;
const directText = String(direct || "").trim();
if (/^ses_[A-Za-z0-9_-]+$/u.test(directText)) return directText;
const match = JSON.stringify(payload ?? "").match(/\bses_[A-Za-z0-9_-]+\b/u);
return match ? match[0] : null;
}
async function workbenchSessionSnapshot(targetPage = page) {
return targetPage.evaluate(() => {
const activeTab = document.querySelector(".session-tab[data-active='true'], .session-tab[aria-selected='true']");
const routeMatch = window.location.pathname.match(/\/workbench\/sessions\/([^/]+)/u) || window.location.pathname.match(/\/workspace\/sessions\/([^/]+)/u);
const input = document.querySelector("#command-input");
const warning = document.querySelector(".composer-warning")?.textContent?.trim() || null;
const visible = (element) => {
const rect = element.getBoundingClientRect();
const style = window.getComputedStyle(element);
return rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none";
};
return {
url: window.location.href,
routeSessionId: routeMatch ? decodeURIComponent(routeMatch[1] || "") : null,
activeSessionId: activeTab?.getAttribute("data-session-id") || null,
activeConversationId: activeTab?.getAttribute("data-conversation-id") || null,
activeStatus: activeTab?.getAttribute("data-status") || null,
tabCount: document.querySelectorAll(".session-tab").length,
messageCount: Array.from(document.querySelectorAll('article.message-card, .message-card[data-message-id], article[data-message-id]')).filter(visible).length,
traceRowCount: Array.from(document.querySelectorAll('[data-testid*="trace" i], [class*="trace" i], [data-trace-id], [data-testid*="event" i]')).filter(visible).length,
loadingCount: Array.from(document.querySelectorAll('[aria-busy="true"], [data-loading="true"], [class*="loading" i], [data-testid*="loading" i]')).filter(visible).length,
composerReady: Boolean(activeTab && input && !input.disabled && !warning),
warning
};
}).catch(() => null);
}
async function sendPrompt(text) {
if (text.trim().length === 0) throw new Error("sendPrompt requires non-empty text");
const beforeUrl = currentPageUrl();
const beforeEvidence = await promptSideEffectSnapshot();
const primaryEditor = page.locator("#command-input").last();
const editor = await primaryEditor.isVisible().catch(() => false)
? primaryEditor
: page.locator('textarea, [role="textbox"], [contenteditable="true"], input[type="text"]').last();
await editor.waitFor({ state: "visible", timeout: 15000 });
const tag = await editor.evaluate((element) => element.tagName.toLowerCase()).catch(() => "");
const editable = await editor.evaluate((element) => element.getAttribute("contenteditable") === "true").catch(() => false);
if (tag === "textarea" || tag === "input") await editor.fill(text);
else if (editable) {
await editor.click();
await page.keyboard.insertText(text);
} else {
await editor.click();
await page.keyboard.insertText(text);
}
const primarySubmit = page.locator('#command-submit, [data-testid="command-submit"], [data-testid="composer-submit"], [data-testid="send-command"]').last();
const submit = await primarySubmit.isVisible().catch(() => false)
? primarySubmit
: page.locator([
'button[type="submit"]',
'button:has-text("发送")',
'button:has-text("Send")',
'[data-testid*="send" i]',
'[aria-label*="send" i]',
'[aria-label*="发送"]'
].join(", ")).last();
await submit.waitFor({ state: "visible", timeout: 15000 });
const chatResponsePromise = page.waitForResponse((response) => {
const request = response.request();
if (request.method().toUpperCase() !== "POST") return false;
try {
return new URL(response.url()).pathname === "/v1/agent/chat";
} catch {
return false;
}
}, { timeout: 45000 }).catch((error) => ({ waitError: errorSummary(error) }));
await submit.click();
const chatResponse = await chatResponsePromise;
await page.waitForTimeout(500);
if (chatResponse?.waitError) {
const sideEffect = await waitForPromptSideEffect(beforeEvidence, 5000);
if (sideEffect.submitted) {
return {
beforeUrl,
afterUrl: currentPageUrl(),
textHash: sha256Text(text),
textBytes: Buffer.byteLength(text),
chatSubmit: { status: null, statusText: null, urlPath: "/v1/agent/chat", waitError: chatResponse.waitError, sideEffectObserved: true, sideEffect },
pageId
};
}
throw new Error("sendPrompt did not observe POST /v1/agent/chat response after submit: " + (chatResponse.waitError.message || chatResponse.waitError.name || "timeout"));
}
const chatStatus = chatResponse.status();
if (chatStatus < 200 || chatStatus >= 300) {
throw new Error("sendPrompt observed POST /v1/agent/chat HTTP " + chatStatus + " " + chatResponse.statusText());
}
let chatPayload = null;
let chatPayloadError = null;
try {
chatPayload = await chatResponse.json();
} catch (error) {
chatPayloadError = errorSummary(error);
}
let chatUrlPath = "/v1/agent/chat";
try {
chatUrlPath = new URL(chatResponse.url()).pathname;
} catch {
chatUrlPath = "/v1/agent/chat";
}
const payloadText = chatPayload ? JSON.stringify(chatPayload) : "";
const traceId = payloadText.match(/\btrc_[A-Za-z0-9_-]+\b/u)?.[0] || null;
const otelTraceId = typeof chatPayload?.otelTrace?.traceId === "string" && /^[0-9a-f]{32}$/u.test(chatPayload.otelTrace.traceId)
? chatPayload.otelTrace.traceId
: null;
return {
beforeUrl,
afterUrl: currentPageUrl(),
textHash: sha256Text(text),
textBytes: Buffer.byteLength(text),
chatSubmit: {
status: chatStatus,
statusText: chatResponse.statusText(),
urlPath: chatUrlPath,
traceId,
otelTraceId,
resultUrl: safeUrlPath(chatPayload?.resultUrl),
turnUrl: safeUrlPath(chatPayload?.turnUrl),
streamUrl: safeUrlPath(chatPayload?.streamUrl),
responseParsed: chatPayload !== null,
responseParseError: chatPayloadError,
valuesRedacted: true
},
pageId
};
}
async function waitForPromptSideEffect(beforeEvidence, timeoutMs) {
await page.waitForFunction((before) => {
const current = (() => {
const text = document.body?.innerText || "";
const runIds = Array.from(new Set(text.match(/run_[A-Za-z0-9_:-]+/gu) || [])).slice(-20);
const traceIds = Array.from(new Set(text.match(/trc_[A-Za-z0-9_:-]+/gu) || [])).slice(-20);
const running = /Trace running|最近\s*\d+\s*(?:秒|分钟|分|小时)前|Code Agent\s*耗时/iu.test(text);
const executionError = /AgentRun error|agentrun:error:|provider-stream-disconnected|provider-unavailable/iu.test(text);
const textBytes = new TextEncoder().encode(text).length;
return { runIds, traceIds, running, executionError, textBytes };
})();
const beforeRuns = Array.isArray(before?.runIds) ? before.runIds : [];
const beforeTraces = Array.isArray(before?.traceIds) ? before.traceIds : [];
const newRun = current.runIds.some((id) => !beforeRuns.includes(id));
const newTrace = current.traceIds.some((id) => !beforeTraces.includes(id));
const meaningfulChange = Math.abs(current.textBytes - Number(before?.textBytes || 0)) > 20 && (current.running || current.executionError);
return newRun || newTrace || meaningfulChange;
}, beforeEvidence || {}, { timeout: timeoutMs }).catch(() => null);
const after = await promptSideEffectSnapshot();
const beforeRuns = Array.isArray(beforeEvidence?.runIds) ? beforeEvidence.runIds : [];
const beforeTraces = Array.isArray(beforeEvidence?.traceIds) ? beforeEvidence.traceIds : [];
const newRunIds = after.runIds.filter((id) => !beforeRuns.includes(id));
const newTraceIds = after.traceIds.filter((id) => !beforeTraces.includes(id));
const meaningfulChange = Math.abs(after.textBytes - Number(beforeEvidence?.textBytes || 0)) > 20 && (after.running || after.executionError);
return { ...after, newRunIds, newTraceIds, submitted: newRunIds.length > 0 || newTraceIds.length > 0 || meaningfulChange, valuesRedacted: true };
}
async function promptSideEffectSnapshot() {
return page.evaluate(() => {
const text = document.body?.innerText || "";
return {
runIds: Array.from(new Set(text.match(/run_[A-Za-z0-9_:-]+/gu) || [])).slice(-20),
traceIds: Array.from(new Set(text.match(/trc_[A-Za-z0-9_:-]+/gu) || [])).slice(-20),
running: /Trace running|最近\s*\d+\s*(?:秒|分钟|分|小时)前|Code Agent\s*耗时/iu.test(text),
executionError: /AgentRun error|agentrun:error:|provider-stream-disconnected|provider-unavailable/iu.test(text),
textBytes: new TextEncoder().encode(text).length,
valuesRedacted: true
};
}).catch(() => ({ runIds: [], traceIds: [], running: false, executionError: false, textBytes: 0, valuesRedacted: true }));
}
async function selectProvider(provider) {
const target = String(provider || "").trim();
if (!target) throw new Error("selectProvider requires provider name");
const beforeUrl = currentPageUrl();
const beforePath = safeUrlPath(beforeUrl);
if (!String(beforePath || "").startsWith("/workbench")) throw new Error("selectProvider requires a Workbench page; currentPath=" + (beforePath || "-") + "; run observe command --type goto --path /workbench or --type newSession first");
const nativeSelect = await page.evaluate((name) => {
const normalized = String(name).toLowerCase();
const visible = (element) => {
const rect = element.getBoundingClientRect();
const style = window.getComputedStyle(element);
return rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none";
};
for (const select of Array.from(document.querySelectorAll("select")).filter(visible)) {
const options = Array.from(select.options || []);
const option = options.find((item) => String(item.value || "").toLowerCase().includes(normalized) || String(item.textContent || "").toLowerCase().includes(normalized));
if (!option) continue;
select.value = option.value;
select.dispatchEvent(new Event("input", { bubbles: true }));
select.dispatchEvent(new Event("change", { bubbles: true }));
return { kind: "native-select", value: option.value, text: option.textContent || "" };
}
return null;
}, target).catch(() => null);
if (nativeSelect) {
await page.waitForTimeout(500);
return { beforeUrl, afterUrl: currentPageUrl(), provider: target, selected: nativeSelect, pageId };
}
const optionVisible = page.getByText(target, { exact: false }).last();
if (await optionVisible.isVisible().catch(() => false)) {
await optionVisible.click();
await page.waitForTimeout(500);
return { beforeUrl, afterUrl: currentPageUrl(), provider: target, selected: { kind: "visible-text" }, pageId };
}
const control = page.locator([
'[data-testid*="provider" i]',
'[data-testid*="model" i]',
'[aria-label*="provider" i]',
'[aria-label*="model" i]',
'[aria-label*="模型"]',
'[aria-label*="提供"]',
'[role="combobox"]',
'[aria-haspopup="listbox"]',
].join(", "));
const count = Math.min(await control.count().catch(() => 0), 60);
const attempts = [];
for (let index = count - 1; index >= 0; index -= 1) {
const item = control.nth(index);
const visible = await item.isVisible().catch(() => false);
if (!visible) continue;
const label = await item.evaluate((element) => String(element.getAttribute("aria-label") || element.getAttribute("data-testid") || element.textContent || "").slice(0, 200)).catch(() => "");
if (isProviderNavigationLabel(label)) continue;
if (!/provider|profile|model|模型|提供|codex|openai|moon|api/iu.test(label)) continue;
attempts.push({ index, label });
await item.click({ timeout: 3000 }).catch(() => null);
await page.waitForTimeout(300);
const option = page.getByText(target, { exact: false }).last();
if (await option.isVisible().catch(() => false)) {
await option.click();
await page.waitForTimeout(700);
return { beforeUrl, afterUrl: currentPageUrl(), provider: target, selected: { kind: "opened-control", controlIndex: index, label }, attempts, pageId };
}
await page.keyboard.press("Escape").catch(() => null);
}
const providerCandidates = await collectProviderCandidates();
throw new Error("provider option not found: " + target + "; providerCandidates=" + JSON.stringify(providerCandidates.slice(0, 50)) + "; attempts=" + JSON.stringify(attempts.slice(-10)));
}
async function collectProviderCandidates() {
return page.evaluate(() => {
const rows = [];
const seen = new Set();
const isNavigationLabel = (value) => {
const label = String(value || "").replace(/\s+/gu, " ").trim().toLowerCase();
if (!label) return false;
if (label === "api keys" || label === "kapi keys" || label === "profiles" || label === "rprofiles") return true;
return /^(?:api keys|profiles)$/iu.test(label.replace(/^[a-z]\s*/iu, ""));
};
const visible = (element) => {
const rect = element.getBoundingClientRect();
const style = window.getComputedStyle(element);
return rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none";
};
const push = (kind, element, value, text) => {
const normalizedValue = String(value || "").trim().slice(0, 160);
const normalizedText = String(text || "").replace(/\s+/gu, " ").trim().slice(0, 220);
const label = (normalizedValue + " " + normalizedText).trim();
if (!label) return;
const key = (kind + ":" + label).toLowerCase();
if (seen.has(key)) return;
seen.add(key);
rows.push({ kind, value: normalizedValue, text: normalizedText, testId: String(element?.getAttribute?.("data-testid") || "").slice(0, 120), ariaLabel: String(element?.getAttribute?.("aria-label") || "").slice(0, 160) });
};
for (const select of Array.from(document.querySelectorAll("select")).filter(visible)) {
for (const option of Array.from(select.options || [])) push("select-option", select, option.value, option.textContent || "");
}
const selector = [
'[role="option"]',
'[role="menuitem"]',
'[data-radix-collection-item]',
'[data-testid*="provider" i]',
'[data-testid*="profile" i]',
'[data-testid*="model" i]',
'[aria-label*="provider" i]',
'[aria-label*="profile" i]',
'[aria-label*="model" i]',
'[aria-label*="模型"]',
'[aria-label*="提供"]',
'button'
].join(", ");
for (const element of Array.from(document.querySelectorAll(selector)).filter(visible)) {
const text = String(element.textContent || "").replace(/\s+/gu, " ").trim();
const ariaLabel = String(element.getAttribute("aria-label") || "");
const testId = String(element.getAttribute("data-testid") || "");
const haystack = text + " " + ariaLabel + " " + testId;
if (isNavigationLabel(haystack)) continue;
if (!/provider|profile|model|模型|提供|codex|openai|deepseek|gpt|api|flash|spark|claude|gemini|moon/iu.test(haystack)) continue;
push("visible-control", element, element.getAttribute("value") || "", text || ariaLabel || testId);
}
return rows.slice(0, 80);
}).catch((error) => [{ kind: "candidate-scan-error", value: "", text: String(error?.message || error).slice(0, 240), testId: "", ariaLabel: "" }]);
}
function isProviderNavigationLabel(value) {
const text = String(value || "").replace(/\s+/gu, " ").trim().toLowerCase();
if (!text) return false;
if (text === "api keys" || text === "kapi keys" || text === "profiles" || text === "rprofiles") return true;
return /^(?:api keys|profiles)$/iu.test(text.replace(/^[a-z]\s*/iu, ""));
}
async function clickSession(sessionId) {
if (!sessionId) throw new Error("clickSession requires --session-id or --value");
const beforeUrl = currentPageUrl();
const escaped = cssEscape(sessionId);
const candidate = page.locator("[data-session-id=\"" + escaped + "\"], [href*=\"" + escaped + "\"], text=" + sessionId).first();
await candidate.waitFor({ state: "visible", timeout: 15000 });
await candidate.click();
await page.waitForTimeout(1000);
return { beforeUrl, afterUrl: currentPageUrl(), sessionId, pageId };
}
async function preflightSummary() {
return { currentUrl: currentPageUrl(), title: await page.title().catch(() => null), pageId, auth: publicAuth(auth) };
}
async function samplePage(reason, options = {}) {
if (options?.refreshObserver !== false) await maybeRefreshObserverPage(reason);
const groupSeq = sampleSeq + 1;
if (page && !page.isClosed()) await sampleOnePage(page, { reason, groupSeq, pageRole: "control", targetPageId: pageId });
if (observerPage && !observerPage.isClosed()) {
await sampleOnePage(observerPage, { reason, groupSeq, pageRole: "observer", targetPageId: observerPageId }).catch((error) => appendJsonl(files.errors, eventRecord("observer-sample-error", { pageRole: "observer", pageId: observerPageId, error: errorSummary(error) })));
}
if (options?.screenshot !== false && screenshotIntervalMs > 0 && Date.now() - lastScreenshotAtMs >= screenshotIntervalMs) {
await captureScreenshot("checkpoint", "jpeg").catch((error) => appendJsonl(files.errors, eventRecord("screenshot-error", { pageRole: "control", pageId, error: errorSummary(error) })));
}
await writeHeartbeat({ status: terminalStatus });
}
async function sampleOnePage(targetPage, { reason, groupSeq, pageRole, targetPageId }) {
sampleSeq += 1;
const dom = await targetPage.evaluate(() => {
const trim = (value, limit = 500) => String(value || "").replace(/\s+/g, " ").trim().slice(0, limit);
const visible = (element) => {
const rect = element.getBoundingClientRect();
const style = window.getComputedStyle(element);
return rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none";
};
const textHashInput = (element) => trim(element.textContent || "", 800);
const loadingTextPattern = /加载中|Loading/iu;
const elementTextForLoading = (element) => [
element.textContent || "",
element.getAttribute("aria-label") || "",
element.getAttribute("title") || "",
element.getAttribute("data-testid") || ""
].map((value) => trim(value, 240)).filter(Boolean).join(" ");
const hasLoadingText = (element) => loadingTextPattern.test(elementTextForLoading(element));
const elementDescriptor = (element) => {
if (!element) return null;
const className = String(element.className || "").replace(/\s+/g, " ").trim().split(" ").slice(0, 6).join(" ");
return {
tag: element.tagName.toLowerCase(),
testId: element.getAttribute("data-testid") || null,
role: element.getAttribute("role") || null,
id: element.getAttribute("id") || null,
className: className || null,
status: element.getAttribute("data-status") || element.getAttribute("aria-busy") || null,
sessionId: element.getAttribute("data-session-id") || null,
messageId: element.getAttribute("data-message-id") || null,
traceId: element.getAttribute("data-trace-id") || null,
ariaLabel: element.getAttribute("aria-label") || null
};
};
const ownerKindFor = (element) => {
const value = [
element.getAttribute("data-testid") || "",
element.getAttribute("class") || "",
element.getAttribute("role") || "",
element.tagName || ""
].join(" ").toLowerCase();
if (/message|turn|agent|assistant/.test(value)) return "turn";
if (/session|rail|sidebar|nav/.test(value)) return "session-nav";
if (/composer|prompt|input|textarea/.test(value)) return "composer";
if (/diagnostic|alert|error|warning/.test(value)) return "diagnostic";
if (/trace|event|terminal|log/.test(value)) return "trace";
if (/performance|metric|chart|table/.test(value)) return "performance";
if (/main|workspace|workbench|root/.test(value)) return "workbench";
return "unknown";
};
const ownerLabelFor = (element) => {
const heading = element.querySelector("h1,h2,h3,h4,[data-testid*='title' i],[class*='title' i],[class*='header' i]");
return trim(
element.getAttribute("aria-label")
|| element.getAttribute("data-testid")
|| (heading ? heading.textContent || "" : "")
|| element.getAttribute("class")
|| element.tagName,
160
);
};
const ownerKeyFor = (element) => {
const descriptor = elementDescriptor(element) || {};
return [
ownerKindFor(element),
descriptor.testId || descriptor.id || descriptor.role || descriptor.className || descriptor.tag || "unknown",
descriptor.sessionId || descriptor.messageId || descriptor.traceId || ""
].filter(Boolean).join(":").slice(0, 240);
};
const collectLoadingNodes = () => {
const candidates = Array.from(document.querySelectorAll("body *"))
.filter(visible)
.filter(hasLoadingText)
.filter((element) => !Array.from(element.children).some((child) => visible(child) && hasLoadingText(child)))
.slice(-80);
return candidates.map((element, index) => {
const rect = element.getBoundingClientRect();
const owner = element.closest('[data-message-id], [data-trace-id], article.message-card, .message-card, [data-testid*="message" i], [data-testid*="turn" i], [data-testid*="composer" i], [data-testid*="session" i], [class*="composer" i], [class*="session" i], [class*="trace" i], [class*="diagnostic" i], article, section, aside, main, form, [role="status"], [role="alert"], [role="article"], [role="navigation"]') || element;
const ownerDescriptor = elementDescriptor(owner);
return {
index,
tag: element.tagName.toLowerCase(),
className: String(element.className || "").slice(0, 180),
testId: element.getAttribute("data-testid"),
role: element.getAttribute("role"),
text: trim(elementTextForLoading(element), 240),
ownerKind: ownerKindFor(owner),
ownerKey: ownerKeyFor(owner),
ownerLabel: ownerLabelFor(owner),
owner: ownerDescriptor,
ownerText: trim(owner.textContent || "", 300),
rect: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) },
};
});
};
const sessionIdForElement = (element) => {
const direct = element.getAttribute("data-session-id");
if (direct) return direct;
const href = element.getAttribute("href") || element.closest("a[href]")?.getAttribute("href") || "";
if (!href) return null;
try {
const parsed = new URL(href, location.href);
const match = parsed.pathname.match(/\/(?:workbench|workspace)\/sessions\/([^/?#]+)/u);
return match ? decodeURIComponent(match[1] || "") : null;
} catch {
const match = href.match(/\/(?:workbench|workspace)\/sessions\/([^/?#]+)/u);
return match ? decodeURIComponent(match[1] || "") : null;
}
};
const sessionTitleTextForElement = (element) => {
const titleNode = element.querySelector("[data-testid*='session-title' i], [data-testid*='session-name' i], [class*='session-title' i], [class*='session-name' i], [data-testid*='title' i], [class*='title' i]");
return trim(
(titleNode ? titleNode.textContent || "" : "")
|| element.getAttribute("aria-label")
|| element.getAttribute("title")
|| element.textContent
|| "",
240
);
};
const sessionTitleFallbackPattern = /^(?:Session\s+)?ses_[A-Za-z0-9_.-]+/iu;
const looksLikeSessionTitleFallback = (title, sessionId) => {
const text = trim(title, 240);
const id = String(sessionId || "").trim();
if (!text) return true;
if (sessionTitleFallbackPattern.test(text)) return true;
if (!id) return false;
return text === id || text.startsWith(id) || text.startsWith("Session " + id);
};
const collectSessionRailTitles = () => {
const candidates = Array.from(document.querySelectorAll(".session-tab[data-session-id], [role='tab'][data-session-id], [data-testid*='session' i][data-session-id], a[href*='/workbench/sessions/'], a[href*='/workspace/sessions/']"))
.filter(visible);
const seen = new Set();
const items = [];
for (const element of candidates) {
const sessionId = sessionIdForElement(element);
const titleText = sessionTitleTextForElement(element);
const key = (sessionId || "") + "|" + titleText;
if (seen.has(key)) continue;
seen.add(key);
const rect = element.getBoundingClientRect();
const fallbackTitle = looksLikeSessionTitleFallback(titleText, sessionId);
items.push({
index: items.length,
tag: element.tagName.toLowerCase(),
testId: element.getAttribute("data-testid"),
role: element.getAttribute("role"),
active: element.getAttribute("data-active") === "true" || element.getAttribute("aria-selected") === "true",
sessionId,
sessionIdPrefix: sessionId ? String(sessionId).slice(0, 12) : null,
titleText,
fallbackTitle,
rect: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) },
});
}
const visibleItems = items.slice(0, 120);
const fallbackItems = visibleItems.filter((item) => item.fallbackTitle);
const visibleCount = visibleItems.length;
const fallbackTitleCount = fallbackItems.length;
return {
visibleCount,
fallbackTitleCount,
fallbackTitleRatio: visibleCount > 0 ? Number((fallbackTitleCount / visibleCount).toFixed(4)) : 0,
items: visibleItems.slice(0, 60),
fallbackItems: fallbackItems.slice(0, 12),
};
};
const diagnosticSummaryText = (element) => {
const summarySelectors = [
".api-error-diagnostic-summary-text p",
".api-error-diagnostic-summary-text",
"[class*='diagnostic-summary-text' i] p",
"[class*='diagnostic-summary-text' i]",
"[data-testid*='diagnostic-summary' i]",
"[data-testid*='error-summary' i]",
"[role='alert'] p",
"[role='alert']"
];
const parts = [];
for (const selector of summarySelectors) {
for (const candidate of Array.from(element.querySelectorAll(selector))) {
if (!visible(candidate)) continue;
const text = trim(candidate.textContent || "", 800);
if (text && !parts.includes(text)) parts.push(text);
}
}
const ownText = textHashInput(element);
const text = parts.length > 0 ? parts.join(" ") : ownText;
return text.replace(/\s+(?:!|i诊断|诊断详情)$/u, "").trim();
};
const diagnosticToggleOnly = (element, text) => {
const compact = String(text || "").trim();
if (!/^(?:!|i诊断|诊断|诊断详情)$/u.test(compact)) return false;
const tag = element.tagName.toLowerCase();
const role = element.getAttribute("role");
const aria = element.getAttribute("aria-label") || "";
const title = element.getAttribute("title") || "";
return tag === "button" || role === "button" || /诊断/u.test(aria) || /诊断/u.test(title);
};
const stableMessageText = (element) => {
const bodySelectors = [
".message-markdown.message-text",
".message-text",
"[data-message-body]",
"[data-testid='message-body']",
"[data-testid*='message-text' i]",
"[data-testid*='final-response' i]"
];
const parts = [];
for (const selector of bodySelectors) {
for (const candidate of Array.from(element.querySelectorAll(selector))) {
if (!visible(candidate)) continue;
const text = trim(candidate.textContent || "", 1200);
if (text && !parts.includes(text)) parts.push(text);
}
}
if (parts.length > 0) return parts.join(" ");
const clone = element.cloneNode(true);
for (const selector of [
".message-duration-meta",
".message-activity-meta",
".api-error-diagnostic",
"[class*='diagnostic' i]",
"[class*='trace' i]",
"[data-trace-id]",
"[data-testid*='trace' i]",
"[data-testid*='event' i]",
"[role='status']",
"[role='alert']",
"button"
]) {
for (const child of Array.from(clone.querySelectorAll(selector))) child.remove();
}
return trim(clone.textContent || "", 1200);
};
const numericAttr = (element, names) => {
for (const name of names) {
const raw = element.getAttribute(name);
const value = Number(raw);
if (Number.isFinite(value)) return value;
}
return null;
};
const textAttr = (element, names) => {
for (const name of names) {
const raw = element.getAttribute(name);
if (raw && String(raw).trim()) return String(raw).trim();
}
return null;
};
const summarize = (selector, limit) => Array.from(document.querySelectorAll(selector)).filter(visible).slice(-limit).map((element, index) => {
const rect = element.getBoundingClientRect();
const owner = element.closest('article.message-card, .message-card[data-message-id], article[data-message-id], [data-trace-id]');
const timeElement = element.matches("time,[datetime]") ? element : element.querySelector("time,[datetime]");
return {
index,
tag: element.tagName.toLowerCase(),
testId: element.getAttribute("data-testid"),
role: element.getAttribute("role"),
status: element.getAttribute("data-status") || element.getAttribute("aria-busy") || null,
sessionId: element.getAttribute("data-session-id") || owner?.getAttribute("data-session-id") || null,
messageId: element.getAttribute("data-message-id") || owner?.getAttribute("data-message-id") || element.getAttribute("id") || null,
traceId: element.getAttribute("data-trace-id") || owner?.getAttribute("data-trace-id") || null,
turnId: element.getAttribute("data-turn-id") || owner?.getAttribute("data-turn-id") || null,
projectedSeq: numericAttr(element, ["data-projected-seq", "data-projectedseq", "data-seq", "data-sequence", "aria-posinset"]),
sourceSeq: numericAttr(element, ["data-source-seq", "data-sourceseq", "data-source-event-seq"]),
eventSeq: numericAttr(element, ["data-event-seq", "data-eventseq"]),
eventTimestamp: textAttr(element, ["data-event-ts", "data-event-time", "data-timestamp", "datetime"]) || (timeElement ? textAttr(timeElement, ["datetime", "data-event-ts", "data-event-time", "data-timestamp"]) : null),
eventTimeText: timeElement ? trim(timeElement.textContent || "", 80) : null,
eventKind: textAttr(element, ["data-event-kind", "data-kind", "data-label", "data-status"]) || element.getAttribute("aria-label") || null,
text: textHashInput(element),
rect: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) },
};
});
const summarizeMessages = (selector, limit) => Array.from(document.querySelectorAll(selector)).filter(visible).slice(-limit).map((element, index) => {
const rect = element.getBoundingClientRect();
return {
index,
tag: element.tagName.toLowerCase(),
testId: element.getAttribute("data-testid"),
role: element.getAttribute("role"),
dataRole: element.getAttribute("data-role"),
status: element.getAttribute("data-status") || element.getAttribute("aria-busy") || null,
sessionId: element.getAttribute("data-session-id") || null,
messageId: element.getAttribute("data-message-id") || element.getAttribute("id") || null,
traceId: element.getAttribute("data-trace-id") || null,
turnId: element.getAttribute("data-turn-id") || null,
durationText: trim(element.querySelector(".message-duration-meta")?.textContent || "", 120),
activityText: trim(element.querySelector(".message-activity-meta")?.textContent || "", 120),
text: stableMessageText(element),
rect: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) },
};
});
const resourceTimingSample = (entry) => ({
name: entry.name.split(/[?#]/u)[0].slice(0, 240),
initiatorType: entry.initiatorType,
startTime: Math.round(entry.startTime),
duration: Math.round(entry.duration),
workerStart: Math.round(entry.workerStart || 0),
redirectStart: Math.round(entry.redirectStart || 0),
redirectEnd: Math.round(entry.redirectEnd || 0),
fetchStart: Math.round(entry.fetchStart || 0),
domainLookupStart: Math.round(entry.domainLookupStart || 0),
domainLookupEnd: Math.round(entry.domainLookupEnd || 0),
connectStart: Math.round(entry.connectStart || 0),
connectEnd: Math.round(entry.connectEnd || 0),
secureConnectionStart: Math.round(entry.secureConnectionStart || 0),
requestStart: Math.round(entry.requestStart || 0),
responseStart: Math.round(entry.responseStart || 0),
responseEnd: Math.round(entry.responseEnd || 0),
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,
responseStatus: Number.isFinite(Number(entry.responseStatus)) ? Number(entry.responseStatus) : null,
serverTiming: Array.from(entry.serverTiming || []).slice(0, 8).map((item) => ({
name: String(item.name || "").slice(0, 80),
duration: Number.isFinite(Number(item.duration)) ? Math.round(Number(item.duration)) : null,
description: String(item.description || "").slice(0, 120)
})),
});
const url = location.href;
const routeSessionMatch = url.match(/\/workbench\/sessions\/([^/?#]+)/u);
const activeSession = document.querySelector('[data-active="true"][data-session-id], [aria-selected="true"][data-session-id], .active[data-session-id]');
const activeSessionId = activeSession ? activeSession.getAttribute("data-session-id") : null;
const messageSelector = 'article.message-card, .message-card[data-message-id], article[data-message-id]';
const traceSelector = '[data-testid*="trace" i], [class*="trace" i], [data-trace-id], [data-testid*="event" i]';
const diagnosticSelector = '.api-error-diagnostic, [class*="api-error-diagnostic" i], [class*="message-diagnostic" i], [class*="projection-diagnostic" i], [data-testid="api-error-diagnostic" i], [data-testid="error-diagnostic" i], [data-testid*="diagnostic" i], [role="alert"], [aria-live="assertive"]';
const messages = summarizeMessages(messageSelector, 20);
const traceRows = summarize(traceSelector, 30);
const loadings = collectLoadingNodes();
const sessionRail = collectSessionRailTitles();
const diagnostics = Array.from(document.querySelectorAll(diagnosticSelector)).filter(visible).slice(-40).map((element, index) => {
const rect = element.getBoundingClientRect();
const text = diagnosticSummaryText(element);
if (!text || diagnosticToggleOnly(element, text)) return null;
const traceMatch = text.match(/\b(?:trace_id=)?(trc_[A-Za-z0-9_-]+|[a-f0-9]{16,64})\b/iu);
const httpStatusMatch = text.match(/\bHTTP\s+([1-5][0-9]{2})\b/iu);
const idleMatch = text.match(/\bidle\s+(\d+)s\b/iu);
const waitingForMatch = text.match(/\bwaitingFor=([^\s;,)]+)/iu);
const lastEventLabelMatch = text.match(/\blastEventLabel=([^\s;,)]+)/iu);
const diagnosticCode = httpStatusMatch ? "http-" + httpStatusMatch[1] : /turn\s*超过|无新活动/iu.test(text) ? "turn-idle-no-activity" : /Failed to fetch/iu.test(text) ? "failed-to-fetch" : "diagnostic";
return {
index,
tag: element.tagName.toLowerCase(),
className: String(element.className || "").slice(0, 240),
testId: element.getAttribute("data-testid"),
role: element.getAttribute("role"),
compact: element.getAttribute("data-compact"),
expanded: element.getAttribute("data-expanded") || element.getAttribute("aria-expanded"),
title: element.getAttribute("title"),
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,
text,
rect: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) },
};
}).filter(Boolean);
const turns = Array.from(document.querySelectorAll('article.message-card[data-role="agent"], .message-card[data-role="agent"], article[data-role="agent"]')).filter(visible).map((element, index) => {
const rect = element.getBoundingClientRect();
const text = textHashInput(element);
const traceElement = element.matches("[data-trace-id]") ? element : element.querySelector("[data-trace-id]");
const traceMatch = text.match(/\btrc_[A-Za-z0-9_-]+\b/u);
const durationText = trim(element.querySelector(".message-duration-meta")?.textContent || "", 120);
const activityText = trim(element.querySelector(".message-activity-meta")?.textContent || "", 120);
const directTraceId = element.getAttribute("data-trace-id") || traceElement?.getAttribute("data-trace-id") || traceMatch?.[0] || null;
return {
index,
role: element.getAttribute("data-role") || "agent",
status: element.getAttribute("data-status") || null,
sessionId: element.getAttribute("data-session-id") || null,
messageId: element.getAttribute("data-message-id") || element.getAttribute("id") || null,
traceId: directTraceId,
turnId: element.getAttribute("data-turn-id") || directTraceId,
durationText,
activityText,
text,
rect: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) },
};
}).slice(-20);
const active = document.activeElement;
return {
url,
path: location.pathname,
routeSessionId: routeSessionMatch ? decodeURIComponent(routeSessionMatch[1]) : null,
activeSessionId,
title: document.title,
focus: active ? { tag: active.tagName.toLowerCase(), testId: active.getAttribute("data-testid"), role: active.getAttribute("role") } : null,
viewport: { width: window.innerWidth, height: window.innerHeight, devicePixelRatio: window.devicePixelRatio },
scroll: { x: Math.round(window.scrollX), y: Math.round(window.scrollY), height: Math.round(document.documentElement.scrollHeight), width: Math.round(document.documentElement.scrollWidth) },
messages,
traceRows,
loadings,
sessionRail,
diagnostics,
turns,
pageProvenance: {
url: location.href,
path: location.pathname,
title: document.title,
readyState: document.readyState,
timeOrigin: Math.round(performance.timeOrigin || 0),
navigationStartTime: (performance.getEntriesByType("navigation")[0] || null)?.startTime ?? null,
scripts: Array.from(document.scripts).map((element) => {
if (!element.src) return null;
try {
const url = new URL(element.src, location.href);
const keys = Array.from(url.searchParams.keys()).sort();
return url.pathname + (keys.length > 0 ? "?keys=" + keys.join(",") : "");
} catch {
return null;
}
}).filter(Boolean).sort(),
stylesheets: Array.from(document.querySelectorAll('link[rel~="stylesheet"][href]')).map((element) => {
try {
const url = new URL(element.href, location.href);
const keys = Array.from(url.searchParams.keys()).sort();
return url.pathname + (keys.length > 0 ? "?keys=" + keys.join(",") : "");
} catch {
return null;
}
}).filter(Boolean).sort(),
meta: Array.from(document.querySelectorAll("meta[name], meta[property]")).map((element) => ({
key: String(element.getAttribute("name") || element.getAttribute("property") || "").slice(0, 120),
content: String(element.getAttribute("content") || "").slice(0, 200),
})).filter((item) => item.key).sort((a, b) => a.key.localeCompare(b.key)),
},
performance: performance.getEntriesByType("resource").slice(-80).map(resourceTimingSample),
};
}).catch((error) => ({ error: errorSummary(error), url: pageUrl(targetPage) }));
const sample = {
seq: sampleSeq,
sampleGroupSeq: groupSeq,
ts: new Date().toISOString(),
reason,
pageRole,
pageId: targetPageId,
commandId: activeCommandId,
observerInitiated: false,
...digestDom(dom, pageRole),
};
await appendJsonl(files.samples, sample);
}
function digestDom(dom, pageRole = "control") {
if (dom && dom.error) return dom;
const messages = Array.isArray(dom.messages) ? dom.messages.map((item) => ({ ...item, textHash: sha256Text(item.text || ""), textPreview: truncate(item.text || "", 160), textBytes: Buffer.byteLength(item.text || "") })) : [];
const traceRows = Array.isArray(dom.traceRows) ? dom.traceRows.map((item) => ({ ...item, textHash: sha256Text(item.text || ""), textPreview: truncate(item.text || "", 160), textBytes: Buffer.byteLength(item.text || "") })) : [];
const loadings = Array.isArray(dom.loadings) ? dom.loadings.map((item) => ({ ...item, textHash: sha256Text(item.text || ""), textPreview: truncate(item.text || "", 160), textBytes: Buffer.byteLength(item.text || ""), ownerTextHash: sha256Text(item.ownerText || ""), ownerTextPreview: truncate(item.ownerText || "", 160) })) : [];
const sessionRail = digestSessionRail(dom.sessionRail);
const diagnostics = Array.isArray(dom.diagnostics) ? dom.diagnostics.map((item) => ({ ...item, textHash: sha256Text(item.text || ""), textPreview: truncate(item.text || "", 260), textBytes: Buffer.byteLength(item.text || "") })) : [];
const turns = Array.isArray(dom.turns) ? dom.turns.map((item) => ({ ...item, textHash: sha256Text(item.text || ""), textPreview: truncate(item.text || "", 200), textBytes: Buffer.byteLength(item.text || "") })) : [];
const pageProvenance = normalizePageProvenance(dom.pageProvenance, { reason: "sample", pageLoadSeq: currentPageProvenance?.pageLoadSeq ?? pageLoadSeq });
if (pageRole === "control") currentPageProvenance = pageProvenance;
return { ...dom, messages, traceRows, loadings, sessionRail, diagnostics, turns, pageProvenance: compactPageProvenance(pageProvenance) };
}
function digestSessionRail(value) {
if (!value || typeof value !== "object") return null;
const items = Array.isArray(value.items) ? value.items.map((item) => {
const titleText = String(item?.titleText || item?.titlePreview || "");
return {
index: item?.index ?? null,
tag: item?.tag ?? null,
testId: item?.testId ?? null,
role: item?.role ?? null,
active: item?.active === true,
sessionId: item?.sessionId ?? null,
sessionIdPrefix: item?.sessionIdPrefix ?? (item?.sessionId ? String(item.sessionId).slice(0, 12) : null),
fallbackTitle: item?.fallbackTitle === true,
titleHash: sha256Text(titleText),
titlePreview: truncate(titleText, 160),
titleBytes: Buffer.byteLength(titleText),
rect: item?.rect ?? null,
};
}) : [];
const fallbackItems = items.filter((item) => item.fallbackTitle).slice(0, 12);
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,
};
}
async function captureScreenshot(reason, imageType = "png") {
if (!page || page.isClosed()) throw new Error("page is not available for screenshot");
artifactSeq += 1;
const safeReason = safeId(String(reason || "manual")).slice(0, 40) || "manual";
const type = imageType === "jpeg" || imageType === "jpg" ? "jpeg" : "png";
const ext = type === "jpeg" ? "jpg" : "png";
const file = path.join(dirs.screenshots, String(sampleSeq).padStart(6, "0") + "_" + String(artifactSeq).padStart(4, "0") + "_" + safeReason + "." + ext);
const options = type === "jpeg" ? { path: file, type: "jpeg", quality: 70, fullPage: false } : { path: file, type: "png", fullPage: false };
await page.screenshot(options);
const meta = await fileMeta(file);
const artifact = { seq: artifactSeq, sampleSeq, ts: new Date().toISOString(), kind: "screenshot", reason, path: file, type, byteCount: meta.byteCount, sha256: meta.sha256, pageId, currentUrl: currentPageUrl() };
await appendJsonl(files.artifacts, artifact);
lastScreenshotAtMs = Date.now();
return artifact;
}
function eventRecord(type, data) {
const clean = sanitize(data) || {};
return { ts: new Date().toISOString(), type, jobId, pageId: clean.pageId ?? pageId, pageRole: clean.pageRole ?? "control", sampleSeq, commandId: activeCommandId, ...clean };
}
function controlRecord(command, phase, detail) {
return {
ts: new Date().toISOString(),
seq: commandSeq,
phase,
commandId: command.id,
type: command.type,
source: command.source || "file",
input: commandInputSummary(command),
beforeUrl: command.beforeUrl || null,
afterUrl: currentPageUrl(),
pageId,
detail: sanitize(detail),
};
}
function commandInputSummary(command) {
const text = typeof command.text === "string" ? command.text : null;
return {
type: command.type,
path: command.path || null,
url: command.url ? safeUrl(command.url) : null,
sessionId: command.sessionId || command.value || null,
provider: command.provider || null,
label: command.label ? truncate(command.label, 200) : null,
textHash: text === null ? null : sha256Text(text),
textBytes: text === null ? null : Buffer.byteLength(text),
textPreview: null,
valuesRedacted: true,
};
}
async function appendJsonl(file, value) {
await appendFile(file, JSON.stringify(sanitize(value)) + "\n", { mode: 0o600 });
}
async function fileMeta(file) {
const [buffer, stats] = await Promise.all([readFile(file), stat(file)]);
return { byteCount: stats.size, sha256: "sha256:" + createHash("sha256").update(buffer).digest("hex") };
}
function currentPageUrl() {
return pageUrl(page);
}
function pageUrl(targetPage) {
try { return targetPage && !targetPage.isClosed() ? targetPage.url() : null; } catch { return null; }
}
function routeSessionIdFromUrl(value) {
try {
const pathname = new URL(String(value || ""), baseUrl).pathname;
const match = pathname.match(/\/workbench\/sessions\/([^/?#]+)/u);
return match ? decodeURIComponent(match[1] || "") : null;
} catch {
return null;
}
}
function safeFrameUrl(frame) {
try { return frame ? safeUrl(frame.url()) : null; } catch { return null; }
}
function safeUrl(value) {
try {
const url = new URL(String(value), baseUrl);
for (const key of Array.from(url.searchParams.keys())) {
if (/token|key|secret|password|auth|cookie/iu.test(key)) url.searchParams.set(key, "[redacted]");
}
return url.toString();
} catch {
return truncate(String(value || ""), 300);
}
}
function safeUrlPath(value) {
try {
return new URL(String(value || ""), baseUrl).pathname;
} catch {
return null;
}
}
function normalizeBaseUrl(value) {
const raw = value || "http://127.0.0.1:3000";
const url = new URL(raw);
return url.origin;
}
function parseViewport(value) {
const match = String(value).match(/^(\d{3,5})x(\d{3,5})$/u);
return match ? { width: Number(match[1]), height: Number(match[2]) } : { width: 1440, height: 900 };
}
function positiveInteger(value, fallback) {
const parsed = Number(value);
return Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : fallback;
}
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"),
uncommandedStateChangeCommandWindowMs: requiredPositiveThreshold(raw, "uncommandedStateChangeCommandWindowMs"),
scrollJumpCommandWindowMs: requiredPositiveThreshold(raw, "scrollJumpCommandWindowMs"),
scrollJumpFromY: requiredPositiveThreshold(raw, "scrollJumpFromY"),
scrollJumpToY: requiredPositiveThreshold(raw, "scrollJumpToY"),
sessionRailFallbackRatio,
source: "yaml-env",
};
}
function sha256Text(value) {
return "sha256:" + createHash("sha256").update(String(value)).digest("hex");
}
function safeId(value) {
return String(value || "").replace(/[^A-Za-z0-9_.-]+/gu, "-").replace(/^-+|-+$/gu, "").slice(0, 120) || "item";
}
function cssEscape(value) {
return String(value).replace(/\\/gu, "\\\\").replace(/"/gu, "\\\"");
}
function truncate(value, limit) {
const text = String(value || "");
return text.length > limit ? text.slice(0, limit) + "..." : text;
}
function sanitize(value) {
if (value === null || value === undefined) return value;
if (typeof value === "string") return value === password ? "[redacted]" : value.replaceAll(password, "[redacted]");
if (typeof value === "number" || typeof value === "boolean") return value;
if (Array.isArray(value)) return value.map(sanitize);
if (typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, item]) => /password|cookie|authorization|token|secret/iu.test(key) ? [key, "[redacted]"] : [key, sanitize(item)]));
return String(value);
}
function errorSummary(error) {
const summary = { name: error && error.name ? error.name : "Error", message: error && error.message ? truncate(error.message, 1000) : truncate(String(error), 1000), stackTail: error && error.stack ? truncate(error.stack, 2000) : null };
if (error && error.webProbeAuth) summary.auth = sanitize(error.webProbeAuth);
if (error && Array.isArray(error.attempts)) summary.attempts = sanitize(error.attempts.slice(-5));
if (error && error.navigationReadiness) summary.navigationReadiness = sanitize(error.navigationReadiness);
if (error && error.details) summary.details = sanitize(error.details);
if (error && error.target) summary.target = sanitize(error.target);
return summary;
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
}
`;
}
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 parsed = Number(process.env.UNIDESK_WEB_OBSERVE_ANALYZE_TAIL_SAMPLES); return Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : 360; })();
const alertThresholds = parseAlertThresholds(process.env.UNIDESK_WEB_OBSERVE_ALERT_THRESHOLDS_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 });
const samples = analyzeTailSamples > 0 ? sourceSamples.slice(-analyzeTailSamples) : sourceSamples;
const sampleWindow = sampleTimeWindow(samples, 60_000);
const control = filterRowsByTimeWindow(await readJsonl(dataFile("control.jsonl")), sampleWindow);
const network = filterRowsByTimeWindow(await readJsonl(dataFile("network.jsonl")), sampleWindow);
const consoleEvents = filterRowsByTimeWindow(await readJsonl(dataFile("console.jsonl")), sampleWindow);
const errors = filterRowsByTimeWindow(await readJsonl(dataFile("errors.jsonl")), sampleWindow);
const artifacts = filterRowsByTimeWindow(await readJsonl(dataFile("artifacts.jsonl")), sampleWindow);
const manifest = await readJson(path.join(stateDir, "manifest.json"));
const heartbeat = await readJson(path.join(stateDir, "heartbeat.json"));
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, network);
const runtimeAlerts = buildRuntimeAlerts(samples, control, network, consoleEvents, errors);
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 findings = buildFindings(samples, control, network, errors, sampleMetrics, promptNetwork, runtimeAlerts, pagePerformance, pageProvenance, commandFailures);
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, sampleWindow, 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,
promptNetwork,
runtimeAlerts,
runnerErrors,
commandFailures,
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,
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, 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,
promptNetwork: recentWindow.promptNetwork.summary,
runtimeAlerts: recentWindow.runtimeAlerts.summary,
runnerErrors,
commandFailures: commandFailures.slice(-8),
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,
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,
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,
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; }
}
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"),
uncommandedStateChangeCommandWindowMs: requiredPositiveThreshold(raw, "uncommandedStateChangeCommandWindowMs"),
scrollJumpCommandWindowMs: requiredPositiveThreshold(raw, "scrollJumpCommandWindowMs"),
scrollJumpFromY: requiredPositiveThreshold(raw, "scrollJumpFromY"),
scrollJumpToY: requiredPositiveThreshold(raw, "scrollJumpToY"),
sessionRailFallbackRatio,
source: "yaml-env",
};
}
async function readJsonl(file, 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 [];
}
}
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 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),
pageProvenance: compactSamplePageProvenance(sample.pageProvenance),
performance: compactPerformanceItems(sample.performance)
};
}
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 buildFindings(samples, control, network, errors, sampleMetrics, promptNetwork, runtimeAlerts, pagePerformance, pageProvenance, commandFailures = []) {
const findings = [];
if (commandFailures.length > 0) findings.push({ id: "observer-command-failed", severity: "red", summary: "observer control commands failed; analyze must surface command failure instead of hiding it in command artifacts", count: commandFailures.length, commands: commandFailures.slice(0, 20) });
const commandTimes = control.filter((item) => item.phase === "completed" || item.phase === "started").map((item) => Date.parse(item.ts)).filter(Number.isFinite);
const routeSessions = new Set(samples.map((item) => item.routeSessionId).filter(Boolean));
const activeSessions = new Set(samples.map((item) => item.activeSessionId).filter(Boolean));
if (routeSessions.size > 1) findings.push({ id: "session-route-changed", severity: "amber", summary: "route session changed during observation", routeSessionCount: routeSessions.size, samples: sampleRefs(samples, (item) => item.routeSessionId) });
if (activeSessions.size > 1) findings.push({ id: "active-session-changed", severity: "amber", summary: "active session changed during observation", activeSessionCount: activeSessions.size, samples: sampleRefs(samples, (item) => item.activeSessionId) });
const mismatches = samples.filter((item) => item.routeSessionId && item.activeSessionId && item.routeSessionId !== item.activeSessionId);
if (mismatches.length > 0) findings.push({ id: "route-active-session-mismatch", severity: "red", summary: "routeSessionId and activeSessionId diverged", count: mismatches.length, samples: mismatches.slice(0, 10).map(ref) });
const uncommandedChanges = [];
const commandedPromptSeqs = new Set((sampleMetrics?.timeline ?? []).filter((item) => Number(item?.promptIndex) > 0).map((item) => item.seq).filter((seq) => seq !== null && seq !== undefined));
for (let i = 1; i < samples.length; i += 1) {
const prev = digestSample(samples[i - 1]);
const next = digestSample(samples[i]);
if (prev !== next && !commandedPromptSeqs.has(samples[i]?.seq) && !nearCommand(samples[i], commandTimes, alertThresholds.uncommandedStateChangeCommandWindowMs)) uncommandedChanges.push(ref(samples[i]));
}
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: "red", summary: "terminal Code Agent card displayed 耗时 0 秒; terminal duration must come from durable timing projection, not a missing/zero display fallback", 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: "red", summary: "visible Code Agent card did not display total elapsed time; elapsed must be visible for running and terminal cards", 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: "red", summary: "non-terminal Code Agent card did not display 最近更新", 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: "red",
summary: "completed Code Agent card total elapsed is shorter than trace/final-response duration evidence",
count: cardTimingSummary.durationUnderreportedCount,
samples: (cardTiming.durationUnderreported || []).slice(0, 20),
});
}
if (Number(cardTimingSummary.durationMismatchCount ?? 0) > 0) {
findings.push({
id: "code-agent-card-duration-mismatch",
severity: "red",
summary: "completed Code Agent card total elapsed does not match sealed completion/final-response timing evidence",
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: "red", summary: "Trace row 轮次完成(总耗时 ... does not match the visible Code Agent card total elapsed time within YAML timing slack", 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: "red", summary: "After 轮次完成, card total elapsed or 最近更新 continued changing; terminal timing should be sealed", 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 POST response in the sampling window", count: promptFailures.length, rounds: promptFailures.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: "red", summary: "Code Agent total elapsed jumped from a non-zero value back to 0 seconds", count: elapsedZeroResets.length, samples: elapsedZeroResets.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: "red", summary: "Code Agent total elapsed jumped forward faster than browser sample interval", 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: "red", summary: "terminal Code Agent card total elapsed changed after terminal status; completed/failed/canceled timing must be sealed", 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 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");
if (crossPageProjectionDiffs.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", count: crossPageProjectionDiffs.length, samples: crossPageProjectionDiffs.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 rendered assistant message rows that duplicate the sealed final response", count: traceMessageDuplicates.length, 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" && /\/v1\/agent\/chat(?:\?|$)/u.test(String(item?.url || "")))
.map((item) => {
const failureText = item.failureKind ?? item.failure ?? item.errorText ?? null;
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: urlPath(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 chatPostOk = responseStatuses.some((status) => status >= 200 && status < 300) && failures.length === 0;
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,
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, 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 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 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)
};
if (baseline) baselineExecutionErrors.push(event);
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));
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) {
if (!["/v1/workbench/events", "/v1/web-performance"].includes(String(event?.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|net::ERR_NETWORK_CHANGED|net::ERR_ABORTED|aborted|network changed/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 === "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 = control
.filter((item) => item.type === "sendPrompt" && item.phase === "completed")
.map((item) => ({ ts: item.ts, tsMs: Date.parse(item.ts), commandId: item.commandId ?? null, textHash: item.input?.textHash ?? null, textBytes: item.input?.textBytes ?? null }))
.filter((item) => Number.isFinite(item.tsMs))
.sort((a, b) => a.tsMs - b.tsMs);
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 rounds = buildRoundMetricSummaries(timeline, promptCommands, {
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: turnTiming.rows,
turnTimingNonMonotonic,
turnTimingElapsedZeroResets,
turnTimingTotalElapsedForwardJumps,
turnTimingTerminalElapsedGrowth,
turnTimingRecentUpdateSawtoothJumps,
turnTimingRecentUpdateSteps,
turnTimingRecentUpdateLargestSteps,
turnTimingRecentUpdateResets,
timeline
};
}
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 existing = groups.get(ownerKey) || {
ownerKey,
ownerKind: item?.ownerKind ?? "unknown",
ownerLabel: loadingOwnerLabel(item, ownerKey),
count: 0,
textHashes: []
};
existing.count += 1;
if (item?.textHash && !existing.textHashes.includes(item.textHash)) existing.textHashes.push(item.textHash);
groups.set(ownerKey, existing);
}
return Array.from(groups.values()).sort((a, b) => b.count - a.count || String(a.ownerLabel).localeCompare(String(b.ownerLabel)));
}
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,
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;
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,
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, 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 buildTurnTimingTable(samples, timeline) {
const columns = [];
const registry = new Map();
const rows = [];
for (let index = 0; index < samples.length; index += 1) {
const sample = samples[index];
const timelineItem = timeline[index] || {};
const cells = {};
for (const metric of turnMetricItems(sample, timelineItem)) {
let column = registry.get(metric.key);
if (!column) {
column = {
id: "T" + String(columns.length + 1),
label: "T" + String(columns.length + 1),
keyHash: sha256(metric.key),
source: metric.source,
firstSeq: sample.seq ?? null,
firstTs: sample.ts ?? null,
lastSeq: sample.seq ?? null,
lastTs: sample.ts ?? null,
promptIndex: metric.promptIndex ?? null,
lastPromptIndex: metric.promptIndex ?? null,
traceId: metric.traceId ?? null,
messageId: metric.messageId ?? null,
domIndex: metric.domIndex ?? null,
pageRole: metric.pageRole ?? sample.pageRole ?? null,
pageId: metric.pageId ?? sample.pageId ?? null
};
registry.set(metric.key, column);
columns.push(column);
} else {
column.lastSeq = sample.seq ?? null;
column.lastTs = sample.ts ?? null;
column.lastPromptIndex = metric.promptIndex ?? column.lastPromptIndex ?? null;
if (!column.traceId && metric.traceId) column.traceId = metric.traceId;
if (!column.messageId && metric.messageId) column.messageId = metric.messageId;
}
cells[column.id] = {
totalElapsedSeconds: metric.totalElapsedSeconds,
recentUpdateSeconds: metric.recentUpdateSeconds,
status: metric.status ?? null,
promptIndex: metric.promptIndex ?? null,
source: metric.source,
pageRole: metric.pageRole ?? sample.pageRole ?? null,
pageId: metric.pageId ?? sample.pageId ?? null,
sampleGroupSeq: sample.sampleGroupSeq ?? null,
traceId: metric.traceId ?? null,
messageId: metric.messageId ?? null,
textHash: metric.textHash ?? null
};
}
rows.push({
ts: sample.ts ?? null,
seq: sample.seq ?? null,
sampleGroupSeq: sample.sampleGroupSeq ?? null,
pageRole: sample.pageRole ?? null,
pageId: sample.pageId ?? null,
promptIndex: timelineItem.promptIndex ?? 0,
routeSessionId: sample.routeSessionId ?? null,
activeSessionId: sample.activeSessionId ?? null,
cells
});
}
const timingEvents = detectTurnTimingNonMonotonic(columns, rows);
return {
columns,
rows,
nonMonotonic: timingEvents.anomalies,
elapsedZeroResets: timingEvents.elapsedZeroResets,
totalElapsedForwardJumps: timingEvents.totalElapsedForwardJumps,
terminalElapsedGrowth: timingEvents.terminalElapsedGrowth,
recentUpdateResets: timingEvents.recentUpdateResets,
recentUpdateSteps: timingEvents.recentUpdateSteps
};
}
function detectTurnTimingNonMonotonic(columns, rows) {
const anomalies = [];
const elapsedZeroResets = [];
const totalElapsedForwardJumps = [];
const terminalElapsedGrowth = [];
const recentUpdateResets = [];
const recentUpdateSteps = [];
for (const column of columns) {
const previousByMetric = new Map();
let previousTerminalTotal = null;
for (const row of rows) {
const cell = row.cells?.[column.id];
if (!cell) continue;
for (const metric of ["totalElapsedSeconds", "recentUpdateSeconds"]) {
const value = cell[metric];
if (value === null || value === undefined || !Number.isFinite(Number(value))) continue;
const current = Number(value);
const previous = previousByMetric.get(metric);
if (previous && metric === "totalElapsedSeconds" && current < previous.value) {
const anomaly = previous.value > 0 && current === 0 ? "zero-reset" : "decrease";
const event = {
columnId: column.id,
columnLabel: column.label,
metric,
anomaly,
expectedPattern: anomaly === "zero-reset" ? "total-elapsed-should-not-return-to-zero" : "total-elapsed-monotonic",
fromSeq: previous.seq,
fromTs: previous.ts,
fromValue: previous.value,
toSeq: row.seq ?? null,
toTs: row.ts ?? null,
toValue: current,
delta: current - previous.value,
traceId: cell.traceId ?? column.traceId ?? null,
messageId: cell.messageId ?? column.messageId ?? null,
promptIndex: cell.promptIndex ?? row.promptIndex ?? null,
source: cell.source ?? column.source ?? null,
pageRole: cell.pageRole ?? column.pageRole ?? null,
pageId: cell.pageId ?? column.pageId ?? null,
valuesRedacted: true
};
anomalies.push(event);
if (anomaly === "zero-reset") elapsedZeroResets.push(event);
}
if (previous && metric === "totalElapsedSeconds" && current > previous.value) {
const sampleDeltaSeconds = elapsedSecondsBetween(previous.ts, row.ts);
const delta = current - previous.value;
const allowedIncreaseSeconds = sampleDeltaSeconds + alertThresholds.turnTimingSampleSlackSeconds;
if (delta > allowedIncreaseSeconds) {
totalElapsedForwardJumps.push({
columnId: column.id,
columnLabel: column.label,
metric,
anomaly: "forward-jump",
expectedPattern: "total-elapsed-increase-should-match-browser-sample-interval",
fromSeq: previous.seq,
fromTs: previous.ts,
fromValue: previous.value,
toSeq: row.seq ?? null,
toTs: row.ts ?? null,
toValue: current,
delta,
sampleDeltaSeconds,
allowedIncreaseSeconds,
excessiveIncreaseSeconds: Number((delta - allowedIncreaseSeconds).toFixed(3)),
traceId: cell.traceId ?? column.traceId ?? null,
messageId: cell.messageId ?? column.messageId ?? null,
promptIndex: cell.promptIndex ?? row.promptIndex ?? null,
source: cell.source ?? column.source ?? null,
pageRole: cell.pageRole ?? column.pageRole ?? null,
pageId: cell.pageId ?? column.pageId ?? null,
valuesRedacted: true
});
}
}
if (metric === "totalElapsedSeconds" && isTerminalTurnStatus(cell.status)) {
if (previousTerminalTotal && current > previousTerminalTotal.value) {
terminalElapsedGrowth.push({
columnId: column.id,
columnLabel: column.label,
metric,
anomaly: "terminal-growth",
expectedPattern: "terminal-total-elapsed-sealed",
fromSeq: previousTerminalTotal.seq,
fromTs: previousTerminalTotal.ts,
fromValue: previousTerminalTotal.value,
fromStatus: previousTerminalTotal.status,
toSeq: row.seq ?? null,
toTs: row.ts ?? null,
toValue: current,
toStatus: cell.status ?? null,
delta: current - previousTerminalTotal.value,
traceId: cell.traceId ?? column.traceId ?? null,
messageId: cell.messageId ?? column.messageId ?? null,
promptIndex: cell.promptIndex ?? row.promptIndex ?? null,
source: cell.source ?? column.source ?? null,
pageRole: cell.pageRole ?? column.pageRole ?? null,
pageId: cell.pageId ?? column.pageId ?? null,
valuesRedacted: true
});
}
previousTerminalTotal = { value: current, seq: row.seq ?? null, ts: row.ts ?? null, status: cell.status ?? null };
}
if (previous && metric === "recentUpdateSeconds") {
const elapsedMs = Date.parse(String(row.ts ?? "")) - Date.parse(String(previous.ts ?? ""));
const elapsedSeconds = Number.isFinite(elapsedMs) && elapsedMs >= 0 ? elapsedMs / 1000 : null;
const increase = current - previous.value;
const allowedIncrease = elapsedSeconds === null
? alertThresholds.turnTimingSampleSlackSeconds
: Math.max(alertThresholds.turnTimingSampleSlackSeconds, elapsedSeconds + alertThresholds.turnTimingSampleSlackSeconds);
const excessiveIncrease = increase > allowedIncrease ? increase - allowedIncrease : 0;
recentUpdateSteps.push({
columnId: column.id,
columnLabel: column.label,
metric: "recentUpdateSeconds",
event: increase < 0 ? "reset" : excessiveIncrease > 0 ? "jump" : "increase",
expectedPattern: "sawtooth-increase-or-reset",
fromSeq: previous.seq,
fromTs: previous.ts,
fromValue: previous.value,
toSeq: row.seq ?? null,
toTs: row.ts ?? null,
toValue: current,
delta: increase,
sampleDeltaSeconds: elapsedSeconds,
allowedIncreaseSeconds: allowedIncrease,
excessiveIncreaseSeconds: excessiveIncrease,
traceId: cell.traceId ?? column.traceId ?? null,
messageId: cell.messageId ?? column.messageId ?? null,
promptIndex: cell.promptIndex ?? row.promptIndex ?? null,
source: cell.source ?? column.source ?? null,
pageRole: cell.pageRole ?? column.pageRole ?? null,
pageId: cell.pageId ?? column.pageId ?? null,
valuesRedacted: true
});
if (increase < 0) {
recentUpdateResets.push({
columnId: column.id,
columnLabel: column.label,
metric: "recentUpdateSeconds",
event: "reset",
fromSeq: previous.seq,
fromTs: previous.ts,
fromValue: previous.value,
toSeq: row.seq ?? null,
toTs: row.ts ?? null,
toValue: current,
delta: increase,
sampleDeltaSeconds: elapsedSeconds,
traceId: cell.traceId ?? column.traceId ?? null,
messageId: cell.messageId ?? column.messageId ?? null,
promptIndex: cell.promptIndex ?? row.promptIndex ?? null,
source: cell.source ?? column.source ?? null,
pageRole: cell.pageRole ?? column.pageRole ?? null,
pageId: cell.pageId ?? column.pageId ?? null,
valuesRedacted: true
});
}
if (excessiveIncrease > 0) {
anomalies.push({
columnId: column.id,
columnLabel: column.label,
metric: "recentUpdateSeconds",
anomaly: "jump",
expectedPattern: "sawtooth-increase-or-reset",
fromSeq: previous.seq,
fromTs: previous.ts,
fromValue: previous.value,
toSeq: row.seq ?? null,
toTs: row.ts ?? null,
toValue: current,
delta: increase,
sampleDeltaSeconds: elapsedSeconds,
allowedIncreaseSeconds: allowedIncrease,
excessiveIncreaseSeconds: excessiveIncrease,
traceId: cell.traceId ?? column.traceId ?? null,
messageId: cell.messageId ?? column.messageId ?? null,
promptIndex: cell.promptIndex ?? row.promptIndex ?? null,
source: cell.source ?? column.source ?? null,
pageRole: cell.pageRole ?? column.pageRole ?? null,
pageId: cell.pageId ?? column.pageId ?? null,
valuesRedacted: true
});
}
}
previousByMetric.set(metric, { value: current, seq: row.seq ?? null, ts: row.ts ?? null });
}
}
}
return { anomalies, elapsedZeroResets, totalElapsedForwardJumps, terminalElapsedGrowth, recentUpdateResets, recentUpdateSteps };
}
function elapsedSecondsBetween(fromTs, toTs) {
const from = Date.parse(fromTs);
const to = Date.parse(toTs);
if (!Number.isFinite(from) || !Number.isFinite(to) || to < from) return 0;
return Number(((to - from) / 1000).toFixed(3));
}
function maxPositiveDelta(items) {
const values = (Array.isArray(items) ? items : [])
.map((item) => Number(item.delta))
.filter((value) => Number.isFinite(value) && value > 0);
return values.length > 0 ? Math.max(...values) : 0;
}
function isTerminalTurnStatus(value) {
const status = String(value ?? "").trim().toLowerCase().replace(/_/gu, "-");
return ["completed", "failed", "blocked", "timeout", "canceled", "cancelled", "stale", "thread-resume-failed"].includes(status);
}
function buildTraceOrderMetrics(samples, timeline) {
const rows = [];
const orderAnomalies = [];
const completionNotLast = [];
const groups = new Map();
for (let sampleIndex = 0; sampleIndex < samples.length; sampleIndex += 1) {
const sample = samples[sampleIndex] || {};
const sampleRows = traceTimingRowsForSample(sample, timeline[sampleIndex] || {});
for (const row of sampleRows) {
const normalized = {
...row,
sampleIndex,
sampleSeq: sample.seq ?? null,
timestamp: sample.ts || sample.timestamp || sample.collectedAt || sample.time || null,
pageRole: row.pageRole || sample.pageRole || sample.role || sample.contextRole || null,
pageId: row.pageId || sample.pageId || sample.contextId || null,
sessionId: row.sessionId || sample.sessionId || sample.workbenchSessionId || null,
};
rows.push(normalized);
const key = traceRowGroupKey(normalized);
if (!groups.has(key)) groups.set(key, []);
groups.get(key).push(normalized);
}
}
const slackSeconds = Math.max(1, Number(alertThresholds?.turnTimingSampleSlackSeconds || 0));
for (const groupRows of groups.values()) {
const sorted = groupRows.slice().sort((a, b) => {
if (a.sampleIndex !== b.sampleIndex) return a.sampleIndex - b.sampleIndex;
return (a.rowIndex ?? 0) - (b.rowIndex ?? 0);
});
let previous = null;
for (const row of sorted) {
if (previous) {
const reasons = [];
if (Number.isFinite(previous.totalSeconds) && Number.isFinite(row.totalSeconds) && row.totalSeconds + slackSeconds < previous.totalSeconds) {
reasons.push('total-decreased');
}
if (Number.isFinite(previous.projectedSeq) && Number.isFinite(row.projectedSeq) && row.projectedSeq < previous.projectedSeq) {
reasons.push('projected-seq-decreased');
}
if (Number.isFinite(previous.clockSeconds) && Number.isFinite(row.clockSeconds)) {
const diff = previous.clockSeconds - row.clockSeconds;
if (diff > slackSeconds && diff < 43200) reasons.push('clock-decreased');
}
if (Number.isFinite(previous.timestampMs) && Number.isFinite(row.timestampMs) && row.timestampMs + slackSeconds * 1000 < previous.timestampMs) {
reasons.push('timestamp-decreased');
}
if (reasons.length) {
orderAnomalies.push({
sampleIndex: row.sampleIndex,
sampleSeq: row.sampleSeq,
timestamp: row.timestamp,
pageRole: row.pageRole,
pageId: row.pageId,
sessionId: row.sessionId,
traceId: row.traceId || previous.traceId || null,
previousRowIndex: previous.rowIndex,
currentRowIndex: row.rowIndex,
reasons,
previousTotalSeconds: previous.totalSeconds,
currentTotalSeconds: row.totalSeconds,
previousProjectedSeq: previous.projectedSeq,
currentProjectedSeq: row.projectedSeq,
previousSourceSeq: previous.sourceSeq,
currentSourceSeq: row.sourceSeq,
previousEventSeq: previous.eventSeq,
currentEventSeq: row.eventSeq,
previousClockSeconds: previous.clockSeconds,
currentClockSeconds: row.clockSeconds,
previousTimestampMs: previous.timestampMs,
currentTimestampMs: row.timestampMs,
previousPreview: previous.preview,
currentPreview: row.preview,
});
}
}
previous = row;
}
for (let index = 0; index < sorted.length; index += 1) {
const row = sorted[index];
if (!row.isCompletion) continue;
const later = sorted.slice(index + 1).find((candidate) => {
if (candidate.isCompletion && candidate.preview === row.preview) return false;
return Number.isFinite(candidate.totalSeconds) || Number.isFinite(candidate.clockSeconds) || Number.isFinite(candidate.projectedSeq);
});
if (later) {
completionNotLast.push({
sampleIndex: row.sampleIndex,
sampleSeq: row.sampleSeq,
timestamp: row.timestamp,
pageRole: row.pageRole,
pageId: row.pageId,
sessionId: row.sessionId,
traceId: row.traceId || later.traceId || null,
completionRowIndex: row.rowIndex,
laterRowIndex: later.rowIndex,
completionTotalSeconds: row.totalSeconds,
laterTotalSeconds: later.totalSeconds,
completionProjectedSeq: row.projectedSeq,
laterProjectedSeq: later.projectedSeq,
completionSourceSeq: row.sourceSeq,
laterSourceSeq: later.sourceSeq,
completionEventSeq: row.eventSeq,
laterEventSeq: later.eventSeq,
completionPreview: row.preview,
laterPreview: later.preview,
});
}
}
}
return {
summary: {
sampleCount: samples.length,
traceRowCount: rows.length,
orderAnomalyCount: orderAnomalies.length,
completionNotLastCount: completionNotLast.length,
},
rows: rows.slice(0, 200),
orderAnomalies: orderAnomalies.slice(0, 100),
completionNotLast: completionNotLast.slice(0, 100),
};
}
function buildCodeAgentCardDurationUnderreportedMetrics(samples, timeline) {
const findings = [];
const slackSeconds = Math.max(5, Number(alertThresholds?.turnTimingSampleSlackSeconds || 0));
for (let sampleIndex = 0; sampleIndex < samples.length; sampleIndex += 1) {
const sample = samples[sampleIndex] || {};
const cards = codeAgentCardsForSample(sample);
if (!cards.length) continue;
const traceRows = traceTimingRowsForSample(sample, timeline[sampleIndex] || {});
const terminalCards = cards.filter((card) => isCodeAgentCardTerminal(card));
const sampleText = sampleVisibleText(sample, timeline[sampleIndex] || {});
for (const card of terminalCards) {
const cardText = codeAgentCardText(card);
const parsedCardSeconds = parseTotalElapsedSeconds(cardText).filter(Number.isFinite);
const cardSeconds = Number.isFinite(Number(card.totalElapsedSeconds)) ? Number(card.totalElapsedSeconds) : parsedCardSeconds.length > 0 ? Math.max(...parsedCardSeconds) : NaN;
if (!Number.isFinite(cardSeconds)) continue;
const traceMatched = traceRows.filter((row) => traceRowMatchesCard(row, card, terminalCards.length));
const traceEvidence = maxTraceDurationEvidence(traceMatched);
const textEvidence = maxSelfReportedDurationEvidence([card.text, card.preview, card.finalResponseText, card.runningRecordText, terminalCards.length === 1 ? sampleText : ''].filter(Boolean).join('\n'));
const evidences = [traceEvidence, textEvidence].filter((item) => item && Number.isFinite(item.seconds));
if (!evidences.length) continue;
const strongest = evidences.sort((a, b) => b.seconds - a.seconds)[0];
const tolerance = Math.max(slackSeconds, Math.ceil(strongest.seconds * 0.05));
if (strongest.seconds > cardSeconds + tolerance) {
findings.push({
sampleIndex,
timestamp: sample.timestamp || sample.collectedAt || sample.time || null,
pageRole: card.pageRole || sample.pageRole || sample.role || sample.contextRole || null,
pageId: card.pageId || sample.pageId || sample.contextId || null,
sessionId: card.sessionId || sample.sessionId || sample.workbenchSessionId || null,
traceId: card.traceId || strongest.traceId || null,
status: card.status || card.state || card.phase || null,
cardTotalElapsedSeconds: cardSeconds,
expectedElapsedSeconds: strongest.seconds,
deltaSeconds: strongest.seconds - cardSeconds,
toleranceSeconds: tolerance,
evidenceKind: strongest.kind,
evidencePreview: strongest.preview,
cardPreview: card.preview || compactOneLine(cardText || ''),
});
}
}
}
return findings.slice(0, 100);
}
function buildCodeAgentCardDurationMismatchMetrics(samples, timeline) {
const findings = [];
const slackSeconds = Math.max(5, Number(alertThresholds?.turnTimingSampleSlackSeconds || 0));
for (let sampleIndex = 0; sampleIndex < samples.length; sampleIndex += 1) {
const sample = samples[sampleIndex] || {};
const cards = codeAgentCardsForSample(sample);
if (!cards.length) continue;
const traceRows = traceTimingRowsForSample(sample, timeline[sampleIndex] || {});
const terminalCards = cards.filter((card) => isCodeAgentCardTerminal(card));
const sampleText = sampleVisibleText(sample, timeline[sampleIndex] || {});
for (const card of terminalCards) {
const cardText = codeAgentCardText(card);
const parsedCardSeconds = parseTotalElapsedSeconds(cardText).filter(Number.isFinite);
const cardSeconds = Number.isFinite(Number(card.totalElapsedSeconds)) ? Number(card.totalElapsedSeconds) : parsedCardSeconds.length > 0 ? Math.max(...parsedCardSeconds) : NaN;
if (!Number.isFinite(cardSeconds)) continue;
const traceMatched = traceRows.filter((row) => traceRowMatchesCard(row, card, terminalCards.length));
const traceEvidence = maxTraceDurationEvidence(traceMatched);
const textEvidence = maxSelfReportedDurationEvidence([card.text, card.preview, card.finalResponseText, card.runningRecordText, terminalCards.length === 1 ? sampleText : ''].filter(Boolean).join('\n'));
const evidences = [traceEvidence, textEvidence].filter((item) => item && Number.isFinite(item.seconds));
if (!evidences.length) continue;
const strongest = evidences.sort((a, b) => b.seconds - a.seconds)[0];
const exactEvidence = strongest.exact === true || strongest.kind === 'trace-completion-total' || strongest.kind === 'final-response-duration';
const tolerance = Math.max(slackSeconds, Math.ceil(strongest.seconds * 0.05));
const signedDelta = Number((cardSeconds - strongest.seconds).toFixed(3));
const absoluteDelta = Math.abs(signedDelta);
const underreported = strongest.seconds > cardSeconds + tolerance;
const overreported = exactEvidence && cardSeconds > strongest.seconds + tolerance;
if (!underreported && !overreported) continue;
findings.push({
sampleIndex,
timestamp: sample.timestamp || sample.collectedAt || sample.time || sample.ts || null,
pageRole: card.pageRole || sample.pageRole || sample.role || sample.contextRole || null,
pageId: card.pageId || sample.pageId || sample.contextId || null,
sessionId: card.sessionId || sample.sessionId || sample.workbenchSessionId || null,
traceId: card.traceId || strongest.traceId || null,
status: card.status || card.state || card.phase || null,
direction: underreported ? 'underreported' : 'overreported',
cardTotalElapsedSeconds: cardSeconds,
expectedElapsedSeconds: strongest.seconds,
signedDeltaSeconds: signedDelta,
deltaSeconds: Number(absoluteDelta.toFixed(3)),
toleranceSeconds: tolerance,
evidenceKind: strongest.kind,
exactEvidence,
evidencePreview: strongest.preview,
cardPreview: card.preview || compactOneLine(cardText || ''),
});
}
}
return findings.slice(0, 100);
}
function traceTimingRowsForSample(sample, timelineItem) {
const rows = [];
const seen = new Set();
const appendRowsFromText = (text, source, baseIndex, meta = {}) => {
for (const extracted of extractTraceRowsFromText(text, source, baseIndex, meta)) {
const key = [extracted.pageRole || '', extracted.pageId || '', extracted.rowIndex, extracted.preview].join('|');
if (seen.has(key)) continue;
seen.add(key);
rows.push(extracted);
}
};
for (const candidate of traceRowCandidateArrays(sample, timelineItem)) {
const array = Array.isArray(candidate.rows) ? candidate.rows : [];
array.forEach((item, index) => {
if (typeof item === 'string') {
appendRowsFromText(item, candidate.source, index, candidate.meta || {});
return;
}
if (!item || typeof item !== 'object') return;
const text = objectText(item);
if (!text) return;
appendRowsFromText(text, candidate.source, Number.isFinite(Number(item.index)) ? Number(item.index) : index, {
...(candidate.meta || {}),
pageRole: item.pageRole || item.role || candidate.meta?.pageRole || null,
pageId: item.pageId || item.contextId || candidate.meta?.pageId || null,
sessionId: item.sessionId || item.workbenchSessionId || candidate.meta?.sessionId || null,
traceId: item.traceId || item.trace_id || candidate.meta?.traceId || null,
projectedSeq: item.projectedSeq ?? item.projected_seq ?? item.projectedSequence ?? null,
sourceSeq: item.sourceSeq ?? item.source_seq ?? item.sourceSequence ?? null,
eventSeq: item.eventSeq ?? item.event_seq ?? item.sequence ?? null,
eventTimestamp: item.eventTimestamp ?? item.event_ts ?? item.timestamp ?? item.ts ?? null,
eventTimeText: item.eventTimeText ?? item.timeText ?? null,
eventKind: item.eventKind ?? item.kind ?? item.status ?? null,
});
});
}
if (!rows.length) {
appendRowsFromText(sampleVisibleText(sample, timelineItem), 'visible-text', 0, {
pageRole: sample.pageRole || sample.role || sample.contextRole || null,
pageId: sample.pageId || sample.contextId || null,
sessionId: sample.sessionId || sample.workbenchSessionId || null,
});
}
rows.sort((a, b) => (a.rowIndex ?? 0) - (b.rowIndex ?? 0));
return rows;
}
function extractTraceRowsFromText(text, source, baseIndex, meta = {}) {
const result = [];
const normalized = String(text || '').replace(/\r/g, '\n');
if (!normalized.trim()) return result;
const lines = normalized.split('\n').map((line) => line.trim()).filter(Boolean);
for (let index = 0; index < lines.length; index += 1) {
const line = lines[index];
if (!traceLineLooksRelevant(line)) continue;
const nextLine = index + 1 < lines.length && !/^\d{1,2}:\d{2}:\d{2}\b/.test(lines[index + 1]) ? lines[index + 1] : '';
const preview = compactOneLine(nextLine ? line + ' ' + nextLine : line);
result.push(normalizeTraceTimingRow(preview, source, Number(baseIndex || 0) * 1000 + index, meta));
}
return result;
}
function normalizeTraceTimingRow(text, source, rowIndex, meta = {}) {
const preview = compactOneLine(text).slice(0, 240);
const projectedSeq = firstFiniteNumber(meta.projectedSeq, parseTraceRowProjectedSeq(preview));
const sourceSeq = firstFiniteNumber(meta.sourceSeq);
const eventSeq = firstFiniteNumber(meta.eventSeq);
const timestampMs = parseTraceRowTimestampMs(meta.eventTimestamp || meta.eventTimeText || preview);
return {
source,
rowIndex,
preview,
pageRole: meta.pageRole || null,
pageId: meta.pageId || null,
sessionId: meta.sessionId || null,
traceId: meta.traceId || parseTraceRowTraceId(preview),
clockSeconds: parseTraceRowClockSeconds(preview) ?? parseTraceRowClockSeconds(meta.eventTimeText || ""),
timestampMs,
totalSeconds: parseTraceRowTotalSeconds(preview),
projectedSeq,
sourceSeq,
eventSeq,
eventTimestamp: meta.eventTimestamp || null,
eventTimeText: meta.eventTimeText || null,
eventKind: meta.eventKind || null,
isCompletion: traceRowIsTerminalCompletionText(preview, meta.eventKind || ""),
};
}
function traceRowIsTerminalCompletionText(preview, eventKind = "") {
const value = [preview, eventKind].map((item) => String(item || "")).join(" ");
if (/\bnon[-_ ]?terminal\b/i.test(value)) return false;
if (/轮次完成|turn\s+completed|completed\s+turn|backend[_: -]?turn[_: -]?finished/i.test(value)) return true;
return /\bterminal(?:[_: -]?status|Status)?\s*[=: -]\s*(?:completed|failed|cancelled|canceled|timeout)\b/i.test(value);
}
function traceRowIsRoundCompletionText(preview, eventKind = "") {
const value = [preview, eventKind].map((item) => String(item || "")).join(" ");
if (/\bnon[-_ ]?terminal\b/i.test(value)) return false;
return /轮次完成|turn\s+completed|completed\s+turn|backend[_: -]?turn[_: -]?finished/i.test(value);
}
function traceRowCandidateArrays(sample, timelineItem) {
const candidates = [];
const pushArray = (rows, source, meta = {}) => {
if (Array.isArray(rows) && rows.length) candidates.push({ rows, source, meta });
};
const directSources = [
[sample?.traceRows, 'sample.traceRows'],
[sample?.eventRows, 'sample.eventRows'],
[sample?.activityRows, 'sample.activityRows'],
[sample?.timelineRows, 'sample.timelineRows'],
[sample?.dom?.traceRows, 'sample.dom.traceRows'],
[sample?.dom?.eventRows, 'sample.dom.eventRows'],
[sample?.dom?.activityRows, 'sample.dom.activityRows'],
[sample?.dom?.timelineRows, 'sample.dom.timelineRows'],
[timelineItem?.traceRows, 'timeline.traceRows'],
[timelineItem?.eventRows, 'timeline.eventRows'],
[timelineItem?.activityRows, 'timeline.activityRows'],
[timelineItem?.rows, 'timeline.rows'],
[timelineItem?.events, 'timeline.events'],
];
for (const [rows, source] of directSources) pushArray(rows, source, {});
if (!candidates.length) collectNamedTraceArrays(sample, candidates, 'sample', 0);
if (!candidates.length) collectNamedTraceArrays(timelineItem, candidates, 'timeline', 0);
return candidates;
}
function collectNamedTraceArrays(value, candidates, path, depth) {
if (!value || depth > 5) return;
if (Array.isArray(value)) {
const pathLooksTrace = /trace|timeline|activity|event|log|record/i.test(path);
const valueLooksTrace = value.slice(0, 5).some((item) => traceLineLooksRelevant(typeof item === 'string' ? item : objectText(item)));
if (pathLooksTrace || valueLooksTrace) candidates.push({ rows: value, source: path, meta: {} });
return;
}
if (typeof value !== 'object') return;
for (const [key, child] of Object.entries(value)) {
if (!child) continue;
if (Array.isArray(child)) {
const childPath = path + '.' + key;
const pathLooksTrace = /trace|timeline|activity|event|log|record/i.test(key);
const valueLooksTrace = child.slice(0, 5).some((item) => traceLineLooksRelevant(typeof item === 'string' ? item : objectText(item)));
if (pathLooksTrace || valueLooksTrace) candidates.push({ rows: child, source: childPath, meta: {} });
continue;
}
if (typeof child === 'object' && /dom|trace|timeline|activity|event|log|record|page|card|message|panel|diagnostic/i.test(key)) {
collectNamedTraceArrays(child, candidates, path + '.' + key, depth + 1);
}
}
}
function traceLineLooksRelevant(text) {
const value = String(text || '').trim();
if (!value) return false;
if (/^\d{1,2}:\d{2}:\d{2}\b/.test(value)) return true;
if (/\btotal=\d/.test(value)) return true;
if (/轮次完成(总耗时/.test(value)) return true;
if (/\bseq(?:uence)?[=:]\s*\d+/i.test(value)) return true;
return false;
}
function parseTraceRowClockSeconds(text) {
const match = String(text || '').match(/^\s*(\d{1,2}):(\d{2}):(\d{2})\b/);
if (!match) return null;
return Number(match[1]) * 3600 + Number(match[2]) * 60 + Number(match[3]);
}
function parseTraceRowTotalSeconds(text) {
const value = String(text || '');
const totalMatch = value.match(/\btotal=([0-9:.]+)/i);
if (totalMatch) return parseTraceDurationSeconds(totalMatch[1]);
const completionMatch = value.match(/总耗时\s*([0-9:.]+)/);
if (completionMatch) return parseTraceDurationSeconds(completionMatch[1]);
return null;
}
function parseTraceDurationSeconds(value) {
const text = String(value || '').trim();
if (!text) return null;
const parts = text.split(':').map((part) => Number(part));
if (parts.some((part) => !Number.isFinite(part))) return null;
if (parts.length === 3) return parts[0] * 3600 + parts[1] * 60 + parts[2];
if (parts.length === 2) return parts[0] * 60 + parts[1];
if (parts.length === 1) return parts[0];
return null;
}
function parseTraceRowProjectedSeq(text) {
const value = String(text || '');
const match = value.match(/\b(?:projected[_-]?seq|seq(?:uence)?|event[_-]?seq)\s*[=:]\s*(\d+)/i);
return match ? Number(match[1]) : null;
}
function parseTraceRowTimestampMs(value) {
const text = String(value || '').trim();
if (!text) return null;
const parsed = Date.parse(text);
return Number.isFinite(parsed) ? parsed : null;
}
function firstFiniteNumber(...values) {
for (const value of values) {
const numeric = Number(value);
if (Number.isFinite(numeric)) return numeric;
}
return null;
}
function parseTraceRowTraceId(text) {
const match = String(text || '').match(/\b(?:trace_id=|traceId[:=]?\s*)(trc_[a-z0-9_-]+|[a-f0-9]{16,})\b/i);
return match ? match[1] : null;
}
function traceRowGroupKey(row) {
const identity = row.traceId ? 'trace:' + row.traceId : 'sample:' + (row.sampleIndex ?? '-') + ':' + (row.source || 'unknown');
return [row.pageRole || '', row.pageId || '', row.sessionId || '', row.source || '', row.sampleIndex ?? '', identity].join('|');
}
function traceRowMatchesCard(row, card, terminalCardCount) {
if (!row) return false;
if (card.traceId) return row.traceId === card.traceId;
if (row.traceId) return false;
if (row.sessionId && card.sessionId) return terminalCardCount === 1 && row.sessionId === card.sessionId;
if (terminalCardCount === 1) return true;
return false;
}
function maxTraceDurationEvidence(rows) {
const finiteTotals = rows.map((row) => Number(row.totalSeconds)).filter(Number.isFinite);
const finiteClocks = rows.map((row) => Number(row.clockSeconds)).filter(Number.isFinite);
const evidences = [];
if (finiteTotals.length) {
const maxTotal = Math.max(...finiteTotals);
const source = rows.find((row) => Number(row.totalSeconds) === maxTotal);
const exact = source?.isCompletion === true;
evidences.push({ kind: exact ? 'trace-completion-total' : 'trace-total', seconds: maxTotal, traceId: source?.traceId || null, preview: source?.preview || '', exact });
}
if (finiteClocks.length >= 2) {
const minClock = Math.min(...finiteClocks);
const maxClock = Math.max(...finiteClocks);
const span = maxClock - minClock;
if (span >= 0 && span < 43200) evidences.push({ kind: 'trace-clock-span', seconds: span, traceId: null, preview: 'visible trace row clock span', exact: false });
}
if (!evidences.length) return null;
evidences.sort((a, b) => b.seconds - a.seconds);
return evidences[0];
}
function maxSelfReportedDurationEvidence(text) {
const value = String(text || '');
const lines = value.split(/\n+/).map((line) => line.trim()).filter(Boolean);
let best = null;
for (let index = 0; index < lines.length; index += 1) {
const line = lines[index];
const previous = index > 0 ? lines[index - 1] : '';
const next = index + 1 < lines.length ? lines[index + 1] : '';
const candidateText = selfReportedDurationCandidateText(previous, line, next);
if (!candidateText) continue;
const seconds = parseSelfReportedRoundDurationSeconds(candidateText);
if (!Number.isFinite(seconds)) continue;
if (!best || seconds > best.seconds) {
best = { kind: 'final-response-duration', seconds, preview: compactOneLine(candidateText).slice(0, 240), exact: true };
}
}
return best;
}
function selfReportedDurationCandidateText(previous, line, next) {
const current = String(line || '');
const before = String(previous || '');
const after = String(next || '');
const windowText = [before, current, after].filter(Boolean).join(' ');
const hasDurationKeyword = /耗时|用时|duration|elapsed/i.test(current);
const hasNearbyDurationHeading = /(?:本轮|整轮|全程|任务|round|turn)?\s*(?:耗时|用时|duration|elapsed)/i.test(before)
|| /(?:本轮|整轮|全程|任务|round|turn)?\s*(?:耗时|用时|duration|elapsed)/i.test(after);
const hasDurationValue = /(?:约|大约|around|about)?\s*\d+(?:\.\d+)?\s*(?:小时|分钟|分|秒|hour|hours|hr|hrs|min|mins|minute|minutes|sec|secs|second|seconds)/i.test(windowText);
const hasRoundContext = /本轮|整轮|全程|从.+到|全部通过|smoke|benchmark|round|turn|completed|passed/i.test(windowText);
if (hasDurationKeyword && hasDurationValue) return current;
if (hasNearbyDurationHeading && hasDurationValue) return windowText;
if (hasRoundContext && hasDurationValue && /(?:约|大约|around|about)\s*\d|\d+(?:\.\d+)?\s*(?:分钟|小时|minute|hour)/i.test(current)) return windowText;
return '';
}
function parseSelfReportedRoundDurationSeconds(text) {
const value = String(text || '');
const clock = value.match(/(?:耗时|用时|duration|elapsed)[^0-9]{0,24}(\d{1,2}:\d{2}:\d{2}|\d{1,2}:\d{2})/i);
if (clock) return parseTraceDurationSeconds(clock[1]);
const hour = value.match(/(?:约|大约|around|about)?\s*(\d+(?:\.\d+)?)\s*(?:小时|hour|hours|hr|hrs)/i);
const minute = value.match(/(?:约|大约|around|about)?\s*(\d+(?:\.\d+)?)\s*(?:分钟|分|min|mins|minute|minutes)/i);
const second = value.match(/(?:约|大约|around|about)?\s*(\d+(?:\.\d+)?)\s*(?:秒|sec|secs|second|seconds)/i);
let total = 0;
if (hour) total += Number(hour[1]) * 3600;
if (minute) total += Number(minute[1]) * 60;
if (second) total += Number(second[1]);
return total > 0 ? total : null;
}
function sampleVisibleText(sample, timelineItem) {
const chunks = [];
for (const source of [sample?.visibleText, sample?.text, sample?.innerText, sample?.dom?.visibleText, sample?.dom?.text, timelineItem?.visibleText, timelineItem?.text, timelineItem?.message, timelineItem?.summary]) {
if (typeof source === 'string' && source.trim()) chunks.push(source);
}
return chunks.join('\n');
}
function objectText(value) {
if (!value || typeof value !== 'object') return typeof value === 'string' ? value : '';
const keys = ['text', 'innerText', 'visibleText', 'label', 'title', 'summary', 'message', 'content', 'body', 'preview', 'description'];
const chunks = [];
for (const key of keys) {
const part = value[key];
if (typeof part === 'string' && part.trim()) chunks.push(part);
}
return chunks.join('\n');
}
function compactOneLine(value) {
return String(value || '').replace(/\s+/g, ' ').trim();
}
function buildCodeAgentCardTimingMetrics(samples, timeline, turnTiming) {
const missingElapsed = [];
const missingRecentUpdate = [];
const cardRows = [];
for (let index = 0; index < (Array.isArray(samples) ? samples : []).length; index += 1) {
const sample = samples[index];
const timelineItem = timeline[index] || {};
for (const card of codeAgentCardsForSample(sample)) {
const text = codeAgentCardText(card);
const totalElapsedValues = parseTotalElapsedSeconds(card?.durationText).filter(Number.isFinite);
const recentUpdateValues = parseRecentUpdateSeconds(card?.activityText).filter(Number.isFinite);
const terminal = isCodeAgentCardTerminal(card);
const row = {
...ref(sample),
promptIndex: timelineItem.promptIndex ?? 0,
source: card.source ?? "turn",
status: card.status ?? null,
messageId: card.messageId ?? null,
traceId: card.traceId ?? firstTraceId([text]),
totalElapsedSeconds: totalElapsedValues.length > 0 ? Math.max(...totalElapsedValues) : null,
recentUpdateSeconds: recentUpdateValues.length > 0 ? Math.max(...recentUpdateValues) : null,
terminal,
textHash: card.textHash ?? sha256(text),
textPreview: limitText(text, 180),
valuesRedacted: true
};
cardRows.push(row);
if (row.totalElapsedSeconds === null) missingElapsed.push(row);
if (!terminal && row.recentUpdateSeconds === null) missingRecentUpdate.push(row);
}
}
const roundCompletion = buildRoundCompletionMetrics(samples, timeline, turnTiming);
return {
summary: {
cardSampleCount: cardRows.length,
terminalCardSampleCount: cardRows.filter((item) => item.terminal).length,
runningCardSampleCount: cardRows.filter((item) => !item.terminal).length,
missingElapsedCount: missingElapsed.length,
missingRecentUpdateCount: missingRecentUpdate.length,
roundCompletionEventCount: roundCompletion.events.length,
roundCompletionElapsedMismatchCount: roundCompletion.elapsedMismatches.length,
roundCompletionFinalResponseMissingCount: roundCompletion.finalResponseMissing.length,
roundCompletionPostTimingChangeCount: roundCompletion.postCompletionTimingChanges.length,
roundCompletionPostRecentUpdateVisibleCount: roundCompletion.postCompletionRecentUpdateVisible.length,
elapsedMismatchToleranceSeconds: alertThresholds.turnTimingSampleSlackSeconds,
},
cardSamples: cardRows.slice(0, 200),
missingElapsed: missingElapsed.slice(0, 200),
missingRecentUpdate: missingRecentUpdate.slice(0, 200),
roundCompletion,
valuesRedacted: true
};
}
function codeAgentCardsForSample(sample) {
const turnCards = (Array.isArray(sample?.turns) ? sample.turns : [])
.map((item) => ({ ...item, source: item?.source || "turn" }))
.filter(isCodeAgentCardLike);
if (turnCards.length > 0) return turnCards;
return (Array.isArray(sample?.messages) ? sample.messages : [])
.map((item) => ({ ...item, source: item?.source || "message" }))
.filter(isCodeAgentCardLike);
}
function isCodeAgentCardLike(item) {
const text = codeAgentCardText(item);
const role = String(item?.dataRole || item?.role || "").toLowerCase();
if (/agent|assistant/u.test(role)) return true;
return /Code Agent|运行记录|耗时|最近\s*(?:\d+|一|两|三)|轮次完成|trace_id=trc_/iu.test(text);
}
function codeAgentCardText(item) {
return [
item?.durationText,
item?.activityText,
item?.text,
item?.textPreview,
item?.status
].map((value) => String(value || "")).filter((value) => value.trim().length > 0).join("\n");
}
function isCodeAgentCardTerminal(item) {
if (isTerminalTurnStatus(item?.status)) return true;
const text = codeAgentCardText(item);
return isTerminalTraceText(text) || /轮次完成|轮次失败|轮次取消|已记录|completed|failed|canceled|cancelled|blocked/iu.test(text);
}
function buildRoundCompletionMetrics(samples, timeline, turnTiming) {
const events = [];
const elapsedMismatchToleranceSeconds = Math.max(5, Number(alertThresholds.turnTimingSampleSlackSeconds || 0));
for (let index = 0; index < (Array.isArray(samples) ? samples : []).length; index += 1) {
const sample = samples[index];
const timelineItem = timeline[index] || {};
for (const event of roundCompletionEventsForSample(sample, timelineItem)) events.push(event);
}
const elapsedMismatches = [];
const finalResponseMissing = [];
const postCompletionTimingChanges = [];
const postCompletionRecentUpdateVisible = [];
for (const event of events) {
const sampleIndex = samples.findIndex((sample) => sample?.seq === event.seq && sample?.pageRole === event.pageRole && sample?.pageId === event.pageId);
const sameSample = sampleIndex >= 0 ? samples[sampleIndex] : null;
const sameTimelineItem = sampleIndex >= 0 ? timeline[sampleIndex] || {} : {};
const cards = sameSample ? cardMetricItemsForCompletion(sameSample, sameTimelineItem, event) : [];
const bestCard = cards.filter((card) => Number.isFinite(Number(card.totalElapsedSeconds)))[0] || null;
if (Number.isFinite(Number(event.elapsedSeconds)) && bestCard) {
const delta = Math.abs(Number(bestCard.totalElapsedSeconds) - Number(event.elapsedSeconds));
if (delta > elapsedMismatchToleranceSeconds) {
elapsedMismatches.push({
...eventRef(event),
cardTotalElapsedSeconds: Number(bestCard.totalElapsedSeconds),
completionElapsedSeconds: Number(event.elapsedSeconds),
deltaSeconds: Number(delta.toFixed(3)),
toleranceSeconds: elapsedMismatchToleranceSeconds,
cardTraceId: bestCard.traceId ?? null,
cardMessageId: bestCard.messageId ?? null,
valuesRedacted: true
});
}
}
if (!hasFinalResponseAfterCompletion(samples, timeline, event)) {
finalResponseMissing.push({
...eventRef(event),
completionElapsedSeconds: event.elapsedSeconds,
valuesRedacted: true
});
}
const postTiming = detectPostCompletionTimingChanges(turnTiming, event);
postCompletionTimingChanges.push(...postTiming.timingChanges);
postCompletionRecentUpdateVisible.push(...postTiming.recentUpdateVisible);
}
return {
events: dedupeRoundCompletionRows(events).slice(0, 200),
elapsedMismatches: dedupeRoundCompletionRows(elapsedMismatches).slice(0, 200),
finalResponseMissing: dedupeRoundCompletionRows(finalResponseMissing).slice(0, 200),
postCompletionTimingChanges: dedupeRoundCompletionRows(postCompletionTimingChanges).slice(0, 200),
postCompletionRecentUpdateVisible: dedupeRoundCompletionRows(postCompletionRecentUpdateVisible).slice(0, 200),
valuesRedacted: true
};
}
function roundCompletionEventsForSample(sample, timelineItem) {
const rows = [];
for (const item of traceTimingRowsForSample(sample, timelineItem)) {
const text = String(item?.preview || item?.text || item?.textPreview || "");
if (!traceRowIsRoundCompletionText(text, item?.eventKind || "")) continue;
const elapsedValues = [
Number(item?.totalSeconds),
...parseTotalElapsedSeconds(text).filter(Number.isFinite)
].filter(Number.isFinite);
const attributed = inferRoundCompletionCard(sample, text);
rows.push({
...ref(sample),
promptIndex: timelineItem.promptIndex ?? 0,
traceId: item?.traceId ?? firstTraceId([text]) ?? attributed?.traceId ?? null,
messageId: item?.messageId ?? attributed?.messageId ?? null,
attributed: Boolean(item?.traceId || item?.messageId || attributed?.traceId || attributed?.messageId),
traceRowIndex: item?.rowIndex ?? item?.index ?? null,
elapsedSeconds: elapsedValues.length > 0 ? Math.max(...elapsedValues) : null,
textHash: item?.textHash ?? sha256(text),
preview: limitText(text, 180),
valuesRedacted: true
});
}
return rows;
}
function inferRoundCompletionCard(sample, text) {
const cards = codeAgentCardsForSample(sample)
.filter((card) => isCodeAgentCardTerminal(card))
.filter((card) => card?.traceId || card?.messageId);
const textHash = sha256(String(text || ""));
const direct = cards.filter((card) => {
const cardText = codeAgentCardText(card);
return card?.textHash === textHash || cardText.includes(String(text || "").slice(0, 80)) || /轮次完成/iu.test(cardText);
});
const candidates = direct.length > 0 ? direct : cards;
if (candidates.length !== 1) return null;
const card = candidates[0];
return { traceId: card.traceId ?? null, messageId: card.messageId ?? card.id ?? null };
}
function cardMetricItemsForCompletion(sample, timelineItem, event) {
const metrics = turnMetricItems(sample, timelineItem)
.filter((item) => item.promptIndex === event.promptIndex)
.filter((item) => item.pageRole === event.pageRole || !event.pageRole)
.filter((item) => item.pageId === event.pageId || !event.pageId);
if (!event.traceId && !event.messageId) {
const withElapsed = metrics.filter((item) => Number.isFinite(Number(item.totalElapsedSeconds)));
return withElapsed.length === 1 ? withElapsed : [];
}
return metrics
.filter((item) => !event.traceId || !item.traceId || item.traceId === event.traceId)
.filter((item) => !event.messageId || !item.messageId || item.messageId === event.messageId)
.sort((left, right) => {
const leftTraceMatch = event.traceId && left.traceId === event.traceId ? 0 : 1;
const rightTraceMatch = event.traceId && right.traceId === event.traceId ? 0 : 1;
const leftMessageMatch = event.messageId && left.messageId === event.messageId ? 0 : 1;
const rightMessageMatch = event.messageId && right.messageId === event.messageId ? 0 : 1;
return leftTraceMatch - rightTraceMatch || leftMessageMatch - rightMessageMatch || String(left.source || "").localeCompare(String(right.source || ""));
});
}
function hasFinalResponseAfterCompletion(samples, timeline, event) {
if (!event.traceId && !event.messageId && !event.promptIndex) return true;
const eventTsMs = Date.parse(String(event.ts || ""));
for (let index = 0; index < (Array.isArray(samples) ? samples : []).length; index += 1) {
const sample = samples[index];
const tsMs = Date.parse(String(sample?.ts || ""));
if (Number.isFinite(eventTsMs) && Number.isFinite(tsMs) && tsMs < eventTsMs) continue;
if (sample?.pageRole !== event.pageRole) continue;
const sampleSession = sample?.routeSessionId || sample?.activeSessionId || null;
const eventSession = event.routeSessionId || event.activeSessionId || null;
if (sampleSession && eventSession && sampleSession !== eventSession) continue;
const promptIndex = timeline[index]?.promptIndex ?? 0;
if (event.promptIndex && promptIndex !== event.promptIndex) continue;
if (sampleHasVisibleFinalResponse(sample, event)) return true;
}
return false;
}
function sampleHasVisibleFinalResponse(sample, event = {}) {
for (const [groupName, group] of [["messages", sample?.messages], ["turns", sample?.turns]]) {
if (!Array.isArray(group)) continue;
for (const item of group) {
if (event.traceId && item?.traceId && item.traceId !== event.traceId) continue;
if (event.messageId && item?.messageId && item.messageId !== event.messageId) continue;
const text = normalizedText([item?.text, item?.textPreview].filter(Boolean).join(" "));
if (text.length < 24) continue;
if (isDiagnosticText(text)) continue;
if (groupName === "messages" && isTerminalTurnStatus(item?.status)) return true;
if (isFinalResultText(text)) return true;
if (/运行记录/iu.test(text) && /(?:已完成|完成|新增|实现|验证|测试|结果|README|文件|summary)/iu.test(text)) return true;
}
}
return false;
}
function detectPostCompletionTimingChanges(turnTiming, event) {
const timingChanges = [];
const recentUpdateVisible = [];
if (!event.traceId && !event.messageId) return { timingChanges, recentUpdateVisible };
const rows = Array.isArray(turnTiming?.rows) ? turnTiming.rows : [];
const columns = Array.isArray(turnTiming?.columns) ? turnTiming.columns : [];
const eventTsMs = Date.parse(String(event.ts || ""));
for (const column of columns) {
if (column.pageRole && event.pageRole && column.pageRole !== event.pageRole) continue;
if (event.traceId && column.traceId && column.traceId !== event.traceId) continue;
if (event.messageId && column.messageId && column.messageId !== event.messageId) continue;
if (event.promptIndex && column.promptIndex && column.promptIndex !== event.promptIndex && column.lastPromptIndex !== event.promptIndex) continue;
let previousTotal = null;
let previousRecent = null;
for (const row of rows) {
const rowTsMs = Date.parse(String(row.ts || ""));
if (Number.isFinite(eventTsMs) && Number.isFinite(rowTsMs) && rowTsMs < eventTsMs) continue;
if (row.pageRole && event.pageRole && row.pageRole !== event.pageRole) continue;
const cell = row.cells?.[column.id];
if (!cell) continue;
if (event.traceId && cell.traceId && cell.traceId !== event.traceId) continue;
if (event.messageId && cell.messageId && cell.messageId !== event.messageId) continue;
const total = cell.totalElapsedSeconds === null || cell.totalElapsedSeconds === undefined ? NaN : Number(cell.totalElapsedSeconds);
if (Number.isFinite(total)) {
if (previousTotal && Math.abs(total - previousTotal.value) > alertThresholds.turnTimingSampleSlackSeconds) {
timingChanges.push({
...eventRef(event),
columnId: column.id,
columnLabel: column.label,
metric: "totalElapsedSeconds",
fromSeq: previousTotal.seq,
fromTs: previousTotal.ts,
fromValue: previousTotal.value,
toSeq: row.seq ?? null,
toTs: row.ts ?? null,
toValue: total,
delta: Number((total - previousTotal.value).toFixed(3)),
toleranceSeconds: alertThresholds.turnTimingSampleSlackSeconds,
traceId: cell.traceId ?? column.traceId ?? event.traceId ?? null,
messageId: cell.messageId ?? column.messageId ?? null,
valuesRedacted: true
});
}
previousTotal = { value: total, seq: row.seq ?? null, ts: row.ts ?? null };
}
const recent = cell.recentUpdateSeconds === null || cell.recentUpdateSeconds === undefined ? NaN : Number(cell.recentUpdateSeconds);
if (Number.isFinite(recent)) {
recentUpdateVisible.push({
...eventRef(event),
columnId: column.id,
columnLabel: column.label,
metric: "recentUpdateSeconds",
seq: row.seq ?? null,
ts: row.ts ?? null,
value: recent,
traceId: cell.traceId ?? column.traceId ?? event.traceId ?? null,
messageId: cell.messageId ?? column.messageId ?? null,
valuesRedacted: true
});
if (previousRecent && recent !== previousRecent.value) {
timingChanges.push({
...eventRef(event),
columnId: column.id,
columnLabel: column.label,
metric: "recentUpdateSeconds",
fromSeq: previousRecent.seq,
fromTs: previousRecent.ts,
fromValue: previousRecent.value,
toSeq: row.seq ?? null,
toTs: row.ts ?? null,
toValue: recent,
delta: Number((recent - previousRecent.value).toFixed(3)),
traceId: cell.traceId ?? column.traceId ?? event.traceId ?? null,
messageId: cell.messageId ?? column.messageId ?? null,
valuesRedacted: true
});
}
previousRecent = { value: recent, seq: row.seq ?? null, ts: row.ts ?? null };
}
}
}
return { timingChanges, recentUpdateVisible };
}
function eventRef(event) {
return {
seq: event?.seq ?? null,
sampleGroupSeq: event?.sampleGroupSeq ?? null,
ts: event?.ts ?? null,
pageRole: event?.pageRole ?? null,
pageId: event?.pageId ?? null,
routeSessionId: event?.routeSessionId ?? null,
activeSessionId: event?.activeSessionId ?? null,
promptIndex: event?.promptIndex ?? null,
traceId: event?.traceId ?? null,
};
}
function dedupeRoundCompletionRows(rows) {
const result = [];
const seen = new Set();
for (const row of Array.isArray(rows) ? rows : []) {
const key = [
row?.seq ?? row?.fromSeq ?? "",
row?.toSeq ?? "",
row?.pageRole ?? "",
row?.promptIndex ?? "",
row?.traceId ?? "",
row?.metric ?? "",
row?.textHash ?? "",
row?.columnId ?? ""
].join("|");
if (seen.has(key)) continue;
seen.add(key);
result.push(row);
}
return result;
}
function turnMetricItems(sample, timelineItem) {
const promptIndex = timelineItem.promptIndex ?? 0;
const pageRole = sample?.pageRole || "control";
const pageId = sample?.pageId || "unknown-page";
const sessionKey = pageRole + ":" + pageId + ":" + (sample?.routeSessionId || sample?.activeSessionId || "unknown-session");
const roundKey = String(promptIndex);
const items = [];
if (Array.isArray(sample?.turns) && sample.turns.length > 0) {
for (const turn of sample.turns) {
const texts = turnTexts(turn);
const totalElapsedValues = texts.flatMap(parseTotalElapsedSeconds).filter(Number.isFinite);
const recentUpdateValues = texts.flatMap(parseRecentUpdateSeconds).filter(Number.isFinite);
const traceId = turn.traceId || firstTraceId(texts);
const messageId = turn.messageId || null;
const turnId = turn.turnId || traceId || null;
const stableId = traceId || messageId || turnId || null;
const domIndex = Number.isFinite(Number(turn.index)) ? Number(turn.index) : items.length;
const key = stableId
? "turn:" + sessionKey + ":id-" + stableId
: "turn:" + sessionKey + ":round-" + roundKey + ":dom-index-" + String(domIndex);
items.push({
key,
source: "turn",
pageRole,
pageId,
promptIndex,
traceId,
messageId,
turnId,
domIndex,
status: turn.status ?? null,
totalElapsedSeconds: totalElapsedValues.length > 0 ? Math.max(...totalElapsedValues) : null,
recentUpdateSeconds: recentUpdateValues.length > 0 ? Math.max(...recentUpdateValues) : null,
textHash: turn.textHash || sha256(texts.join("\n"))
});
}
return items;
}
if (Array.isArray(sample?.messages) && sample.messages.length > 0) {
for (const message of sample.messages) {
const text = [message?.durationText, message?.activityText].map((value) => String(value || "")).filter((value) => value.trim().length > 0).join("\n");
const totalElapsedValues = parseTotalElapsedSeconds(text).filter(Number.isFinite);
const recentUpdateValues = parseRecentUpdateSeconds(text).filter(Number.isFinite);
if (totalElapsedValues.length === 0 && recentUpdateValues.length === 0) continue;
const domIndex = Number.isFinite(Number(message.index)) ? Number(message.index) : items.length;
const traceId = message.traceId || firstTraceId([text]);
const messageId = message.messageId || null;
const stableId = traceId || messageId || message.turnId || null;
items.push({
key: stableId
? "message:" + sessionKey + ":id-" + stableId
: "message:" + sessionKey + ":round-" + roundKey + ":dom-index-" + String(domIndex),
source: "message",
pageRole,
pageId,
promptIndex,
traceId,
messageId,
turnId: message.turnId || traceId || null,
domIndex,
status: message.status ?? null,
totalElapsedSeconds: totalElapsedValues.length > 0 ? Math.max(...totalElapsedValues) : null,
recentUpdateSeconds: recentUpdateValues.length > 0 ? Math.max(...recentUpdateValues) : null,
textHash: message.textHash || sha256(text)
});
}
if (items.length > 0) return items;
}
if (timelineItem.totalElapsedSeconds !== null || timelineItem.recentUpdateSeconds !== null) {
return [{
key: "aggregate:" + sessionKey + ":round-" + roundKey,
source: "aggregate",
pageRole,
pageId,
promptIndex,
traceId: null,
messageId: null,
domIndex: null,
status: null,
totalElapsedSeconds: timelineItem.totalElapsedSeconds ?? null,
recentUpdateSeconds: timelineItem.recentUpdateSeconds ?? null,
textHash: timelineItem.textDigest ?? null
}];
}
return [];
}
function turnTexts(turn) {
return [
turn?.durationText,
turn?.activityText
].map((value) => String(value || "")).filter((value) => value.trim().length > 0);
}
function firstTraceId(texts) {
for (const text of texts) {
const match = String(text || "").match(/\btrc_[A-Za-z0-9_-]+\b/u);
if (match) return match[0];
}
return null;
}
function buildRoundMetricSummaries(timeline, promptCommands, timing = {}) {
const rounds = [];
const nonMonotonic = Array.isArray(timing.nonMonotonic) ? timing.nonMonotonic : [];
const totalElapsedForwardJumps = Array.isArray(timing.totalElapsedForwardJumps) ? timing.totalElapsedForwardJumps : [];
const elapsedZeroResets = Array.isArray(timing.elapsedZeroResets) ? timing.elapsedZeroResets : [];
const terminalElapsedGrowth = Array.isArray(timing.terminalElapsedGrowth) ? timing.terminalElapsedGrowth : [];
const recentUpdateResets = Array.isArray(timing.recentUpdateResets) ? timing.recentUpdateResets : [];
const recentUpdateSteps = Array.isArray(timing.recentUpdateSteps) ? timing.recentUpdateSteps : [];
for (let index = 0; index < promptCommands.length; index += 1) {
const promptIndex = index + 1;
const items = timeline.filter((item) => item.promptIndex === promptIndex);
const totalElapsed = items.map((item) => item.totalElapsedSeconds).filter((value) => value !== null);
const recentUpdate = items.map((item) => item.recentUpdateSeconds).filter((value) => value !== null);
const loadingCounts = items.map((item) => Number(item.loadingCount ?? 0)).filter(Number.isFinite);
const loadingOwners = new Set();
for (const item of items) {
for (const owner of Array.isArray(item.loadingOwners) ? item.loadingOwners : []) {
if (owner?.ownerKey) loadingOwners.add(owner.ownerKey);
}
}
const timingAnomalies = nonMonotonic.filter((item) => item.promptIndex === promptIndex);
const timingForwardJumps = totalElapsedForwardJumps.filter((item) => item.promptIndex === promptIndex);
const timingZeroResets = elapsedZeroResets.filter((item) => item.promptIndex === promptIndex);
const timingTerminalGrowth = terminalElapsedGrowth.filter((item) => item.promptIndex === promptIndex);
const timingResets = recentUpdateResets.filter((item) => item.promptIndex === promptIndex);
const timingSteps = recentUpdateSteps.filter((item) => item.promptIndex === promptIndex);
const terminalGrowthDeltas = timingTerminalGrowth.map((item) => Number(item.delta)).filter((value) => Number.isFinite(value) && value > 0);
const timingStepDeltas = timingSteps.map((item) => Number(item.delta)).filter((value) => Number.isFinite(value) && value >= 0);
const timingStepExcess = timingSteps.map((item) => Number(item.excessiveIncreaseSeconds)).filter((value) => Number.isFinite(value) && value > 0);
rounds.push({
promptIndex,
promptCommandId: promptCommands[index].commandId,
promptTextHash: promptCommands[index].textHash,
promptTextBytes: promptCommands[index].textBytes,
promptCompletedAt: promptCommands[index].ts,
sampleCount: items.length,
firstSeq: items[0]?.seq ?? null,
lastSeq: items[items.length - 1]?.seq ?? null,
firstSampleAt: items[0]?.ts ?? null,
lastSampleAt: items[items.length - 1]?.ts ?? null,
withTotalElapsed: totalElapsed.length,
withRecentUpdate: recentUpdate.length,
loadingSamples: loadingCounts.filter((value) => value > 0).length,
maxLoadingCount: loadingCounts.length > 0 ? Math.max(...loadingCounts) : 0,
loadingOwnerCount: loadingOwners.size,
maxTotalElapsedSeconds: totalElapsed.length > 0 ? Math.max(...totalElapsed) : null,
lastTotalElapsedSeconds: lastNonNull(items.map((item) => item.totalElapsedSeconds)),
maxRecentUpdateSeconds: recentUpdate.length > 0 ? Math.max(...recentUpdate) : null,
lastRecentUpdateSeconds: lastNonNull(items.map((item) => item.recentUpdateSeconds)),
diagnosticSamples: items.filter((item) => item.diagnosticSeen).length,
terminalSamples: items.filter((item) => item.terminalSeen).length,
finalTextSamples: items.filter((item) => item.finalResultTextSeen).length,
turnTimingNonMonotonicCount: timingAnomalies.length,
turnTimingTotalElapsedDecreaseCount: timingAnomalies.filter((item) => item.metric === "totalElapsedSeconds").length,
turnTimingTotalElapsedZeroResetCount: timingZeroResets.length,
turnTimingTotalElapsedForwardJumpCount: timingForwardJumps.length,
turnTimingTotalElapsedForwardJumpMaxSeconds: maxPositiveDelta(timingForwardJumps),
turnTimingTerminalElapsedGrowthCount: timingTerminalGrowth.length,
turnTimingTerminalElapsedGrowthMaxSeconds: terminalGrowthDeltas.length > 0 ? Math.max(...terminalGrowthDeltas) : 0,
turnTimingRecentUpdateJumpCount: timingAnomalies.filter((item) => item.metric === "recentUpdateSeconds" && item.anomaly === "jump").length,
turnTimingRecentUpdateSawtoothJumpCount: timingAnomalies.filter((item) => item.metric === "recentUpdateSeconds" && item.anomaly === "jump").length,
turnTimingRecentUpdateStepCount: timingSteps.length,
turnTimingRecentUpdateMaxIncreaseSeconds: timingStepDeltas.length > 0 ? Math.max(...timingStepDeltas) : null,
turnTimingRecentUpdateMaxExcessSeconds: timingStepExcess.length > 0 ? Math.max(...timingStepExcess) : 0,
turnTimingRecentUpdateResetCount: timingResets.length,
turnTimingRecentUpdateDecreaseCount: timingResets.length
});
}
return rounds;
}
function sampleTexts(sample) {
const rows = [];
for (const group of [sample?.messages, sample?.traceRows, sample?.diagnostics]) {
if (!Array.isArray(group)) continue;
for (const item of group) {
const text = String(item?.textPreview || "");
if (text.trim()) rows.push(text);
}
}
return rows;
}
function sampleTurnTimingTexts(sample) {
const rows = [];
for (const group of [sample?.turns, sample?.messages]) {
if (!Array.isArray(group)) continue;
for (const item of group) {
for (const value of [item?.durationText, item?.activityText]) {
const text = String(value || "");
if (text.trim()) rows.push(text);
}
}
}
return rows;
}
function parseTotalElapsedSeconds(text) {
const values = [];
for (const match of String(text || "").matchAll(/(?:总耗时|耗时)\s*[=:]?\s*(\d{1,2}):(\d{2}):(\d{2})/giu)) {
values.push(Number(match[1]) * 3600 + Number(match[2]) * 60 + Number(match[3]));
}
for (const match of String(text || "").matchAll(/(?:总耗时|耗时)\s*[=:]?\s*(\d{1,2}):(\d{2})(?!:)/giu)) {
values.push(Number(match[1]) * 60 + Number(match[2]));
}
for (const match of String(text || "").matchAll(/(?:总耗时|耗时)\s*[=:]?\s*(?:(\d+)\s*天)?\s*(?:(\d+)\s*小时)?\s*(?:(\d+)\s*(?:分钟|分))?\s*(?:(\d+)\s*秒)?/giu)) {
const days = Number(match[1] || 0);
const hours = Number(match[2] || 0);
const minutes = Number(match[3] || 0);
const seconds = Number(match[4] || 0);
if (days || hours || minutes || seconds || /(?:天|小时|分钟|分|秒)/u.test(match[0] || "")) values.push(days * 86400 + hours * 3600 + minutes * 60 + seconds);
}
return values;
}
function lastNonNull(values) {
for (let index = values.length - 1; index >= 0; index -= 1) if (values[index] !== null && values[index] !== undefined) return values[index];
return null;
}
function parseRecentUpdateSeconds(text) {
const values = [];
for (const match of String(text || "").matchAll(/最近\s*(?:(\d+)\s*天)?\s*(?:(\d+)\s*小时)?\s*(?:(\d+)\s*(?:分钟|分))?\s*(?:(\d+)\s*秒)?\s*前/giu)) {
const days = Number(match[1] || 0);
const hours = Number(match[2] || 0);
const minutes = Number(match[3] || 0);
const seconds = Number(match[4] || 0);
values.push(days * 86400 + hours * 3600 + minutes * 60 + seconds);
}
return values;
}
function latestPromptIndex(promptTimes, tsMs) {
let index = 0;
for (let i = 0; i < promptTimes.length; i += 1) {
if (promptTimes[i] <= tsMs) index = i + 1;
else break;
}
return index;
}
function promptIndexForTs(promptTimes, ts) {
const tsMs = Date.parse(ts);
return Number.isFinite(tsMs) ? latestPromptIndex(promptTimes, tsMs) : 0;
}
function buildTransitions(samples) {
const rows = [];
let last = null;
for (const sample of samples) {
const digest = digestSample(sample);
if (digest !== last) {
rows.push({ seq: sample.seq, ts: sample.ts, url: sample.url, routeSessionId: sample.routeSessionId || null, activeSessionId: sample.activeSessionId || null, messageCount: Array.isArray(sample.messages) ? sample.messages.length : 0, traceRowCount: Array.isArray(sample.traceRows) ? sample.traceRows.length : 0, digest });
last = digest;
}
}
return rows.slice(0, 200);
}
function detectFinalFlicker(samples) {
const flickers = [];
const lastNonEmptyByScope = new Map();
for (const sample of samples) {
const scope = finalFlickerScope(sample);
if (!scope) continue;
const messageText = Array.isArray(sample.messages) ? sample.messages.map((item) => item.textPreview || "").join("\n") : "";
const nonEmpty = messageText.trim().length > 0;
const finalLike = /轮次完成|已记录|已完成第\d+轮|final response|terminal result/iu.test(messageText);
const diagnosticLike = /temporarily|timeout|无法连接|暂时|error|failed|超时/iu.test(messageText);
const lastNonEmpty = lastNonEmptyByScope.get(scope);
if (nonEmpty && finalLike && !diagnosticLike) lastNonEmptyByScope.set(scope, { sample, messageText });
if (lastNonEmpty && nonEmpty && diagnosticLike) flickers.push({ scope, from: ref(lastNonEmpty.sample), to: ref(sample) });
if (lastNonEmpty && !nonEmpty) flickers.push({ scope, from: ref(lastNonEmpty.sample), to: ref(sample), reason: "text-disappeared" });
}
return flickers;
}
function finalFlickerScope(sample) {
const pathname = samplePathname(sample);
const sessionId = sample?.routeSessionId || sample?.activeSessionId || workbenchSessionIdFromPath(pathname);
if (!sessionId) return null;
if (!pathname.startsWith("/workbench/sessions/" + sessionId)) return null;
return (sample?.pageRole || "control") + ":" + sessionId;
}
function samplePathname(sample) {
const raw = String(sample?.path || sample?.url || "").trim();
if (!raw) return "";
try {
return new URL(raw, "http://hwlab.local").pathname || raw;
} catch {
return raw.split(/[?#]/u, 1)[0] || raw;
}
}
function workbenchSessionIdFromPath(pathname) {
const match = String(pathname || "").match(/^\/workbench\/sessions\/([^/?#]+)/u);
return match ? match[1] : null;
}
function detectTerminalZeroElapsed(samples) {
const rows = [];
for (const sample of samples) {
const turns = Array.isArray(sample?.turns) ? sample.turns : [];
const messages = Array.isArray(sample?.messages) ? sample.messages : [];
for (const item of [...turns, ...messages]) {
const text = [item?.durationText, item?.activityText, item?.status, item?.textPreview, item?.text].filter(Boolean).join("\n");
if (!/(?:Code Agent|运行记录|completed|failed|canceled|blocked)/iu.test(text)) continue;
if (!/(?:completed|failed|canceled|blocked|已完成|失败|取消)/iu.test(text)) continue;
const timingText = [item?.durationText, item?.activityText].filter(Boolean).join("\n");
const elapsedValues = parseTotalElapsedSeconds(timingText);
if (!elapsedValues.includes(0)) continue;
rows.push({
...ref(sample),
status: item?.status ?? null,
messageId: item?.messageId ?? null,
traceId: item?.traceId ?? null,
durationText: item?.durationText ?? null,
activityText: item?.activityText ?? null,
textPreview: limitText(item?.textPreview || item?.text || "", 180)
});
}
}
return rows;
}
function detectCrossPageProjectionDiffs(samples) {
const groups = new Map();
for (const sample of samples) {
const key = sample?.sampleGroupSeq ?? sample?.seq;
if (key === null || key === undefined) continue;
const group = groups.get(key) || {};
if (sample?.pageRole === "control") group.control = sample;
else if (sample?.pageRole === "observer") group.observer = sample;
groups.set(key, group);
}
const rows = [];
for (const [sampleGroupSeq, group] of groups.entries()) {
const control = group.control;
const observer = group.observer;
if (!control || !observer) continue;
const controlTraceIds = visibleTraceIds(control);
const observerTraceIds = visibleTraceIds(observer);
const missingInObserver = [...controlTraceIds].filter((item) => !observerTraceIds.has(item));
const controlMessages = Array.isArray(control.messages) ? control.messages.length : 0;
const observerMessages = Array.isArray(observer.messages) ? observer.messages.length : 0;
const controlTraceRows = Array.isArray(control.traceRows) ? control.traceRows.length : 0;
const observerTraceRows = Array.isArray(observer.traceRows) ? observer.traceRows.length : 0;
const controlZero = detectTerminalZeroElapsed([control]).length > 0;
const observerZero = detectTerminalZeroElapsed([observer]).length > 0;
const sameSession = (control.routeSessionId || control.activeSessionId || null) && (control.routeSessionId || control.activeSessionId || null) === (observer.routeSessionId || observer.activeSessionId || null);
const controlMessageDigest = digestMessageTexts(control);
const observerMessageDigest = digestMessageTexts(observer);
const messageTextDigestDiff = controlMessageDigest !== observerMessageDigest;
const projectionDivergent = sameSession && (Math.abs(controlMessages - observerMessages) > 0 || controlZero !== observerZero || messageTextDigestDiff);
const traceVisibilityDivergent = sameSession && !projectionDivergent && (missingInObserver.length > 0 || Math.abs(controlTraceRows - observerTraceRows) > 0);
if (!projectionDivergent && !traceVisibilityDivergent) continue;
rows.push({
sampleGroupSeq,
diffKind: traceVisibilityDivergent ? "trace-visibility" : "projection",
control: ref(control),
observer: ref(observer),
controlTraceIds: [...controlTraceIds].slice(0, 8),
observerTraceIds: [...observerTraceIds].slice(0, 8),
missingTraceIdsInObserver: missingInObserver.slice(0, 8),
controlMessageCount: controlMessages,
observerMessageCount: observerMessages,
controlTraceRowCount: controlTraceRows,
observerTraceRowCount: observerTraceRows,
terminalZeroElapsedDiff: controlZero !== observerZero,
messageTextDigestDiff,
controlMessageDigest,
observerMessageDigest
});
}
return rows;
}
function mergeCrossPageDiffRows(...groups) {
const rows = [];
const seen = new Set();
for (const group of groups) {
if (!Array.isArray(group)) continue;
for (const row of group) {
const key = [
row?.diffKind || "projection",
row?.control?.seq ?? null,
row?.observer?.seq ?? null,
row?.controlMessageCount ?? null,
row?.observerMessageCount ?? null,
row?.controlTraceRowCount ?? null,
row?.observerTraceRowCount ?? null
].join(":");
if (seen.has(key)) continue;
seen.add(key);
rows.push(row);
}
}
return rows;
}
function detectAdjacentCrossPageProjectionDiffs(samples) {
const rows = [];
const ordered = (Array.isArray(samples) ? samples : []).slice().sort((a, b) => Number(a?.seq ?? 0) - Number(b?.seq ?? 0));
for (let i = 1; i < ordered.length; i += 1) {
const a = ordered[i - 1];
const b = ordered[i];
const roles = new Set([a?.pageRole, b?.pageRole]);
if (!roles.has("control") || !roles.has("observer")) continue;
const control = a?.pageRole === "control" ? a : b;
const observer = a?.pageRole === "observer" ? a : b;
const controlSession = control.routeSessionId || control.activeSessionId || null;
const observerSession = observer.routeSessionId || observer.activeSessionId || null;
if (!controlSession || controlSession !== observerSession) continue;
const controlAt = Date.parse(String(control.ts || ""));
const observerAt = Date.parse(String(observer.ts || ""));
if (Number.isFinite(controlAt) && Number.isFinite(observerAt) && Math.abs(controlAt - observerAt) > 1500) continue;
const controlMessages = Array.isArray(control.messages) ? control.messages.length : 0;
const observerMessages = Array.isArray(observer.messages) ? observer.messages.length : 0;
const controlTraceRows = Array.isArray(control.traceRows) ? control.traceRows.length : 0;
const observerTraceRows = Array.isArray(observer.traceRows) ? observer.traceRows.length : 0;
const controlZero = detectTerminalZeroElapsed([control]).length > 0;
const observerZero = detectTerminalZeroElapsed([observer]).length > 0;
const missingInObserver = [...visibleTraceIds(control)].filter((item) => !visibleTraceIds(observer).has(item));
const controlTraceIds = visibleTraceIds(control);
const observerTraceIds = visibleTraceIds(observer);
const controlMessageDigest = digestMessageTexts(control);
const observerMessageDigest = digestMessageTexts(observer);
const messageTextDigestDiff = controlMessageDigest !== observerMessageDigest;
const projectionDivergent = controlMessages !== observerMessages || controlZero !== observerZero || messageTextDigestDiff;
const traceVisibilityDivergent = !projectionDivergent && (missingInObserver.length > 0 || controlTraceRows !== observerTraceRows);
if (!projectionDivergent && !traceVisibilityDivergent) continue;
rows.push({
sampleGroupSeq: control.sampleGroupSeq ?? observer.sampleGroupSeq ?? null,
adjacentPair: true,
diffKind: traceVisibilityDivergent ? "trace-visibility" : "projection",
control: ref(control),
observer: ref(observer),
controlTraceIds: [...controlTraceIds].slice(0, 8),
observerTraceIds: [...observerTraceIds].slice(0, 8),
missingTraceIdsInObserver: missingInObserver.slice(0, 8),
controlMessageCount: controlMessages,
observerMessageCount: observerMessages,
controlTraceRowCount: controlTraceRows,
observerTraceRowCount: observerTraceRows,
terminalZeroElapsedDiff: controlZero !== observerZero,
messageTextDigestDiff,
controlMessageDigest,
observerMessageDigest
});
}
return rows;
}
function detectTraceMessageDuplication(samples) {
const rows = [];
for (const sample of samples) {
const finalText = normalizedText((Array.isArray(sample?.messages) ? sample.messages : []).map((item) => item?.textPreview || item?.text || "").join("\n"));
if (finalText.length < 40) continue;
const traceRows = Array.isArray(sample?.traceRows) ? sample.traceRows : [];
for (const row of traceRows) {
const rowTextRaw = String(row?.textPreview || row?.text || "");
if (!/(?:助手消息|assistant\s+message|assistant)/iu.test(rowTextRaw)) continue;
const rowText = normalizedText(rowTextRaw);
if (rowText.length < 24) continue;
const overlap = longestSharedSubstringLength(finalText, rowText);
if (overlap < 40 && overlap < Math.min(rowText.length, finalText.length) * 0.55) continue;
rows.push({ ...ref(sample), traceId: row?.traceId ?? null, rowIndex: row?.index ?? null, rowTextPreview: limitText(rowTextRaw, 180) });
}
}
return rows;
}
function detectTurnTraceIdMissing(samples) {
const rows = [];
for (const sample of samples) {
for (const item of Array.isArray(sample?.turns) ? sample.turns : []) {
const text = [item?.status, item?.durationText, item?.activityText, item?.textPreview, item?.text].filter(Boolean).join("\n");
if (!/(?:Code Agent|运行记录|耗时|最近)/iu.test(text)) continue;
if (item?.traceId) continue;
rows.push({ ...ref(sample), status: item?.status ?? null, messageId: item?.messageId ?? null, textPreview: limitText(item?.textPreview || item?.text || "", 180) });
}
}
return rows;
}
function visibleTraceIds(sample) {
const ids = new Set();
for (const group of [sample?.turns, sample?.messages, sample?.traceRows]) {
if (!Array.isArray(group)) continue;
for (const item of group) if (item?.traceId) ids.add(String(item.traceId));
}
return ids;
}
function digestMessageTexts(sample) {
return sha256((Array.isArray(sample?.messages) ? sample.messages : []).map((item) => item?.textHash || normalizedText(item?.textPreview || item?.text || "")).join("|"));
}
function normalizedText(value) {
return String(value || "").replace(/\s+/gu, " ").trim();
}
function longestSharedSubstringLength(a, b) {
if (!a || !b) return 0;
const left = a.length <= b.length ? a : b;
const right = a.length <= b.length ? b : a;
const max = Math.min(left.length, 280);
let best = 0;
for (let start = 0; start < max; start += 1) {
for (let end = Math.min(max, start + 160); end > start + best; end -= 1) {
if (right.includes(left.slice(start, end))) {
best = end - start;
break;
}
}
}
return best;
}
function digestSample(sample) {
const messages = Array.isArray(sample.messages) ? sample.messages.map((item) => item.textHash || item.textPreview || "").join("|") : "";
const trace = Array.isArray(sample.traceRows) ? sample.traceRows.map((item) => (item.status || "") + ":" + (item.textHash || item.textPreview || "")).join("|") : "";
const diagnostics = Array.isArray(sample.diagnostics) ? sample.diagnostics.map((item) => (item.className || "") + ":" + (item.textHash || item.textPreview || "")).join("|") : "";
return sha256((sample.routeSessionId || "") + "|" + (sample.activeSessionId || "") + "|" + messages + "|" + trace + "|" + diagnostics);
}
function nearCommand(sample, commandTimes, windowMs) {
const ts = Date.parse(sample.ts);
return Number.isFinite(ts) && commandTimes.some((item) => Math.abs(ts - item) <= windowMs);
}
function sampleRefs(samples, pick) {
const seen = new Set();
const refs = [];
for (const sample of samples) {
const value = pick(sample);
if (!value || seen.has(value)) continue;
seen.add(value);
refs.push({ ...ref(sample), value });
}
return refs.slice(0, 20);
}
function ref(sample) {
if (!sample) return null;
return { seq: sample.seq ?? null, sampleGroupSeq: sample.sampleGroupSeq ?? null, ts: sample.ts ?? null, pageRole: sample.pageRole ?? null, pageId: sample.pageId ?? null, url: sample.url ?? null, routeSessionId: sample.routeSessionId ?? null, activeSessionId: sample.activeSessionId ?? null };
}
async function artifactSummary(artifacts) {
const items = artifacts.slice(-30).map((item) => ({ kind: item.kind, reason: item.reason, sampleSeq: item.sampleSeq, path: item.path, sha256: item.sha256, byteCount: item.byteCount }));
return { count: artifacts.length, latest: items };
}
function compactManifest(value) {
if (!value) return null;
return { jobId: value.jobId, stateDir: value.stateDir, baseUrl: value.baseUrl, targetPath: value.targetPath, startedAt: value.startedAt, status: value.status, pageAuthority: value.pageAuthority ?? null, sampling: value.sampling, pageProvenance: value.pageProvenance ?? null, safety: value.safety };
}
function compactHeartbeat(value) {
if (!value) return null;
return { jobId: value.jobId, pid: value.pid, status: value.status, sampleSeq: value.sampleSeq, commandSeq: value.commandSeq, pageId: value.pageId ?? null, observerPageId: value.observerPageId ?? null, currentUrl: value.currentUrl, observerUrl: value.observerUrl ?? null, observerRefreshIntervalMs: value.observerRefreshIntervalMs ?? null, lastObserverRefreshAt: value.lastObserverRefreshAt ?? null, pageProvenance: value.pageProvenance ?? null, updatedAt: value.updatedAt, uptimeMs: value.uptimeMs };
}
function renderTurnTimingTable(sampleMetrics) {
const columns = Array.isArray(sampleMetrics?.turnColumns) ? sampleMetrics.turnColumns : [];
const rows = Array.isArray(sampleMetrics?.turnTimingTable) ? sampleMetrics.turnTimingTable : [];
if (columns.length === 0 || rows.length === 0) return "- 无 turn 时间表。";
const header = ["时间戳"];
for (const column of columns) {
header.push(column.label + " 总耗时(s)");
header.push(column.label + " 最近更新(s)");
}
const lines = [];
lines.push("| " + header.map(escapeMarkdownCell).join(" | ") + " |");
lines.push("| " + header.map(() => "---").join(" | ") + " |");
for (const row of rows) {
const cells = [row.ts || "-"];
for (const column of columns) {
const cell = row.cells?.[column.id] || {};
cells.push(formatMetricCell(cell.totalElapsedSeconds));
cells.push(formatMetricCell(cell.recentUpdateSeconds));
}
lines.push("| " + cells.map(escapeMarkdownCell).join(" | ") + " |");
}
const columnLines = columns.map((column) => "- " + column.label + ": source=" + (column.source || "-") + " prompt=" + (column.promptIndex ?? "-") + " lastPrompt=" + (column.lastPromptIndex ?? "-") + " firstSeq=" + (column.firstSeq ?? "-") + " lastSeq=" + (column.lastSeq ?? "-") + " traceId=" + (column.traceId || "-") + " messageId=" + (column.messageId || "-")).join("\n");
const nonMonotonic = Array.isArray(sampleMetrics?.turnTimingNonMonotonic) ? sampleMetrics.turnTimingNonMonotonic : [];
const nonMonotonicLines = nonMonotonic.length > 0
? nonMonotonic.slice(0, 80).map((item) => "- " + (item.columnLabel || item.columnId || "-") + " " + item.metric + (item.anomaly ? " " + item.anomaly : "") + " " + (item.fromValue ?? "-") + " -> " + (item.toValue ?? "-") + " delta=" + (item.delta ?? "-") + " sampleDelta=" + (item.sampleDeltaSeconds ?? "-") + " allowed=" + (item.allowedIncreaseSeconds ?? "-") + " seq " + (item.fromSeq ?? "-") + " -> " + (item.toSeq ?? "-") + " ts " + (item.fromTs || "-") + " -> " + (item.toTs || "-") + " traceId=" + (item.traceId || "-")).join("\n")
: "- 未观察到总耗时下降或最近更新异常跳增。";
const terminalGrowth = Array.isArray(sampleMetrics?.turnTimingTerminalElapsedGrowth) ? sampleMetrics.turnTimingTerminalElapsedGrowth : [];
const terminalGrowthLines = terminalGrowth.length > 0
? terminalGrowth.slice(0, 80).map((item) => "- " + (item.columnLabel || item.columnId || "-") + " terminal totalElapsed growth " + (item.fromValue ?? "-") + " -> " + (item.toValue ?? "-") + " delta=" + (item.delta ?? "-") + " status " + (item.fromStatus || "-") + " -> " + (item.toStatus || "-") + " seq " + (item.fromSeq ?? "-") + " -> " + (item.toSeq ?? "-") + " ts " + (item.fromTs || "-") + " -> " + (item.toTs || "-") + " traceId=" + (item.traceId || "-")).join("\n")
: "- 未观察到 terminal 后总耗时增长。";
const elapsedZeroResets = Array.isArray(sampleMetrics?.turnTimingElapsedZeroResets) ? sampleMetrics.turnTimingElapsedZeroResets : [];
const elapsedZeroResetLines = elapsedZeroResets.length > 0
? elapsedZeroResets.slice(0, 80).map((item) => "- " + (item.columnLabel || item.columnId || "-") + " totalElapsed zero-reset " + (item.fromValue ?? "-") + " -> " + (item.toValue ?? "-") + " delta=" + (item.delta ?? "-") + " seq " + (item.fromSeq ?? "-") + " -> " + (item.toSeq ?? "-") + " ts " + (item.fromTs || "-") + " -> " + (item.toTs || "-") + " traceId=" + (item.traceId || "-")).join("\n")
: "- 未观察到总耗时从非零跳回 0 秒。";
const totalElapsedForwardJumps = Array.isArray(sampleMetrics?.turnTimingTotalElapsedForwardJumps) ? sampleMetrics.turnTimingTotalElapsedForwardJumps : [];
const totalElapsedForwardJumpLines = totalElapsedForwardJumps.length > 0
? totalElapsedForwardJumps.slice(0, 80).map((item) => "- " + (item.columnLabel || item.columnId || "-") + " totalElapsed forward-jump " + (item.fromValue ?? "-") + " -> " + (item.toValue ?? "-") + " delta=" + (item.delta ?? "-") + " sampleDelta=" + (item.sampleDeltaSeconds ?? "-") + " allowed=" + (item.allowedIncreaseSeconds ?? "-") + " seq " + (item.fromSeq ?? "-") + " -> " + (item.toSeq ?? "-") + " ts " + (item.fromTs || "-") + " -> " + (item.toTs || "-") + " traceId=" + (item.traceId || "-")).join("\n")
: "- 未观察到总耗时超出采样间隔的异常前跳。";
const sawtoothJumps = Array.isArray(sampleMetrics?.turnTimingRecentUpdateSawtoothJumps)
? sampleMetrics.turnTimingRecentUpdateSawtoothJumps
: nonMonotonic.filter((item) => item.metric === "recentUpdateSeconds" && item.anomaly === "jump");
const sawtoothJumpLines = sawtoothJumps.length > 0
? sawtoothJumps.slice(0, 80).map((item) => "- " + (item.columnLabel || item.columnId || "-") + " recentUpdate sawtooth-jump " + (item.fromValue ?? "-") + " -> " + (item.toValue ?? "-") + " delta=" + (item.delta ?? "-") + " sampleDelta=" + (item.sampleDeltaSeconds ?? "-") + " allowed=" + (item.allowedIncreaseSeconds ?? "-") + " seq " + (item.fromSeq ?? "-") + " -> " + (item.toSeq ?? "-") + " ts " + (item.fromTs || "-") + " -> " + (item.toTs || "-") + " traceId=" + (item.traceId || "-")).join("\n")
: "- 未观察到最近更新三角波异常跳增。";
const recentUpdateSteps = Array.isArray(sampleMetrics?.turnTimingRecentUpdateLargestSteps)
? sampleMetrics.turnTimingRecentUpdateLargestSteps
: Array.isArray(sampleMetrics?.turnTimingRecentUpdateSteps)
? sampleMetrics.turnTimingRecentUpdateSteps.filter((item) => Number.isFinite(Number(item.delta))).slice().sort((a, b) => Number(b.delta) - Number(a.delta)).slice(0, 200)
: [];
const stepLines = recentUpdateSteps.length > 0
? recentUpdateSteps.slice(0, 80).map((item) => "- " + (item.columnLabel || item.columnId || "-") + " recentUpdate step " + (item.fromValue ?? "-") + " -> " + (item.toValue ?? "-") + " delta=" + (item.delta ?? "-") + " sampleDelta=" + (item.sampleDeltaSeconds ?? "-") + " allowed=" + (item.allowedIncreaseSeconds ?? "-") + " excess=" + (item.excessiveIncreaseSeconds ?? 0) + " event=" + (item.event || "-") + " seq " + (item.fromSeq ?? "-") + " -> " + (item.toSeq ?? "-") + " ts " + (item.fromTs || "-") + " -> " + (item.toTs || "-") + " traceId=" + (item.traceId || "-")).join("\n")
: "- 未观察到最近更新相邻采样 step。";
const recentUpdateResets = Array.isArray(sampleMetrics?.turnTimingRecentUpdateResets) ? sampleMetrics.turnTimingRecentUpdateResets : [];
const resetLines = recentUpdateResets.length > 0
? recentUpdateResets.slice(0, 80).map((item) => "- " + (item.columnLabel || item.columnId || "-") + " reset " + (item.fromValue ?? "-") + " -> " + (item.toValue ?? "-") + " delta=" + (item.delta ?? "-") + " sampleDelta=" + (item.sampleDeltaSeconds ?? "-") + " seq " + (item.fromSeq ?? "-") + " -> " + (item.toSeq ?? "-") + " ts " + (item.fromTs || "-") + " -> " + (item.toTs || "-") + " traceId=" + (item.traceId || "-")).join("\n")
: "- 未观察到最近更新归零/回落。";
return lines.join("\n") + "\n\n列说明:\n" + columnLines + "\n\n异常事件(仅报表暴露,不做下游 repair;最近更新按三角波模型检测异常跳增):\n" + nonMonotonicLines + "\n\nTerminal 后总耗时增长事件(终态 turn 的总耗时应 sealed,不应继续增长):\n" + terminalGrowthLines + "\n\n总耗时归零跳变事件(例如已显示真实耗时后又变成 0 秒):\n" + elapsedZeroResetLines + "\n\n总耗时异常前跳事件(预期按采样间隔近似递增;例如 14 秒 -> 1137 秒应被列入这里):\n" + totalElapsedForwardJumpLines + "\n\n最近更新 sawtooth jump 事件(预期每秒增长约 1,遇到新活动归零;例如 1 秒 -> 1 分 4 秒应被列入这里):\n" + sawtoothJumpLines + "\n\n最近更新相邻采样 step(按 delta 降序;用于人工识别一秒跳几十秒/一分钟的瞬态):\n" + stepLines + "\n\n最近更新 reset 事件(预期三角波归零,不计为异常):\n" + resetLines;
}
function formatMetricCell(value) {
if (value === null || value === undefined || !Number.isFinite(Number(value))) return "-";
return String(Number(value));
}
function escapeMarkdownCell(value) {
return String(value ?? "-").replace(/\|/gu, "\\|");
}
function renderMarkdown(report) {
const findingLines = report.findings.length === 0 ? "- 无红灯项。" : report.findings.map((item) => "- " + item.severity + ": " + item.id + " - " + item.summary).join("\n");
const commandLines = report.commandTimeline.length === 0 ? "- 无控制命令。" : report.commandTimeline.map((item) => "- " + item.ts + " " + item.phase + " " + item.type + " " + item.commandId + " " + (item.afterUrl || "")).join("\n");
const commandFailureLines = Array.isArray(report.commandFailures) && report.commandFailures.length > 0
? report.commandFailures.slice(0, 80).map((item) => "- " + (item.ts || "-") + " type=" + (item.type || "-") + " commandId=" + (item.commandId || "-") + " durationMs=" + (item.durationMs ?? "-") + " sampleSeq=" + (item.sampleSeq ?? "-") + " path=" + (item.beforePath || "-") + "->" + (item.afterPath || "-") + " message=" + escapeMarkdownCell(item.message || item.failureKind || item.name || "-")).join("\n")
: "- 无失败控制命令。";
const transitionLines = report.transitions.length === 0 ? "- 无状态变化。" : report.transitions.slice(0, 80).map((item) => "- #" + item.seq + " " + item.ts + " messages=" + item.messageCount + " traceRows=" + item.traceRowCount + " route=" + (item.routeSessionId || "-") + " active=" + (item.activeSessionId || "-")).join("\n");
const metricSummary = report.sampleMetrics?.summary || {};
const loading = report.sampleMetrics?.loading || {};
const loadingSummary = loading.summary || {};
const sessionRailTitles = report.sampleMetrics?.sessionRailTitles || {};
const sessionRailTitleSummary = sessionRailTitles.summary || {};
const codeAgentCardTiming = report.sampleMetrics?.codeAgentCardTiming || {};
const codeAgentCardTimingSummary = codeAgentCardTiming.summary || {};
const roundCompletion = codeAgentCardTiming.roundCompletion || {};
const traceOrder = report.sampleMetrics?.traceOrder || {};
const traceOrderSummary = traceOrder.summary || {};
const alertSummary = report.runtimeAlerts?.summary || {};
const httpAlertLines = Array.isArray(report.runtimeAlerts?.networkHttpErrorsByPath) && report.runtimeAlerts.networkHttpErrorsByPath.length > 0
? report.runtimeAlerts.networkHttpErrorsByPath.slice(0, 40).map((item) => "- HTTP " + (item.status ?? "-") + " " + item.method + " " + item.urlPath + " count=" + item.count + " prompts=" + (item.promptIndexes?.join(",") || "-") + " first=" + (item.firstAt || "-") + " last=" + (item.lastAt || "-")).join("\n")
: "- 无 HTTP 错误。";
const requestFailedLines = Array.isArray(report.runtimeAlerts?.networkRequestFailedByPath) && report.runtimeAlerts.networkRequestFailedByPath.length > 0
? report.runtimeAlerts.networkRequestFailedByPath.slice(0, 40).map((item) => "- requestfailed " + item.method + " " + item.urlPath + " count=" + item.count + " failure=" + (item.failureKinds?.slice(0, 4).join(",") || "-") + " prompts=" + (item.promptIndexes?.join(",") || "-") + " first=" + (item.firstAt || "-") + " last=" + (item.lastAt || "-")).join("\n")
: "- 无 requestfailed。";
const domDiagnosticLines = Array.isArray(report.runtimeAlerts?.domDiagnostics) && report.runtimeAlerts.domDiagnostics.length > 0
? report.runtimeAlerts.domDiagnostics.slice(0, 40).map((item) => "- #" + (item.seq ?? "-") + " " + (item.ts || "-") + " prompt=" + (item.promptIndex ?? "-") + " source=" + (item.source || "-") + " code=" + (item.diagnosticCode || "-") + " traceId=" + (item.traceId || "-") + " http=" + (item.httpStatus ?? "-") + " idle=" + (item.idleSeconds ?? "-") + " waitingFor=" + (item.waitingFor || "-") + " lastEventLabel=" + (item.lastEventLabel || "-") + " textHash=" + (item.textHash || "-") + " preview=" + escapeMarkdownCell(item.preview || "")).join("\n")
: "- 无 DOM 诊断文本。";
const consoleAlertLines = Array.isArray(report.runtimeAlerts?.consoleAlerts) && report.runtimeAlerts.consoleAlerts.length > 0
? report.runtimeAlerts.consoleAlerts.slice(0, 40).map((item) => "- " + (item.ts || "-") + " prompt=" + (item.promptIndex ?? "-") + " type=" + (item.type || "-") + " status=" + (item.status ?? "-") + " path=" + (item.urlPath || "-") + " traceId=" + (item.traceId || "-") + " textHash=" + (item.textHash || "-") + " preview=" + escapeMarkdownCell(item.preview || "")).join("\n")
: "- 无 console warning/error。";
const consoleAlertGroupLines = Array.isArray(report.runtimeAlerts?.consoleAlertsByPath) && report.runtimeAlerts.consoleAlertsByPath.length > 0
? report.runtimeAlerts.consoleAlertsByPath.slice(0, 40).map((item) => "- console " + (item.type || "-") + " status=" + (item.status ?? "-") + " path=" + (item.urlPath || "-") + " count=" + item.count + " prompts=" + (item.promptIndexes?.join(",") || "-") + " traces=" + (item.traceIds?.slice(0, 6).join(",") || "-")).join("\n")
: "- 无 console 分组。";
const promptNetworkLines = Array.isArray(report.promptNetwork?.rounds) && report.promptNetwork.rounds.length > 0
? report.promptNetwork.rounds.map((item) => "- round " + item.promptIndex + " promptHash=" + (item.promptTextHash || "-") + " chatPostOk=" + String(item.chatPostOk) + " failure=" + (item.failureKind || "-") + " statuses=" + (Array.isArray(item.responseStatuses) && item.responseStatuses.length > 0 ? item.responseStatuses.join(",") : "-") + " firstChat=" + (item.firstChatEventAt || "-") + " lastChat=" + (item.lastChatEventAt || "-")).join("\n")
: "- 无 prompt 网络记录。";
const roundLines = Array.isArray(report.sampleMetrics?.rounds) && report.sampleMetrics.rounds.length > 0
? report.sampleMetrics.rounds.map((item) => "- round " + item.promptIndex + " promptHash=" + (item.promptTextHash || "-") + " samples=" + item.sampleCount + " loadingSamples=" + (item.loadingSamples ?? 0) + " maxLoading=" + (item.maxLoadingCount ?? 0) + " loadingOwners=" + (item.loadingOwnerCount ?? 0) + " totalMax=" + (item.maxTotalElapsedSeconds ?? "-") + " totalLast=" + (item.lastTotalElapsedSeconds ?? "-") + " recentMax=" + (item.maxRecentUpdateSeconds ?? "-") + " recentLast=" + (item.lastRecentUpdateSeconds ?? "-") + " totalDecrease=" + (item.turnTimingTotalElapsedDecreaseCount ?? 0) + " totalForwardJump=" + (item.turnTimingTotalElapsedForwardJumpCount ?? 0) + " totalForwardJumpMax=" + (item.turnTimingTotalElapsedForwardJumpMaxSeconds ?? 0) + " terminalGrowth=" + (item.turnTimingTerminalElapsedGrowthCount ?? 0) + " terminalGrowthMax=" + (item.turnTimingTerminalElapsedGrowthMaxSeconds ?? 0) + " recentJump=" + (item.turnTimingRecentUpdateJumpCount ?? 0) + " recentSawtoothJump=" + (item.turnTimingRecentUpdateSawtoothJumpCount ?? item.turnTimingRecentUpdateJumpCount ?? 0) + " recentStep=" + (item.turnTimingRecentUpdateStepCount ?? 0) + " recentMaxIncrease=" + (item.turnTimingRecentUpdateMaxIncreaseSeconds ?? "-") + " recentMaxExcess=" + (item.turnTimingRecentUpdateMaxExcessSeconds ?? 0) + " recentReset=" + (item.turnTimingRecentUpdateResetCount ?? 0) + " diagnostics=" + item.diagnosticSamples + " terminal=" + item.terminalSamples + " finalText=" + item.finalTextSamples).join("\n")
: "- 无轮次指标。";
const cardMissingElapsedLines = Array.isArray(codeAgentCardTiming.missingElapsed) && codeAgentCardTiming.missingElapsed.length > 0
? codeAgentCardTiming.missingElapsed.slice(0, 80).map((item) => "- #" + (item.seq ?? "-") + " " + (item.ts || "-") + " prompt=" + (item.promptIndex ?? "-") + " role=" + (item.pageRole || "-") + " status=" + (item.status || "-") + " traceId=" + (item.traceId || "-") + " messageId=" + (item.messageId || "-") + " preview=" + escapeMarkdownCell(item.textPreview || "")).join("\n")
: "- 未观察到 Code Agent 卡片缺少耗时。";
const cardMissingRecentLines = Array.isArray(codeAgentCardTiming.missingRecentUpdate) && codeAgentCardTiming.missingRecentUpdate.length > 0
? codeAgentCardTiming.missingRecentUpdate.slice(0, 80).map((item) => "- #" + (item.seq ?? "-") + " " + (item.ts || "-") + " prompt=" + (item.promptIndex ?? "-") + " role=" + (item.pageRole || "-") + " status=" + (item.status || "-") + " traceId=" + (item.traceId || "-") + " messageId=" + (item.messageId || "-") + " total=" + (item.totalElapsedSeconds ?? "-") + " preview=" + escapeMarkdownCell(item.textPreview || "")).join("\n")
: "- 未观察到未终态 Code Agent 卡片缺少最近更新。";
const roundCompletionLines = Array.isArray(roundCompletion.events) && roundCompletion.events.length > 0
? roundCompletion.events.slice(0, 80).map((item) => "- #" + (item.seq ?? "-") + " " + (item.ts || "-") + " prompt=" + (item.promptIndex ?? "-") + " role=" + (item.pageRole || "-") + " traceId=" + (item.traceId || "-") + " completionElapsed=" + (item.elapsedSeconds ?? "-") + " preview=" + escapeMarkdownCell(item.preview || "")).join("\n")
: "- 未观察到“轮次完成(总耗时 ...)”trace 行。";
const roundCompletionMismatchLines = Array.isArray(roundCompletion.elapsedMismatches) && roundCompletion.elapsedMismatches.length > 0
? roundCompletion.elapsedMismatches.slice(0, 80).map((item) => "- #" + (item.seq ?? "-") + " " + (item.ts || "-") + " prompt=" + (item.promptIndex ?? "-") + " traceId=" + (item.traceId || "-") + " completion=" + (item.completionElapsedSeconds ?? "-") + " card=" + (item.cardTotalElapsedSeconds ?? "-") + " delta=" + (item.deltaSeconds ?? "-") + " tolerance=" + (item.toleranceSeconds ?? "-")).join("\n")
: "- 未观察到轮次完成耗时与卡片耗时不一致。";
const roundCompletionFinalMissingLines = Array.isArray(roundCompletion.finalResponseMissing) && roundCompletion.finalResponseMissing.length > 0
? roundCompletion.finalResponseMissing.slice(0, 80).map((item) => "- #" + (item.seq ?? "-") + " " + (item.ts || "-") + " prompt=" + (item.promptIndex ?? "-") + " role=" + (item.pageRole || "-") + " traceId=" + (item.traceId || "-") + " completionElapsed=" + (item.completionElapsedSeconds ?? "-")).join("\n")
: "- 未观察到轮次完成后 final response 缺失。";
const roundCompletionPostTimingLines = Array.isArray(roundCompletion.postCompletionTimingChanges) && roundCompletion.postCompletionTimingChanges.length > 0
? roundCompletion.postCompletionTimingChanges.slice(0, 80).map((item) => "- " + (item.columnLabel || item.columnId || "-") + " " + (item.metric || "-") + " " + (item.fromValue ?? "-") + " -> " + (item.toValue ?? "-") + " delta=" + (item.delta ?? "-") + " completionSeq=" + (item.seq ?? "-") + " seq " + (item.fromSeq ?? "-") + " -> " + (item.toSeq ?? "-") + " traceId=" + (item.traceId || "-")).join("\n")
: "- 未观察到轮次完成后耗时/最近更新继续变化。";
const durationUnderreportedLines = Array.isArray(codeAgentCardTiming.durationUnderreported) && codeAgentCardTiming.durationUnderreported.length > 0
? codeAgentCardTiming.durationUnderreported.slice(0, 80).map((item) => "- sample=" + (item.sampleIndex ?? "-") + " " + (item.timestamp || "-") + " role=" + (item.pageRole || "-") + " status=" + (item.status || "-") + " traceId=" + (item.traceId || "-") + " card=" + (item.cardTotalElapsedSeconds ?? "-") + "s expected=" + (item.expectedElapsedSeconds ?? "-") + "s delta=" + (item.deltaSeconds ?? "-") + "s evidence=" + (item.evidenceKind || "-") + " preview=" + escapeMarkdownCell(item.evidencePreview || item.cardPreview || "")).join("\n")
: "- 未观察到 Code Agent 卡片耗时低于 trace/final-response 证据。";
const durationMismatchLines = Array.isArray(codeAgentCardTiming.durationMismatches) && codeAgentCardTiming.durationMismatches.length > 0
? codeAgentCardTiming.durationMismatches.slice(0, 80).map((item) => "- sample=" + (item.sampleIndex ?? "-") + " " + (item.timestamp || "-") + " role=" + (item.pageRole || "-") + " status=" + (item.status || "-") + " traceId=" + (item.traceId || "-") + " direction=" + (item.direction || "-") + " card=" + (item.cardTotalElapsedSeconds ?? "-") + "s expected=" + (item.expectedElapsedSeconds ?? "-") + "s signedDelta=" + (item.signedDeltaSeconds ?? "-") + "s delta=" + (item.deltaSeconds ?? "-") + "s tolerance=" + (item.toleranceSeconds ?? "-") + "s evidence=" + (item.evidenceKind || "-") + " exact=" + String(item.exactEvidence === true) + " preview=" + escapeMarkdownCell(item.evidencePreview || item.cardPreview || "")).join("\n")
: "- 未观察到 Code Agent 卡片耗时与 completion/final-response 封口证据不一致。";
const traceOrderAnomalyLines = Array.isArray(traceOrder.orderAnomalies) && traceOrder.orderAnomalies.length > 0
? traceOrder.orderAnomalies.slice(0, 80).map((item) => "- sample=" + (item.sampleIndex ?? "-") + " " + (item.timestamp || "-") + " role=" + (item.pageRole || "-") + " traceId=" + (item.traceId || "-") + " rows=" + (item.previousRowIndex ?? "-") + "->" + (item.currentRowIndex ?? "-") + " reasons=" + (Array.isArray(item.reasons) ? item.reasons.join(",") : "-") + " total=" + (item.previousTotalSeconds ?? "-") + "->" + (item.currentTotalSeconds ?? "-") + " clock=" + (item.previousClockSeconds ?? "-") + "->" + (item.currentClockSeconds ?? "-") + " preview=" + escapeMarkdownCell((item.previousPreview || "") + " / " + (item.currentPreview || ""))).join("\n")
: "- 未观察到可见 trace 行顺序非单调。";
const traceCompletionNotLastLines = Array.isArray(traceOrder.completionNotLast) && traceOrder.completionNotLast.length > 0
? traceOrder.completionNotLast.slice(0, 80).map((item) => "- sample=" + (item.sampleIndex ?? "-") + " " + (item.timestamp || "-") + " role=" + (item.pageRole || "-") + " traceId=" + (item.traceId || "-") + " rows=" + (item.completionRowIndex ?? "-") + "->" + (item.laterRowIndex ?? "-") + " total=" + (item.completionTotalSeconds ?? "-") + "->" + (item.laterTotalSeconds ?? "-") + " completion=" + escapeMarkdownCell(item.completionPreview || "") + " later=" + escapeMarkdownCell(item.laterPreview || "")).join("\n")
: "- 未观察到 completion 行后还有同 trace 后续行。";
const loadingSegmentLines = Array.isArray(loading.segments) && loading.segments.length > 0
? loading.segments.slice(0, 80).map((item) => "- observedDuration=" + (item.durationSeconds ?? 0) + "s upperBound=" + (item.upperBoundSeconds ?? item.durationSeconds ?? 0) + "s endedGap=" + (item.endedGapSeconds ?? "-") + "s samples=" + (item.sampleCount ?? 0) + " countMax=" + (item.maxCount ?? 0) + " owners=" + (item.ownerCount ?? 0) + " seq=" + (item.firstSeq ?? "-") + ".." + (item.lastSeq ?? "-") + " ts=" + (item.firstAt || "-") + ".." + (item.lastAt || "-") + " endedAt=" + (item.endedAt || (item.ongoing ? "ongoing" : "-")) + " ownerLabels=" + ((Array.isArray(item.owners) ? item.owners : []).slice(0, 6).map((owner) => (owner.ownerKind || "-") + ":" + (owner.ownerLabel || "-") + "x" + (owner.count ?? 0)).join(",") || "-")).join("\n")
: "- 未观察到“加载中”可见区间。";
const loadingOwnerLines = Array.isArray(loading.owners) && loading.owners.length > 0
? loading.owners.slice(0, 80).map((item) => "- " + (item.ownerKind || "-") + " " + escapeMarkdownCell(item.ownerLabel || item.ownerKey || "-") + " samples=" + (item.sampleCount ?? 0) + " occurrences=" + (item.occurrenceCount ?? 0) + " maxCount=" + (item.maxSimultaneousCount ?? 0) + " longest=" + (item.longestContinuousSeconds ?? 0) + "s seq=" + (item.firstSeq ?? "-") + ".." + (item.lastSeq ?? "-") + " prompts=" + (Array.isArray(item.promptIndexes) ? item.promptIndexes.join(",") : "-")).join("\n")
: "- 未观察到“加载中”归属。";
const loadingTimelineLines = Array.isArray(loading.timeline) && loading.timeline.length > 0
? loading.timeline.slice(0, 160).map((item) => "- #" + (item.seq ?? "-") + " " + (item.ts || "-") + " prompt=" + (item.promptIndex ?? "-") + " loadingCount=" + (item.loadingCount ?? 0) + " ownerCount=" + (item.ownerCount ?? 0) + " owners=" + ((Array.isArray(item.owners) ? item.owners : []).slice(0, 6).map((owner) => (owner.ownerKind || "-") + ":" + (owner.ownerLabel || "-") + "x" + (owner.count ?? 0)).join(",") || "-")).join("\n")
: "- 未观察到“加载中”采样点。";
const sessionRailTitleSampleLines = Array.isArray(sessionRailTitles.samples) && sessionRailTitles.samples.length > 0
? sessionRailTitles.samples.slice(0, 80).map((item) => "- #" + (item.seq ?? "-") + " " + (item.ts || "-") + " role=" + (item.pageRole || "-") + " visible=" + (item.visibleCount ?? 0) + " fallback=" + (item.fallbackTitleCount ?? 0) + " ratio=" + (item.fallbackTitleRatio ?? 0) + " examples=" + ((Array.isArray(item.examples) ? item.examples : []).slice(0, 4).map((example) => escapeMarkdownCell(example.titlePreview || example.titleHash || "-")).join(",") || "-")).join("\n")
: "- 未观察到超过一半 fallback 的 session 列表采样点。";
const sessionRailTitleExampleLines = Array.isArray(sessionRailTitles.examples) && sessionRailTitles.examples.length > 0
? sessionRailTitles.examples.slice(0, 80).map((item) => "- firstSeq=" + (item.firstSeq ?? "-") + " role=" + (item.pageRole || "-") + " active=" + String(item.active === true) + " sessionPrefix=" + (item.sessionIdPrefix || "-") + " titleHash=" + (item.titleHash || "-") + " preview=" + escapeMarkdownCell(item.titlePreview || "")).join("\n")
: "- 无 fallback 标题示例。";
const provenanceLines = Array.isArray(report.pageProvenance?.segments) && report.pageProvenance.segments.length > 0
? report.pageProvenance.segments.slice(0, 40).map((item) => "- fingerprint=" + (item.assetFingerprint || "-") + " samples=" + item.sampleCount + " seq=" + (item.firstSeq ?? "-") + ".." + (item.lastSeq ?? "-") + " ts=" + (item.firstAt || "-") + ".." + (item.lastAt || "-") + " scripts=" + (item.scriptCount ?? "-") + " styles=" + (item.stylesheetCount ?? "-") + " urlPaths=" + (Array.isArray(item.urlPaths) ? item.urlPaths.slice(0, 4).join(",") : "-")).join("\n")
: "- 无页面 provenance segment。";
const ordinaryPerformanceItems = Array.isArray(report.pagePerformance?.sameOriginApiByPath) ? report.pagePerformance.sameOriginApiByPath.filter((item) => item.isLongLivedStream !== true) : [];
const streamPerformanceItems = Array.isArray(report.pagePerformance?.sameOriginApiByPath) ? report.pagePerformance.sameOriginApiByPath.filter((item) => item.isLongLivedStream === true) : [];
const sameOriginApiBudgetMs = Number(report.alertThresholds?.sameOriginApiSlowMs ?? report.pagePerformance?.summary?.budgetMs);
const streamOpenBudgetMs = Number(report.alertThresholds?.longLivedStreamOpenSlowMs);
const performanceLines = ordinaryPerformanceItems.length > 0
? ordinaryPerformanceItems.slice(0, 80).map((item) => "- " + item.path + " kind=" + (item.routeKind || "same-origin-api") + " budgetMetric=" + (item.budgetMetric || "durationMs") + " samples=" + item.sampleCount + " p50=" + (item.p50Ms ?? "-") + "ms p75=" + (item.p75Ms ?? "-") + "ms p95=" + (item.p95Ms ?? "-") + "ms max=" + (item.maxMs ?? "-") + "ms >budget=" + (item.overBudgetCount ?? item.overFiveSecondCount ?? 0) + " budgetMs=" + (item.budgetMs ?? sameOriginApiBudgetMs) + " legacy>5s=" + (item.overFiveSecondCount ?? 0) + " window=" + (item.firstAt || "-") + ".." + (item.lastAt || "-")).join("\n")
: "- 无同源 API Resource Timing 样本。";
const streamPerformanceLines = streamPerformanceItems.length > 0
? streamPerformanceItems.slice(0, 80).map((item) => "- " + item.path + " kind=" + (item.routeKind || "same-origin-api-stream") + " samples=" + item.sampleCount + " streamOpenP50=" + (item.streamOpenP50Ms ?? "-") + "ms streamOpenP75=" + (item.streamOpenP75Ms ?? "-") + "ms streamOpenP95=" + (item.streamOpenP95Ms ?? "-") + "ms streamOpenMax=" + (item.streamOpenMaxMs ?? "-") + "ms streamOpen>budget=" + (item.streamOpenOverBudgetCount ?? item.streamOpenOverFiveSecondCount ?? 0) + " streamOpenBudgetMs=" + (item.streamOpenBudgetMs ?? streamOpenBudgetMs) + " streamOpenLegacy>5s=" + (item.streamOpenOverFiveSecondCount ?? 0) + " streamLifetime>5s=" + (item.streamLifetimeOverFiveSecondCount ?? 0) + " lifetimeMax=" + (item.maxMs ?? "-") + "ms window=" + (item.firstAt || "-") + ".." + (item.lastAt || "-")).join("\n")
: "- 无同源长连接 Resource Timing 样本。";
const metricLines = Array.isArray(report.sampleMetrics?.timeline) && report.sampleMetrics.timeline.length > 0
? report.sampleMetrics.timeline.slice(0, 120).map((item) => "- #" + item.seq + " " + item.ts + " prompt=" + item.promptIndex + " loadingCount=" + (item.loadingCount ?? 0) + " loadingOwners=" + (item.loadingOwnerCount ?? 0) + " totalElapsedSeconds=" + (item.totalElapsedSeconds ?? "-") + " recentUpdateSeconds=" + (item.recentUpdateSeconds ?? "-") + " terminal=" + item.terminalSeen + " finalText=" + item.finalResultTextSeen + " diagnostic=" + item.diagnosticSeen).join("\n")
: "- 无采样指标。";
const turnTimingTable = renderTurnTimingTable(report.sampleMetrics);
return "# web-probe observe analysis\n\n"
+ "- stateDir: " + report.stateDir + "\n"
+ "- generatedAt: " + report.generatedAt + "\n"
+ "- samples: " + report.counts.samples + "\n"
+ "- control: " + report.counts.control + "\n"
+ "- network: " + report.counts.network + "\n"
+ "- console: " + (report.counts.console ?? 0) + "\n"
+ "- errors: " + report.counts.errors + "\n\n"
+ "## Findings\n\n" + findingLines + "\n\n"
+ "## Command failures\n\n" + commandFailureLines + "\n\n"
+ "## Sample metrics\n\n"
+ "- sampleCount: " + (metricSummary.sampleCount ?? 0) + "\n"
+ "- withTotalElapsed: " + (metricSummary.withTotalElapsed ?? 0) + "\n"
+ "- withRecentUpdate: " + (metricSummary.withRecentUpdate ?? 0) + "\n"
+ "- diagnostics: " + (metricSummary.diagnostics ?? 0) + "\n"
+ "- loadingSamples: " + (metricSummary.loadingSampleCount ?? 0) + "\n"
+ "- loadingMaxCount: " + (metricSummary.loadingMaxCount ?? 0) + "\n"
+ "- loadingMaxOwnerCount: " + (metricSummary.loadingMaxOwnerCount ?? 0) + "\n"
+ "- loadingOwnerCount: " + (metricSummary.loadingOwnerCount ?? 0) + "\n"
+ "- loadingLongestContinuousSeconds: " + (metricSummary.loadingLongestContinuousSeconds ?? 0) + "\n"
+ "- loadingCurrentContinuousSeconds: " + (metricSummary.loadingCurrentContinuousSeconds ?? 0) + "\n"
+ "- loadingOverFiveSecondSegmentCount: " + (metricSummary.loadingOverFiveSecondSegmentCount ?? 0) + "\n"
+ "- sessionRailFallbackMajoritySampleCount: " + (metricSummary.sessionRailFallbackMajoritySampleCount ?? 0) + "\n"
+ "- sessionRailFallbackMaxRatio: " + (metricSummary.sessionRailFallbackMaxRatio ?? 0) + "\n"
+ "- sessionRailFallbackMaxCount: " + (metricSummary.sessionRailFallbackMaxCount ?? 0) + "\n"
+ "- promptSegments: " + (metricSummary.promptSegments ?? 0) + "\n\n"
+ "- turnColumns: " + (metricSummary.turnColumns ?? 0) + "\n"
+ "- turnTimingRows: " + (metricSummary.turnTimingRows ?? 0) + "\n"
+ "- turnTimingNonMonotonicCount: " + (metricSummary.turnTimingNonMonotonicCount ?? 0) + "\n"
+ "- turnTimingTotalElapsedDecreaseCount: " + (metricSummary.turnTimingTotalElapsedDecreaseCount ?? 0) + "\n"
+ "- turnTimingTotalElapsedForwardJumpCount: " + (metricSummary.turnTimingTotalElapsedForwardJumpCount ?? 0) + "\n"
+ "- turnTimingTotalElapsedForwardJumpMaxSeconds: " + (metricSummary.turnTimingTotalElapsedForwardJumpMaxSeconds ?? 0) + "\n"
+ "- turnTimingTerminalElapsedGrowthCount: " + (metricSummary.turnTimingTerminalElapsedGrowthCount ?? 0) + "\n"
+ "- turnTimingTerminalElapsedGrowthMaxSeconds: " + (metricSummary.turnTimingTerminalElapsedGrowthMaxSeconds ?? 0) + "\n"
+ "- turnTimingRecentUpdateJumpCount: " + (metricSummary.turnTimingRecentUpdateJumpCount ?? 0) + "\n"
+ "- turnTimingRecentUpdateSawtoothJumpCount: " + (metricSummary.turnTimingRecentUpdateSawtoothJumpCount ?? metricSummary.turnTimingRecentUpdateJumpCount ?? 0) + "\n"
+ "- turnTimingRecentUpdateStepCount: " + (metricSummary.turnTimingRecentUpdateStepCount ?? 0) + "\n"
+ "- turnTimingRecentUpdateMaxIncreaseSeconds: " + (metricSummary.turnTimingRecentUpdateMaxIncreaseSeconds ?? "-") + "\n"
+ "- turnTimingRecentUpdateMaxExcessSeconds: " + (metricSummary.turnTimingRecentUpdateMaxExcessSeconds ?? 0) + "\n"
+ "- turnTimingRecentUpdateResetCount: " + (metricSummary.turnTimingRecentUpdateResetCount ?? 0) + "\n\n"
+ "- codeAgentCardSampleCount: " + (metricSummary.codeAgentCardSampleCount ?? 0) + "\n"
+ "- codeAgentCardMissingElapsedCount: " + (metricSummary.codeAgentCardMissingElapsedCount ?? 0) + "\n"
+ "- codeAgentCardMissingRecentUpdateCount: " + (metricSummary.codeAgentCardMissingRecentUpdateCount ?? 0) + "\n"
+ "- codeAgentCardDurationUnderreportedCount: " + (metricSummary.codeAgentCardDurationUnderreportedCount ?? 0) + "\n"
+ "- codeAgentCardDurationMismatchCount: " + (metricSummary.codeAgentCardDurationMismatchCount ?? 0) + "\n"
+ "- traceRowCount: " + (metricSummary.traceRowCount ?? 0) + "\n"
+ "- traceRowOrderAnomalyCount: " + (metricSummary.traceRowOrderAnomalyCount ?? 0) + "\n"
+ "- traceRowCompletionNotLastCount: " + (metricSummary.traceRowCompletionNotLastCount ?? 0) + "\n"
+ "- roundCompletionEventCount: " + (metricSummary.roundCompletionEventCount ?? 0) + "\n"
+ "- roundCompletionElapsedMismatchCount: " + (metricSummary.roundCompletionElapsedMismatchCount ?? 0) + "\n"
+ "- roundCompletionFinalResponseMissingCount: " + (metricSummary.roundCompletionFinalResponseMissingCount ?? 0) + "\n"
+ "- roundCompletionPostTimingChangeCount: " + (metricSummary.roundCompletionPostTimingChangeCount ?? 0) + "\n\n"
+ "### Rounds\n\n" + roundLines + "\n\n"
+ "### Code Agent card timing display\n\n"
+ "- cardSampleCount: " + (codeAgentCardTimingSummary.cardSampleCount ?? 0) + "\n"
+ "- runningCardSampleCount: " + (codeAgentCardTimingSummary.runningCardSampleCount ?? 0) + "\n"
+ "- terminalCardSampleCount: " + (codeAgentCardTimingSummary.terminalCardSampleCount ?? 0) + "\n"
+ "- missingElapsedCount: " + (codeAgentCardTimingSummary.missingElapsedCount ?? 0) + "\n"
+ "- missingRecentUpdateCount: " + (codeAgentCardTimingSummary.missingRecentUpdateCount ?? 0) + "\n"
+ "- durationUnderreportedCount: " + (codeAgentCardTimingSummary.durationUnderreportedCount ?? 0) + "\n"
+ "- durationMismatchCount: " + (codeAgentCardTimingSummary.durationMismatchCount ?? 0) + "\n"
+ "- policy: Code Agent 卡片无论终态/非终态都必须显示耗时;非终态必须显示最近更新。该 analyzer 只报告采样到的页面表现,不做下游 repair。\n\n"
+ "#### Missing elapsed samples\n\n" + cardMissingElapsedLines + "\n\n"
+ "#### Missing recent update samples\n\n" + cardMissingRecentLines + "\n\n"
+ "#### Duration underreported samples\n\n" + durationUnderreportedLines + "\n\n"
+ "#### Duration mismatch samples\n\n" + durationMismatchLines + "\n\n"
+ "### Trace row visual order\n\n"
+ "- traceRowCount: " + (traceOrderSummary.traceRowCount ?? 0) + "\n"
+ "- orderAnomalyCount: " + (traceOrderSummary.orderAnomalyCount ?? 0) + "\n"
+ "- completionNotLastCount: " + (traceOrderSummary.completionNotLastCount ?? 0) + "\n"
+ "- policy: 可见 trace 行在同一 trace 内必须按 total/时钟/projected seq 单调展示;completion 行不得出现在同 trace 后续行之前。\n\n"
+ "#### Trace order anomalies\n\n" + traceOrderAnomalyLines + "\n\n"
+ "#### Completion row not last samples\n\n" + traceCompletionNotLastLines + "\n\n"
+ "### Round completion consistency\n\n"
+ "- completionEventCount: " + (codeAgentCardTimingSummary.roundCompletionEventCount ?? 0) + "\n"
+ "- elapsedMismatchCount: " + (codeAgentCardTimingSummary.roundCompletionElapsedMismatchCount ?? 0) + "\n"
+ "- finalResponseMissingCount: " + (codeAgentCardTimingSummary.roundCompletionFinalResponseMissingCount ?? 0) + "\n"
+ "- postTimingChangeCount: " + (codeAgentCardTimingSummary.roundCompletionPostTimingChangeCount ?? 0) + "\n"
+ "- postRecentUpdateVisibleCount: " + (codeAgentCardTimingSummary.roundCompletionPostRecentUpdateVisibleCount ?? 0) + "\n"
+ "- elapsedMismatchToleranceSeconds: " + (codeAgentCardTimingSummary.elapsedMismatchToleranceSeconds ?? "-") + "\n"
+ "- policy: 轮次完成(总耗时 ...) 的耗时必须与卡片总耗时一致;完成后 final response 必须可见,耗时/最近更新不得继续跳变。\n\n"
+ "#### Round completion events\n\n" + roundCompletionLines + "\n\n"
+ "#### Completion elapsed mismatches\n\n" + roundCompletionMismatchLines + "\n\n"
+ "#### Final response missing after completion\n\n" + roundCompletionFinalMissingLines + "\n\n"
+ "#### Post-completion timing changes\n\n" + roundCompletionPostTimingLines + "\n\n"
+ "### Loading visibility: visible 加载中\n\n"
+ "- sampleCount: " + (loadingSummary.sampleCount ?? 0) + "\n"
+ "- loadingSampleCount: " + (loadingSummary.loadingSampleCount ?? 0) + "\n"
+ "- maxSimultaneousCount: " + (loadingSummary.maxSimultaneousCount ?? 0) + "\n"
+ "- maxSimultaneousOwnerCount: " + (loadingSummary.maxSimultaneousOwnerCount ?? 0) + "\n"
+ "- concurrentLoadingSampleCount: " + (loadingSummary.concurrentLoadingSampleCount ?? 0) + "\n"
+ "- ownerCount: " + (loadingSummary.ownerCount ?? 0) + "\n"
+ "- segmentCount: " + (loadingSummary.segmentCount ?? 0) + "\n"
+ "- overFiveSecondSegmentCount: " + (loadingSummary.overFiveSecondSegmentCount ?? 0) + "\n"
+ "- longestContinuousSeconds: " + (loadingSummary.longestContinuousSeconds ?? 0) + "\n"
+ "- currentContinuousSeconds: " + (loadingSummary.currentContinuousSeconds ?? 0) + "\n"
+ "- budgetSeconds: " + (loadingSummary.budgetSeconds ?? (Number.isFinite(Number(report.alertThresholds?.visibleLoadingSlowMs)) ? Number(report.alertThresholds.visibleLoadingSlowMs) / 1000 : "unconfigured")) + "\n"
+ "- policy: 该指标只能证明用户真实看到“加载中”的持续时间;修复必须降低真实请求/投影/渲染耗时,禁止提前展示未加载完内容来压低该指标。\n\n"
+ "#### Loading segments\n\n" + loadingSegmentLines + "\n\n"
+ "#### Loading owners\n\n" + loadingOwnerLines + "\n\n"
+ "#### Loading sample timeline\n\n" + loadingTimelineLines + "\n\n"
+ "### Session rail titles\n\n"
+ "- sampleCount: " + (sessionRailTitleSummary.sampleCount ?? 0) + "\n"
+ "- visibleSampleCount: " + (sessionRailTitleSummary.visibleSampleCount ?? 0) + "\n"
+ "- fallbackSampleCount: " + (sessionRailTitleSummary.fallbackSampleCount ?? 0) + "\n"
+ "- majorityFallbackSampleCount: " + (sessionRailTitleSummary.majorityFallbackSampleCount ?? 0) + "\n"
+ "- maxFallbackRatio: " + (sessionRailTitleSummary.maxFallbackRatio ?? 0) + "\n"
+ "- maxVisibleCount: " + (sessionRailTitleSummary.maxVisibleCount ?? 0) + "\n"
+ "- maxFallbackTitleCount: " + (sessionRailTitleSummary.maxFallbackTitleCount ?? 0) + "\n"
+ "- policy: 可见 session 列表中 'Session ses_...' fallback 标题超过一半必须报警;修复应让上游 session list projection 直接携带名称,不能靠点击详情后下游修补。\n\n"
+ "#### Session rail fallback samples\n\n" + sessionRailTitleSampleLines + "\n\n"
+ "#### Session rail fallback examples\n\n" + sessionRailTitleExampleLines + "\n\n"
+ "### Page provenance\n\n"
+ "- segmentCount: " + (report.pageProvenance?.summary?.segmentCount ?? 0) + "\n"
+ "- controlSegmentCount: " + (report.pageProvenance?.summary?.controlSegmentCount ?? 0) + "\n\n"
+ provenanceLines + "\n\n"
+ "### Page performance: same-origin API Resource Timing\n\n"
+ "- budgetMs: " + (report.pagePerformance?.summary?.budgetMs ?? sameOriginApiBudgetMs) + "\n"
+ "- sameOriginApiPathCount: " + (report.pagePerformance?.summary?.sameOriginApiPathCount ?? 0) + "\n"
+ "- sameOriginApiSampleCount: " + (report.pagePerformance?.summary?.sameOriginApiSampleCount ?? 0) + "\n"
+ "- longLivedStreamPathCount: " + (report.pagePerformance?.summary?.longLivedStreamPathCount ?? 0) + "\n"
+ "- longLivedStreamSampleCount: " + (report.pagePerformance?.summary?.longLivedStreamSampleCount ?? 0) + "\n"
+ "- longLivedStreamOpenOverFiveSecondPathCount: " + (report.pagePerformance?.summary?.longLivedStreamOpenOverFiveSecondPathCount ?? 0) + "\n"
+ "- longLivedStreamOpenOverFiveSecondSampleCount: " + (report.pagePerformance?.summary?.longLivedStreamOpenOverFiveSecondSampleCount ?? 0) + "\n"
+ "- longLivedStreamLifetimeOverFiveSecondSampleCount: " + (report.pagePerformance?.summary?.longLivedStreamLifetimeOverFiveSecondSampleCount ?? 0) + "\n"
+ "- slowPathCount: " + (report.pagePerformance?.summary?.slowPathCount ?? 0) + "\n"
+ "- slowSampleCount: " + (report.pagePerformance?.summary?.slowSampleCount ?? 0) + "\n"
+ "- worstP95Ms: " + (report.pagePerformance?.summary?.worstP95Ms ?? "-") + "\n\n"
+ performanceLines + "\n\n"
+ "### Page performance: long-lived streams\n\n"
+ "- policy: SSE/long-lived stream lifetime is not ordinary API load latency; only stream open latency is compared with the YAML usability budget, while disconnects remain runtime alerts.\n\n"
+ streamPerformanceLines + "\n\n"
+ "### Prompt network\n\n" + promptNetworkLines + "\n\n"
+ "### Runtime alerts\n\n"
+ "- httpErrorCount: " + (alertSummary.httpErrorCount ?? 0) + "\n"
+ "- requestFailedCount: " + (alertSummary.requestFailedCount ?? 0) + "\n"
+ "- domDiagnosticSampleCount: " + (alertSummary.domDiagnosticSampleCount ?? 0) + "\n"
+ "- consoleAlertCount: " + (alertSummary.consoleAlertCount ?? 0) + "\n"
+ "- pageErrorCount: " + (alertSummary.pageErrorCount ?? 0) + "\n\n"
+ "#### HTTP errors\n\n" + httpAlertLines + "\n\n"
+ "#### Request failed\n\n" + requestFailedLines + "\n\n"
+ "#### DOM diagnostics\n\n" + domDiagnosticLines + "\n\n"
+ "#### Console alerts\n\n" + consoleAlertLines + "\n\n"
+ "#### Console alert groups\n\n" + consoleAlertGroupLines + "\n\n"
+ "### Turn timing table\n\n"
+ turnTimingTable + "\n\n"
+ "### Aggregate timeline\n\n"
+ metricLines + "\n\n"
+ "## Command timeline\n\n" + commandLines + "\n\n"
+ "## State transitions\n\n" + transitionLines + "\n";
}
async function fileMeta(file) {
const [buffer, stats] = await Promise.all([readFile(file), stat(file)]);
return { byteCount: stats.size, sha256: "sha256:" + createHash("sha256").update(buffer).digest("hex") };
}
function sha256(value) {
return "sha256:" + createHash("sha256").update(String(value)).digest("hex");
}
function urlPath(value) {
try {
const url = new URL(String(value || "http://invalid.local/"));
return url.pathname;
} catch {
return "-";
}
}
function compactLocation(value) {
if (!value || typeof value !== "object") return null;
return { urlPath: urlPath(value.url), lineNumber: value.lineNumber ?? null, columnNumber: value.columnNumber ?? null };
}
function limitText(value, limit) {
const text = String(value ?? "");
if (text.length <= limit) return text;
return text.slice(0, Math.max(0, limit - 1)) + "…";
}
`;
}