// 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)) + "…"; } `; }