From baf0c2bb0568e210296df62e990603afb64b5e1e Mon Sep 17 00:00:00 2001 From: Lyon <88232613+pikasTech@users.noreply.github.com> Date: Thu, 25 Jun 2026 03:38:05 +0800 Subject: [PATCH] feat: add web-probe observe control views (#848) Co-authored-by: Codex --- .agents/skills/unidesk-webdev/SKILL.md | 10 +- scripts/src/hwlab-node-help.ts | 7 +- scripts/src/hwlab-node-impl.ts | 465 +- .../hwlab-node-web-observe-analyzer-source.ts | 5002 ++++++++++++++++ scripts/src/hwlab-node-web-observe-collect.ts | 169 + scripts/src/hwlab-node-web-observe-render.ts | 465 ++ ...-node-web-observe-runner-actions-source.ts | 49 + .../hwlab-node-web-observe-runner-source.ts | 5008 +---------------- 8 files changed, 5756 insertions(+), 5419 deletions(-) create mode 100644 scripts/src/hwlab-node-web-observe-analyzer-source.ts create mode 100644 scripts/src/hwlab-node-web-observe-collect.ts create mode 100644 scripts/src/hwlab-node-web-observe-render.ts create mode 100644 scripts/src/hwlab-node-web-observe-runner-actions-source.ts diff --git a/.agents/skills/unidesk-webdev/SKILL.md b/.agents/skills/unidesk-webdev/SKILL.md index f9013c3e..3a644ac4 100644 --- a/.agents/skills/unidesk-webdev/SKILL.md +++ b/.agents/skills/unidesk-webdev/SKILL.md @@ -49,6 +49,8 @@ web-probe 入口分三类: - `script`:受控 Playwright 托管脚本,适合一轮 55 秒内完成的 DOM/API 断言、截图、route/intercept 和边界采样。 - `observe`:纯客户端长程观测,适合同一 Workbench session 多轮任务、realtime/projection 问题、长时间 trace/DOM/network 采样和无副作用报告生成。长程 Workbench 观测默认同时打开两个浏览器页面:control 页面只执行显式 `observe command` 用户动作,observer 页面只打开同一个 session 做被动观察,并默认每 3 分钟整页刷新一次同一 session,模拟用户离开后返回,用来抓多用户/多页面下同一 session 的投影差异、历史 trace 丢失、耗时跳变和 loading 差异。 +工测优先级:Workbench trace 乱序、完成行位置、耗时不一致、时间跳变、final response flicker、session 不刷新、性能慢路径和多轮可靠性问题,默认先用 `observe` 采样并跑 `observe analyze`;不要把临时 Playwright spec、裸 DOM 调试脚本或单次 `script` 结果当作主要工测证据。`script/run` 只作为短路径断言、截图/API 摘要或 observe/analyze 后的补充复核。 + 需要 Playwright route/intercept、延迟 API、读取 in-flight DOM 或截图时仍使用受控 `web-probe script`,不要裸写 Playwright: ```bash @@ -84,10 +86,14 @@ bun scripts/cli.ts hwlab nodes web-probe observe start --node D601 --lane v03 -- bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type newSession bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type selectProvider --provider codex-api bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type sendPrompt --text 'ping' +bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type steer --text '继续观察当前 trace' +bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type cancel bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type sendPrompt --text-stdin <<'EOF' long prompt EOF bun scripts/cli.ts hwlab nodes web-probe observe status webobs-xxxx --tail-lines 6 +bun scripts/cli.ts hwlab nodes web-probe observe collect webobs-xxxx --view turn-summary +bun scripts/cli.ts hwlab nodes web-probe observe collect webobs-xxxx --view trace-frame --trace-id trc_xxx --sample-seq 42 bun scripts/cli.ts hwlab nodes web-probe observe stop webobs-xxxx bun scripts/cli.ts hwlab nodes web-probe observe analyze webobs-xxxx ``` @@ -98,7 +104,9 @@ bun scripts/cli.ts hwlab nodes web-probe observe analyze webobs-xxxx - `web-probe script` 不运行默认探针,必须通过 stdin heredoc 或 `--script-file ` 提供脚本;只需要 repo-owned 标准 DOM probe 时使用 `web-probe run`。 - `web-probe run|script|observe start` 的默认 URL、browser proxy mode 和 observe/analyze 报警阈值必须来自 `config/hwlab-node-lanes.yaml` 的 `webProbe`;需要排除公网/FRP/跨国 proxy 抖动时,在 YAML 里把目标 node/lane 的 `webProbe.defaultOrigin` 配成内部 Service ClusterIP origin,不要在命令行长期手写 `--url` 或裸 Playwright。 -- `web-probe observe start` 默认是被动观测:记录 DOM 摘要、自然页面 request/response/requestfailed、截图和 performance 样本,不主动 fetch Workbench API、不切换 control session、不拦截路由、不调用 repair helper。长程 Workbench 观测必须保留 control/observer 双页面模型:control 页面执行显式 command,observer 页面只同步到同一 session URL 后被动采样,并按默认 180000ms 周期整页刷新同一 session 来模拟用户往返;周期刷新只作用于 observer,不得改变 control active session 或作为通过条件。两页的 `pageRole`、`pageId`、`sampleGroupSeq` 必须进入样本和 analyzer 报表。任何 `newSession`、`selectProvider`、`sendPrompt`、`goto`、`screenshot`、`mark`、`stop` 都必须通过 `observe command` 显式下发,并进入 `control.jsonl`;长 prompt 必须优先用 `sendPrompt --text-stdin`,不要为了绕开 shell quoting 退回裸 Playwright 或临时脚本。 +- `web-probe observe start` 默认是被动观测:记录 DOM 摘要、自然页面 request/response/requestfailed、截图和 performance 样本,不主动 fetch Workbench API、不切换 control session、不拦截路由、不调用 repair helper。长程 Workbench 观测必须保留 control/observer 双页面模型:control 页面执行显式 command,observer 页面只同步到同一 session URL 后被动采样,并按默认 180000ms 周期整页刷新同一 session 来模拟用户往返;周期刷新只作用于 observer,不得改变 control active session 或作为通过条件。两页的 `pageRole`、`pageId`、`sampleGroupSeq` 必须进入样本和 analyzer 报表。任何 `newSession`、`selectProvider`、`sendPrompt`、`steer`、`cancel`、`goto`、`screenshot`、`mark`、`stop` 都必须通过 `observe command` 显式下发,并进入 `control.jsonl`;长 prompt 必须优先用 `sendPrompt --text-stdin` 或 `steer --text-stdin`,不要为了绕开 shell quoting 退回裸 Playwright 或临时脚本。 +- `observe command --type steer` 和 `--type cancel` 是显式用户/control action:steer 复用当前 Workbench composer 的运行中 turn 引导路径,cancel 复用同一 composer 主按钮的取消路径。二者必须进入 `control.jsonl`,不能用后端私有 API、AgentRun direct cancel 或测试后门替代。 +- `observe collect --view turn-summary` 是第一层 CLI 阅读视图:只从 `samples.jsonl`、`control.jsonl` 和已有 `analysis/report.json` 按需渲染同一 session 的多 turn 摘要,包含用户消息 preview/hash、traceId、状态、耗时/最近更新时间、steer/cancel 标记和 Final Response 摘要。`observe collect --view trace-frame --trace-id --sample-seq ` 是第二层 CLI 阅读视图:从同一采样帧渲染单帧 trace 文字截图,并固定输出 `Final Response` 区块。collect 视图不是采样器新增保存物,不构成第二事实源。 - `web-probe observe` 的 issue evidence 优先记录 observer id、stateDir、report JSON/Markdown SHA、samples/control/network/artifact 计数、routeSessionId、activeSessionId、prompt hash/textBytes、traceId、AgentRun runId/commandId、最终 status 和必要摘要;不要把 prompt 原文、assistant 大段正文、完整 stdout/stderr 或 provider payload 粘贴到 issue。 - 多轮 Workbench 采样必须证明同一个 `sessionId` 连续承载所有轮次;每轮至少记录 prompt hash、traceId、终态、最终回答摘要和性能/产物表。若 Web UI 投影卡住但 Code Agent/AgentRun result 已 terminal,应同时登记“执行终态”和“Workbench 投影未收敛”,不得用 `goto`、reload、切 session 或 result polling 把 UI 失败伪装成通过。 - `observe analyze` 是离线分析,只读取 artifact JSONL 并写 `analysis/report.md` 与 `analysis/report.json`,不访问 Workbench API、不驱动浏览器。`observe start` 每次启动必须先把同一 stateDir 中已有的根目录 JSONL 轮转到带时间戳的 `archive/` 文件;`observe analyze` 默认只分析当前根目录 JSONL,不扫描历史 archive,只有显式指定 archive prefix 时才分析历史轮转窗口。报告必须输出采样点 vs 每个 turn 的总耗时/最近更新时间表、trace row 视觉顺序异常、terminal/轮次完成 row 是否最后、Code Agent 卡片耗时与 trace/轮次完成总耗时一致性、可见“加载中”的数量/归属/并发 owner/连续出现区间、DOM diagnostic/HTTP/console/requestfailed/runtime execution error 分组、page asset provenance segment、同源 API Resource Timing 分位表和超过 YAML `webProbe.alertThresholds` budget 的慢路径 finding;页面/API 加载、可见“加载中”、长连接打开耗时、turn timing 跳变、trace row 顺序、卡片耗时/轮次完成耗时一致性和 session fallback 标题比例的报警阈值只能改 YAML,不能在 analyzer/renderer 中写死。修复必须降低真实请求、投影、渲染或后端路径耗时,禁止为了减少“加载中”出现时间而提前展示未加载完的内容,也不能靠下游 retry/reload/fallback 掩盖。报告里的 `trace-row-order-nonmonotonic`、`trace-completion-row-not-last`、`round-completion-elapsed-mismatch`、`code-agent-card-duration-underreported`、`final-response-flicker`、`uncommanded-visible-state-change`、session changed、network 503 等 finding 是排障线索;用于 closeout 时必须结合原始 session/trace/DOM 证据解释,避免把采样噪声直接当作业务结论。 diff --git a/scripts/src/hwlab-node-help.ts b/scripts/src/hwlab-node-help.ts index 7b8b329d..913184ae 100644 --- a/scripts/src/hwlab-node-help.ts +++ b/scripts/src/hwlab-node-help.ts @@ -81,7 +81,11 @@ export function hwlabNodeWebProbeHelp(): Record { "bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type selectProvider --provider codex-api", "bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type sendPrompt --text 'ping'", "bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type sendPrompt --text-stdin <<'EOF'\nlong prompt\nEOF", + "bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type steer --text '继续观察当前 trace'", + "bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type cancel", "bun scripts/cli.ts hwlab nodes web-probe observe status webobs-xxxx", + "bun scripts/cli.ts hwlab nodes web-probe observe collect webobs-xxxx --view turn-summary", + "bun scripts/cli.ts hwlab nodes web-probe observe collect webobs-xxxx --view trace-frame --trace-id trc_xxx --sample-seq 42", "bun scripts/cli.ts hwlab nodes web-probe observe analyze webobs-xxxx", "bun scripts/cli.ts hwlab nodes web-probe script --node D601 --lane v03 <<'JS'\nexport default async ({ waitWorkbenchReady, fetchJson, fetchApiMatrix, recordStep, collectText, safeEvaluate, screenshot }) => {\n const ready = await waitWorkbenchReady();\n const workspace = await fetchJson('/v1/workbench/workspace?projectId=prj_hwpod_workbench');\n const apiMatrix = await fetchApiMatrix(['/v1/workbench/workspace?projectId=prj_hwpod_workbench', '/auth/session']);\n const workspaceText = await collectText('#workspace');\n const evaluated = await safeEvaluate(({ a, b }) => ({ sum: a + b }), { a: 1, b: 2 });\n await screenshot('workbench.png');\n recordStep('workbench-summary', { finalUrl: ready.finalUrl, workspaceOk: workspace.ok, apiMatrixOk: apiMatrix.ok });\n return { finalUrl: ready.finalUrl, workspaceOk: workspace.ok, workspaceText, evaluated };\n};\nJS", ], @@ -96,7 +100,8 @@ export function hwlabNodeWebProbeHelp(): Record { "Issue-ready evidence is available under issueEvidence and summary.issueEvidence; full script report is persisted under probe.reportPath with a SHA-256 fingerprint.", "observe sampling is passive by default: it records DOM summaries and natural page request/response/requestfailed events with observerInitiated=false; it does not actively fetch Workbench APIs, reload, switch sessions, route/intercept, or call repair helpers.", "observe start registers a local UniDesk-side observer id under .state/web-observe/index.json; after start, prefer observe status|command|stop|collect|analyze instead of repeating --node/--lane/--state-dir.", - "observe command actions are explicit user/control actions and are appended to control.jsonl; use --type newSession/selectProvider/sendPrompt/goto/screenshot/mark/stop. For long prompts use sendPrompt --text-stdin; keep prompt text out of issue comments by citing textHash/textBytes.", + "observe command actions are explicit user/control actions and are appended to control.jsonl; use --type newSession/selectProvider/sendPrompt/steer/cancel/goto/screenshot/mark/stop. steer/cancel reuse the Workbench composer path, not private backend APIs. For long prompts use sendPrompt/steer --text-stdin; keep prompt text out of issue comments by citing textHash/textBytes.", + "observe collect --view turn-summary renders the multi-turn CLI reading layer from samples/control artifacts; --view trace-frame renders one sampled trace frame with a fixed Final Response block. collect views do not save a second source of truth.", "observe analyze is offline-only: it reads artifact JSONL and writes analysis/report.md plus analysis/report.json without accessing Workbench APIs or driving the browser.", "observe analyze scans every sampled DOM point, extracts Workbench timing text such as 总耗时/total and 最近 N 秒/分前, and writes a sample point vs turn timing report: each Markdown table row starts with the timestamp, followed by each turn's 总耗时(s) and 最近更新(s). Timing series are reported for post-processing/manual analysis instead of auto-judged from status tail output.", "observe analyze also reports visible “加载中” count, owner attribution, concurrent loading owners, and continuous visible segments; fixes must reduce real loading latency, not reveal incomplete content early to make this metric disappear.", diff --git a/scripts/src/hwlab-node-impl.ts b/scripts/src/hwlab-node-impl.ts index 46c06e4a..ec072022 100644 --- a/scripts/src/hwlab-node-impl.ts +++ b/scripts/src/hwlab-node-impl.ts @@ -10,7 +10,10 @@ import { classifySshTcpPoolFailure } from "./ssh"; import { HWLAB_NODE_CONTROL_PLANE_CONFIG_PATH, hwlabNodeControlPlaneInfraHelp, runHwlabNodeControlPlaneInfra } from "./hwlab-node-control-plane"; import { hwlabRuntimeLaneConfigPath, hwlabRuntimeLaneIds, hwlabRuntimeLaneSpec, hwlabRuntimeLaneSpecForNode, hwlabRuntimeNodeIds, isHwlabRuntimeLane, type HwlabRuntimeLane, type HwlabRuntimeLaneSpec, type HwlabRuntimeObservabilityRecordingRuleSpec, type HwlabRuntimeObservabilitySpec, type HwlabRuntimeObservabilityWarningAlertSpec, type HwlabRuntimePublicExposureSpec, type HwlabRuntimeWebProbeAlertThresholdsSpec } from "./hwlab-node-lanes"; import { nodeWebProbeScriptRunnerSource } from "./hwlab-node-web-probe-runner-source"; -import { nodeWebObserveAnalyzerSource, nodeWebObserveRunnerSource } from "./hwlab-node-web-observe-runner-source"; +import { nodeWebObserveAnalyzerSource } from "./hwlab-node-web-observe-analyzer-source"; +import { nodeWebObserveRunnerSource } from "./hwlab-node-web-observe-runner-source"; +import { nodeWebObserveCollectViewNodeScript, parseNodeWebProbeObserveCollectView, type NodeWebProbeObserveCollectView } from "./hwlab-node-web-observe-collect"; +import { withWebObserveCollectRendered, withWebObserveCommandRendered, withWebObserveStatusRendered } from "./hwlab-node-web-observe-render"; import { hwlabNodeHelp, hwlabNodeObservabilityHelp, hwlabNodeWebProbeHelp } from "./hwlab-node-help"; import { compactWebProbeResult, compactWebProbeScriptResult } from "./hwlab-node-web-probe-summary"; import { nodeObservabilityRecordingRuleExpression, nodeObservabilityRecordingRuleSummaries, nodeObservabilityWarningAlertExpression, nodeObservabilityWarningAlertSummaries } from "./hwlab-node-observability-promql"; @@ -61,7 +64,7 @@ interface NodeWebProbeScriptOptions { } type NodeWebProbeObserveAction = "start" | "status" | "command" | "stop" | "collect" | "analyze"; -type NodeWebProbeObserveCommandType = "login" | "preflight" | "goto" | "newSession" | "sendPrompt" | "selectProvider" | "clickSession" | "screenshot" | "mark" | "stop"; +type NodeWebProbeObserveCommandType = "login" | "preflight" | "goto" | "newSession" | "sendPrompt" | "steer" | "cancel" | "selectProvider" | "clickSession" | "screenshot" | "mark" | "stop"; interface NodeWebProbeObserveOptions { action: "observe"; @@ -81,9 +84,14 @@ interface NodeWebProbeObserveOptions { waitMs: number; tailLines: number; maxFiles: number; + collectView: NodeWebProbeObserveCollectView; collectFile: string | null; collectFinding: string | null; collectGrep: string | null; + collectTraceId: string | null; + collectSampleSeq: number | null; + collectTimestamp: string | null; + collectTurn: number | null; analyzeArchivePrefix: string | null; analyzeTailSamples: number | null; full: boolean; @@ -7359,9 +7367,14 @@ function parseNodeWebProbeObserveOptions( "--wait-ms", "--tail-lines", "--max-files", + "--view", "--file", "--finding", "--grep", + "--trace-id", + "--sample-seq", + "--timestamp", + "--turn", "--archive-prefix", "--tail-samples", "--state-dir", @@ -7379,12 +7392,23 @@ function parseNodeWebProbeObserveOptions( const jobId = optionValue(args, "--job-id") ?? observeId ?? indexed?.id ?? null; if (stateDir !== null && !isSafeWebObserveStateDir(stateDir)) throw new Error(`unsafe web-probe observe --state-dir: ${stateDir}`); if (jobId !== null && !isSafeWebObserveJobId(jobId)) throw new Error(`unsafe web-probe observe --job-id: ${jobId}`); + const collectView = parseNodeWebProbeObserveCollectView(optionValue(args, "--view") ?? "files"); const collectFile = optionValue(args, "--file") ?? null; if (collectFile !== null && !isSafeWebObserveCollectFile(collectFile)) throw new Error(`unsafe web-probe observe --file: ${collectFile}`); const collectFinding = optionValue(args, "--finding") ?? null; if (collectFinding !== null && !isSafeWebObserveFindingId(collectFinding)) throw new Error(`unsafe web-probe observe --finding: ${collectFinding}`); const collectGrep = optionValue(args, "--grep") ?? null; if (collectGrep !== null && (collectGrep.includes("\0") || collectGrep.length > 200)) throw new Error("unsafe web-probe observe --grep: expected 1-200 non-NUL chars"); + const collectTraceId = optionValue(args, "--trace-id") ?? null; + if (collectTraceId !== null && !isSafeWebObserveTraceId(collectTraceId)) throw new Error(`unsafe web-probe observe --trace-id: ${collectTraceId}`); + const collectSampleSeqRaw = optionValue(args, "--sample-seq") ?? null; + const collectSampleSeq = collectSampleSeqRaw === null ? null : Number(collectSampleSeqRaw); + if (collectSampleSeq !== null && (!Number.isInteger(collectSampleSeq) || collectSampleSeq < 1)) throw new Error("unsafe web-probe observe --sample-seq: expected positive integer"); + const collectTimestamp = optionValue(args, "--timestamp") ?? null; + if (collectTimestamp !== null && (collectTimestamp.includes("\0") || collectTimestamp.length > 80 || !Number.isFinite(Date.parse(collectTimestamp)))) throw new Error("unsafe web-probe observe --timestamp: expected parseable timestamp"); + const collectTurnRaw = optionValue(args, "--turn") ?? null; + const collectTurn = collectTurnRaw === null ? null : Number(collectTurnRaw); + if (collectTurn !== null && (!Number.isInteger(collectTurn) || collectTurn < 1)) throw new Error("unsafe web-probe observe --turn: expected positive integer"); const analyzeArchivePrefix = optionValue(args, "--archive-prefix") ?? null; if (analyzeArchivePrefix !== null && !isSafeWebObserveArchivePrefix(analyzeArchivePrefix)) throw new Error(`unsafe web-probe observe --archive-prefix: ${analyzeArchivePrefix}`); const analyzeTailSamplesRaw = optionValue(args, "--tail-samples") ?? null; @@ -7422,9 +7446,14 @@ function parseNodeWebProbeObserveOptions( waitMs: positiveIntegerOption(args, "--wait-ms", 0, 600000), tailLines: positiveIntegerOption(args, "--tail-lines", 5, 200), maxFiles: positiveIntegerOption(args, "--max-files", 80, 5000), + collectView, collectFile, collectFinding, collectGrep, + collectTraceId, + collectSampleSeq, + collectTimestamp, + collectTurn, analyzeArchivePrefix, analyzeTailSamples, full: args.includes("--full"), @@ -7447,13 +7476,15 @@ function parseNodeWebProbeObserveCommandType(value: string): NodeWebProbeObserve || value === "goto" || value === "newSession" || value === "sendPrompt" + || value === "steer" + || value === "cancel" || value === "selectProvider" || value === "clickSession" || value === "screenshot" || value === "mark" || value === "stop" ) return value; - throw new Error(`web-probe observe command --type must be login, preflight, goto, newSession, sendPrompt, selectProvider, clickSession, screenshot, mark, or stop; got ${value}`); + throw new Error(`web-probe observe command --type must be login, preflight, goto, newSession, sendPrompt, steer, cancel, selectProvider, clickSession, screenshot, mark, or stop; got ${value}`); } function parseWebProbeBrowserProxyMode(value: string | undefined): WebProbeBrowserProxyMode { @@ -8082,153 +8113,6 @@ function runNodeWebProbeObserveStatus(options: NodeWebProbeObserveOptions, spec: }); } -function withWebObserveStatusRendered(value: Record): RenderedCliResult { - return { - ok: value.ok !== false, - command: typeof value.command === "string" ? value.command : "hwlab nodes web-probe observe status", - contentType: "text/plain", - renderedText: renderWebObserveStatusTable(value), - }; -} - -function renderWebObserveStatusTable(value: Record): string { - const observer = record(value.observer); - const manifest = record(observer?.manifest); - const heartbeat = record(observer?.heartbeat); - const tails = record(observer?.tails); - const result = record(value.result); - const samples = Array.isArray(tails?.samples) ? tails.samples.map(record).filter((item): item is Record => item !== null).slice(-5) : []; - const controls = Array.isArray(tails?.control) ? tails.control.map(record).filter((item): item is Record => item !== null).slice(-5) : []; - const completedControlIds = new Set(controls - .filter((item) => item.phase === "completed" || item.phase === "failed") - .map((item) => webObserveText(item.commandId)) - .filter((item) => item !== "-")); - const activeControl = controls.slice().reverse().find((item) => item.phase === "started" && !completedControlIds.has(webObserveText(item.commandId))) ?? null; - const activeControlAgeSeconds = activeControl === null ? null : Math.max(0, Math.round((Date.now() - Date.parse(webObserveText(activeControl.ts))) / 1000)); - const network = Array.isArray(tails?.network) - ? tails.network.map(record).filter((item): item is Record => item !== null).filter((item) => { - const status = typeof item.status === "number" ? item.status : null; - return status === null || status >= 400 || item.failure !== null && item.failure !== undefined; - }).slice(-5) - : []; - const next = record(value.next); - const heartbeatError = record(heartbeat?.error) ?? record(manifest?.error); - const heartbeatAuth = record(heartbeat?.auth) ?? record(heartbeatError?.auth); - const manifestNetwork = record(manifest?.network); - const manifestBrowser = record(manifestNetwork?.browser); - const manifestProxy = record(manifestNetwork?.proxy); - const lines = [ - `hwlab nodes web-probe observe status (${webObserveText(value.status)})`, - "", - webObserveTable(["ID", "NODE", "LANE", "ALIVE", "PID", "SAMPLE", "UPDATED", "TARGET"], [[ - webObserveText(value.id), - webObserveText(value.node), - webObserveText(value.lane), - webObserveText(observer?.processAlive), - webObserveText(observer?.pid), - webObserveText(heartbeat?.sampleSeq), - webObserveShort(webObserveText(heartbeat?.updatedAt ?? heartbeat?.lastSampleAt), 24), - webObserveShort(`${webObserveText(manifest?.baseUrl)}${webObserveText(manifest?.targetPath) === "-" ? "" : webObserveText(manifest?.targetPath)}`, 52), - ]]), - "", - ...(value.ok === false ? [ - "Blocked detail:", - webObserveTable(["REASON", "EXIT", "TIMEOUT", "STDOUT", "STDERR"], [[ - webObserveShort(webObserveText(value.degradedReason), 48), - webObserveText(result.exitCode), - webObserveText(result.timedOut), - webObserveShort(webObserveText(result.stdoutTail), 120), - webObserveShort(webObserveText(result.stderr), 120), - ]]), - "", - ] : []), - ...(heartbeatError !== null ? [ - "Observer error:", - webObserveTable(["STATUS", "RETRY", "EXHAUSTED", "BROWSER_PROXY", "MESSAGE"], [[ - webObserveText(heartbeat?.status ?? manifest?.status), - webObserveText(heartbeatAuth?.lastRetryLabel), - webObserveText(heartbeatAuth?.retryExhausted), - webObserveShort(webObserveText(manifestBrowser?.proxyMode ?? manifestProxy?.enabled), 32), - webObserveShort(webObserveText(heartbeatError.message ?? heartbeatAuth?.lastError), 120), - ]]), - "", - ] : []), - ...(heartbeatAuth !== null ? [ - "Auth progress:", - webObserveTable(["PHASE", "RETRY", "DELAY_MS", "STATUS", "RETRYABLE", "COOKIE", "EXHAUSTED", "LAST_ERROR"], [[ - webObserveText(heartbeatAuth.phase), - webObserveText(heartbeatAuth.lastRetryLabel), - webObserveText(heartbeatAuth.retryDelayMs), - webObserveText(heartbeatAuth.lastStatusText === undefined ? heartbeatAuth.lastStatus : `${webObserveText(heartbeatAuth.lastStatus)} ${webObserveText(heartbeatAuth.lastStatusText)}`), - webObserveText(heartbeatAuth.retryable), - webObserveText(heartbeatAuth.cookiePresent), - webObserveText(heartbeatAuth.retryExhausted), - webObserveShort(webObserveText(heartbeatAuth.lastError), 80), - ]]), - "", - ] : []), - ...(activeControl !== null ? [ - "Active command:", - webObserveTable(["TYPE", "COMMAND", "AGE_S", "STARTED_AT", "DETAIL"], [[ - webObserveText(activeControl.type), - webObserveShort(webObserveText(activeControl.commandId), 28), - webObserveText(activeControlAgeSeconds), - webObserveShort(webObserveText(activeControl.ts), 24), - webObserveShort(webObserveText(activeControl.detail), 80), - ]]), - "", - ] : []), - "Recent samples:", - webObserveTable(["SEQ", "TS", "PATH", "ROUTE_SESSION", "ACTIVE_SESSION", "MSG", "TRACE"], samples.length > 0 ? samples.map((sample) => [ - webObserveText(sample.seq), - webObserveShort(webObserveText(sample.ts), 24), - webObserveShort(webObserveText(sample.path), 24), - webObserveShort(webObserveText(sample.routeSessionId), 20), - webObserveShort(webObserveText(sample.activeSessionId), 20), - webObserveText(sample.messageCount), - webObserveText(sample.traceRowCount), - ]) : [["-", "-", "-", "-", "-", "-", "-"]]), - "", - "Recent control:", - webObserveTable(["SEQ", "TS", "PHASE", "TYPE", "COMMAND", "RETRY", "DETAIL"], controls.length > 0 ? controls.map((control) => [ - webObserveText(control.seq), - webObserveShort(webObserveText(control.ts), 24), - webObserveText(control.phase), - webObserveText(control.type), - webObserveShort(webObserveText(control.commandId), 28), - webObserveText(control.retry), - webObserveShort(webObserveText(control.detail), 80), - ]) : [["-", "-", "-", "-", "-", "-", "-"]]), - "", - "Network alerts:", - webObserveTable(["TS", "METHOD", "STATUS", "TYPE", "URL_OR_FAILURE"], network.length > 0 ? network.map((item) => [ - webObserveShort(webObserveText(item.ts), 24), - webObserveText(item.method), - webObserveText(item.status), - webObserveText(item.type), - webObserveShort(webObserveText(item.failure ?? item.url), 80), - ]) : [["-", "-", "-", "-", "-"]]), - "", - "Next:", - ]; - for (const key of ["status", "command", "stop", "analyze"] as const) { - const command = webObserveText(next?.[key]); - if (command !== "-") lines.push(` ${command}`); - } - lines.push("", "Disclosure:"); - lines.push(" default view is a bounded table; use observe collect/analyze for artifact-level drill-down."); - return lines.join("\n"); -} - -function webObserveTable(headers: string[], rows: string[][]): string { - const normalizedRows = rows.map((row) => headers.map((_, index) => row[index] ?? "")); - const widths = headers.map((header, index) => Math.max(header.length, ...normalizedRows.map((row) => row[index].length))); - return [ - headers.map((header, index) => header.padEnd(widths[index])).join(" ").trimEnd(), - ...normalizedRows.map((row) => row.map((cell, index) => cell.padEnd(widths[index])).join(" ").trimEnd()), - ].join("\n"); -} - function webObserveText(value: unknown): string { if (value === null || value === undefined || value === "") return "-"; if (typeof value === "string") return value; @@ -8307,57 +8191,21 @@ function runNodeWebProbeObserveCommand(options: NodeWebProbeObserveOptions, spec }); } -function withWebObserveCommandRendered(value: Record): RenderedCliResult { - return { - ok: value.ok !== false, - command: typeof value.command === "string" ? value.command : "hwlab nodes web-probe observe command", - contentType: "text/plain", - renderedText: renderWebObserveCommandTable(value), - }; -} - -function renderWebObserveCommandTable(value: Record): string { - const observerCommand = record(value.observerCommand); - const observer = record(value.observer); - const result = record(observer?.result); - const id = webObserveText(value.id); - const full = value.full === true; - const lines = [ - `hwlab nodes web-probe observe command (${webObserveText(value.status)})`, - "", - webObserveTable(["OBSERVER", "COMMAND", "TYPE", "STATUS", "TEXT_BYTES", "TEXT_HASH", "DETAIL"], [[ - id, - webObserveText(value.commandId), - webObserveText(observerCommand?.type), - webObserveText(observer?.ok === false ? "failed" : observer?.completedAt !== undefined ? "completed" : value.status), - webObserveText(observerCommand?.textBytes), - webObserveShort(webObserveText(observerCommand?.textHash), 24), - webObserveShort(webObserveText(result?.mark ?? result?.currentUrl ?? observer?.error ?? observer?.queued), 80), - ]]), - ...(full ? [ - "", - "Full command result:", - "```json", - JSON.stringify(observer ?? {}, null, 2), - "```", - ] : []), - "", - "Next:", - ` bun scripts/cli.ts hwlab nodes web-probe observe status ${id}`, - ` bun scripts/cli.ts hwlab nodes web-probe observe analyze ${id}`, - "", - "Disclosure:", - " default view is a bounded command result; pass --full to expose the complete command result JSON.", - " use observe status/analyze for sampled state.", - ]; - return lines.join("\n"); -} - function runNodeWebProbeObserveCollect(options: NodeWebProbeObserveOptions, spec: HwlabRuntimeLaneSpec): Record | RenderedCliResult { + const collectScript = options.collectView === "files" + ? nodeWebObserveCollectNodeScript(options.maxFiles, options.collectFile, options.collectFinding, options.collectGrep) + : nodeWebObserveCollectViewNodeScript({ + maxFiles: options.maxFiles, + view: options.collectView, + traceId: options.collectTraceId, + sampleSeq: options.collectSampleSeq, + timestamp: options.collectTimestamp, + turn: options.collectTurn, + }); const script = [ "set -eu", nodeWebObserveResolveStateDirShell(options), - nodeWebObserveCollectNodeScript(options.maxFiles, options.collectFile, options.collectFinding, options.collectGrep), + collectScript, ].join("\n"); const result = runTransWorkspaceStdinScript(options.node, spec.workspace, script, options.commandTimeoutSeconds); const collect = parseJsonObject(result.stdout); @@ -8369,6 +8217,7 @@ function runNodeWebProbeObserveCollect(options: NodeWebProbeObserveOptions, spec node: options.node, lane: options.lane, workspace: spec.workspace, + view: options.collectView, requestedFile: options.collectFile, requestedGrep: options.collectGrep, degradedReason: collect === null ? "collect-json-parse-failed" : null, @@ -8378,224 +8227,6 @@ function runNodeWebProbeObserveCollect(options: NodeWebProbeObserveOptions, spec }); } -function withWebObserveCollectRendered(value: Record): RenderedCliResult { - return { - ok: value.ok !== false, - command: typeof value.command === "string" ? value.command : "hwlab nodes web-probe observe collect", - contentType: "text/plain", - renderedText: renderWebObserveCollectTable(value), - }; -} - -function renderWebObserveCollectTable(value: Record): string { - const collect = nullableRecord(value.collect); - const result = record(value.result); - const file = record(collect?.file); - const files = Array.isArray(collect?.files) ? collect.files.map(record).filter((item): item is Record => item !== null).slice(0, 20) : []; - const jsonlTail = Array.isArray(file.jsonlTail) ? file.jsonlTail.map(record).filter((item): item is Record => item !== null).slice(-8) : []; - const jsonSummary = record(file.jsonSummary); - const grepSummary = record(file.grep); - const grepLines = Array.isArray(grepSummary?.lines) ? grepSummary.lines.map(record).filter((item): item is Record => item !== null).slice(0, 40) : []; - const jsonContentPreview = file.jsonContent === undefined || file.jsonContent === null ? "" : JSON.stringify(file.jsonContent, null, 2); - const jsonRunnerErrors = Array.isArray(jsonSummary.runnerErrors) ? jsonSummary.runnerErrors.map(record).filter((item): item is Record => item !== null).slice(-8) : []; - const jsonFindings = Array.isArray(jsonSummary.findings) ? jsonSummary.findings.map(record).filter((item): item is Record => item !== null).slice(0, 8) : []; - const jsonFindingDetail = record(jsonSummary.findingDetail); - const jsonFindingSamples = Array.isArray(jsonFindingDetail?.samples) ? jsonFindingDetail.samples.map(record).filter((item): item is Record => item !== null).slice(0, 12) : []; - const lines = [ - `hwlab nodes web-probe observe collect (${webObserveText(value.status)})`, - "", - webObserveTable(["ID", "NODE", "LANE", "MODE", "STATE_DIR"], [[ - webObserveText(value.id), - webObserveText(value.node), - webObserveText(value.lane), - webObserveText(collect?.mode ?? "files"), - webObserveShort(webObserveText(collect?.stateDir), 88), - ]]), - "", - ]; - if (collect === null) { - lines.push( - "Collect command:", - webObserveTable(["REQUESTED", "REASON", "EXIT", "TIMED_OUT", "STDOUT_BYTES", "STDOUT_TAIL", "STDERR"], [[ - webObserveShort(webObserveText(value.requestedFile), 64), - webObserveShort(webObserveText(value.degradedReason), 40), - webObserveText(result.exitCode), - webObserveText(result.timedOut), - webObserveText(result.stdoutBytes), - webObserveShort(webObserveText(result.stdoutTail), 160), - webObserveShort(webObserveText(result.stderr), 160), - ]]), - "", - "Disclosure:", - " collect stdout was not valid JSON; fix the collect command or rerun with a narrower --file after the root cause is visible.", - ); - return lines.join("\n"); - } - if (collect.mode === "file") { - lines.push( - "File:", - webObserveTable(["RELATIVE", "BYTES", "TRUNC", "SHA256"], [[ - webObserveShort(webObserveText(file.relative), 64), - webObserveText(file.byteCount), - webObserveText(file.truncated), - webObserveShort(webObserveText(file.sha256), 28), - ]]), - "", - ); - if (Object.keys(jsonSummary).length > 0) { - lines.push( - "JSON summary:", - webObserveTable(["KEYS", "COUNTS", "PARSE_ERROR"], [[ - webObserveShort(webObserveText(jsonSummary.topLevelKeys), 80), - webObserveShort(webObserveText(jsonSummary.counts), 80), - webObserveShort(webObserveText(jsonSummary.parseError), 80), - ]]), - "", - ); - } - if (grepSummary !== null) { - lines.push( - "Grep matches:", - webObserveTable(["PATTERN_SHA", "MATCHES", "RETURNED", "TRUNC"], [[ - webObserveShort(webObserveText(grepSummary.patternSha256), 24), - webObserveText(grepSummary.matchCount), - webObserveText(grepSummary.returnedLineCount), - webObserveText(grepSummary.truncated), - ]]), - webObserveTable(["LINE", "MATCH", "TEXT"], grepLines.length > 0 ? grepLines.map((item) => [ - webObserveText(item.lineNo), - webObserveText(item.match === true), - webObserveShort(webObserveText(item.text), 360), - ]) : [["-", "-", "-"]]), - "Grep context:", - grepLines.length > 0 - ? grepLines.map((item) => "- " + webObserveText(item.lineNo) + (item.match === true ? " * " : " ") + webObserveShort(webObserveText(item.text), 500)).join("\n") - : "-", - "", - ); - } - if (jsonContentPreview.length > 0) { - lines.push("JSON content:", webObserveShort(jsonContentPreview, 2400), ""); - } - if (jsonRunnerErrors.length > 0) { - lines.push( - "Runner errors:", - webObserveTable(["TS", "TYPE", "ATTEMPTS", "READY", "MESSAGE"], jsonRunnerErrors.map((item) => [ - webObserveShort(webObserveText(item.ts), 24), - webObserveShort(webObserveText(item.type), 20), - webObserveText(item.attemptCount), - webObserveShort(webObserveText(item.lastReadinessReason), 24), - webObserveShort(webObserveText(item.message ?? item.lastError), 96), - ])), - "", - ); - } - if (jsonFindings.length > 0) { - lines.push( - "Findings:", - webObserveTable(["KIND", "SEVERITY", "COUNT", "SUMMARY"], jsonFindings.map((item) => [ - webObserveShort(webObserveText(item.kind ?? item.id ?? item.code), 48), - webObserveText(item.severity ?? item.level), - webObserveText(item.count ?? item.sampleCount), - webObserveShort(webObserveText(item.summary ?? item.message), 96), - ])), - "", - ); - } - if (jsonFindingDetail !== null) { - lines.push( - "Finding detail:", - webObserveTable(["ID", "SEVERITY", "COUNT", "SAMPLE_SOURCE", "SUMMARY"], [[ - webObserveShort(webObserveText(jsonFindingDetail.id), 48), - webObserveText(jsonFindingDetail.severity), - webObserveText(jsonFindingDetail.count), - webObserveText(jsonFindingDetail.sampleSource), - webObserveShort(webObserveText(jsonFindingDetail.summary), 96), - ]]), - "", - "Finding samples:", - webObserveTable(["SEQ", "TS", "KIND", "FROM", "TO", "DELTA", "SAMPLE_S", "ALLOWED", "EXCESS", "SESSION/PATH", "TRACE", "DETAIL"], jsonFindingSamples.length > 0 ? jsonFindingSamples.map((item) => [ - webObserveText(item.seq ?? item.sampleSeq ?? item.fromSeq), - webObserveShort(webObserveText(item.ts ?? item.sampleTs ?? item.fromTs), 24), - webObserveShort(webObserveText(item.diffKind ?? item.kind ?? item.type ?? item.metric ?? item.anomaly), 28), - webObserveShort(webObserveText(item.fromValue ?? item.from ?? item.control ?? item.controlValue ?? item.beforeValue), 28), - webObserveShort(webObserveText(item.toValue ?? item.to ?? item.observer ?? item.observerValue ?? item.afterValue), 28), - webObserveText(item.delta), - webObserveText(item.sampleDeltaSeconds), - webObserveText(item.allowedIncreaseSeconds), - webObserveText(item.excessiveIncreaseSeconds), - webObserveShort(webObserveText(item.sessionId ?? item.routeSessionId ?? item.activeSessionId ?? item.path ?? item.urlPath ?? item.url), 48), - webObserveShort(webObserveText(item.trace ?? item.traceId ?? item.controlTraceId ?? item.observerTraceId), 48), - webObserveShort(webObserveText(item.detail ?? item.summary ?? item.message ?? item.preview), 96), - ]) : [["-", "-", "-", "-", "-", "-", "-", "-", "-", "-", "-", "-"]]), - "", - ); - } - if (jsonlTail.length > 0) { - lines.push( - "JSONL tail:", - webObserveTable(["TS", "ROLE", "TYPE", "COMMAND", "MSG", "TRACE", "TRACE_IDS", "MSG_STATUS", "LOAD", "ATTEMPTS", "READY", "DOM", "PATH", "MESSAGE"], jsonlTail.map((item) => { - const error = record(item.error); - const attempts = Array.isArray(error.attempts) ? error.attempts : []; - const lastAttempt = attempts.length > 0 ? record(attempts[attempts.length - 1]) : {}; - const rawReadiness = record(lastAttempt.readiness ?? error.navigationReadiness); - const readiness = record(rawReadiness.snapshot ?? rawReadiness); - const domBits = [ - `shell=${readiness.workbenchShellVisible === true ? "Y" : "n"}`, - `create=${readiness.sessionCreateVisible === true ? "Y" : "n"}`, - `input=${readiness.commandInputPresent === true ? "Y" : "n"}`, - `tab=${readiness.activeTabPresent === true ? "Y" : "n"}`, - `login=${readiness.loginVisible === true ? "Y" : "n"}`, - ].join(" "); - return [ - webObserveShort(webObserveText(item.ts), 24), - webObserveShort(webObserveText(item.pageRole ?? item.role), 12), - webObserveShort(webObserveText(item.type), 20), - webObserveShort(webObserveText(item.commandId), 28), - webObserveText(item.messageCount), - webObserveText(item.traceRowCount ?? item.traceEventCount), - webObserveShort(webObserveText(item.traceIds), 48), - webObserveShort(webObserveText(item.messageStatuses), 48), - webObserveText(item.loadingCount), - webObserveText(attempts.length), - webObserveShort(webObserveText(readiness.reason), 24), - webObserveShort(domBits, 48), - webObserveShort(webObserveText(item.path ?? item.urlPath ?? item.url ?? item.currentUrl ?? item.status), 60), - webObserveShort(webObserveText(error.message ?? item.message ?? item.text ?? item.preview), 96), - ]; - })), - "", - ); - } else if (typeof file.content === "string" && file.content.length > 0) { - lines.push("Content preview:", webObserveShort(file.content, 4000), ""); - } - } else { - lines.push( - "Files:", - webObserveTable(["RELATIVE", "BYTES", "SHA256"], files.length > 0 ? files.map((item) => [ - webObserveShort(webObserveText(item.relative), 72), - webObserveText(item.byteCount), - webObserveShort(webObserveText(item.sha256), 28), - ]) : [["-", "-", "-"]]), - "", - ); - } - if (value.ok === false) { - lines.push( - "Blocked detail:", - webObserveTable(["REASON", "EXIT", "TIMEOUT", "STDERR"], [[ - webObserveShort(webObserveText(collect.reason ?? value.degradedReason), 48), - webObserveText(result.exitCode), - webObserveText(result.timedOut), - webObserveShort(webObserveText(result.stderr), 120), - ]]), - "", - ); - } - lines.push("Disclosure:", " collect is bounded; use --file plus --finding for id-specific drill-down; exact --grep also expands finding samples."); - return lines.join("\n"); -} - function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOptions, spec: HwlabRuntimeLaneSpec): Record | RenderedCliResult { const analyzerB64 = Buffer.from(nodeWebObserveAnalyzerSource(), "utf8").toString("base64"); const alertThresholds = nodeWebProbeAlertThresholds(spec); @@ -10703,6 +10334,10 @@ function isSafeWebObserveCollectFile(value: string): boolean { && /^[A-Za-z0-9_.\/-]+$/u.test(value); } +function isSafeWebObserveTraceId(value: string): boolean { + return /^trc_[A-Za-z0-9_-]{6,120}$/u.test(value); +} + function isSafeWebObserveFindingId(value: string): boolean { return value.length > 0 && value.length <= 120 diff --git a/scripts/src/hwlab-node-web-observe-analyzer-source.ts b/scripts/src/hwlab-node-web-observe-analyzer-source.ts new file mode 100644 index 00000000..7361e331 --- /dev/null +++ b/scripts/src/hwlab-node-web-observe-analyzer-source.ts @@ -0,0 +1,5002 @@ +// SPEC: PJ2026-01040111 long-running Workbench observation. +// Responsibility: Source string for the offline web-probe observe analyzer. +export function nodeWebObserveAnalyzerSource(): string { + return String.raw`#!/usr/bin/env node +import { createHash } from "node:crypto"; +import { createReadStream } from "node:fs"; +import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { createInterface } from "node:readline"; + +const stateDir = path.resolve(process.env.UNIDESK_WEB_OBSERVE_STATE_DIR || process.argv[2] || ".state/web-observe/manual"); +const archivePrefix = safeArchivePrefix(process.env.UNIDESK_WEB_OBSERVE_ANALYZE_ARCHIVE_PREFIX || ""); +const analyzeTailSamples = (() => { + const raw = process.env.UNIDESK_WEB_OBSERVE_ANALYZE_TAIL_SAMPLES; + if (raw === undefined || raw === null || String(raw).trim() === "") return 360; + const parsed = Number(raw); + return Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : 360; +})(); +const alertThresholds = parseAlertThresholds(process.env.UNIDESK_WEB_OBSERVE_ALERT_THRESHOLDS_JSON); +const dataDir = archivePrefix ? path.join(stateDir, "archive") : stateDir; +const dataFile = (name) => path.join(dataDir, archivePrefix ? archivePrefix + "-" + name : name); +const analysisDir = path.join(stateDir, "analysis"); +const reportJsonPath = path.join(analysisDir, "report.json"); +const reportMdPath = path.join(analysisDir, "report.md"); +const jsonlReadIssues = []; +const sourceSamples = await readJsonl(dataFile("samples.jsonl"), { compact: compactSampleForAnalysis, tail: analyzeTailSamples }); +const relatedJsonlTailLimit = analyzeTailSamples > 0 ? Math.max(1200, analyzeTailSamples * 50) : 0; +const smallJsonlTailLimit = analyzeTailSamples > 0 ? Math.max(500, analyzeTailSamples * 8) : 0; +const sourceSampleWindow = sampleTimeWindow(sourceSamples, 60_000); +const sourceControlAll = await readJsonl(dataFile("control.jsonl"), { tail: relatedJsonlTailLimit }); +const analysisFocus = analysisFocusFromControl(sourceControlAll); +const sourceControl = filterRowsByTimeWindow(sourceControlAll, sourceSampleWindow); +const samples = applyAnalysisFocus(sourceSamples, analysisFocus); +const sampleWindow = sampleTimeWindow(samples, 60_000); +const controlWindow = analysisControlWindow(sampleWindow, analysisFocus, 1000); +const control = applyAnalysisFocus(filterRowsByTimeWindow(sourceControlAll, controlWindow), analysisFocus, 1000); +const sourceNetworkAll = await readJsonl(dataFile("network.jsonl"), { tail: relatedJsonlTailLimit }); +const network = applyAnalysisFocus(filterRowsByTimeWindow(sourceNetworkAll, sampleWindow), analysisFocus); +const promptNetworkRows = applyAnalysisFocus(filterRowsByTimeWindow(sourceNetworkAll, controlWindow), analysisFocus); +const consoleEvents = applyAnalysisFocus(filterRowsByTimeWindow(await readJsonl(dataFile("console.jsonl"), { tail: smallJsonlTailLimit }), sampleWindow), analysisFocus); +const errors = applyAnalysisFocus(filterRowsByTimeWindow(await readJsonl(dataFile("errors.jsonl"), { tail: smallJsonlTailLimit }), sampleWindow), analysisFocus); +const artifacts = applyAnalysisFocus(filterRowsByTimeWindow(await readJsonl(dataFile("artifacts.jsonl"), { tail: smallJsonlTailLimit }), sampleWindow), analysisFocus); +const manifest = await readJson(path.join(stateDir, "manifest.json")); +const heartbeat = await readJson(path.join(stateDir, "heartbeat.json")); + +await mkdir(analysisDir, { recursive: true, mode: 0o700 }); +const transitions = buildTransitions(samples); +const sampleMetrics = buildSampleMetrics(samples, control); +const pageProvenance = buildPageProvenanceReport(samples, control, manifest); +const pagePerformance = buildPagePerformanceReport(samples, manifest); +const promptNetwork = buildPromptNetworkReport(control, promptNetworkRows); +const runtimeAlerts = buildRuntimeAlerts(samples, control, network, consoleEvents, errors); +const 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, sourceControlCount: sourceControlAll.length, sampleWindow, focus: analysisFocus, valuesRedacted: true }, + alertThresholds, + manifest: compactManifest(manifest), + heartbeat: compactHeartbeat(heartbeat), + counts: { samples: samples.length, control: control.length, network: network.length, console: consoleEvents.length, errors: errors.length, artifacts: artifacts.length }, + commandTimeline, + transitions, + sampleMetrics, + pageProvenance, + pagePerformance, + 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, pageRole: item.pageRole ?? null, pageId: item.pageId ?? null, pageEpoch: item.pageEpoch ?? null, promptIndex: item.promptIndex, lastPromptIndex: item.lastPromptIndex, firstSeq: item.firstSeq, lastSeq: item.lastSeq, traceId: item.traceId, messageId: item.messageId })), + loading: compactLoadingMetricsForOutput(recentWindow.sampleMetrics.loading), + sessionRailTitles: compactSessionRailTitleMetricsForOutput(recentWindow.sampleMetrics.sessionRailTitles), + }, + pageProvenance: recentWindow.pageProvenance.summary, + pagePerformance: recentWindow.pagePerformance.summary, + 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, + pageRole: item.pageRole ?? null, + pageId: item.pageId ?? null, + pageEpoch: item.pageEpoch ?? null, + promptIndex: item.promptIndex ?? null, + traceId: item.traceId ?? null, + fromSeq: item.fromSeq ?? null, + toSeq: item.toSeq ?? null, + fromTs: item.fromTs ?? null, + toTs: item.toTs ?? null, + fromValue: item.fromValue ?? null, + toValue: item.toValue ?? null, + delta: item.delta ?? null, + sampleDeltaSeconds: item.sampleDeltaSeconds ?? null, + allowedIncreaseSeconds: item.allowedIncreaseSeconds ?? null, + excessiveIncreaseSeconds: item.excessiveIncreaseSeconds ?? null, + })), + turnTimingElapsedZeroResets: recentWindow.sampleMetrics.turnTimingElapsedZeroResets.slice(0, 8).map((item) => ({ + columnLabel: item.columnLabel ?? item.columnId ?? null, + pageRole: item.pageRole ?? null, + pageId: item.pageId ?? null, + pageEpoch: item.pageEpoch ?? null, + promptIndex: item.promptIndex ?? null, + traceId: item.traceId ?? null, + fromSeq: item.fromSeq ?? null, + toSeq: item.toSeq ?? null, + fromTs: item.fromTs ?? null, + toTs: item.toTs ?? null, + fromValue: item.fromValue ?? null, + toValue: item.toValue ?? null, + delta: item.delta ?? null, + anomaly: item.anomaly ?? null, + })), + turnTimingTotalElapsedForwardJumps: recentWindow.sampleMetrics.turnTimingTotalElapsedForwardJumps.slice(0, 8).map((item) => ({ + columnLabel: item.columnLabel ?? item.columnId ?? null, + pageRole: item.pageRole ?? null, + pageId: item.pageId ?? null, + pageEpoch: item.pageEpoch ?? null, + promptIndex: item.promptIndex ?? null, + traceId: item.traceId ?? null, + fromSeq: item.fromSeq ?? null, + toSeq: item.toSeq ?? null, + fromTs: item.fromTs ?? null, + toTs: item.toTs ?? null, + fromValue: item.fromValue ?? null, + toValue: item.toValue ?? null, + delta: item.delta ?? null, + sampleDeltaSeconds: item.sampleDeltaSeconds ?? null, + allowedIncreaseSeconds: item.allowedIncreaseSeconds ?? null, + excessiveIncreaseSeconds: item.excessiveIncreaseSeconds ?? null, + anomaly: item.anomaly ?? null, + })), + pagePerformanceSlowApi: recentWindow.pagePerformance.sameOriginApiByPath.filter((item) => item.isLongLivedStream !== true && Number(item.overBudgetCount ?? item.overFiveSecondCount ?? 0) > 0).slice(0, 5).map((item) => ({ path: item.path, route: item.route, sampleCount: item.sampleCount, p95Ms: item.p95Ms, maxMs: item.maxMs, overBudgetCount: item.overBudgetCount, budgetMs: item.budgetMs, overFiveSecondCount: item.overFiveSecondCount, slowSamples: item.slowSamples })), + archivePagePerformanceSlowApi: pagePerformance.sameOriginApiByPath.filter((item) => item.isLongLivedStream !== true && Number(item.overBudgetCount ?? item.overFiveSecondCount ?? 0) > 0).slice(0, 8).map((item) => ({ path: item.path, route: item.route, sampleCount: item.sampleCount, p95Ms: item.p95Ms, maxMs: item.maxMs, overBudgetCount: item.overBudgetCount, budgetMs: item.budgetMs, overFiveSecondCount: item.overFiveSecondCount, slowSamples: item.slowSamples })), + pagePerformancePartialApi: recentWindow.pagePerformance.sameOriginApiByPath.filter((item) => item.isLongLivedStream !== true && Number(item.partialOverBudgetCount ?? item.partialOverFiveSecondCount ?? 0) > 0).slice(0, 5).map((item) => ({ path: item.path, route: item.route, sampleCount: item.sampleCount, completeTimingSampleCount: item.completeTimingSampleCount, partialTimingSampleCount: item.partialTimingSampleCount, partialOverBudgetCount: item.partialOverBudgetCount, budgetMs: item.partialBudgetMs, partialOverFiveSecondCount: item.partialOverFiveSecondCount, partialSamples: item.partialSamples })), + pagePerformanceSseStreams: recentWindow.pagePerformance.sameOriginApiByPath.filter((item) => item.isLongLivedStream === true).slice(0, 5).map((item) => ({ path: item.path, route: item.route, sampleCount: item.sampleCount, streamOpenSampleCount: item.streamOpenSampleCount, streamOpenP95Ms: item.streamOpenP95Ms, streamOpenMaxMs: item.streamOpenMaxMs, streamOpenOverBudgetCount: item.streamOpenOverBudgetCount, streamOpenBudgetMs: item.streamOpenBudgetMs, streamOpenOverFiveSecondCount: item.streamOpenOverFiveSecondCount, streamLifetimeOverFiveSecondCount: item.streamLifetimeOverFiveSecondCount, slowSamples: item.slowSamples })), + findings: prioritizeFindings(recentWindow.findings).slice(0, 8).map((item) => ({ kind: item.id ?? item.kind ?? item.code, severity: item.severity, count: item.count ?? item.sampleCount ?? null, summary: String(item.summary ?? item.message ?? "").slice(0, 180) })), + traceOrderAnomalies: (recentWindow.sampleMetrics?.traceOrder?.orderAnomalies || []).slice(0, 8).map((item) => ({ + sampleSeq: item.sampleSeq ?? null, + sampleIndex: item.sampleIndex ?? null, + timestamp: item.timestamp ?? null, + pageRole: item.pageRole ?? null, + traceId: item.traceId ?? null, + reasons: item.reasons ?? [], + previousRowIndex: item.previousRowIndex ?? null, + currentRowIndex: item.currentRowIndex ?? null, + previousTotalSeconds: item.previousTotalSeconds ?? null, + currentTotalSeconds: item.currentTotalSeconds ?? null, + previousProjectedSeq: item.previousProjectedSeq ?? null, + currentProjectedSeq: item.currentProjectedSeq ?? null, + previousSourceSeq: item.previousSourceSeq ?? null, + currentSourceSeq: item.currentSourceSeq ?? null, + previousEventSeq: item.previousEventSeq ?? null, + currentEventSeq: item.currentEventSeq ?? null, + previousPreview: String(item.previousPreview || "").slice(0, 120), + currentPreview: String(item.currentPreview || "").slice(0, 120), + })), + traceCompletionNotLast: (recentWindow.sampleMetrics?.traceOrder?.completionNotLast || []).slice(0, 8).map((item) => ({ + sampleSeq: item.sampleSeq ?? null, + sampleIndex: item.sampleIndex ?? null, + timestamp: item.timestamp ?? null, + pageRole: item.pageRole ?? null, + traceId: item.traceId ?? null, + completionRowIndex: item.completionRowIndex ?? null, + laterRowIndex: item.laterRowIndex ?? null, + completionTotalSeconds: item.completionTotalSeconds ?? null, + laterTotalSeconds: item.laterTotalSeconds ?? null, + completionProjectedSeq: item.completionProjectedSeq ?? null, + laterProjectedSeq: item.laterProjectedSeq ?? null, + completionPreview: String(item.completionPreview || "").slice(0, 120), + laterPreview: String(item.laterPreview || "").slice(0, 120), + })), + roundCompletionElapsedMismatches: (recentWindow.sampleMetrics?.codeAgentCardTiming?.roundCompletion?.elapsedMismatches || []).slice(0, 8).map((item) => ({ + seq: item.seq ?? null, + ts: item.ts ?? null, + pageRole: item.pageRole ?? null, + promptIndex: item.promptIndex ?? null, + traceId: item.traceId ?? null, + completionElapsedSeconds: item.completionElapsedSeconds ?? null, + cardTotalElapsedSeconds: item.cardTotalElapsedSeconds ?? null, + deltaSeconds: item.deltaSeconds ?? null, + toleranceSeconds: item.toleranceSeconds ?? null, + })), + codeAgentCardDurationUnderreported: (recentWindow.sampleMetrics?.codeAgentCardTiming?.durationUnderreported || []).slice(0, 8).map((item) => ({ + sampleIndex: item.sampleIndex ?? null, + timestamp: item.timestamp ?? null, + pageRole: item.pageRole ?? null, + traceId: item.traceId ?? null, + cardTotalElapsedSeconds: item.cardTotalElapsedSeconds ?? null, + expectedElapsedSeconds: item.expectedElapsedSeconds ?? null, + deltaSeconds: item.deltaSeconds ?? null, + evidenceKind: item.evidenceKind ?? null, + evidencePreview: String(item.evidencePreview || "").slice(0, 120), + })), + codeAgentCardDurationMismatches: (recentWindow.sampleMetrics?.codeAgentCardTiming?.durationMismatches || []).slice(0, 8).map((item) => ({ + sampleIndex: item.sampleIndex ?? null, + timestamp: item.timestamp ?? null, + pageRole: item.pageRole ?? null, + traceId: item.traceId ?? null, + direction: item.direction ?? null, + cardTotalElapsedSeconds: item.cardTotalElapsedSeconds ?? null, + expectedElapsedSeconds: item.expectedElapsedSeconds ?? null, + signedDeltaSeconds: item.signedDeltaSeconds ?? null, + deltaSeconds: item.deltaSeconds ?? null, + evidenceKind: item.evidenceKind ?? null, + exactEvidence: item.exactEvidence === true, + evidencePreview: String(item.evidencePreview || "").slice(0, 120), + })), + valuesRedacted: true, +})); + +async function readJson(file) { + try { return JSON.parse(await readFile(file, "utf8")); } catch { return null; } +} + +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, + crossPageProjectionDivergenceRedMs: positiveNumber(raw.crossPageProjectionDivergenceRedMs, requiredPositiveThreshold(raw, "visibleLoadingSlowMs")), + source: "yaml-env", + }; +} + +async function readJsonl(file, options = {}) { + const tailLimit = Number.isFinite(Number(options.tail)) && Number(options.tail) > 0 ? Math.floor(Number(options.tail)) : 0; + if (tailLimit > 0) return readJsonlTail(file, tailLimit, options); + const rows = []; + try { + const input = createReadStream(file, { encoding: "utf8" }); + const lines = createInterface({ input, crlfDelay: Infinity }); + let lineNo = 0; + for await (const rawLine of lines) { + lineNo += 1; + const line = String(rawLine || "").trim(); + if (!line) continue; + try { + const parsed = JSON.parse(line); + rows.push(typeof options.compact === "function" ? options.compact(parsed) : parsed); + } catch (error) { + const item = { parseError: true, lineNo, rawHash: sha256(line), errorMessage: limitText(error && error.message ? error.message : String(error), 240) }; + rows.push(item); + if (jsonlReadIssues.length < 50) jsonlReadIssues.push({ file: path.basename(file), kind: "parse-error", lineNo, rawHash: item.rawHash, errorMessage: item.errorMessage }); + } + } + return rows; + } catch (error) { + if (error && error.code === "ENOENT") return []; + if (jsonlReadIssues.length < 50) jsonlReadIssues.push({ file: path.basename(file), kind: "read-error", code: error && error.code ? String(error.code) : null, errorMessage: limitText(error && error.message ? error.message : String(error), 240) }); + return []; + } +} + +async function readJsonlTail(file, limit, options = {}) { + try { + const lines = await readTailLines(file, limit); + const rows = []; + let lineNo = 0; + for (const rawLine of lines) { + lineNo += 1; + const line = String(rawLine || "").trim(); + if (!line) continue; + try { + const parsed = JSON.parse(line); + rows.push(typeof options.compact === "function" ? options.compact(parsed) : parsed); + } catch (error) { + const item = { parseError: true, lineNo, tail: true, rawHash: sha256(line), errorMessage: limitText(error && error.message ? error.message : String(error), 240) }; + rows.push(item); + if (jsonlReadIssues.length < 50) jsonlReadIssues.push({ file: path.basename(file), kind: "parse-error", lineNo, tail: true, rawHash: item.rawHash, errorMessage: item.errorMessage }); + } + } + return rows; + } catch (error) { + if (error && error.code === "ENOENT") return []; + if (jsonlReadIssues.length < 50) jsonlReadIssues.push({ file: path.basename(file), kind: "read-error", tail: true, code: error && error.code ? String(error.code) : null, errorMessage: limitText(error && error.message ? error.message : String(error), 240) }); + return []; + } +} + +async function readTailLines(file, limit) { + const info = await stat(file); + if (!info.size || limit <= 0) return []; + const chunkSize = 64 * 1024; + const maxBytes = Math.max(32 * 1024 * 1024, Math.min(256 * 1024 * 1024, limit * 512 * 1024)); + const chunks = []; + let position = info.size; + let readBytes = 0; + let newlineCount = 0; + while (position > 0 && newlineCount <= limit && readBytes < maxBytes) { + const readSize = Math.min(chunkSize, position, maxBytes - readBytes); + position -= readSize; + const chunk = await readFileSlice(file, position, readSize); + chunks.unshift(chunk); + readBytes += chunk.length; + for (let index = 0; index < chunk.length; index += 1) { + if (chunk[index] === 10) newlineCount += 1; + } + } + if (position > 0 && newlineCount <= limit && jsonlReadIssues.length < 50) { + jsonlReadIssues.push({ file: path.basename(file), kind: "tail-scan-truncated", limit, readBytes, fileBytes: info.size }); + } + const text = Buffer.concat(chunks).toString("utf8"); + let lines = text.split(/\r?\n/u); + if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop(); + if (position > 0 && lines.length > 0) lines.shift(); + return lines.slice(-limit); +} + +async function readFileSlice(file, start, length) { + const chunks = []; + await new Promise((resolve, reject) => { + const stream = createReadStream(file, { start, end: start + length - 1 }); + stream.on("data", (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))); + stream.on("error", reject); + stream.on("end", resolve); + }); + return Buffer.concat(chunks); +} + +function sampleTimeWindow(samples, paddingMs) { + const times = samples + .map((item) => Date.parse(String(item && item.ts || ""))) + .filter((value) => Number.isFinite(value)); + if (times.length === 0) return { startMs: null, endMs: null, startAt: null, endAt: null, paddingMs }; + const startMs = Math.max(0, Math.min(...times) - Math.max(0, paddingMs || 0)); + const endMs = Math.max(...times) + Math.max(0, paddingMs || 0); + return { startMs, endMs, startAt: new Date(startMs).toISOString(), endAt: new Date(endMs).toISOString(), paddingMs }; +} + +function filterRowsByTimeWindow(rows, window) { + if (!window || !Number.isFinite(window.startMs) || !Number.isFinite(window.endMs)) return rows; + return rows.filter((item) => { + const value = item && (item.ts || item.observedAt || item.startedAt || item.finishedAt || item.createdAt || item.updatedAt); + const ms = Date.parse(String(value || "")); + if (!Number.isFinite(ms)) return true; + return ms >= window.startMs && ms <= window.endMs; + }); +} + +function analysisFocusFromControl(control) { + const completedNewSessions = (Array.isArray(control) ? control : []) + .filter((item) => item?.type === "newSession" && item?.phase === "completed" && Number.isFinite(Date.parse(String(item.ts || "")))) + .sort((a, b) => Date.parse(String(a.ts || "")) - Date.parse(String(b.ts || ""))); + const latest = completedNewSessions.at(-1) ?? null; + if (!latest) return { mode: "all", reason: "no-new-session-command", startAt: null, startMs: null, commandId: null, valuesRedacted: true }; + const startMs = Date.parse(String(latest.ts)); + return { + mode: "after-new-session", + reason: "latest-completed-new-session", + startAt: new Date(startMs).toISOString(), + startMs, + commandId: latest.commandId ?? null, + valuesRedacted: true + }; +} + +function applyAnalysisFocus(rows, focus, graceMs = 0) { + if (!focus || !Number.isFinite(focus.startMs)) return rows; + const minMs = focus.startMs - Math.max(0, Number(graceMs) || 0); + return (Array.isArray(rows) ? rows : []).filter((item) => { + const value = item && (item.ts || item.observedAt || item.startedAt || item.finishedAt || item.createdAt || item.updatedAt); + const ms = Date.parse(String(value || "")); + return Number.isFinite(ms) ? ms >= minMs : true; + }); +} + +function analysisControlWindow(sampleWindow, focus, graceMs = 0) { + if (!sampleWindow || !Number.isFinite(sampleWindow.endMs)) return sampleWindow; + if (!focus || !Number.isFinite(focus.startMs)) return sampleWindow; + const startMs = Math.max(0, Math.min(Number(sampleWindow.startMs ?? focus.startMs), focus.startMs - Math.max(0, Number(graceMs) || 0))); + return { + ...sampleWindow, + startMs, + startAt: new Date(startMs).toISOString() + }; +} + +function compactSampleForAnalysis(sample) { + if (!sample || typeof sample !== "object") return sample; + return { + seq: sample.seq ?? null, + ts: sample.ts ?? null, + reason: sample.reason ?? null, + sampleGroupSeq: sample.sampleGroupSeq ?? null, + pageId: sample.pageId ?? null, + pageRole: sample.pageRole ?? null, + commandId: sample.commandId ?? null, + observerInitiated: sample.observerInitiated ?? null, + url: sample.url ?? null, + path: sample.path ?? null, + routeSessionId: sample.routeSessionId ?? null, + activeSessionId: sample.activeSessionId ?? null, + messages: compactDomItems(sample.messages), + traceRows: compactDomItems(sample.traceRows), + loadings: compactLoadingItems(sample.loadings), + sessionRail: compactSessionRail(sample.sessionRail), + turns: compactDomItems(sample.turns), + diagnostics: compactDomItems(sample.diagnostics), + 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" || item.type === "observer-periodic-refresh") + .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)); + const previousDigestByPage = new Map(); + for (const sample of samples) { + const pageKey = samplePageKey(sample); + const previous = previousDigestByPage.get(pageKey); + const next = digestSample(sample); + if (previous && previous.digest !== next && !commandedPromptSeqs.has(sample?.seq) && !nearCommand(sample, commandTimes, alertThresholds.uncommandedStateChangeCommandWindowMs)) { + uncommandedChanges.push({ + ...ref(sample), + previousSeq: previous.sample?.seq ?? null, + previousTs: previous.sample?.timestamp ?? previous.sample?.ts ?? null, + fromDigest: previous.digest, + toDigest: next, + fromMessageCount: previous.sample?.messageCount ?? previous.sample?.messages?.length ?? null, + toMessageCount: sample?.messageCount ?? sample?.messages?.length ?? null, + fromTraceRowCount: Array.isArray(previous.sample?.traceRows) ? previous.sample.traceRows.length : previous.sample?.traceRowCount ?? null, + toTraceRowCount: Array.isArray(sample?.traceRows) ? sample.traceRows.length : sample?.traceRowCount ?? null, + fromPath: previous.sample?.path ?? previous.sample?.urlPath ?? null, + toPath: sample?.path ?? sample?.urlPath ?? null, + detail: "visible digest changed without nearby command", + }); + } + previousDigestByPage.set(pageKey, { digest: next, sample }); + } + if (uncommandedChanges.length > 0) findings.push({ id: "uncommanded-visible-state-change", severity: "amber", summary: "visible message/trace digest changed without a nearby command", count: uncommandedChanges.length, samples: uncommandedChanges.slice(0, 20) }); + const finalFlicker = detectFinalFlicker(samples); + if (finalFlicker.length > 0) findings.push({ id: "final-response-flicker", severity: "red", summary: "message text digest disappeared or switched to diagnostic-like text after non-empty final text", count: finalFlicker.length, samples: finalFlicker.slice(0, 20) }); + const terminalZeroElapsed = detectTerminalZeroElapsed(samples); + if (terminalZeroElapsed.length > 0) findings.push({ id: "turn-terminal-zero-elapsed", severity: "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 or /v1/agent/chat/steer POST response in the sampling window", count: promptFailures.length, rounds: promptFailures.slice(0, 10) }); + const promptSteerRounds = Array.isArray(promptNetwork?.rounds) ? promptNetwork.rounds.filter((item) => item.steerUsed === true) : []; + if (promptSteerRounds.length > 0) findings.push({ id: "prompt-routed-to-steer", severity: "amber", summary: "sendPrompt was submitted through /v1/agent/chat/steer; verify the previous turn was truly in-flight and not an unsealed terminal failure", count: promptSteerRounds.length, rounds: promptSteerRounds.slice(0, 10) }); + const elapsedZeroResets = Array.isArray(sampleMetrics?.turnTimingElapsedZeroResets) ? sampleMetrics.turnTimingElapsedZeroResets : []; + if (elapsedZeroResets.length > 0) findings.push({ id: "turn-timing-total-elapsed-zero-reset", severity: "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 elapsedDecreases = Array.isArray(sampleMetrics?.turnTimingNonMonotonic) + ? sampleMetrics.turnTimingNonMonotonic.filter((item) => item.metric === "totalElapsedSeconds" && item.anomaly !== "zero-reset") + : []; + if (elapsedDecreases.length > 0) findings.push({ id: "turn-timing-total-elapsed-decrease", severity: "red", summary: "Code Agent total elapsed decreased between adjacent samples; total elapsed must be monotonic per turn", count: elapsedDecreases.length, samples: elapsedDecreases.slice(0, 20) }); + const elapsedForwardJumps = Array.isArray(sampleMetrics?.turnTimingTotalElapsedForwardJumps) ? sampleMetrics.turnTimingTotalElapsedForwardJumps : []; + if (elapsedForwardJumps.length > 0) findings.push({ id: "turn-timing-total-elapsed-forward-jump", severity: "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"); + const crossPageProjectionBudgetMs = alertThresholds.crossPageProjectionDivergenceRedMs; + const timedCrossPageProjectionDiffs = annotateCrossPageDiffTiming(crossPageProjectionDiffs); + const persistentCrossPageProjectionDiffs = timedCrossPageProjectionDiffs.filter((item) => Number(item.observedSpanMs ?? 0) > crossPageProjectionBudgetMs); + const transientCrossPageProjectionDiffs = timedCrossPageProjectionDiffs.filter((item) => Number(item.observedSpanMs ?? 0) <= crossPageProjectionBudgetMs); + if (persistentCrossPageProjectionDiffs.length > 0) findings.push({ id: "cross-page-projection-divergence", severity: "red", summary: "control and observer pages saw different projection state for the same sampled session beyond the configured budget", count: persistentCrossPageProjectionDiffs.length, budgetMs: crossPageProjectionBudgetMs, samples: persistentCrossPageProjectionDiffs.slice(0, 20) }); + if (transientCrossPageProjectionDiffs.length > 0) findings.push({ id: "cross-page-projection-transient-divergence", severity: "info", summary: "control and observer pages briefly differed near a sampled transition; retained as transient evidence but not treated as persistent projection failure", count: transientCrossPageProjectionDiffs.length, budgetMs: crossPageProjectionBudgetMs, samples: transientCrossPageProjectionDiffs.slice(0, 20) }); + if (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" && promptSubmitModeForUrl(item?.url) !== null) + .map((item) => { + const failureText = item.failureKind ?? item.failure ?? item.errorText ?? null; + const urlPathValue = urlPath(item.url); + return { + ts: item.ts ?? null, + tsMs: Date.parse(item.ts), + type: item.type ?? null, + status: Number.isFinite(Number(item.status)) ? Number(item.status) : null, + commandId: item.commandId ?? null, + urlPath: urlPathValue, + submitMode: promptSubmitModeForUrl(item.url), + failureKind: failureText ? String(failureText) : null, + errorTextHash: failureText ? sha256(failureText) : null + }; + }) + .filter((item) => Number.isFinite(item.tsMs)) + .sort((a, b) => a.tsMs - b.tsMs); + const rounds = prompts.map((prompt) => { + const startMs = Date.parse(prompt.startedAt || prompt.completedAt || prompt.failedAt || ""); + const endAnchorMs = Date.parse(prompt.completedAt || prompt.failedAt || prompt.startedAt || ""); + const fromMs = Number.isFinite(startMs) ? startMs - 3000 : Number.NEGATIVE_INFINITY; + const toMs = Number.isFinite(endAnchorMs) ? endAnchorMs + 30000 : Number.POSITIVE_INFINITY; + const events = chatEvents.filter((event) => { + if (event.commandId && prompt.commandId && event.commandId === prompt.commandId) return true; + return event.tsMs >= fromMs && event.tsMs <= toMs; + }); + const responses = events.filter((event) => event.type === "response"); + const failures = events.filter((event) => event.type === "requestfailed"); + const responseStatuses = responses.map((event) => event.status).filter((status) => status !== null); + const submitModes = Array.from(new Set(events.map((event) => event.submitMode).filter(Boolean))).sort(); + const chatPostOk = responseStatuses.some((status) => status >= 200 && status < 300) && 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, + submitModes, + steerUsed: submitModes.includes("steer"), + firstChatEventAt: events[0]?.ts ?? null, + lastChatEventAt: events[events.length - 1]?.ts ?? null, + events: events.slice(0, 12).map((event) => ({ ts: event.ts, type: event.type, status: event.status, urlPath: event.urlPath, submitMode: event.submitMode, failureKind: event.failureKind, errorTextHash: event.errorTextHash })) + }; + }); + return { + summary: { + promptCount: rounds.length, + chatPostOk: rounds.filter((item) => item.chatPostOk === true).length, + chatPostFailed: rounds.filter((item) => item.chatPostOk === false).length, + chatPostMissing: rounds.filter((item) => item.failureKind === "missing-response").length + }, + rounds + }; +} + +function promptSubmitModeForUrl(value) { + const pathValue = urlPath(value); + if (pathValue === "/v1/agent/chat") return "chat"; + if (pathValue === "/v1/agent/chat/steer") return "steer"; + return null; +} + +function parseDomDiagnosticSummary(text) { + const value = String(text || ""); + const traceMatch = value.match(/\b(?:trace_id=)?(trc_[A-Za-z0-9_-]+|[a-f0-9]{16,64})\b/iu); + const httpStatusMatch = value.match(/\bHTTP\s+([1-5][0-9]{2})\b/iu); + const idleMatch = value.match(/\bidle\s+(\d+)s\b/iu); + const waitingForMatch = value.match(/\bwaitingFor=([^\s;;,,)]+)/iu); + const lastEventLabelMatch = value.match(/\blastEventLabel=([^\s;;,,)]+)/iu); + const diagnosticCode = httpStatusMatch + ? "http-" + httpStatusMatch[1] + : /turn\s*超过|无新活动/iu.test(value) + ? "turn-idle-no-activity" + : /Failed to fetch/iu.test(value) + ? "failed-to-fetch" + : "diagnostic"; + return { + diagnosticCode, + traceId: traceMatch?.[1] || null, + httpStatus: httpStatusMatch ? Number(httpStatusMatch[1]) : null, + idleSeconds: idleMatch ? Number(idleMatch[1]) : null, + waitingFor: waitingForMatch?.[1] || null, + lastEventLabel: lastEventLabelMatch?.[1] || null + }; +} + +function isDomDiagnosticSampleText(text) { + const value = String(text || "").replace(/\s+/g, " ").trim(); + if (!value) return false; + const strongDiagnostic = [ + /\bHTTP\s+[45][0-9]{2}\b(?:[\s\S]{0,120}\btrace_id=|\b)/iu, + /\btrace_id=(?:trc_[A-Za-z0-9_-]+|[a-f0-9]{16,64})\b/iu, + /workbench\s+turn\s*超过\s*\d+ms\s*无新活动/iu, + /\bturn\s*超过\b[\s\S]{0,120}\b无新活动\b/iu, + /\bprojection-resume:sync-failed\b/iu, + /\bAgentRun\s+GET\b[\s\S]*\/result\b[\s\S]*timed out after\s+\d+ms\b/iu, + /\bFailed to fetch\b/iu, + /\bFailed to load resource\b[\s\S]{0,180}\bstatus of\s+[45][0-9]{2}\b/iu, + /\bserver responded with a status of\s+[45][0-9]{2}\b/iu, + /Code Agent\b[\s\S]{0,120}(?:无法连接上游|请求已结束)/iu + ].some((pattern) => pattern.test(value)); + if (!strongDiagnostic) return false; + const looksLikeToolStdout = /\b(?:stdout|stderr):/iu.test(value) + && /(?:\becho\s+["']?===|\bnode\s+|\.tspy\b|tspy\/|===\s*[A-Za-z0-9_.-]+\s*===)/iu.test(value); + if (!looksLikeToolStdout) return true; + return /\b(?:trace_id=|HTTP\s+[45][0-9]{2}|workbench\s+turn\s*超过|projection-resume:sync-failed|Failed to fetch|Failed to load resource|server responded with a status of\s+[45][0-9]{2}|Code Agent\b[\s\S]{0,120}(?:无法连接上游|请求已结束))\b/iu.test(value); +} + +function buildRuntimeAlerts(samples, control, network, consoleEvents, errors) { + const promptTimes = control + .filter((item) => item.type === "sendPrompt" && item.phase === "completed") + .map((item) => Date.parse(item.ts)) + .filter(Number.isFinite) + .sort((a, b) => a - b); + const observerRefreshTimes = control + .filter((item) => item.type === "observer-periodic-refresh") + .map((item) => Date.parse(item.ts)) + .filter(Number.isFinite) + .sort((a, b) => a - b); + const naturalNetwork = network.filter((item) => item?.observerInitiated !== true); + const httpErrors = naturalNetwork + .filter((item) => item?.type === "response" && Number(item.status) >= 400) + .map((item) => networkAlertEvent(item, promptTimes)); + const requestFailed = naturalNetwork + .filter((item) => item?.type === "requestfailed") + .map((item) => networkAlertEvent(item, promptTimes)); + const significantRequestFailed = requestFailed.filter( + (item) => !isBenignLongLivedStreamClosureAlert(item) && !isObserverRefreshClosureAlert(item, observerRefreshTimes), + ); + const domDiagnostics = []; + const executionErrors = []; + const baselineExecutionErrors = []; + const 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) && !isObserverRefreshClosureAlert(item, observerRefreshTimes)); + const pageErrors = errors.map((item) => ({ + ts: item.ts ?? null, + promptIndex: promptIndexForTs(promptTimes, item.ts), + type: item.type ?? null, + errorName: item.error?.name ?? item.name ?? null, + messageHash: item.error?.message ? sha256(item.error.message) : item.message ? sha256(item.message) : null, + preview: limitText(item.error?.message || item.message || item.error || "", 220) + })); + return { + summary: { + httpErrorCount: httpErrors.length, + requestFailedCount: requestFailed.length, + significantRequestFailedCount: significantRequestFailed.length, + benignLongLivedStreamClosureCount: requestFailed.length - significantRequestFailed.length, + domDiagnosticSampleCount: domDiagnostics.length, + domDiagnosticGroupCount: groupDomDiagnostics(domDiagnostics).length, + executionErrorCount: executionErrors.length, + baselineExecutionErrorCount: baselineExecutionErrors.length, + consoleAlertCount: consoleAlerts.length, + significantConsoleAlertCount: significantConsoleAlerts.length, + pageErrorCount: pageErrors.length, + networkErrorGroupCount: groupNetworkAlerts(httpErrors).length, + requestFailedGroupCount: groupNetworkAlerts(requestFailed).length, + significantRequestFailedGroupCount: groupNetworkAlerts(significantRequestFailed).length, + executionErrorGroupCount: groupExecutionErrors(executionErrors).length, + baselineExecutionErrorGroupCount: groupExecutionErrors(baselineExecutionErrors).length, + consoleAlertGroupCount: groupConsoleAlerts(consoleAlerts).length, + significantConsoleAlertGroupCount: groupConsoleAlerts(significantConsoleAlerts).length + }, + networkHttpErrorsByPath: groupNetworkAlerts(httpErrors), + networkRequestFailedByPath: groupNetworkAlerts(requestFailed), + networkSignificantRequestFailedByPath: groupNetworkAlerts(significantRequestFailed), + domDiagnostics: domDiagnostics.slice(-80), + domDiagnosticsByText: groupDomDiagnostics(domDiagnostics), + domDiagnosticsByFingerprint: groupDomDiagnostics(domDiagnostics).slice(0, 80), + runtimeExecutionErrors: executionErrors.slice(0, 120), + runtimeExecutionErrorsByCode: groupExecutionErrors(executionErrors), + baselineRuntimeExecutionErrors: baselineExecutionErrors.slice(0, 80), + baselineRuntimeExecutionErrorsByCode: groupExecutionErrors(baselineExecutionErrors), + consoleAlerts: consoleAlerts.slice(0, 80), + consoleAlertsByPath: groupConsoleAlerts(consoleAlerts), + significantConsoleAlerts: significantConsoleAlerts.slice(0, 80), + significantConsoleAlertsByPath: groupConsoleAlerts(significantConsoleAlerts), + pageErrors: pageErrors.slice(0, 40) + }; +} + +function groupDomDiagnostics(events) { + const groups = new Map(); + for (const item of events || []) { + const preview = String(item?.preview || "").trim(); + if (!isReportableDomDiagnostic(item, preview)) continue; + const normalizedPreview = normalizeDiagnosticPreview(preview); + const key = [ + item?.diagnosticCode || "", + normalizedPreview + ].join("|"); + const existing = groups.get(key) || { + source: item?.source || null, + sources: new Set(), + diagnosticCode: item?.diagnosticCode || null, + textHash: item?.textHash || null, + normalizedPreview, + preview, + count: 0, + firstAt: item?.ts || null, + lastAt: item?.ts || null, + promptIndexes: new Set(), + traceIds: new Set(), + sampleSeqs: [] + }; + if (item?.source) existing.sources.add(String(item.source)); + existing.count += 1; + existing.firstAt = minIso(existing.firstAt, item?.ts || null); + existing.lastAt = maxIso(existing.lastAt, item?.ts || null); + if (Number.isFinite(Number(item?.promptIndex))) existing.promptIndexes.add(Number(item.promptIndex)); + for (const traceId of extractDiagnosticTraceIds(item, preview)) existing.traceIds.add(traceId); + if (existing.sampleSeqs.length < 12 && item?.seq !== undefined && item?.seq !== null) existing.sampleSeqs.push(item.seq); + groups.set(key, existing); + } + return Array.from(groups.values()) + .map((item) => ({ + source: item.source, + sources: Array.from(item.sources).sort(), + diagnosticCode: item.diagnosticCode, + textHash: item.textHash, + normalizedPreview: item.normalizedPreview, + preview: item.preview, + count: item.count, + firstAt: item.firstAt, + lastAt: item.lastAt, + promptIndexes: Array.from(item.promptIndexes).sort((a, b) => a - b), + traceIds: Array.from(item.traceIds).sort(), + sampleSeqs: item.sampleSeqs + })) + .sort((a, b) => (b.count - a.count) || String(a.firstAt || "").localeCompare(String(b.firstAt || ""))); +} + +function isReportableDomDiagnostic(item, preview) { + if (item?.source === "diagnostic-node" || item?.source === "execution-row") return true; + return /trace_id=|HTTP\s+\d{3}\b|Failed to load resource|ERR_[A-Z_]+|provider-unavailable|AgentRun error|超过\s*\d+\s*ms\s*无新活动|代理暂时无法连接上游|Trace 更新超时|加载失败/iu.test(String(preview || "")); +} + +function normalizeDiagnosticPreview(text) { + return String(text || "") + .replace(/trace_id=[A-Za-z0-9_-]+/gu, "trace_id=:traceId") + .replace(/\btrc_[A-Za-z0-9_-]+\b/gu, "trc_:traceId") + .replace(/\bses_[A-Za-z0-9_-]+\b/gu, "ses_:sessionId") + .replace(/\brun_[A-Za-z0-9_-]+\b/gu, "run_:runId") + .replace(/\bcmd_[A-Za-z0-9_-]+\b/gu, "cmd_:commandId") + .replace(/[!!]+$/gu, "") + .replace(/\s+/gu, " ") + .trim(); +} + +function extractDiagnosticTraceIds(item, preview) { + const ids = new Set(); + if (item?.traceId) ids.add(String(item.traceId)); + const text = String(preview || ""); + for (const match of text.matchAll(/\btrc_[A-Za-z0-9_-]+\b/gu)) ids.add(match[0]); + for (const match of text.matchAll(/trace_id=([A-Za-z0-9_-]+)/gu)) ids.add(match[1]); + return ids; +} + +function minIso(a, b) { + if (!a) return b || null; + if (!b) return a || null; + return Date.parse(a) <= Date.parse(b) ? a : b; +} + +function maxIso(a, b) { + if (!a) return b || null; + if (!b) return a || null; + return Date.parse(a) >= Date.parse(b) ? a : b; +} + +function sampleExecutionErrorCandidates(sample) { + const candidates = []; + const add = (source, items) => { + if (!Array.isArray(items)) return; + for (const item of items) { + const text = String(item?.textPreview || item?.text || item?.preview || "").trim(); + if (!text) continue; + if (!parseExecutionErrorText(text)) continue; + candidates.push({ + source, + text, + traceId: item?.traceId ?? null, + messageId: item?.messageId ?? null, + status: item?.status ?? null + }); + } + }; + add("diagnostic-node", sample?.diagnostics); + add("message", sample?.messages); + add("trace-row", sample?.traceRows); + add("turn", sample?.turns); + const specific = candidates.filter((candidate) => { + const parsed = parseExecutionErrorText(candidate.text); + return parsed && parsed.code !== "error"; + }); + return specific.length > 0 ? specific : candidates; +} + +function parseExecutionErrorText(text) { + const value = String(text || ""); + const agentRunCodeMatch = value.match(/\bagentrun:error:([A-Za-z0-9_.:-]+)/u); + const agentRunText = /\bAgentRun\s+error\b|\bagentrun:error:/iu.test(value); + const providerUnavailable = /\bprovider[-_\s]*unavailable\b/iu.test(value); + if (!agentRunCodeMatch && !agentRunText && !providerUnavailable) return null; + const statusMatch = value.match(/\b(fail(?:ed)?|error|blocked|cancel(?:ed)?)\b/iu); + const traceMatch = value.match(/\btrc_[A-Za-z0-9_-]+\b/u); + const totalMatch = value.match(/\btotal\s*=\s*([0-9]{1,2}:[0-9]{2}(?::[0-9]{2})?)\b/iu) + || value.match(/总耗时\s*[::]?\s*([0-9]{1,2}:[0-9]{2}(?::[0-9]{2})?)/iu); + const agentRunCode = cleanExecutionCode(agentRunCodeMatch?.[1] || ""); + const rawCode = agentRunCode ? "agentrun:error:" + agentRunCode : providerUnavailable ? "provider-unavailable" : "agentrun:error"; + return { + backend: agentRunText || agentRunCodeMatch ? "agentrun" : "unknown", + status: normalizeExecutionStatus(statusMatch?.[1] || "error"), + code: agentRunCode || (providerUnavailable ? "provider-unavailable" : "error"), + rawCode, + totalSeconds: totalMatch ? parseClockDurationSeconds(totalMatch[1]) : null, + traceId: traceMatch?.[0] || null + }; +} + +function cleanExecutionCode(code) { + const value = String(code || "").replace(/(?:AgentRun|Error|Failed).*$/u, "").replace(/[^A-Za-z0-9_.:-].*$/u, ""); + return value || null; +} + +function normalizeExecutionStatus(status) { + const value = String(status || "").toLowerCase(); + if (value === "failed") return "fail"; + if (value === "cancelled" || value === "canceled") return "cancel"; + return value || "error"; +} + +function parseClockDurationSeconds(value) { + const parts = String(value || "").split(":").map((part) => Number(part)); + if (parts.length === 2 && parts.every(Number.isFinite)) return parts[0] * 60 + parts[1]; + if (parts.length === 3 && parts.every(Number.isFinite)) return parts[0] * 3600 + parts[1] * 60 + parts[2]; + return null; +} + +function groupExecutionErrors(events) { + const groups = new Map(); + for (const event of events) { + const key = [event.backend || "-", event.status || "-", event.code || "-"].join(" "); + const group = groups.get(key) || { + backend: event.backend ?? null, + status: event.status ?? null, + code: event.code ?? null, + rawCode: event.rawCode ?? null, + count: 0, + firstAt: event.ts, + lastAt: event.ts, + promptIndexes: [], + traceIds: [], + sources: [] + }; + group.count += 1; + group.lastAt = event.ts; + if (event.promptIndex && !group.promptIndexes.includes(event.promptIndex)) group.promptIndexes.push(event.promptIndex); + if (event.traceId && !group.traceIds.includes(event.traceId)) group.traceIds.push(event.traceId); + if (event.source && !group.sources.includes(event.source)) group.sources.push(event.source); + groups.set(key, group); + } + return Array.from(groups.values()).sort((a, b) => b.count - a.count || String(a.code).localeCompare(String(b.code))); +} + +function consoleAlertEvent(item, promptTimes) { + const text = String(item?.text || ""); + const statusMatch = text.match(/\bstatus\s+of\s+([1-5][0-9]{2})\b/iu) || text.match(/\bHTTP\s+([1-5][0-9]{2})\b/iu); + const location = compactLocation(item.location); + const traceMatch = (location?.urlPath || text).match(/\btrc_[A-Za-z0-9_-]+\b/u); + return { + ts: item.ts ?? null, + promptIndex: promptIndexForTs(promptTimes, item.ts), + type: item.type ?? null, + status: statusMatch ? Number(statusMatch[1]) : null, + urlPath: location?.urlPath || "-", + traceId: traceMatch?.[0] || null, + textHash: item.text ? sha256(item.text) : null, + preview: limitText(text, 220), + location + }; +} + +function groupConsoleAlerts(events) { + const groups = new Map(); + for (const event of events) { + const key = [event.type || "-", event.status ?? "-", event.urlPath || "-"].join(" "); + const group = groups.get(key) || { + type: event.type ?? null, + status: event.status ?? null, + urlPath: event.urlPath || "-", + count: 0, + firstAt: event.ts, + lastAt: event.ts, + promptIndexes: [], + traceIds: [] + }; + group.count += 1; + group.lastAt = event.ts; + if (event.promptIndex && !group.promptIndexes.includes(event.promptIndex)) group.promptIndexes.push(event.promptIndex); + if (event.traceId && !group.traceIds.includes(event.traceId)) group.traceIds.push(event.traceId); + groups.set(key, group); + } + return Array.from(groups.values()).sort((a, b) => b.count - a.count || String(a.urlPath).localeCompare(String(b.urlPath))); +} + +function isBenignLongLivedStreamClosureAlert(event) { + if (event?.urlPath !== "/v1/workbench/events") return false; + if (event.status !== null && event.status !== undefined && Number(event.status) > 0) return false; + const text = String(event.failureKind || event.errorPreview || event.preview || ""); + return /ERR_NETWORK_CHANGED|ERR_ABORTED|net::ERR_NETWORK_CHANGED|net::ERR_ABORTED|aborted|network changed/iu.test(text); +} + +function isObserverRefreshClosureAlert(event, observerRefreshTimes) { + const urlPath = String(event?.urlPath || ""); + if (!["/v1/workbench/events", "/v1/web-performance"].includes(urlPath) && !/^\/v1\/workbench\/traces\/[^/]+\/events$/u.test(urlPath)) return false; + if (event.status !== null && event.status !== undefined && Number(event.status) > 0) return false; + const text = String(event.failureKind || event.errorPreview || event.preview || ""); + if (!/ERR_NETWORK_CHANGED|ERR_ABORTED|ERR_INCOMPLETE_CHUNKED_ENCODING|ERR_INVALID_CHUNKED_ENCODING|net::ERR_NETWORK_CHANGED|net::ERR_ABORTED|aborted|network changed|incomplete chunked|invalid chunked/iu.test(text)) return false; + const ts = Date.parse(String(event.ts || "")); + return Number.isFinite(ts) && observerRefreshTimes.some((refreshTs) => Math.abs(ts - refreshTs) <= 8000); +} + +function networkAlertEvent(item, promptTimes) { + const failureText = item.failureKind ?? item.failure ?? item.errorText ?? null; + return { + ts: item.ts ?? null, + promptIndex: promptIndexForTs(promptTimes, item.ts), + method: String(item.method || "GET").toUpperCase(), + status: Number.isFinite(Number(item.status)) ? Number(item.status) : null, + type: item.type ?? null, + urlPath: urlPath(item.url), + urlHash: item.url ? sha256(item.url) : null, + failureKind: failureText ? String(failureText) : null, + errorTextHash: failureText ? sha256(failureText) : null, + errorPreview: failureText ? limitText(failureText, 160) : null + }; +} + +function groupNetworkAlerts(events) { + const groups = new Map(); + for (const event of events) { + const key = [event.method, event.urlPath, event.status ?? "-", event.type].join(" "); + const group = groups.get(key) || { + method: event.method, + urlPath: event.urlPath, + status: event.status, + type: event.type, + count: 0, + firstAt: event.ts, + lastAt: event.ts, + promptIndexes: [], + failureKinds: [], + errorTextHashes: [] + }; + group.count += 1; + group.lastAt = event.ts; + if (event.promptIndex && !group.promptIndexes.includes(event.promptIndex)) group.promptIndexes.push(event.promptIndex); + if (event.failureKind && !group.failureKinds.includes(event.failureKind)) group.failureKinds.push(event.failureKind); + if (event.errorTextHash && !group.errorTextHashes.includes(event.errorTextHash)) group.errorTextHashes.push(event.errorTextHash); + groups.set(key, group); + } + return Array.from(groups.values()).sort((a, b) => b.count - a.count || String(a.urlPath).localeCompare(String(b.urlPath))); +} + +function isDiagnosticText(text) { + const value = String(text || ""); + return /Failed to (?:fetch|load resource)|request failed|net::ERR_[A-Z0-9_:-]+|server responded with a status of [45][0-9]{2}|HTTP\s+[45][0-9]{2}\b|trace_id=|workbench turn\s*超过|turn\s*超过|无新活动|idle\s+\d+s|waitingFor=|lastEventLabel=|无法连接上游|代理暂时无法连接上游|provider-unavailable|agentrun:error|AgentRun error|projection-resume|sync-failed|durable projection store|realtime-gap|Trace 更新超时|加载失败|请求失败|请求已失败/iu.test(value); +} + +function prioritizeFindings(findings) { + const items = Array.isArray(findings) ? findings : []; + const severityRank = (severity) => { + const value = String(severity || "").toLowerCase(); + if (value === "red") return 0; + if (value === "amber" || value === "warning") return 1; + if (value === "info") return 3; + return 2; + }; + const kindRank = (item) => { + const id = String(item?.id ?? item?.kind ?? item?.code ?? ""); + if (id === "page-performance-slow-same-origin-api") return 0; + if (id === "session-rail-title-fallback-majority") return 0.5; + if (id.startsWith("code-agent-card-")) return 0.8; + if (id.startsWith("round-completion-")) return 0.9; + if (id.startsWith("turn-timing-total-elapsed")) return 1; + if (id.startsWith("turn-timing-terminal-elapsed")) return 1.1; + if (id.startsWith("turn-timing-recent-update")) return 2; + if (id.includes("runtime-execution") || id.includes("prompt-chat-submit-failed")) return 3; + return 10; + }; + return items.slice().sort((left, right) => { + const kindDelta = kindRank(left) - kindRank(right); + if (kindDelta !== 0) return kindDelta; + return severityRank(left?.severity ?? left?.level) - severityRank(right?.severity ?? right?.level); + }); +} + +function isTerminalTraceText(text) { + return /轮次完成|轮次失败|轮次取消|已记录|已完成第\d+轮|final response|sealed final response|turn completed|turn failed|turn canceled|terminal result|\bcompleted\b|\bfailed\b|\bcanceled\b|\bcancelled\b|\bterminal\b|\bdone\b/iu.test(String(text || "")); +} + +function isFinalResultText(text) { + return /已完成第\d+轮|已按第\d+轮完成|final response|sealed final response|最终结果|已完成[::]|smoke\s*测试结果|benchmark|PVC\/workspace|修改文件|Results:/iu.test(String(text || "")); +} + +function buildSampleMetrics(samples, control) { + const promptCommands = buildSendPromptCommandTimeline(control); + const promptTimes = promptCommands.map((item) => item.tsMs); + const timeline = samples.map((sample) => { + const texts = sampleTexts(sample); + const timingTexts = sampleTurnTimingTexts(sample); + const tsMs = Date.parse(sample.ts); + const promptIndex = Number.isFinite(tsMs) ? latestPromptIndex(promptTimes, tsMs) : 0; + const totalElapsedValues = timingTexts.flatMap(parseTotalElapsedSeconds).filter(Number.isFinite); + const recentUpdateValues = timingTexts.flatMap(parseRecentUpdateSeconds).filter(Number.isFinite); + const diagnosticTexts = texts.filter(isDiagnosticText).slice(0, 5); + const terminalTexts = texts.filter(isTerminalTraceText).slice(0, 5); + const finalResultTexts = texts.filter(isFinalResultText).slice(0, 5); + const loadings = Array.isArray(sample.loadings) ? sample.loadings : []; + const loadingOwners = uniqueLoadingOwners(loadings); + return { + seq: sample.seq ?? null, + ts: sample.ts ?? null, + routeSessionId: sample.routeSessionId ?? null, + activeSessionId: sample.activeSessionId ?? null, + promptIndex, + messageCount: Array.isArray(sample.messages) ? sample.messages.length : 0, + traceRowCount: Array.isArray(sample.traceRows) ? sample.traceRows.length : 0, + loadingCount: loadings.length, + loadingOwnerCount: loadingOwners.length, + loadingOwners: loadingOwners.map((item) => ({ ownerKey: item.ownerKey, ownerKind: item.ownerKind, ownerLabel: item.ownerLabel, count: item.count })).slice(0, 12), + sessionRailVisibleCount: Number(sample?.sessionRail?.visibleCount ?? 0), + sessionRailFallbackTitleCount: Number(sample?.sessionRail?.fallbackTitleCount ?? 0), + sessionRailFallbackTitleRatio: Number(sample?.sessionRail?.fallbackTitleRatio ?? 0), + totalElapsedSeconds: totalElapsedValues.length > 0 ? Math.max(...totalElapsedValues) : null, + recentUpdateSeconds: recentUpdateValues.length > 0 ? Math.max(...recentUpdateValues) : null, + terminalSeen: terminalTexts.length > 0, + finalResultTextSeen: finalResultTexts.length > 0, + diagnosticSeen: diagnosticTexts.length > 0, + diagnosticTextHashes: diagnosticTexts.map(sha256).slice(0, 5), + textDigest: digestSample(sample) + }; + }); + const turnTiming = buildTurnTimingTable(samples, timeline); + const traceOrder = buildTraceOrderMetrics(samples, timeline); + const codeAgentCardTiming = buildCodeAgentCardTimingMetrics(samples, timeline, turnTiming); + const codeAgentCardDurationUnderreported = buildCodeAgentCardDurationUnderreportedMetrics(samples, timeline); + const codeAgentCardDurationMismatches = buildCodeAgentCardDurationMismatchMetrics(samples, timeline); + if (codeAgentCardTiming && codeAgentCardTiming.summary) { + codeAgentCardTiming.summary.durationUnderreportedCount = codeAgentCardDurationUnderreported.length; + codeAgentCardTiming.summary.durationMismatchCount = codeAgentCardDurationMismatches.length; + codeAgentCardTiming.durationUnderreported = codeAgentCardDurationUnderreported; + codeAgentCardTiming.durationMismatches = codeAgentCardDurationMismatches; + } + const turnCells = turnTiming.rows.flatMap((row) => Object.values(row.cells || {})); + const turnTimingNonMonotonic = Array.isArray(turnTiming.nonMonotonic) ? turnTiming.nonMonotonic : []; + const turnTimingElapsedZeroResets = Array.isArray(turnTiming.elapsedZeroResets) ? turnTiming.elapsedZeroResets : []; + const turnTimingTotalElapsedForwardJumps = Array.isArray(turnTiming.totalElapsedForwardJumps) ? turnTiming.totalElapsedForwardJumps : []; + const turnTimingRecentUpdateSawtoothJumps = turnTimingNonMonotonic.filter((item) => item.metric === "recentUpdateSeconds" && item.anomaly === "jump"); + const turnTimingTerminalElapsedGrowth = Array.isArray(turnTiming.terminalElapsedGrowth) ? turnTiming.terminalElapsedGrowth : []; + const turnTimingRecentUpdateResets = Array.isArray(turnTiming.recentUpdateResets) ? turnTiming.recentUpdateResets : []; + const turnTimingRecentUpdateSteps = Array.isArray(turnTiming.recentUpdateSteps) ? turnTiming.recentUpdateSteps : []; + const turnTimingTerminalElapsedGrowthDeltas = turnTimingTerminalElapsedGrowth + .map((item) => Number(item.delta)) + .filter((value) => Number.isFinite(value) && value > 0); + const turnTimingRecentUpdateLargestSteps = turnTimingRecentUpdateSteps + .filter((item) => Number.isFinite(Number(item.delta))) + .slice() + .sort((a, b) => Number(b.delta) - Number(a.delta)) + .slice(0, 200); + const turnTimingRecentUpdatePositiveSteps = turnTimingRecentUpdateSteps + .map((item) => Number(item.delta)) + .filter((value) => Number.isFinite(value) && value >= 0); + const turnTimingRecentUpdateExcessSteps = turnTimingRecentUpdateSteps + .map((item) => Number(item.excessiveIncreaseSeconds)) + .filter((value) => Number.isFinite(value) && value > 0); + const withTotal = timeline.filter((item) => item.totalElapsedSeconds !== null).length; + const withRecent = timeline.filter((item) => item.recentUpdateSeconds !== null).length; + const diagnostics = timeline.filter((item) => item.diagnosticSeen).length; + const loading = buildLoadingMetrics(samples, timeline); + const sessionRailTitles = buildSessionRailTitleMetrics(samples, timeline); + const reportTurnTimingRows = boundedTurnTimingRowsForReport(turnTiming.rows); + const reportTimeline = boundedRowsForReport(timeline); + const rounds = buildRoundMetricSummaries(timeline, promptCommands, { + columns: turnTiming.columns, + rows: turnTiming.rows, + nonMonotonic: turnTimingNonMonotonic, + elapsedZeroResets: turnTimingElapsedZeroResets, + totalElapsedForwardJumps: turnTimingTotalElapsedForwardJumps, + terminalElapsedGrowth: turnTimingTerminalElapsedGrowth, + recentUpdateResets: turnTimingRecentUpdateResets, + recentUpdateSteps: turnTimingRecentUpdateSteps + }); + const recentUpdateJumpCount = turnTimingRecentUpdateSawtoothJumps.length; + return { + summary: { + sampleCount: timeline.length, + withTotalElapsed: withTotal, + withRecentUpdate: withRecent, + diagnostics, + loadingSampleCount: loading.summary.loadingSampleCount, + loadingMaxCount: loading.summary.maxSimultaneousCount, + loadingMaxOwnerCount: loading.summary.maxSimultaneousOwnerCount, + loadingOwnerCount: loading.summary.ownerCount, + loadingConcurrentSampleCount: loading.summary.concurrentLoadingSampleCount, + loadingLongestContinuousSeconds: loading.summary.longestContinuousSeconds, + loadingCurrentContinuousSeconds: loading.summary.currentContinuousSeconds, + loadingOverFiveSecondSegmentCount: loading.summary.overFiveSecondSegmentCount, + loadingOverBudgetSegmentCount: loading.summary.overBudgetSegmentCount, + sessionRailSampleCount: sessionRailTitles.summary.sampleCount, + sessionRailVisibleSampleCount: sessionRailTitles.summary.visibleSampleCount, + sessionRailFallbackMajoritySampleCount: sessionRailTitles.summary.majorityFallbackSampleCount, + sessionRailFallbackMaxRatio: sessionRailTitles.summary.maxFallbackRatio, + sessionRailFallbackMaxVisibleCount: sessionRailTitles.summary.maxVisibleCount, + sessionRailFallbackMaxCount: sessionRailTitles.summary.maxFallbackTitleCount, + promptSegments: Math.max(0, promptTimes.length), + rounds: rounds.length, + turnColumns: turnTiming.columns.length, + turnTimingRows: turnTiming.rows.length, + turnCellsWithTotalElapsed: turnCells.filter((item) => item.totalElapsedSeconds !== null).length, + turnCellsWithRecentUpdate: turnCells.filter((item) => item.recentUpdateSeconds !== null).length, + turnTimingNonMonotonicCount: turnTimingNonMonotonic.length, + turnTimingTotalElapsedDecreaseCount: turnTimingNonMonotonic.filter((item) => item.metric === "totalElapsedSeconds").length, + turnTimingTotalElapsedZeroResetCount: turnTimingElapsedZeroResets.length, + turnTimingTotalElapsedForwardJumpCount: turnTimingTotalElapsedForwardJumps.length, + turnTimingTotalElapsedForwardJumpMaxSeconds: maxPositiveDelta(turnTimingTotalElapsedForwardJumps), + turnTimingTerminalElapsedGrowthCount: turnTimingTerminalElapsedGrowth.length, + turnTimingTerminalElapsedGrowthMaxSeconds: turnTimingTerminalElapsedGrowthDeltas.length > 0 ? Math.max(...turnTimingTerminalElapsedGrowthDeltas) : 0, + turnTimingRecentUpdateJumpCount: recentUpdateJumpCount, + turnTimingRecentUpdateSawtoothJumpCount: recentUpdateJumpCount, + turnTimingRecentUpdateStepCount: turnTimingRecentUpdateSteps.length, + turnTimingRecentUpdateMaxIncreaseSeconds: turnTimingRecentUpdatePositiveSteps.length > 0 ? Math.max(...turnTimingRecentUpdatePositiveSteps) : null, + turnTimingRecentUpdateMaxExcessSeconds: turnTimingRecentUpdateExcessSteps.length > 0 ? Math.max(...turnTimingRecentUpdateExcessSteps) : 0, + turnTimingRecentUpdateResetCount: turnTimingRecentUpdateResets.length, + turnTimingRecentUpdateDecreaseCount: turnTimingRecentUpdateResets.length, + codeAgentCardSampleCount: codeAgentCardTiming.summary.cardSampleCount, + codeAgentCardMissingElapsedCount: codeAgentCardTiming.summary.missingElapsedCount, + codeAgentCardMissingRecentUpdateCount: codeAgentCardTiming.summary.missingRecentUpdateCount, + roundCompletionEventCount: codeAgentCardTiming.summary.roundCompletionEventCount, + roundCompletionElapsedMismatchCount: codeAgentCardTiming.summary.roundCompletionElapsedMismatchCount, + roundCompletionFinalResponseMissingCount: codeAgentCardTiming.summary.roundCompletionFinalResponseMissingCount, + roundCompletionPostTimingChangeCount: codeAgentCardTiming.summary.roundCompletionPostTimingChangeCount, + codeAgentCardDurationUnderreportedCount: codeAgentCardTiming.summary.durationUnderreportedCount, + codeAgentCardDurationMismatchCount: codeAgentCardTiming.summary.durationMismatchCount, + traceRowCount: traceOrder.summary.traceRowCount, + traceRowOrderAnomalyCount: traceOrder.summary.orderAnomalyCount, + traceRowCompletionNotLastCount: traceOrder.summary.completionNotLastCount, + roundsWithTurnTimingNonMonotonic: rounds.filter((item) => item.turnTimingNonMonotonicCount > 0).length, + roundsWithTurnTimingTotalElapsedForwardJumps: rounds.filter((item) => item.turnTimingTotalElapsedForwardJumpCount > 0).length, + roundsWithTerminalElapsedGrowth: rounds.filter((item) => item.turnTimingTerminalElapsedGrowthCount > 0).length, + roundsWithRecentUpdateJumps: rounds.filter((item) => item.turnTimingRecentUpdateJumpCount > 0).length + }, + loading, + sessionRailTitles, + codeAgentCardTiming, + traceOrder, + rounds, + turnColumns: turnTiming.columns, + turnTimingTable: reportTurnTimingRows.rows, + turnTimingTableDisclosure: reportTurnTimingRows.disclosure, + turnTimingNonMonotonic, + turnTimingElapsedZeroResets, + turnTimingTotalElapsedForwardJumps, + turnTimingTerminalElapsedGrowth, + turnTimingRecentUpdateSawtoothJumps, + turnTimingRecentUpdateSteps, + turnTimingRecentUpdateLargestSteps, + turnTimingRecentUpdateResets, + timeline: reportTimeline.rows, + timelineDisclosure: reportTimeline.disclosure + }; +} + +function boundedRowsForReport(rows) { + const sourceRows = Array.isArray(rows) ? rows : []; + const maxRows = 1200; + const headRows = 120; + if (sourceRows.length <= maxRows) return { rows: sourceRows, disclosure: { truncated: false, totalRows: sourceRows.length, includedRows: sourceRows.length, omittedRows: 0, headRows: sourceRows.length, tailRows: 0 } }; + const tailRows = Math.max(0, maxRows - headRows); + return { + rows: [...sourceRows.slice(0, headRows), ...sourceRows.slice(-tailRows)], + disclosure: { + truncated: true, + totalRows: sourceRows.length, + includedRows: maxRows, + omittedRows: Math.max(0, sourceRows.length - maxRows), + headRows, + tailRows, + policy: "report-bounded-head-tail; summary metrics are computed before truncation", + valuesRedacted: true + } + }; +} + +function buildSendPromptCommandTimeline(control) { + const byCommand = new Map(); + let ordinal = 0; + for (const item of Array.isArray(control) ? control : []) { + if (item?.type !== "sendPrompt") continue; + const commandId = item.commandId ? String(item.commandId) : "sendPrompt-" + String(ordinal++); + const existing = byCommand.get(commandId) || { + commandId, + startedAt: null, + startedTsMs: null, + completedAt: null, + completedTsMs: null, + failedAt: null, + failedTsMs: null, + textHash: null, + textBytes: null, + }; + if (!existing.textHash && item.input?.textHash) existing.textHash = item.input.textHash; + if (!existing.textBytes && item.input?.textBytes) existing.textBytes = item.input.textBytes; + const tsMs = Date.parse(item.ts); + if (item.phase === "started" && Number.isFinite(tsMs)) { + existing.startedAt = item.ts ?? existing.startedAt; + existing.startedTsMs = tsMs; + } else if (item.phase === "completed" && Number.isFinite(tsMs)) { + existing.completedAt = item.ts ?? existing.completedAt; + existing.completedTsMs = tsMs; + } else if (item.phase === "failed" && Number.isFinite(tsMs)) { + existing.failedAt = item.ts ?? existing.failedAt; + existing.failedTsMs = tsMs; + } + byCommand.set(commandId, existing); + } + return Array.from(byCommand.values()) + .map((item) => { + const tsMs = Number.isFinite(item.startedTsMs) ? item.startedTsMs : Number.isFinite(item.completedTsMs) ? item.completedTsMs : item.failedTsMs; + const ts = Number.isFinite(item.startedTsMs) ? item.startedAt : Number.isFinite(item.completedTsMs) ? item.completedAt : item.failedAt; + return { ...item, ts, tsMs }; + }) + .filter((item) => Number.isFinite(item.tsMs)) + .sort((a, b) => a.tsMs - b.tsMs) + .map((item, index) => ({ ...item, promptIndex: index + 1 })); +} + +function buildSessionRailTitleMetrics(samples, timeline) { + const rows = []; + const examplesByHash = new Map(); + for (let index = 0; index < (Array.isArray(samples) ? samples : []).length; index += 1) { + const sample = samples[index]; + const rail = sample?.sessionRail && typeof sample.sessionRail === "object" ? sample.sessionRail : null; + if (!rail) continue; + const visibleCount = Number(rail.visibleCount ?? 0); + const fallbackTitleCount = Number(rail.fallbackTitleCount ?? 0); + const safeVisibleCount = Number.isFinite(visibleCount) && visibleCount > 0 ? visibleCount : 0; + const safeFallbackTitleCount = Number.isFinite(fallbackTitleCount) && fallbackTitleCount > 0 ? fallbackTitleCount : 0; + const fallbackTitleRatio = safeVisibleCount > 0 ? Number((safeFallbackTitleCount / safeVisibleCount).toFixed(4)) : 0; + const fallbackItems = Array.isArray(rail.fallbackItems) ? rail.fallbackItems : []; + for (const item of fallbackItems) { + const hash = String(item?.titleHash || item?.titlePreview || item?.sessionIdPrefix || "").trim(); + if (!hash || examplesByHash.has(hash)) continue; + examplesByHash.set(hash, { + titleHash: item?.titleHash ?? null, + titlePreview: limitText(String(item?.titlePreview || ""), 160), + sessionIdPrefix: item?.sessionIdPrefix ?? null, + active: item?.active === true, + firstSeq: sample?.seq ?? null, + firstAt: sample?.ts ?? null, + pageRole: sample?.pageRole ?? null, + }); + } + rows.push({ + ...ref(sample), + promptIndex: timeline[index]?.promptIndex ?? 0, + visibleCount: safeVisibleCount, + fallbackTitleCount: safeFallbackTitleCount, + fallbackTitleRatio, + majorityFallback: safeVisibleCount > 0 && safeFallbackTitleCount > safeVisibleCount / 2, + overThreshold: safeVisibleCount > 0 && fallbackTitleRatio > alertThresholds.sessionRailFallbackRatio, + examples: fallbackItems.slice(0, 5).map((item) => ({ + titleHash: item?.titleHash ?? null, + titlePreview: limitText(String(item?.titlePreview || ""), 160), + sessionIdPrefix: item?.sessionIdPrefix ?? null, + active: item?.active === true, + })), + }); + } + const visibleRows = rows.filter((item) => item.visibleCount > 0); + const majorityRows = rows.filter((item) => item.majorityFallback); + const overThresholdRows = rows.filter((item) => item.overThreshold); + const fallbackRows = rows.filter((item) => item.fallbackTitleCount > 0); + const maxFallbackRatio = rows.length > 0 ? Math.max(...rows.map((item) => Number(item.fallbackTitleRatio) || 0)) : 0; + const maxVisibleCount = rows.length > 0 ? Math.max(...rows.map((item) => Number(item.visibleCount) || 0)) : 0; + const maxFallbackTitleCount = rows.length > 0 ? Math.max(...rows.map((item) => Number(item.fallbackTitleCount) || 0)) : 0; + return { + summary: { + sampleCount: rows.length, + visibleSampleCount: visibleRows.length, + fallbackSampleCount: fallbackRows.length, + majorityFallbackSampleCount: majorityRows.length, + overThresholdSampleCount: overThresholdRows.length, + thresholdRatio: alertThresholds.sessionRailFallbackRatio, + maxFallbackRatio, + maxVisibleCount, + maxFallbackTitleCount, + }, + samples: majorityRows.slice(0, 80), + examples: Array.from(examplesByHash.values()).slice(0, 80), + timeline: rows.slice(-200), + valuesRedacted: true + }; +} + +function uniqueLoadingOwners(loadings) { + const groups = new Map(); + for (let index = 0; index < (Array.isArray(loadings) ? loadings : []).length; index += 1) { + const item = loadings[index]; + const ownerKey = loadingOwnerKey(item, index); + const ownerIdentity = loadingOwnerIdentity(item); + const existing = groups.get(ownerKey) || { + ownerKey, + ownerKind: item?.ownerKind ?? "unknown", + ownerLabel: loadingOwnerLabel(item, ownerKey), + ...ownerIdentity, + count: 0, + textHashes: [] + }; + existing.count += 1; + if (item?.textHash && !existing.textHashes.includes(item.textHash)) existing.textHashes.push(item.textHash); + for (const key of ["ownerSessionId", "ownerMessageId", "ownerTraceId"]) { + if (!existing[key] && ownerIdentity[key]) existing[key] = ownerIdentity[key]; + } + groups.set(ownerKey, existing); + } + return Array.from(groups.values()).sort((a, b) => b.count - a.count || String(a.ownerLabel).localeCompare(String(b.ownerLabel))); +} + +function loadingOwnerIdentity(item) { + const owner = item?.owner && typeof item.owner === "object" ? item.owner : {}; + return { + ownerSessionId: owner.sessionId ?? null, + ownerMessageId: owner.messageId ?? null, + ownerTraceId: owner.traceId ?? null, + }; +} + +function loadingOwnerKey(item, index = 0) { + const key = String(item?.ownerKey || "").trim(); + if (key) return key.slice(0, 240); + const owner = item?.owner && typeof item.owner === "object" ? item.owner : {}; + return [ + item?.ownerKind || "unknown", + owner.testId || item?.testId || owner.id || owner.role || owner.className || item?.role || item?.tag || "node", + owner.sessionId || owner.messageId || owner.traceId || item?.textHash || String(index) + ].filter(Boolean).join(":").slice(0, 240); +} + +function loadingOwnerLabel(item, fallback) { + return limitText(String(item?.ownerLabel || item?.owner?.ariaLabel || item?.owner?.testId || item?.owner?.className || fallback || "unknown"), 160); +} + +function buildLoadingMetrics(samples, timeline) { + const events = samples.map((sample, index) => { + const tsMs = Date.parse(sample?.ts); + const loadings = Array.isArray(sample?.loadings) ? sample.loadings : []; + const owners = uniqueLoadingOwners(loadings); + return { + seq: sample?.seq ?? null, + ts: sample?.ts ?? null, + tsMs, + promptIndex: timeline[index]?.promptIndex ?? 0, + routeSessionId: sample?.routeSessionId ?? null, + activeSessionId: sample?.activeSessionId ?? null, + loadingCount: loadings.length, + ownerCount: owners.length, + owners, + ownerKeys: owners.map((item) => item.ownerKey), + ownerLabels: owners.map((item) => item.ownerLabel).slice(0, 8) + }; + }).filter((item) => Number.isFinite(item.tsMs)); + const continuityThresholdMs = loadingContinuityThresholdMs(events); + const segments = buildLoadingSegments(events, continuityThresholdMs, (event) => event.loadingCount, (event) => event.owners) + .sort((a, b) => Number(b.durationSeconds ?? 0) - Number(a.durationSeconds ?? 0) || Number(b.maxCount ?? 0) - Number(a.maxCount ?? 0)); + const ownerMap = new Map(); + for (const event of events) { + for (const owner of event.owners) { + const existing = ownerMap.get(owner.ownerKey) || { + ownerKey: owner.ownerKey, + ownerKind: owner.ownerKind, + ownerLabel: owner.ownerLabel, + ownerSessionId: owner.ownerSessionId ?? null, + ownerMessageId: owner.ownerMessageId ?? null, + ownerTraceId: owner.ownerTraceId ?? null, + sampleCount: 0, + occurrenceCount: 0, + maxSimultaneousCount: 0, + firstAt: event.ts, + lastAt: event.ts, + firstSeq: event.seq, + lastSeq: event.seq, + promptIndexes: new Set(), + events: [] + }; + existing.sampleCount += 1; + existing.occurrenceCount += owner.count; + existing.maxSimultaneousCount = Math.max(existing.maxSimultaneousCount, owner.count); + existing.lastAt = event.ts; + existing.lastSeq = event.seq; + for (const key of ["ownerSessionId", "ownerMessageId", "ownerTraceId"]) { + if (!existing[key] && owner[key]) existing[key] = owner[key]; + } + if (Number.isFinite(Number(event.promptIndex))) existing.promptIndexes.add(Number(event.promptIndex)); + existing.events.push({ ...event, loadingCount: owner.count, owners: [owner] }); + ownerMap.set(owner.ownerKey, existing); + } + } + const owners = Array.from(ownerMap.values()).map((owner) => { + const ownerSegments = buildLoadingSegments(owner.events, continuityThresholdMs, (event) => event.loadingCount, (event) => event.owners); + const longest = ownerSegments.reduce((max, item) => Math.max(max, Number(item.durationSeconds ?? 0)), 0); + return { + ownerKey: owner.ownerKey, + ownerKind: owner.ownerKind, + ownerLabel: owner.ownerLabel, + ownerSessionId: owner.ownerSessionId ?? null, + ownerMessageId: owner.ownerMessageId ?? null, + ownerTraceId: owner.ownerTraceId ?? null, + sampleCount: owner.sampleCount, + occurrenceCount: owner.occurrenceCount, + maxSimultaneousCount: owner.maxSimultaneousCount, + longestContinuousSeconds: longest, + firstAt: owner.firstAt, + lastAt: owner.lastAt, + firstSeq: owner.firstSeq, + lastSeq: owner.lastSeq, + promptIndexes: Array.from(owner.promptIndexes).sort((a, b) => a - b), + segments: ownerSegments.sort((a, b) => Number(b.durationSeconds ?? 0) - Number(a.durationSeconds ?? 0)).slice(0, 8), + valuesRedacted: true + }; + }).sort((a, b) => Number(b.longestContinuousSeconds ?? 0) - Number(a.longestContinuousSeconds ?? 0) || Number(b.occurrenceCount ?? 0) - Number(a.occurrenceCount ?? 0)); + const latest = events[events.length - 1] || null; + const currentSegment = latest && latest.loadingCount > 0 + ? segments.find((segment) => segment.ongoing === true && segment.lastSeq === latest.seq) || null + : null; + const timelineRows = events + .filter((event, index) => event.loadingCount > 0 || (index > 0 && events[index - 1]?.loadingCount > 0)) + .slice(0, 500) + .map((event) => ({ + seq: event.seq, + ts: event.ts, + promptIndex: event.promptIndex, + loadingCount: event.loadingCount, + ownerCount: event.ownerCount, + owners: event.owners.map((owner) => ({ ownerKind: owner.ownerKind, ownerLabel: owner.ownerLabel, ownerSessionId: owner.ownerSessionId ?? null, ownerMessageId: owner.ownerMessageId ?? null, ownerTraceId: owner.ownerTraceId ?? null, count: owner.count })).slice(0, 8) + })); + return { + summary: { + sampleCount: events.length, + loadingSampleCount: events.filter((event) => event.loadingCount > 0).length, + maxSimultaneousCount: events.reduce((max, event) => Math.max(max, event.loadingCount), 0), + maxSimultaneousOwnerCount: events.reduce((max, event) => Math.max(max, event.ownerCount), 0), + concurrentLoadingSampleCount: events.filter((event) => event.loadingCount > 1).length, + ownerCount: owners.length, + segmentCount: segments.length, + overFiveSecondSegmentCount: segments.filter((segment) => Number(segment.durationSeconds ?? 0) > 5).length, + overBudgetSegmentCount: segments.filter((segment) => Number(segment.durationSeconds ?? 0) > alertThresholds.visibleLoadingSlowMs / 1000).length, + budgetSeconds: alertThresholds.visibleLoadingSlowMs / 1000, + longestContinuousSeconds: segments.length > 0 ? Number(segments[0].durationSeconds ?? 0) : 0, + currentContinuousSeconds: currentSegment ? Number(currentSegment.durationSeconds ?? 0) : 0, + continuityThresholdMs, + latestLoadingCount: latest?.loadingCount ?? 0, + latestOwnerCount: latest?.ownerCount ?? 0, + valuesRedacted: true + }, + segments: segments.slice(0, 80), + owners: owners.slice(0, 80), + timeline: timelineRows, + valuesRedacted: true + }; +} + +function loadingContinuityThresholdMs(events) { + const deltas = []; + for (let index = 1; index < events.length; index += 1) { + const delta = events[index].tsMs - events[index - 1].tsMs; + if (Number.isFinite(delta) && delta > 0) deltas.push(delta); + } + if (deltas.length === 0) return 5000; + const sorted = deltas.slice().sort((a, b) => a - b); + const median = sorted[Math.floor(sorted.length / 2)]; + return Math.min(15000, Math.max(1500, Math.round(median * 2.5))); +} + +function buildLoadingSegments(events, continuityThresholdMs, countForEvent, ownersForEvent) { + const segments = []; + let segment = null; + let previousTsMs = null; + for (const event of events) { + const count = Number(countForEvent(event) ?? 0); + const gapOk = previousTsMs === null || !Number.isFinite(event.tsMs) || event.tsMs - previousTsMs <= continuityThresholdMs; + if (count > 0) { + if (!segment || !gapOk) { + if (segment) segments.push(finalizeLoadingSegment(segment, null)); + segment = { + firstAt: event.ts, + lastAt: event.ts, + firstSeq: event.seq, + lastSeq: event.seq, + promptIndexes: new Set(), + ownerKeys: new Set(), + ownerLabels: new Map(), + sampleCount: 0, + maxCount: 0, + ongoing: true + }; + } + segment.lastAt = event.ts; + segment.lastSeq = event.seq; + segment.sampleCount += 1; + segment.maxCount = Math.max(segment.maxCount, count); + if (Number.isFinite(Number(event.promptIndex))) segment.promptIndexes.add(Number(event.promptIndex)); + for (const owner of ownersForEvent(event) || []) { + if (!owner?.ownerKey) continue; + segment.ownerKeys.add(owner.ownerKey); + if (!segment.ownerLabels.has(owner.ownerKey)) segment.ownerLabels.set(owner.ownerKey, { ownerKey: owner.ownerKey, ownerKind: owner.ownerKind, ownerLabel: owner.ownerLabel, count: 0 }); + const label = segment.ownerLabels.get(owner.ownerKey); + label.count += owner.count ?? 1; + } + } else if (segment) { + segment.ongoing = false; + segment.endedAt = event.ts; + segment.endSeq = event.seq; + segments.push(finalizeLoadingSegment(segment, event)); + segment = null; + } + previousTsMs = event.tsMs; + } + if (segment) segments.push(finalizeLoadingSegment(segment, null)); + return segments; +} + +function finalizeLoadingSegment(segment, absentEvent) { + const startMs = Date.parse(segment.firstAt || ""); + const lastMs = Date.parse(segment.lastAt || ""); + const absentMs = Date.parse(absentEvent?.ts || ""); + const durationSeconds = Number.isFinite(startMs) && Number.isFinite(lastMs) && lastMs >= startMs ? Number(((lastMs - startMs) / 1000).toFixed(3)) : 0; + const upperBoundSeconds = Number.isFinite(startMs) && Number.isFinite(absentMs) && absentMs >= startMs ? Number(((absentMs - startMs) / 1000).toFixed(3)) : durationSeconds; + const endedGapSeconds = Number.isFinite(lastMs) && Number.isFinite(absentMs) && absentMs >= lastMs ? Number(((absentMs - lastMs) / 1000).toFixed(3)) : null; + return { + firstAt: segment.firstAt, + lastAt: segment.lastAt, + endedAt: absentEvent?.ts ?? null, + firstSeq: segment.firstSeq, + lastSeq: segment.lastSeq, + endSeq: absentEvent?.seq ?? null, + durationSeconds, + upperBoundSeconds, + endedGapSeconds, + sampleCount: segment.sampleCount, + maxCount: segment.maxCount, + ownerCount: segment.ownerKeys.size, + owners: Array.from(segment.ownerLabels.values()).sort((a, b) => b.count - a.count || String(a.ownerLabel).localeCompare(String(b.ownerLabel))).slice(0, 12), + promptIndexes: Array.from(segment.promptIndexes).sort((a, b) => a - b), + ongoing: absentEvent ? false : segment.ongoing === true, + valuesRedacted: true + }; +} + +function boundedTurnTimingRowsForReport(rows) { + const sourceRows = Array.isArray(rows) ? rows : []; + const maxRows = 1200; + const headRows = 120; + if (sourceRows.length <= maxRows) return { rows: sourceRows, disclosure: { truncated: false, totalRows: sourceRows.length, includedRows: sourceRows.length, omittedRows: 0, headRows: sourceRows.length, tailRows: 0 } }; + const tailRows = Math.max(0, maxRows - headRows); + return { + rows: [...sourceRows.slice(0, headRows), ...sourceRows.slice(-tailRows)], + disclosure: { + truncated: true, + totalRows: sourceRows.length, + includedRows: maxRows, + omittedRows: Math.max(0, sourceRows.length - maxRows), + headRows, + tailRows, + policy: "report-bounded-head-tail; full anomaly counters are computed before truncation", + valuesRedacted: true + } + }; +} + +function buildTurnTimingTable(samples, timeline) { + const columns = []; + const registry = new Map(); + const promptAssignmentByKey = new Map(); + const rows = []; + for (let index = 0; index < samples.length; index += 1) { + const sample = samples[index]; + const timelineItem = timeline[index] || {}; + const cells = {}; + const rawMetrics = turnMetricItems(sample, timelineItem); + const domIndexes = rawMetrics.map((item) => Number(item.domIndex)).filter(Number.isFinite); + const maxDomIndex = domIndexes.length > 0 ? Math.max(...domIndexes) : null; + for (const rawMetric of rawMetrics) { + const scopedKey = turnTimingScopedMetricKey(rawMetric, sample); + const samplePromptIndex = Number(timelineItem.promptIndex ?? 0); + const evidencePromptIndex = inferTurnMetricPromptIndex(rawMetric, samplePromptIndex, maxDomIndex); + if (evidencePromptIndex !== null) { + const existingPromptIndex = promptAssignmentByKey.get(scopedKey); + if (existingPromptIndex === undefined || evidencePromptIndex > existingPromptIndex) promptAssignmentByKey.set(scopedKey, evidencePromptIndex); + } + const assignedPromptIndex = promptAssignmentByKey.get(scopedKey) ?? null; + const metric = { ...rawMetric, key: scopedKey, baseKey: rawMetric.key, promptIndex: assignedPromptIndex, samplePromptIndex, pageEpoch: Number(sample?.pageEpoch ?? rawMetric.pageEpoch ?? 0) || 0 }; + let column = registry.get(metric.key); + if (!column) { + column = { + id: "T" + String(columns.length + 1), + label: "T" + String(columns.length + 1), + keyHash: sha256(metric.key), + source: metric.source, + firstSeq: sample.seq ?? null, + firstTs: sample.ts ?? null, + lastSeq: sample.seq ?? null, + lastTs: sample.ts ?? null, + promptIndex: metric.promptIndex ?? null, + lastPromptIndex: metric.promptIndex ?? null, + traceId: metric.traceId ?? null, + messageId: metric.messageId ?? null, + domIndex: metric.domIndex ?? null, + pageRole: metric.pageRole ?? sample.pageRole ?? null, + pageId: metric.pageId ?? sample.pageId ?? null, + pageEpoch: metric.pageEpoch ?? null + }; + registry.set(metric.key, column); + columns.push(column); + } else { + column.lastSeq = sample.seq ?? null; + column.lastTs = sample.ts ?? null; + if (column.source !== "turn" && metric.source === "turn") column.source = "turn"; + if (metric.promptIndex && column.promptIndex !== metric.promptIndex) column.promptIndex = metric.promptIndex; + column.lastPromptIndex = metric.promptIndex ?? column.lastPromptIndex ?? null; + if (!column.traceId && metric.traceId) column.traceId = metric.traceId; + if (!column.messageId && metric.messageId) column.messageId = metric.messageId; + } + cells[column.id] = { + totalElapsedSeconds: metric.totalElapsedSeconds, + recentUpdateSeconds: metric.recentUpdateSeconds, + status: metric.status ?? null, + promptIndex: metric.promptIndex ?? null, + source: metric.source, + pageRole: metric.pageRole ?? sample.pageRole ?? null, + pageId: metric.pageId ?? sample.pageId ?? null, + pageEpoch: metric.pageEpoch ?? null, + sampleGroupSeq: sample.sampleGroupSeq ?? null, + traceId: metric.traceId ?? null, + messageId: metric.messageId ?? null, + textHash: metric.textHash ?? null, + samplePromptIndex: metric.samplePromptIndex ?? null + }; + } + rows.push({ + ts: sample.ts ?? null, + seq: sample.seq ?? null, + sampleGroupSeq: sample.sampleGroupSeq ?? null, + pageRole: sample.pageRole ?? null, + pageId: sample.pageId ?? null, + pageEpoch: Number(sample?.pageEpoch ?? 0) || 0, + promptIndex: timelineItem.promptIndex ?? 0, + routeSessionId: sample.routeSessionId ?? null, + activeSessionId: sample.activeSessionId ?? null, + cells + }); + } + const timingEvents = detectTurnTimingNonMonotonic(columns, rows); + return { + columns, + rows, + nonMonotonic: timingEvents.anomalies, + elapsedZeroResets: timingEvents.elapsedZeroResets, + totalElapsedForwardJumps: timingEvents.totalElapsedForwardJumps, + terminalElapsedGrowth: timingEvents.terminalElapsedGrowth, + recentUpdateResets: timingEvents.recentUpdateResets, + recentUpdateSteps: timingEvents.recentUpdateSteps + }; +} + +function turnTimingScopedMetricKey(metric, sample) { + return [ + metric?.key ?? "unknown", + metric?.pageRole ?? sample?.pageRole ?? "unknown-role", + metric?.pageId ?? sample?.pageId ?? "unknown-page", + Number(sample?.pageEpoch ?? metric?.pageEpoch ?? 0) || 0 + ].join("|page:"); +} + +function inferTurnMetricPromptIndex(metric, samplePromptIndex, maxDomIndex) { + const promptIndex = Number(samplePromptIndex); + if (!Number.isFinite(promptIndex) || promptIndex <= 0) return null; + if (metric?.source === "aggregate") return promptIndex; + if (isActiveTurnStatus(metric?.status)) return promptIndex; + const domIndex = Number(metric?.domIndex); + if (!isTerminalTurnStatus(metric?.status) && Number.isFinite(domIndex) && Number.isFinite(maxDomIndex) && domIndex === maxDomIndex && metric?.recentUpdateSeconds !== null && metric?.recentUpdateSeconds !== undefined) return promptIndex; + return null; +} + +function detectTurnTimingNonMonotonic(columns, rows) { + const anomalies = []; + const elapsedZeroResets = []; + const totalElapsedForwardJumps = []; + const terminalElapsedGrowth = []; + const 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 ?? null, + samplePromptIndex: row.promptIndex ?? null, + source: cell.source ?? column.source ?? null, + pageRole: cell.pageRole ?? column.pageRole ?? null, + pageId: cell.pageId ?? column.pageId ?? null, + pageEpoch: cell.pageEpoch ?? row.pageEpoch ?? column.pageEpoch ?? null, + valuesRedacted: true + }; + anomalies.push(event); + if (anomaly === "zero-reset") elapsedZeroResets.push(event); + } + if (previous && metric === "totalElapsedSeconds" && current > previous.value) { + const sampleDeltaSeconds = elapsedSecondsBetween(previous.ts, row.ts); + const delta = current - previous.value; + const allowedIncreaseSeconds = sampleDeltaSeconds + alertThresholds.turnTimingSampleSlackSeconds; + const terminalTransition = !isTerminalTurnStatus(previous.status) && isTerminalTurnStatus(cell.status); + if (delta > allowedIncreaseSeconds && !terminalTransition) { + totalElapsedForwardJumps.push({ + columnId: column.id, + columnLabel: column.label, + metric, + anomaly: "forward-jump", + expectedPattern: "total-elapsed-increase-should-match-browser-sample-interval", + fromSeq: previous.seq, + fromTs: previous.ts, + fromValue: previous.value, + fromStatus: previous.status ?? null, + toSeq: row.seq ?? null, + toTs: row.ts ?? null, + toValue: current, + toStatus: cell.status ?? null, + delta, + sampleDeltaSeconds, + allowedIncreaseSeconds, + excessiveIncreaseSeconds: Number((delta - allowedIncreaseSeconds).toFixed(3)), + traceId: cell.traceId ?? column.traceId ?? null, + messageId: cell.messageId ?? column.messageId ?? null, + promptIndex: cell.promptIndex ?? null, + samplePromptIndex: row.promptIndex ?? null, + source: cell.source ?? column.source ?? null, + pageRole: cell.pageRole ?? column.pageRole ?? null, + pageId: cell.pageId ?? column.pageId ?? null, + pageEpoch: cell.pageEpoch ?? row.pageEpoch ?? column.pageEpoch ?? null, + valuesRedacted: true + }); + } + } + if (metric === "totalElapsedSeconds" && isTerminalTurnStatus(cell.status)) { + if (previousTerminalTotal && current > previousTerminalTotal.value) { + terminalElapsedGrowth.push({ + columnId: column.id, + columnLabel: column.label, + metric, + anomaly: "terminal-growth", + expectedPattern: "terminal-total-elapsed-sealed", + fromSeq: previousTerminalTotal.seq, + fromTs: previousTerminalTotal.ts, + fromValue: previousTerminalTotal.value, + fromStatus: previousTerminalTotal.status, + toSeq: row.seq ?? null, + toTs: row.ts ?? null, + toValue: current, + toStatus: cell.status ?? null, + delta: current - previousTerminalTotal.value, + traceId: cell.traceId ?? column.traceId ?? null, + messageId: cell.messageId ?? column.messageId ?? null, + promptIndex: cell.promptIndex ?? null, + samplePromptIndex: row.promptIndex ?? null, + source: cell.source ?? column.source ?? null, + pageRole: cell.pageRole ?? column.pageRole ?? null, + pageId: cell.pageId ?? column.pageId ?? null, + pageEpoch: cell.pageEpoch ?? row.pageEpoch ?? column.pageEpoch ?? null, + valuesRedacted: true + }); + } + previousTerminalTotal = { value: current, seq: row.seq ?? null, ts: row.ts ?? null, status: cell.status ?? null }; + } + if (previous && metric === "recentUpdateSeconds") { + const elapsedMs = Date.parse(String(row.ts ?? "")) - Date.parse(String(previous.ts ?? "")); + const elapsedSeconds = Number.isFinite(elapsedMs) && elapsedMs >= 0 ? elapsedMs / 1000 : null; + const increase = current - previous.value; + const allowedIncrease = elapsedSeconds === null + ? alertThresholds.turnTimingSampleSlackSeconds + : Math.max(alertThresholds.turnTimingSampleSlackSeconds, elapsedSeconds + alertThresholds.turnTimingSampleSlackSeconds); + const excessiveIncrease = increase > allowedIncrease ? increase - allowedIncrease : 0; + recentUpdateSteps.push({ + columnId: column.id, + columnLabel: column.label, + metric: "recentUpdateSeconds", + event: increase < 0 ? "reset" : excessiveIncrease > 0 ? "jump" : "increase", + expectedPattern: "sawtooth-increase-or-reset", + fromSeq: previous.seq, + fromTs: previous.ts, + fromValue: previous.value, + toSeq: row.seq ?? null, + toTs: row.ts ?? null, + toValue: current, + delta: increase, + sampleDeltaSeconds: elapsedSeconds, + allowedIncreaseSeconds: allowedIncrease, + excessiveIncreaseSeconds: excessiveIncrease, + traceId: cell.traceId ?? column.traceId ?? null, + messageId: cell.messageId ?? column.messageId ?? null, + promptIndex: cell.promptIndex ?? null, + samplePromptIndex: row.promptIndex ?? null, + source: cell.source ?? column.source ?? null, + pageRole: cell.pageRole ?? column.pageRole ?? null, + pageId: cell.pageId ?? column.pageId ?? null, + pageEpoch: cell.pageEpoch ?? row.pageEpoch ?? column.pageEpoch ?? null, + valuesRedacted: true + }); + if (increase < 0) { + recentUpdateResets.push({ + columnId: column.id, + columnLabel: column.label, + metric: "recentUpdateSeconds", + event: "reset", + fromSeq: previous.seq, + fromTs: previous.ts, + fromValue: previous.value, + toSeq: row.seq ?? null, + toTs: row.ts ?? null, + toValue: current, + delta: increase, + sampleDeltaSeconds: elapsedSeconds, + traceId: cell.traceId ?? column.traceId ?? null, + messageId: cell.messageId ?? column.messageId ?? null, + promptIndex: cell.promptIndex ?? null, + samplePromptIndex: row.promptIndex ?? null, + source: cell.source ?? column.source ?? null, + pageRole: cell.pageRole ?? column.pageRole ?? null, + pageId: cell.pageId ?? column.pageId ?? null, + pageEpoch: cell.pageEpoch ?? row.pageEpoch ?? column.pageEpoch ?? null, + valuesRedacted: true + }); + } + if (excessiveIncrease > 0) { + anomalies.push({ + columnId: column.id, + columnLabel: column.label, + metric: "recentUpdateSeconds", + anomaly: "jump", + expectedPattern: "sawtooth-increase-or-reset", + fromSeq: previous.seq, + fromTs: previous.ts, + fromValue: previous.value, + toSeq: row.seq ?? null, + toTs: row.ts ?? null, + toValue: current, + delta: increase, + sampleDeltaSeconds: elapsedSeconds, + allowedIncreaseSeconds: allowedIncrease, + excessiveIncreaseSeconds: excessiveIncrease, + traceId: cell.traceId ?? column.traceId ?? null, + messageId: cell.messageId ?? column.messageId ?? null, + promptIndex: cell.promptIndex ?? null, + samplePromptIndex: row.promptIndex ?? null, + source: cell.source ?? column.source ?? null, + pageRole: cell.pageRole ?? column.pageRole ?? null, + pageId: cell.pageId ?? column.pageId ?? null, + pageEpoch: cell.pageEpoch ?? row.pageEpoch ?? column.pageEpoch ?? null, + valuesRedacted: true + }); + } + } + previousByMetric.set(metric, { value: current, seq: row.seq ?? null, ts: row.ts ?? null, status: cell.status ?? null }); + } + } + } + return { anomalies, elapsedZeroResets, totalElapsedForwardJumps, terminalElapsedGrowth, recentUpdateResets, recentUpdateSteps }; +} + +function elapsedSecondsBetween(fromTs, toTs) { + const from = Date.parse(fromTs); + const to = Date.parse(toTs); + if (!Number.isFinite(from) || !Number.isFinite(to) || to < from) return 0; + return Number(((to - from) / 1000).toFixed(3)); +} + +function maxPositiveDelta(items) { + const values = (Array.isArray(items) ? items : []) + .map((item) => Number(item.delta)) + .filter((value) => Number.isFinite(value) && value > 0); + return values.length > 0 ? Math.max(...values) : 0; +} + +function isTerminalTurnStatus(value) { + const status = String(value ?? "").trim().toLowerCase().replace(/_/gu, "-"); + return ["completed", "succeeded", "success", "failed", "error", "blocked", "timeout", "canceled", "cancelled", "stale", "done", "terminal", "thread-resume-failed"].includes(status); +} + + + +function buildTraceOrderMetrics(samples, timeline) { + const rows = []; + const orderAnomalies = []; + const completionNotLast = []; + const groups = new Map(); + for (let sampleIndex = 0; sampleIndex < samples.length; sampleIndex += 1) { + const sample = samples[sampleIndex] || {}; + const sampleRows = traceTimingRowsForSample(sample, timeline[sampleIndex] || {}); + for (const row of sampleRows) { + const normalized = { + ...row, + sampleIndex, + sampleSeq: sample.seq ?? null, + timestamp: sample.ts || sample.timestamp || sample.collectedAt || sample.time || null, + pageRole: row.pageRole || sample.pageRole || sample.role || sample.contextRole || null, + pageId: row.pageId || sample.pageId || sample.contextId || null, + sessionId: row.sessionId || sample.sessionId || sample.workbenchSessionId || null, + }; + rows.push(normalized); + const key = traceRowGroupKey(normalized); + if (!groups.has(key)) groups.set(key, []); + groups.get(key).push(normalized); + } + } + const slackSeconds = Math.max(1, Number(alertThresholds?.turnTimingSampleSlackSeconds || 0)); + for (const groupRows of groups.values()) { + const sorted = groupRows.slice().sort((a, b) => { + if (a.sampleIndex !== b.sampleIndex) return a.sampleIndex - b.sampleIndex; + return (a.rowIndex ?? 0) - (b.rowIndex ?? 0); + }); + let previous = null; + for (const row of sorted) { + if (previous) { + const reasons = []; + if (Number.isFinite(previous.totalSeconds) && Number.isFinite(row.totalSeconds) && row.totalSeconds + slackSeconds < previous.totalSeconds) { + reasons.push('total-decreased'); + } + if (Number.isFinite(previous.projectedSeq) && Number.isFinite(row.projectedSeq) && row.projectedSeq < previous.projectedSeq) { + reasons.push('projected-seq-decreased'); + } + if (Number.isFinite(previous.clockSeconds) && Number.isFinite(row.clockSeconds)) { + const diff = previous.clockSeconds - row.clockSeconds; + if (diff > slackSeconds && diff < 43200) reasons.push('clock-decreased'); + } + if (Number.isFinite(previous.timestampMs) && Number.isFinite(row.timestampMs) && row.timestampMs + slackSeconds * 1000 < previous.timestampMs) { + reasons.push('timestamp-decreased'); + } + if (reasons.length) { + orderAnomalies.push({ + sampleIndex: row.sampleIndex, + sampleSeq: row.sampleSeq, + timestamp: row.timestamp, + pageRole: row.pageRole, + pageId: row.pageId, + sessionId: row.sessionId, + traceId: row.traceId || previous.traceId || null, + previousRowIndex: previous.rowIndex, + currentRowIndex: row.rowIndex, + reasons, + previousTotalSeconds: previous.totalSeconds, + currentTotalSeconds: row.totalSeconds, + previousProjectedSeq: previous.projectedSeq, + currentProjectedSeq: row.projectedSeq, + previousSourceSeq: previous.sourceSeq, + currentSourceSeq: row.sourceSeq, + previousEventSeq: previous.eventSeq, + currentEventSeq: row.eventSeq, + previousClockSeconds: previous.clockSeconds, + currentClockSeconds: row.clockSeconds, + previousTimestampMs: previous.timestampMs, + currentTimestampMs: row.timestampMs, + previousPreview: previous.preview, + currentPreview: row.preview, + }); + } + } + previous = row; + } + for (let index = 0; index < sorted.length; index += 1) { + const row = sorted[index]; + if (!row.isCompletion) continue; + const later = sorted.slice(index + 1).find((candidate) => { + if (candidate.isCompletion && candidate.preview === row.preview) return false; + return Number.isFinite(candidate.totalSeconds) || Number.isFinite(candidate.clockSeconds) || Number.isFinite(candidate.projectedSeq); + }); + if (later) { + completionNotLast.push({ + sampleIndex: row.sampleIndex, + sampleSeq: row.sampleSeq, + timestamp: row.timestamp, + pageRole: row.pageRole, + pageId: row.pageId, + sessionId: row.sessionId, + traceId: row.traceId || later.traceId || null, + completionRowIndex: row.rowIndex, + laterRowIndex: later.rowIndex, + completionTotalSeconds: row.totalSeconds, + laterTotalSeconds: later.totalSeconds, + completionProjectedSeq: row.projectedSeq, + laterProjectedSeq: later.projectedSeq, + completionSourceSeq: row.sourceSeq, + laterSourceSeq: later.sourceSeq, + completionEventSeq: row.eventSeq, + laterEventSeq: later.eventSeq, + completionPreview: row.preview, + laterPreview: later.preview, + }); + } + } + } + return { + summary: { + sampleCount: samples.length, + traceRowCount: rows.length, + orderAnomalyCount: orderAnomalies.length, + completionNotLastCount: completionNotLast.length, + }, + rows: rows.slice(0, 200), + orderAnomalies: orderAnomalies.slice(0, 100), + completionNotLast: completionNotLast.slice(0, 100), + }; +} + +function buildCodeAgentCardDurationUnderreportedMetrics(samples, timeline) { + const findings = []; + const slackSeconds = Math.max(5, Number(alertThresholds?.turnTimingSampleSlackSeconds || 0)); + for (let sampleIndex = 0; sampleIndex < samples.length; sampleIndex += 1) { + const sample = samples[sampleIndex] || {}; + const cards = codeAgentCardsForSample(sample); + if (!cards.length) continue; + const traceRows = traceTimingRowsForSample(sample, timeline[sampleIndex] || {}); + const terminalCards = cards.filter((card) => isCodeAgentCardTerminal(card)); + const sampleText = sampleVisibleText(sample, timeline[sampleIndex] || {}); + for (const card of terminalCards) { + const cardText = codeAgentCardText(card); + const parsedCardSeconds = parseTotalElapsedSeconds(cardText).filter(Number.isFinite); + const cardSeconds = Number.isFinite(Number(card.totalElapsedSeconds)) ? Number(card.totalElapsedSeconds) : parsedCardSeconds.length > 0 ? Math.max(...parsedCardSeconds) : NaN; + if (!Number.isFinite(cardSeconds)) continue; + const traceMatched = traceRows.filter((row) => traceRowMatchesCard(row, card, terminalCards.length)); + const traceEvidence = maxTraceDurationEvidence(traceMatched); + const textEvidence = maxSelfReportedDurationEvidence([card.text, card.preview, card.finalResponseText, card.runningRecordText, terminalCards.length === 1 ? sampleText : ''].filter(Boolean).join('\n')); + const evidences = [traceEvidence, textEvidence].filter((item) => item && item.exact === true && Number.isFinite(item.seconds)); + if (!evidences.length) continue; + const strongest = evidences.sort((a, b) => b.seconds - a.seconds)[0]; + const tolerance = Math.max(slackSeconds, Math.ceil(strongest.seconds * 0.05)); + if (strongest.seconds > cardSeconds + tolerance) { + findings.push({ + sampleIndex, + timestamp: sample.timestamp || sample.collectedAt || sample.time || null, + pageRole: card.pageRole || sample.pageRole || sample.role || sample.contextRole || null, + pageId: card.pageId || sample.pageId || sample.contextId || null, + sessionId: card.sessionId || sample.sessionId || sample.workbenchSessionId || null, + traceId: card.traceId || strongest.traceId || null, + status: card.status || card.state || card.phase || null, + cardTotalElapsedSeconds: cardSeconds, + expectedElapsedSeconds: strongest.seconds, + deltaSeconds: strongest.seconds - cardSeconds, + toleranceSeconds: tolerance, + evidenceKind: strongest.kind, + evidencePreview: strongest.preview, + cardPreview: card.preview || compactOneLine(cardText || ''), + }); + } + } + } + return findings.slice(0, 100); +} + +function buildCodeAgentCardDurationMismatchMetrics(samples, timeline) { + const findings = []; + const slackSeconds = Math.max(5, Number(alertThresholds?.turnTimingSampleSlackSeconds || 0)); + for (let sampleIndex = 0; sampleIndex < samples.length; sampleIndex += 1) { + const sample = samples[sampleIndex] || {}; + const cards = codeAgentCardsForSample(sample); + if (!cards.length) continue; + const traceRows = traceTimingRowsForSample(sample, timeline[sampleIndex] || {}); + const terminalCards = cards.filter((card) => isCodeAgentCardTerminal(card)); + const sampleText = sampleVisibleText(sample, timeline[sampleIndex] || {}); + for (const card of terminalCards) { + const cardText = codeAgentCardText(card); + const parsedCardSeconds = parseTotalElapsedSeconds(cardText).filter(Number.isFinite); + const cardSeconds = Number.isFinite(Number(card.totalElapsedSeconds)) ? Number(card.totalElapsedSeconds) : parsedCardSeconds.length > 0 ? Math.max(...parsedCardSeconds) : NaN; + if (!Number.isFinite(cardSeconds)) continue; + const traceMatched = traceRows.filter((row) => traceRowMatchesCard(row, card, terminalCards.length)); + const traceEvidence = maxTraceDurationEvidence(traceMatched); + const textEvidence = maxSelfReportedDurationEvidence([card.text, card.preview, card.finalResponseText, card.runningRecordText, terminalCards.length === 1 ? sampleText : ''].filter(Boolean).join('\n')); + const evidences = [traceEvidence, textEvidence].filter((item) => item && item.exact === true && Number.isFinite(item.seconds)); + if (!evidences.length) continue; + const strongest = evidences.sort((a, b) => b.seconds - a.seconds)[0]; + const exactEvidence = strongest.exact === true || strongest.kind === 'trace-completion-total' || strongest.kind === 'final-response-duration'; + const tolerance = Math.max(slackSeconds, Math.ceil(strongest.seconds * 0.05)); + const signedDelta = Number((cardSeconds - strongest.seconds).toFixed(3)); + const absoluteDelta = Math.abs(signedDelta); + const underreported = strongest.seconds > cardSeconds + tolerance; + const overreported = exactEvidence && cardSeconds > strongest.seconds + tolerance; + if (!underreported && !overreported) continue; + findings.push({ + sampleIndex, + timestamp: sample.timestamp || sample.collectedAt || sample.time || sample.ts || null, + pageRole: card.pageRole || sample.pageRole || sample.role || sample.contextRole || null, + pageId: card.pageId || sample.pageId || sample.contextId || null, + sessionId: card.sessionId || sample.sessionId || sample.workbenchSessionId || null, + traceId: card.traceId || strongest.traceId || null, + status: card.status || card.state || card.phase || null, + direction: underreported ? 'underreported' : 'overreported', + cardTotalElapsedSeconds: cardSeconds, + expectedElapsedSeconds: strongest.seconds, + signedDeltaSeconds: signedDelta, + deltaSeconds: Number(absoluteDelta.toFixed(3)), + toleranceSeconds: tolerance, + evidenceKind: strongest.kind, + exactEvidence, + evidencePreview: strongest.preview, + cardPreview: card.preview || compactOneLine(cardText || ''), + }); + } + } + return findings.slice(0, 100); +} + +function traceTimingRowsForSample(sample, timelineItem) { + const rows = []; + const seen = new Set(); + const appendRowsFromText = (text, source, baseIndex, meta = {}) => { + for (const extracted of extractTraceRowsFromText(text, source, baseIndex, meta)) { + const key = [extracted.pageRole || '', extracted.pageId || '', extracted.rowIndex, extracted.preview].join('|'); + if (seen.has(key)) continue; + seen.add(key); + rows.push(extracted); + } + }; + for (const candidate of traceRowCandidateArrays(sample, timelineItem)) { + const array = Array.isArray(candidate.rows) ? candidate.rows : []; + array.forEach((item, index) => { + if (typeof item === 'string') { + appendRowsFromText(item, candidate.source, index, candidate.meta || {}); + return; + } + if (!item || typeof item !== 'object') return; + const text = objectText(item); + if (!text) return; + appendRowsFromText(text, candidate.source, Number.isFinite(Number(item.index)) ? Number(item.index) : index, { + ...(candidate.meta || {}), + pageRole: item.pageRole || item.role || candidate.meta?.pageRole || null, + pageId: item.pageId || item.contextId || candidate.meta?.pageId || null, + sessionId: item.sessionId || item.workbenchSessionId || candidate.meta?.sessionId || null, + traceId: item.traceId || item.trace_id || candidate.meta?.traceId || null, + projectedSeq: item.projectedSeq ?? item.projected_seq ?? item.projectedSequence ?? null, + sourceSeq: item.sourceSeq ?? item.source_seq ?? item.sourceSequence ?? null, + eventSeq: item.eventSeq ?? item.event_seq ?? item.sequence ?? null, + eventTimestamp: item.eventTimestamp ?? item.event_ts ?? item.timestamp ?? item.ts ?? null, + eventTimeText: item.eventTimeText ?? item.timeText ?? null, + eventKind: item.eventKind ?? item.kind ?? item.status ?? null, + }); + }); + } + if (!rows.length) { + appendRowsFromText(sampleVisibleText(sample, timelineItem), 'visible-text', 0, { + pageRole: sample.pageRole || sample.role || sample.contextRole || null, + pageId: sample.pageId || sample.contextId || null, + sessionId: sample.sessionId || sample.workbenchSessionId || null, + }); + } + rows.sort((a, b) => (a.rowIndex ?? 0) - (b.rowIndex ?? 0)); + return rows; +} + +function extractTraceRowsFromText(text, source, baseIndex, meta = {}) { + const result = []; + const normalized = String(text || '').replace(/\r/g, '\n'); + if (!normalized.trim()) return result; + const lines = normalized.split('\n').map((line) => line.trim()).filter(Boolean); + for (let index = 0; index < lines.length; index += 1) { + const line = lines[index]; + if (!traceLineLooksRelevant(line)) continue; + const nextLine = index + 1 < lines.length && !/^\d{1,2}:\d{2}:\d{2}\b/.test(lines[index + 1]) ? lines[index + 1] : ''; + const preview = compactOneLine(nextLine ? line + ' ' + nextLine : line); + result.push(normalizeTraceTimingRow(preview, source, Number(baseIndex || 0) * 1000 + index, meta)); + } + return result; +} + +function normalizeTraceTimingRow(text, source, rowIndex, meta = {}) { + const preview = compactOneLine(text).slice(0, 240); + const projectedSeq = firstFiniteNumber(meta.projectedSeq, parseTraceRowProjectedSeq(preview)); + const sourceSeq = firstFiniteNumber(meta.sourceSeq); + const eventSeq = firstFiniteNumber(meta.eventSeq); + const timestampMs = parseTraceRowTimestampMs(meta.eventTimestamp || meta.eventTimeText || preview); + return { + source, + rowIndex, + preview, + pageRole: meta.pageRole || null, + pageId: meta.pageId || null, + sessionId: meta.sessionId || null, + traceId: meta.traceId || parseTraceRowTraceId(preview), + clockSeconds: parseTraceRowClockSeconds(preview) ?? parseTraceRowClockSeconds(meta.eventTimeText || ""), + timestampMs, + totalSeconds: parseTraceRowTotalSeconds(preview), + projectedSeq, + sourceSeq, + eventSeq, + eventTimestamp: meta.eventTimestamp || null, + eventTimeText: meta.eventTimeText || null, + eventKind: meta.eventKind || null, + isCompletion: traceRowIsTerminalCompletionText(preview, meta.eventKind || ""), + }; +} + +function traceRowIsTerminalCompletionText(preview, eventKind = "") { + const value = [preview, eventKind].map((item) => String(item || "")).join(" "); + if (/\bnon[-_ ]?terminal\b/i.test(value)) return false; + if (/轮次完成|turn\s+completed|completed\s+turn|backend[_: -]?turn[_: -]?finished/i.test(value)) return true; + return /\bterminal(?:[_: -]?status|Status)?\s*[=: -]\s*(?:completed|failed|cancelled|canceled|timeout)\b/i.test(value); +} + +function traceRowIsRoundCompletionText(preview, eventKind = "") { + const value = [preview, eventKind].map((item) => String(item || "")).join(" "); + if (/\bnon[-_ ]?terminal\b/i.test(value)) return false; + return /轮次完成|turn\s+completed|completed\s+turn|backend[_: -]?turn[_: -]?finished/i.test(value); +} + +function traceRowCandidateArrays(sample, timelineItem) { + const candidates = []; + const pushArray = (rows, source, meta = {}) => { + if (Array.isArray(rows) && rows.length) candidates.push({ rows, source, meta }); + }; + const directSources = [ + [sample?.traceRows, 'sample.traceRows'], + [sample?.eventRows, 'sample.eventRows'], + [sample?.activityRows, 'sample.activityRows'], + [sample?.timelineRows, 'sample.timelineRows'], + [sample?.dom?.traceRows, 'sample.dom.traceRows'], + [sample?.dom?.eventRows, 'sample.dom.eventRows'], + [sample?.dom?.activityRows, 'sample.dom.activityRows'], + [sample?.dom?.timelineRows, 'sample.dom.timelineRows'], + [timelineItem?.traceRows, 'timeline.traceRows'], + [timelineItem?.eventRows, 'timeline.eventRows'], + [timelineItem?.activityRows, 'timeline.activityRows'], + [timelineItem?.rows, 'timeline.rows'], + [timelineItem?.events, 'timeline.events'], + ]; + for (const [rows, source] of directSources) pushArray(rows, source, {}); + if (!candidates.length) collectNamedTraceArrays(sample, candidates, 'sample', 0); + if (!candidates.length) collectNamedTraceArrays(timelineItem, candidates, 'timeline', 0); + return candidates; +} + +function collectNamedTraceArrays(value, candidates, path, depth) { + if (!value || depth > 5) return; + if (Array.isArray(value)) { + const pathLooksTrace = /trace|timeline|activity|event|log|record/i.test(path); + const valueLooksTrace = value.slice(0, 5).some((item) => traceLineLooksRelevant(typeof item === 'string' ? item : objectText(item))); + if (pathLooksTrace || valueLooksTrace) candidates.push({ rows: value, source: path, meta: {} }); + return; + } + if (typeof value !== 'object') return; + for (const [key, child] of Object.entries(value)) { + if (!child) continue; + if (Array.isArray(child)) { + const childPath = path + '.' + key; + const pathLooksTrace = /trace|timeline|activity|event|log|record/i.test(key); + const valueLooksTrace = child.slice(0, 5).some((item) => traceLineLooksRelevant(typeof item === 'string' ? item : objectText(item))); + if (pathLooksTrace || valueLooksTrace) candidates.push({ rows: child, source: childPath, meta: {} }); + continue; + } + if (typeof child === 'object' && /dom|trace|timeline|activity|event|log|record|page|card|message|panel|diagnostic/i.test(key)) { + collectNamedTraceArrays(child, candidates, path + '.' + key, depth + 1); + } + } +} + +function traceLineLooksRelevant(text) { + const value = String(text || '').trim(); + if (!value) return false; + if (/^\d{1,2}:\d{2}:\d{2}\b/.test(value)) return true; + if (/\btotal=\d/.test(value)) return true; + if (/轮次完成(总耗时/.test(value)) return true; + if (/\bseq(?:uence)?[=:]\s*\d+/i.test(value)) return true; + return false; +} + +function parseTraceRowClockSeconds(text) { + const match = String(text || '').match(/^\s*(\d{1,2}):(\d{2}):(\d{2})\b/); + if (!match) return null; + return Number(match[1]) * 3600 + Number(match[2]) * 60 + Number(match[3]); +} + +function parseTraceRowTotalSeconds(text) { + const value = String(text || ''); + const totalMatch = value.match(/\btotal=([0-9:.]+)/i); + if (totalMatch) return parseTraceDurationSeconds(totalMatch[1]); + const completionMatch = value.match(/总耗时\s*([0-9:.]+)/); + if (completionMatch) return parseTraceDurationSeconds(completionMatch[1]); + return null; +} + +function parseTraceDurationSeconds(value) { + const text = String(value || '').trim(); + if (!text) return null; + const parts = text.split(':').map((part) => Number(part)); + if (parts.some((part) => !Number.isFinite(part))) return null; + if (parts.length === 3) return parts[0] * 3600 + parts[1] * 60 + parts[2]; + if (parts.length === 2) return parts[0] * 60 + parts[1]; + if (parts.length === 1) return parts[0]; + return null; +} + +function parseTraceRowProjectedSeq(text) { + const value = String(text || ''); + const match = value.match(/\b(?:projected[_-]?seq|seq(?:uence)?|event[_-]?seq)\s*[=:]\s*(\d+)/i); + return match ? Number(match[1]) : null; +} + +function parseTraceRowTimestampMs(value) { + const text = String(value || '').trim(); + if (!text) return null; + const parsed = Date.parse(text); + return Number.isFinite(parsed) ? parsed : null; +} + +function firstFiniteNumber(...values) { + for (const value of values) { + const numeric = Number(value); + if (Number.isFinite(numeric)) return numeric; + } + return null; +} + +function parseTraceRowTraceId(text) { + const match = String(text || '').match(/\b(?:trace_id=|traceId[:=]?\s*)(trc_[a-z0-9_-]+|[a-f0-9]{16,})\b/i); + return match ? match[1] : null; +} + +function traceRowGroupKey(row) { + const identity = row.traceId ? 'trace:' + row.traceId : 'sample:' + (row.sampleIndex ?? '-') + ':' + (row.source || 'unknown'); + return [row.pageRole || '', row.pageId || '', row.sessionId || '', row.source || '', row.sampleIndex ?? '', identity].join('|'); +} + +function traceRowMatchesCard(row, card, terminalCardCount) { + if (!row) return false; + if (card.traceId) return row.traceId === card.traceId; + if (row.traceId) return false; + if (row.sessionId && card.sessionId) return terminalCardCount === 1 && row.sessionId === card.sessionId; + if (terminalCardCount === 1) return true; + return false; +} + +function maxTraceDurationEvidence(rows) { + const finiteTotals = rows.map((row) => Number(row.totalSeconds)).filter(Number.isFinite); + const finiteClocks = rows.map((row) => Number(row.clockSeconds)).filter(Number.isFinite); + const evidences = []; + if (finiteTotals.length) { + const maxTotal = Math.max(...finiteTotals); + const source = rows.find((row) => Number(row.totalSeconds) === maxTotal); + const exact = source?.isCompletion === true; + evidences.push({ kind: exact ? 'trace-completion-total' : 'trace-total', seconds: maxTotal, traceId: source?.traceId || null, preview: source?.preview || '', exact }); + } + if (finiteClocks.length >= 2) { + const minClock = Math.min(...finiteClocks); + const maxClock = Math.max(...finiteClocks); + const span = maxClock - minClock; + if (span >= 0 && span < 43200) evidences.push({ kind: 'trace-clock-span', seconds: span, traceId: null, preview: 'visible trace row clock span', exact: false }); + } + if (!evidences.length) return null; + evidences.sort((a, b) => b.seconds - a.seconds); + return evidences[0]; +} + +function maxSelfReportedDurationEvidence(text) { + const value = String(text || ''); + const lines = value.split(/\n+/).map((line) => line.trim()).filter(Boolean); + let best = null; + for (let index = 0; index < lines.length; index += 1) { + const line = lines[index]; + const previous = index > 0 ? lines[index - 1] : ''; + const next = index + 1 < lines.length ? lines[index + 1] : ''; + const candidateText = selfReportedDurationCandidateText(previous, line, next); + if (!candidateText) continue; + const seconds = parseSelfReportedRoundDurationSeconds(candidateText); + if (!Number.isFinite(seconds)) continue; + if (!best || seconds > best.seconds) { + best = { kind: 'final-response-duration', seconds, preview: compactOneLine(candidateText).slice(0, 240), exact: true }; + } + } + return best; +} + +function selfReportedDurationCandidateText(previous, line, next) { + const current = String(line || ''); + const before = String(previous || ''); + const after = String(next || ''); + const windowText = [before, current, after].filter(Boolean).join(' '); + const hasDurationKeyword = /耗时|用时|duration|elapsed/i.test(current); + const hasNearbyDurationHeading = /(?:本轮|整轮|全程|任务|round|turn)?\s*(?:耗时|用时|duration|elapsed)/i.test(before) + || /(?:本轮|整轮|全程|任务|round|turn)?\s*(?:耗时|用时|duration|elapsed)/i.test(after); + const hasDurationValue = /(?:约|大约|around|about)?\s*\d+(?:\.\d+)?\s*(?:小时|分钟|分|秒|hour|hours|hr|hrs|min|mins|minute|minutes|sec|secs|second|seconds)/i.test(windowText); + const hasRoundContext = /本轮|整轮|全程|从.+到|全部通过|smoke|benchmark|round|turn|completed|passed/i.test(windowText); + if (hasDurationKeyword && hasDurationValue) return current; + if (hasNearbyDurationHeading && hasDurationValue) return windowText; + if (hasRoundContext && hasDurationValue && /(?:约|大约|around|about)\s*\d|\d+(?:\.\d+)?\s*(?:分钟|小时|minute|hour)/i.test(current)) return windowText; + return ''; +} + +function parseSelfReportedRoundDurationSeconds(text) { + const value = String(text || ''); + const clock = value.match(/(?:耗时|用时|duration|elapsed)[^0-9]{0,24}(\d{1,2}:\d{2}:\d{2}|\d{1,2}:\d{2})/i); + if (clock) return parseTraceDurationSeconds(clock[1]); + const hour = value.match(/(?:约|大约|around|about)?\s*(\d+(?:\.\d+)?)\s*(?:小时|hour|hours|hr|hrs)/i); + const minute = value.match(/(?:约|大约|around|about)?\s*(\d+(?:\.\d+)?)\s*(?:分钟|分|min|mins|minute|minutes)/i); + const second = value.match(/(?:约|大约|around|about)?\s*(\d+(?:\.\d+)?)\s*(?:秒|sec|secs|second|seconds)/i); + let total = 0; + if (hour) total += Number(hour[1]) * 3600; + if (minute) total += Number(minute[1]) * 60; + if (second) total += Number(second[1]); + return total > 0 ? total : null; +} + +function sampleVisibleText(sample, timelineItem) { + const chunks = []; + for (const source of [sample?.visibleText, sample?.text, sample?.innerText, sample?.dom?.visibleText, sample?.dom?.text, timelineItem?.visibleText, timelineItem?.text, timelineItem?.message, timelineItem?.summary]) { + if (typeof source === 'string' && source.trim()) chunks.push(source); + } + return chunks.join('\n'); +} + +function objectText(value) { + if (!value || typeof value !== 'object') return typeof value === 'string' ? value : ''; + const keys = ['text', 'innerText', 'visibleText', 'label', 'title', 'summary', 'message', 'content', 'body', 'preview', 'description']; + const chunks = []; + for (const key of keys) { + const part = value[key]; + if (typeof part === 'string' && part.trim()) chunks.push(part); + } + return chunks.join('\n'); +} + +function compactOneLine(value) { + return String(value || '').replace(/\s+/g, ' ').trim(); +} + +function buildCodeAgentCardTimingMetrics(samples, timeline, turnTiming) { + const missingElapsed = []; + const missingRecentUpdate = []; + const cardRows = []; + for (let index = 0; index < (Array.isArray(samples) ? samples : []).length; index += 1) { + const sample = samples[index]; + const timelineItem = timeline[index] || {}; + for (const card of codeAgentCardsForSample(sample)) { + const text = codeAgentCardText(card); + const totalElapsedValues = parseTotalElapsedSeconds(card?.durationText).filter(Number.isFinite); + const recentUpdateValues = parseRecentUpdateSeconds(card?.activityText).filter(Number.isFinite); + const terminal = isCodeAgentCardTerminal(card); + const row = { + ...ref(sample), + promptIndex: timelineItem.promptIndex ?? 0, + source: card.source ?? "turn", + status: card.status ?? null, + messageId: card.messageId ?? null, + traceId: card.traceId ?? firstTraceId([text]), + sessionId: card.sessionId ?? sample.sessionId ?? sample.workbenchSessionId ?? sample.routeSessionId ?? sample.activeSessionId ?? null, + durationText: limitText(card.durationText, 120), + activityText: limitText(card.activityText, 120), + totalElapsedSeconds: totalElapsedValues.length > 0 ? Math.max(...totalElapsedValues) : null, + recentUpdateSeconds: recentUpdateValues.length > 0 ? Math.max(...recentUpdateValues) : null, + terminal, + textHash: card.textHash ?? sha256(text), + textPreview: limitText(text, 180), + valuesRedacted: true + }; + cardRows.push(row); + if (row.totalElapsedSeconds === null) missingElapsed.push(row); + if (!terminal && row.recentUpdateSeconds === null) missingRecentUpdate.push(row); + } + } + const roundCompletion = buildRoundCompletionMetrics(samples, timeline, turnTiming); + return { + summary: { + cardSampleCount: cardRows.length, + terminalCardSampleCount: cardRows.filter((item) => item.terminal).length, + runningCardSampleCount: cardRows.filter((item) => !item.terminal).length, + missingElapsedCount: missingElapsed.length, + missingRecentUpdateCount: missingRecentUpdate.length, + roundCompletionEventCount: roundCompletion.events.length, + roundCompletionElapsedMismatchCount: roundCompletion.elapsedMismatches.length, + roundCompletionFinalResponseMissingCount: roundCompletion.finalResponseMissing.length, + roundCompletionPostTimingChangeCount: roundCompletion.postCompletionTimingChanges.length, + roundCompletionPostRecentUpdateVisibleCount: roundCompletion.postCompletionRecentUpdateVisible.length, + elapsedMismatchToleranceSeconds: alertThresholds.turnTimingSampleSlackSeconds, + }, + cardSamples: cardRows.slice(0, 200), + missingElapsed: missingElapsed.slice(0, 200), + missingRecentUpdate: missingRecentUpdate.slice(0, 200), + roundCompletion, + valuesRedacted: true + }; +} + +function codeAgentCardsForSample(sample) { + const turnCards = (Array.isArray(sample?.turns) ? sample.turns : []) + .map((item) => ({ ...item, source: item?.source || "turn" })) + .filter(isCodeAgentCardLike); + if (turnCards.length > 0) return turnCards; + return (Array.isArray(sample?.messages) ? sample.messages : []) + .map((item) => ({ ...item, source: item?.source || "message" })) + .filter(isCodeAgentCardLike); +} + +function isCodeAgentCardLike(item) { + const text = codeAgentCardText(item); + const role = String(item?.dataRole || item?.role || "").toLowerCase(); + if (/agent|assistant/u.test(role)) return true; + return /Code Agent|运行记录|耗时|最近\s*(?:\d+|一|两|三)|轮次完成|trace_id=trc_/iu.test(text); +} + +function codeAgentCardText(item) { + return [ + item?.durationText, + item?.activityText, + item?.text, + item?.textPreview, + item?.status + ].map((value) => String(value || "")).filter((value) => value.trim().length > 0).join("\n"); +} + +function isCodeAgentCardTerminal(item) { + const status = String(item?.status ?? item?.state ?? item?.phase ?? "").trim().toLowerCase().replace(/_/gu, "-"); + if (isActiveTurnStatus(status)) return false; + if (isTerminalTurnStatus(status)) return true; + if (item?.terminal === true || item?.sealed === true) return true; + const text = codeAgentCardText(item); + return isTerminalTraceText(text) || /轮次完成|轮次失败|轮次取消|已记录|completed|failed|canceled|cancelled|blocked/iu.test(text); +} + +function isActiveTurnStatus(value) { + const status = String(value ?? "").trim().toLowerCase().replace(/_/gu, "-"); + return ["pending", "running", "queued", "admitted", "dispatching", "in-progress", "inprogress", "executing", "progress", "thinking", "working", "active", "streaming", "created", "started"].includes(status); +} + +function buildRoundCompletionMetrics(samples, timeline, turnTiming) { + const events = []; + const elapsedMismatchToleranceSeconds = Math.max(5, Number(alertThresholds.turnTimingSampleSlackSeconds || 0)); + for (let index = 0; index < (Array.isArray(samples) ? samples : []).length; index += 1) { + const sample = samples[index]; + const timelineItem = timeline[index] || {}; + for (const event of roundCompletionEventsForSample(sample, timelineItem)) events.push(event); + } + const completionEvents = dedupeRoundCompletionEvents(events); + const elapsedMismatches = []; + const finalResponseMissing = []; + const postCompletionTimingChanges = []; + const postCompletionRecentUpdateVisible = []; + for (const event of completionEvents) { + const sampleIndex = samples.findIndex((sample) => sample?.seq === event.seq && sample?.pageRole === event.pageRole && sample?.pageId === event.pageId); + const sameSample = sampleIndex >= 0 ? samples[sampleIndex] : null; + const sameTimelineItem = sampleIndex >= 0 ? timeline[sampleIndex] || {} : {}; + const cards = sameSample ? cardMetricItemsForCompletion(sameSample, sameTimelineItem, event) : []; + const bestCard = cards.filter((card) => Number.isFinite(Number(card.totalElapsedSeconds)))[0] || null; + if (Number.isFinite(Number(event.elapsedSeconds)) && bestCard) { + const delta = Math.abs(Number(bestCard.totalElapsedSeconds) - Number(event.elapsedSeconds)); + if (delta > elapsedMismatchToleranceSeconds) { + elapsedMismatches.push({ + ...eventRef(event), + cardTotalElapsedSeconds: Number(bestCard.totalElapsedSeconds), + completionElapsedSeconds: Number(event.elapsedSeconds), + deltaSeconds: Number(delta.toFixed(3)), + toleranceSeconds: elapsedMismatchToleranceSeconds, + cardTraceId: bestCard.traceId ?? null, + cardMessageId: bestCard.messageId ?? null, + valuesRedacted: true + }); + } + } + if (!(sameSample && sampleHasTerminalAgentResultCard(sameSample, event)) && !(sameSample && sampleHasVisibleFinalResponse(sameSample, event)) && !hasFinalResponseAfterCompletion(samples, timeline, event)) { + finalResponseMissing.push({ + ...eventRef(event), + completionElapsedSeconds: event.elapsedSeconds, + finalResponseProbe: sameSample ? terminalAgentResultProbe(sameSample, event) : { ok: false, reason: "sample-not-found" }, + valuesRedacted: true + }); + } + const postTiming = detectPostCompletionTimingChanges(turnTiming, event); + postCompletionTimingChanges.push(...postTiming.timingChanges); + postCompletionRecentUpdateVisible.push(...postTiming.recentUpdateVisible); + } + return { + events: completionEvents.slice(0, 200), + elapsedMismatches: dedupeRoundCompletionRows(elapsedMismatches).slice(0, 200), + finalResponseMissing: dedupeRoundCompletionRows(finalResponseMissing).slice(0, 200), + postCompletionTimingChanges: dedupeRoundCompletionRows(postCompletionTimingChanges).slice(0, 200), + postCompletionRecentUpdateVisible: dedupeRoundCompletionRows(postCompletionRecentUpdateVisible).slice(0, 200), + valuesRedacted: true + }; +} + +function roundCompletionEventsForSample(sample, timelineItem) { + const rows = []; + for (const item of traceTimingRowsForSample(sample, timelineItem)) { + const text = String(item?.preview || item?.text || item?.textPreview || ""); + if (!traceRowIsRoundCompletionText(text, item?.eventKind || "")) continue; + const elapsedValues = [ + Number(item?.totalSeconds), + ...parseTotalElapsedSeconds(text).filter(Number.isFinite) + ].filter(Number.isFinite); + const attributed = inferRoundCompletionCard(sample, text); + rows.push({ + ...ref(sample), + promptIndex: timelineItem.promptIndex ?? 0, + traceId: item?.traceId ?? firstTraceId([text]) ?? attributed?.traceId ?? null, + messageId: item?.messageId ?? attributed?.messageId ?? null, + attributed: Boolean(item?.traceId || item?.messageId || attributed?.traceId || attributed?.messageId), + traceRowIndex: item?.rowIndex ?? item?.index ?? null, + elapsedSeconds: elapsedValues.length > 0 ? Math.max(...elapsedValues) : null, + textHash: item?.textHash ?? sha256(text), + preview: limitText(text, 180), + valuesRedacted: true + }); + } + return rows; +} + +function inferRoundCompletionCard(sample, text) { + const cards = codeAgentCardsForSample(sample) + .filter((card) => isCodeAgentCardTerminal(card)) + .filter((card) => card?.traceId || card?.messageId); + const textHash = sha256(String(text || "")); + const direct = cards.filter((card) => { + const cardText = codeAgentCardText(card); + return card?.textHash === textHash || cardText.includes(String(text || "").slice(0, 80)) || /轮次完成/iu.test(cardText); + }); + const candidates = direct.length > 0 ? direct : cards; + if (candidates.length !== 1) return null; + const card = candidates[0]; + return { traceId: card.traceId ?? null, messageId: card.messageId ?? card.id ?? null }; +} + +function cardMetricItemsForCompletion(sample, timelineItem, event) { + const metrics = turnMetricItems(sample, timelineItem) + .filter((item) => item.promptIndex === event.promptIndex) + .filter((item) => item.pageRole === event.pageRole || !event.pageRole) + .filter((item) => item.pageId === event.pageId || !event.pageId); + if (!event.traceId && !event.messageId) { + const withElapsed = metrics.filter((item) => Number.isFinite(Number(item.totalElapsedSeconds))); + return withElapsed.length === 1 ? withElapsed : []; + } + return metrics + .filter((item) => !event.traceId || !item.traceId || item.traceId === event.traceId) + .filter((item) => !event.messageId || !item.messageId || item.messageId === event.messageId) + .sort((left, right) => { + const leftTraceMatch = event.traceId && left.traceId === event.traceId ? 0 : 1; + const rightTraceMatch = event.traceId && right.traceId === event.traceId ? 0 : 1; + const leftMessageMatch = event.messageId && left.messageId === event.messageId ? 0 : 1; + const rightMessageMatch = event.messageId && right.messageId === event.messageId ? 0 : 1; + return leftTraceMatch - rightTraceMatch || leftMessageMatch - rightMessageMatch || String(left.source || "").localeCompare(String(right.source || "")); + }); +} + +function hasFinalResponseAfterCompletion(samples, timeline, event) { + if (!event.traceId && !event.messageId && !event.promptIndex) return true; + const eventTsMs = Date.parse(String(event.ts || "")); + for (let index = 0; index < (Array.isArray(samples) ? samples : []).length; index += 1) { + const sample = samples[index]; + const tsMs = Date.parse(String(sample?.ts || "")); + if (Number.isFinite(eventTsMs) && Number.isFinite(tsMs) && tsMs < eventTsMs) continue; + if (sample?.pageRole !== event.pageRole) continue; + const sampleSession = sample?.routeSessionId || sample?.activeSessionId || null; + const eventSession = event.routeSessionId || event.activeSessionId || null; + if (sampleSession && eventSession && sampleSession !== eventSession) continue; + const promptIndex = timeline[index]?.promptIndex ?? 0; + if (!event.traceId && !event.messageId && event.promptIndex && promptIndex !== event.promptIndex) continue; + if (sampleHasTerminalAgentResultCard(sample, event)) return true; + if (sampleHasVisibleFinalResponse(sample, event)) return true; + } + return false; +} + +function sampleHasTerminalAgentResultCard(sample, event = {}) { + return terminalAgentResultProbe(sample, event).ok === true; +} + +function terminalAgentResultProbe(sample, event = {}) { + const decisions = []; + for (const item of codeAgentCardsForSample(sample)) { + const text = normalizedText(codeAgentCardText(item)); + const traceMatched = Boolean(event.traceId && item?.traceId && item.traceId === event.traceId); + const decision = { + source: item?.source || null, + role: item?.role ?? null, + dataRole: item?.dataRole ?? null, + status: item?.status ?? null, + traceId: item?.traceId ?? null, + messageId: item?.messageId ?? null, + textBytes: Buffer.byteLength(text), + valuesRedacted: true + }; + if (event.traceId && item?.traceId && item.traceId !== event.traceId) { + decisions.push({ ...decision, decision: "skip-trace" }); + continue; + } + if (!traceMatched && event.messageId && item?.messageId && item.messageId !== event.messageId) { + decisions.push({ ...decision, decision: "skip-message" }); + continue; + } + if (!isAssistantFinalResponseCandidate(item, item?.source || "turn")) { + decisions.push({ ...decision, decision: "skip-role" }); + continue; + } + if (!isCodeAgentCardTerminal(item)) { + decisions.push({ ...decision, decision: "skip-non-terminal" }); + continue; + } + if (text.length < 24) { + decisions.push({ ...decision, decision: "skip-short" }); + continue; + } + decisions.push({ ...decision, decision: "accept-terminal-agent-card" }); + return { ok: true, decisions: decisions.slice(-8), valuesRedacted: true }; + } + return { ok: false, decisions: decisions.slice(-8), valuesRedacted: true }; +} + +function sampleHasVisibleFinalResponse(sample, event = {}) { + for (const [groupName, group] of [["messages", sample?.messages], ["turns", sample?.turns]]) { + if (!Array.isArray(group)) continue; + for (const item of group) { + const traceMatched = Boolean(event.traceId && item?.traceId && item.traceId === event.traceId); + if (event.traceId && item?.traceId && item.traceId !== event.traceId) continue; + if (!traceMatched && event.messageId && item?.messageId && item.messageId !== event.messageId) continue; + if (!isAssistantFinalResponseCandidate(item, groupName)) continue; + const text = normalizedText([item?.text, item?.textPreview].filter(Boolean).join(" ")); + if (text.length < 24) continue; + if (groupName === "messages" && isTerminalTurnStatus(item?.status)) return true; + if (isDiagnosticText(text)) continue; + if (isFinalResultText(text)) return true; + if (/运行记录/iu.test(text) && /(?:已完成|完成|新增|实现|验证|测试|结果|README|文件|summary)/iu.test(text)) return true; + } + } + return false; +} + +function normalizedDomRole(item) { + return String(item?.dataRole ?? item?.role ?? item?.ariaRole ?? "") + .trim() + .toLowerCase() + .replace(/[_\s]+/gu, "-"); +} + +function isAssistantFinalResponseCandidate(item, groupName) { + const role = normalizedDomRole(item); + if (/agent|assistant|code-agent|bot/iu.test(role)) return true; + if (/user|human|client|prompt/iu.test(role)) return false; + if (groupName === "turns") return isCodeAgentCardLike(item); + const text = codeAgentCardText(item); + if (isTerminalTurnStatus(item?.status) && /Code Agent|运行记录|assistant|agent/iu.test(text)) return true; + return false; +} + +function detectPostCompletionTimingChanges(turnTiming, event) { + const timingChanges = []; + const recentUpdateVisible = []; + if (!event.traceId && !event.messageId) return { timingChanges, recentUpdateVisible }; + const rows = Array.isArray(turnTiming?.rows) ? turnTiming.rows : []; + const columns = Array.isArray(turnTiming?.columns) ? turnTiming.columns : []; + const eventTsMs = Date.parse(String(event.ts || "")); + for (const column of columns) { + if (column.pageRole && event.pageRole && column.pageRole !== event.pageRole) continue; + if (event.traceId && column.traceId && column.traceId !== event.traceId) continue; + if (event.messageId && column.messageId && column.messageId !== event.messageId) continue; + if (event.promptIndex && column.promptIndex && column.promptIndex !== event.promptIndex && column.lastPromptIndex !== event.promptIndex) continue; + let previousTotal = null; + let previousRecent = null; + for (const row of rows) { + const rowTsMs = Date.parse(String(row.ts || "")); + if (Number.isFinite(eventTsMs) && Number.isFinite(rowTsMs) && rowTsMs < eventTsMs) continue; + if (row.pageRole && event.pageRole && row.pageRole !== event.pageRole) continue; + const cell = row.cells?.[column.id]; + if (!cell) continue; + if (event.traceId && cell.traceId && cell.traceId !== event.traceId) continue; + if (event.messageId && cell.messageId && cell.messageId !== event.messageId) continue; + const total = cell.totalElapsedSeconds === null || cell.totalElapsedSeconds === undefined ? NaN : Number(cell.totalElapsedSeconds); + if (Number.isFinite(total)) { + if (previousTotal && Math.abs(total - previousTotal.value) > alertThresholds.turnTimingSampleSlackSeconds) { + timingChanges.push({ + ...eventRef(event), + columnId: column.id, + columnLabel: column.label, + metric: "totalElapsedSeconds", + fromSeq: previousTotal.seq, + fromTs: previousTotal.ts, + fromValue: previousTotal.value, + toSeq: row.seq ?? null, + toTs: row.ts ?? null, + toValue: total, + delta: Number((total - previousTotal.value).toFixed(3)), + toleranceSeconds: alertThresholds.turnTimingSampleSlackSeconds, + traceId: cell.traceId ?? column.traceId ?? event.traceId ?? null, + messageId: cell.messageId ?? column.messageId ?? null, + valuesRedacted: true + }); + } + previousTotal = { value: total, seq: row.seq ?? null, ts: row.ts ?? null }; + } + const recent = cell.recentUpdateSeconds === null || cell.recentUpdateSeconds === undefined ? NaN : Number(cell.recentUpdateSeconds); + if (Number.isFinite(recent)) { + recentUpdateVisible.push({ + ...eventRef(event), + columnId: column.id, + columnLabel: column.label, + metric: "recentUpdateSeconds", + seq: row.seq ?? null, + ts: row.ts ?? null, + value: recent, + traceId: cell.traceId ?? column.traceId ?? event.traceId ?? null, + messageId: cell.messageId ?? column.messageId ?? null, + valuesRedacted: true + }); + if (previousRecent && recent !== previousRecent.value) { + timingChanges.push({ + ...eventRef(event), + columnId: column.id, + columnLabel: column.label, + metric: "recentUpdateSeconds", + fromSeq: previousRecent.seq, + fromTs: previousRecent.ts, + fromValue: previousRecent.value, + toSeq: row.seq ?? null, + toTs: row.ts ?? null, + toValue: recent, + delta: Number((recent - previousRecent.value).toFixed(3)), + traceId: cell.traceId ?? column.traceId ?? event.traceId ?? null, + messageId: cell.messageId ?? column.messageId ?? null, + valuesRedacted: true + }); + } + previousRecent = { value: recent, seq: row.seq ?? null, ts: row.ts ?? null }; + } + } + } + return { timingChanges, recentUpdateVisible }; +} + +function dedupeRoundCompletionEvents(rows) { + const result = []; + const seen = new Set(); + for (const row of Array.isArray(rows) ? rows : []) { + const key = [ + row?.pageRole ?? "", + row?.pageId ?? "", + row?.promptIndex ?? "", + row?.traceId ?? "", + row?.messageId ?? "", + row?.textHash ?? "", + row?.elapsedSeconds ?? "" + ].join("|"); + if (seen.has(key)) continue; + seen.add(key); + result.push(row); + } + return result; +} + +function eventRef(event) { + return { + seq: event?.seq ?? null, + sampleGroupSeq: event?.sampleGroupSeq ?? null, + ts: event?.ts ?? null, + pageRole: event?.pageRole ?? null, + pageId: event?.pageId ?? null, + routeSessionId: event?.routeSessionId ?? null, + activeSessionId: event?.activeSessionId ?? null, + promptIndex: event?.promptIndex ?? null, + traceId: event?.traceId ?? null, + messageId: event?.messageId ?? null, + }; +} + +function dedupeRoundCompletionRows(rows) { + const result = []; + const seen = new Set(); + for (const row of Array.isArray(rows) ? rows : []) { + const key = [ + row?.seq ?? row?.fromSeq ?? "", + row?.toSeq ?? "", + row?.pageRole ?? "", + row?.promptIndex ?? "", + row?.traceId ?? "", + row?.metric ?? "", + row?.textHash ?? "", + row?.columnId ?? "" + ].join("|"); + if (seen.has(key)) continue; + seen.add(key); + result.push(row); + } + return result; +} + +function turnMetricItems(sample, timelineItem) { + const promptIndex = timelineItem.promptIndex ?? 0; + const pageRole = sample?.pageRole || "control"; + const pageId = sample?.pageId || "unknown-page"; + const sessionKey = pageRole + ":" + pageId + ":" + (sample?.routeSessionId || sample?.activeSessionId || "unknown-session"); + const roundKey = String(promptIndex); + const items = []; + if (Array.isArray(sample?.turns) && sample.turns.length > 0) { + for (const turn of sample.turns) { + const texts = turnTexts(turn); + const totalElapsedValues = texts.flatMap(parseTotalElapsedSeconds).filter(Number.isFinite); + const recentUpdateValues = texts.flatMap(parseRecentUpdateSeconds).filter(Number.isFinite); + const traceId = turn.traceId || firstTraceId(texts); + const messageId = turn.messageId || null; + const turnId = turn.turnId || traceId || null; + const stableId = traceId || messageId || turnId || null; + const domIndex = Number.isFinite(Number(turn.index)) ? Number(turn.index) : items.length; + const key = stableId + ? "timing:" + sessionKey + ":id-" + stableId + : "turn:" + sessionKey + ":round-" + roundKey + ":dom-index-" + String(domIndex); + items.push({ + key, + source: "turn", + pageRole, + pageId, + promptIndex, + traceId, + messageId, + turnId, + domIndex, + status: turn.status ?? null, + totalElapsedSeconds: totalElapsedValues.length > 0 ? Math.max(...totalElapsedValues) : null, + recentUpdateSeconds: recentUpdateValues.length > 0 ? Math.max(...recentUpdateValues) : null, + textHash: turn.textHash || sha256(texts.join("\n")) + }); + } + return items; + } + if (Array.isArray(sample?.messages) && sample.messages.length > 0) { + for (const message of sample.messages) { + const text = [message?.durationText, message?.activityText].map((value) => String(value || "")).filter((value) => value.trim().length > 0).join("\n"); + const totalElapsedValues = parseTotalElapsedSeconds(text).filter(Number.isFinite); + const recentUpdateValues = parseRecentUpdateSeconds(text).filter(Number.isFinite); + if (totalElapsedValues.length === 0 && recentUpdateValues.length === 0) continue; + const domIndex = Number.isFinite(Number(message.index)) ? Number(message.index) : items.length; + const traceId = message.traceId || firstTraceId([text]); + const messageId = message.messageId || null; + const stableId = traceId || messageId || message.turnId || null; + items.push({ + key: stableId + ? "timing:" + sessionKey + ":id-" + stableId + : "message:" + sessionKey + ":round-" + roundKey + ":dom-index-" + String(domIndex), + source: "message", + pageRole, + pageId, + promptIndex, + traceId, + messageId, + turnId: message.turnId || traceId || null, + domIndex, + status: message.status ?? null, + totalElapsedSeconds: totalElapsedValues.length > 0 ? Math.max(...totalElapsedValues) : null, + recentUpdateSeconds: recentUpdateValues.length > 0 ? Math.max(...recentUpdateValues) : null, + textHash: message.textHash || sha256(text) + }); + } + if (items.length > 0) return items; + } + if (timelineItem.totalElapsedSeconds !== null || timelineItem.recentUpdateSeconds !== null) { + return [{ + key: "aggregate:" + sessionKey + ":round-" + roundKey, + source: "aggregate", + pageRole, + pageId, + promptIndex, + traceId: null, + messageId: null, + domIndex: null, + status: null, + totalElapsedSeconds: timelineItem.totalElapsedSeconds ?? null, + recentUpdateSeconds: timelineItem.recentUpdateSeconds ?? null, + textHash: timelineItem.textDigest ?? null + }]; + } + return []; +} + +function turnTexts(turn) { + return [ + turn?.durationText, + turn?.activityText + ].map((value) => String(value || "")).filter((value) => value.trim().length > 0); +} + +function firstTraceId(texts) { + for (const text of texts) { + const match = String(text || "").match(/\btrc_[A-Za-z0-9_-]+\b/u); + if (match) return match[0]; + } + return null; +} + +function buildRoundMetricSummaries(timeline, promptCommands, timing = {}) { + const rounds = []; + const timingColumns = Array.isArray(timing.columns) ? timing.columns : []; + const timingRows = Array.isArray(timing.rows) ? timing.rows : []; + const nonMonotonic = Array.isArray(timing.nonMonotonic) ? timing.nonMonotonic : []; + const totalElapsedForwardJumps = Array.isArray(timing.totalElapsedForwardJumps) ? timing.totalElapsedForwardJumps : []; + const elapsedZeroResets = Array.isArray(timing.elapsedZeroResets) ? timing.elapsedZeroResets : []; + const terminalElapsedGrowth = Array.isArray(timing.terminalElapsedGrowth) ? timing.terminalElapsedGrowth : []; + const recentUpdateResets = Array.isArray(timing.recentUpdateResets) ? timing.recentUpdateResets : []; + const recentUpdateSteps = Array.isArray(timing.recentUpdateSteps) ? timing.recentUpdateSteps : []; + for (let index = 0; index < promptCommands.length; index += 1) { + const promptIndex = index + 1; + const items = timeline.filter((item) => item.promptIndex === promptIndex); + const aggregateTotalElapsed = items.map((item) => item.totalElapsedSeconds).filter((value) => value !== null); + const aggregateRecentUpdate = items.map((item) => item.recentUpdateSeconds).filter((value) => value !== null); + const promptTurnTiming = roundPromptTurnTimingValues(timingRows, timingColumns, promptIndex, { + firstSeq: items[0]?.seq ?? null, + firstSampleAt: items[0]?.ts ?? null + }); + const totalElapsed = promptTurnTiming.totalElapsed.length > 0 ? promptTurnTiming.totalElapsed : aggregateTotalElapsed; + const recentUpdate = promptTurnTiming.recentUpdate.length > 0 ? promptTurnTiming.recentUpdate : aggregateRecentUpdate; + const loadingCounts = items.map((item) => Number(item.loadingCount ?? 0)).filter(Number.isFinite); + const loadingOwners = new Set(); + for (const item of items) { + for (const owner of Array.isArray(item.loadingOwners) ? item.loadingOwners : []) { + if (owner?.ownerKey) loadingOwners.add(owner.ownerKey); + } + } + const timingAnomalies = nonMonotonic.filter((item) => item.promptIndex === promptIndex); + const timingForwardJumps = totalElapsedForwardJumps.filter((item) => item.promptIndex === promptIndex); + const timingZeroResets = elapsedZeroResets.filter((item) => item.promptIndex === promptIndex); + const timingTerminalGrowth = terminalElapsedGrowth.filter((item) => item.promptIndex === promptIndex); + const timingResets = recentUpdateResets.filter((item) => item.promptIndex === promptIndex); + const timingSteps = recentUpdateSteps.filter((item) => item.promptIndex === promptIndex); + const terminalGrowthDeltas = timingTerminalGrowth.map((item) => Number(item.delta)).filter((value) => Number.isFinite(value) && value > 0); + const timingStepDeltas = timingSteps.map((item) => Number(item.delta)).filter((value) => Number.isFinite(value) && value >= 0); + const timingStepExcess = timingSteps.map((item) => Number(item.excessiveIncreaseSeconds)).filter((value) => Number.isFinite(value) && value > 0); + rounds.push({ + promptIndex, + promptCommandId: promptCommands[index].commandId, + promptTextHash: promptCommands[index].textHash, + promptTextBytes: promptCommands[index].textBytes, + promptCompletedAt: promptCommands[index].ts, + sampleCount: items.length, + firstSeq: items[0]?.seq ?? null, + lastSeq: items[items.length - 1]?.seq ?? null, + firstSampleAt: items[0]?.ts ?? null, + lastSampleAt: items[items.length - 1]?.ts ?? null, + withTotalElapsed: totalElapsed.length, + withRecentUpdate: recentUpdate.length, + loadingSamples: loadingCounts.filter((value) => value > 0).length, + maxLoadingCount: loadingCounts.length > 0 ? Math.max(...loadingCounts) : 0, + loadingOwnerCount: loadingOwners.size, + maxTotalElapsedSeconds: totalElapsed.length > 0 ? Math.max(...totalElapsed) : null, + lastTotalElapsedSeconds: lastNonNull(totalElapsed), + maxRecentUpdateSeconds: recentUpdate.length > 0 ? Math.max(...recentUpdate) : null, + lastRecentUpdateSeconds: lastNonNull(recentUpdate), + diagnosticSamples: items.filter((item) => item.diagnosticSeen).length, + terminalSamples: items.filter((item) => item.terminalSeen).length, + finalTextSamples: items.filter((item) => item.finalResultTextSeen).length, + turnTimingNonMonotonicCount: timingAnomalies.length, + turnTimingTotalElapsedDecreaseCount: timingAnomalies.filter((item) => item.metric === "totalElapsedSeconds").length, + turnTimingTotalElapsedZeroResetCount: timingZeroResets.length, + turnTimingTotalElapsedForwardJumpCount: timingForwardJumps.length, + turnTimingTotalElapsedForwardJumpMaxSeconds: maxPositiveDelta(timingForwardJumps), + turnTimingTerminalElapsedGrowthCount: timingTerminalGrowth.length, + turnTimingTerminalElapsedGrowthMaxSeconds: terminalGrowthDeltas.length > 0 ? Math.max(...terminalGrowthDeltas) : 0, + turnTimingRecentUpdateJumpCount: timingAnomalies.filter((item) => item.metric === "recentUpdateSeconds" && item.anomaly === "jump").length, + turnTimingRecentUpdateSawtoothJumpCount: timingAnomalies.filter((item) => item.metric === "recentUpdateSeconds" && item.anomaly === "jump").length, + turnTimingRecentUpdateStepCount: timingSteps.length, + turnTimingRecentUpdateMaxIncreaseSeconds: timingStepDeltas.length > 0 ? Math.max(...timingStepDeltas) : null, + turnTimingRecentUpdateMaxExcessSeconds: timingStepExcess.length > 0 ? Math.max(...timingStepExcess) : 0, + turnTimingRecentUpdateResetCount: timingResets.length, + turnTimingRecentUpdateDecreaseCount: timingResets.length + }); + } + return rounds; +} + +function roundPromptTurnTimingValues(rows, columns, promptIndex, round = {}) { + const roundFirstSeq = Number(round.firstSeq); + const roundFirstMs = Date.parse(String(round.firstSampleAt ?? "")); + const seqSlack = 3; + const timeSlackMs = 3000; + const promptColumnIds = new Set(columns + .filter((column) => { + if (Number(column?.promptIndex) === Number(promptIndex)) return true; + const firstSeq = Number(column?.firstSeq); + if (Number.isFinite(roundFirstSeq) && Number.isFinite(firstSeq) && firstSeq >= roundFirstSeq - seqSlack) return true; + const firstMs = Date.parse(String(column?.firstTs ?? "")); + return Number.isFinite(roundFirstMs) && Number.isFinite(firstMs) && firstMs >= roundFirstMs - timeSlackMs; + }) + .map((column) => String(column?.id || "")) + .filter(Boolean)); + if (promptColumnIds.size === 0) return { totalElapsed: [], recentUpdate: [] }; + const totalElapsed = []; + const recentUpdate = []; + for (const row of rows) { + if (Number(row?.promptIndex ?? 0) !== Number(promptIndex)) continue; + const cells = row?.cells && typeof row.cells === "object" ? row.cells : {}; + for (const [columnId, cell] of Object.entries(cells)) { + if (!promptColumnIds.has(columnId)) continue; + const total = Number(cell?.totalElapsedSeconds); + if (Number.isFinite(total)) totalElapsed.push(total); + const recent = Number(cell?.recentUpdateSeconds); + if (Number.isFinite(recent)) recentUpdate.push(recent); + } + } + return { totalElapsed, recentUpdate }; +} + +function sampleTexts(sample) { + const rows = []; + for (const group of [sample?.messages, sample?.traceRows, sample?.diagnostics]) { + if (!Array.isArray(group)) continue; + for (const item of group) { + const text = String(item?.textPreview || ""); + if (text.trim()) rows.push(text); + } + } + return rows; +} + +function sampleTurnTimingTexts(sample) { + const rows = []; + for (const group of [sample?.turns, sample?.messages]) { + if (!Array.isArray(group)) continue; + for (const item of group) { + for (const value of [item?.durationText, item?.activityText]) { + const text = String(value || ""); + if (text.trim()) rows.push(text); + } + } + } + return rows; +} + +function parseTotalElapsedSeconds(text) { + const values = []; + for (const match of String(text || "").matchAll(/(?:总耗时|耗时)\s*[=::]?\s*(\d{1,2}):(\d{2}):(\d{2})/giu)) { + values.push(Number(match[1]) * 3600 + Number(match[2]) * 60 + Number(match[3])); + } + for (const match of String(text || "").matchAll(/(?:总耗时|耗时)\s*[=::]?\s*(\d{1,2}):(\d{2})(?!:)/giu)) { + values.push(Number(match[1]) * 60 + Number(match[2])); + } + for (const match of String(text || "").matchAll(/(?:总耗时|耗时)\s*[=::]?\s*(?:(\d+)\s*天)?\s*(?:(\d+)\s*小时)?\s*(?:(\d+)\s*(?:分钟|分))?\s*(?:(\d+)\s*秒)?/giu)) { + const days = Number(match[1] || 0); + const hours = Number(match[2] || 0); + const minutes = Number(match[3] || 0); + const seconds = Number(match[4] || 0); + if (days || hours || minutes || seconds || /(?:天|小时|分钟|分|秒)/u.test(match[0] || "")) values.push(days * 86400 + hours * 3600 + minutes * 60 + seconds); + } + return values; +} + +function lastNonNull(values) { + for (let index = values.length - 1; index >= 0; index -= 1) if (values[index] !== null && values[index] !== undefined) return values[index]; + return null; +} + +function parseRecentUpdateSeconds(text) { + const values = []; + for (const match of String(text || "").matchAll(/最近\s*(?:(\d+)\s*天)?\s*(?:(\d+)\s*小时)?\s*(?:(\d+)\s*(?:分钟|分))?\s*(?:(\d+)\s*秒)?\s*前/giu)) { + const days = Number(match[1] || 0); + const hours = Number(match[2] || 0); + const minutes = Number(match[3] || 0); + const seconds = Number(match[4] || 0); + values.push(days * 86400 + hours * 3600 + minutes * 60 + seconds); + } + return values; +} + +function latestPromptIndex(promptTimes, tsMs) { + let index = 0; + for (let i = 0; i < promptTimes.length; i += 1) { + if (promptTimes[i] <= tsMs) index = i + 1; + else break; + } + return index; +} + +function promptIndexForTs(promptTimes, ts) { + const tsMs = Date.parse(ts); + return Number.isFinite(tsMs) ? latestPromptIndex(promptTimes, tsMs) : 0; +} + +function buildTransitions(samples) { + const rows = []; + let last = null; + for (const sample of samples) { + const digest = digestSample(sample); + if (digest !== last) { + rows.push({ seq: sample.seq, ts: sample.ts, url: sample.url, routeSessionId: sample.routeSessionId || null, activeSessionId: sample.activeSessionId || null, messageCount: Array.isArray(sample.messages) ? sample.messages.length : 0, traceRowCount: Array.isArray(sample.traceRows) ? sample.traceRows.length : 0, digest }); + last = digest; + } + } + return rows.slice(0, 200); +} + +function detectFinalFlicker(samples) { + const flickers = []; + const lastNonEmptyByScope = new Map(); + for (const sample of samples) { + const scope = finalFlickerScope(sample); + if (!scope) continue; + const messageText = Array.isArray(sample.messages) ? sample.messages.map((item) => item.textPreview || "").join("\n") : ""; + const nonEmpty = messageText.trim().length > 0; + const finalLike = /轮次完成|已记录|已完成第\d+轮|final response|terminal result/iu.test(messageText); + const diagnosticLike = /temporarily|timeout|无法连接|暂时|error|failed|超时/iu.test(messageText); + const lastNonEmpty = lastNonEmptyByScope.get(scope); + if (nonEmpty && finalLike && !diagnosticLike) lastNonEmptyByScope.set(scope, { sample, messageText }); + if (lastNonEmpty && nonEmpty && diagnosticLike) flickers.push({ scope, from: ref(lastNonEmpty.sample), to: ref(sample) }); + if (lastNonEmpty && !nonEmpty) flickers.push({ scope, from: ref(lastNonEmpty.sample), to: ref(sample), reason: "text-disappeared" }); + } + return flickers; +} + +function finalFlickerScope(sample) { + const pathname = samplePathname(sample); + const sessionId = sample?.routeSessionId || sample?.activeSessionId || workbenchSessionIdFromPath(pathname); + if (!sessionId) return null; + if (!pathname.startsWith("/workbench/sessions/" + sessionId)) return null; + return (sample?.pageRole || "control") + ":" + sessionId; +} + +function samplePathname(sample) { + const raw = String(sample?.path || sample?.url || "").trim(); + if (!raw) return ""; + try { + return new URL(raw, "http://hwlab.local").pathname || raw; + } catch { + return raw.split(/[?#]/u, 1)[0] || raw; + } +} + +function workbenchSessionIdFromPath(pathname) { + const match = String(pathname || "").match(/^\/workbench\/sessions\/([^/?#]+)/u); + return match ? match[1] : null; +} + +function detectTerminalZeroElapsed(samples) { + const rows = []; + for (const sample of samples) { + const turns = Array.isArray(sample?.turns) ? sample.turns : []; + const messages = Array.isArray(sample?.messages) ? sample.messages : []; + for (const item of [...turns, ...messages]) { + const text = [item?.durationText, item?.activityText, item?.status, item?.textPreview, item?.text].filter(Boolean).join("\n"); + if (!/(?:Code Agent|运行记录|completed|failed|canceled|blocked)/iu.test(text)) continue; + if (!/(?:completed|failed|canceled|blocked|已完成|失败|取消)/iu.test(text)) continue; + const timingText = [item?.durationText, item?.activityText].filter(Boolean).join("\n"); + const elapsedValues = parseTotalElapsedSeconds(timingText); + if (!elapsedValues.includes(0)) continue; + rows.push({ + ...ref(sample), + status: item?.status ?? null, + messageId: item?.messageId ?? null, + traceId: item?.traceId ?? null, + durationText: item?.durationText ?? null, + activityText: item?.activityText ?? null, + textPreview: limitText(item?.textPreview || item?.text || "", 180) + }); + } + } + return rows; +} + +function detectCrossPageProjectionDiffs(samples) { + const groups = new Map(); + for (const sample of samples) { + const key = sample?.sampleGroupSeq ?? sample?.seq; + if (key === null || key === undefined) continue; + const group = groups.get(key) || {}; + if (sample?.pageRole === "control") group.control = sample; + else if (sample?.pageRole === "observer") group.observer = sample; + groups.set(key, group); + } + const rows = []; + for (const [sampleGroupSeq, group] of groups.entries()) { + const control = group.control; + const observer = group.observer; + if (!control || !observer) continue; + const controlTraceIds = visibleTraceIds(control); + const observerTraceIds = visibleTraceIds(observer); + const missingInObserver = [...controlTraceIds].filter((item) => !observerTraceIds.has(item)); + const controlMessages = Array.isArray(control.messages) ? control.messages.length : 0; + const observerMessages = Array.isArray(observer.messages) ? observer.messages.length : 0; + const controlTraceRows = Array.isArray(control.traceRows) ? control.traceRows.length : 0; + const observerTraceRows = Array.isArray(observer.traceRows) ? observer.traceRows.length : 0; + const controlZero = detectTerminalZeroElapsed([control]).length > 0; + const observerZero = detectTerminalZeroElapsed([observer]).length > 0; + const sameSession = (control.routeSessionId || control.activeSessionId || null) && (control.routeSessionId || control.activeSessionId || null) === (observer.routeSessionId || observer.activeSessionId || null); + const controlMessageDigest = digestMessageTexts(control); + const observerMessageDigest = digestMessageTexts(observer); + const messageTextDigestDiff = controlMessageDigest !== observerMessageDigest; + const projectionDivergent = sameSession && (Math.abs(controlMessages - observerMessages) > 0 || controlZero !== observerZero); + const traceVisibilityDivergent = sameSession && !projectionDivergent && (missingInObserver.length > 0 || Math.abs(controlTraceRows - observerTraceRows) > 0); + if (!projectionDivergent && !traceVisibilityDivergent) continue; + rows.push({ + sampleGroupSeq, + diffKind: traceVisibilityDivergent ? "trace-visibility" : "projection", + control: ref(control), + observer: ref(observer), + controlTraceIds: [...controlTraceIds].slice(0, 8), + observerTraceIds: [...observerTraceIds].slice(0, 8), + missingTraceIdsInObserver: missingInObserver.slice(0, 8), + controlMessageCount: controlMessages, + observerMessageCount: observerMessages, + controlTraceRowCount: controlTraceRows, + observerTraceRowCount: observerTraceRows, + terminalZeroElapsedDiff: controlZero !== observerZero, + messageTextDigestDiff, + controlMessageDigest, + observerMessageDigest + }); + } + return rows; +} + +function mergeCrossPageDiffRows(...groups) { + const rows = []; + const seen = new Set(); + for (const group of groups) { + if (!Array.isArray(group)) continue; + for (const row of group) { + const key = [ + row?.diffKind || "projection", + row?.control?.seq ?? null, + row?.observer?.seq ?? null, + row?.controlMessageCount ?? null, + row?.observerMessageCount ?? null, + row?.controlTraceRowCount ?? null, + row?.observerTraceRowCount ?? null + ].join(":"); + if (seen.has(key)) continue; + seen.add(key); + rows.push(row); + } + } + return rows; +} + +function annotateCrossPageDiffTiming(rows) { + const groups = new Map(); + for (const row of Array.isArray(rows) ? rows : []) { + const controlAt = Date.parse(String(row?.control?.ts || "")); + const observerAt = Date.parse(String(row?.observer?.ts || "")); + const timestamps = [controlAt, observerAt].filter(Number.isFinite); + const startMs = timestamps.length > 0 ? Math.min(...timestamps) : null; + const endMs = timestamps.length > 0 ? Math.max(...timestamps) : null; + const sessionId = row?.control?.routeSessionId || row?.control?.activeSessionId || row?.observer?.routeSessionId || row?.observer?.activeSessionId || "unknown-session"; + const key = [row?.diffKind || "projection", sessionId].join(":"); + const group = groups.get(key) || { rows: [], firstMs: null, lastMs: null }; + const annotated = { + ...row, + sampleStartAt: startMs === null ? null : new Date(startMs).toISOString(), + sampleEndAt: endMs === null ? null : new Date(endMs).toISOString(), + pairSkewMs: startMs === null || endMs === null ? null : endMs - startMs, + }; + group.rows.push(annotated); + if (startMs !== null && (group.firstMs === null || startMs < group.firstMs)) group.firstMs = startMs; + if (endMs !== null && (group.lastMs === null || endMs > group.lastMs)) group.lastMs = endMs; + groups.set(key, group); + } + const result = []; + for (const group of groups.values()) { + const sortedRows = group.rows.slice().sort((a, b) => Number(Date.parse(String(a.sampleStartAt || ""))) - Number(Date.parse(String(b.sampleStartAt || "")))); + const segments = []; + const splitGapMs = Math.max(1000, Number(alertThresholds.crossPageProjectionDivergenceRedMs || alertThresholds.visibleLoadingSlowMs || 10_000)); + for (const row of sortedRows) { + const startMs = Date.parse(String(row.sampleStartAt || "")); + const endMs = Date.parse(String(row.sampleEndAt || row.sampleStartAt || "")); + const last = segments.at(-1); + const lastEndMs = last && last.lastMs !== null ? last.lastMs : null; + if (!last || (Number.isFinite(startMs) && lastEndMs !== null && startMs - lastEndMs > splitGapMs)) { + segments.push({ rows: [row], firstMs: Number.isFinite(startMs) ? startMs : null, lastMs: Number.isFinite(endMs) ? endMs : Number.isFinite(startMs) ? startMs : null }); + continue; + } + last.rows.push(row); + if (Number.isFinite(startMs) && (last.firstMs === null || startMs < last.firstMs)) last.firstMs = startMs; + const effectiveEndMs = Number.isFinite(endMs) ? endMs : Number.isFinite(startMs) ? startMs : null; + if (effectiveEndMs !== null && (last.lastMs === null || effectiveEndMs > last.lastMs)) last.lastMs = effectiveEndMs; + } + for (let segmentIndex = 0; segmentIndex < segments.length; segmentIndex += 1) { + const segment = segments[segmentIndex]; + const observedSpanMs = segment.firstMs === null || segment.lastMs === null ? null : segment.lastMs - segment.firstMs; + for (const row of segment.rows) { + result.push({ + ...row, + segmentIndex, + observedFirstAt: segment.firstMs === null ? null : new Date(segment.firstMs).toISOString(), + observedLastAt: segment.lastMs === null ? null : new Date(segment.lastMs).toISOString(), + observedSpanMs, + }); + } + } + } + return result; +} + +function detectAdjacentCrossPageProjectionDiffs(samples) { + const rows = []; + const ordered = (Array.isArray(samples) ? samples : []).slice().sort((a, b) => Number(a?.seq ?? 0) - Number(b?.seq ?? 0)); + for (let i = 1; i < ordered.length; i += 1) { + const a = ordered[i - 1]; + const b = ordered[i]; + const roles = new Set([a?.pageRole, b?.pageRole]); + if (!roles.has("control") || !roles.has("observer")) continue; + const control = a?.pageRole === "control" ? a : b; + const observer = a?.pageRole === "observer" ? a : b; + const controlSession = control.routeSessionId || control.activeSessionId || null; + const observerSession = observer.routeSessionId || observer.activeSessionId || null; + if (!controlSession || controlSession !== observerSession) continue; + const controlAt = Date.parse(String(control.ts || "")); + const observerAt = Date.parse(String(observer.ts || "")); + if (Number.isFinite(controlAt) && Number.isFinite(observerAt) && Math.abs(controlAt - observerAt) > 1500) continue; + const controlMessages = Array.isArray(control.messages) ? control.messages.length : 0; + const observerMessages = Array.isArray(observer.messages) ? observer.messages.length : 0; + const controlTraceRows = Array.isArray(control.traceRows) ? control.traceRows.length : 0; + const observerTraceRows = Array.isArray(observer.traceRows) ? observer.traceRows.length : 0; + const controlZero = detectTerminalZeroElapsed([control]).length > 0; + const observerZero = detectTerminalZeroElapsed([observer]).length > 0; + const missingInObserver = [...visibleTraceIds(control)].filter((item) => !visibleTraceIds(observer).has(item)); + const controlTraceIds = visibleTraceIds(control); + const observerTraceIds = visibleTraceIds(observer); + const controlMessageDigest = digestMessageTexts(control); + const observerMessageDigest = digestMessageTexts(observer); + const messageTextDigestDiff = controlMessageDigest !== observerMessageDigest; + const projectionDivergent = controlMessages !== observerMessages || controlZero !== observerZero; + const traceVisibilityDivergent = !projectionDivergent && (missingInObserver.length > 0 || controlTraceRows !== observerTraceRows); + if (!projectionDivergent && !traceVisibilityDivergent) continue; + rows.push({ + sampleGroupSeq: control.sampleGroupSeq ?? observer.sampleGroupSeq ?? null, + adjacentPair: true, + diffKind: traceVisibilityDivergent ? "trace-visibility" : "projection", + control: ref(control), + observer: ref(observer), + controlTraceIds: [...controlTraceIds].slice(0, 8), + observerTraceIds: [...observerTraceIds].slice(0, 8), + missingTraceIdsInObserver: missingInObserver.slice(0, 8), + controlMessageCount: controlMessages, + observerMessageCount: observerMessages, + controlTraceRowCount: controlTraceRows, + observerTraceRowCount: observerTraceRows, + terminalZeroElapsedDiff: controlZero !== observerZero, + messageTextDigestDiff, + controlMessageDigest, + observerMessageDigest + }); + } + return rows; +} + +function detectTraceMessageDuplication(samples) { + const rows = []; + for (const sample of samples) { + const 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) => stableDigestItem(item, ["dataRole", "role", "status", "sessionId", "messageId", "traceId", "turnId"])).join("|") : ""; + const trace = Array.isArray(sample.traceRows) ? sample.traceRows.map((item) => stableDigestItem(item, ["status", "sessionId", "messageId", "traceId", "turnId", "projectedSeq", "sourceSeq", "eventSeq", "eventKind"])).join("|") : ""; + const diagnostics = Array.isArray(sample.diagnostics) ? sample.diagnostics.map((item) => stableDigestItem(item, ["className", "status", "sessionId", "messageId", "traceId", "turnId", "diagnosticCode"])).join("|") : ""; + return sha256((sample.routeSessionId || "") + "|" + (sample.activeSessionId || "") + "|" + messages + "|" + trace + "|" + diagnostics); +} + +function samplePageKey(sample) { + return String(sample?.pageRole || "control") + ":" + String(sample?.pageId || "default"); +} + +function stableDigestItem(item, fields) { + if (!item || typeof item !== "object") return ""; + const identity = fields.map((field) => String(item?.[field] ?? "")).join(":"); + return identity + ":" + stableVisibleDigestText(item?.textPreview || item?.text || item?.textHash || ""); +} + +function stableVisibleDigestText(value) { + return normalizedText(value) + .replace(/\btotal=\d{1,2}:\d{2}:\d{2}\b/giu, "total=") + .replace(/(?:总耗时|耗时)\s*[=::]?\s*(?:(?:\d+\s*天)?\s*(?:\d+\s*小时)?\s*(?:\d+\s*(?:分钟|分))?\s*(?:\d+\s*秒)?|\d{1,2}:\d{2}(?::\d{2})?)/giu, "耗时 ") + .replace(/最近\s*(?:(?:\d+\s*天)?\s*(?:\d+\s*小时)?\s*(?:\d+\s*(?:分钟|分))?\s*(?:\d+\s*秒)?)\s*前/giu, "最近 前"); +} + +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 : []; + const disclosure = sampleMetrics?.turnTimingTableDisclosure || null; + if (columns.length === 0 || rows.length === 0) return "- 无 turn 时间表。"; + const header = ["时间戳"]; + for (const column of columns) { + const columnLabel = formatTurnColumnDisplayLabel(column); + header.push(columnLabel + " 总耗时(s)"); + header.push(columnLabel + " 最近更新(s)"); + } + const lines = []; + lines.push("| " + header.map(escapeMarkdownCell).join(" | ") + " |"); + lines.push("| " + header.map(() => "---").join(" | ") + " |"); + for (const row of rows) { + const cells = [row.ts || "-"]; + for (const column of columns) { + const cell = row.cells?.[column.id] || {}; + cells.push(formatMetricCell(cell.totalElapsedSeconds)); + cells.push(formatMetricCell(cell.recentUpdateSeconds)); + } + lines.push("| " + cells.map(escapeMarkdownCell).join(" | ") + " |"); + } + const columnLines = columns.map((column) => "- " + formatTurnColumnDisplayLabel(column) + ": pageRole=" + (column.pageRole || "-") + " pageId=" + (column.pageId || "-") + " source=" + (column.source || "-") + " prompt=" + (column.promptIndex ?? "-") + " lastPrompt=" + (column.lastPromptIndex ?? "-") + " firstSeq=" + (column.firstSeq ?? "-") + " lastSeq=" + (column.lastSeq ?? "-") + " traceId=" + (column.traceId || "-") + " messageId=" + (column.messageId || "-")).join("\n"); + const nonMonotonic = Array.isArray(sampleMetrics?.turnTimingNonMonotonic) ? sampleMetrics.turnTimingNonMonotonic : []; + const nonMonotonicLines = nonMonotonic.length > 0 + ? nonMonotonic.slice(0, 80).map((item) => "- " + formatTurnEventDisplayLabel(item) + " " + item.metric + (item.anomaly ? " " + item.anomaly : "") + " " + (item.fromValue ?? "-") + " -> " + (item.toValue ?? "-") + " delta=" + (item.delta ?? "-") + " sampleDelta=" + (item.sampleDeltaSeconds ?? "-") + " allowed=" + (item.allowedIncreaseSeconds ?? "-") + " seq " + (item.fromSeq ?? "-") + " -> " + (item.toSeq ?? "-") + " ts " + (item.fromTs || "-") + " -> " + (item.toTs || "-") + " traceId=" + (item.traceId || "-")).join("\n") + : "- 未观察到总耗时下降或最近更新异常跳增。"; + const terminalGrowth = Array.isArray(sampleMetrics?.turnTimingTerminalElapsedGrowth) ? sampleMetrics.turnTimingTerminalElapsedGrowth : []; + const terminalGrowthLines = terminalGrowth.length > 0 + ? terminalGrowth.slice(0, 80).map((item) => "- " + formatTurnEventDisplayLabel(item) + " terminal totalElapsed growth " + (item.fromValue ?? "-") + " -> " + (item.toValue ?? "-") + " delta=" + (item.delta ?? "-") + " status " + (item.fromStatus || "-") + " -> " + (item.toStatus || "-") + " seq " + (item.fromSeq ?? "-") + " -> " + (item.toSeq ?? "-") + " ts " + (item.fromTs || "-") + " -> " + (item.toTs || "-") + " traceId=" + (item.traceId || "-")).join("\n") + : "- 未观察到 terminal 后总耗时增长。"; + const elapsedZeroResets = Array.isArray(sampleMetrics?.turnTimingElapsedZeroResets) ? sampleMetrics.turnTimingElapsedZeroResets : []; + const elapsedZeroResetLines = elapsedZeroResets.length > 0 + ? elapsedZeroResets.slice(0, 80).map((item) => "- " + formatTurnEventDisplayLabel(item) + " totalElapsed zero-reset " + (item.fromValue ?? "-") + " -> " + (item.toValue ?? "-") + " delta=" + (item.delta ?? "-") + " seq " + (item.fromSeq ?? "-") + " -> " + (item.toSeq ?? "-") + " ts " + (item.fromTs || "-") + " -> " + (item.toTs || "-") + " traceId=" + (item.traceId || "-")).join("\n") + : "- 未观察到总耗时从非零跳回 0 秒。"; + const totalElapsedForwardJumps = Array.isArray(sampleMetrics?.turnTimingTotalElapsedForwardJumps) ? sampleMetrics.turnTimingTotalElapsedForwardJumps : []; + const totalElapsedForwardJumpLines = totalElapsedForwardJumps.length > 0 + ? totalElapsedForwardJumps.slice(0, 80).map((item) => "- " + formatTurnEventDisplayLabel(item) + " totalElapsed forward-jump " + (item.fromValue ?? "-") + " -> " + (item.toValue ?? "-") + " delta=" + (item.delta ?? "-") + " sampleDelta=" + (item.sampleDeltaSeconds ?? "-") + " allowed=" + (item.allowedIncreaseSeconds ?? "-") + " seq " + (item.fromSeq ?? "-") + " -> " + (item.toSeq ?? "-") + " ts " + (item.fromTs || "-") + " -> " + (item.toTs || "-") + " traceId=" + (item.traceId || "-")).join("\n") + : "- 未观察到总耗时超出采样间隔的异常前跳。"; + const sawtoothJumps = Array.isArray(sampleMetrics?.turnTimingRecentUpdateSawtoothJumps) + ? sampleMetrics.turnTimingRecentUpdateSawtoothJumps + : nonMonotonic.filter((item) => item.metric === "recentUpdateSeconds" && item.anomaly === "jump"); + const sawtoothJumpLines = sawtoothJumps.length > 0 + ? sawtoothJumps.slice(0, 80).map((item) => "- " + formatTurnEventDisplayLabel(item) + " recentUpdate sawtooth-jump " + (item.fromValue ?? "-") + " -> " + (item.toValue ?? "-") + " delta=" + (item.delta ?? "-") + " sampleDelta=" + (item.sampleDeltaSeconds ?? "-") + " allowed=" + (item.allowedIncreaseSeconds ?? "-") + " seq " + (item.fromSeq ?? "-") + " -> " + (item.toSeq ?? "-") + " ts " + (item.fromTs || "-") + " -> " + (item.toTs || "-") + " traceId=" + (item.traceId || "-")).join("\n") + : "- 未观察到最近更新三角波异常跳增。"; + const recentUpdateSteps = Array.isArray(sampleMetrics?.turnTimingRecentUpdateLargestSteps) + ? sampleMetrics.turnTimingRecentUpdateLargestSteps + : Array.isArray(sampleMetrics?.turnTimingRecentUpdateSteps) + ? sampleMetrics.turnTimingRecentUpdateSteps.filter((item) => Number.isFinite(Number(item.delta))).slice().sort((a, b) => Number(b.delta) - Number(a.delta)).slice(0, 200) + : []; + const stepLines = recentUpdateSteps.length > 0 + ? recentUpdateSteps.slice(0, 80).map((item) => "- " + formatTurnEventDisplayLabel(item) + " recentUpdate step " + (item.fromValue ?? "-") + " -> " + (item.toValue ?? "-") + " delta=" + (item.delta ?? "-") + " sampleDelta=" + (item.sampleDeltaSeconds ?? "-") + " allowed=" + (item.allowedIncreaseSeconds ?? "-") + " excess=" + (item.excessiveIncreaseSeconds ?? 0) + " event=" + (item.event || "-") + " seq " + (item.fromSeq ?? "-") + " -> " + (item.toSeq ?? "-") + " ts " + (item.fromTs || "-") + " -> " + (item.toTs || "-") + " traceId=" + (item.traceId || "-")).join("\n") + : "- 未观察到最近更新相邻采样 step。"; + const recentUpdateResets = Array.isArray(sampleMetrics?.turnTimingRecentUpdateResets) ? sampleMetrics.turnTimingRecentUpdateResets : []; + const resetLines = recentUpdateResets.length > 0 + ? recentUpdateResets.slice(0, 80).map((item) => "- " + formatTurnEventDisplayLabel(item) + " reset " + (item.fromValue ?? "-") + " -> " + (item.toValue ?? "-") + " delta=" + (item.delta ?? "-") + " sampleDelta=" + (item.sampleDeltaSeconds ?? "-") + " seq " + (item.fromSeq ?? "-") + " -> " + (item.toSeq ?? "-") + " ts " + (item.fromTs || "-") + " -> " + (item.toTs || "-") + " traceId=" + (item.traceId || "-")).join("\n") + : "- 未观察到最近更新归零/回落。"; + const disclosureLine = disclosure?.truncated + ? "表格披露:已按 head/tail 有界输出,totalRows=" + disclosure.totalRows + " includedRows=" + disclosure.includedRows + " omittedRows=" + disclosure.omittedRows + ";异常计数在截断前基于全量采样计算。" + : "表格披露:完整输出当前分析窗口的采样点。"; + return disclosureLine + "\n\n" + lines.join("\n") + "\n\n列说明:\n" + columnLines + "\n\n异常事件(仅报表暴露,不做下游 repair;最近更新按三角波模型检测异常跳增):\n" + nonMonotonicLines + "\n\nTerminal 后总耗时增长事件(终态 turn 的总耗时应 sealed,不应继续增长):\n" + terminalGrowthLines + "\n\n总耗时归零跳变事件(例如已显示真实耗时后又变成 0 秒):\n" + elapsedZeroResetLines + "\n\n总耗时异常前跳事件(预期按采样间隔近似递增;例如 14 秒 -> 1137 秒应被列入这里):\n" + totalElapsedForwardJumpLines + "\n\n最近更新 sawtooth jump 事件(预期每秒增长约 1,遇到新活动归零;例如 1 秒 -> 1 分 4 秒应被列入这里):\n" + sawtoothJumpLines + "\n\n最近更新相邻采样 step(按 delta 降序;用于人工识别一秒跳几十秒/一分钟的瞬态):\n" + stepLines + "\n\n最近更新 reset 事件(预期三角波归零,不计为异常):\n" + resetLines; +} + +function formatTurnColumnDisplayLabel(column) { + const base = String(column?.label || column?.id || "-"); + const role = String(column?.pageRole || "unknown"); + const pageId = compactPageId(column?.pageId); + return pageId ? base + "@" + role + "/" + pageId : base + "@" + role; +} + +function formatTurnEventDisplayLabel(item) { + const base = String(item?.columnLabel || item?.columnId || "-"); + const role = String(item?.pageRole || "unknown"); + const pageId = compactPageId(item?.pageId); + return pageId ? base + "@" + role + "/" + pageId : base + "@" + role; +} + +function compactPageId(value) { + if (value === null || value === undefined || value === "") return ""; + const text = String(value); + if (text.length <= 18) return text; + return text.slice(0, 12) + ".." + text.slice(-4); +} + +function formatMetricCell(value) { + if (value === null || value === undefined || !Number.isFinite(Number(value))) return "-"; + return String(Number(value)); +} + +function escapeMarkdownCell(value) { + return String(value ?? "-").replace(/\|/gu, "\\|"); +} + +function 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) + " modes=" + (Array.isArray(item.submitModes) && item.submitModes.length > 0 ? item.submitModes.join(",") : "-") + " failure=" + (item.failureKind || "-") + " statuses=" + (Array.isArray(item.responseStatuses) && item.responseStatuses.length > 0 ? item.responseStatuses.join(",") : "-") + " firstChat=" + (item.firstChatEventAt || "-") + " lastChat=" + (item.lastChatEventAt || "-")).join("\n") + : "- 无 prompt 网络记录。"; + const roundLines = Array.isArray(report.sampleMetrics?.rounds) && report.sampleMetrics.rounds.length > 0 + ? report.sampleMetrics.rounds.map((item) => "- round " + item.promptIndex + " promptHash=" + (item.promptTextHash || "-") + " samples=" + item.sampleCount + " loadingSamples=" + (item.loadingSamples ?? 0) + " maxLoading=" + (item.maxLoadingCount ?? 0) + " loadingOwners=" + (item.loadingOwnerCount ?? 0) + " totalMax=" + (item.maxTotalElapsedSeconds ?? "-") + " totalLast=" + (item.lastTotalElapsedSeconds ?? "-") + " recentMax=" + (item.maxRecentUpdateSeconds ?? "-") + " recentLast=" + (item.lastRecentUpdateSeconds ?? "-") + " totalDecrease=" + (item.turnTimingTotalElapsedDecreaseCount ?? 0) + " totalForwardJump=" + (item.turnTimingTotalElapsedForwardJumpCount ?? 0) + " totalForwardJumpMax=" + (item.turnTimingTotalElapsedForwardJumpMaxSeconds ?? 0) + " terminalGrowth=" + (item.turnTimingTerminalElapsedGrowthCount ?? 0) + " terminalGrowthMax=" + (item.turnTimingTerminalElapsedGrowthMaxSeconds ?? 0) + " recentJump=" + (item.turnTimingRecentUpdateJumpCount ?? 0) + " recentSawtoothJump=" + (item.turnTimingRecentUpdateSawtoothJumpCount ?? item.turnTimingRecentUpdateJumpCount ?? 0) + " recentStep=" + (item.turnTimingRecentUpdateStepCount ?? 0) + " recentMaxIncrease=" + (item.turnTimingRecentUpdateMaxIncreaseSeconds ?? "-") + " recentMaxExcess=" + (item.turnTimingRecentUpdateMaxExcessSeconds ?? 0) + " recentReset=" + (item.turnTimingRecentUpdateResetCount ?? 0) + " diagnostics=" + item.diagnosticSamples + " terminal=" + item.terminalSamples + " finalText=" + item.finalTextSamples).join("\n") + : "- 无轮次指标。"; + const cardMissingElapsedLines = Array.isArray(codeAgentCardTiming.missingElapsed) && codeAgentCardTiming.missingElapsed.length > 0 + ? codeAgentCardTiming.missingElapsed.slice(0, 80).map((item) => "- #" + (item.seq ?? "-") + " " + (item.ts || "-") + " prompt=" + (item.promptIndex ?? "-") + " role=" + (item.pageRole || "-") + " status=" + (item.status || "-") + " traceId=" + (item.traceId || "-") + " messageId=" + (item.messageId || "-") + " preview=" + escapeMarkdownCell(item.textPreview || "")).join("\n") + : "- 未观察到 Code Agent 卡片缺少耗时。"; + const cardMissingRecentLines = Array.isArray(codeAgentCardTiming.missingRecentUpdate) && codeAgentCardTiming.missingRecentUpdate.length > 0 + ? codeAgentCardTiming.missingRecentUpdate.slice(0, 80).map((item) => "- #" + (item.seq ?? "-") + " " + (item.ts || "-") + " prompt=" + (item.promptIndex ?? "-") + " role=" + (item.pageRole || "-") + " status=" + (item.status || "-") + " traceId=" + (item.traceId || "-") + " messageId=" + (item.messageId || "-") + " total=" + (item.totalElapsedSeconds ?? "-") + " preview=" + escapeMarkdownCell(item.textPreview || "")).join("\n") + : "- 未观察到未终态 Code Agent 卡片缺少最近更新。"; + const roundCompletionLines = Array.isArray(roundCompletion.events) && roundCompletion.events.length > 0 + ? roundCompletion.events.slice(0, 80).map((item) => "- #" + (item.seq ?? "-") + " " + (item.ts || "-") + " prompt=" + (item.promptIndex ?? "-") + " role=" + (item.pageRole || "-") + " traceId=" + (item.traceId || "-") + " completionElapsed=" + (item.elapsedSeconds ?? "-") + " preview=" + escapeMarkdownCell(item.preview || "")).join("\n") + : "- 未观察到“轮次完成(总耗时 ...)”trace 行。"; + const roundCompletionMismatchLines = Array.isArray(roundCompletion.elapsedMismatches) && roundCompletion.elapsedMismatches.length > 0 + ? roundCompletion.elapsedMismatches.slice(0, 80).map((item) => "- #" + (item.seq ?? "-") + " " + (item.ts || "-") + " prompt=" + (item.promptIndex ?? "-") + " traceId=" + (item.traceId || "-") + " completion=" + (item.completionElapsedSeconds ?? "-") + " card=" + (item.cardTotalElapsedSeconds ?? "-") + " delta=" + (item.deltaSeconds ?? "-") + " tolerance=" + (item.toleranceSeconds ?? "-")).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 || "-") + " traceId=" + (item.ownerTraceId || "-") + " messageId=" + (item.ownerMessageId || "-") + " sessionId=" + (item.ownerSessionId || "-") + " samples=" + (item.sampleCount ?? 0) + " occurrences=" + (item.occurrenceCount ?? 0) + " maxCount=" + (item.maxSimultaneousCount ?? 0) + " longest=" + (item.longestContinuousSeconds ?? 0) + "s seq=" + (item.firstSeq ?? "-") + ".." + (item.lastSeq ?? "-") + " prompts=" + (Array.isArray(item.promptIndexes) ? item.promptIndexes.join(",") : "-")).join("\n") + : "- 未观察到“加载中”归属。"; + const loadingTimelineLines = Array.isArray(loading.timeline) && loading.timeline.length > 0 + ? loading.timeline.slice(0, 160).map((item) => "- #" + (item.seq ?? "-") + " " + (item.ts || "-") + " prompt=" + (item.promptIndex ?? "-") + " loadingCount=" + (item.loadingCount ?? 0) + " ownerCount=" + (item.ownerCount ?? 0) + " owners=" + ((Array.isArray(item.owners) ? item.owners : []).slice(0, 6).map((owner) => (owner.ownerKind || "-") + ":" + (owner.ownerLabel || "-") + " trace=" + (owner.ownerTraceId || "-") + "x" + (owner.count ?? 0)).join(",") || "-")).join("\n") + : "- 未观察到“加载中”采样点。"; + const sessionRailTitleSampleLines = Array.isArray(sessionRailTitles.samples) && sessionRailTitles.samples.length > 0 + ? sessionRailTitles.samples.slice(0, 80).map((item) => "- #" + (item.seq ?? "-") + " " + (item.ts || "-") + " role=" + (item.pageRole || "-") + " visible=" + (item.visibleCount ?? 0) + " fallback=" + (item.fallbackTitleCount ?? 0) + " ratio=" + (item.fallbackTitleRatio ?? 0) + " examples=" + ((Array.isArray(item.examples) ? item.examples : []).slice(0, 4).map((example) => escapeMarkdownCell(example.titlePreview || example.titleHash || "-")).join(",") || "-")).join("\n") + : "- 未观察到超过一半 fallback 的 session 列表采样点。"; + const sessionRailTitleExampleLines = Array.isArray(sessionRailTitles.examples) && sessionRailTitles.examples.length > 0 + ? sessionRailTitles.examples.slice(0, 80).map((item) => "- firstSeq=" + (item.firstSeq ?? "-") + " role=" + (item.pageRole || "-") + " active=" + String(item.active === true) + " sessionPrefix=" + (item.sessionIdPrefix || "-") + " titleHash=" + (item.titleHash || "-") + " preview=" + escapeMarkdownCell(item.titlePreview || "")).join("\n") + : "- 无 fallback 标题示例。"; + const provenanceLines = Array.isArray(report.pageProvenance?.segments) && report.pageProvenance.segments.length > 0 + ? report.pageProvenance.segments.slice(0, 40).map((item) => "- fingerprint=" + (item.assetFingerprint || "-") + " samples=" + item.sampleCount + " seq=" + (item.firstSeq ?? "-") + ".." + (item.lastSeq ?? "-") + " ts=" + (item.firstAt || "-") + ".." + (item.lastAt || "-") + " scripts=" + (item.scriptCount ?? "-") + " styles=" + (item.stylesheetCount ?? "-") + " urlPaths=" + (Array.isArray(item.urlPaths) ? item.urlPaths.slice(0, 4).join(",") : "-")).join("\n") + : "- 无页面 provenance segment。"; + const ordinaryPerformanceItems = Array.isArray(report.pagePerformance?.sameOriginApiByPath) ? report.pagePerformance.sameOriginApiByPath.filter((item) => item.isLongLivedStream !== true) : []; + const streamPerformanceItems = Array.isArray(report.pagePerformance?.sameOriginApiByPath) ? report.pagePerformance.sameOriginApiByPath.filter((item) => item.isLongLivedStream === true) : []; + const sameOriginApiBudgetMs = Number(report.alertThresholds?.sameOriginApiSlowMs ?? report.pagePerformance?.summary?.budgetMs); + const streamOpenBudgetMs = Number(report.alertThresholds?.longLivedStreamOpenSlowMs); + const performanceLines = ordinaryPerformanceItems.length > 0 + ? ordinaryPerformanceItems.slice(0, 80).map((item) => "- " + item.path + " kind=" + (item.routeKind || "same-origin-api") + " budgetMetric=" + (item.budgetMetric || "durationMs") + " samples=" + item.sampleCount + " p50=" + (item.p50Ms ?? "-") + "ms p75=" + (item.p75Ms ?? "-") + "ms p95=" + (item.p95Ms ?? "-") + "ms max=" + (item.maxMs ?? "-") + "ms >budget=" + (item.overBudgetCount ?? item.overFiveSecondCount ?? 0) + " budgetMs=" + (item.budgetMs ?? sameOriginApiBudgetMs) + " legacy>5s=" + (item.overFiveSecondCount ?? 0) + " window=" + (item.firstAt || "-") + ".." + (item.lastAt || "-")).join("\n") + : "- 无同源 API Resource Timing 样本。"; + const streamPerformanceLines = streamPerformanceItems.length > 0 + ? streamPerformanceItems.slice(0, 80).map((item) => "- " + item.path + " kind=" + (item.routeKind || "same-origin-api-stream") + " samples=" + item.sampleCount + " streamOpenP50=" + (item.streamOpenP50Ms ?? "-") + "ms streamOpenP75=" + (item.streamOpenP75Ms ?? "-") + "ms streamOpenP95=" + (item.streamOpenP95Ms ?? "-") + "ms streamOpenMax=" + (item.streamOpenMaxMs ?? "-") + "ms streamOpen>budget=" + (item.streamOpenOverBudgetCount ?? item.streamOpenOverFiveSecondCount ?? 0) + " streamOpenBudgetMs=" + (item.streamOpenBudgetMs ?? streamOpenBudgetMs) + " streamOpenLegacy>5s=" + (item.streamOpenOverFiveSecondCount ?? 0) + " streamLifetime>5s=" + (item.streamLifetimeOverFiveSecondCount ?? 0) + " lifetimeMax=" + (item.maxMs ?? "-") + "ms window=" + (item.firstAt || "-") + ".." + (item.lastAt || "-")).join("\n") + : "- 无同源长连接 Resource Timing 样本。"; + const 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)) + "…"; +} +`; +} diff --git a/scripts/src/hwlab-node-web-observe-collect.ts b/scripts/src/hwlab-node-web-observe-collect.ts new file mode 100644 index 00000000..fc93ebbf --- /dev/null +++ b/scripts/src/hwlab-node-web-observe-collect.ts @@ -0,0 +1,169 @@ +// SPEC: PJ2026-01040111 长程观测 draft-2026-06-20-p0-passive-web-probe-observer. +// Responsibility: Offline CLI view renderers for HWLAB web-probe observe artifacts. +import { shellQuote } from "./ssh"; + +export type NodeWebProbeObserveCollectView = "files" | "turn-summary" | "trace-frame"; + +export function parseNodeWebProbeObserveCollectView(value: string): NodeWebProbeObserveCollectView { + if (value === "files" || value === "turn-summary" || value === "trace-frame") return value; + throw new Error(`web-probe observe collect --view must be files, turn-summary, or trace-frame; got ${value}`); +} + +export function nodeWebObserveCollectViewNodeScript(input: { + maxFiles: number; + view: NodeWebProbeObserveCollectView; + traceId: string | null; + sampleSeq: number | null; + timestamp: string | null; + turn: number | null; +}): string { + const requestedSampleSeq = input.sampleSeq === null ? "null" : String(input.sampleSeq); + const requestedTurn = input.turn === null ? "null" : String(input.turn); + return `node -e ${shellQuote(` +const fs=require('fs'),path=require('path'),crypto=require('crypto'); +const dir=process.argv[1]; const maxFiles=${input.maxFiles}; const view=${JSON.stringify(input.view)}; const requestedTraceId=${JSON.stringify(input.traceId)}; const requestedSampleSeq=${requestedSampleSeq}; const requestedTimestamp=${JSON.stringify(input.timestamp)}; const requestedTurn=${requestedTurn}; +const sha=(value)=>'sha256:'+crypto.createHash('sha256').update(String(value||'')).digest('hex'); +const short=(value,limit=96)=>{const text=String(value||'').replace(/\\s+/gu,' ').trim(); return text.length>limit?text.slice(0,Math.max(1,limit-1))+'...':text;}; +const textOf=(value)=>String(value?.text||value?.textPreview||value?.preview||''); +const readJson=(rel)=>{try{return JSON.parse(fs.readFileSync(path.join(dir,rel),'utf8'))}catch{return null}}; +const readJsonl=(rel)=>{try{return fs.readFileSync(path.join(dir,rel),'utf8').split(/\\r?\\n/u).filter(Boolean).map((line)=>{try{return JSON.parse(line)}catch{return {parseError:true,rawHash:sha(line)}}})}catch{return []}}; +const out=[]; const walk=(p)=>{for(const ent of fs.readdirSync(p,{withFileTypes:true})){const full=path.join(p,ent.name); if(ent.isDirectory()) walk(full); else out.push(full); if(out.length>=maxFiles) return;}}; +walk(dir); +const files=out.slice(0,maxFiles).map(file=>{const st=fs.statSync(file); const hash=crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex'); return {path:file,relative:path.relative(dir,file),byteCount:st.size,sha256:'sha256:'+hash}}); +const totalBytes=files.reduce((sum,item)=>sum+item.byteCount,0); +if(view==='files'){ + console.log(JSON.stringify({ok:true,command:'web-probe-observe collect',view,stateDir:dir,fileCount:files.length,totalBytes,files,valuesRedacted:true},null,2)); + process.exit(0); +} +const samples=readJsonl('samples.jsonl'); +const control=readJsonl('control.jsonl'); +const manifest=readJson('manifest.json')||{}; +function unique(values){return Array.from(new Set(values.filter(Boolean)));} +function tsMs(value){const ms=Date.parse(String(value||'')); return Number.isFinite(ms)?ms:null} +function promptCommands(){ + const map=new Map(); + for(const item of control){ + if((item.type!=='sendPrompt'&&item.type!=='steer')||(item.phase!=='started'&&item.phase!=='completed'&&item.phase!=='failed')) continue; + const id=item.commandId||item.seq||String(map.size+1); + const existing=map.get(id)||{}; + map.set(id,{...existing,...item,input:{...(existing.input||{}),...(item.input||{})},firstTs:existing.firstTs||item.ts,lastTs:item.ts}); + } + return Array.from(map.values()).filter((item)=>tsMs(item.firstTs)!==null).sort((a,b)=>tsMs(a.firstTs)-tsMs(b.firstTs)); +} +function segmentFor(index,prompts){ + const start=tsMs(prompts[index]?.firstTs); const end=index+1{const ms=tsMs(sample.ts); return ms!==null&&ms>=start&&ms{const ms=tsMs(item.ts); return ms!==null&&ms>=start&&ms0) values.push(total); + } + for(const m of raw.matchAll(/\\b(?:total|elapsed)\\s*=\\s*(?:(\\d{1,2}):)?(\\d{1,2}):(\\d{2})\\b/giu)){ + values.push(Number(m[1]||0)*3600+Number(m[2]||0)*60+Number(m[3]||0)); + } + return values.length?Math.max(...values):null; +} +function parseRecent(text){const m=String(text||'').match(/最近\\s*(\\d+)\\s*(秒|分钟|分|小时)前/u); if(!m)return null; const n=Number(m[1]); return m[2]==='小时'?n*3600:m[2]==='秒'?n:n*60} +function fmtDuration(seconds){if(seconds===null||seconds===undefined||!Number.isFinite(Number(seconds)))return '-'; const value=Math.max(0,Math.round(Number(seconds))); const h=Math.floor(value/3600), m=Math.floor((value%3600)/60), s=value%60; return (h>0?String(h).padStart(2,'0')+':':'')+String(m).padStart(2,'0')+':'+String(s).padStart(2,'0')} +function terminalText(text){return /轮次完成|轮次失败|轮次取消|已记录|final response|sealed final response|turn completed|turn failed|turn canceled|completed|failed|canceled|cancelled|terminal/iu.test(String(text||''))} +function statusFor(items){ + const texts=items.flatMap((sample)=>[...(Array.isArray(sample.turns)?sample.turns:[]),...(Array.isArray(sample.traceRows)?sample.traceRows:[]),...(Array.isArray(sample.messages)?sample.messages:[])].map(textOf)); + const joined=texts.join('\\n'); + const lastTurn=[].concat(...items.map((sample)=>Array.isArray(sample.turns)?sample.turns:[])).slice(-1)[0]||null; + const raw=String(lastTurn?.status||'').toLowerCase(); + if(/cancel/iu.test(joined)||raw.includes('cancel'))return 'canceled'; + if(/failed|error|agentrun:error/iu.test(joined)||raw.includes('fail'))return 'failed'; + if(texts.some(terminalText)||raw.includes('complete'))return 'completed'; + if(/running|运行中|Code Agent\\s*耗时|最近\\s*\\d+/iu.test(joined)||raw.includes('running'))return 'running'; + return 'unknown'; +} +function userMessageFor(items,prompt){ + const hash=prompt?.input?.textHash||null; + for(const sample of items){ + for(const message of Array.isArray(sample.messages)?sample.messages:[]){ + if(hash&&message.textHash===hash)return {preview:short(textOf(message),90),textHash:message.textHash||hash,textBytes:message.textBytes??prompt?.input?.textBytes??null}; + } + } + return {preview:short(prompt?.input?.textPreview||'',90)||'(hash only)',textHash:hash,textBytes:prompt?.input?.textBytes??null}; +} +function finalResponseFor(items,traceId){ + const turns=[].concat(...items.map((sample)=>Array.isArray(sample.turns)?sample.turns:[])); + const candidates=(traceId?turns.filter((turn)=>turn.traceId===traceId):turns).slice().reverse(); + for(const turn of candidates){ + const status=String(turn.status||'').toLowerCase(); const text=textOf(turn); + if(status.includes('running')&&!terminalText(text))continue; + if(!terminalText(text)&&!status.match(/complete|fail|cancel|block/u))continue; + const preview=short(text,180); + return preview?{preview,textHash:turn.textHash||sha(text),textBytes:turn.textBytes??Buffer.byteLength(text),empty:false}:{preview:'(空内容)',textHash:null,textBytes:0,empty:true}; + } + return {preview:'(空内容)',textHash:null,textBytes:0,empty:true}; +} +function turnSummaryRows(){ + const prompts=promptCommands(); + if(prompts.length===0){ + const allTraceIds=traceIdsFromSamples(samples); + return [{round:0,commandId:null,commandType:null,userPreview:'(无 sendPrompt/steer control 记录)',userHash:null,userBytes:null,traceId:allTraceIds[0]||null,status:statusFor(samples),elapsedSeconds:null,recentUpdateSeconds:null,marks:'-',firstSeq:samples[0]?.seq??null,lastSeq:samples[samples.length-1]?.seq??null,lastTs:samples[samples.length-1]?.ts??null,finalResponse:finalResponseFor(samples,allTraceIds[0]||null),valuesRedacted:true}]; + } + return prompts.map((prompt,index)=>{ + const segment=segmentFor(index,prompts); const segmentControls=controlsFor(index,prompts); const ids=traceIdsFromSamples(segment); const traceId=ids[0]||null; const user=userMessageFor(segment,prompt); + const texts=segment.flatMap((sample)=>[...(Array.isArray(sample.turns)?sample.turns:[]),...(Array.isArray(sample.traceRows)?sample.traceRows:[]),...(Array.isArray(sample.messages)?sample.messages:[])].map(textOf)); + const elapsedValues=texts.map(parseElapsed).filter((value)=>value!==null); const recentValues=texts.map(parseRecent).filter((value)=>value!==null); + const marks=unique([prompt.type==='steer'?'steer':null,...segmentControls.map((item)=>item.type==='cancel'?'cancel':item.type==='steer'?'steer':null)]).join(',')||'-'; + return {round:index+1,commandId:prompt.commandId||null,commandType:prompt.type||null,userPreview:user.preview,userHash:user.textHash,userBytes:user.textBytes,traceId,status:statusFor(segment),elapsedSeconds:elapsedValues.length?Math.max(...elapsedValues):null,recentUpdateSeconds:recentValues.length?Math.max(...recentValues):null,marks,firstSeq:segment[0]?.seq??null,lastSeq:segment[segment.length-1]?.seq??null,lastTs:segment[segment.length-1]?.ts??null,finalResponse:finalResponseFor(segment,traceId),valuesRedacted:true}; + }); +} +function pad(value,width){const text=short(value,width); return text+Array(Math.max(0,width-text.length)+1).join(' ')} +function renderTurnSummary(rows){ + const title='Workbench Session '+(samples[samples.length-1]?.routeSessionId||samples[samples.length-1]?.activeSessionId||'-')+' observer '+(manifest.jobId||'-'); + const header='轮次 用户消息 Trace 状态 耗时/最近 标记 Final Response'; + const body=rows.map((row)=>pad(row.round,5)+' '+pad((row.userHash?row.userHash.slice(0,18)+' ':'')+row.userPreview,32)+' '+pad(row.traceId||'-',12)+' '+pad(row.status,10)+' '+pad(fmtDuration(row.elapsedSeconds)+'/'+(row.recentUpdateSeconds===null?'-':String(row.recentUpdateSeconds)+'s'),13)+' '+pad(row.marks,11)+' '+short(row.finalResponse?.preview||'(空内容)',80)).join('\\n'); + return [title,'=======================================================',header,body||'(空内容)','', 'NEXT', ' detail: bun scripts/cli.ts hwlab nodes web-probe observe collect '+(manifest.jobId||'')+' --view trace-frame --trace-id --sample-seq '].join('\\n'); +} +function selectSample(rows){ + if(requestedSampleSeq!==null){const exact=samples.find((sample)=>Number(sample.seq)===requestedSampleSeq); if(exact)return exact;} + if(requestedTimestamp){const target=tsMs(requestedTimestamp); if(target!==null){const before=samples.filter((sample)=>{const ms=tsMs(sample.ts); return ms!==null&&ms<=target}).slice(-1)[0]; if(before)return before;}} + if(requestedTurn!==null&&rows[requestedTurn-1]?.lastSeq!==null){const byTurn=samples.find((sample)=>Number(sample.seq)===Number(rows[requestedTurn-1].lastSeq)); if(byTurn)return byTurn;} + if(requestedTraceId){const byTrace=samples.filter((sample)=>traceIdsFromSamples([sample]).includes(requestedTraceId)).slice(-1)[0]; if(byTrace)return byTrace;} + return samples[samples.length-1]||null; +} +function renderTraceFrame(sample,rows){ + if(!sample)return {ok:false,renderedText:'TRACE_FRAME_BLOCKER\\nno sample matched --sample-seq/--timestamp/--trace-id/--turn',blocker:'sample-not-found'}; + const sampleTraceIds=traceIdsFromSamples([sample]); const traceId=requestedTraceId||sampleTraceIds[0]||rows.find((row)=>row.lastSeq===sample.seq)?.traceId||null; + const traceRows=(Array.isArray(sample.traceRows)?sample.traceRows:[]).filter((row)=>!traceId||row.traceId===traceId||!row.traceId||textOf(row).includes(traceId)); + const turns=(Array.isArray(sample.turns)?sample.turns:[]).filter((turn)=>!traceId||turn.traceId===traceId||textOf(turn).includes(traceId)); + const texts=[...turns.map(textOf),...traceRows.map(textOf)]; + const elapsed=Math.max(-1,...texts.map(parseElapsed).filter((value)=>value!==null)); const recent=Math.max(-1,...texts.map(parseRecent).filter((value)=>value!==null)); + const status=statusFor([sample]); const finalResponse=finalResponseFor([sample],traceId); + const visibleTraceRows=traceRows.slice(-24); + const rowLines=visibleTraceRows.map((row,index)=>{const text=textOf(row); return short((row.status?row.status+' ':'')+text,180)||('row#'+index+' '+(row.textHash||'-'));}); + if(traceRows.length>visibleTraceRows.length) rowLines.unshift('(已省略 '+(traceRows.length-visibleTraceRows.length)+' 条较早 trace rows;需要原始数据请看 samples.jsonl)'); + const visibleTurns=turns.slice(-6); + const assistantLines=visibleTurns.map((turn)=>'助手消息 '+(turn.status||'-')+' '+short(textOf(turn),180)); + if(turns.length>visibleTurns.length) assistantLines.unshift('(已省略 '+(turns.length-visibleTurns.length)+' 条较早 assistant turn summaries)'); + const rendered=['Code Agent 耗时 '+(elapsed>=0?fmtDuration(elapsed):'-')+' 最近 '+(recent>=0?String(recent)+' 秒前':'-')+' ('+status+')','=======================================================','sample seq='+(sample.seq??'-')+' ts='+(sample.ts||'-')+' traceId='+(traceId||'-')+' routeSession='+(sample.routeSessionId||'-')+' activeSession='+(sample.activeSessionId||'-'),...(rowLines.length?rowLines:['(无 trace rows;这是 blocker,不能当业务通过证据)']),...(assistantLines.length?assistantLines:[]),'==========================','Final Response',finalResponse.preview||'(空内容)'].join('\\n'); + return {ok:rowLines.length>0,renderedText:rendered,blocker:rowLines.length>0?null:'trace-rows-missing',sampleSeq:sample.seq??null,traceId,finalResponse,valuesRedacted:true}; +} +const rows=turnSummaryRows(); +if(view==='turn-summary'){ + console.log(JSON.stringify({ok:true,command:'web-probe-observe collect',view,stateDir:dir,turnCount:rows.length,rows:rows.slice(0,80),renderedText:renderTurnSummary(rows),sourceFiles:['samples.jsonl','control.jsonl','analysis/report.json'],valuesRedacted:true},null,2)); + process.exit(0); +} +const frame=renderTraceFrame(selectSample(rows),rows); +console.log(JSON.stringify({ok:frame.ok!==false,command:'web-probe-observe collect',view,stateDir:dir,...frame,sourceFiles:['samples.jsonl','control.jsonl','analysis/report.json'],valuesRedacted:true},null,2)); +`)} "$state_dir"`; +} diff --git a/scripts/src/hwlab-node-web-observe-render.ts b/scripts/src/hwlab-node-web-observe-render.ts new file mode 100644 index 00000000..2836b5dc --- /dev/null +++ b/scripts/src/hwlab-node-web-observe-render.ts @@ -0,0 +1,465 @@ +// SPEC: PJ2026-01040111 long-running Workbench observation. +// Responsibility: CLI text rendering for hwlab nodes web-probe observe status/command/collect. +import type { RenderedCliResult } from "./output"; + +export function withWebObserveStatusRendered(value: Record): RenderedCliResult { + return { + ok: value.ok !== false, + command: typeof value.command === "string" ? value.command : "hwlab nodes web-probe observe status", + contentType: "text/plain", + renderedText: renderWebObserveStatusTable(value), + }; +} + +export function withWebObserveCommandRendered(value: Record): RenderedCliResult { + return { + ok: value.ok !== false, + command: typeof value.command === "string" ? value.command : "hwlab nodes web-probe observe command", + contentType: "text/plain", + renderedText: renderWebObserveCommandTable(value), + }; +} + +export function withWebObserveCollectRendered(value: Record): RenderedCliResult { + return { + ok: value.ok !== false, + command: typeof value.command === "string" ? value.command : "hwlab nodes web-probe observe collect", + contentType: "text/plain", + renderedText: renderWebObserveCollectTable(value), + }; +} + +function renderWebObserveStatusTable(value: Record): string { + const observer = record(value.observer); + const manifest = record(observer?.manifest); + const heartbeat = record(observer?.heartbeat); + const tails = record(observer?.tails); + const result = record(value.result); + const samples = Array.isArray(tails?.samples) ? tails.samples.map(record).filter((item): item is Record => item !== null).slice(-5) : []; + const controls = Array.isArray(tails?.control) ? tails.control.map(record).filter((item): item is Record => item !== null).slice(-5) : []; + const completedControlIds = new Set(controls + .filter((item) => item.phase === "completed" || item.phase === "failed") + .map((item) => webObserveText(item.commandId)) + .filter((item) => item !== "-")); + const activeControl = controls.slice().reverse().find((item) => item.phase === "started" && !completedControlIds.has(webObserveText(item.commandId))) ?? null; + const activeControlAgeSeconds = activeControl === null ? null : Math.max(0, Math.round((Date.now() - Date.parse(webObserveText(activeControl.ts))) / 1000)); + const network = Array.isArray(tails?.network) + ? tails.network.map(record).filter((item): item is Record => item !== null).filter((item) => { + const status = typeof item.status === "number" ? item.status : null; + return status === null || status >= 400 || item.failure !== null && item.failure !== undefined; + }).slice(-5) + : []; + const next = record(value.next); + const heartbeatError = record(heartbeat?.error) ?? record(manifest?.error); + const heartbeatAuth = record(heartbeat?.auth) ?? record(heartbeatError?.auth); + const manifestNetwork = record(manifest?.network); + const manifestBrowser = record(manifestNetwork?.browser); + const manifestProxy = record(manifestNetwork?.proxy); + const lines = [ + `hwlab nodes web-probe observe status (${webObserveText(value.status)})`, + "", + webObserveTable(["ID", "NODE", "LANE", "ALIVE", "PID", "SAMPLE", "UPDATED", "TARGET"], [[ + value.id, + value.node, + value.lane, + observer?.processAlive, + observer?.pid, + heartbeat?.sampleSeq, + webObserveShort(webObserveText(heartbeat?.updatedAt ?? heartbeat?.lastSampleAt), 24), + webObserveShort(`${webObserveText(manifest?.baseUrl)}${webObserveText(manifest?.targetPath) === "-" ? "" : webObserveText(manifest?.targetPath)}`, 52), + ]]), + "", + ...(value.ok === false ? [ + "Blocked detail:", + webObserveTable(["REASON", "EXIT", "TIMEOUT", "STDOUT", "STDERR"], [[ + webObserveShort(webObserveText(value.degradedReason), 48), + result.exitCode, + result.timedOut, + webObserveShort(webObserveText(result.stdoutTail), 120), + webObserveShort(webObserveText(result.stderr), 120), + ]]), + "", + ] : []), + ...(heartbeatError !== null ? [ + "Observer error:", + webObserveTable(["STATUS", "RETRY", "EXHAUSTED", "BROWSER_PROXY", "MESSAGE"], [[ + heartbeat?.status ?? manifest?.status, + heartbeatAuth?.lastRetryLabel, + heartbeatAuth?.retryExhausted, + webObserveShort(webObserveText(manifestBrowser?.proxyMode ?? manifestProxy?.enabled), 32), + webObserveShort(webObserveText(heartbeatError.message ?? heartbeatAuth?.lastError), 120), + ]]), + "", + ] : []), + ...(heartbeatAuth !== null ? [ + "Auth progress:", + webObserveTable(["PHASE", "RETRY", "DELAY_MS", "STATUS", "RETRYABLE", "COOKIE", "EXHAUSTED", "LAST_ERROR"], [[ + heartbeatAuth.phase, + heartbeatAuth.lastRetryLabel, + heartbeatAuth.retryDelayMs, + heartbeatAuth.lastStatusText === undefined ? heartbeatAuth.lastStatus : `${webObserveText(heartbeatAuth.lastStatus)} ${webObserveText(heartbeatAuth.lastStatusText)}`, + heartbeatAuth.retryable, + heartbeatAuth.cookiePresent, + heartbeatAuth.retryExhausted, + webObserveShort(webObserveText(heartbeatAuth.lastError), 80), + ]]), + "", + ] : []), + ...(activeControl !== null ? [ + "Active command:", + webObserveTable(["TYPE", "COMMAND", "AGE_S", "STARTED_AT", "DETAIL"], [[ + activeControl.type, + webObserveShort(webObserveText(activeControl.commandId), 28), + activeControlAgeSeconds, + webObserveShort(webObserveText(activeControl.ts), 24), + webObserveShort(webObserveText(activeControl.detail), 80), + ]]), + "", + ] : []), + "Recent samples:", + webObserveTable(["SEQ", "TS", "PATH", "ROUTE_SESSION", "ACTIVE_SESSION", "MSG", "TRACE"], samples.length > 0 ? samples.map((sample) => [ + sample.seq, + webObserveShort(webObserveText(sample.ts), 24), + webObserveShort(webObserveText(sample.path), 24), + webObserveShort(webObserveText(sample.routeSessionId), 20), + webObserveShort(webObserveText(sample.activeSessionId), 20), + sample.messageCount, + sample.traceRowCount, + ]) : [["-", "-", "-", "-", "-", "-", "-"]]), + "", + "Recent control:", + webObserveTable(["SEQ", "TS", "PHASE", "TYPE", "COMMAND", "RETRY", "DETAIL"], controls.length > 0 ? controls.map((control) => [ + control.seq, + webObserveShort(webObserveText(control.ts), 24), + control.phase, + control.type, + webObserveShort(webObserveText(control.commandId), 28), + control.retry, + webObserveShort(webObserveText(control.detail), 80), + ]) : [["-", "-", "-", "-", "-", "-", "-"]]), + "", + "Network alerts:", + webObserveTable(["TS", "METHOD", "STATUS", "TYPE", "URL_OR_FAILURE"], network.length > 0 ? network.map((item) => [ + webObserveShort(webObserveText(item.ts), 24), + item.method, + item.status, + item.type, + webObserveShort(webObserveText(item.failure ?? item.url), 80), + ]) : [["-", "-", "-", "-", "-"]]), + "", + "Next:", + ]; + for (const key of ["status", "command", "stop", "analyze"] as const) { + const command = webObserveText(next?.[key]); + if (command !== "-") lines.push(` ${command}`); + } + lines.push("", "Disclosure:"); + lines.push(" default view is a bounded table; use observe collect/analyze for artifact-level drill-down."); + return lines.join("\n"); +} + +function renderWebObserveCommandTable(value: Record): string { + const observerCommand = record(value.observerCommand); + const observer = record(value.observer); + const result = record(observer?.result); + const id = webObserveText(value.id); + const full = value.full === true; + const lines = [ + `hwlab nodes web-probe observe command (${webObserveText(value.status)})`, + "", + webObserveTable(["OBSERVER", "COMMAND", "TYPE", "STATUS", "TEXT_BYTES", "TEXT_HASH", "DETAIL"], [[ + id, + value.commandId, + observerCommand?.type, + observer?.ok === false ? "failed" : observer?.completedAt !== undefined ? "completed" : value.status, + observerCommand?.textBytes, + webObserveShort(webObserveText(observerCommand?.textHash), 24), + webObserveShort(webObserveText(result?.mark ?? result?.currentUrl ?? observer?.error ?? observer?.queued), 80), + ]]), + ...(full ? [ + "", + "Full command result:", + "```json", + JSON.stringify(observer ?? {}, null, 2), + "```", + ] : []), + "", + "Next:", + ` bun scripts/cli.ts hwlab nodes web-probe observe status ${id}`, + ` bun scripts/cli.ts hwlab nodes web-probe observe analyze ${id}`, + "", + "Disclosure:", + " default view is a bounded command result; pass --full to expose the complete command result JSON.", + " use observe status/analyze for sampled state.", + ]; + return lines.join("\n"); +} + +function renderWebObserveCollectTable(value: Record): string { + const collect = nullableRecord(value.collect); + if (typeof collect?.renderedText === "string") { + return [ + `hwlab nodes web-probe observe collect (${webObserveText(value.status)})`, + "", + collect.renderedText, + "", + "Source:", + webObserveTable(["ID", "NODE", "LANE", "VIEW", "STATE_DIR"], [[ + value.id, + value.node, + value.lane, + collect.view ?? value.view, + webObserveShort(webObserveText(collect.stateDir), 88), + ]]), + "", + "Disclosure:", + " collect views are rendered from existing samples/control/analysis artifacts; they do not create a new sampling source.", + ].join("\n"); + } + + const result = record(value.result); + const file = record(collect?.file); + const files = Array.isArray(collect?.files) ? collect.files.map(record).filter((item): item is Record => item !== null).slice(0, 20) : []; + const jsonlTail = Array.isArray(file.jsonlTail) ? file.jsonlTail.map(record).filter((item): item is Record => item !== null).slice(-8) : []; + const jsonSummary = record(file.jsonSummary); + const grepSummary = record(file.grep); + const grepLines = Array.isArray(grepSummary?.lines) ? grepSummary.lines.map(record).filter((item): item is Record => item !== null).slice(0, 40) : []; + const jsonContentPreview = file.jsonContent === undefined || file.jsonContent === null ? "" : JSON.stringify(file.jsonContent, null, 2); + const jsonRunnerErrors = Array.isArray(jsonSummary.runnerErrors) ? jsonSummary.runnerErrors.map(record).filter((item): item is Record => item !== null).slice(-8) : []; + const jsonFindings = Array.isArray(jsonSummary.findings) ? jsonSummary.findings.map(record).filter((item): item is Record => item !== null).slice(0, 8) : []; + const jsonFindingDetail = record(jsonSummary.findingDetail); + const jsonFindingSamples = Array.isArray(jsonFindingDetail?.samples) ? jsonFindingDetail.samples.map(record).filter((item): item is Record => item !== null).slice(0, 12) : []; + const lines = [ + `hwlab nodes web-probe observe collect (${webObserveText(value.status)})`, + "", + webObserveTable(["ID", "NODE", "LANE", "MODE", "STATE_DIR"], [[ + value.id, + value.node, + value.lane, + collect?.mode ?? "files", + webObserveShort(webObserveText(collect?.stateDir), 88), + ]]), + "", + ]; + + if (collect === null) { + lines.push( + "Collect command:", + webObserveTable(["REQUESTED", "REASON", "EXIT", "TIMED_OUT", "STDOUT_BYTES", "STDOUT_TAIL", "STDERR"], [[ + webObserveShort(webObserveText(value.requestedFile), 64), + webObserveShort(webObserveText(value.degradedReason), 40), + result.exitCode, + result.timedOut, + result.stdoutBytes, + webObserveShort(webObserveText(result.stdoutTail), 160), + webObserveShort(webObserveText(result.stderr), 160), + ]]), + "", + "Disclosure:", + " collect stdout was not valid JSON; fix the collect command or rerun with a narrower --file after the root cause is visible.", + ); + return lines.join("\n"); + } + + if (collect.mode === "file") { + lines.push( + "File:", + webObserveTable(["RELATIVE", "BYTES", "TRUNC", "SHA256"], [[ + webObserveShort(webObserveText(file.relative), 64), + file.byteCount, + file.truncated, + webObserveShort(webObserveText(file.sha256), 28), + ]]), + "", + ); + if (Object.keys(jsonSummary).length > 0) { + lines.push( + "JSON summary:", + webObserveTable(["KEYS", "COUNTS", "PARSE_ERROR"], [[ + webObserveShort(webObserveText(jsonSummary.topLevelKeys), 80), + webObserveShort(webObserveText(jsonSummary.counts), 80), + webObserveShort(webObserveText(jsonSummary.parseError), 80), + ]]), + "", + ); + } + if (grepSummary !== null) { + lines.push( + "Grep matches:", + webObserveTable(["PATTERN_SHA", "MATCHES", "RETURNED", "TRUNC"], [[ + webObserveShort(webObserveText(grepSummary.patternSha256), 24), + grepSummary.matchCount, + grepSummary.returnedLineCount, + grepSummary.truncated, + ]]), + webObserveTable(["LINE", "MATCH", "TEXT"], grepLines.length > 0 ? grepLines.map((item) => [ + item.lineNo, + item.match === true, + webObserveShort(webObserveText(item.text), 360), + ]) : [["-", "-", "-"]]), + "Grep context:", + grepLines.length > 0 + ? grepLines.map((item) => "- " + webObserveText(item.lineNo) + (item.match === true ? " * " : " ") + webObserveShort(webObserveText(item.text), 500)).join("\n") + : "-", + "", + ); + } + if (jsonContentPreview.length > 0) { + lines.push("JSON content:", webObserveShort(jsonContentPreview, 2400), ""); + } + if (jsonRunnerErrors.length > 0) { + lines.push( + "Runner errors:", + webObserveTable(["TS", "TYPE", "ATTEMPTS", "READY", "MESSAGE"], jsonRunnerErrors.map((item) => [ + webObserveShort(webObserveText(item.ts), 24), + webObserveShort(webObserveText(item.type), 20), + item.attemptCount, + webObserveShort(webObserveText(item.lastReadinessReason), 24), + webObserveShort(webObserveText(item.message ?? item.lastError), 96), + ])), + "", + ); + } + if (jsonFindings.length > 0) { + lines.push( + "Findings:", + webObserveTable(["KIND", "SEVERITY", "COUNT", "SUMMARY"], jsonFindings.map((item) => [ + webObserveShort(webObserveText(item.kind ?? item.id ?? item.code), 48), + item.severity ?? item.level, + item.count ?? item.sampleCount, + webObserveShort(webObserveText(item.summary ?? item.message), 96), + ])), + "", + ); + } + if (jsonFindingDetail !== null) { + lines.push( + "Finding detail:", + webObserveTable(["ID", "SEVERITY", "COUNT", "SAMPLE_SOURCE", "SUMMARY"], [[ + webObserveShort(webObserveText(jsonFindingDetail.id), 48), + jsonFindingDetail.severity, + jsonFindingDetail.count, + jsonFindingDetail.sampleSource, + webObserveShort(webObserveText(jsonFindingDetail.summary), 96), + ]]), + "", + "Finding samples:", + webObserveTable(["SEQ", "TS", "KIND", "FROM", "TO", "DELTA", "SAMPLE_S", "ALLOWED", "EXCESS", "SESSION/PATH", "TRACE", "DETAIL"], jsonFindingSamples.length > 0 ? jsonFindingSamples.map((item) => [ + item.seq ?? item.sampleSeq ?? item.fromSeq, + webObserveShort(webObserveText(item.ts ?? item.sampleTs ?? item.fromTs), 24), + webObserveShort(webObserveText(item.diffKind ?? item.kind ?? item.type ?? item.metric ?? item.anomaly), 28), + webObserveShort(webObserveText(item.fromValue ?? item.from ?? item.control ?? item.controlValue ?? item.beforeValue), 28), + webObserveShort(webObserveText(item.toValue ?? item.to ?? item.observer ?? item.observerValue ?? item.afterValue), 28), + item.delta, + item.sampleDeltaSeconds, + item.allowedIncreaseSeconds, + item.excessiveIncreaseSeconds, + webObserveShort(webObserveText(item.sessionId ?? item.routeSessionId ?? item.activeSessionId ?? item.path ?? item.urlPath ?? item.url), 48), + webObserveShort(webObserveText(item.trace ?? item.traceId ?? item.controlTraceId ?? item.observerTraceId), 48), + webObserveShort(webObserveText(item.detail ?? item.summary ?? item.message ?? item.preview), 96), + ]) : [["-", "-", "-", "-", "-", "-", "-", "-", "-", "-", "-", "-"]]), + "", + ); + } + if (jsonlTail.length > 0) { + lines.push( + "JSONL tail:", + webObserveTable(["TS", "ROLE", "TYPE", "COMMAND", "MSG", "TRACE", "TRACE_IDS", "MSG_STATUS", "LOAD", "ATTEMPTS", "READY", "DOM", "PATH", "MESSAGE"], jsonlTail.map((item) => { + const error = record(item.error); + const attempts = Array.isArray(error.attempts) ? error.attempts : []; + const lastAttempt = attempts.length > 0 ? record(attempts[attempts.length - 1]) : {}; + const rawReadiness = record(lastAttempt.readiness ?? error.navigationReadiness); + const readiness = record(rawReadiness.snapshot ?? rawReadiness); + const domBits = [ + `shell=${readiness.workbenchShellVisible === true ? "Y" : "n"}`, + `create=${readiness.sessionCreateVisible === true ? "Y" : "n"}`, + `input=${readiness.commandInputPresent === true ? "Y" : "n"}`, + `tab=${readiness.activeTabPresent === true ? "Y" : "n"}`, + `login=${readiness.loginVisible === true ? "Y" : "n"}`, + ].join(" "); + return [ + webObserveShort(webObserveText(item.ts), 24), + webObserveShort(webObserveText(item.pageRole ?? item.role), 12), + webObserveShort(webObserveText(item.type), 20), + webObserveShort(webObserveText(item.commandId), 28), + item.messageCount, + item.traceRowCount ?? item.traceEventCount, + webObserveShort(webObserveText(item.traceIds), 48), + webObserveShort(webObserveText(item.messageStatuses), 48), + item.loadingCount, + attempts.length, + webObserveShort(webObserveText(readiness.reason), 24), + webObserveShort(domBits, 48), + webObserveShort(webObserveText(item.path ?? item.urlPath ?? item.url ?? item.currentUrl ?? item.status), 60), + webObserveShort(webObserveText(error.message ?? item.message ?? item.text ?? item.preview), 96), + ]; + })), + "", + ); + } else if (typeof file.content === "string" && file.content.length > 0) { + lines.push("Content preview:", webObserveShort(file.content, 4000), ""); + } + } else { + lines.push( + "Files:", + webObserveTable(["RELATIVE", "BYTES", "SHA256"], files.length > 0 ? files.map((item) => [ + webObserveShort(webObserveText(item.relative), 72), + item.byteCount, + webObserveShort(webObserveText(item.sha256), 28), + ]) : [["-", "-", "-"]]), + "", + ); + } + + if (value.ok === false) { + lines.push( + "Blocked detail:", + webObserveTable(["REASON", "EXIT", "TIMEOUT", "STDERR"], [[ + webObserveShort(webObserveText(collect.reason ?? value.degradedReason), 48), + result.exitCode, + result.timedOut, + webObserveShort(webObserveText(result.stderr), 120), + ]]), + "", + ); + } + lines.push("Disclosure:", " collect is bounded; use --file plus --finding for id-specific drill-down; exact --grep also expands finding samples."); + return lines.join("\n"); +} + +function webObserveTable(headers: string[], rows: unknown[][]): string { + const stringRows = rows.map((row) => headers.map((_, index) => webObserveCell(row[index]))); + const widths = headers.map((header, index) => Math.max(header.length, ...stringRows.map((row) => row[index]?.length ?? 0))); + const renderRow = (row: string[]) => row.map((cell, index) => cell.padEnd(widths[index] ?? cell.length)).join(" ").trimEnd(); + return [renderRow(headers), ...stringRows.map(renderRow)].join("\n"); +} + +function webObserveCell(value: unknown, maxLength = 96): string { + if (value === null || value === undefined) return "-"; + const text = typeof value === "string" ? value : String(value); + const compact = text.replace(/\s+/gu, " ").trim(); + if (compact.length === 0) return "-"; + if (compact.length <= maxLength) return compact; + return `${compact.slice(0, Math.max(1, maxLength - 1))}...`; +} + +function webObserveText(value: unknown): string { + if (value === null || value === undefined || value === "") return "-"; + if (typeof value === "string") return value; + if (typeof value === "number" || typeof value === "boolean") return String(value); + return JSON.stringify(value); +} + +function webObserveShort(value: string, maxLength: number): string { + if (value.length <= maxLength) return value; + if (maxLength <= 1) return value.slice(0, maxLength); + return `${value.slice(0, maxLength - 1)}~`; +} + +function record(value: unknown): Record { + return value && typeof value === "object" && !Array.isArray(value) ? value as Record : {}; +} + +function nullableRecord(value: unknown): Record | null { + return value && typeof value === "object" && !Array.isArray(value) ? value as Record : null; +} diff --git a/scripts/src/hwlab-node-web-observe-runner-actions-source.ts b/scripts/src/hwlab-node-web-observe-runner-actions-source.ts new file mode 100644 index 00000000..6266603f --- /dev/null +++ b/scripts/src/hwlab-node-web-observe-runner-actions-source.ts @@ -0,0 +1,49 @@ +// SPEC: PJ2026-01040111 长程观测 draft-2026-06-20-p0-passive-web-probe-observer. +// Responsibility: Extra control actions injected into the pure-client web-probe observer runner. + +export function nodeWebObserveRunnerCommandActionsSource(): string { + return String.raw` +async function cancelRunningTurn() { + const beforeUrl = currentPageUrl(); + const editor = page.locator("#command-input").last(); + if (await editor.isVisible().catch(() => false)) { + 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(""); + else if (editable) { + await editor.click(); + await page.keyboard.press(process.platform === "darwin" ? "Meta+A" : "Control+A").catch(() => {}); + await page.keyboard.press("Backspace").catch(() => {}); + } + } + const primaryButton = page.locator('#command-submit, [data-testid="command-submit"], [data-testid="composer-submit"], [data-testid="send-command"]').last(); + const button = await primaryButton.isVisible().catch(() => false) + ? primaryButton + : page.locator([ + 'button:has-text("取消")', + 'button:has-text("Cancel")', + '[data-testid*="cancel" i]', + '[aria-label*="cancel" i]', + '[aria-label*="取消"]' + ].join(", ")).last(); + await button.waitFor({ state: "visible", timeout: 15000 }); + const cancelResponsePromise = page.waitForResponse((response) => { + const request = response.request(); + if (request.method().toUpperCase() !== "POST") return false; + try { + const pathname = new URL(response.url()).pathname; + return pathname === "/v1/agent/chat/cancel" || /\/cancel$/u.test(pathname); + } catch { + return false; + } + }, { timeout: 10000 }).catch((error) => ({ waitError: errorSummary(error) })); + await button.click(); + const cancelResponse = await cancelResponsePromise; + await page.waitForTimeout(500); + if (cancelResponse?.waitError) { + return { beforeUrl, afterUrl: currentPageUrl(), cancelSubmit: { status: null, statusText: null, waitError: cancelResponse.waitError, responseObserved: false }, pageId }; + } + return { beforeUrl, afterUrl: currentPageUrl(), cancelSubmit: { status: cancelResponse.status(), statusText: cancelResponse.statusText(), urlPath: safeUrlPath(cancelResponse.url()), responseObserved: true }, pageId }; +} +`; +} diff --git a/scripts/src/hwlab-node-web-observe-runner-source.ts b/scripts/src/hwlab-node-web-observe-runner-source.ts index 82cf01a0..99503fb7 100644 --- a/scripts/src/hwlab-node-web-observe-runner-source.ts +++ b/scripts/src/hwlab-node-web-observe-runner-source.ts @@ -1,5 +1,6 @@ // 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. +// Responsibility: Source string for the pure-client HWLAB web-probe observer runner. +import { nodeWebObserveRunnerCommandActionsSource } from "./hwlab-node-web-observe-runner-actions-source"; export function nodeWebObserveRunnerSource(): string { return String.raw`#!/usr/bin/env node @@ -341,6 +342,8 @@ async function processCommand(command) { 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 "steer": return withObserverSync(await sendPrompt(String(command.text || "")), "steer"); + case "cancel": return withObserverSync(await cancelRunningTurn(), "cancel"); 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"); @@ -1217,6 +1220,8 @@ async function promptSideEffectSnapshot() { }).catch(() => ({ runIds: [], traceIds: [], running: false, executionError: false, messageCount: 0, textBytes: 0, valuesRedacted: true })); } +${nodeWebObserveRunnerCommandActionsSource()} + async function selectProvider(provider) { const target = String(provider || "").trim(); if (!target) throw new Error("selectProvider requires provider name"); @@ -2091,5004 +2096,3 @@ function sleep(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 raw = process.env.UNIDESK_WEB_OBSERVE_ANALYZE_TAIL_SAMPLES; - if (raw === undefined || raw === null || String(raw).trim() === "") return 360; - const parsed = Number(raw); - return Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : 360; -})(); -const alertThresholds = parseAlertThresholds(process.env.UNIDESK_WEB_OBSERVE_ALERT_THRESHOLDS_JSON); -const dataDir = archivePrefix ? path.join(stateDir, "archive") : stateDir; -const dataFile = (name) => path.join(dataDir, archivePrefix ? archivePrefix + "-" + name : name); -const analysisDir = path.join(stateDir, "analysis"); -const reportJsonPath = path.join(analysisDir, "report.json"); -const reportMdPath = path.join(analysisDir, "report.md"); -const jsonlReadIssues = []; -const sourceSamples = await readJsonl(dataFile("samples.jsonl"), { compact: compactSampleForAnalysis, tail: analyzeTailSamples }); -const relatedJsonlTailLimit = analyzeTailSamples > 0 ? Math.max(1200, analyzeTailSamples * 50) : 0; -const smallJsonlTailLimit = analyzeTailSamples > 0 ? Math.max(500, analyzeTailSamples * 8) : 0; -const sourceSampleWindow = sampleTimeWindow(sourceSamples, 60_000); -const sourceControlAll = await readJsonl(dataFile("control.jsonl"), { tail: relatedJsonlTailLimit }); -const analysisFocus = analysisFocusFromControl(sourceControlAll); -const sourceControl = filterRowsByTimeWindow(sourceControlAll, sourceSampleWindow); -const samples = applyAnalysisFocus(sourceSamples, analysisFocus); -const sampleWindow = sampleTimeWindow(samples, 60_000); -const controlWindow = analysisControlWindow(sampleWindow, analysisFocus, 1000); -const control = applyAnalysisFocus(filterRowsByTimeWindow(sourceControlAll, controlWindow), analysisFocus, 1000); -const sourceNetworkAll = await readJsonl(dataFile("network.jsonl"), { tail: relatedJsonlTailLimit }); -const network = applyAnalysisFocus(filterRowsByTimeWindow(sourceNetworkAll, sampleWindow), analysisFocus); -const promptNetworkRows = applyAnalysisFocus(filterRowsByTimeWindow(sourceNetworkAll, controlWindow), analysisFocus); -const consoleEvents = applyAnalysisFocus(filterRowsByTimeWindow(await readJsonl(dataFile("console.jsonl"), { tail: smallJsonlTailLimit }), sampleWindow), analysisFocus); -const errors = applyAnalysisFocus(filterRowsByTimeWindow(await readJsonl(dataFile("errors.jsonl"), { tail: smallJsonlTailLimit }), sampleWindow), analysisFocus); -const artifacts = applyAnalysisFocus(filterRowsByTimeWindow(await readJsonl(dataFile("artifacts.jsonl"), { tail: smallJsonlTailLimit }), sampleWindow), analysisFocus); -const manifest = await readJson(path.join(stateDir, "manifest.json")); -const heartbeat = await readJson(path.join(stateDir, "heartbeat.json")); - -await mkdir(analysisDir, { recursive: true, mode: 0o700 }); -const transitions = buildTransitions(samples); -const sampleMetrics = buildSampleMetrics(samples, control); -const pageProvenance = buildPageProvenanceReport(samples, control, manifest); -const pagePerformance = buildPagePerformanceReport(samples, manifest); -const promptNetwork = buildPromptNetworkReport(control, promptNetworkRows); -const runtimeAlerts = buildRuntimeAlerts(samples, control, network, consoleEvents, errors); -const 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, sourceControlCount: sourceControlAll.length, sampleWindow, focus: analysisFocus, valuesRedacted: true }, - alertThresholds, - manifest: compactManifest(manifest), - heartbeat: compactHeartbeat(heartbeat), - counts: { samples: samples.length, control: control.length, network: network.length, console: consoleEvents.length, errors: errors.length, artifacts: artifacts.length }, - commandTimeline, - transitions, - sampleMetrics, - pageProvenance, - pagePerformance, - 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, pageRole: item.pageRole ?? null, pageId: item.pageId ?? null, pageEpoch: item.pageEpoch ?? null, promptIndex: item.promptIndex, lastPromptIndex: item.lastPromptIndex, firstSeq: item.firstSeq, lastSeq: item.lastSeq, traceId: item.traceId, messageId: item.messageId })), - loading: compactLoadingMetricsForOutput(recentWindow.sampleMetrics.loading), - sessionRailTitles: compactSessionRailTitleMetricsForOutput(recentWindow.sampleMetrics.sessionRailTitles), - }, - pageProvenance: recentWindow.pageProvenance.summary, - pagePerformance: recentWindow.pagePerformance.summary, - 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, - pageRole: item.pageRole ?? null, - pageId: item.pageId ?? null, - pageEpoch: item.pageEpoch ?? null, - promptIndex: item.promptIndex ?? null, - traceId: item.traceId ?? null, - fromSeq: item.fromSeq ?? null, - toSeq: item.toSeq ?? null, - fromTs: item.fromTs ?? null, - toTs: item.toTs ?? null, - fromValue: item.fromValue ?? null, - toValue: item.toValue ?? null, - delta: item.delta ?? null, - sampleDeltaSeconds: item.sampleDeltaSeconds ?? null, - allowedIncreaseSeconds: item.allowedIncreaseSeconds ?? null, - excessiveIncreaseSeconds: item.excessiveIncreaseSeconds ?? null, - })), - turnTimingElapsedZeroResets: recentWindow.sampleMetrics.turnTimingElapsedZeroResets.slice(0, 8).map((item) => ({ - columnLabel: item.columnLabel ?? item.columnId ?? null, - pageRole: item.pageRole ?? null, - pageId: item.pageId ?? null, - pageEpoch: item.pageEpoch ?? null, - promptIndex: item.promptIndex ?? null, - traceId: item.traceId ?? null, - fromSeq: item.fromSeq ?? null, - toSeq: item.toSeq ?? null, - fromTs: item.fromTs ?? null, - toTs: item.toTs ?? null, - fromValue: item.fromValue ?? null, - toValue: item.toValue ?? null, - delta: item.delta ?? null, - anomaly: item.anomaly ?? null, - })), - turnTimingTotalElapsedForwardJumps: recentWindow.sampleMetrics.turnTimingTotalElapsedForwardJumps.slice(0, 8).map((item) => ({ - columnLabel: item.columnLabel ?? item.columnId ?? null, - pageRole: item.pageRole ?? null, - pageId: item.pageId ?? null, - pageEpoch: item.pageEpoch ?? null, - promptIndex: item.promptIndex ?? null, - traceId: item.traceId ?? null, - fromSeq: item.fromSeq ?? null, - toSeq: item.toSeq ?? null, - fromTs: item.fromTs ?? null, - toTs: item.toTs ?? null, - fromValue: item.fromValue ?? null, - toValue: item.toValue ?? null, - delta: item.delta ?? null, - sampleDeltaSeconds: item.sampleDeltaSeconds ?? null, - allowedIncreaseSeconds: item.allowedIncreaseSeconds ?? null, - excessiveIncreaseSeconds: item.excessiveIncreaseSeconds ?? null, - anomaly: item.anomaly ?? null, - })), - pagePerformanceSlowApi: recentWindow.pagePerformance.sameOriginApiByPath.filter((item) => item.isLongLivedStream !== true && Number(item.overBudgetCount ?? item.overFiveSecondCount ?? 0) > 0).slice(0, 5).map((item) => ({ path: item.path, route: item.route, sampleCount: item.sampleCount, p95Ms: item.p95Ms, maxMs: item.maxMs, overBudgetCount: item.overBudgetCount, budgetMs: item.budgetMs, overFiveSecondCount: item.overFiveSecondCount, slowSamples: item.slowSamples })), - archivePagePerformanceSlowApi: pagePerformance.sameOriginApiByPath.filter((item) => item.isLongLivedStream !== true && Number(item.overBudgetCount ?? item.overFiveSecondCount ?? 0) > 0).slice(0, 8).map((item) => ({ path: item.path, route: item.route, sampleCount: item.sampleCount, p95Ms: item.p95Ms, maxMs: item.maxMs, overBudgetCount: item.overBudgetCount, budgetMs: item.budgetMs, overFiveSecondCount: item.overFiveSecondCount, slowSamples: item.slowSamples })), - pagePerformancePartialApi: recentWindow.pagePerformance.sameOriginApiByPath.filter((item) => item.isLongLivedStream !== true && Number(item.partialOverBudgetCount ?? item.partialOverFiveSecondCount ?? 0) > 0).slice(0, 5).map((item) => ({ path: item.path, route: item.route, sampleCount: item.sampleCount, completeTimingSampleCount: item.completeTimingSampleCount, partialTimingSampleCount: item.partialTimingSampleCount, partialOverBudgetCount: item.partialOverBudgetCount, budgetMs: item.partialBudgetMs, partialOverFiveSecondCount: item.partialOverFiveSecondCount, partialSamples: item.partialSamples })), - pagePerformanceSseStreams: recentWindow.pagePerformance.sameOriginApiByPath.filter((item) => item.isLongLivedStream === true).slice(0, 5).map((item) => ({ path: item.path, route: item.route, sampleCount: item.sampleCount, streamOpenSampleCount: item.streamOpenSampleCount, streamOpenP95Ms: item.streamOpenP95Ms, streamOpenMaxMs: item.streamOpenMaxMs, streamOpenOverBudgetCount: item.streamOpenOverBudgetCount, streamOpenBudgetMs: item.streamOpenBudgetMs, streamOpenOverFiveSecondCount: item.streamOpenOverFiveSecondCount, streamLifetimeOverFiveSecondCount: item.streamLifetimeOverFiveSecondCount, slowSamples: item.slowSamples })), - findings: prioritizeFindings(recentWindow.findings).slice(0, 8).map((item) => ({ kind: item.id ?? item.kind ?? item.code, severity: item.severity, count: item.count ?? item.sampleCount ?? null, summary: String(item.summary ?? item.message ?? "").slice(0, 180) })), - traceOrderAnomalies: (recentWindow.sampleMetrics?.traceOrder?.orderAnomalies || []).slice(0, 8).map((item) => ({ - sampleSeq: item.sampleSeq ?? null, - sampleIndex: item.sampleIndex ?? null, - timestamp: item.timestamp ?? null, - pageRole: item.pageRole ?? null, - traceId: item.traceId ?? null, - reasons: item.reasons ?? [], - previousRowIndex: item.previousRowIndex ?? null, - currentRowIndex: item.currentRowIndex ?? null, - previousTotalSeconds: item.previousTotalSeconds ?? null, - currentTotalSeconds: item.currentTotalSeconds ?? null, - previousProjectedSeq: item.previousProjectedSeq ?? null, - currentProjectedSeq: item.currentProjectedSeq ?? null, - previousSourceSeq: item.previousSourceSeq ?? null, - currentSourceSeq: item.currentSourceSeq ?? null, - previousEventSeq: item.previousEventSeq ?? null, - currentEventSeq: item.currentEventSeq ?? null, - previousPreview: String(item.previousPreview || "").slice(0, 120), - currentPreview: String(item.currentPreview || "").slice(0, 120), - })), - traceCompletionNotLast: (recentWindow.sampleMetrics?.traceOrder?.completionNotLast || []).slice(0, 8).map((item) => ({ - sampleSeq: item.sampleSeq ?? null, - sampleIndex: item.sampleIndex ?? null, - timestamp: item.timestamp ?? null, - pageRole: item.pageRole ?? null, - traceId: item.traceId ?? null, - completionRowIndex: item.completionRowIndex ?? null, - laterRowIndex: item.laterRowIndex ?? null, - completionTotalSeconds: item.completionTotalSeconds ?? null, - laterTotalSeconds: item.laterTotalSeconds ?? null, - completionProjectedSeq: item.completionProjectedSeq ?? null, - laterProjectedSeq: item.laterProjectedSeq ?? null, - completionPreview: String(item.completionPreview || "").slice(0, 120), - laterPreview: String(item.laterPreview || "").slice(0, 120), - })), - roundCompletionElapsedMismatches: (recentWindow.sampleMetrics?.codeAgentCardTiming?.roundCompletion?.elapsedMismatches || []).slice(0, 8).map((item) => ({ - seq: item.seq ?? null, - ts: item.ts ?? null, - pageRole: item.pageRole ?? null, - promptIndex: item.promptIndex ?? null, - traceId: item.traceId ?? null, - completionElapsedSeconds: item.completionElapsedSeconds ?? null, - cardTotalElapsedSeconds: item.cardTotalElapsedSeconds ?? null, - deltaSeconds: item.deltaSeconds ?? null, - toleranceSeconds: item.toleranceSeconds ?? null, - })), - codeAgentCardDurationUnderreported: (recentWindow.sampleMetrics?.codeAgentCardTiming?.durationUnderreported || []).slice(0, 8).map((item) => ({ - sampleIndex: item.sampleIndex ?? null, - timestamp: item.timestamp ?? null, - pageRole: item.pageRole ?? null, - traceId: item.traceId ?? null, - cardTotalElapsedSeconds: item.cardTotalElapsedSeconds ?? null, - expectedElapsedSeconds: item.expectedElapsedSeconds ?? null, - deltaSeconds: item.deltaSeconds ?? null, - evidenceKind: item.evidenceKind ?? null, - evidencePreview: String(item.evidencePreview || "").slice(0, 120), - })), - codeAgentCardDurationMismatches: (recentWindow.sampleMetrics?.codeAgentCardTiming?.durationMismatches || []).slice(0, 8).map((item) => ({ - sampleIndex: item.sampleIndex ?? null, - timestamp: item.timestamp ?? null, - pageRole: item.pageRole ?? null, - traceId: item.traceId ?? null, - direction: item.direction ?? null, - cardTotalElapsedSeconds: item.cardTotalElapsedSeconds ?? null, - expectedElapsedSeconds: item.expectedElapsedSeconds ?? null, - signedDeltaSeconds: item.signedDeltaSeconds ?? null, - deltaSeconds: item.deltaSeconds ?? null, - evidenceKind: item.evidenceKind ?? null, - exactEvidence: item.exactEvidence === true, - evidencePreview: String(item.evidencePreview || "").slice(0, 120), - })), - valuesRedacted: true, -})); - -async function readJson(file) { - try { return JSON.parse(await readFile(file, "utf8")); } catch { return null; } -} - -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, - crossPageProjectionDivergenceRedMs: positiveNumber(raw.crossPageProjectionDivergenceRedMs, requiredPositiveThreshold(raw, "visibleLoadingSlowMs")), - source: "yaml-env", - }; -} - -async function readJsonl(file, options = {}) { - const tailLimit = Number.isFinite(Number(options.tail)) && Number(options.tail) > 0 ? Math.floor(Number(options.tail)) : 0; - if (tailLimit > 0) return readJsonlTail(file, tailLimit, options); - const rows = []; - try { - const input = createReadStream(file, { encoding: "utf8" }); - const lines = createInterface({ input, crlfDelay: Infinity }); - let lineNo = 0; - for await (const rawLine of lines) { - lineNo += 1; - const line = String(rawLine || "").trim(); - if (!line) continue; - try { - const parsed = JSON.parse(line); - rows.push(typeof options.compact === "function" ? options.compact(parsed) : parsed); - } catch (error) { - const item = { parseError: true, lineNo, rawHash: sha256(line), errorMessage: limitText(error && error.message ? error.message : String(error), 240) }; - rows.push(item); - if (jsonlReadIssues.length < 50) jsonlReadIssues.push({ file: path.basename(file), kind: "parse-error", lineNo, rawHash: item.rawHash, errorMessage: item.errorMessage }); - } - } - return rows; - } catch (error) { - if (error && error.code === "ENOENT") return []; - if (jsonlReadIssues.length < 50) jsonlReadIssues.push({ file: path.basename(file), kind: "read-error", code: error && error.code ? String(error.code) : null, errorMessage: limitText(error && error.message ? error.message : String(error), 240) }); - return []; - } -} - -async function readJsonlTail(file, limit, options = {}) { - try { - const lines = await readTailLines(file, limit); - const rows = []; - let lineNo = 0; - for (const rawLine of lines) { - lineNo += 1; - const line = String(rawLine || "").trim(); - if (!line) continue; - try { - const parsed = JSON.parse(line); - rows.push(typeof options.compact === "function" ? options.compact(parsed) : parsed); - } catch (error) { - const item = { parseError: true, lineNo, tail: true, rawHash: sha256(line), errorMessage: limitText(error && error.message ? error.message : String(error), 240) }; - rows.push(item); - if (jsonlReadIssues.length < 50) jsonlReadIssues.push({ file: path.basename(file), kind: "parse-error", lineNo, tail: true, rawHash: item.rawHash, errorMessage: item.errorMessage }); - } - } - return rows; - } catch (error) { - if (error && error.code === "ENOENT") return []; - if (jsonlReadIssues.length < 50) jsonlReadIssues.push({ file: path.basename(file), kind: "read-error", tail: true, code: error && error.code ? String(error.code) : null, errorMessage: limitText(error && error.message ? error.message : String(error), 240) }); - return []; - } -} - -async function readTailLines(file, limit) { - const info = await stat(file); - if (!info.size || limit <= 0) return []; - const chunkSize = 64 * 1024; - const maxBytes = Math.max(32 * 1024 * 1024, Math.min(256 * 1024 * 1024, limit * 512 * 1024)); - const chunks = []; - let position = info.size; - let readBytes = 0; - let newlineCount = 0; - while (position > 0 && newlineCount <= limit && readBytes < maxBytes) { - const readSize = Math.min(chunkSize, position, maxBytes - readBytes); - position -= readSize; - const chunk = await readFileSlice(file, position, readSize); - chunks.unshift(chunk); - readBytes += chunk.length; - for (let index = 0; index < chunk.length; index += 1) { - if (chunk[index] === 10) newlineCount += 1; - } - } - if (position > 0 && newlineCount <= limit && jsonlReadIssues.length < 50) { - jsonlReadIssues.push({ file: path.basename(file), kind: "tail-scan-truncated", limit, readBytes, fileBytes: info.size }); - } - const text = Buffer.concat(chunks).toString("utf8"); - let lines = text.split(/\r?\n/u); - if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop(); - if (position > 0 && lines.length > 0) lines.shift(); - return lines.slice(-limit); -} - -async function readFileSlice(file, start, length) { - const chunks = []; - await new Promise((resolve, reject) => { - const stream = createReadStream(file, { start, end: start + length - 1 }); - stream.on("data", (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))); - stream.on("error", reject); - stream.on("end", resolve); - }); - return Buffer.concat(chunks); -} - -function sampleTimeWindow(samples, paddingMs) { - const times = samples - .map((item) => Date.parse(String(item && item.ts || ""))) - .filter((value) => Number.isFinite(value)); - if (times.length === 0) return { startMs: null, endMs: null, startAt: null, endAt: null, paddingMs }; - const startMs = Math.max(0, Math.min(...times) - Math.max(0, paddingMs || 0)); - const endMs = Math.max(...times) + Math.max(0, paddingMs || 0); - return { startMs, endMs, startAt: new Date(startMs).toISOString(), endAt: new Date(endMs).toISOString(), paddingMs }; -} - -function filterRowsByTimeWindow(rows, window) { - if (!window || !Number.isFinite(window.startMs) || !Number.isFinite(window.endMs)) return rows; - return rows.filter((item) => { - const value = item && (item.ts || item.observedAt || item.startedAt || item.finishedAt || item.createdAt || item.updatedAt); - const ms = Date.parse(String(value || "")); - if (!Number.isFinite(ms)) return true; - return ms >= window.startMs && ms <= window.endMs; - }); -} - -function analysisFocusFromControl(control) { - const completedNewSessions = (Array.isArray(control) ? control : []) - .filter((item) => item?.type === "newSession" && item?.phase === "completed" && Number.isFinite(Date.parse(String(item.ts || "")))) - .sort((a, b) => Date.parse(String(a.ts || "")) - Date.parse(String(b.ts || ""))); - const latest = completedNewSessions.at(-1) ?? null; - if (!latest) return { mode: "all", reason: "no-new-session-command", startAt: null, startMs: null, commandId: null, valuesRedacted: true }; - const startMs = Date.parse(String(latest.ts)); - return { - mode: "after-new-session", - reason: "latest-completed-new-session", - startAt: new Date(startMs).toISOString(), - startMs, - commandId: latest.commandId ?? null, - valuesRedacted: true - }; -} - -function applyAnalysisFocus(rows, focus, graceMs = 0) { - if (!focus || !Number.isFinite(focus.startMs)) return rows; - const minMs = focus.startMs - Math.max(0, Number(graceMs) || 0); - return (Array.isArray(rows) ? rows : []).filter((item) => { - const value = item && (item.ts || item.observedAt || item.startedAt || item.finishedAt || item.createdAt || item.updatedAt); - const ms = Date.parse(String(value || "")); - return Number.isFinite(ms) ? ms >= minMs : true; - }); -} - -function analysisControlWindow(sampleWindow, focus, graceMs = 0) { - if (!sampleWindow || !Number.isFinite(sampleWindow.endMs)) return sampleWindow; - if (!focus || !Number.isFinite(focus.startMs)) return sampleWindow; - const startMs = Math.max(0, Math.min(Number(sampleWindow.startMs ?? focus.startMs), focus.startMs - Math.max(0, Number(graceMs) || 0))); - return { - ...sampleWindow, - startMs, - startAt: new Date(startMs).toISOString() - }; -} - -function compactSampleForAnalysis(sample) { - if (!sample || typeof sample !== "object") return sample; - return { - seq: sample.seq ?? null, - ts: sample.ts ?? null, - reason: sample.reason ?? null, - sampleGroupSeq: sample.sampleGroupSeq ?? null, - pageId: sample.pageId ?? null, - pageRole: sample.pageRole ?? null, - commandId: sample.commandId ?? null, - observerInitiated: sample.observerInitiated ?? null, - url: sample.url ?? null, - path: sample.path ?? null, - routeSessionId: sample.routeSessionId ?? null, - activeSessionId: sample.activeSessionId ?? null, - messages: compactDomItems(sample.messages), - traceRows: compactDomItems(sample.traceRows), - loadings: compactLoadingItems(sample.loadings), - sessionRail: compactSessionRail(sample.sessionRail), - turns: compactDomItems(sample.turns), - diagnostics: compactDomItems(sample.diagnostics), - 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" || item.type === "observer-periodic-refresh") - .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)); - const previousDigestByPage = new Map(); - for (const sample of samples) { - const pageKey = samplePageKey(sample); - const previous = previousDigestByPage.get(pageKey); - const next = digestSample(sample); - if (previous && previous.digest !== next && !commandedPromptSeqs.has(sample?.seq) && !nearCommand(sample, commandTimes, alertThresholds.uncommandedStateChangeCommandWindowMs)) { - uncommandedChanges.push({ - ...ref(sample), - previousSeq: previous.sample?.seq ?? null, - previousTs: previous.sample?.timestamp ?? previous.sample?.ts ?? null, - fromDigest: previous.digest, - toDigest: next, - fromMessageCount: previous.sample?.messageCount ?? previous.sample?.messages?.length ?? null, - toMessageCount: sample?.messageCount ?? sample?.messages?.length ?? null, - fromTraceRowCount: Array.isArray(previous.sample?.traceRows) ? previous.sample.traceRows.length : previous.sample?.traceRowCount ?? null, - toTraceRowCount: Array.isArray(sample?.traceRows) ? sample.traceRows.length : sample?.traceRowCount ?? null, - fromPath: previous.sample?.path ?? previous.sample?.urlPath ?? null, - toPath: sample?.path ?? sample?.urlPath ?? null, - detail: "visible digest changed without nearby command", - }); - } - previousDigestByPage.set(pageKey, { digest: next, sample }); - } - if (uncommandedChanges.length > 0) findings.push({ id: "uncommanded-visible-state-change", severity: "amber", summary: "visible message/trace digest changed without a nearby command", count: uncommandedChanges.length, samples: uncommandedChanges.slice(0, 20) }); - const finalFlicker = detectFinalFlicker(samples); - if (finalFlicker.length > 0) findings.push({ id: "final-response-flicker", severity: "red", summary: "message text digest disappeared or switched to diagnostic-like text after non-empty final text", count: finalFlicker.length, samples: finalFlicker.slice(0, 20) }); - const terminalZeroElapsed = detectTerminalZeroElapsed(samples); - if (terminalZeroElapsed.length > 0) findings.push({ id: "turn-terminal-zero-elapsed", severity: "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 or /v1/agent/chat/steer POST response in the sampling window", count: promptFailures.length, rounds: promptFailures.slice(0, 10) }); - const promptSteerRounds = Array.isArray(promptNetwork?.rounds) ? promptNetwork.rounds.filter((item) => item.steerUsed === true) : []; - if (promptSteerRounds.length > 0) findings.push({ id: "prompt-routed-to-steer", severity: "amber", summary: "sendPrompt was submitted through /v1/agent/chat/steer; verify the previous turn was truly in-flight and not an unsealed terminal failure", count: promptSteerRounds.length, rounds: promptSteerRounds.slice(0, 10) }); - const elapsedZeroResets = Array.isArray(sampleMetrics?.turnTimingElapsedZeroResets) ? sampleMetrics.turnTimingElapsedZeroResets : []; - if (elapsedZeroResets.length > 0) findings.push({ id: "turn-timing-total-elapsed-zero-reset", severity: "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 elapsedDecreases = Array.isArray(sampleMetrics?.turnTimingNonMonotonic) - ? sampleMetrics.turnTimingNonMonotonic.filter((item) => item.metric === "totalElapsedSeconds" && item.anomaly !== "zero-reset") - : []; - if (elapsedDecreases.length > 0) findings.push({ id: "turn-timing-total-elapsed-decrease", severity: "red", summary: "Code Agent total elapsed decreased between adjacent samples; total elapsed must be monotonic per turn", count: elapsedDecreases.length, samples: elapsedDecreases.slice(0, 20) }); - const elapsedForwardJumps = Array.isArray(sampleMetrics?.turnTimingTotalElapsedForwardJumps) ? sampleMetrics.turnTimingTotalElapsedForwardJumps : []; - if (elapsedForwardJumps.length > 0) findings.push({ id: "turn-timing-total-elapsed-forward-jump", severity: "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"); - const crossPageProjectionBudgetMs = alertThresholds.crossPageProjectionDivergenceRedMs; - const timedCrossPageProjectionDiffs = annotateCrossPageDiffTiming(crossPageProjectionDiffs); - const persistentCrossPageProjectionDiffs = timedCrossPageProjectionDiffs.filter((item) => Number(item.observedSpanMs ?? 0) > crossPageProjectionBudgetMs); - const transientCrossPageProjectionDiffs = timedCrossPageProjectionDiffs.filter((item) => Number(item.observedSpanMs ?? 0) <= crossPageProjectionBudgetMs); - if (persistentCrossPageProjectionDiffs.length > 0) findings.push({ id: "cross-page-projection-divergence", severity: "red", summary: "control and observer pages saw different projection state for the same sampled session beyond the configured budget", count: persistentCrossPageProjectionDiffs.length, budgetMs: crossPageProjectionBudgetMs, samples: persistentCrossPageProjectionDiffs.slice(0, 20) }); - if (transientCrossPageProjectionDiffs.length > 0) findings.push({ id: "cross-page-projection-transient-divergence", severity: "info", summary: "control and observer pages briefly differed near a sampled transition; retained as transient evidence but not treated as persistent projection failure", count: transientCrossPageProjectionDiffs.length, budgetMs: crossPageProjectionBudgetMs, samples: transientCrossPageProjectionDiffs.slice(0, 20) }); - if (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" && promptSubmitModeForUrl(item?.url) !== null) - .map((item) => { - const failureText = item.failureKind ?? item.failure ?? item.errorText ?? null; - const urlPathValue = urlPath(item.url); - return { - ts: item.ts ?? null, - tsMs: Date.parse(item.ts), - type: item.type ?? null, - status: Number.isFinite(Number(item.status)) ? Number(item.status) : null, - commandId: item.commandId ?? null, - urlPath: urlPathValue, - submitMode: promptSubmitModeForUrl(item.url), - failureKind: failureText ? String(failureText) : null, - errorTextHash: failureText ? sha256(failureText) : null - }; - }) - .filter((item) => Number.isFinite(item.tsMs)) - .sort((a, b) => a.tsMs - b.tsMs); - const rounds = prompts.map((prompt) => { - const startMs = Date.parse(prompt.startedAt || prompt.completedAt || prompt.failedAt || ""); - const endAnchorMs = Date.parse(prompt.completedAt || prompt.failedAt || prompt.startedAt || ""); - const fromMs = Number.isFinite(startMs) ? startMs - 3000 : Number.NEGATIVE_INFINITY; - const toMs = Number.isFinite(endAnchorMs) ? endAnchorMs + 30000 : Number.POSITIVE_INFINITY; - const events = chatEvents.filter((event) => { - if (event.commandId && prompt.commandId && event.commandId === prompt.commandId) return true; - return event.tsMs >= fromMs && event.tsMs <= toMs; - }); - const responses = events.filter((event) => event.type === "response"); - const failures = events.filter((event) => event.type === "requestfailed"); - const responseStatuses = responses.map((event) => event.status).filter((status) => status !== null); - const submitModes = Array.from(new Set(events.map((event) => event.submitMode).filter(Boolean))).sort(); - const chatPostOk = responseStatuses.some((status) => status >= 200 && status < 300) && 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, - submitModes, - steerUsed: submitModes.includes("steer"), - firstChatEventAt: events[0]?.ts ?? null, - lastChatEventAt: events[events.length - 1]?.ts ?? null, - events: events.slice(0, 12).map((event) => ({ ts: event.ts, type: event.type, status: event.status, urlPath: event.urlPath, submitMode: event.submitMode, failureKind: event.failureKind, errorTextHash: event.errorTextHash })) - }; - }); - return { - summary: { - promptCount: rounds.length, - chatPostOk: rounds.filter((item) => item.chatPostOk === true).length, - chatPostFailed: rounds.filter((item) => item.chatPostOk === false).length, - chatPostMissing: rounds.filter((item) => item.failureKind === "missing-response").length - }, - rounds - }; -} - -function promptSubmitModeForUrl(value) { - const pathValue = urlPath(value); - if (pathValue === "/v1/agent/chat") return "chat"; - if (pathValue === "/v1/agent/chat/steer") return "steer"; - return null; -} - -function parseDomDiagnosticSummary(text) { - const value = String(text || ""); - const traceMatch = value.match(/\b(?:trace_id=)?(trc_[A-Za-z0-9_-]+|[a-f0-9]{16,64})\b/iu); - const httpStatusMatch = value.match(/\bHTTP\s+([1-5][0-9]{2})\b/iu); - const idleMatch = value.match(/\bidle\s+(\d+)s\b/iu); - const waitingForMatch = value.match(/\bwaitingFor=([^\s;;,,)]+)/iu); - const lastEventLabelMatch = value.match(/\blastEventLabel=([^\s;;,,)]+)/iu); - const diagnosticCode = httpStatusMatch - ? "http-" + httpStatusMatch[1] - : /turn\s*超过|无新活动/iu.test(value) - ? "turn-idle-no-activity" - : /Failed to fetch/iu.test(value) - ? "failed-to-fetch" - : "diagnostic"; - return { - diagnosticCode, - traceId: traceMatch?.[1] || null, - httpStatus: httpStatusMatch ? Number(httpStatusMatch[1]) : null, - idleSeconds: idleMatch ? Number(idleMatch[1]) : null, - waitingFor: waitingForMatch?.[1] || null, - lastEventLabel: lastEventLabelMatch?.[1] || null - }; -} - -function isDomDiagnosticSampleText(text) { - const value = String(text || "").replace(/\s+/g, " ").trim(); - if (!value) return false; - const strongDiagnostic = [ - /\bHTTP\s+[45][0-9]{2}\b(?:[\s\S]{0,120}\btrace_id=|\b)/iu, - /\btrace_id=(?:trc_[A-Za-z0-9_-]+|[a-f0-9]{16,64})\b/iu, - /workbench\s+turn\s*超过\s*\d+ms\s*无新活动/iu, - /\bturn\s*超过\b[\s\S]{0,120}\b无新活动\b/iu, - /\bprojection-resume:sync-failed\b/iu, - /\bAgentRun\s+GET\b[\s\S]*\/result\b[\s\S]*timed out after\s+\d+ms\b/iu, - /\bFailed to fetch\b/iu, - /\bFailed to load resource\b[\s\S]{0,180}\bstatus of\s+[45][0-9]{2}\b/iu, - /\bserver responded with a status of\s+[45][0-9]{2}\b/iu, - /Code Agent\b[\s\S]{0,120}(?:无法连接上游|请求已结束)/iu - ].some((pattern) => pattern.test(value)); - if (!strongDiagnostic) return false; - const looksLikeToolStdout = /\b(?:stdout|stderr):/iu.test(value) - && /(?:\becho\s+["']?===|\bnode\s+|\.tspy\b|tspy\/|===\s*[A-Za-z0-9_.-]+\s*===)/iu.test(value); - if (!looksLikeToolStdout) return true; - return /\b(?:trace_id=|HTTP\s+[45][0-9]{2}|workbench\s+turn\s*超过|projection-resume:sync-failed|Failed to fetch|Failed to load resource|server responded with a status of\s+[45][0-9]{2}|Code Agent\b[\s\S]{0,120}(?:无法连接上游|请求已结束))\b/iu.test(value); -} - -function buildRuntimeAlerts(samples, control, network, consoleEvents, errors) { - const promptTimes = control - .filter((item) => item.type === "sendPrompt" && item.phase === "completed") - .map((item) => Date.parse(item.ts)) - .filter(Number.isFinite) - .sort((a, b) => a - b); - const observerRefreshTimes = control - .filter((item) => item.type === "observer-periodic-refresh") - .map((item) => Date.parse(item.ts)) - .filter(Number.isFinite) - .sort((a, b) => a - b); - const naturalNetwork = network.filter((item) => item?.observerInitiated !== true); - const httpErrors = naturalNetwork - .filter((item) => item?.type === "response" && Number(item.status) >= 400) - .map((item) => networkAlertEvent(item, promptTimes)); - const requestFailed = naturalNetwork - .filter((item) => item?.type === "requestfailed") - .map((item) => networkAlertEvent(item, promptTimes)); - const significantRequestFailed = requestFailed.filter( - (item) => !isBenignLongLivedStreamClosureAlert(item) && !isObserverRefreshClosureAlert(item, observerRefreshTimes), - ); - const domDiagnostics = []; - const executionErrors = []; - const baselineExecutionErrors = []; - const 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) && !isObserverRefreshClosureAlert(item, observerRefreshTimes)); - const pageErrors = errors.map((item) => ({ - ts: item.ts ?? null, - promptIndex: promptIndexForTs(promptTimes, item.ts), - type: item.type ?? null, - errorName: item.error?.name ?? item.name ?? null, - messageHash: item.error?.message ? sha256(item.error.message) : item.message ? sha256(item.message) : null, - preview: limitText(item.error?.message || item.message || item.error || "", 220) - })); - return { - summary: { - httpErrorCount: httpErrors.length, - requestFailedCount: requestFailed.length, - significantRequestFailedCount: significantRequestFailed.length, - benignLongLivedStreamClosureCount: requestFailed.length - significantRequestFailed.length, - domDiagnosticSampleCount: domDiagnostics.length, - domDiagnosticGroupCount: groupDomDiagnostics(domDiagnostics).length, - executionErrorCount: executionErrors.length, - baselineExecutionErrorCount: baselineExecutionErrors.length, - consoleAlertCount: consoleAlerts.length, - significantConsoleAlertCount: significantConsoleAlerts.length, - pageErrorCount: pageErrors.length, - networkErrorGroupCount: groupNetworkAlerts(httpErrors).length, - requestFailedGroupCount: groupNetworkAlerts(requestFailed).length, - significantRequestFailedGroupCount: groupNetworkAlerts(significantRequestFailed).length, - executionErrorGroupCount: groupExecutionErrors(executionErrors).length, - baselineExecutionErrorGroupCount: groupExecutionErrors(baselineExecutionErrors).length, - consoleAlertGroupCount: groupConsoleAlerts(consoleAlerts).length, - significantConsoleAlertGroupCount: groupConsoleAlerts(significantConsoleAlerts).length - }, - networkHttpErrorsByPath: groupNetworkAlerts(httpErrors), - networkRequestFailedByPath: groupNetworkAlerts(requestFailed), - networkSignificantRequestFailedByPath: groupNetworkAlerts(significantRequestFailed), - domDiagnostics: domDiagnostics.slice(-80), - domDiagnosticsByText: groupDomDiagnostics(domDiagnostics), - domDiagnosticsByFingerprint: groupDomDiagnostics(domDiagnostics).slice(0, 80), - runtimeExecutionErrors: executionErrors.slice(0, 120), - runtimeExecutionErrorsByCode: groupExecutionErrors(executionErrors), - baselineRuntimeExecutionErrors: baselineExecutionErrors.slice(0, 80), - baselineRuntimeExecutionErrorsByCode: groupExecutionErrors(baselineExecutionErrors), - consoleAlerts: consoleAlerts.slice(0, 80), - consoleAlertsByPath: groupConsoleAlerts(consoleAlerts), - significantConsoleAlerts: significantConsoleAlerts.slice(0, 80), - significantConsoleAlertsByPath: groupConsoleAlerts(significantConsoleAlerts), - pageErrors: pageErrors.slice(0, 40) - }; -} - -function groupDomDiagnostics(events) { - const groups = new Map(); - for (const item of events || []) { - const preview = String(item?.preview || "").trim(); - if (!isReportableDomDiagnostic(item, preview)) continue; - const normalizedPreview = normalizeDiagnosticPreview(preview); - const key = [ - item?.diagnosticCode || "", - normalizedPreview - ].join("|"); - const existing = groups.get(key) || { - source: item?.source || null, - sources: new Set(), - diagnosticCode: item?.diagnosticCode || null, - textHash: item?.textHash || null, - normalizedPreview, - preview, - count: 0, - firstAt: item?.ts || null, - lastAt: item?.ts || null, - promptIndexes: new Set(), - traceIds: new Set(), - sampleSeqs: [] - }; - if (item?.source) existing.sources.add(String(item.source)); - existing.count += 1; - existing.firstAt = minIso(existing.firstAt, item?.ts || null); - existing.lastAt = maxIso(existing.lastAt, item?.ts || null); - if (Number.isFinite(Number(item?.promptIndex))) existing.promptIndexes.add(Number(item.promptIndex)); - for (const traceId of extractDiagnosticTraceIds(item, preview)) existing.traceIds.add(traceId); - if (existing.sampleSeqs.length < 12 && item?.seq !== undefined && item?.seq !== null) existing.sampleSeqs.push(item.seq); - groups.set(key, existing); - } - return Array.from(groups.values()) - .map((item) => ({ - source: item.source, - sources: Array.from(item.sources).sort(), - diagnosticCode: item.diagnosticCode, - textHash: item.textHash, - normalizedPreview: item.normalizedPreview, - preview: item.preview, - count: item.count, - firstAt: item.firstAt, - lastAt: item.lastAt, - promptIndexes: Array.from(item.promptIndexes).sort((a, b) => a - b), - traceIds: Array.from(item.traceIds).sort(), - sampleSeqs: item.sampleSeqs - })) - .sort((a, b) => (b.count - a.count) || String(a.firstAt || "").localeCompare(String(b.firstAt || ""))); -} - -function isReportableDomDiagnostic(item, preview) { - if (item?.source === "diagnostic-node" || item?.source === "execution-row") return true; - return /trace_id=|HTTP\s+\d{3}\b|Failed to load resource|ERR_[A-Z_]+|provider-unavailable|AgentRun error|超过\s*\d+\s*ms\s*无新活动|代理暂时无法连接上游|Trace 更新超时|加载失败/iu.test(String(preview || "")); -} - -function normalizeDiagnosticPreview(text) { - return String(text || "") - .replace(/trace_id=[A-Za-z0-9_-]+/gu, "trace_id=:traceId") - .replace(/\btrc_[A-Za-z0-9_-]+\b/gu, "trc_:traceId") - .replace(/\bses_[A-Za-z0-9_-]+\b/gu, "ses_:sessionId") - .replace(/\brun_[A-Za-z0-9_-]+\b/gu, "run_:runId") - .replace(/\bcmd_[A-Za-z0-9_-]+\b/gu, "cmd_:commandId") - .replace(/[!!]+$/gu, "") - .replace(/\s+/gu, " ") - .trim(); -} - -function extractDiagnosticTraceIds(item, preview) { - const ids = new Set(); - if (item?.traceId) ids.add(String(item.traceId)); - const text = String(preview || ""); - for (const match of text.matchAll(/\btrc_[A-Za-z0-9_-]+\b/gu)) ids.add(match[0]); - for (const match of text.matchAll(/trace_id=([A-Za-z0-9_-]+)/gu)) ids.add(match[1]); - return ids; -} - -function minIso(a, b) { - if (!a) return b || null; - if (!b) return a || null; - return Date.parse(a) <= Date.parse(b) ? a : b; -} - -function maxIso(a, b) { - if (!a) return b || null; - if (!b) return a || null; - return Date.parse(a) >= Date.parse(b) ? a : b; -} - -function sampleExecutionErrorCandidates(sample) { - const candidates = []; - const add = (source, items) => { - if (!Array.isArray(items)) return; - for (const item of items) { - const text = String(item?.textPreview || item?.text || item?.preview || "").trim(); - if (!text) continue; - if (!parseExecutionErrorText(text)) continue; - candidates.push({ - source, - text, - traceId: item?.traceId ?? null, - messageId: item?.messageId ?? null, - status: item?.status ?? null - }); - } - }; - add("diagnostic-node", sample?.diagnostics); - add("message", sample?.messages); - add("trace-row", sample?.traceRows); - add("turn", sample?.turns); - const specific = candidates.filter((candidate) => { - const parsed = parseExecutionErrorText(candidate.text); - return parsed && parsed.code !== "error"; - }); - return specific.length > 0 ? specific : candidates; -} - -function parseExecutionErrorText(text) { - const value = String(text || ""); - const agentRunCodeMatch = value.match(/\bagentrun:error:([A-Za-z0-9_.:-]+)/u); - const agentRunText = /\bAgentRun\s+error\b|\bagentrun:error:/iu.test(value); - const providerUnavailable = /\bprovider[-_\s]*unavailable\b/iu.test(value); - if (!agentRunCodeMatch && !agentRunText && !providerUnavailable) return null; - const statusMatch = value.match(/\b(fail(?:ed)?|error|blocked|cancel(?:ed)?)\b/iu); - const traceMatch = value.match(/\btrc_[A-Za-z0-9_-]+\b/u); - const totalMatch = value.match(/\btotal\s*=\s*([0-9]{1,2}:[0-9]{2}(?::[0-9]{2})?)\b/iu) - || value.match(/总耗时\s*[::]?\s*([0-9]{1,2}:[0-9]{2}(?::[0-9]{2})?)/iu); - const agentRunCode = cleanExecutionCode(agentRunCodeMatch?.[1] || ""); - const rawCode = agentRunCode ? "agentrun:error:" + agentRunCode : providerUnavailable ? "provider-unavailable" : "agentrun:error"; - return { - backend: agentRunText || agentRunCodeMatch ? "agentrun" : "unknown", - status: normalizeExecutionStatus(statusMatch?.[1] || "error"), - code: agentRunCode || (providerUnavailable ? "provider-unavailable" : "error"), - rawCode, - totalSeconds: totalMatch ? parseClockDurationSeconds(totalMatch[1]) : null, - traceId: traceMatch?.[0] || null - }; -} - -function cleanExecutionCode(code) { - const value = String(code || "").replace(/(?:AgentRun|Error|Failed).*$/u, "").replace(/[^A-Za-z0-9_.:-].*$/u, ""); - return value || null; -} - -function normalizeExecutionStatus(status) { - const value = String(status || "").toLowerCase(); - if (value === "failed") return "fail"; - if (value === "cancelled" || value === "canceled") return "cancel"; - return value || "error"; -} - -function parseClockDurationSeconds(value) { - const parts = String(value || "").split(":").map((part) => Number(part)); - if (parts.length === 2 && parts.every(Number.isFinite)) return parts[0] * 60 + parts[1]; - if (parts.length === 3 && parts.every(Number.isFinite)) return parts[0] * 3600 + parts[1] * 60 + parts[2]; - return null; -} - -function groupExecutionErrors(events) { - const groups = new Map(); - for (const event of events) { - const key = [event.backend || "-", event.status || "-", event.code || "-"].join(" "); - const group = groups.get(key) || { - backend: event.backend ?? null, - status: event.status ?? null, - code: event.code ?? null, - rawCode: event.rawCode ?? null, - count: 0, - firstAt: event.ts, - lastAt: event.ts, - promptIndexes: [], - traceIds: [], - sources: [] - }; - group.count += 1; - group.lastAt = event.ts; - if (event.promptIndex && !group.promptIndexes.includes(event.promptIndex)) group.promptIndexes.push(event.promptIndex); - if (event.traceId && !group.traceIds.includes(event.traceId)) group.traceIds.push(event.traceId); - if (event.source && !group.sources.includes(event.source)) group.sources.push(event.source); - groups.set(key, group); - } - return Array.from(groups.values()).sort((a, b) => b.count - a.count || String(a.code).localeCompare(String(b.code))); -} - -function consoleAlertEvent(item, promptTimes) { - const text = String(item?.text || ""); - const statusMatch = text.match(/\bstatus\s+of\s+([1-5][0-9]{2})\b/iu) || text.match(/\bHTTP\s+([1-5][0-9]{2})\b/iu); - const location = compactLocation(item.location); - const traceMatch = (location?.urlPath || text).match(/\btrc_[A-Za-z0-9_-]+\b/u); - return { - ts: item.ts ?? null, - promptIndex: promptIndexForTs(promptTimes, item.ts), - type: item.type ?? null, - status: statusMatch ? Number(statusMatch[1]) : null, - urlPath: location?.urlPath || "-", - traceId: traceMatch?.[0] || null, - textHash: item.text ? sha256(item.text) : null, - preview: limitText(text, 220), - location - }; -} - -function groupConsoleAlerts(events) { - const groups = new Map(); - for (const event of events) { - const key = [event.type || "-", event.status ?? "-", event.urlPath || "-"].join(" "); - const group = groups.get(key) || { - type: event.type ?? null, - status: event.status ?? null, - urlPath: event.urlPath || "-", - count: 0, - firstAt: event.ts, - lastAt: event.ts, - promptIndexes: [], - traceIds: [] - }; - group.count += 1; - group.lastAt = event.ts; - if (event.promptIndex && !group.promptIndexes.includes(event.promptIndex)) group.promptIndexes.push(event.promptIndex); - if (event.traceId && !group.traceIds.includes(event.traceId)) group.traceIds.push(event.traceId); - groups.set(key, group); - } - return Array.from(groups.values()).sort((a, b) => b.count - a.count || String(a.urlPath).localeCompare(String(b.urlPath))); -} - -function isBenignLongLivedStreamClosureAlert(event) { - if (event?.urlPath !== "/v1/workbench/events") return false; - if (event.status !== null && event.status !== undefined && Number(event.status) > 0) return false; - const text = String(event.failureKind || event.errorPreview || event.preview || ""); - return /ERR_NETWORK_CHANGED|ERR_ABORTED|net::ERR_NETWORK_CHANGED|net::ERR_ABORTED|aborted|network changed/iu.test(text); -} - -function isObserverRefreshClosureAlert(event, observerRefreshTimes) { - const urlPath = String(event?.urlPath || ""); - if (!["/v1/workbench/events", "/v1/web-performance"].includes(urlPath) && !/^\/v1\/workbench\/traces\/[^/]+\/events$/u.test(urlPath)) return false; - if (event.status !== null && event.status !== undefined && Number(event.status) > 0) return false; - const text = String(event.failureKind || event.errorPreview || event.preview || ""); - if (!/ERR_NETWORK_CHANGED|ERR_ABORTED|ERR_INCOMPLETE_CHUNKED_ENCODING|ERR_INVALID_CHUNKED_ENCODING|net::ERR_NETWORK_CHANGED|net::ERR_ABORTED|aborted|network changed|incomplete chunked|invalid chunked/iu.test(text)) return false; - const ts = Date.parse(String(event.ts || "")); - return Number.isFinite(ts) && observerRefreshTimes.some((refreshTs) => Math.abs(ts - refreshTs) <= 8000); -} - -function networkAlertEvent(item, promptTimes) { - const failureText = item.failureKind ?? item.failure ?? item.errorText ?? null; - return { - ts: item.ts ?? null, - promptIndex: promptIndexForTs(promptTimes, item.ts), - method: String(item.method || "GET").toUpperCase(), - status: Number.isFinite(Number(item.status)) ? Number(item.status) : null, - type: item.type ?? null, - urlPath: urlPath(item.url), - urlHash: item.url ? sha256(item.url) : null, - failureKind: failureText ? String(failureText) : null, - errorTextHash: failureText ? sha256(failureText) : null, - errorPreview: failureText ? limitText(failureText, 160) : null - }; -} - -function groupNetworkAlerts(events) { - const groups = new Map(); - for (const event of events) { - const key = [event.method, event.urlPath, event.status ?? "-", event.type].join(" "); - const group = groups.get(key) || { - method: event.method, - urlPath: event.urlPath, - status: event.status, - type: event.type, - count: 0, - firstAt: event.ts, - lastAt: event.ts, - promptIndexes: [], - failureKinds: [], - errorTextHashes: [] - }; - group.count += 1; - group.lastAt = event.ts; - if (event.promptIndex && !group.promptIndexes.includes(event.promptIndex)) group.promptIndexes.push(event.promptIndex); - if (event.failureKind && !group.failureKinds.includes(event.failureKind)) group.failureKinds.push(event.failureKind); - if (event.errorTextHash && !group.errorTextHashes.includes(event.errorTextHash)) group.errorTextHashes.push(event.errorTextHash); - groups.set(key, group); - } - return Array.from(groups.values()).sort((a, b) => b.count - a.count || String(a.urlPath).localeCompare(String(b.urlPath))); -} - -function isDiagnosticText(text) { - const value = String(text || ""); - return /Failed to (?:fetch|load resource)|request failed|net::ERR_[A-Z0-9_:-]+|server responded with a status of [45][0-9]{2}|HTTP\s+[45][0-9]{2}\b|trace_id=|workbench turn\s*超过|turn\s*超过|无新活动|idle\s+\d+s|waitingFor=|lastEventLabel=|无法连接上游|代理暂时无法连接上游|provider-unavailable|agentrun:error|AgentRun error|projection-resume|sync-failed|durable projection store|realtime-gap|Trace 更新超时|加载失败|请求失败|请求已失败/iu.test(value); -} - -function prioritizeFindings(findings) { - const items = Array.isArray(findings) ? findings : []; - const severityRank = (severity) => { - const value = String(severity || "").toLowerCase(); - if (value === "red") return 0; - if (value === "amber" || value === "warning") return 1; - if (value === "info") return 3; - return 2; - }; - const kindRank = (item) => { - const id = String(item?.id ?? item?.kind ?? item?.code ?? ""); - if (id === "page-performance-slow-same-origin-api") return 0; - if (id === "session-rail-title-fallback-majority") return 0.5; - if (id.startsWith("code-agent-card-")) return 0.8; - if (id.startsWith("round-completion-")) return 0.9; - if (id.startsWith("turn-timing-total-elapsed")) return 1; - if (id.startsWith("turn-timing-terminal-elapsed")) return 1.1; - if (id.startsWith("turn-timing-recent-update")) return 2; - if (id.includes("runtime-execution") || id.includes("prompt-chat-submit-failed")) return 3; - return 10; - }; - return items.slice().sort((left, right) => { - const kindDelta = kindRank(left) - kindRank(right); - if (kindDelta !== 0) return kindDelta; - return severityRank(left?.severity ?? left?.level) - severityRank(right?.severity ?? right?.level); - }); -} - -function isTerminalTraceText(text) { - return /轮次完成|轮次失败|轮次取消|已记录|已完成第\d+轮|final response|sealed final response|turn completed|turn failed|turn canceled|terminal result|\bcompleted\b|\bfailed\b|\bcanceled\b|\bcancelled\b|\bterminal\b|\bdone\b/iu.test(String(text || "")); -} - -function isFinalResultText(text) { - return /已完成第\d+轮|已按第\d+轮完成|final response|sealed final response|最终结果|已完成[::]|smoke\s*测试结果|benchmark|PVC\/workspace|修改文件|Results:/iu.test(String(text || "")); -} - -function buildSampleMetrics(samples, control) { - const promptCommands = buildSendPromptCommandTimeline(control); - const promptTimes = promptCommands.map((item) => item.tsMs); - const timeline = samples.map((sample) => { - const texts = sampleTexts(sample); - const timingTexts = sampleTurnTimingTexts(sample); - const tsMs = Date.parse(sample.ts); - const promptIndex = Number.isFinite(tsMs) ? latestPromptIndex(promptTimes, tsMs) : 0; - const totalElapsedValues = timingTexts.flatMap(parseTotalElapsedSeconds).filter(Number.isFinite); - const recentUpdateValues = timingTexts.flatMap(parseRecentUpdateSeconds).filter(Number.isFinite); - const diagnosticTexts = texts.filter(isDiagnosticText).slice(0, 5); - const terminalTexts = texts.filter(isTerminalTraceText).slice(0, 5); - const finalResultTexts = texts.filter(isFinalResultText).slice(0, 5); - const loadings = Array.isArray(sample.loadings) ? sample.loadings : []; - const loadingOwners = uniqueLoadingOwners(loadings); - return { - seq: sample.seq ?? null, - ts: sample.ts ?? null, - routeSessionId: sample.routeSessionId ?? null, - activeSessionId: sample.activeSessionId ?? null, - promptIndex, - messageCount: Array.isArray(sample.messages) ? sample.messages.length : 0, - traceRowCount: Array.isArray(sample.traceRows) ? sample.traceRows.length : 0, - loadingCount: loadings.length, - loadingOwnerCount: loadingOwners.length, - loadingOwners: loadingOwners.map((item) => ({ ownerKey: item.ownerKey, ownerKind: item.ownerKind, ownerLabel: item.ownerLabel, count: item.count })).slice(0, 12), - sessionRailVisibleCount: Number(sample?.sessionRail?.visibleCount ?? 0), - sessionRailFallbackTitleCount: Number(sample?.sessionRail?.fallbackTitleCount ?? 0), - sessionRailFallbackTitleRatio: Number(sample?.sessionRail?.fallbackTitleRatio ?? 0), - totalElapsedSeconds: totalElapsedValues.length > 0 ? Math.max(...totalElapsedValues) : null, - recentUpdateSeconds: recentUpdateValues.length > 0 ? Math.max(...recentUpdateValues) : null, - terminalSeen: terminalTexts.length > 0, - finalResultTextSeen: finalResultTexts.length > 0, - diagnosticSeen: diagnosticTexts.length > 0, - diagnosticTextHashes: diagnosticTexts.map(sha256).slice(0, 5), - textDigest: digestSample(sample) - }; - }); - const turnTiming = buildTurnTimingTable(samples, timeline); - const traceOrder = buildTraceOrderMetrics(samples, timeline); - const codeAgentCardTiming = buildCodeAgentCardTimingMetrics(samples, timeline, turnTiming); - const codeAgentCardDurationUnderreported = buildCodeAgentCardDurationUnderreportedMetrics(samples, timeline); - const codeAgentCardDurationMismatches = buildCodeAgentCardDurationMismatchMetrics(samples, timeline); - if (codeAgentCardTiming && codeAgentCardTiming.summary) { - codeAgentCardTiming.summary.durationUnderreportedCount = codeAgentCardDurationUnderreported.length; - codeAgentCardTiming.summary.durationMismatchCount = codeAgentCardDurationMismatches.length; - codeAgentCardTiming.durationUnderreported = codeAgentCardDurationUnderreported; - codeAgentCardTiming.durationMismatches = codeAgentCardDurationMismatches; - } - const turnCells = turnTiming.rows.flatMap((row) => Object.values(row.cells || {})); - const turnTimingNonMonotonic = Array.isArray(turnTiming.nonMonotonic) ? turnTiming.nonMonotonic : []; - const turnTimingElapsedZeroResets = Array.isArray(turnTiming.elapsedZeroResets) ? turnTiming.elapsedZeroResets : []; - const turnTimingTotalElapsedForwardJumps = Array.isArray(turnTiming.totalElapsedForwardJumps) ? turnTiming.totalElapsedForwardJumps : []; - const turnTimingRecentUpdateSawtoothJumps = turnTimingNonMonotonic.filter((item) => item.metric === "recentUpdateSeconds" && item.anomaly === "jump"); - const turnTimingTerminalElapsedGrowth = Array.isArray(turnTiming.terminalElapsedGrowth) ? turnTiming.terminalElapsedGrowth : []; - const turnTimingRecentUpdateResets = Array.isArray(turnTiming.recentUpdateResets) ? turnTiming.recentUpdateResets : []; - const turnTimingRecentUpdateSteps = Array.isArray(turnTiming.recentUpdateSteps) ? turnTiming.recentUpdateSteps : []; - const turnTimingTerminalElapsedGrowthDeltas = turnTimingTerminalElapsedGrowth - .map((item) => Number(item.delta)) - .filter((value) => Number.isFinite(value) && value > 0); - const turnTimingRecentUpdateLargestSteps = turnTimingRecentUpdateSteps - .filter((item) => Number.isFinite(Number(item.delta))) - .slice() - .sort((a, b) => Number(b.delta) - Number(a.delta)) - .slice(0, 200); - const turnTimingRecentUpdatePositiveSteps = turnTimingRecentUpdateSteps - .map((item) => Number(item.delta)) - .filter((value) => Number.isFinite(value) && value >= 0); - const turnTimingRecentUpdateExcessSteps = turnTimingRecentUpdateSteps - .map((item) => Number(item.excessiveIncreaseSeconds)) - .filter((value) => Number.isFinite(value) && value > 0); - const withTotal = timeline.filter((item) => item.totalElapsedSeconds !== null).length; - const withRecent = timeline.filter((item) => item.recentUpdateSeconds !== null).length; - const diagnostics = timeline.filter((item) => item.diagnosticSeen).length; - const loading = buildLoadingMetrics(samples, timeline); - const sessionRailTitles = buildSessionRailTitleMetrics(samples, timeline); - const reportTurnTimingRows = boundedTurnTimingRowsForReport(turnTiming.rows); - const reportTimeline = boundedRowsForReport(timeline); - const rounds = buildRoundMetricSummaries(timeline, promptCommands, { - columns: turnTiming.columns, - rows: turnTiming.rows, - nonMonotonic: turnTimingNonMonotonic, - elapsedZeroResets: turnTimingElapsedZeroResets, - totalElapsedForwardJumps: turnTimingTotalElapsedForwardJumps, - terminalElapsedGrowth: turnTimingTerminalElapsedGrowth, - recentUpdateResets: turnTimingRecentUpdateResets, - recentUpdateSteps: turnTimingRecentUpdateSteps - }); - const recentUpdateJumpCount = turnTimingRecentUpdateSawtoothJumps.length; - return { - summary: { - sampleCount: timeline.length, - withTotalElapsed: withTotal, - withRecentUpdate: withRecent, - diagnostics, - loadingSampleCount: loading.summary.loadingSampleCount, - loadingMaxCount: loading.summary.maxSimultaneousCount, - loadingMaxOwnerCount: loading.summary.maxSimultaneousOwnerCount, - loadingOwnerCount: loading.summary.ownerCount, - loadingConcurrentSampleCount: loading.summary.concurrentLoadingSampleCount, - loadingLongestContinuousSeconds: loading.summary.longestContinuousSeconds, - loadingCurrentContinuousSeconds: loading.summary.currentContinuousSeconds, - loadingOverFiveSecondSegmentCount: loading.summary.overFiveSecondSegmentCount, - loadingOverBudgetSegmentCount: loading.summary.overBudgetSegmentCount, - sessionRailSampleCount: sessionRailTitles.summary.sampleCount, - sessionRailVisibleSampleCount: sessionRailTitles.summary.visibleSampleCount, - sessionRailFallbackMajoritySampleCount: sessionRailTitles.summary.majorityFallbackSampleCount, - sessionRailFallbackMaxRatio: sessionRailTitles.summary.maxFallbackRatio, - sessionRailFallbackMaxVisibleCount: sessionRailTitles.summary.maxVisibleCount, - sessionRailFallbackMaxCount: sessionRailTitles.summary.maxFallbackTitleCount, - promptSegments: Math.max(0, promptTimes.length), - rounds: rounds.length, - turnColumns: turnTiming.columns.length, - turnTimingRows: turnTiming.rows.length, - turnCellsWithTotalElapsed: turnCells.filter((item) => item.totalElapsedSeconds !== null).length, - turnCellsWithRecentUpdate: turnCells.filter((item) => item.recentUpdateSeconds !== null).length, - turnTimingNonMonotonicCount: turnTimingNonMonotonic.length, - turnTimingTotalElapsedDecreaseCount: turnTimingNonMonotonic.filter((item) => item.metric === "totalElapsedSeconds").length, - turnTimingTotalElapsedZeroResetCount: turnTimingElapsedZeroResets.length, - turnTimingTotalElapsedForwardJumpCount: turnTimingTotalElapsedForwardJumps.length, - turnTimingTotalElapsedForwardJumpMaxSeconds: maxPositiveDelta(turnTimingTotalElapsedForwardJumps), - turnTimingTerminalElapsedGrowthCount: turnTimingTerminalElapsedGrowth.length, - turnTimingTerminalElapsedGrowthMaxSeconds: turnTimingTerminalElapsedGrowthDeltas.length > 0 ? Math.max(...turnTimingTerminalElapsedGrowthDeltas) : 0, - turnTimingRecentUpdateJumpCount: recentUpdateJumpCount, - turnTimingRecentUpdateSawtoothJumpCount: recentUpdateJumpCount, - turnTimingRecentUpdateStepCount: turnTimingRecentUpdateSteps.length, - turnTimingRecentUpdateMaxIncreaseSeconds: turnTimingRecentUpdatePositiveSteps.length > 0 ? Math.max(...turnTimingRecentUpdatePositiveSteps) : null, - turnTimingRecentUpdateMaxExcessSeconds: turnTimingRecentUpdateExcessSteps.length > 0 ? Math.max(...turnTimingRecentUpdateExcessSteps) : 0, - turnTimingRecentUpdateResetCount: turnTimingRecentUpdateResets.length, - turnTimingRecentUpdateDecreaseCount: turnTimingRecentUpdateResets.length, - codeAgentCardSampleCount: codeAgentCardTiming.summary.cardSampleCount, - codeAgentCardMissingElapsedCount: codeAgentCardTiming.summary.missingElapsedCount, - codeAgentCardMissingRecentUpdateCount: codeAgentCardTiming.summary.missingRecentUpdateCount, - roundCompletionEventCount: codeAgentCardTiming.summary.roundCompletionEventCount, - roundCompletionElapsedMismatchCount: codeAgentCardTiming.summary.roundCompletionElapsedMismatchCount, - roundCompletionFinalResponseMissingCount: codeAgentCardTiming.summary.roundCompletionFinalResponseMissingCount, - roundCompletionPostTimingChangeCount: codeAgentCardTiming.summary.roundCompletionPostTimingChangeCount, - codeAgentCardDurationUnderreportedCount: codeAgentCardTiming.summary.durationUnderreportedCount, - codeAgentCardDurationMismatchCount: codeAgentCardTiming.summary.durationMismatchCount, - traceRowCount: traceOrder.summary.traceRowCount, - traceRowOrderAnomalyCount: traceOrder.summary.orderAnomalyCount, - traceRowCompletionNotLastCount: traceOrder.summary.completionNotLastCount, - roundsWithTurnTimingNonMonotonic: rounds.filter((item) => item.turnTimingNonMonotonicCount > 0).length, - roundsWithTurnTimingTotalElapsedForwardJumps: rounds.filter((item) => item.turnTimingTotalElapsedForwardJumpCount > 0).length, - roundsWithTerminalElapsedGrowth: rounds.filter((item) => item.turnTimingTerminalElapsedGrowthCount > 0).length, - roundsWithRecentUpdateJumps: rounds.filter((item) => item.turnTimingRecentUpdateJumpCount > 0).length - }, - loading, - sessionRailTitles, - codeAgentCardTiming, - traceOrder, - rounds, - turnColumns: turnTiming.columns, - turnTimingTable: reportTurnTimingRows.rows, - turnTimingTableDisclosure: reportTurnTimingRows.disclosure, - turnTimingNonMonotonic, - turnTimingElapsedZeroResets, - turnTimingTotalElapsedForwardJumps, - turnTimingTerminalElapsedGrowth, - turnTimingRecentUpdateSawtoothJumps, - turnTimingRecentUpdateSteps, - turnTimingRecentUpdateLargestSteps, - turnTimingRecentUpdateResets, - timeline: reportTimeline.rows, - timelineDisclosure: reportTimeline.disclosure - }; -} - -function boundedRowsForReport(rows) { - const sourceRows = Array.isArray(rows) ? rows : []; - const maxRows = 1200; - const headRows = 120; - if (sourceRows.length <= maxRows) return { rows: sourceRows, disclosure: { truncated: false, totalRows: sourceRows.length, includedRows: sourceRows.length, omittedRows: 0, headRows: sourceRows.length, tailRows: 0 } }; - const tailRows = Math.max(0, maxRows - headRows); - return { - rows: [...sourceRows.slice(0, headRows), ...sourceRows.slice(-tailRows)], - disclosure: { - truncated: true, - totalRows: sourceRows.length, - includedRows: maxRows, - omittedRows: Math.max(0, sourceRows.length - maxRows), - headRows, - tailRows, - policy: "report-bounded-head-tail; summary metrics are computed before truncation", - valuesRedacted: true - } - }; -} - -function buildSendPromptCommandTimeline(control) { - const byCommand = new Map(); - let ordinal = 0; - for (const item of Array.isArray(control) ? control : []) { - if (item?.type !== "sendPrompt") continue; - const commandId = item.commandId ? String(item.commandId) : "sendPrompt-" + String(ordinal++); - const existing = byCommand.get(commandId) || { - commandId, - startedAt: null, - startedTsMs: null, - completedAt: null, - completedTsMs: null, - failedAt: null, - failedTsMs: null, - textHash: null, - textBytes: null, - }; - if (!existing.textHash && item.input?.textHash) existing.textHash = item.input.textHash; - if (!existing.textBytes && item.input?.textBytes) existing.textBytes = item.input.textBytes; - const tsMs = Date.parse(item.ts); - if (item.phase === "started" && Number.isFinite(tsMs)) { - existing.startedAt = item.ts ?? existing.startedAt; - existing.startedTsMs = tsMs; - } else if (item.phase === "completed" && Number.isFinite(tsMs)) { - existing.completedAt = item.ts ?? existing.completedAt; - existing.completedTsMs = tsMs; - } else if (item.phase === "failed" && Number.isFinite(tsMs)) { - existing.failedAt = item.ts ?? existing.failedAt; - existing.failedTsMs = tsMs; - } - byCommand.set(commandId, existing); - } - return Array.from(byCommand.values()) - .map((item) => { - const tsMs = Number.isFinite(item.startedTsMs) ? item.startedTsMs : Number.isFinite(item.completedTsMs) ? item.completedTsMs : item.failedTsMs; - const ts = Number.isFinite(item.startedTsMs) ? item.startedAt : Number.isFinite(item.completedTsMs) ? item.completedAt : item.failedAt; - return { ...item, ts, tsMs }; - }) - .filter((item) => Number.isFinite(item.tsMs)) - .sort((a, b) => a.tsMs - b.tsMs) - .map((item, index) => ({ ...item, promptIndex: index + 1 })); -} - -function buildSessionRailTitleMetrics(samples, timeline) { - const rows = []; - const examplesByHash = new Map(); - for (let index = 0; index < (Array.isArray(samples) ? samples : []).length; index += 1) { - const sample = samples[index]; - const rail = sample?.sessionRail && typeof sample.sessionRail === "object" ? sample.sessionRail : null; - if (!rail) continue; - const visibleCount = Number(rail.visibleCount ?? 0); - const fallbackTitleCount = Number(rail.fallbackTitleCount ?? 0); - const safeVisibleCount = Number.isFinite(visibleCount) && visibleCount > 0 ? visibleCount : 0; - const safeFallbackTitleCount = Number.isFinite(fallbackTitleCount) && fallbackTitleCount > 0 ? fallbackTitleCount : 0; - const fallbackTitleRatio = safeVisibleCount > 0 ? Number((safeFallbackTitleCount / safeVisibleCount).toFixed(4)) : 0; - const fallbackItems = Array.isArray(rail.fallbackItems) ? rail.fallbackItems : []; - for (const item of fallbackItems) { - const hash = String(item?.titleHash || item?.titlePreview || item?.sessionIdPrefix || "").trim(); - if (!hash || examplesByHash.has(hash)) continue; - examplesByHash.set(hash, { - titleHash: item?.titleHash ?? null, - titlePreview: limitText(String(item?.titlePreview || ""), 160), - sessionIdPrefix: item?.sessionIdPrefix ?? null, - active: item?.active === true, - firstSeq: sample?.seq ?? null, - firstAt: sample?.ts ?? null, - pageRole: sample?.pageRole ?? null, - }); - } - rows.push({ - ...ref(sample), - promptIndex: timeline[index]?.promptIndex ?? 0, - visibleCount: safeVisibleCount, - fallbackTitleCount: safeFallbackTitleCount, - fallbackTitleRatio, - majorityFallback: safeVisibleCount > 0 && safeFallbackTitleCount > safeVisibleCount / 2, - overThreshold: safeVisibleCount > 0 && fallbackTitleRatio > alertThresholds.sessionRailFallbackRatio, - examples: fallbackItems.slice(0, 5).map((item) => ({ - titleHash: item?.titleHash ?? null, - titlePreview: limitText(String(item?.titlePreview || ""), 160), - sessionIdPrefix: item?.sessionIdPrefix ?? null, - active: item?.active === true, - })), - }); - } - const visibleRows = rows.filter((item) => item.visibleCount > 0); - const majorityRows = rows.filter((item) => item.majorityFallback); - const overThresholdRows = rows.filter((item) => item.overThreshold); - const fallbackRows = rows.filter((item) => item.fallbackTitleCount > 0); - const maxFallbackRatio = rows.length > 0 ? Math.max(...rows.map((item) => Number(item.fallbackTitleRatio) || 0)) : 0; - const maxVisibleCount = rows.length > 0 ? Math.max(...rows.map((item) => Number(item.visibleCount) || 0)) : 0; - const maxFallbackTitleCount = rows.length > 0 ? Math.max(...rows.map((item) => Number(item.fallbackTitleCount) || 0)) : 0; - return { - summary: { - sampleCount: rows.length, - visibleSampleCount: visibleRows.length, - fallbackSampleCount: fallbackRows.length, - majorityFallbackSampleCount: majorityRows.length, - overThresholdSampleCount: overThresholdRows.length, - thresholdRatio: alertThresholds.sessionRailFallbackRatio, - maxFallbackRatio, - maxVisibleCount, - maxFallbackTitleCount, - }, - samples: majorityRows.slice(0, 80), - examples: Array.from(examplesByHash.values()).slice(0, 80), - timeline: rows.slice(-200), - valuesRedacted: true - }; -} - -function uniqueLoadingOwners(loadings) { - const groups = new Map(); - for (let index = 0; index < (Array.isArray(loadings) ? loadings : []).length; index += 1) { - const item = loadings[index]; - const ownerKey = loadingOwnerKey(item, index); - const ownerIdentity = loadingOwnerIdentity(item); - const existing = groups.get(ownerKey) || { - ownerKey, - ownerKind: item?.ownerKind ?? "unknown", - ownerLabel: loadingOwnerLabel(item, ownerKey), - ...ownerIdentity, - count: 0, - textHashes: [] - }; - existing.count += 1; - if (item?.textHash && !existing.textHashes.includes(item.textHash)) existing.textHashes.push(item.textHash); - for (const key of ["ownerSessionId", "ownerMessageId", "ownerTraceId"]) { - if (!existing[key] && ownerIdentity[key]) existing[key] = ownerIdentity[key]; - } - groups.set(ownerKey, existing); - } - return Array.from(groups.values()).sort((a, b) => b.count - a.count || String(a.ownerLabel).localeCompare(String(b.ownerLabel))); -} - -function loadingOwnerIdentity(item) { - const owner = item?.owner && typeof item.owner === "object" ? item.owner : {}; - return { - ownerSessionId: owner.sessionId ?? null, - ownerMessageId: owner.messageId ?? null, - ownerTraceId: owner.traceId ?? null, - }; -} - -function loadingOwnerKey(item, index = 0) { - const key = String(item?.ownerKey || "").trim(); - if (key) return key.slice(0, 240); - const owner = item?.owner && typeof item.owner === "object" ? item.owner : {}; - return [ - item?.ownerKind || "unknown", - owner.testId || item?.testId || owner.id || owner.role || owner.className || item?.role || item?.tag || "node", - owner.sessionId || owner.messageId || owner.traceId || item?.textHash || String(index) - ].filter(Boolean).join(":").slice(0, 240); -} - -function loadingOwnerLabel(item, fallback) { - return limitText(String(item?.ownerLabel || item?.owner?.ariaLabel || item?.owner?.testId || item?.owner?.className || fallback || "unknown"), 160); -} - -function buildLoadingMetrics(samples, timeline) { - const events = samples.map((sample, index) => { - const tsMs = Date.parse(sample?.ts); - const loadings = Array.isArray(sample?.loadings) ? sample.loadings : []; - const owners = uniqueLoadingOwners(loadings); - return { - seq: sample?.seq ?? null, - ts: sample?.ts ?? null, - tsMs, - promptIndex: timeline[index]?.promptIndex ?? 0, - routeSessionId: sample?.routeSessionId ?? null, - activeSessionId: sample?.activeSessionId ?? null, - loadingCount: loadings.length, - ownerCount: owners.length, - owners, - ownerKeys: owners.map((item) => item.ownerKey), - ownerLabels: owners.map((item) => item.ownerLabel).slice(0, 8) - }; - }).filter((item) => Number.isFinite(item.tsMs)); - const continuityThresholdMs = loadingContinuityThresholdMs(events); - const segments = buildLoadingSegments(events, continuityThresholdMs, (event) => event.loadingCount, (event) => event.owners) - .sort((a, b) => Number(b.durationSeconds ?? 0) - Number(a.durationSeconds ?? 0) || Number(b.maxCount ?? 0) - Number(a.maxCount ?? 0)); - const ownerMap = new Map(); - for (const event of events) { - for (const owner of event.owners) { - const existing = ownerMap.get(owner.ownerKey) || { - ownerKey: owner.ownerKey, - ownerKind: owner.ownerKind, - ownerLabel: owner.ownerLabel, - ownerSessionId: owner.ownerSessionId ?? null, - ownerMessageId: owner.ownerMessageId ?? null, - ownerTraceId: owner.ownerTraceId ?? null, - sampleCount: 0, - occurrenceCount: 0, - maxSimultaneousCount: 0, - firstAt: event.ts, - lastAt: event.ts, - firstSeq: event.seq, - lastSeq: event.seq, - promptIndexes: new Set(), - events: [] - }; - existing.sampleCount += 1; - existing.occurrenceCount += owner.count; - existing.maxSimultaneousCount = Math.max(existing.maxSimultaneousCount, owner.count); - existing.lastAt = event.ts; - existing.lastSeq = event.seq; - for (const key of ["ownerSessionId", "ownerMessageId", "ownerTraceId"]) { - if (!existing[key] && owner[key]) existing[key] = owner[key]; - } - if (Number.isFinite(Number(event.promptIndex))) existing.promptIndexes.add(Number(event.promptIndex)); - existing.events.push({ ...event, loadingCount: owner.count, owners: [owner] }); - ownerMap.set(owner.ownerKey, existing); - } - } - const owners = Array.from(ownerMap.values()).map((owner) => { - const ownerSegments = buildLoadingSegments(owner.events, continuityThresholdMs, (event) => event.loadingCount, (event) => event.owners); - const longest = ownerSegments.reduce((max, item) => Math.max(max, Number(item.durationSeconds ?? 0)), 0); - return { - ownerKey: owner.ownerKey, - ownerKind: owner.ownerKind, - ownerLabel: owner.ownerLabel, - ownerSessionId: owner.ownerSessionId ?? null, - ownerMessageId: owner.ownerMessageId ?? null, - ownerTraceId: owner.ownerTraceId ?? null, - sampleCount: owner.sampleCount, - occurrenceCount: owner.occurrenceCount, - maxSimultaneousCount: owner.maxSimultaneousCount, - longestContinuousSeconds: longest, - firstAt: owner.firstAt, - lastAt: owner.lastAt, - firstSeq: owner.firstSeq, - lastSeq: owner.lastSeq, - promptIndexes: Array.from(owner.promptIndexes).sort((a, b) => a - b), - segments: ownerSegments.sort((a, b) => Number(b.durationSeconds ?? 0) - Number(a.durationSeconds ?? 0)).slice(0, 8), - valuesRedacted: true - }; - }).sort((a, b) => Number(b.longestContinuousSeconds ?? 0) - Number(a.longestContinuousSeconds ?? 0) || Number(b.occurrenceCount ?? 0) - Number(a.occurrenceCount ?? 0)); - const latest = events[events.length - 1] || null; - const currentSegment = latest && latest.loadingCount > 0 - ? segments.find((segment) => segment.ongoing === true && segment.lastSeq === latest.seq) || null - : null; - const timelineRows = events - .filter((event, index) => event.loadingCount > 0 || (index > 0 && events[index - 1]?.loadingCount > 0)) - .slice(0, 500) - .map((event) => ({ - seq: event.seq, - ts: event.ts, - promptIndex: event.promptIndex, - loadingCount: event.loadingCount, - ownerCount: event.ownerCount, - owners: event.owners.map((owner) => ({ ownerKind: owner.ownerKind, ownerLabel: owner.ownerLabel, ownerSessionId: owner.ownerSessionId ?? null, ownerMessageId: owner.ownerMessageId ?? null, ownerTraceId: owner.ownerTraceId ?? null, count: owner.count })).slice(0, 8) - })); - return { - summary: { - sampleCount: events.length, - loadingSampleCount: events.filter((event) => event.loadingCount > 0).length, - maxSimultaneousCount: events.reduce((max, event) => Math.max(max, event.loadingCount), 0), - maxSimultaneousOwnerCount: events.reduce((max, event) => Math.max(max, event.ownerCount), 0), - concurrentLoadingSampleCount: events.filter((event) => event.loadingCount > 1).length, - ownerCount: owners.length, - segmentCount: segments.length, - overFiveSecondSegmentCount: segments.filter((segment) => Number(segment.durationSeconds ?? 0) > 5).length, - overBudgetSegmentCount: segments.filter((segment) => Number(segment.durationSeconds ?? 0) > alertThresholds.visibleLoadingSlowMs / 1000).length, - budgetSeconds: alertThresholds.visibleLoadingSlowMs / 1000, - longestContinuousSeconds: segments.length > 0 ? Number(segments[0].durationSeconds ?? 0) : 0, - currentContinuousSeconds: currentSegment ? Number(currentSegment.durationSeconds ?? 0) : 0, - continuityThresholdMs, - latestLoadingCount: latest?.loadingCount ?? 0, - latestOwnerCount: latest?.ownerCount ?? 0, - valuesRedacted: true - }, - segments: segments.slice(0, 80), - owners: owners.slice(0, 80), - timeline: timelineRows, - valuesRedacted: true - }; -} - -function loadingContinuityThresholdMs(events) { - const deltas = []; - for (let index = 1; index < events.length; index += 1) { - const delta = events[index].tsMs - events[index - 1].tsMs; - if (Number.isFinite(delta) && delta > 0) deltas.push(delta); - } - if (deltas.length === 0) return 5000; - const sorted = deltas.slice().sort((a, b) => a - b); - const median = sorted[Math.floor(sorted.length / 2)]; - return Math.min(15000, Math.max(1500, Math.round(median * 2.5))); -} - -function buildLoadingSegments(events, continuityThresholdMs, countForEvent, ownersForEvent) { - const segments = []; - let segment = null; - let previousTsMs = null; - for (const event of events) { - const count = Number(countForEvent(event) ?? 0); - const gapOk = previousTsMs === null || !Number.isFinite(event.tsMs) || event.tsMs - previousTsMs <= continuityThresholdMs; - if (count > 0) { - if (!segment || !gapOk) { - if (segment) segments.push(finalizeLoadingSegment(segment, null)); - segment = { - firstAt: event.ts, - lastAt: event.ts, - firstSeq: event.seq, - lastSeq: event.seq, - promptIndexes: new Set(), - ownerKeys: new Set(), - ownerLabels: new Map(), - sampleCount: 0, - maxCount: 0, - ongoing: true - }; - } - segment.lastAt = event.ts; - segment.lastSeq = event.seq; - segment.sampleCount += 1; - segment.maxCount = Math.max(segment.maxCount, count); - if (Number.isFinite(Number(event.promptIndex))) segment.promptIndexes.add(Number(event.promptIndex)); - for (const owner of ownersForEvent(event) || []) { - if (!owner?.ownerKey) continue; - segment.ownerKeys.add(owner.ownerKey); - if (!segment.ownerLabels.has(owner.ownerKey)) segment.ownerLabels.set(owner.ownerKey, { ownerKey: owner.ownerKey, ownerKind: owner.ownerKind, ownerLabel: owner.ownerLabel, count: 0 }); - const label = segment.ownerLabels.get(owner.ownerKey); - label.count += owner.count ?? 1; - } - } else if (segment) { - segment.ongoing = false; - segment.endedAt = event.ts; - segment.endSeq = event.seq; - segments.push(finalizeLoadingSegment(segment, event)); - segment = null; - } - previousTsMs = event.tsMs; - } - if (segment) segments.push(finalizeLoadingSegment(segment, null)); - return segments; -} - -function finalizeLoadingSegment(segment, absentEvent) { - const startMs = Date.parse(segment.firstAt || ""); - const lastMs = Date.parse(segment.lastAt || ""); - const absentMs = Date.parse(absentEvent?.ts || ""); - const durationSeconds = Number.isFinite(startMs) && Number.isFinite(lastMs) && lastMs >= startMs ? Number(((lastMs - startMs) / 1000).toFixed(3)) : 0; - const upperBoundSeconds = Number.isFinite(startMs) && Number.isFinite(absentMs) && absentMs >= startMs ? Number(((absentMs - startMs) / 1000).toFixed(3)) : durationSeconds; - const endedGapSeconds = Number.isFinite(lastMs) && Number.isFinite(absentMs) && absentMs >= lastMs ? Number(((absentMs - lastMs) / 1000).toFixed(3)) : null; - return { - firstAt: segment.firstAt, - lastAt: segment.lastAt, - endedAt: absentEvent?.ts ?? null, - firstSeq: segment.firstSeq, - lastSeq: segment.lastSeq, - endSeq: absentEvent?.seq ?? null, - durationSeconds, - upperBoundSeconds, - endedGapSeconds, - sampleCount: segment.sampleCount, - maxCount: segment.maxCount, - ownerCount: segment.ownerKeys.size, - owners: Array.from(segment.ownerLabels.values()).sort((a, b) => b.count - a.count || String(a.ownerLabel).localeCompare(String(b.ownerLabel))).slice(0, 12), - promptIndexes: Array.from(segment.promptIndexes).sort((a, b) => a - b), - ongoing: absentEvent ? false : segment.ongoing === true, - valuesRedacted: true - }; -} - -function boundedTurnTimingRowsForReport(rows) { - const sourceRows = Array.isArray(rows) ? rows : []; - const maxRows = 1200; - const headRows = 120; - if (sourceRows.length <= maxRows) return { rows: sourceRows, disclosure: { truncated: false, totalRows: sourceRows.length, includedRows: sourceRows.length, omittedRows: 0, headRows: sourceRows.length, tailRows: 0 } }; - const tailRows = Math.max(0, maxRows - headRows); - return { - rows: [...sourceRows.slice(0, headRows), ...sourceRows.slice(-tailRows)], - disclosure: { - truncated: true, - totalRows: sourceRows.length, - includedRows: maxRows, - omittedRows: Math.max(0, sourceRows.length - maxRows), - headRows, - tailRows, - policy: "report-bounded-head-tail; full anomaly counters are computed before truncation", - valuesRedacted: true - } - }; -} - -function buildTurnTimingTable(samples, timeline) { - const columns = []; - const registry = new Map(); - const promptAssignmentByKey = new Map(); - const rows = []; - for (let index = 0; index < samples.length; index += 1) { - const sample = samples[index]; - const timelineItem = timeline[index] || {}; - const cells = {}; - const rawMetrics = turnMetricItems(sample, timelineItem); - const domIndexes = rawMetrics.map((item) => Number(item.domIndex)).filter(Number.isFinite); - const maxDomIndex = domIndexes.length > 0 ? Math.max(...domIndexes) : null; - for (const rawMetric of rawMetrics) { - const scopedKey = turnTimingScopedMetricKey(rawMetric, sample); - const samplePromptIndex = Number(timelineItem.promptIndex ?? 0); - const evidencePromptIndex = inferTurnMetricPromptIndex(rawMetric, samplePromptIndex, maxDomIndex); - if (evidencePromptIndex !== null) { - const existingPromptIndex = promptAssignmentByKey.get(scopedKey); - if (existingPromptIndex === undefined || evidencePromptIndex > existingPromptIndex) promptAssignmentByKey.set(scopedKey, evidencePromptIndex); - } - const assignedPromptIndex = promptAssignmentByKey.get(scopedKey) ?? null; - const metric = { ...rawMetric, key: scopedKey, baseKey: rawMetric.key, promptIndex: assignedPromptIndex, samplePromptIndex, pageEpoch: Number(sample?.pageEpoch ?? rawMetric.pageEpoch ?? 0) || 0 }; - let column = registry.get(metric.key); - if (!column) { - column = { - id: "T" + String(columns.length + 1), - label: "T" + String(columns.length + 1), - keyHash: sha256(metric.key), - source: metric.source, - firstSeq: sample.seq ?? null, - firstTs: sample.ts ?? null, - lastSeq: sample.seq ?? null, - lastTs: sample.ts ?? null, - promptIndex: metric.promptIndex ?? null, - lastPromptIndex: metric.promptIndex ?? null, - traceId: metric.traceId ?? null, - messageId: metric.messageId ?? null, - domIndex: metric.domIndex ?? null, - pageRole: metric.pageRole ?? sample.pageRole ?? null, - pageId: metric.pageId ?? sample.pageId ?? null, - pageEpoch: metric.pageEpoch ?? null - }; - registry.set(metric.key, column); - columns.push(column); - } else { - column.lastSeq = sample.seq ?? null; - column.lastTs = sample.ts ?? null; - if (column.source !== "turn" && metric.source === "turn") column.source = "turn"; - if (metric.promptIndex && column.promptIndex !== metric.promptIndex) column.promptIndex = metric.promptIndex; - column.lastPromptIndex = metric.promptIndex ?? column.lastPromptIndex ?? null; - if (!column.traceId && metric.traceId) column.traceId = metric.traceId; - if (!column.messageId && metric.messageId) column.messageId = metric.messageId; - } - cells[column.id] = { - totalElapsedSeconds: metric.totalElapsedSeconds, - recentUpdateSeconds: metric.recentUpdateSeconds, - status: metric.status ?? null, - promptIndex: metric.promptIndex ?? null, - source: metric.source, - pageRole: metric.pageRole ?? sample.pageRole ?? null, - pageId: metric.pageId ?? sample.pageId ?? null, - pageEpoch: metric.pageEpoch ?? null, - sampleGroupSeq: sample.sampleGroupSeq ?? null, - traceId: metric.traceId ?? null, - messageId: metric.messageId ?? null, - textHash: metric.textHash ?? null, - samplePromptIndex: metric.samplePromptIndex ?? null - }; - } - rows.push({ - ts: sample.ts ?? null, - seq: sample.seq ?? null, - sampleGroupSeq: sample.sampleGroupSeq ?? null, - pageRole: sample.pageRole ?? null, - pageId: sample.pageId ?? null, - pageEpoch: Number(sample?.pageEpoch ?? 0) || 0, - promptIndex: timelineItem.promptIndex ?? 0, - routeSessionId: sample.routeSessionId ?? null, - activeSessionId: sample.activeSessionId ?? null, - cells - }); - } - const timingEvents = detectTurnTimingNonMonotonic(columns, rows); - return { - columns, - rows, - nonMonotonic: timingEvents.anomalies, - elapsedZeroResets: timingEvents.elapsedZeroResets, - totalElapsedForwardJumps: timingEvents.totalElapsedForwardJumps, - terminalElapsedGrowth: timingEvents.terminalElapsedGrowth, - recentUpdateResets: timingEvents.recentUpdateResets, - recentUpdateSteps: timingEvents.recentUpdateSteps - }; -} - -function turnTimingScopedMetricKey(metric, sample) { - return [ - metric?.key ?? "unknown", - metric?.pageRole ?? sample?.pageRole ?? "unknown-role", - metric?.pageId ?? sample?.pageId ?? "unknown-page", - Number(sample?.pageEpoch ?? metric?.pageEpoch ?? 0) || 0 - ].join("|page:"); -} - -function inferTurnMetricPromptIndex(metric, samplePromptIndex, maxDomIndex) { - const promptIndex = Number(samplePromptIndex); - if (!Number.isFinite(promptIndex) || promptIndex <= 0) return null; - if (metric?.source === "aggregate") return promptIndex; - if (isActiveTurnStatus(metric?.status)) return promptIndex; - const domIndex = Number(metric?.domIndex); - if (!isTerminalTurnStatus(metric?.status) && Number.isFinite(domIndex) && Number.isFinite(maxDomIndex) && domIndex === maxDomIndex && metric?.recentUpdateSeconds !== null && metric?.recentUpdateSeconds !== undefined) return promptIndex; - return null; -} - -function detectTurnTimingNonMonotonic(columns, rows) { - const anomalies = []; - const elapsedZeroResets = []; - const totalElapsedForwardJumps = []; - const terminalElapsedGrowth = []; - const 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 ?? null, - samplePromptIndex: row.promptIndex ?? null, - source: cell.source ?? column.source ?? null, - pageRole: cell.pageRole ?? column.pageRole ?? null, - pageId: cell.pageId ?? column.pageId ?? null, - pageEpoch: cell.pageEpoch ?? row.pageEpoch ?? column.pageEpoch ?? null, - valuesRedacted: true - }; - anomalies.push(event); - if (anomaly === "zero-reset") elapsedZeroResets.push(event); - } - if (previous && metric === "totalElapsedSeconds" && current > previous.value) { - const sampleDeltaSeconds = elapsedSecondsBetween(previous.ts, row.ts); - const delta = current - previous.value; - const allowedIncreaseSeconds = sampleDeltaSeconds + alertThresholds.turnTimingSampleSlackSeconds; - const terminalTransition = !isTerminalTurnStatus(previous.status) && isTerminalTurnStatus(cell.status); - if (delta > allowedIncreaseSeconds && !terminalTransition) { - totalElapsedForwardJumps.push({ - columnId: column.id, - columnLabel: column.label, - metric, - anomaly: "forward-jump", - expectedPattern: "total-elapsed-increase-should-match-browser-sample-interval", - fromSeq: previous.seq, - fromTs: previous.ts, - fromValue: previous.value, - fromStatus: previous.status ?? null, - toSeq: row.seq ?? null, - toTs: row.ts ?? null, - toValue: current, - toStatus: cell.status ?? null, - delta, - sampleDeltaSeconds, - allowedIncreaseSeconds, - excessiveIncreaseSeconds: Number((delta - allowedIncreaseSeconds).toFixed(3)), - traceId: cell.traceId ?? column.traceId ?? null, - messageId: cell.messageId ?? column.messageId ?? null, - promptIndex: cell.promptIndex ?? null, - samplePromptIndex: row.promptIndex ?? null, - source: cell.source ?? column.source ?? null, - pageRole: cell.pageRole ?? column.pageRole ?? null, - pageId: cell.pageId ?? column.pageId ?? null, - pageEpoch: cell.pageEpoch ?? row.pageEpoch ?? column.pageEpoch ?? null, - valuesRedacted: true - }); - } - } - if (metric === "totalElapsedSeconds" && isTerminalTurnStatus(cell.status)) { - if (previousTerminalTotal && current > previousTerminalTotal.value) { - terminalElapsedGrowth.push({ - columnId: column.id, - columnLabel: column.label, - metric, - anomaly: "terminal-growth", - expectedPattern: "terminal-total-elapsed-sealed", - fromSeq: previousTerminalTotal.seq, - fromTs: previousTerminalTotal.ts, - fromValue: previousTerminalTotal.value, - fromStatus: previousTerminalTotal.status, - toSeq: row.seq ?? null, - toTs: row.ts ?? null, - toValue: current, - toStatus: cell.status ?? null, - delta: current - previousTerminalTotal.value, - traceId: cell.traceId ?? column.traceId ?? null, - messageId: cell.messageId ?? column.messageId ?? null, - promptIndex: cell.promptIndex ?? null, - samplePromptIndex: row.promptIndex ?? null, - source: cell.source ?? column.source ?? null, - pageRole: cell.pageRole ?? column.pageRole ?? null, - pageId: cell.pageId ?? column.pageId ?? null, - pageEpoch: cell.pageEpoch ?? row.pageEpoch ?? column.pageEpoch ?? null, - valuesRedacted: true - }); - } - previousTerminalTotal = { value: current, seq: row.seq ?? null, ts: row.ts ?? null, status: cell.status ?? null }; - } - if (previous && metric === "recentUpdateSeconds") { - const elapsedMs = Date.parse(String(row.ts ?? "")) - Date.parse(String(previous.ts ?? "")); - const elapsedSeconds = Number.isFinite(elapsedMs) && elapsedMs >= 0 ? elapsedMs / 1000 : null; - const increase = current - previous.value; - const allowedIncrease = elapsedSeconds === null - ? alertThresholds.turnTimingSampleSlackSeconds - : Math.max(alertThresholds.turnTimingSampleSlackSeconds, elapsedSeconds + alertThresholds.turnTimingSampleSlackSeconds); - const excessiveIncrease = increase > allowedIncrease ? increase - allowedIncrease : 0; - recentUpdateSteps.push({ - columnId: column.id, - columnLabel: column.label, - metric: "recentUpdateSeconds", - event: increase < 0 ? "reset" : excessiveIncrease > 0 ? "jump" : "increase", - expectedPattern: "sawtooth-increase-or-reset", - fromSeq: previous.seq, - fromTs: previous.ts, - fromValue: previous.value, - toSeq: row.seq ?? null, - toTs: row.ts ?? null, - toValue: current, - delta: increase, - sampleDeltaSeconds: elapsedSeconds, - allowedIncreaseSeconds: allowedIncrease, - excessiveIncreaseSeconds: excessiveIncrease, - traceId: cell.traceId ?? column.traceId ?? null, - messageId: cell.messageId ?? column.messageId ?? null, - promptIndex: cell.promptIndex ?? null, - samplePromptIndex: row.promptIndex ?? null, - source: cell.source ?? column.source ?? null, - pageRole: cell.pageRole ?? column.pageRole ?? null, - pageId: cell.pageId ?? column.pageId ?? null, - pageEpoch: cell.pageEpoch ?? row.pageEpoch ?? column.pageEpoch ?? null, - valuesRedacted: true - }); - if (increase < 0) { - recentUpdateResets.push({ - columnId: column.id, - columnLabel: column.label, - metric: "recentUpdateSeconds", - event: "reset", - fromSeq: previous.seq, - fromTs: previous.ts, - fromValue: previous.value, - toSeq: row.seq ?? null, - toTs: row.ts ?? null, - toValue: current, - delta: increase, - sampleDeltaSeconds: elapsedSeconds, - traceId: cell.traceId ?? column.traceId ?? null, - messageId: cell.messageId ?? column.messageId ?? null, - promptIndex: cell.promptIndex ?? null, - samplePromptIndex: row.promptIndex ?? null, - source: cell.source ?? column.source ?? null, - pageRole: cell.pageRole ?? column.pageRole ?? null, - pageId: cell.pageId ?? column.pageId ?? null, - pageEpoch: cell.pageEpoch ?? row.pageEpoch ?? column.pageEpoch ?? null, - valuesRedacted: true - }); - } - if (excessiveIncrease > 0) { - anomalies.push({ - columnId: column.id, - columnLabel: column.label, - metric: "recentUpdateSeconds", - anomaly: "jump", - expectedPattern: "sawtooth-increase-or-reset", - fromSeq: previous.seq, - fromTs: previous.ts, - fromValue: previous.value, - toSeq: row.seq ?? null, - toTs: row.ts ?? null, - toValue: current, - delta: increase, - sampleDeltaSeconds: elapsedSeconds, - allowedIncreaseSeconds: allowedIncrease, - excessiveIncreaseSeconds: excessiveIncrease, - traceId: cell.traceId ?? column.traceId ?? null, - messageId: cell.messageId ?? column.messageId ?? null, - promptIndex: cell.promptIndex ?? null, - samplePromptIndex: row.promptIndex ?? null, - source: cell.source ?? column.source ?? null, - pageRole: cell.pageRole ?? column.pageRole ?? null, - pageId: cell.pageId ?? column.pageId ?? null, - pageEpoch: cell.pageEpoch ?? row.pageEpoch ?? column.pageEpoch ?? null, - valuesRedacted: true - }); - } - } - previousByMetric.set(metric, { value: current, seq: row.seq ?? null, ts: row.ts ?? null, status: cell.status ?? null }); - } - } - } - return { anomalies, elapsedZeroResets, totalElapsedForwardJumps, terminalElapsedGrowth, recentUpdateResets, recentUpdateSteps }; -} - -function elapsedSecondsBetween(fromTs, toTs) { - const from = Date.parse(fromTs); - const to = Date.parse(toTs); - if (!Number.isFinite(from) || !Number.isFinite(to) || to < from) return 0; - return Number(((to - from) / 1000).toFixed(3)); -} - -function maxPositiveDelta(items) { - const values = (Array.isArray(items) ? items : []) - .map((item) => Number(item.delta)) - .filter((value) => Number.isFinite(value) && value > 0); - return values.length > 0 ? Math.max(...values) : 0; -} - -function isTerminalTurnStatus(value) { - const status = String(value ?? "").trim().toLowerCase().replace(/_/gu, "-"); - return ["completed", "succeeded", "success", "failed", "error", "blocked", "timeout", "canceled", "cancelled", "stale", "done", "terminal", "thread-resume-failed"].includes(status); -} - - - -function buildTraceOrderMetrics(samples, timeline) { - const rows = []; - const orderAnomalies = []; - const completionNotLast = []; - const groups = new Map(); - for (let sampleIndex = 0; sampleIndex < samples.length; sampleIndex += 1) { - const sample = samples[sampleIndex] || {}; - const sampleRows = traceTimingRowsForSample(sample, timeline[sampleIndex] || {}); - for (const row of sampleRows) { - const normalized = { - ...row, - sampleIndex, - sampleSeq: sample.seq ?? null, - timestamp: sample.ts || sample.timestamp || sample.collectedAt || sample.time || null, - pageRole: row.pageRole || sample.pageRole || sample.role || sample.contextRole || null, - pageId: row.pageId || sample.pageId || sample.contextId || null, - sessionId: row.sessionId || sample.sessionId || sample.workbenchSessionId || null, - }; - rows.push(normalized); - const key = traceRowGroupKey(normalized); - if (!groups.has(key)) groups.set(key, []); - groups.get(key).push(normalized); - } - } - const slackSeconds = Math.max(1, Number(alertThresholds?.turnTimingSampleSlackSeconds || 0)); - for (const groupRows of groups.values()) { - const sorted = groupRows.slice().sort((a, b) => { - if (a.sampleIndex !== b.sampleIndex) return a.sampleIndex - b.sampleIndex; - return (a.rowIndex ?? 0) - (b.rowIndex ?? 0); - }); - let previous = null; - for (const row of sorted) { - if (previous) { - const reasons = []; - if (Number.isFinite(previous.totalSeconds) && Number.isFinite(row.totalSeconds) && row.totalSeconds + slackSeconds < previous.totalSeconds) { - reasons.push('total-decreased'); - } - if (Number.isFinite(previous.projectedSeq) && Number.isFinite(row.projectedSeq) && row.projectedSeq < previous.projectedSeq) { - reasons.push('projected-seq-decreased'); - } - if (Number.isFinite(previous.clockSeconds) && Number.isFinite(row.clockSeconds)) { - const diff = previous.clockSeconds - row.clockSeconds; - if (diff > slackSeconds && diff < 43200) reasons.push('clock-decreased'); - } - if (Number.isFinite(previous.timestampMs) && Number.isFinite(row.timestampMs) && row.timestampMs + slackSeconds * 1000 < previous.timestampMs) { - reasons.push('timestamp-decreased'); - } - if (reasons.length) { - orderAnomalies.push({ - sampleIndex: row.sampleIndex, - sampleSeq: row.sampleSeq, - timestamp: row.timestamp, - pageRole: row.pageRole, - pageId: row.pageId, - sessionId: row.sessionId, - traceId: row.traceId || previous.traceId || null, - previousRowIndex: previous.rowIndex, - currentRowIndex: row.rowIndex, - reasons, - previousTotalSeconds: previous.totalSeconds, - currentTotalSeconds: row.totalSeconds, - previousProjectedSeq: previous.projectedSeq, - currentProjectedSeq: row.projectedSeq, - previousSourceSeq: previous.sourceSeq, - currentSourceSeq: row.sourceSeq, - previousEventSeq: previous.eventSeq, - currentEventSeq: row.eventSeq, - previousClockSeconds: previous.clockSeconds, - currentClockSeconds: row.clockSeconds, - previousTimestampMs: previous.timestampMs, - currentTimestampMs: row.timestampMs, - previousPreview: previous.preview, - currentPreview: row.preview, - }); - } - } - previous = row; - } - for (let index = 0; index < sorted.length; index += 1) { - const row = sorted[index]; - if (!row.isCompletion) continue; - const later = sorted.slice(index + 1).find((candidate) => { - if (candidate.isCompletion && candidate.preview === row.preview) return false; - return Number.isFinite(candidate.totalSeconds) || Number.isFinite(candidate.clockSeconds) || Number.isFinite(candidate.projectedSeq); - }); - if (later) { - completionNotLast.push({ - sampleIndex: row.sampleIndex, - sampleSeq: row.sampleSeq, - timestamp: row.timestamp, - pageRole: row.pageRole, - pageId: row.pageId, - sessionId: row.sessionId, - traceId: row.traceId || later.traceId || null, - completionRowIndex: row.rowIndex, - laterRowIndex: later.rowIndex, - completionTotalSeconds: row.totalSeconds, - laterTotalSeconds: later.totalSeconds, - completionProjectedSeq: row.projectedSeq, - laterProjectedSeq: later.projectedSeq, - completionSourceSeq: row.sourceSeq, - laterSourceSeq: later.sourceSeq, - completionEventSeq: row.eventSeq, - laterEventSeq: later.eventSeq, - completionPreview: row.preview, - laterPreview: later.preview, - }); - } - } - } - return { - summary: { - sampleCount: samples.length, - traceRowCount: rows.length, - orderAnomalyCount: orderAnomalies.length, - completionNotLastCount: completionNotLast.length, - }, - rows: rows.slice(0, 200), - orderAnomalies: orderAnomalies.slice(0, 100), - completionNotLast: completionNotLast.slice(0, 100), - }; -} - -function buildCodeAgentCardDurationUnderreportedMetrics(samples, timeline) { - const findings = []; - const slackSeconds = Math.max(5, Number(alertThresholds?.turnTimingSampleSlackSeconds || 0)); - for (let sampleIndex = 0; sampleIndex < samples.length; sampleIndex += 1) { - const sample = samples[sampleIndex] || {}; - const cards = codeAgentCardsForSample(sample); - if (!cards.length) continue; - const traceRows = traceTimingRowsForSample(sample, timeline[sampleIndex] || {}); - const terminalCards = cards.filter((card) => isCodeAgentCardTerminal(card)); - const sampleText = sampleVisibleText(sample, timeline[sampleIndex] || {}); - for (const card of terminalCards) { - const cardText = codeAgentCardText(card); - const parsedCardSeconds = parseTotalElapsedSeconds(cardText).filter(Number.isFinite); - const cardSeconds = Number.isFinite(Number(card.totalElapsedSeconds)) ? Number(card.totalElapsedSeconds) : parsedCardSeconds.length > 0 ? Math.max(...parsedCardSeconds) : NaN; - if (!Number.isFinite(cardSeconds)) continue; - const traceMatched = traceRows.filter((row) => traceRowMatchesCard(row, card, terminalCards.length)); - const traceEvidence = maxTraceDurationEvidence(traceMatched); - const textEvidence = maxSelfReportedDurationEvidence([card.text, card.preview, card.finalResponseText, card.runningRecordText, terminalCards.length === 1 ? sampleText : ''].filter(Boolean).join('\n')); - const evidences = [traceEvidence, textEvidence].filter((item) => item && item.exact === true && Number.isFinite(item.seconds)); - if (!evidences.length) continue; - const strongest = evidences.sort((a, b) => b.seconds - a.seconds)[0]; - const tolerance = Math.max(slackSeconds, Math.ceil(strongest.seconds * 0.05)); - if (strongest.seconds > cardSeconds + tolerance) { - findings.push({ - sampleIndex, - timestamp: sample.timestamp || sample.collectedAt || sample.time || null, - pageRole: card.pageRole || sample.pageRole || sample.role || sample.contextRole || null, - pageId: card.pageId || sample.pageId || sample.contextId || null, - sessionId: card.sessionId || sample.sessionId || sample.workbenchSessionId || null, - traceId: card.traceId || strongest.traceId || null, - status: card.status || card.state || card.phase || null, - cardTotalElapsedSeconds: cardSeconds, - expectedElapsedSeconds: strongest.seconds, - deltaSeconds: strongest.seconds - cardSeconds, - toleranceSeconds: tolerance, - evidenceKind: strongest.kind, - evidencePreview: strongest.preview, - cardPreview: card.preview || compactOneLine(cardText || ''), - }); - } - } - } - return findings.slice(0, 100); -} - -function buildCodeAgentCardDurationMismatchMetrics(samples, timeline) { - const findings = []; - const slackSeconds = Math.max(5, Number(alertThresholds?.turnTimingSampleSlackSeconds || 0)); - for (let sampleIndex = 0; sampleIndex < samples.length; sampleIndex += 1) { - const sample = samples[sampleIndex] || {}; - const cards = codeAgentCardsForSample(sample); - if (!cards.length) continue; - const traceRows = traceTimingRowsForSample(sample, timeline[sampleIndex] || {}); - const terminalCards = cards.filter((card) => isCodeAgentCardTerminal(card)); - const sampleText = sampleVisibleText(sample, timeline[sampleIndex] || {}); - for (const card of terminalCards) { - const cardText = codeAgentCardText(card); - const parsedCardSeconds = parseTotalElapsedSeconds(cardText).filter(Number.isFinite); - const cardSeconds = Number.isFinite(Number(card.totalElapsedSeconds)) ? Number(card.totalElapsedSeconds) : parsedCardSeconds.length > 0 ? Math.max(...parsedCardSeconds) : NaN; - if (!Number.isFinite(cardSeconds)) continue; - const traceMatched = traceRows.filter((row) => traceRowMatchesCard(row, card, terminalCards.length)); - const traceEvidence = maxTraceDurationEvidence(traceMatched); - const textEvidence = maxSelfReportedDurationEvidence([card.text, card.preview, card.finalResponseText, card.runningRecordText, terminalCards.length === 1 ? sampleText : ''].filter(Boolean).join('\n')); - const evidences = [traceEvidence, textEvidence].filter((item) => item && item.exact === true && Number.isFinite(item.seconds)); - if (!evidences.length) continue; - const strongest = evidences.sort((a, b) => b.seconds - a.seconds)[0]; - const exactEvidence = strongest.exact === true || strongest.kind === 'trace-completion-total' || strongest.kind === 'final-response-duration'; - const tolerance = Math.max(slackSeconds, Math.ceil(strongest.seconds * 0.05)); - const signedDelta = Number((cardSeconds - strongest.seconds).toFixed(3)); - const absoluteDelta = Math.abs(signedDelta); - const underreported = strongest.seconds > cardSeconds + tolerance; - const overreported = exactEvidence && cardSeconds > strongest.seconds + tolerance; - if (!underreported && !overreported) continue; - findings.push({ - sampleIndex, - timestamp: sample.timestamp || sample.collectedAt || sample.time || sample.ts || null, - pageRole: card.pageRole || sample.pageRole || sample.role || sample.contextRole || null, - pageId: card.pageId || sample.pageId || sample.contextId || null, - sessionId: card.sessionId || sample.sessionId || sample.workbenchSessionId || null, - traceId: card.traceId || strongest.traceId || null, - status: card.status || card.state || card.phase || null, - direction: underreported ? 'underreported' : 'overreported', - cardTotalElapsedSeconds: cardSeconds, - expectedElapsedSeconds: strongest.seconds, - signedDeltaSeconds: signedDelta, - deltaSeconds: Number(absoluteDelta.toFixed(3)), - toleranceSeconds: tolerance, - evidenceKind: strongest.kind, - exactEvidence, - evidencePreview: strongest.preview, - cardPreview: card.preview || compactOneLine(cardText || ''), - }); - } - } - return findings.slice(0, 100); -} - -function traceTimingRowsForSample(sample, timelineItem) { - const rows = []; - const seen = new Set(); - const appendRowsFromText = (text, source, baseIndex, meta = {}) => { - for (const extracted of extractTraceRowsFromText(text, source, baseIndex, meta)) { - const key = [extracted.pageRole || '', extracted.pageId || '', extracted.rowIndex, extracted.preview].join('|'); - if (seen.has(key)) continue; - seen.add(key); - rows.push(extracted); - } - }; - for (const candidate of traceRowCandidateArrays(sample, timelineItem)) { - const array = Array.isArray(candidate.rows) ? candidate.rows : []; - array.forEach((item, index) => { - if (typeof item === 'string') { - appendRowsFromText(item, candidate.source, index, candidate.meta || {}); - return; - } - if (!item || typeof item !== 'object') return; - const text = objectText(item); - if (!text) return; - appendRowsFromText(text, candidate.source, Number.isFinite(Number(item.index)) ? Number(item.index) : index, { - ...(candidate.meta || {}), - pageRole: item.pageRole || item.role || candidate.meta?.pageRole || null, - pageId: item.pageId || item.contextId || candidate.meta?.pageId || null, - sessionId: item.sessionId || item.workbenchSessionId || candidate.meta?.sessionId || null, - traceId: item.traceId || item.trace_id || candidate.meta?.traceId || null, - projectedSeq: item.projectedSeq ?? item.projected_seq ?? item.projectedSequence ?? null, - sourceSeq: item.sourceSeq ?? item.source_seq ?? item.sourceSequence ?? null, - eventSeq: item.eventSeq ?? item.event_seq ?? item.sequence ?? null, - eventTimestamp: item.eventTimestamp ?? item.event_ts ?? item.timestamp ?? item.ts ?? null, - eventTimeText: item.eventTimeText ?? item.timeText ?? null, - eventKind: item.eventKind ?? item.kind ?? item.status ?? null, - }); - }); - } - if (!rows.length) { - appendRowsFromText(sampleVisibleText(sample, timelineItem), 'visible-text', 0, { - pageRole: sample.pageRole || sample.role || sample.contextRole || null, - pageId: sample.pageId || sample.contextId || null, - sessionId: sample.sessionId || sample.workbenchSessionId || null, - }); - } - rows.sort((a, b) => (a.rowIndex ?? 0) - (b.rowIndex ?? 0)); - return rows; -} - -function extractTraceRowsFromText(text, source, baseIndex, meta = {}) { - const result = []; - const normalized = String(text || '').replace(/\r/g, '\n'); - if (!normalized.trim()) return result; - const lines = normalized.split('\n').map((line) => line.trim()).filter(Boolean); - for (let index = 0; index < lines.length; index += 1) { - const line = lines[index]; - if (!traceLineLooksRelevant(line)) continue; - const nextLine = index + 1 < lines.length && !/^\d{1,2}:\d{2}:\d{2}\b/.test(lines[index + 1]) ? lines[index + 1] : ''; - const preview = compactOneLine(nextLine ? line + ' ' + nextLine : line); - result.push(normalizeTraceTimingRow(preview, source, Number(baseIndex || 0) * 1000 + index, meta)); - } - return result; -} - -function normalizeTraceTimingRow(text, source, rowIndex, meta = {}) { - const preview = compactOneLine(text).slice(0, 240); - const projectedSeq = firstFiniteNumber(meta.projectedSeq, parseTraceRowProjectedSeq(preview)); - const sourceSeq = firstFiniteNumber(meta.sourceSeq); - const eventSeq = firstFiniteNumber(meta.eventSeq); - const timestampMs = parseTraceRowTimestampMs(meta.eventTimestamp || meta.eventTimeText || preview); - return { - source, - rowIndex, - preview, - pageRole: meta.pageRole || null, - pageId: meta.pageId || null, - sessionId: meta.sessionId || null, - traceId: meta.traceId || parseTraceRowTraceId(preview), - clockSeconds: parseTraceRowClockSeconds(preview) ?? parseTraceRowClockSeconds(meta.eventTimeText || ""), - timestampMs, - totalSeconds: parseTraceRowTotalSeconds(preview), - projectedSeq, - sourceSeq, - eventSeq, - eventTimestamp: meta.eventTimestamp || null, - eventTimeText: meta.eventTimeText || null, - eventKind: meta.eventKind || null, - isCompletion: traceRowIsTerminalCompletionText(preview, meta.eventKind || ""), - }; -} - -function traceRowIsTerminalCompletionText(preview, eventKind = "") { - const value = [preview, eventKind].map((item) => String(item || "")).join(" "); - if (/\bnon[-_ ]?terminal\b/i.test(value)) return false; - if (/轮次完成|turn\s+completed|completed\s+turn|backend[_: -]?turn[_: -]?finished/i.test(value)) return true; - return /\bterminal(?:[_: -]?status|Status)?\s*[=: -]\s*(?:completed|failed|cancelled|canceled|timeout)\b/i.test(value); -} - -function traceRowIsRoundCompletionText(preview, eventKind = "") { - const value = [preview, eventKind].map((item) => String(item || "")).join(" "); - if (/\bnon[-_ ]?terminal\b/i.test(value)) return false; - return /轮次完成|turn\s+completed|completed\s+turn|backend[_: -]?turn[_: -]?finished/i.test(value); -} - -function traceRowCandidateArrays(sample, timelineItem) { - const candidates = []; - const pushArray = (rows, source, meta = {}) => { - if (Array.isArray(rows) && rows.length) candidates.push({ rows, source, meta }); - }; - const directSources = [ - [sample?.traceRows, 'sample.traceRows'], - [sample?.eventRows, 'sample.eventRows'], - [sample?.activityRows, 'sample.activityRows'], - [sample?.timelineRows, 'sample.timelineRows'], - [sample?.dom?.traceRows, 'sample.dom.traceRows'], - [sample?.dom?.eventRows, 'sample.dom.eventRows'], - [sample?.dom?.activityRows, 'sample.dom.activityRows'], - [sample?.dom?.timelineRows, 'sample.dom.timelineRows'], - [timelineItem?.traceRows, 'timeline.traceRows'], - [timelineItem?.eventRows, 'timeline.eventRows'], - [timelineItem?.activityRows, 'timeline.activityRows'], - [timelineItem?.rows, 'timeline.rows'], - [timelineItem?.events, 'timeline.events'], - ]; - for (const [rows, source] of directSources) pushArray(rows, source, {}); - if (!candidates.length) collectNamedTraceArrays(sample, candidates, 'sample', 0); - if (!candidates.length) collectNamedTraceArrays(timelineItem, candidates, 'timeline', 0); - return candidates; -} - -function collectNamedTraceArrays(value, candidates, path, depth) { - if (!value || depth > 5) return; - if (Array.isArray(value)) { - const pathLooksTrace = /trace|timeline|activity|event|log|record/i.test(path); - const valueLooksTrace = value.slice(0, 5).some((item) => traceLineLooksRelevant(typeof item === 'string' ? item : objectText(item))); - if (pathLooksTrace || valueLooksTrace) candidates.push({ rows: value, source: path, meta: {} }); - return; - } - if (typeof value !== 'object') return; - for (const [key, child] of Object.entries(value)) { - if (!child) continue; - if (Array.isArray(child)) { - const childPath = path + '.' + key; - const pathLooksTrace = /trace|timeline|activity|event|log|record/i.test(key); - const valueLooksTrace = child.slice(0, 5).some((item) => traceLineLooksRelevant(typeof item === 'string' ? item : objectText(item))); - if (pathLooksTrace || valueLooksTrace) candidates.push({ rows: child, source: childPath, meta: {} }); - continue; - } - if (typeof child === 'object' && /dom|trace|timeline|activity|event|log|record|page|card|message|panel|diagnostic/i.test(key)) { - collectNamedTraceArrays(child, candidates, path + '.' + key, depth + 1); - } - } -} - -function traceLineLooksRelevant(text) { - const value = String(text || '').trim(); - if (!value) return false; - if (/^\d{1,2}:\d{2}:\d{2}\b/.test(value)) return true; - if (/\btotal=\d/.test(value)) return true; - if (/轮次完成(总耗时/.test(value)) return true; - if (/\bseq(?:uence)?[=:]\s*\d+/i.test(value)) return true; - return false; -} - -function parseTraceRowClockSeconds(text) { - const match = String(text || '').match(/^\s*(\d{1,2}):(\d{2}):(\d{2})\b/); - if (!match) return null; - return Number(match[1]) * 3600 + Number(match[2]) * 60 + Number(match[3]); -} - -function parseTraceRowTotalSeconds(text) { - const value = String(text || ''); - const totalMatch = value.match(/\btotal=([0-9:.]+)/i); - if (totalMatch) return parseTraceDurationSeconds(totalMatch[1]); - const completionMatch = value.match(/总耗时\s*([0-9:.]+)/); - if (completionMatch) return parseTraceDurationSeconds(completionMatch[1]); - return null; -} - -function parseTraceDurationSeconds(value) { - const text = String(value || '').trim(); - if (!text) return null; - const parts = text.split(':').map((part) => Number(part)); - if (parts.some((part) => !Number.isFinite(part))) return null; - if (parts.length === 3) return parts[0] * 3600 + parts[1] * 60 + parts[2]; - if (parts.length === 2) return parts[0] * 60 + parts[1]; - if (parts.length === 1) return parts[0]; - return null; -} - -function parseTraceRowProjectedSeq(text) { - const value = String(text || ''); - const match = value.match(/\b(?:projected[_-]?seq|seq(?:uence)?|event[_-]?seq)\s*[=:]\s*(\d+)/i); - return match ? Number(match[1]) : null; -} - -function parseTraceRowTimestampMs(value) { - const text = String(value || '').trim(); - if (!text) return null; - const parsed = Date.parse(text); - return Number.isFinite(parsed) ? parsed : null; -} - -function firstFiniteNumber(...values) { - for (const value of values) { - const numeric = Number(value); - if (Number.isFinite(numeric)) return numeric; - } - return null; -} - -function parseTraceRowTraceId(text) { - const match = String(text || '').match(/\b(?:trace_id=|traceId[:=]?\s*)(trc_[a-z0-9_-]+|[a-f0-9]{16,})\b/i); - return match ? match[1] : null; -} - -function traceRowGroupKey(row) { - const identity = row.traceId ? 'trace:' + row.traceId : 'sample:' + (row.sampleIndex ?? '-') + ':' + (row.source || 'unknown'); - return [row.pageRole || '', row.pageId || '', row.sessionId || '', row.source || '', row.sampleIndex ?? '', identity].join('|'); -} - -function traceRowMatchesCard(row, card, terminalCardCount) { - if (!row) return false; - if (card.traceId) return row.traceId === card.traceId; - if (row.traceId) return false; - if (row.sessionId && card.sessionId) return terminalCardCount === 1 && row.sessionId === card.sessionId; - if (terminalCardCount === 1) return true; - return false; -} - -function maxTraceDurationEvidence(rows) { - const finiteTotals = rows.map((row) => Number(row.totalSeconds)).filter(Number.isFinite); - const finiteClocks = rows.map((row) => Number(row.clockSeconds)).filter(Number.isFinite); - const evidences = []; - if (finiteTotals.length) { - const maxTotal = Math.max(...finiteTotals); - const source = rows.find((row) => Number(row.totalSeconds) === maxTotal); - const exact = source?.isCompletion === true; - evidences.push({ kind: exact ? 'trace-completion-total' : 'trace-total', seconds: maxTotal, traceId: source?.traceId || null, preview: source?.preview || '', exact }); - } - if (finiteClocks.length >= 2) { - const minClock = Math.min(...finiteClocks); - const maxClock = Math.max(...finiteClocks); - const span = maxClock - minClock; - if (span >= 0 && span < 43200) evidences.push({ kind: 'trace-clock-span', seconds: span, traceId: null, preview: 'visible trace row clock span', exact: false }); - } - if (!evidences.length) return null; - evidences.sort((a, b) => b.seconds - a.seconds); - return evidences[0]; -} - -function maxSelfReportedDurationEvidence(text) { - const value = String(text || ''); - const lines = value.split(/\n+/).map((line) => line.trim()).filter(Boolean); - let best = null; - for (let index = 0; index < lines.length; index += 1) { - const line = lines[index]; - const previous = index > 0 ? lines[index - 1] : ''; - const next = index + 1 < lines.length ? lines[index + 1] : ''; - const candidateText = selfReportedDurationCandidateText(previous, line, next); - if (!candidateText) continue; - const seconds = parseSelfReportedRoundDurationSeconds(candidateText); - if (!Number.isFinite(seconds)) continue; - if (!best || seconds > best.seconds) { - best = { kind: 'final-response-duration', seconds, preview: compactOneLine(candidateText).slice(0, 240), exact: true }; - } - } - return best; -} - -function selfReportedDurationCandidateText(previous, line, next) { - const current = String(line || ''); - const before = String(previous || ''); - const after = String(next || ''); - const windowText = [before, current, after].filter(Boolean).join(' '); - const hasDurationKeyword = /耗时|用时|duration|elapsed/i.test(current); - const hasNearbyDurationHeading = /(?:本轮|整轮|全程|任务|round|turn)?\s*(?:耗时|用时|duration|elapsed)/i.test(before) - || /(?:本轮|整轮|全程|任务|round|turn)?\s*(?:耗时|用时|duration|elapsed)/i.test(after); - const hasDurationValue = /(?:约|大约|around|about)?\s*\d+(?:\.\d+)?\s*(?:小时|分钟|分|秒|hour|hours|hr|hrs|min|mins|minute|minutes|sec|secs|second|seconds)/i.test(windowText); - const hasRoundContext = /本轮|整轮|全程|从.+到|全部通过|smoke|benchmark|round|turn|completed|passed/i.test(windowText); - if (hasDurationKeyword && hasDurationValue) return current; - if (hasNearbyDurationHeading && hasDurationValue) return windowText; - if (hasRoundContext && hasDurationValue && /(?:约|大约|around|about)\s*\d|\d+(?:\.\d+)?\s*(?:分钟|小时|minute|hour)/i.test(current)) return windowText; - return ''; -} - -function parseSelfReportedRoundDurationSeconds(text) { - const value = String(text || ''); - const clock = value.match(/(?:耗时|用时|duration|elapsed)[^0-9]{0,24}(\d{1,2}:\d{2}:\d{2}|\d{1,2}:\d{2})/i); - if (clock) return parseTraceDurationSeconds(clock[1]); - const hour = value.match(/(?:约|大约|around|about)?\s*(\d+(?:\.\d+)?)\s*(?:小时|hour|hours|hr|hrs)/i); - const minute = value.match(/(?:约|大约|around|about)?\s*(\d+(?:\.\d+)?)\s*(?:分钟|分|min|mins|minute|minutes)/i); - const second = value.match(/(?:约|大约|around|about)?\s*(\d+(?:\.\d+)?)\s*(?:秒|sec|secs|second|seconds)/i); - let total = 0; - if (hour) total += Number(hour[1]) * 3600; - if (minute) total += Number(minute[1]) * 60; - if (second) total += Number(second[1]); - return total > 0 ? total : null; -} - -function sampleVisibleText(sample, timelineItem) { - const chunks = []; - for (const source of [sample?.visibleText, sample?.text, sample?.innerText, sample?.dom?.visibleText, sample?.dom?.text, timelineItem?.visibleText, timelineItem?.text, timelineItem?.message, timelineItem?.summary]) { - if (typeof source === 'string' && source.trim()) chunks.push(source); - } - return chunks.join('\n'); -} - -function objectText(value) { - if (!value || typeof value !== 'object') return typeof value === 'string' ? value : ''; - const keys = ['text', 'innerText', 'visibleText', 'label', 'title', 'summary', 'message', 'content', 'body', 'preview', 'description']; - const chunks = []; - for (const key of keys) { - const part = value[key]; - if (typeof part === 'string' && part.trim()) chunks.push(part); - } - return chunks.join('\n'); -} - -function compactOneLine(value) { - return String(value || '').replace(/\s+/g, ' ').trim(); -} - -function buildCodeAgentCardTimingMetrics(samples, timeline, turnTiming) { - const missingElapsed = []; - const missingRecentUpdate = []; - const cardRows = []; - for (let index = 0; index < (Array.isArray(samples) ? samples : []).length; index += 1) { - const sample = samples[index]; - const timelineItem = timeline[index] || {}; - for (const card of codeAgentCardsForSample(sample)) { - const text = codeAgentCardText(card); - const totalElapsedValues = parseTotalElapsedSeconds(card?.durationText).filter(Number.isFinite); - const recentUpdateValues = parseRecentUpdateSeconds(card?.activityText).filter(Number.isFinite); - const terminal = isCodeAgentCardTerminal(card); - const row = { - ...ref(sample), - promptIndex: timelineItem.promptIndex ?? 0, - source: card.source ?? "turn", - status: card.status ?? null, - messageId: card.messageId ?? null, - traceId: card.traceId ?? firstTraceId([text]), - sessionId: card.sessionId ?? sample.sessionId ?? sample.workbenchSessionId ?? sample.routeSessionId ?? sample.activeSessionId ?? null, - durationText: limitText(card.durationText, 120), - activityText: limitText(card.activityText, 120), - totalElapsedSeconds: totalElapsedValues.length > 0 ? Math.max(...totalElapsedValues) : null, - recentUpdateSeconds: recentUpdateValues.length > 0 ? Math.max(...recentUpdateValues) : null, - terminal, - textHash: card.textHash ?? sha256(text), - textPreview: limitText(text, 180), - valuesRedacted: true - }; - cardRows.push(row); - if (row.totalElapsedSeconds === null) missingElapsed.push(row); - if (!terminal && row.recentUpdateSeconds === null) missingRecentUpdate.push(row); - } - } - const roundCompletion = buildRoundCompletionMetrics(samples, timeline, turnTiming); - return { - summary: { - cardSampleCount: cardRows.length, - terminalCardSampleCount: cardRows.filter((item) => item.terminal).length, - runningCardSampleCount: cardRows.filter((item) => !item.terminal).length, - missingElapsedCount: missingElapsed.length, - missingRecentUpdateCount: missingRecentUpdate.length, - roundCompletionEventCount: roundCompletion.events.length, - roundCompletionElapsedMismatchCount: roundCompletion.elapsedMismatches.length, - roundCompletionFinalResponseMissingCount: roundCompletion.finalResponseMissing.length, - roundCompletionPostTimingChangeCount: roundCompletion.postCompletionTimingChanges.length, - roundCompletionPostRecentUpdateVisibleCount: roundCompletion.postCompletionRecentUpdateVisible.length, - elapsedMismatchToleranceSeconds: alertThresholds.turnTimingSampleSlackSeconds, - }, - cardSamples: cardRows.slice(0, 200), - missingElapsed: missingElapsed.slice(0, 200), - missingRecentUpdate: missingRecentUpdate.slice(0, 200), - roundCompletion, - valuesRedacted: true - }; -} - -function codeAgentCardsForSample(sample) { - const turnCards = (Array.isArray(sample?.turns) ? sample.turns : []) - .map((item) => ({ ...item, source: item?.source || "turn" })) - .filter(isCodeAgentCardLike); - if (turnCards.length > 0) return turnCards; - return (Array.isArray(sample?.messages) ? sample.messages : []) - .map((item) => ({ ...item, source: item?.source || "message" })) - .filter(isCodeAgentCardLike); -} - -function isCodeAgentCardLike(item) { - const text = codeAgentCardText(item); - const role = String(item?.dataRole || item?.role || "").toLowerCase(); - if (/agent|assistant/u.test(role)) return true; - return /Code Agent|运行记录|耗时|最近\s*(?:\d+|一|两|三)|轮次完成|trace_id=trc_/iu.test(text); -} - -function codeAgentCardText(item) { - return [ - item?.durationText, - item?.activityText, - item?.text, - item?.textPreview, - item?.status - ].map((value) => String(value || "")).filter((value) => value.trim().length > 0).join("\n"); -} - -function isCodeAgentCardTerminal(item) { - const status = String(item?.status ?? item?.state ?? item?.phase ?? "").trim().toLowerCase().replace(/_/gu, "-"); - if (isActiveTurnStatus(status)) return false; - if (isTerminalTurnStatus(status)) return true; - if (item?.terminal === true || item?.sealed === true) return true; - const text = codeAgentCardText(item); - return isTerminalTraceText(text) || /轮次完成|轮次失败|轮次取消|已记录|completed|failed|canceled|cancelled|blocked/iu.test(text); -} - -function isActiveTurnStatus(value) { - const status = String(value ?? "").trim().toLowerCase().replace(/_/gu, "-"); - return ["pending", "running", "queued", "admitted", "dispatching", "in-progress", "inprogress", "executing", "progress", "thinking", "working", "active", "streaming", "created", "started"].includes(status); -} - -function buildRoundCompletionMetrics(samples, timeline, turnTiming) { - const events = []; - const elapsedMismatchToleranceSeconds = Math.max(5, Number(alertThresholds.turnTimingSampleSlackSeconds || 0)); - for (let index = 0; index < (Array.isArray(samples) ? samples : []).length; index += 1) { - const sample = samples[index]; - const timelineItem = timeline[index] || {}; - for (const event of roundCompletionEventsForSample(sample, timelineItem)) events.push(event); - } - const completionEvents = dedupeRoundCompletionEvents(events); - const elapsedMismatches = []; - const finalResponseMissing = []; - const postCompletionTimingChanges = []; - const postCompletionRecentUpdateVisible = []; - for (const event of completionEvents) { - const sampleIndex = samples.findIndex((sample) => sample?.seq === event.seq && sample?.pageRole === event.pageRole && sample?.pageId === event.pageId); - const sameSample = sampleIndex >= 0 ? samples[sampleIndex] : null; - const sameTimelineItem = sampleIndex >= 0 ? timeline[sampleIndex] || {} : {}; - const cards = sameSample ? cardMetricItemsForCompletion(sameSample, sameTimelineItem, event) : []; - const bestCard = cards.filter((card) => Number.isFinite(Number(card.totalElapsedSeconds)))[0] || null; - if (Number.isFinite(Number(event.elapsedSeconds)) && bestCard) { - const delta = Math.abs(Number(bestCard.totalElapsedSeconds) - Number(event.elapsedSeconds)); - if (delta > elapsedMismatchToleranceSeconds) { - elapsedMismatches.push({ - ...eventRef(event), - cardTotalElapsedSeconds: Number(bestCard.totalElapsedSeconds), - completionElapsedSeconds: Number(event.elapsedSeconds), - deltaSeconds: Number(delta.toFixed(3)), - toleranceSeconds: elapsedMismatchToleranceSeconds, - cardTraceId: bestCard.traceId ?? null, - cardMessageId: bestCard.messageId ?? null, - valuesRedacted: true - }); - } - } - if (!(sameSample && sampleHasTerminalAgentResultCard(sameSample, event)) && !(sameSample && sampleHasVisibleFinalResponse(sameSample, event)) && !hasFinalResponseAfterCompletion(samples, timeline, event)) { - finalResponseMissing.push({ - ...eventRef(event), - completionElapsedSeconds: event.elapsedSeconds, - finalResponseProbe: sameSample ? terminalAgentResultProbe(sameSample, event) : { ok: false, reason: "sample-not-found" }, - valuesRedacted: true - }); - } - const postTiming = detectPostCompletionTimingChanges(turnTiming, event); - postCompletionTimingChanges.push(...postTiming.timingChanges); - postCompletionRecentUpdateVisible.push(...postTiming.recentUpdateVisible); - } - return { - events: completionEvents.slice(0, 200), - elapsedMismatches: dedupeRoundCompletionRows(elapsedMismatches).slice(0, 200), - finalResponseMissing: dedupeRoundCompletionRows(finalResponseMissing).slice(0, 200), - postCompletionTimingChanges: dedupeRoundCompletionRows(postCompletionTimingChanges).slice(0, 200), - postCompletionRecentUpdateVisible: dedupeRoundCompletionRows(postCompletionRecentUpdateVisible).slice(0, 200), - valuesRedacted: true - }; -} - -function roundCompletionEventsForSample(sample, timelineItem) { - const rows = []; - for (const item of traceTimingRowsForSample(sample, timelineItem)) { - const text = String(item?.preview || item?.text || item?.textPreview || ""); - if (!traceRowIsRoundCompletionText(text, item?.eventKind || "")) continue; - const elapsedValues = [ - Number(item?.totalSeconds), - ...parseTotalElapsedSeconds(text).filter(Number.isFinite) - ].filter(Number.isFinite); - const attributed = inferRoundCompletionCard(sample, text); - rows.push({ - ...ref(sample), - promptIndex: timelineItem.promptIndex ?? 0, - traceId: item?.traceId ?? firstTraceId([text]) ?? attributed?.traceId ?? null, - messageId: item?.messageId ?? attributed?.messageId ?? null, - attributed: Boolean(item?.traceId || item?.messageId || attributed?.traceId || attributed?.messageId), - traceRowIndex: item?.rowIndex ?? item?.index ?? null, - elapsedSeconds: elapsedValues.length > 0 ? Math.max(...elapsedValues) : null, - textHash: item?.textHash ?? sha256(text), - preview: limitText(text, 180), - valuesRedacted: true - }); - } - return rows; -} - -function inferRoundCompletionCard(sample, text) { - const cards = codeAgentCardsForSample(sample) - .filter((card) => isCodeAgentCardTerminal(card)) - .filter((card) => card?.traceId || card?.messageId); - const textHash = sha256(String(text || "")); - const direct = cards.filter((card) => { - const cardText = codeAgentCardText(card); - return card?.textHash === textHash || cardText.includes(String(text || "").slice(0, 80)) || /轮次完成/iu.test(cardText); - }); - const candidates = direct.length > 0 ? direct : cards; - if (candidates.length !== 1) return null; - const card = candidates[0]; - return { traceId: card.traceId ?? null, messageId: card.messageId ?? card.id ?? null }; -} - -function cardMetricItemsForCompletion(sample, timelineItem, event) { - const metrics = turnMetricItems(sample, timelineItem) - .filter((item) => item.promptIndex === event.promptIndex) - .filter((item) => item.pageRole === event.pageRole || !event.pageRole) - .filter((item) => item.pageId === event.pageId || !event.pageId); - if (!event.traceId && !event.messageId) { - const withElapsed = metrics.filter((item) => Number.isFinite(Number(item.totalElapsedSeconds))); - return withElapsed.length === 1 ? withElapsed : []; - } - return metrics - .filter((item) => !event.traceId || !item.traceId || item.traceId === event.traceId) - .filter((item) => !event.messageId || !item.messageId || item.messageId === event.messageId) - .sort((left, right) => { - const leftTraceMatch = event.traceId && left.traceId === event.traceId ? 0 : 1; - const rightTraceMatch = event.traceId && right.traceId === event.traceId ? 0 : 1; - const leftMessageMatch = event.messageId && left.messageId === event.messageId ? 0 : 1; - const rightMessageMatch = event.messageId && right.messageId === event.messageId ? 0 : 1; - return leftTraceMatch - rightTraceMatch || leftMessageMatch - rightMessageMatch || String(left.source || "").localeCompare(String(right.source || "")); - }); -} - -function hasFinalResponseAfterCompletion(samples, timeline, event) { - if (!event.traceId && !event.messageId && !event.promptIndex) return true; - const eventTsMs = Date.parse(String(event.ts || "")); - for (let index = 0; index < (Array.isArray(samples) ? samples : []).length; index += 1) { - const sample = samples[index]; - const tsMs = Date.parse(String(sample?.ts || "")); - if (Number.isFinite(eventTsMs) && Number.isFinite(tsMs) && tsMs < eventTsMs) continue; - if (sample?.pageRole !== event.pageRole) continue; - const sampleSession = sample?.routeSessionId || sample?.activeSessionId || null; - const eventSession = event.routeSessionId || event.activeSessionId || null; - if (sampleSession && eventSession && sampleSession !== eventSession) continue; - const promptIndex = timeline[index]?.promptIndex ?? 0; - if (!event.traceId && !event.messageId && event.promptIndex && promptIndex !== event.promptIndex) continue; - if (sampleHasTerminalAgentResultCard(sample, event)) return true; - if (sampleHasVisibleFinalResponse(sample, event)) return true; - } - return false; -} - -function sampleHasTerminalAgentResultCard(sample, event = {}) { - return terminalAgentResultProbe(sample, event).ok === true; -} - -function terminalAgentResultProbe(sample, event = {}) { - const decisions = []; - for (const item of codeAgentCardsForSample(sample)) { - const text = normalizedText(codeAgentCardText(item)); - const traceMatched = Boolean(event.traceId && item?.traceId && item.traceId === event.traceId); - const decision = { - source: item?.source || null, - role: item?.role ?? null, - dataRole: item?.dataRole ?? null, - status: item?.status ?? null, - traceId: item?.traceId ?? null, - messageId: item?.messageId ?? null, - textBytes: Buffer.byteLength(text), - valuesRedacted: true - }; - if (event.traceId && item?.traceId && item.traceId !== event.traceId) { - decisions.push({ ...decision, decision: "skip-trace" }); - continue; - } - if (!traceMatched && event.messageId && item?.messageId && item.messageId !== event.messageId) { - decisions.push({ ...decision, decision: "skip-message" }); - continue; - } - if (!isAssistantFinalResponseCandidate(item, item?.source || "turn")) { - decisions.push({ ...decision, decision: "skip-role" }); - continue; - } - if (!isCodeAgentCardTerminal(item)) { - decisions.push({ ...decision, decision: "skip-non-terminal" }); - continue; - } - if (text.length < 24) { - decisions.push({ ...decision, decision: "skip-short" }); - continue; - } - decisions.push({ ...decision, decision: "accept-terminal-agent-card" }); - return { ok: true, decisions: decisions.slice(-8), valuesRedacted: true }; - } - return { ok: false, decisions: decisions.slice(-8), valuesRedacted: true }; -} - -function sampleHasVisibleFinalResponse(sample, event = {}) { - for (const [groupName, group] of [["messages", sample?.messages], ["turns", sample?.turns]]) { - if (!Array.isArray(group)) continue; - for (const item of group) { - const traceMatched = Boolean(event.traceId && item?.traceId && item.traceId === event.traceId); - if (event.traceId && item?.traceId && item.traceId !== event.traceId) continue; - if (!traceMatched && event.messageId && item?.messageId && item.messageId !== event.messageId) continue; - if (!isAssistantFinalResponseCandidate(item, groupName)) continue; - const text = normalizedText([item?.text, item?.textPreview].filter(Boolean).join(" ")); - if (text.length < 24) continue; - if (groupName === "messages" && isTerminalTurnStatus(item?.status)) return true; - if (isDiagnosticText(text)) continue; - if (isFinalResultText(text)) return true; - if (/运行记录/iu.test(text) && /(?:已完成|完成|新增|实现|验证|测试|结果|README|文件|summary)/iu.test(text)) return true; - } - } - return false; -} - -function normalizedDomRole(item) { - return String(item?.dataRole ?? item?.role ?? item?.ariaRole ?? "") - .trim() - .toLowerCase() - .replace(/[_\s]+/gu, "-"); -} - -function isAssistantFinalResponseCandidate(item, groupName) { - const role = normalizedDomRole(item); - if (/agent|assistant|code-agent|bot/iu.test(role)) return true; - if (/user|human|client|prompt/iu.test(role)) return false; - if (groupName === "turns") return isCodeAgentCardLike(item); - const text = codeAgentCardText(item); - if (isTerminalTurnStatus(item?.status) && /Code Agent|运行记录|assistant|agent/iu.test(text)) return true; - return false; -} - -function detectPostCompletionTimingChanges(turnTiming, event) { - const timingChanges = []; - const recentUpdateVisible = []; - if (!event.traceId && !event.messageId) return { timingChanges, recentUpdateVisible }; - const rows = Array.isArray(turnTiming?.rows) ? turnTiming.rows : []; - const columns = Array.isArray(turnTiming?.columns) ? turnTiming.columns : []; - const eventTsMs = Date.parse(String(event.ts || "")); - for (const column of columns) { - if (column.pageRole && event.pageRole && column.pageRole !== event.pageRole) continue; - if (event.traceId && column.traceId && column.traceId !== event.traceId) continue; - if (event.messageId && column.messageId && column.messageId !== event.messageId) continue; - if (event.promptIndex && column.promptIndex && column.promptIndex !== event.promptIndex && column.lastPromptIndex !== event.promptIndex) continue; - let previousTotal = null; - let previousRecent = null; - for (const row of rows) { - const rowTsMs = Date.parse(String(row.ts || "")); - if (Number.isFinite(eventTsMs) && Number.isFinite(rowTsMs) && rowTsMs < eventTsMs) continue; - if (row.pageRole && event.pageRole && row.pageRole !== event.pageRole) continue; - const cell = row.cells?.[column.id]; - if (!cell) continue; - if (event.traceId && cell.traceId && cell.traceId !== event.traceId) continue; - if (event.messageId && cell.messageId && cell.messageId !== event.messageId) continue; - const total = cell.totalElapsedSeconds === null || cell.totalElapsedSeconds === undefined ? NaN : Number(cell.totalElapsedSeconds); - if (Number.isFinite(total)) { - if (previousTotal && Math.abs(total - previousTotal.value) > alertThresholds.turnTimingSampleSlackSeconds) { - timingChanges.push({ - ...eventRef(event), - columnId: column.id, - columnLabel: column.label, - metric: "totalElapsedSeconds", - fromSeq: previousTotal.seq, - fromTs: previousTotal.ts, - fromValue: previousTotal.value, - toSeq: row.seq ?? null, - toTs: row.ts ?? null, - toValue: total, - delta: Number((total - previousTotal.value).toFixed(3)), - toleranceSeconds: alertThresholds.turnTimingSampleSlackSeconds, - traceId: cell.traceId ?? column.traceId ?? event.traceId ?? null, - messageId: cell.messageId ?? column.messageId ?? null, - valuesRedacted: true - }); - } - previousTotal = { value: total, seq: row.seq ?? null, ts: row.ts ?? null }; - } - const recent = cell.recentUpdateSeconds === null || cell.recentUpdateSeconds === undefined ? NaN : Number(cell.recentUpdateSeconds); - if (Number.isFinite(recent)) { - recentUpdateVisible.push({ - ...eventRef(event), - columnId: column.id, - columnLabel: column.label, - metric: "recentUpdateSeconds", - seq: row.seq ?? null, - ts: row.ts ?? null, - value: recent, - traceId: cell.traceId ?? column.traceId ?? event.traceId ?? null, - messageId: cell.messageId ?? column.messageId ?? null, - valuesRedacted: true - }); - if (previousRecent && recent !== previousRecent.value) { - timingChanges.push({ - ...eventRef(event), - columnId: column.id, - columnLabel: column.label, - metric: "recentUpdateSeconds", - fromSeq: previousRecent.seq, - fromTs: previousRecent.ts, - fromValue: previousRecent.value, - toSeq: row.seq ?? null, - toTs: row.ts ?? null, - toValue: recent, - delta: Number((recent - previousRecent.value).toFixed(3)), - traceId: cell.traceId ?? column.traceId ?? event.traceId ?? null, - messageId: cell.messageId ?? column.messageId ?? null, - valuesRedacted: true - }); - } - previousRecent = { value: recent, seq: row.seq ?? null, ts: row.ts ?? null }; - } - } - } - return { timingChanges, recentUpdateVisible }; -} - -function dedupeRoundCompletionEvents(rows) { - const result = []; - const seen = new Set(); - for (const row of Array.isArray(rows) ? rows : []) { - const key = [ - row?.pageRole ?? "", - row?.pageId ?? "", - row?.promptIndex ?? "", - row?.traceId ?? "", - row?.messageId ?? "", - row?.textHash ?? "", - row?.elapsedSeconds ?? "" - ].join("|"); - if (seen.has(key)) continue; - seen.add(key); - result.push(row); - } - return result; -} - -function eventRef(event) { - return { - seq: event?.seq ?? null, - sampleGroupSeq: event?.sampleGroupSeq ?? null, - ts: event?.ts ?? null, - pageRole: event?.pageRole ?? null, - pageId: event?.pageId ?? null, - routeSessionId: event?.routeSessionId ?? null, - activeSessionId: event?.activeSessionId ?? null, - promptIndex: event?.promptIndex ?? null, - traceId: event?.traceId ?? null, - messageId: event?.messageId ?? null, - }; -} - -function dedupeRoundCompletionRows(rows) { - const result = []; - const seen = new Set(); - for (const row of Array.isArray(rows) ? rows : []) { - const key = [ - row?.seq ?? row?.fromSeq ?? "", - row?.toSeq ?? "", - row?.pageRole ?? "", - row?.promptIndex ?? "", - row?.traceId ?? "", - row?.metric ?? "", - row?.textHash ?? "", - row?.columnId ?? "" - ].join("|"); - if (seen.has(key)) continue; - seen.add(key); - result.push(row); - } - return result; -} - -function turnMetricItems(sample, timelineItem) { - const promptIndex = timelineItem.promptIndex ?? 0; - const pageRole = sample?.pageRole || "control"; - const pageId = sample?.pageId || "unknown-page"; - const sessionKey = pageRole + ":" + pageId + ":" + (sample?.routeSessionId || sample?.activeSessionId || "unknown-session"); - const roundKey = String(promptIndex); - const items = []; - if (Array.isArray(sample?.turns) && sample.turns.length > 0) { - for (const turn of sample.turns) { - const texts = turnTexts(turn); - const totalElapsedValues = texts.flatMap(parseTotalElapsedSeconds).filter(Number.isFinite); - const recentUpdateValues = texts.flatMap(parseRecentUpdateSeconds).filter(Number.isFinite); - const traceId = turn.traceId || firstTraceId(texts); - const messageId = turn.messageId || null; - const turnId = turn.turnId || traceId || null; - const stableId = traceId || messageId || turnId || null; - const domIndex = Number.isFinite(Number(turn.index)) ? Number(turn.index) : items.length; - const key = stableId - ? "timing:" + sessionKey + ":id-" + stableId - : "turn:" + sessionKey + ":round-" + roundKey + ":dom-index-" + String(domIndex); - items.push({ - key, - source: "turn", - pageRole, - pageId, - promptIndex, - traceId, - messageId, - turnId, - domIndex, - status: turn.status ?? null, - totalElapsedSeconds: totalElapsedValues.length > 0 ? Math.max(...totalElapsedValues) : null, - recentUpdateSeconds: recentUpdateValues.length > 0 ? Math.max(...recentUpdateValues) : null, - textHash: turn.textHash || sha256(texts.join("\n")) - }); - } - return items; - } - if (Array.isArray(sample?.messages) && sample.messages.length > 0) { - for (const message of sample.messages) { - const text = [message?.durationText, message?.activityText].map((value) => String(value || "")).filter((value) => value.trim().length > 0).join("\n"); - const totalElapsedValues = parseTotalElapsedSeconds(text).filter(Number.isFinite); - const recentUpdateValues = parseRecentUpdateSeconds(text).filter(Number.isFinite); - if (totalElapsedValues.length === 0 && recentUpdateValues.length === 0) continue; - const domIndex = Number.isFinite(Number(message.index)) ? Number(message.index) : items.length; - const traceId = message.traceId || firstTraceId([text]); - const messageId = message.messageId || null; - const stableId = traceId || messageId || message.turnId || null; - items.push({ - key: stableId - ? "timing:" + sessionKey + ":id-" + stableId - : "message:" + sessionKey + ":round-" + roundKey + ":dom-index-" + String(domIndex), - source: "message", - pageRole, - pageId, - promptIndex, - traceId, - messageId, - turnId: message.turnId || traceId || null, - domIndex, - status: message.status ?? null, - totalElapsedSeconds: totalElapsedValues.length > 0 ? Math.max(...totalElapsedValues) : null, - recentUpdateSeconds: recentUpdateValues.length > 0 ? Math.max(...recentUpdateValues) : null, - textHash: message.textHash || sha256(text) - }); - } - if (items.length > 0) return items; - } - if (timelineItem.totalElapsedSeconds !== null || timelineItem.recentUpdateSeconds !== null) { - return [{ - key: "aggregate:" + sessionKey + ":round-" + roundKey, - source: "aggregate", - pageRole, - pageId, - promptIndex, - traceId: null, - messageId: null, - domIndex: null, - status: null, - totalElapsedSeconds: timelineItem.totalElapsedSeconds ?? null, - recentUpdateSeconds: timelineItem.recentUpdateSeconds ?? null, - textHash: timelineItem.textDigest ?? null - }]; - } - return []; -} - -function turnTexts(turn) { - return [ - turn?.durationText, - turn?.activityText - ].map((value) => String(value || "")).filter((value) => value.trim().length > 0); -} - -function firstTraceId(texts) { - for (const text of texts) { - const match = String(text || "").match(/\btrc_[A-Za-z0-9_-]+\b/u); - if (match) return match[0]; - } - return null; -} - -function buildRoundMetricSummaries(timeline, promptCommands, timing = {}) { - const rounds = []; - const timingColumns = Array.isArray(timing.columns) ? timing.columns : []; - const timingRows = Array.isArray(timing.rows) ? timing.rows : []; - const nonMonotonic = Array.isArray(timing.nonMonotonic) ? timing.nonMonotonic : []; - const totalElapsedForwardJumps = Array.isArray(timing.totalElapsedForwardJumps) ? timing.totalElapsedForwardJumps : []; - const elapsedZeroResets = Array.isArray(timing.elapsedZeroResets) ? timing.elapsedZeroResets : []; - const terminalElapsedGrowth = Array.isArray(timing.terminalElapsedGrowth) ? timing.terminalElapsedGrowth : []; - const recentUpdateResets = Array.isArray(timing.recentUpdateResets) ? timing.recentUpdateResets : []; - const recentUpdateSteps = Array.isArray(timing.recentUpdateSteps) ? timing.recentUpdateSteps : []; - for (let index = 0; index < promptCommands.length; index += 1) { - const promptIndex = index + 1; - const items = timeline.filter((item) => item.promptIndex === promptIndex); - const aggregateTotalElapsed = items.map((item) => item.totalElapsedSeconds).filter((value) => value !== null); - const aggregateRecentUpdate = items.map((item) => item.recentUpdateSeconds).filter((value) => value !== null); - const promptTurnTiming = roundPromptTurnTimingValues(timingRows, timingColumns, promptIndex, { - firstSeq: items[0]?.seq ?? null, - firstSampleAt: items[0]?.ts ?? null - }); - const totalElapsed = promptTurnTiming.totalElapsed.length > 0 ? promptTurnTiming.totalElapsed : aggregateTotalElapsed; - const recentUpdate = promptTurnTiming.recentUpdate.length > 0 ? promptTurnTiming.recentUpdate : aggregateRecentUpdate; - const loadingCounts = items.map((item) => Number(item.loadingCount ?? 0)).filter(Number.isFinite); - const loadingOwners = new Set(); - for (const item of items) { - for (const owner of Array.isArray(item.loadingOwners) ? item.loadingOwners : []) { - if (owner?.ownerKey) loadingOwners.add(owner.ownerKey); - } - } - const timingAnomalies = nonMonotonic.filter((item) => item.promptIndex === promptIndex); - const timingForwardJumps = totalElapsedForwardJumps.filter((item) => item.promptIndex === promptIndex); - const timingZeroResets = elapsedZeroResets.filter((item) => item.promptIndex === promptIndex); - const timingTerminalGrowth = terminalElapsedGrowth.filter((item) => item.promptIndex === promptIndex); - const timingResets = recentUpdateResets.filter((item) => item.promptIndex === promptIndex); - const timingSteps = recentUpdateSteps.filter((item) => item.promptIndex === promptIndex); - const terminalGrowthDeltas = timingTerminalGrowth.map((item) => Number(item.delta)).filter((value) => Number.isFinite(value) && value > 0); - const timingStepDeltas = timingSteps.map((item) => Number(item.delta)).filter((value) => Number.isFinite(value) && value >= 0); - const timingStepExcess = timingSteps.map((item) => Number(item.excessiveIncreaseSeconds)).filter((value) => Number.isFinite(value) && value > 0); - rounds.push({ - promptIndex, - promptCommandId: promptCommands[index].commandId, - promptTextHash: promptCommands[index].textHash, - promptTextBytes: promptCommands[index].textBytes, - promptCompletedAt: promptCommands[index].ts, - sampleCount: items.length, - firstSeq: items[0]?.seq ?? null, - lastSeq: items[items.length - 1]?.seq ?? null, - firstSampleAt: items[0]?.ts ?? null, - lastSampleAt: items[items.length - 1]?.ts ?? null, - withTotalElapsed: totalElapsed.length, - withRecentUpdate: recentUpdate.length, - loadingSamples: loadingCounts.filter((value) => value > 0).length, - maxLoadingCount: loadingCounts.length > 0 ? Math.max(...loadingCounts) : 0, - loadingOwnerCount: loadingOwners.size, - maxTotalElapsedSeconds: totalElapsed.length > 0 ? Math.max(...totalElapsed) : null, - lastTotalElapsedSeconds: lastNonNull(totalElapsed), - maxRecentUpdateSeconds: recentUpdate.length > 0 ? Math.max(...recentUpdate) : null, - lastRecentUpdateSeconds: lastNonNull(recentUpdate), - diagnosticSamples: items.filter((item) => item.diagnosticSeen).length, - terminalSamples: items.filter((item) => item.terminalSeen).length, - finalTextSamples: items.filter((item) => item.finalResultTextSeen).length, - turnTimingNonMonotonicCount: timingAnomalies.length, - turnTimingTotalElapsedDecreaseCount: timingAnomalies.filter((item) => item.metric === "totalElapsedSeconds").length, - turnTimingTotalElapsedZeroResetCount: timingZeroResets.length, - turnTimingTotalElapsedForwardJumpCount: timingForwardJumps.length, - turnTimingTotalElapsedForwardJumpMaxSeconds: maxPositiveDelta(timingForwardJumps), - turnTimingTerminalElapsedGrowthCount: timingTerminalGrowth.length, - turnTimingTerminalElapsedGrowthMaxSeconds: terminalGrowthDeltas.length > 0 ? Math.max(...terminalGrowthDeltas) : 0, - turnTimingRecentUpdateJumpCount: timingAnomalies.filter((item) => item.metric === "recentUpdateSeconds" && item.anomaly === "jump").length, - turnTimingRecentUpdateSawtoothJumpCount: timingAnomalies.filter((item) => item.metric === "recentUpdateSeconds" && item.anomaly === "jump").length, - turnTimingRecentUpdateStepCount: timingSteps.length, - turnTimingRecentUpdateMaxIncreaseSeconds: timingStepDeltas.length > 0 ? Math.max(...timingStepDeltas) : null, - turnTimingRecentUpdateMaxExcessSeconds: timingStepExcess.length > 0 ? Math.max(...timingStepExcess) : 0, - turnTimingRecentUpdateResetCount: timingResets.length, - turnTimingRecentUpdateDecreaseCount: timingResets.length - }); - } - return rounds; -} - -function roundPromptTurnTimingValues(rows, columns, promptIndex, round = {}) { - const roundFirstSeq = Number(round.firstSeq); - const roundFirstMs = Date.parse(String(round.firstSampleAt ?? "")); - const seqSlack = 3; - const timeSlackMs = 3000; - const promptColumnIds = new Set(columns - .filter((column) => { - if (Number(column?.promptIndex) === Number(promptIndex)) return true; - const firstSeq = Number(column?.firstSeq); - if (Number.isFinite(roundFirstSeq) && Number.isFinite(firstSeq) && firstSeq >= roundFirstSeq - seqSlack) return true; - const firstMs = Date.parse(String(column?.firstTs ?? "")); - return Number.isFinite(roundFirstMs) && Number.isFinite(firstMs) && firstMs >= roundFirstMs - timeSlackMs; - }) - .map((column) => String(column?.id || "")) - .filter(Boolean)); - if (promptColumnIds.size === 0) return { totalElapsed: [], recentUpdate: [] }; - const totalElapsed = []; - const recentUpdate = []; - for (const row of rows) { - if (Number(row?.promptIndex ?? 0) !== Number(promptIndex)) continue; - const cells = row?.cells && typeof row.cells === "object" ? row.cells : {}; - for (const [columnId, cell] of Object.entries(cells)) { - if (!promptColumnIds.has(columnId)) continue; - const total = Number(cell?.totalElapsedSeconds); - if (Number.isFinite(total)) totalElapsed.push(total); - const recent = Number(cell?.recentUpdateSeconds); - if (Number.isFinite(recent)) recentUpdate.push(recent); - } - } - return { totalElapsed, recentUpdate }; -} - -function sampleTexts(sample) { - const rows = []; - for (const group of [sample?.messages, sample?.traceRows, sample?.diagnostics]) { - if (!Array.isArray(group)) continue; - for (const item of group) { - const text = String(item?.textPreview || ""); - if (text.trim()) rows.push(text); - } - } - return rows; -} - -function sampleTurnTimingTexts(sample) { - const rows = []; - for (const group of [sample?.turns, sample?.messages]) { - if (!Array.isArray(group)) continue; - for (const item of group) { - for (const value of [item?.durationText, item?.activityText]) { - const text = String(value || ""); - if (text.trim()) rows.push(text); - } - } - } - return rows; -} - -function parseTotalElapsedSeconds(text) { - const values = []; - for (const match of String(text || "").matchAll(/(?:总耗时|耗时)\s*[=::]?\s*(\d{1,2}):(\d{2}):(\d{2})/giu)) { - values.push(Number(match[1]) * 3600 + Number(match[2]) * 60 + Number(match[3])); - } - for (const match of String(text || "").matchAll(/(?:总耗时|耗时)\s*[=::]?\s*(\d{1,2}):(\d{2})(?!:)/giu)) { - values.push(Number(match[1]) * 60 + Number(match[2])); - } - for (const match of String(text || "").matchAll(/(?:总耗时|耗时)\s*[=::]?\s*(?:(\d+)\s*天)?\s*(?:(\d+)\s*小时)?\s*(?:(\d+)\s*(?:分钟|分))?\s*(?:(\d+)\s*秒)?/giu)) { - const days = Number(match[1] || 0); - const hours = Number(match[2] || 0); - const minutes = Number(match[3] || 0); - const seconds = Number(match[4] || 0); - if (days || hours || minutes || seconds || /(?:天|小时|分钟|分|秒)/u.test(match[0] || "")) values.push(days * 86400 + hours * 3600 + minutes * 60 + seconds); - } - return values; -} - -function lastNonNull(values) { - for (let index = values.length - 1; index >= 0; index -= 1) if (values[index] !== null && values[index] !== undefined) return values[index]; - return null; -} - -function parseRecentUpdateSeconds(text) { - const values = []; - for (const match of String(text || "").matchAll(/最近\s*(?:(\d+)\s*天)?\s*(?:(\d+)\s*小时)?\s*(?:(\d+)\s*(?:分钟|分))?\s*(?:(\d+)\s*秒)?\s*前/giu)) { - const days = Number(match[1] || 0); - const hours = Number(match[2] || 0); - const minutes = Number(match[3] || 0); - const seconds = Number(match[4] || 0); - values.push(days * 86400 + hours * 3600 + minutes * 60 + seconds); - } - return values; -} - -function latestPromptIndex(promptTimes, tsMs) { - let index = 0; - for (let i = 0; i < promptTimes.length; i += 1) { - if (promptTimes[i] <= tsMs) index = i + 1; - else break; - } - return index; -} - -function promptIndexForTs(promptTimes, ts) { - const tsMs = Date.parse(ts); - return Number.isFinite(tsMs) ? latestPromptIndex(promptTimes, tsMs) : 0; -} - -function buildTransitions(samples) { - const rows = []; - let last = null; - for (const sample of samples) { - const digest = digestSample(sample); - if (digest !== last) { - rows.push({ seq: sample.seq, ts: sample.ts, url: sample.url, routeSessionId: sample.routeSessionId || null, activeSessionId: sample.activeSessionId || null, messageCount: Array.isArray(sample.messages) ? sample.messages.length : 0, traceRowCount: Array.isArray(sample.traceRows) ? sample.traceRows.length : 0, digest }); - last = digest; - } - } - return rows.slice(0, 200); -} - -function detectFinalFlicker(samples) { - const flickers = []; - const lastNonEmptyByScope = new Map(); - for (const sample of samples) { - const scope = finalFlickerScope(sample); - if (!scope) continue; - const messageText = Array.isArray(sample.messages) ? sample.messages.map((item) => item.textPreview || "").join("\n") : ""; - const nonEmpty = messageText.trim().length > 0; - const finalLike = /轮次完成|已记录|已完成第\d+轮|final response|terminal result/iu.test(messageText); - const diagnosticLike = /temporarily|timeout|无法连接|暂时|error|failed|超时/iu.test(messageText); - const lastNonEmpty = lastNonEmptyByScope.get(scope); - if (nonEmpty && finalLike && !diagnosticLike) lastNonEmptyByScope.set(scope, { sample, messageText }); - if (lastNonEmpty && nonEmpty && diagnosticLike) flickers.push({ scope, from: ref(lastNonEmpty.sample), to: ref(sample) }); - if (lastNonEmpty && !nonEmpty) flickers.push({ scope, from: ref(lastNonEmpty.sample), to: ref(sample), reason: "text-disappeared" }); - } - return flickers; -} - -function finalFlickerScope(sample) { - const pathname = samplePathname(sample); - const sessionId = sample?.routeSessionId || sample?.activeSessionId || workbenchSessionIdFromPath(pathname); - if (!sessionId) return null; - if (!pathname.startsWith("/workbench/sessions/" + sessionId)) return null; - return (sample?.pageRole || "control") + ":" + sessionId; -} - -function samplePathname(sample) { - const raw = String(sample?.path || sample?.url || "").trim(); - if (!raw) return ""; - try { - return new URL(raw, "http://hwlab.local").pathname || raw; - } catch { - return raw.split(/[?#]/u, 1)[0] || raw; - } -} - -function workbenchSessionIdFromPath(pathname) { - const match = String(pathname || "").match(/^\/workbench\/sessions\/([^/?#]+)/u); - return match ? match[1] : null; -} - -function detectTerminalZeroElapsed(samples) { - const rows = []; - for (const sample of samples) { - const turns = Array.isArray(sample?.turns) ? sample.turns : []; - const messages = Array.isArray(sample?.messages) ? sample.messages : []; - for (const item of [...turns, ...messages]) { - const text = [item?.durationText, item?.activityText, item?.status, item?.textPreview, item?.text].filter(Boolean).join("\n"); - if (!/(?:Code Agent|运行记录|completed|failed|canceled|blocked)/iu.test(text)) continue; - if (!/(?:completed|failed|canceled|blocked|已完成|失败|取消)/iu.test(text)) continue; - const timingText = [item?.durationText, item?.activityText].filter(Boolean).join("\n"); - const elapsedValues = parseTotalElapsedSeconds(timingText); - if (!elapsedValues.includes(0)) continue; - rows.push({ - ...ref(sample), - status: item?.status ?? null, - messageId: item?.messageId ?? null, - traceId: item?.traceId ?? null, - durationText: item?.durationText ?? null, - activityText: item?.activityText ?? null, - textPreview: limitText(item?.textPreview || item?.text || "", 180) - }); - } - } - return rows; -} - -function detectCrossPageProjectionDiffs(samples) { - const groups = new Map(); - for (const sample of samples) { - const key = sample?.sampleGroupSeq ?? sample?.seq; - if (key === null || key === undefined) continue; - const group = groups.get(key) || {}; - if (sample?.pageRole === "control") group.control = sample; - else if (sample?.pageRole === "observer") group.observer = sample; - groups.set(key, group); - } - const rows = []; - for (const [sampleGroupSeq, group] of groups.entries()) { - const control = group.control; - const observer = group.observer; - if (!control || !observer) continue; - const controlTraceIds = visibleTraceIds(control); - const observerTraceIds = visibleTraceIds(observer); - const missingInObserver = [...controlTraceIds].filter((item) => !observerTraceIds.has(item)); - const controlMessages = Array.isArray(control.messages) ? control.messages.length : 0; - const observerMessages = Array.isArray(observer.messages) ? observer.messages.length : 0; - const controlTraceRows = Array.isArray(control.traceRows) ? control.traceRows.length : 0; - const observerTraceRows = Array.isArray(observer.traceRows) ? observer.traceRows.length : 0; - const controlZero = detectTerminalZeroElapsed([control]).length > 0; - const observerZero = detectTerminalZeroElapsed([observer]).length > 0; - const sameSession = (control.routeSessionId || control.activeSessionId || null) && (control.routeSessionId || control.activeSessionId || null) === (observer.routeSessionId || observer.activeSessionId || null); - const controlMessageDigest = digestMessageTexts(control); - const observerMessageDigest = digestMessageTexts(observer); - const messageTextDigestDiff = controlMessageDigest !== observerMessageDigest; - const projectionDivergent = sameSession && (Math.abs(controlMessages - observerMessages) > 0 || controlZero !== observerZero); - const traceVisibilityDivergent = sameSession && !projectionDivergent && (missingInObserver.length > 0 || Math.abs(controlTraceRows - observerTraceRows) > 0); - if (!projectionDivergent && !traceVisibilityDivergent) continue; - rows.push({ - sampleGroupSeq, - diffKind: traceVisibilityDivergent ? "trace-visibility" : "projection", - control: ref(control), - observer: ref(observer), - controlTraceIds: [...controlTraceIds].slice(0, 8), - observerTraceIds: [...observerTraceIds].slice(0, 8), - missingTraceIdsInObserver: missingInObserver.slice(0, 8), - controlMessageCount: controlMessages, - observerMessageCount: observerMessages, - controlTraceRowCount: controlTraceRows, - observerTraceRowCount: observerTraceRows, - terminalZeroElapsedDiff: controlZero !== observerZero, - messageTextDigestDiff, - controlMessageDigest, - observerMessageDigest - }); - } - return rows; -} - -function mergeCrossPageDiffRows(...groups) { - const rows = []; - const seen = new Set(); - for (const group of groups) { - if (!Array.isArray(group)) continue; - for (const row of group) { - const key = [ - row?.diffKind || "projection", - row?.control?.seq ?? null, - row?.observer?.seq ?? null, - row?.controlMessageCount ?? null, - row?.observerMessageCount ?? null, - row?.controlTraceRowCount ?? null, - row?.observerTraceRowCount ?? null - ].join(":"); - if (seen.has(key)) continue; - seen.add(key); - rows.push(row); - } - } - return rows; -} - -function annotateCrossPageDiffTiming(rows) { - const groups = new Map(); - for (const row of Array.isArray(rows) ? rows : []) { - const controlAt = Date.parse(String(row?.control?.ts || "")); - const observerAt = Date.parse(String(row?.observer?.ts || "")); - const timestamps = [controlAt, observerAt].filter(Number.isFinite); - const startMs = timestamps.length > 0 ? Math.min(...timestamps) : null; - const endMs = timestamps.length > 0 ? Math.max(...timestamps) : null; - const sessionId = row?.control?.routeSessionId || row?.control?.activeSessionId || row?.observer?.routeSessionId || row?.observer?.activeSessionId || "unknown-session"; - const key = [row?.diffKind || "projection", sessionId].join(":"); - const group = groups.get(key) || { rows: [], firstMs: null, lastMs: null }; - const annotated = { - ...row, - sampleStartAt: startMs === null ? null : new Date(startMs).toISOString(), - sampleEndAt: endMs === null ? null : new Date(endMs).toISOString(), - pairSkewMs: startMs === null || endMs === null ? null : endMs - startMs, - }; - group.rows.push(annotated); - if (startMs !== null && (group.firstMs === null || startMs < group.firstMs)) group.firstMs = startMs; - if (endMs !== null && (group.lastMs === null || endMs > group.lastMs)) group.lastMs = endMs; - groups.set(key, group); - } - const result = []; - for (const group of groups.values()) { - const sortedRows = group.rows.slice().sort((a, b) => Number(Date.parse(String(a.sampleStartAt || ""))) - Number(Date.parse(String(b.sampleStartAt || "")))); - const segments = []; - const splitGapMs = Math.max(1000, Number(alertThresholds.crossPageProjectionDivergenceRedMs || alertThresholds.visibleLoadingSlowMs || 10_000)); - for (const row of sortedRows) { - const startMs = Date.parse(String(row.sampleStartAt || "")); - const endMs = Date.parse(String(row.sampleEndAt || row.sampleStartAt || "")); - const last = segments.at(-1); - const lastEndMs = last && last.lastMs !== null ? last.lastMs : null; - if (!last || (Number.isFinite(startMs) && lastEndMs !== null && startMs - lastEndMs > splitGapMs)) { - segments.push({ rows: [row], firstMs: Number.isFinite(startMs) ? startMs : null, lastMs: Number.isFinite(endMs) ? endMs : Number.isFinite(startMs) ? startMs : null }); - continue; - } - last.rows.push(row); - if (Number.isFinite(startMs) && (last.firstMs === null || startMs < last.firstMs)) last.firstMs = startMs; - const effectiveEndMs = Number.isFinite(endMs) ? endMs : Number.isFinite(startMs) ? startMs : null; - if (effectiveEndMs !== null && (last.lastMs === null || effectiveEndMs > last.lastMs)) last.lastMs = effectiveEndMs; - } - for (let segmentIndex = 0; segmentIndex < segments.length; segmentIndex += 1) { - const segment = segments[segmentIndex]; - const observedSpanMs = segment.firstMs === null || segment.lastMs === null ? null : segment.lastMs - segment.firstMs; - for (const row of segment.rows) { - result.push({ - ...row, - segmentIndex, - observedFirstAt: segment.firstMs === null ? null : new Date(segment.firstMs).toISOString(), - observedLastAt: segment.lastMs === null ? null : new Date(segment.lastMs).toISOString(), - observedSpanMs, - }); - } - } - } - return result; -} - -function detectAdjacentCrossPageProjectionDiffs(samples) { - const rows = []; - const ordered = (Array.isArray(samples) ? samples : []).slice().sort((a, b) => Number(a?.seq ?? 0) - Number(b?.seq ?? 0)); - for (let i = 1; i < ordered.length; i += 1) { - const a = ordered[i - 1]; - const b = ordered[i]; - const roles = new Set([a?.pageRole, b?.pageRole]); - if (!roles.has("control") || !roles.has("observer")) continue; - const control = a?.pageRole === "control" ? a : b; - const observer = a?.pageRole === "observer" ? a : b; - const controlSession = control.routeSessionId || control.activeSessionId || null; - const observerSession = observer.routeSessionId || observer.activeSessionId || null; - if (!controlSession || controlSession !== observerSession) continue; - const controlAt = Date.parse(String(control.ts || "")); - const observerAt = Date.parse(String(observer.ts || "")); - if (Number.isFinite(controlAt) && Number.isFinite(observerAt) && Math.abs(controlAt - observerAt) > 1500) continue; - const controlMessages = Array.isArray(control.messages) ? control.messages.length : 0; - const observerMessages = Array.isArray(observer.messages) ? observer.messages.length : 0; - const controlTraceRows = Array.isArray(control.traceRows) ? control.traceRows.length : 0; - const observerTraceRows = Array.isArray(observer.traceRows) ? observer.traceRows.length : 0; - const controlZero = detectTerminalZeroElapsed([control]).length > 0; - const observerZero = detectTerminalZeroElapsed([observer]).length > 0; - const missingInObserver = [...visibleTraceIds(control)].filter((item) => !visibleTraceIds(observer).has(item)); - const controlTraceIds = visibleTraceIds(control); - const observerTraceIds = visibleTraceIds(observer); - const controlMessageDigest = digestMessageTexts(control); - const observerMessageDigest = digestMessageTexts(observer); - const messageTextDigestDiff = controlMessageDigest !== observerMessageDigest; - const projectionDivergent = controlMessages !== observerMessages || controlZero !== observerZero; - const traceVisibilityDivergent = !projectionDivergent && (missingInObserver.length > 0 || controlTraceRows !== observerTraceRows); - if (!projectionDivergent && !traceVisibilityDivergent) continue; - rows.push({ - sampleGroupSeq: control.sampleGroupSeq ?? observer.sampleGroupSeq ?? null, - adjacentPair: true, - diffKind: traceVisibilityDivergent ? "trace-visibility" : "projection", - control: ref(control), - observer: ref(observer), - controlTraceIds: [...controlTraceIds].slice(0, 8), - observerTraceIds: [...observerTraceIds].slice(0, 8), - missingTraceIdsInObserver: missingInObserver.slice(0, 8), - controlMessageCount: controlMessages, - observerMessageCount: observerMessages, - controlTraceRowCount: controlTraceRows, - observerTraceRowCount: observerTraceRows, - terminalZeroElapsedDiff: controlZero !== observerZero, - messageTextDigestDiff, - controlMessageDigest, - observerMessageDigest - }); - } - return rows; -} - -function detectTraceMessageDuplication(samples) { - const rows = []; - for (const sample of samples) { - const 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) => stableDigestItem(item, ["dataRole", "role", "status", "sessionId", "messageId", "traceId", "turnId"])).join("|") : ""; - const trace = Array.isArray(sample.traceRows) ? sample.traceRows.map((item) => stableDigestItem(item, ["status", "sessionId", "messageId", "traceId", "turnId", "projectedSeq", "sourceSeq", "eventSeq", "eventKind"])).join("|") : ""; - const diagnostics = Array.isArray(sample.diagnostics) ? sample.diagnostics.map((item) => stableDigestItem(item, ["className", "status", "sessionId", "messageId", "traceId", "turnId", "diagnosticCode"])).join("|") : ""; - return sha256((sample.routeSessionId || "") + "|" + (sample.activeSessionId || "") + "|" + messages + "|" + trace + "|" + diagnostics); -} - -function samplePageKey(sample) { - return String(sample?.pageRole || "control") + ":" + String(sample?.pageId || "default"); -} - -function stableDigestItem(item, fields) { - if (!item || typeof item !== "object") return ""; - const identity = fields.map((field) => String(item?.[field] ?? "")).join(":"); - return identity + ":" + stableVisibleDigestText(item?.textPreview || item?.text || item?.textHash || ""); -} - -function stableVisibleDigestText(value) { - return normalizedText(value) - .replace(/\btotal=\d{1,2}:\d{2}:\d{2}\b/giu, "total=") - .replace(/(?:总耗时|耗时)\s*[=::]?\s*(?:(?:\d+\s*天)?\s*(?:\d+\s*小时)?\s*(?:\d+\s*(?:分钟|分))?\s*(?:\d+\s*秒)?|\d{1,2}:\d{2}(?::\d{2})?)/giu, "耗时 ") - .replace(/最近\s*(?:(?:\d+\s*天)?\s*(?:\d+\s*小时)?\s*(?:\d+\s*(?:分钟|分))?\s*(?:\d+\s*秒)?)\s*前/giu, "最近 前"); -} - -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 : []; - const disclosure = sampleMetrics?.turnTimingTableDisclosure || null; - if (columns.length === 0 || rows.length === 0) return "- 无 turn 时间表。"; - const header = ["时间戳"]; - for (const column of columns) { - const columnLabel = formatTurnColumnDisplayLabel(column); - header.push(columnLabel + " 总耗时(s)"); - header.push(columnLabel + " 最近更新(s)"); - } - const lines = []; - lines.push("| " + header.map(escapeMarkdownCell).join(" | ") + " |"); - lines.push("| " + header.map(() => "---").join(" | ") + " |"); - for (const row of rows) { - const cells = [row.ts || "-"]; - for (const column of columns) { - const cell = row.cells?.[column.id] || {}; - cells.push(formatMetricCell(cell.totalElapsedSeconds)); - cells.push(formatMetricCell(cell.recentUpdateSeconds)); - } - lines.push("| " + cells.map(escapeMarkdownCell).join(" | ") + " |"); - } - const columnLines = columns.map((column) => "- " + formatTurnColumnDisplayLabel(column) + ": pageRole=" + (column.pageRole || "-") + " pageId=" + (column.pageId || "-") + " source=" + (column.source || "-") + " prompt=" + (column.promptIndex ?? "-") + " lastPrompt=" + (column.lastPromptIndex ?? "-") + " firstSeq=" + (column.firstSeq ?? "-") + " lastSeq=" + (column.lastSeq ?? "-") + " traceId=" + (column.traceId || "-") + " messageId=" + (column.messageId || "-")).join("\n"); - const nonMonotonic = Array.isArray(sampleMetrics?.turnTimingNonMonotonic) ? sampleMetrics.turnTimingNonMonotonic : []; - const nonMonotonicLines = nonMonotonic.length > 0 - ? nonMonotonic.slice(0, 80).map((item) => "- " + formatTurnEventDisplayLabel(item) + " " + item.metric + (item.anomaly ? " " + item.anomaly : "") + " " + (item.fromValue ?? "-") + " -> " + (item.toValue ?? "-") + " delta=" + (item.delta ?? "-") + " sampleDelta=" + (item.sampleDeltaSeconds ?? "-") + " allowed=" + (item.allowedIncreaseSeconds ?? "-") + " seq " + (item.fromSeq ?? "-") + " -> " + (item.toSeq ?? "-") + " ts " + (item.fromTs || "-") + " -> " + (item.toTs || "-") + " traceId=" + (item.traceId || "-")).join("\n") - : "- 未观察到总耗时下降或最近更新异常跳增。"; - const terminalGrowth = Array.isArray(sampleMetrics?.turnTimingTerminalElapsedGrowth) ? sampleMetrics.turnTimingTerminalElapsedGrowth : []; - const terminalGrowthLines = terminalGrowth.length > 0 - ? terminalGrowth.slice(0, 80).map((item) => "- " + formatTurnEventDisplayLabel(item) + " terminal totalElapsed growth " + (item.fromValue ?? "-") + " -> " + (item.toValue ?? "-") + " delta=" + (item.delta ?? "-") + " status " + (item.fromStatus || "-") + " -> " + (item.toStatus || "-") + " seq " + (item.fromSeq ?? "-") + " -> " + (item.toSeq ?? "-") + " ts " + (item.fromTs || "-") + " -> " + (item.toTs || "-") + " traceId=" + (item.traceId || "-")).join("\n") - : "- 未观察到 terminal 后总耗时增长。"; - const elapsedZeroResets = Array.isArray(sampleMetrics?.turnTimingElapsedZeroResets) ? sampleMetrics.turnTimingElapsedZeroResets : []; - const elapsedZeroResetLines = elapsedZeroResets.length > 0 - ? elapsedZeroResets.slice(0, 80).map((item) => "- " + formatTurnEventDisplayLabel(item) + " totalElapsed zero-reset " + (item.fromValue ?? "-") + " -> " + (item.toValue ?? "-") + " delta=" + (item.delta ?? "-") + " seq " + (item.fromSeq ?? "-") + " -> " + (item.toSeq ?? "-") + " ts " + (item.fromTs || "-") + " -> " + (item.toTs || "-") + " traceId=" + (item.traceId || "-")).join("\n") - : "- 未观察到总耗时从非零跳回 0 秒。"; - const totalElapsedForwardJumps = Array.isArray(sampleMetrics?.turnTimingTotalElapsedForwardJumps) ? sampleMetrics.turnTimingTotalElapsedForwardJumps : []; - const totalElapsedForwardJumpLines = totalElapsedForwardJumps.length > 0 - ? totalElapsedForwardJumps.slice(0, 80).map((item) => "- " + formatTurnEventDisplayLabel(item) + " totalElapsed forward-jump " + (item.fromValue ?? "-") + " -> " + (item.toValue ?? "-") + " delta=" + (item.delta ?? "-") + " sampleDelta=" + (item.sampleDeltaSeconds ?? "-") + " allowed=" + (item.allowedIncreaseSeconds ?? "-") + " seq " + (item.fromSeq ?? "-") + " -> " + (item.toSeq ?? "-") + " ts " + (item.fromTs || "-") + " -> " + (item.toTs || "-") + " traceId=" + (item.traceId || "-")).join("\n") - : "- 未观察到总耗时超出采样间隔的异常前跳。"; - const sawtoothJumps = Array.isArray(sampleMetrics?.turnTimingRecentUpdateSawtoothJumps) - ? sampleMetrics.turnTimingRecentUpdateSawtoothJumps - : nonMonotonic.filter((item) => item.metric === "recentUpdateSeconds" && item.anomaly === "jump"); - const sawtoothJumpLines = sawtoothJumps.length > 0 - ? sawtoothJumps.slice(0, 80).map((item) => "- " + formatTurnEventDisplayLabel(item) + " recentUpdate sawtooth-jump " + (item.fromValue ?? "-") + " -> " + (item.toValue ?? "-") + " delta=" + (item.delta ?? "-") + " sampleDelta=" + (item.sampleDeltaSeconds ?? "-") + " allowed=" + (item.allowedIncreaseSeconds ?? "-") + " seq " + (item.fromSeq ?? "-") + " -> " + (item.toSeq ?? "-") + " ts " + (item.fromTs || "-") + " -> " + (item.toTs || "-") + " traceId=" + (item.traceId || "-")).join("\n") - : "- 未观察到最近更新三角波异常跳增。"; - const recentUpdateSteps = Array.isArray(sampleMetrics?.turnTimingRecentUpdateLargestSteps) - ? sampleMetrics.turnTimingRecentUpdateLargestSteps - : Array.isArray(sampleMetrics?.turnTimingRecentUpdateSteps) - ? sampleMetrics.turnTimingRecentUpdateSteps.filter((item) => Number.isFinite(Number(item.delta))).slice().sort((a, b) => Number(b.delta) - Number(a.delta)).slice(0, 200) - : []; - const stepLines = recentUpdateSteps.length > 0 - ? recentUpdateSteps.slice(0, 80).map((item) => "- " + formatTurnEventDisplayLabel(item) + " recentUpdate step " + (item.fromValue ?? "-") + " -> " + (item.toValue ?? "-") + " delta=" + (item.delta ?? "-") + " sampleDelta=" + (item.sampleDeltaSeconds ?? "-") + " allowed=" + (item.allowedIncreaseSeconds ?? "-") + " excess=" + (item.excessiveIncreaseSeconds ?? 0) + " event=" + (item.event || "-") + " seq " + (item.fromSeq ?? "-") + " -> " + (item.toSeq ?? "-") + " ts " + (item.fromTs || "-") + " -> " + (item.toTs || "-") + " traceId=" + (item.traceId || "-")).join("\n") - : "- 未观察到最近更新相邻采样 step。"; - const recentUpdateResets = Array.isArray(sampleMetrics?.turnTimingRecentUpdateResets) ? sampleMetrics.turnTimingRecentUpdateResets : []; - const resetLines = recentUpdateResets.length > 0 - ? recentUpdateResets.slice(0, 80).map((item) => "- " + formatTurnEventDisplayLabel(item) + " reset " + (item.fromValue ?? "-") + " -> " + (item.toValue ?? "-") + " delta=" + (item.delta ?? "-") + " sampleDelta=" + (item.sampleDeltaSeconds ?? "-") + " seq " + (item.fromSeq ?? "-") + " -> " + (item.toSeq ?? "-") + " ts " + (item.fromTs || "-") + " -> " + (item.toTs || "-") + " traceId=" + (item.traceId || "-")).join("\n") - : "- 未观察到最近更新归零/回落。"; - const disclosureLine = disclosure?.truncated - ? "表格披露:已按 head/tail 有界输出,totalRows=" + disclosure.totalRows + " includedRows=" + disclosure.includedRows + " omittedRows=" + disclosure.omittedRows + ";异常计数在截断前基于全量采样计算。" - : "表格披露:完整输出当前分析窗口的采样点。"; - return disclosureLine + "\n\n" + lines.join("\n") + "\n\n列说明:\n" + columnLines + "\n\n异常事件(仅报表暴露,不做下游 repair;最近更新按三角波模型检测异常跳增):\n" + nonMonotonicLines + "\n\nTerminal 后总耗时增长事件(终态 turn 的总耗时应 sealed,不应继续增长):\n" + terminalGrowthLines + "\n\n总耗时归零跳变事件(例如已显示真实耗时后又变成 0 秒):\n" + elapsedZeroResetLines + "\n\n总耗时异常前跳事件(预期按采样间隔近似递增;例如 14 秒 -> 1137 秒应被列入这里):\n" + totalElapsedForwardJumpLines + "\n\n最近更新 sawtooth jump 事件(预期每秒增长约 1,遇到新活动归零;例如 1 秒 -> 1 分 4 秒应被列入这里):\n" + sawtoothJumpLines + "\n\n最近更新相邻采样 step(按 delta 降序;用于人工识别一秒跳几十秒/一分钟的瞬态):\n" + stepLines + "\n\n最近更新 reset 事件(预期三角波归零,不计为异常):\n" + resetLines; -} - -function formatTurnColumnDisplayLabel(column) { - const base = String(column?.label || column?.id || "-"); - const role = String(column?.pageRole || "unknown"); - const pageId = compactPageId(column?.pageId); - return pageId ? base + "@" + role + "/" + pageId : base + "@" + role; -} - -function formatTurnEventDisplayLabel(item) { - const base = String(item?.columnLabel || item?.columnId || "-"); - const role = String(item?.pageRole || "unknown"); - const pageId = compactPageId(item?.pageId); - return pageId ? base + "@" + role + "/" + pageId : base + "@" + role; -} - -function compactPageId(value) { - if (value === null || value === undefined || value === "") return ""; - const text = String(value); - if (text.length <= 18) return text; - return text.slice(0, 12) + ".." + text.slice(-4); -} - -function formatMetricCell(value) { - if (value === null || value === undefined || !Number.isFinite(Number(value))) return "-"; - return String(Number(value)); -} - -function escapeMarkdownCell(value) { - return String(value ?? "-").replace(/\|/gu, "\\|"); -} - -function 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) + " modes=" + (Array.isArray(item.submitModes) && item.submitModes.length > 0 ? item.submitModes.join(",") : "-") + " failure=" + (item.failureKind || "-") + " statuses=" + (Array.isArray(item.responseStatuses) && item.responseStatuses.length > 0 ? item.responseStatuses.join(",") : "-") + " firstChat=" + (item.firstChatEventAt || "-") + " lastChat=" + (item.lastChatEventAt || "-")).join("\n") - : "- 无 prompt 网络记录。"; - const roundLines = Array.isArray(report.sampleMetrics?.rounds) && report.sampleMetrics.rounds.length > 0 - ? report.sampleMetrics.rounds.map((item) => "- round " + item.promptIndex + " promptHash=" + (item.promptTextHash || "-") + " samples=" + item.sampleCount + " loadingSamples=" + (item.loadingSamples ?? 0) + " maxLoading=" + (item.maxLoadingCount ?? 0) + " loadingOwners=" + (item.loadingOwnerCount ?? 0) + " totalMax=" + (item.maxTotalElapsedSeconds ?? "-") + " totalLast=" + (item.lastTotalElapsedSeconds ?? "-") + " recentMax=" + (item.maxRecentUpdateSeconds ?? "-") + " recentLast=" + (item.lastRecentUpdateSeconds ?? "-") + " totalDecrease=" + (item.turnTimingTotalElapsedDecreaseCount ?? 0) + " totalForwardJump=" + (item.turnTimingTotalElapsedForwardJumpCount ?? 0) + " totalForwardJumpMax=" + (item.turnTimingTotalElapsedForwardJumpMaxSeconds ?? 0) + " terminalGrowth=" + (item.turnTimingTerminalElapsedGrowthCount ?? 0) + " terminalGrowthMax=" + (item.turnTimingTerminalElapsedGrowthMaxSeconds ?? 0) + " recentJump=" + (item.turnTimingRecentUpdateJumpCount ?? 0) + " recentSawtoothJump=" + (item.turnTimingRecentUpdateSawtoothJumpCount ?? item.turnTimingRecentUpdateJumpCount ?? 0) + " recentStep=" + (item.turnTimingRecentUpdateStepCount ?? 0) + " recentMaxIncrease=" + (item.turnTimingRecentUpdateMaxIncreaseSeconds ?? "-") + " recentMaxExcess=" + (item.turnTimingRecentUpdateMaxExcessSeconds ?? 0) + " recentReset=" + (item.turnTimingRecentUpdateResetCount ?? 0) + " diagnostics=" + item.diagnosticSamples + " terminal=" + item.terminalSamples + " finalText=" + item.finalTextSamples).join("\n") - : "- 无轮次指标。"; - const cardMissingElapsedLines = Array.isArray(codeAgentCardTiming.missingElapsed) && codeAgentCardTiming.missingElapsed.length > 0 - ? codeAgentCardTiming.missingElapsed.slice(0, 80).map((item) => "- #" + (item.seq ?? "-") + " " + (item.ts || "-") + " prompt=" + (item.promptIndex ?? "-") + " role=" + (item.pageRole || "-") + " status=" + (item.status || "-") + " traceId=" + (item.traceId || "-") + " messageId=" + (item.messageId || "-") + " preview=" + escapeMarkdownCell(item.textPreview || "")).join("\n") - : "- 未观察到 Code Agent 卡片缺少耗时。"; - const cardMissingRecentLines = Array.isArray(codeAgentCardTiming.missingRecentUpdate) && codeAgentCardTiming.missingRecentUpdate.length > 0 - ? codeAgentCardTiming.missingRecentUpdate.slice(0, 80).map((item) => "- #" + (item.seq ?? "-") + " " + (item.ts || "-") + " prompt=" + (item.promptIndex ?? "-") + " role=" + (item.pageRole || "-") + " status=" + (item.status || "-") + " traceId=" + (item.traceId || "-") + " messageId=" + (item.messageId || "-") + " total=" + (item.totalElapsedSeconds ?? "-") + " preview=" + escapeMarkdownCell(item.textPreview || "")).join("\n") - : "- 未观察到未终态 Code Agent 卡片缺少最近更新。"; - const roundCompletionLines = Array.isArray(roundCompletion.events) && roundCompletion.events.length > 0 - ? roundCompletion.events.slice(0, 80).map((item) => "- #" + (item.seq ?? "-") + " " + (item.ts || "-") + " prompt=" + (item.promptIndex ?? "-") + " role=" + (item.pageRole || "-") + " traceId=" + (item.traceId || "-") + " completionElapsed=" + (item.elapsedSeconds ?? "-") + " preview=" + escapeMarkdownCell(item.preview || "")).join("\n") - : "- 未观察到“轮次完成(总耗时 ...)”trace 行。"; - const roundCompletionMismatchLines = Array.isArray(roundCompletion.elapsedMismatches) && roundCompletion.elapsedMismatches.length > 0 - ? roundCompletion.elapsedMismatches.slice(0, 80).map((item) => "- #" + (item.seq ?? "-") + " " + (item.ts || "-") + " prompt=" + (item.promptIndex ?? "-") + " traceId=" + (item.traceId || "-") + " completion=" + (item.completionElapsedSeconds ?? "-") + " card=" + (item.cardTotalElapsedSeconds ?? "-") + " delta=" + (item.deltaSeconds ?? "-") + " tolerance=" + (item.toleranceSeconds ?? "-")).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 || "-") + " traceId=" + (item.ownerTraceId || "-") + " messageId=" + (item.ownerMessageId || "-") + " sessionId=" + (item.ownerSessionId || "-") + " samples=" + (item.sampleCount ?? 0) + " occurrences=" + (item.occurrenceCount ?? 0) + " maxCount=" + (item.maxSimultaneousCount ?? 0) + " longest=" + (item.longestContinuousSeconds ?? 0) + "s seq=" + (item.firstSeq ?? "-") + ".." + (item.lastSeq ?? "-") + " prompts=" + (Array.isArray(item.promptIndexes) ? item.promptIndexes.join(",") : "-")).join("\n") - : "- 未观察到“加载中”归属。"; - const loadingTimelineLines = Array.isArray(loading.timeline) && loading.timeline.length > 0 - ? loading.timeline.slice(0, 160).map((item) => "- #" + (item.seq ?? "-") + " " + (item.ts || "-") + " prompt=" + (item.promptIndex ?? "-") + " loadingCount=" + (item.loadingCount ?? 0) + " ownerCount=" + (item.ownerCount ?? 0) + " owners=" + ((Array.isArray(item.owners) ? item.owners : []).slice(0, 6).map((owner) => (owner.ownerKind || "-") + ":" + (owner.ownerLabel || "-") + " trace=" + (owner.ownerTraceId || "-") + "x" + (owner.count ?? 0)).join(",") || "-")).join("\n") - : "- 未观察到“加载中”采样点。"; - const sessionRailTitleSampleLines = Array.isArray(sessionRailTitles.samples) && sessionRailTitles.samples.length > 0 - ? sessionRailTitles.samples.slice(0, 80).map((item) => "- #" + (item.seq ?? "-") + " " + (item.ts || "-") + " role=" + (item.pageRole || "-") + " visible=" + (item.visibleCount ?? 0) + " fallback=" + (item.fallbackTitleCount ?? 0) + " ratio=" + (item.fallbackTitleRatio ?? 0) + " examples=" + ((Array.isArray(item.examples) ? item.examples : []).slice(0, 4).map((example) => escapeMarkdownCell(example.titlePreview || example.titleHash || "-")).join(",") || "-")).join("\n") - : "- 未观察到超过一半 fallback 的 session 列表采样点。"; - const sessionRailTitleExampleLines = Array.isArray(sessionRailTitles.examples) && sessionRailTitles.examples.length > 0 - ? sessionRailTitles.examples.slice(0, 80).map((item) => "- firstSeq=" + (item.firstSeq ?? "-") + " role=" + (item.pageRole || "-") + " active=" + String(item.active === true) + " sessionPrefix=" + (item.sessionIdPrefix || "-") + " titleHash=" + (item.titleHash || "-") + " preview=" + escapeMarkdownCell(item.titlePreview || "")).join("\n") - : "- 无 fallback 标题示例。"; - const provenanceLines = Array.isArray(report.pageProvenance?.segments) && report.pageProvenance.segments.length > 0 - ? report.pageProvenance.segments.slice(0, 40).map((item) => "- fingerprint=" + (item.assetFingerprint || "-") + " samples=" + item.sampleCount + " seq=" + (item.firstSeq ?? "-") + ".." + (item.lastSeq ?? "-") + " ts=" + (item.firstAt || "-") + ".." + (item.lastAt || "-") + " scripts=" + (item.scriptCount ?? "-") + " styles=" + (item.stylesheetCount ?? "-") + " urlPaths=" + (Array.isArray(item.urlPaths) ? item.urlPaths.slice(0, 4).join(",") : "-")).join("\n") - : "- 无页面 provenance segment。"; - const ordinaryPerformanceItems = Array.isArray(report.pagePerformance?.sameOriginApiByPath) ? report.pagePerformance.sameOriginApiByPath.filter((item) => item.isLongLivedStream !== true) : []; - const streamPerformanceItems = Array.isArray(report.pagePerformance?.sameOriginApiByPath) ? report.pagePerformance.sameOriginApiByPath.filter((item) => item.isLongLivedStream === true) : []; - const sameOriginApiBudgetMs = Number(report.alertThresholds?.sameOriginApiSlowMs ?? report.pagePerformance?.summary?.budgetMs); - const streamOpenBudgetMs = Number(report.alertThresholds?.longLivedStreamOpenSlowMs); - const performanceLines = ordinaryPerformanceItems.length > 0 - ? ordinaryPerformanceItems.slice(0, 80).map((item) => "- " + item.path + " kind=" + (item.routeKind || "same-origin-api") + " budgetMetric=" + (item.budgetMetric || "durationMs") + " samples=" + item.sampleCount + " p50=" + (item.p50Ms ?? "-") + "ms p75=" + (item.p75Ms ?? "-") + "ms p95=" + (item.p95Ms ?? "-") + "ms max=" + (item.maxMs ?? "-") + "ms >budget=" + (item.overBudgetCount ?? item.overFiveSecondCount ?? 0) + " budgetMs=" + (item.budgetMs ?? sameOriginApiBudgetMs) + " legacy>5s=" + (item.overFiveSecondCount ?? 0) + " window=" + (item.firstAt || "-") + ".." + (item.lastAt || "-")).join("\n") - : "- 无同源 API Resource Timing 样本。"; - const streamPerformanceLines = streamPerformanceItems.length > 0 - ? streamPerformanceItems.slice(0, 80).map((item) => "- " + item.path + " kind=" + (item.routeKind || "same-origin-api-stream") + " samples=" + item.sampleCount + " streamOpenP50=" + (item.streamOpenP50Ms ?? "-") + "ms streamOpenP75=" + (item.streamOpenP75Ms ?? "-") + "ms streamOpenP95=" + (item.streamOpenP95Ms ?? "-") + "ms streamOpenMax=" + (item.streamOpenMaxMs ?? "-") + "ms streamOpen>budget=" + (item.streamOpenOverBudgetCount ?? item.streamOpenOverFiveSecondCount ?? 0) + " streamOpenBudgetMs=" + (item.streamOpenBudgetMs ?? streamOpenBudgetMs) + " streamOpenLegacy>5s=" + (item.streamOpenOverFiveSecondCount ?? 0) + " streamLifetime>5s=" + (item.streamLifetimeOverFiveSecondCount ?? 0) + " lifetimeMax=" + (item.maxMs ?? "-") + "ms window=" + (item.firstAt || "-") + ".." + (item.lastAt || "-")).join("\n") - : "- 无同源长连接 Resource Timing 样本。"; - const 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)) + "…"; -} -`; -}