// SPEC: PJ2026-01040111 long-running Workbench observation. // Responsibility: Analyzer artifact IO, YAML-derived config parsing, JSONL tailing, focus windows, and sample compaction source fragment. export function nodeWebObserveAnalyzerIoSource(): string { return String.raw`async function readJson(file) { try { return JSON.parse(await readFile(file, "utf8")); } catch { return null; } } async function readCommandState(rootDir) { const buckets = {}; for (const bucket of ["pending", "processing", "done", "failed", "abandoned"]) { buckets[bucket] = await readCommandBucket(path.join(rootDir, "commands", bucket), bucket); } return { pendingCount: buckets.pending.count, processingCount: buckets.processing.count, doneCount: buckets.done.count, failedCount: buckets.failed.count, abandonedCount: buckets.abandoned.count, pending: buckets.pending.items, processing: buckets.processing.items, failed: buckets.failed.items.slice(0, 12), abandoned: buckets.abandoned.items.slice(0, 20), summary: { backlogCount: buckets.pending.count + buckets.processing.count, oldestPendingAgeSeconds: buckets.pending.oldestAgeSeconds, oldestProcessingAgeSeconds: buckets.processing.oldestAgeSeconds, valuesRedacted: true }, valuesRedacted: true }; } async function readCommandBucket(dir, bucket) { let names = []; try { names = (await readdir(dir)).filter((name) => name.endsWith(".json")).sort(); } catch (error) { if (!error || error.code !== "ENOENT") jsonlReadIssues.push({ file: path.basename(dir), kind: "command-dir-read-error", code: error && error.code ? String(error.code) : null, errorMessage: limitText(error && error.message ? error.message : String(error), 240) }); return { bucket, count: 0, oldestAgeSeconds: null, items: [] }; } const items = []; for (const name of names.slice(0, 200)) { const file = path.join(dir, name); const parsed = await readJson(file) || {}; let meta = null; try { meta = await stat(file); } catch {} const timestamp = parsed.createdAt || parsed.abandonedAt || parsed.completedAt || parsed.failedAt || (meta ? meta.mtime.toISOString() : null); const timestampMs = Date.parse(String(timestamp || "")); items.push({ bucket, id: parsed.id || parsed.commandId || name.replace(/[.]json$/u, ""), type: parsed.type || parsed.command?.type || null, createdAt: parsed.createdAt || null, completedAt: parsed.completedAt || null, failedAt: parsed.failedAt || null, abandonedAt: parsed.abandonedAt || null, reason: parsed.reason || parsed.error?.message || null, ageSeconds: Number.isFinite(timestampMs) ? Math.max(0, Math.round((Date.now() - timestampMs) / 1000)) : null, mtime: meta ? meta.mtime.toISOString() : null, valuesRedacted: true }); } const ages = items.map((item) => item.ageSeconds).filter((value) => Number.isFinite(value)); return { bucket, count: names.length, oldestAgeSeconds: ages.length > 0 ? Math.max(...ages) : null, items }; } function buildToolFindings({ manifest, heartbeat, commandState }) { const findings = []; const diagnostics = heartbeatDiagnostics(manifest, heartbeat); const startupReadiness = toolStartupReadiness(manifest, heartbeat); if (startupReadiness !== null) { findings.push({ id: "tool-target-page-not-ready", severity: "red", summary: "web-probe observe target page did not reach the expected Workbench app shell; treat this as probe setup/runtime access blocker before interpreting business turns or frontend hotspots", count: 1, reason: startupReadiness.reason, rootCause: startupReadiness.reason === "nav-access-denied" ? "web_probe_target_nav_access_denied" : startupReadiness.reason === "workbench-blank-document" ? "web_probe_target_blank_workbench_document" : "web_probe_target_page_not_ready", rootCauseStatus: "confirmed-from-startup-readiness", rootCauseConfidence: "high", nextAction: "Fix the selected node/lane Web access or Workbench shell loading first, then rerun observe start and performanceCapture; do not treat an empty performance report from this run as a business performance result.", readiness: startupReadiness, valuesRedacted: true }); } if (diagnostics.heartbeatStale) { findings.push({ id: "tool-runner-heartbeat-stale", severity: "red", summary: "web-probe observe runner heartbeat is stale; treat this as observer tooling failure, not Workbench behavior", count: 1, diagnostics, valuesRedacted: true }); } if ((commandState?.pendingCount ?? 0) > 0 || (commandState?.processingCount ?? 0) > 0) { findings.push({ id: "tool-pending-commands-unconsumed", severity: "red", summary: "web-probe observe has pending/processing control commands that were not consumed by the runner", count: (commandState?.pendingCount ?? 0) + (commandState?.processingCount ?? 0), pending: (commandState?.pending ?? []).slice(0, 12), processing: (commandState?.processing ?? []).slice(0, 12), valuesRedacted: true }); } if ((commandState?.abandonedCount ?? 0) > 0) { findings.push({ id: "tool-commands-abandoned", severity: "info", summary: "web-probe observe force-stop abandoned queued commands; do not interpret these as Workbench command failures", count: commandState.abandonedCount, commands: (commandState.abandoned ?? []).slice(0, 20), valuesRedacted: true }); } if (heartbeat?.forceStop || manifest?.forceStop) { findings.push({ id: "tool-runner-force-stopped", severity: "info", summary: "web-probe observe runner was stopped by the CLI outside the command queue", count: 1, forceStop: heartbeat?.forceStop || manifest?.forceStop, valuesRedacted: true }); } return findings; } function toolStartupReadiness(manifest, heartbeat) { const candidates = [ heartbeat?.error?.navigationReadiness, heartbeat?.error?.details?.readiness, heartbeat?.error?.details?.readinessAfterWait, heartbeat?.error?.details?.readinessBeforeClick, manifest?.error?.navigationReadiness, manifest?.error?.details?.readiness, manifest?.error?.details?.readinessAfterWait, manifest?.error?.details?.readinessBeforeClick, ]; for (const candidate of candidates) { const readiness = candidate && typeof candidate === "object" && !Array.isArray(candidate) ? candidate : null; if (!readiness) continue; const snapshot = readiness.snapshot && typeof readiness.snapshot === "object" && !Array.isArray(readiness.snapshot) ? readiness.snapshot : readiness; const reason = typeof readiness.reason === "string" && readiness.reason ? readiness.reason : typeof snapshot.reason === "string" && snapshot.reason ? snapshot.reason : null; if (reason === null) continue; return { reason, path: typeof snapshot.path === "string" ? snapshot.path : null, search: typeof snapshot.search === "string" ? snapshot.search : null, blockedReason: typeof snapshot.blockedReason === "string" ? snapshot.blockedReason : null, readyState: typeof snapshot.readyState === "string" ? snapshot.readyState : null, workbenchShellVisible: snapshot.workbenchShellVisible === true, sessionCreatePresent: snapshot.sessionCreatePresent === true, sessionCreateVisible: snapshot.sessionCreateVisible === true, sessionRailPresent: snapshot.sessionRailPresent === true, commandInputPresent: snapshot.commandInputPresent === true, activeTabPresent: snapshot.activeTabPresent === true, loginVisible: snapshot.loginVisible === true, bodyTextBytes: Number.isFinite(Number(snapshot.bodyTextBytes)) ? Number(snapshot.bodyTextBytes) : null, bodyTextHash: typeof snapshot.bodyTextHash === "string" ? snapshot.bodyTextHash : null, valuesRedacted: true }; } return null; } function heartbeatDiagnostics(manifest, heartbeat) { const status = String(heartbeat?.status || manifest?.status || ""); const terminal = /^(completed|failed|force-stopped|stopped|abandoned)$/u.test(status); const sampleIntervalMs = Number(manifest?.sampling?.sampleIntervalMs) || 5000; const staleAfterMs = Math.max(60000, sampleIntervalMs * 3); const updatedAt = heartbeat?.updatedAt || heartbeat?.lastSampleAt || null; const updatedMs = Date.parse(String(updatedAt || "")); const ageSeconds = Number.isFinite(updatedMs) ? Math.max(0, Math.round((Date.now() - updatedMs) / 1000)) : null; const heartbeatStale = !terminal && (!Number.isFinite(updatedMs) || Date.now() - updatedMs > staleAfterMs); return { status: status || null, terminal, updatedAt, heartbeatAgeSeconds: ageSeconds, heartbeatStale, heartbeatStaleAfterSeconds: Math.round(staleAfterMs / 1000), sampleSeq: heartbeat?.sampleSeq ?? null, commandSeq: heartbeat?.commandSeq ?? null, activeCommandId: heartbeat?.activeCommandId ?? null, valuesRedacted: true }; } function safeArchivePrefix(value) { const text = String(value || "").trim(); if (!text) return ""; if (!/^[A-Za-z0-9_.-]+$/u.test(text) || text.includes("..")) throw new Error("unsafe archive prefix: " + text); return text; } function positiveNumber(value, fallback) { const parsed = Number(value); return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; } function requiredPositiveThreshold(raw, key) { const parsed = Number(raw?.[key]); if (!Number.isFinite(parsed) || parsed <= 0) { throw new Error("UNIDESK_WEB_OBSERVE_ALERT_THRESHOLDS_JSON requires positive " + key + "; configure config/hwlab-node-lanes.yaml webProbe.alertThresholds"); } return parsed; } function parseAlertThresholds(value) { if (!value) { throw new Error("UNIDESK_WEB_OBSERVE_ALERT_THRESHOLDS_JSON is required; configure config/hwlab-node-lanes.yaml webProbe.alertThresholds for the selected node/lane"); } const raw = (() => { try { return JSON.parse(value); } catch (error) { throw new Error("UNIDESK_WEB_OBSERVE_ALERT_THRESHOLDS_JSON is invalid JSON: " + (error instanceof Error ? error.message : String(error))); } })(); const sessionRailFallbackRatio = requiredPositiveThreshold(raw, "sessionRailFallbackRatio"); if (sessionRailFallbackRatio > 1) { throw new Error("UNIDESK_WEB_OBSERVE_ALERT_THRESHOLDS_JSON sessionRailFallbackRatio must be <= 1"); } return { sameOriginApiSlowMs: requiredPositiveThreshold(raw, "sameOriginApiSlowMs"), partialApiSlowMs: requiredPositiveThreshold(raw, "partialApiSlowMs"), longLivedStreamOpenSlowMs: requiredPositiveThreshold(raw, "longLivedStreamOpenSlowMs"), visibleLoadingSlowMs: requiredPositiveThreshold(raw, "visibleLoadingSlowMs"), turnTimingSampleSlackSeconds: requiredPositiveThreshold(raw, "turnTimingSampleSlackSeconds"), turnElapsedSevereTimeoutSeconds: requiredPositiveThreshold(raw, "turnElapsedSevereTimeoutSeconds"), domEvaluateTimeoutRedCount: requiredPositiveThreshold(raw, "domEvaluateTimeoutRedCount"), domEvaluateTimeoutRedWindowMs: requiredPositiveThreshold(raw, "domEvaluateTimeoutRedWindowMs"), screenshotTimeoutRedCount: requiredPositiveThreshold(raw, "screenshotTimeoutRedCount"), pageErrorRedCount: requiredPositiveThreshold(raw, "pageErrorRedCount"), longTaskRedMs: requiredPositiveThreshold(raw, "longTaskRedMs"), longAnimationFrameRedMs: requiredPositiveThreshold(raw, "longAnimationFrameRedMs"), eventLoopGapRedMs: requiredPositiveThreshold(raw, "eventLoopGapRedMs"), browserProcessSampleIntervalMs: requiredPositiveThreshold(raw, "browserProcessSampleIntervalMs"), requestRateBucketMs: requiredPositiveThreshold(raw, "requestRateBucketMs"), requestRateTotalRedPerMinute: requiredPositiveThreshold(raw, "requestRateTotalRedPerMinute"), requestRatePageRedPerMinute: requiredPositiveThreshold(raw, "requestRatePageRedPerMinute"), requestRateApiPathRedPerMinute: requiredPositiveThreshold(raw, "requestRateApiPathRedPerMinute"), browserTotalRssRedMb: requiredPositiveThreshold(raw, "browserTotalRssRedMb"), browserProcessRssRedMb: requiredPositiveThreshold(raw, "browserProcessRssRedMb"), browserRssGrowthRedMb: requiredPositiveThreshold(raw, "browserRssGrowthRedMb"), browserRssGrowthWindowMs: requiredPositiveThreshold(raw, "browserRssGrowthWindowMs"), playwrightResponsivenessRedMs: requiredPositiveThreshold(raw, "playwrightResponsivenessRedMs"), playwrightResponsivenessTimeoutRedCount: requiredPositiveThreshold(raw, "playwrightResponsivenessTimeoutRedCount"), cdpMetricsTimeoutRedCount: requiredPositiveThreshold(raw, "cdpMetricsTimeoutRedCount"), uncommandedStateChangeCommandWindowMs: requiredPositiveThreshold(raw, "uncommandedStateChangeCommandWindowMs"), scrollJumpCommandWindowMs: requiredPositiveThreshold(raw, "scrollJumpCommandWindowMs"), scrollJumpFromY: requiredPositiveThreshold(raw, "scrollJumpFromY"), scrollJumpToY: requiredPositiveThreshold(raw, "scrollJumpToY"), sessionRailFallbackRatio, crossPageProjectionDivergenceRedMs: positiveNumber(raw.crossPageProjectionDivergenceRedMs, requiredPositiveThreshold(raw, "visibleLoadingSlowMs")), source: "yaml-env", }; } function parseBrowserFreezePolicy(value) { if (!value) { throw new Error("UNIDESK_WEB_OBSERVE_BROWSER_FREEZE_POLICY_JSON is required; configure config/hwlab-node-lanes.yaml webProbe.browserFreezePolicy for the selected node/lane"); } const raw = (() => { try { return JSON.parse(value); } catch (error) { throw new Error("UNIDESK_WEB_OBSERVE_BROWSER_FREEZE_POLICY_JSON is invalid JSON: " + (error instanceof Error ? error.message : String(error))); } })(); const memory = requiredPolicyRecord(raw, "memory", "webProbe.browserFreezePolicy"); const responsiveness = requiredPolicyRecord(raw, "responsiveness", "webProbe.browserFreezePolicy"); const cdp = requiredPolicyRecord(raw, "cdp", "webProbe.browserFreezePolicy"); const kill = requiredPolicyRecord(raw, "kill", "webProbe.browserFreezePolicy"); return { enabled: requiredPolicyBoolean(raw, "enabled", "webProbe.browserFreezePolicy"), blockerWindowMs: requiredPolicyPositiveNumber(raw, "blockerWindowMs", "webProbe.browserFreezePolicy"), memory: { totalRssBlockerMb: requiredPolicyPositiveNumber(memory, "totalRssBlockerMb", "webProbe.browserFreezePolicy.memory"), processRssBlockerMb: requiredPolicyPositiveNumber(memory, "processRssBlockerMb", "webProbe.browserFreezePolicy.memory"), growthBlockerMb: requiredPolicyPositiveNumber(memory, "growthBlockerMb", "webProbe.browserFreezePolicy.memory"), }, responsiveness: { latencyBlockerMs: requiredPolicyPositiveNumber(responsiveness, "latencyBlockerMs", "webProbe.browserFreezePolicy.responsiveness"), eventBlockerCount: requiredPolicyPositiveNumber(responsiveness, "eventBlockerCount", "webProbe.browserFreezePolicy.responsiveness"), }, cdp: { metricsTimeoutBlockerCount: requiredPolicyPositiveNumber(cdp, "metricsTimeoutBlockerCount", "webProbe.browserFreezePolicy.cdp"), }, kill: { enabled: requiredPolicyBoolean(kill, "enabled", "webProbe.browserFreezePolicy.kill"), gracefulSignal: requiredPolicySignal(kill, "gracefulSignal", "webProbe.browserFreezePolicy.kill", "SIGTERM"), forceSignal: requiredPolicySignal(kill, "forceSignal", "webProbe.browserFreezePolicy.kill", "SIGKILL"), graceMs: requiredPolicyIntegerInRange(kill, "graceMs", "webProbe.browserFreezePolicy.kill", 1, 120000), pollIntervalMs: requiredPolicyIntegerInRange(kill, "pollIntervalMs", "webProbe.browserFreezePolicy.kill", 1, 10000), exitCode: requiredPolicyIntegerInRange(kill, "exitCode", "webProbe.browserFreezePolicy.kill", 1, 125), }, source: "yaml-env", valuesRedacted: true, }; } function requiredPolicyRecord(raw, key, pathLabel) { const value = raw?.[key]; if (!value || typeof value !== "object" || Array.isArray(value)) { throw new Error("UNIDESK_WEB_OBSERVE_BROWSER_FREEZE_POLICY_JSON requires object " + pathLabel + "." + key); } return value; } function requiredPolicyBoolean(raw, key, pathLabel) { const value = raw?.[key]; if (typeof value !== "boolean") { throw new Error("UNIDESK_WEB_OBSERVE_BROWSER_FREEZE_POLICY_JSON requires boolean " + pathLabel + "." + key); } return value; } function requiredPolicyPositiveNumber(raw, key, pathLabel) { const value = Number(raw?.[key]); if (!Number.isFinite(value) || value <= 0) { throw new Error("UNIDESK_WEB_OBSERVE_BROWSER_FREEZE_POLICY_JSON requires positive number " + pathLabel + "." + key); } return value; } function requiredPolicyIntegerInRange(raw, key, pathLabel, min, max) { const value = Number(raw?.[key]); if (!Number.isInteger(value) || value < min || value > max) { throw new Error("UNIDESK_WEB_OBSERVE_BROWSER_FREEZE_POLICY_JSON requires integer " + pathLabel + "." + key + " between " + min + " and " + max); } return value; } function requiredPolicySignal(raw, key, pathLabel, expected) { const value = String(raw?.[key] || ""); if (value !== expected) { throw new Error("UNIDESK_WEB_OBSERVE_BROWSER_FREEZE_POLICY_JSON requires " + pathLabel + "." + key + "=" + expected); } return value; } function parseProjectManagementConfig(value) { if (!value || value === "null") { return { enabled: false, targetPaths: [], readinessSelectors: [], naturalApiPathPrefixes: [], commandAllowlist: [], launchRoute: "", slowApiBudgetMs: 0, source: "yaml-env", valuesRedacted: true }; } const raw = (() => { try { return JSON.parse(value); } catch (error) { throw new Error("UNIDESK_WEB_OBSERVE_PROJECT_MANAGEMENT_JSON is invalid JSON: " + (error instanceof Error ? error.message : String(error))); } })(); if (raw?.enabled !== true && raw?.enabled !== false) throw new Error("UNIDESK_WEB_OBSERVE_PROJECT_MANAGEMENT_JSON requires boolean enabled"); if (raw.enabled !== true) return { enabled: false, targetPaths: [], readinessSelectors: [], naturalApiPathPrefixes: [], commandAllowlist: [], launchRoute: "", slowApiBudgetMs: 0, source: "yaml-env", valuesRedacted: true }; const stringList = (key) => { const list = raw?.[key]; if (!Array.isArray(list) || list.some((item) => typeof item !== "string" || item.length === 0)) throw new Error("UNIDESK_WEB_OBSERVE_PROJECT_MANAGEMENT_JSON requires string[] " + key + "; configure config/hwlab-node-lanes.yaml webProbe.projectManagement"); return list; }; const slowApiBudgetMs = Number(raw?.slowApiBudgetMs); if (!Number.isFinite(slowApiBudgetMs) || slowApiBudgetMs <= 0) throw new Error("UNIDESK_WEB_OBSERVE_PROJECT_MANAGEMENT_JSON requires positive slowApiBudgetMs"); const launchRoute = String(raw.launchRoute || ""); if (!launchRoute.startsWith("/")) throw new Error("UNIDESK_WEB_OBSERVE_PROJECT_MANAGEMENT_JSON launchRoute must be an absolute path"); return { enabled: true, targetPaths: stringList("targetPaths"), readinessSelectors: stringList("readinessSelectors"), naturalApiPathPrefixes: stringList("naturalApiPathPrefixes"), commandAllowlist: stringList("commandAllowlist"), launchRoute, slowApiBudgetMs, source: "yaml-env", valuesRedacted: true }; } async function readJsonl(file, options = {}) { const tailLimit = Number.isFinite(Number(options.tail)) && Number(options.tail) > 0 ? Math.floor(Number(options.tail)) : 0; if (tailLimit > 0) return readJsonlTail(file, tailLimit, options); const rows = []; try { const input = createReadStream(file, { encoding: "utf8" }); const lines = createInterface({ input, crlfDelay: Infinity }); let lineNo = 0; for await (const rawLine of lines) { lineNo += 1; const line = String(rawLine || "").trim(); if (!line) continue; try { const parsed = JSON.parse(line); rows.push(typeof options.compact === "function" ? options.compact(parsed) : parsed); } catch (error) { const item = { parseError: true, lineNo, rawHash: sha256(line), errorMessage: limitText(error && error.message ? error.message : String(error), 240) }; rows.push(item); if (jsonlReadIssues.length < 50) jsonlReadIssues.push({ file: path.basename(file), kind: "parse-error", lineNo, rawHash: item.rawHash, errorMessage: item.errorMessage }); } } return rows; } catch (error) { if (error && error.code === "ENOENT") return []; if (jsonlReadIssues.length < 50) jsonlReadIssues.push({ file: path.basename(file), kind: "read-error", code: error && error.code ? String(error.code) : null, errorMessage: limitText(error && error.message ? error.message : String(error), 240) }); return []; } } async function readJsonlTail(file, limit, options = {}) { try { const lines = await readTailLines(file, limit); const rows = []; let lineNo = 0; for (const rawLine of lines) { lineNo += 1; const line = String(rawLine || "").trim(); if (!line) continue; try { const parsed = JSON.parse(line); rows.push(typeof options.compact === "function" ? options.compact(parsed) : parsed); } catch (error) { const item = { parseError: true, lineNo, tail: true, rawHash: sha256(line), errorMessage: limitText(error && error.message ? error.message : String(error), 240) }; rows.push(item); if (jsonlReadIssues.length < 50) jsonlReadIssues.push({ file: path.basename(file), kind: "parse-error", lineNo, tail: true, rawHash: item.rawHash, errorMessage: item.errorMessage }); } } return rows; } catch (error) { if (error && error.code === "ENOENT") return []; if (jsonlReadIssues.length < 50) jsonlReadIssues.push({ file: path.basename(file), kind: "read-error", tail: true, code: error && error.code ? String(error.code) : null, errorMessage: limitText(error && error.message ? error.message : String(error), 240) }); return []; } } async function readTailLines(file, limit) { const info = await stat(file); if (!info.size || limit <= 0) return []; const chunkSize = 64 * 1024; const maxBytes = Math.max(32 * 1024 * 1024, Math.min(256 * 1024 * 1024, limit * 512 * 1024)); const chunks = []; let position = info.size; let readBytes = 0; let newlineCount = 0; while (position > 0 && newlineCount <= limit && readBytes < maxBytes) { const readSize = Math.min(chunkSize, position, maxBytes - readBytes); position -= readSize; const chunk = await readFileSlice(file, position, readSize); chunks.unshift(chunk); readBytes += chunk.length; for (let index = 0; index < chunk.length; index += 1) { if (chunk[index] === 10) newlineCount += 1; } } if (position > 0 && newlineCount <= limit && jsonlReadIssues.length < 50) { jsonlReadIssues.push({ file: path.basename(file), kind: "tail-scan-truncated", limit, readBytes, fileBytes: info.size }); } const text = Buffer.concat(chunks).toString("utf8"); let lines = text.split(/\r?\n/u); if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop(); if (position > 0 && lines.length > 0) lines.shift(); return lines.slice(-limit); } async function readFileSlice(file, start, length) { const chunks = []; await new Promise((resolve, reject) => { const stream = createReadStream(file, { start, end: start + length - 1 }); stream.on("data", (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))); stream.on("error", reject); stream.on("end", resolve); }); return Buffer.concat(chunks); } function sampleTimeWindow(samples, paddingMs) { const times = samples .map((item) => Date.parse(String(item && item.ts || ""))) .filter((value) => Number.isFinite(value)); if (times.length === 0) return { startMs: null, endMs: null, startAt: null, endAt: null, paddingMs }; const startMs = Math.max(0, Math.min(...times) - Math.max(0, paddingMs || 0)); const endMs = Math.max(...times) + Math.max(0, paddingMs || 0); return { startMs, endMs, startAt: new Date(startMs).toISOString(), endAt: new Date(endMs).toISOString(), paddingMs }; } function filterRowsByTimeWindow(rows, window) { if (!window || !Number.isFinite(window.startMs) || !Number.isFinite(window.endMs)) return rows; return rows.filter((item) => { const value = item && (item.ts || item.observedAt || item.startedAt || item.finishedAt || item.createdAt || item.updatedAt); const ms = Date.parse(String(value || "")); if (!Number.isFinite(ms)) return true; return ms >= window.startMs && ms <= window.endMs; }); } function analysisFocusFromControl(control) { const completedNewSessions = (Array.isArray(control) ? control : []) .filter((item) => item?.type === "newSession" && item?.phase === "completed" && Number.isFinite(Date.parse(String(item.ts || "")))) .sort((a, b) => Date.parse(String(a.ts || "")) - Date.parse(String(b.ts || ""))); const latest = completedNewSessions.at(-1) ?? null; if (!latest) return { mode: "all", reason: "no-new-session-command", startAt: null, startMs: null, commandId: null, valuesRedacted: true }; const startMs = Date.parse(String(latest.ts)); return { mode: "after-new-session", reason: "latest-completed-new-session", startAt: new Date(startMs).toISOString(), startMs, commandId: latest.commandId ?? null, valuesRedacted: true }; } function applyAnalysisFocus(rows, focus, graceMs = 0) { if (!focus || !Number.isFinite(focus.startMs)) return rows; const minMs = focus.startMs - Math.max(0, Number(graceMs) || 0); return (Array.isArray(rows) ? rows : []).filter((item) => { const value = item && (item.ts || item.observedAt || item.startedAt || item.finishedAt || item.createdAt || item.updatedAt); const ms = Date.parse(String(value || "")); return Number.isFinite(ms) ? ms >= minMs : true; }); } function analysisControlWindow(sampleWindow, focus, graceMs = 0) { if (!sampleWindow || !Number.isFinite(sampleWindow.endMs)) return sampleWindow; if (!focus || !Number.isFinite(focus.startMs)) return sampleWindow; const startMs = Math.max(0, Math.min(Number(sampleWindow.startMs ?? focus.startMs), focus.startMs - Math.max(0, Number(graceMs) || 0))); return { ...sampleWindow, startMs, startAt: new Date(startMs).toISOString() }; } function compactSampleForAnalysis(sample) { if (!sample || typeof sample !== "object") return sample; return { seq: sample.seq ?? null, ts: sample.ts ?? null, reason: sample.reason ?? null, sampleGroupSeq: sample.sampleGroupSeq ?? null, pageId: sample.pageId ?? null, pageRole: sample.pageRole ?? null, commandId: sample.commandId ?? null, observerInitiated: sample.observerInitiated ?? null, url: sample.url ?? null, path: sample.path ?? null, title: sample.title ?? null, routeSessionId: sample.routeSessionId ?? null, activeSessionId: sample.activeSessionId ?? null, messages: compactDomItems(sample.messages), traceRows: compactDomItems(sample.traceRows), loadings: compactLoadingItems(sample.loadings), sessionRail: compactSessionRail(sample.sessionRail), turns: compactDomItems(sample.turns), diagnostics: compactDomItems(sample.diagnostics), composer: compactComposer(sample.composer), projectManagement: compactProjectManagementSample(sample.projectManagement), pageProvenance: compactSamplePageProvenance(sample.pageProvenance), performance: compactPerformanceItems(sample.performance) }; } function compactProjectManagementSample(value) { if (!value || typeof value !== "object") return null; return { pageKind: value.pageKind ?? null, configuredPath: value.configuredPath === true, rootVisible: value.rootVisible === true, mdtodoVisible: value.mdtodoVisible === true, sourceCount: value.sourceCount ?? null, fileCount: value.fileCount ?? null, taskCount: value.taskCount ?? null, taskRefMissingCount: value.taskRefMissingCount ?? null, selectedSourceId: value.selectedSourceId ?? null, selectedFileRef: value.selectedFileRef ?? null, selectedTaskRef: value.selectedTaskRef ?? null, selectedTaskStatus: value.selectedTaskStatus ?? null, sourceSelectVisible: value.sourceSelectVisible === true, fileSelectVisible: value.fileSelectVisible === true, sourceConfigVisible: value.sourceConfigVisible === true, taskEditorVisible: value.taskEditorVisible === true, taskBodyVisible: value.taskBodyVisible === true, taskBody: value.taskBody ?? null, reportLinkCount: value.reportLinkCount ?? 0, reportPreviewVisible: value.reportPreviewVisible === true, reportPreview: value.reportPreview ?? null, reportFullscreenVisible: value.reportFullscreenVisible === true, newTaskDraftVisible: value.newTaskDraftVisible === true, taskStatusCounts: value.taskStatusCounts && typeof value.taskStatusCounts === "object" ? value.taskStatusCounts : {}, launchButtonVisible: value.launchButtonVisible === true, launchButtonEnabled: value.launchButtonEnabled === true, launchButtonText: value.launchButtonText ?? null, blockerCount: value.blockerCount ?? 0, blockers: Array.isArray(value.blockers) ? value.blockers.slice(0, 6) : [], paneGaps: Array.isArray(value.paneGaps) ? value.paneGaps.slice(0, 8).map((item) => ({ name: item?.name ?? null, visible: item?.visible === true, widthPx: item?.widthPx ?? null, heightPx: item?.heightPx ?? null, bottomGapPx: item?.bottomGapPx ?? null, bottomGapRatio: item?.bottomGapRatio ?? null, contentNodeCount: item?.contentNodeCount ?? null, valuesRedacted: true })) : [], workbenchLinkCount: value.workbenchLinkCount ?? 0, valuesRedacted: true }; } function compactSessionRail(value) { if (!value || typeof value !== "object") return null; const items = Array.isArray(value.items) ? value.items.slice(0, 80).map((item) => ({ index: item?.index ?? null, tag: item?.tag ?? null, testId: item?.testId ?? null, role: item?.role ?? null, active: item?.active === true, status: item?.status ?? null, dataStatus: item?.dataStatus ?? null, running: item?.running === true, dataRunning: item?.dataRunning ?? null, ariaBusy: item?.ariaBusy ?? null, 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), activeItem: items.find((item) => item.active) || null, items, fallbackItems, valuesRedacted: true }; } function compactComposer(value) { if (!value || typeof value !== "object") return null; return { inputPresent: value.inputPresent === true, inputDisabled: value.inputDisabled === true, warningPresent: value.warningPresent === true, submitPresent: value.submitPresent === true, submitDisabled: value.submitDisabled === true, submitAction: value.submitAction ?? null, activeStatus: value.activeStatus ?? null, valuesRedacted: true }; } function compactLoadingItems(items) { if (!Array.isArray(items)) return []; return items.map((item) => { if (!item || typeof item !== "object") return item; const rawText = String(item.text ?? item.textPreview ?? ""); return { index: item.index ?? null, tag: item.tag ?? null, testId: item.testId ?? null, role: item.role ?? null, ownerKind: item.ownerKind ?? null, ownerKey: item.ownerKey ?? null, ownerLabel: item.ownerLabel ?? null, owner: item.owner && typeof item.owner === "object" ? { tag: item.owner.tag ?? null, testId: item.owner.testId ?? null, role: item.owner.role ?? null, id: item.owner.id ?? null, className: item.owner.className ?? null, status: item.owner.status ?? null, sessionId: item.owner.sessionId ?? null, messageId: item.owner.messageId ?? null, traceId: item.owner.traceId ?? null, ariaLabel: item.owner.ariaLabel ?? null, } : null, text: limitText(rawText, 400), textPreview: limitText(String(item.textPreview ?? rawText), 240), textHash: item.textHash ?? sha256(rawText), textBytes: item.textBytes ?? Buffer.byteLength(rawText), ownerTextHash: item.ownerTextHash ?? null, ownerTextPreview: item.ownerTextPreview ? limitText(item.ownerTextPreview, 240) : null, }; }); } function compactDomItems(items) { if (!Array.isArray(items)) return []; return items.map(compactDomItem); } function compactDomItem(item) { if (!item || typeof item !== "object") return item; const rawText = String(item.text ?? item.textPreview ?? ""); const preview = String(item.textPreview ?? limitText(rawText, 240)); const hasBodyTextPresent = Object.prototype.hasOwnProperty.call(item, "bodyTextPresent"); const hasFinalResponsePresent = Object.prototype.hasOwnProperty.call(item, "finalResponsePresent"); 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, bodyTextSource: item.bodyTextSource ?? null, bodyTextCandidateCount: Number.isFinite(Number(item.bodyTextCandidateCount)) ? Number(item.bodyTextCandidateCount) : null, bodyTextPresent: hasBodyTextPresent ? item.bodyTextPresent === true : null, bodyTextPreview: item.bodyTextPreview ? limitText(item.bodyTextPreview, 600) : undefined, bodyTextHash: item.bodyTextHash ?? null, bodyTextBytes: Number.isFinite(Number(item.bodyTextBytes)) ? Number(item.bodyTextBytes) : null, finalResponsePresent: hasFinalResponsePresent ? item.finalResponsePresent === true : null, finalResponseTextSource: item.finalResponseTextSource ?? null, finalResponseCandidateCount: Number.isFinite(Number(item.finalResponseCandidateCount)) ? Number(item.finalResponseCandidateCount) : null, finalResponseTextPreview: item.finalResponseTextPreview ? limitText(item.finalResponseTextPreview, 600) : undefined, finalResponseTextHash: item.finalResponseTextHash ?? null, finalResponseTextBytes: Number.isFinite(Number(item.finalResponseTextBytes)) ? Number(item.finalResponseTextBytes) : null, className: item.className ?? null, diagnosticCode: item.diagnosticCode ?? null, source: item.source ?? null, sources: Array.isArray(item.sources) ? item.sources.slice(0, 8) : undefined, text: limitText(rawText, 4000), textPreview: limitText(preview, 600), textHash: item.textHash ?? sha256(rawText), textBytes: item.textBytes ?? Buffer.byteLength(rawText) }; } function compactPerformanceItems(items) { if (!Array.isArray(items)) return []; return items.map((item) => ({ name: item?.name ?? null, initiatorType: item?.initiatorType ?? null, startTime: item?.startTime ?? null, duration: item?.duration ?? null, workerStart: item?.workerStart ?? null, redirectStart: item?.redirectStart ?? null, redirectEnd: item?.redirectEnd ?? null, fetchStart: item?.fetchStart ?? null, domainLookupStart: item?.domainLookupStart ?? null, domainLookupEnd: item?.domainLookupEnd ?? null, connectStart: item?.connectStart ?? null, connectEnd: item?.connectEnd ?? null, secureConnectionStart: item?.secureConnectionStart ?? null, requestStart: item?.requestStart ?? null, responseStart: item?.responseStart ?? null, responseEnd: item?.responseEnd ?? null, transferSize: item?.transferSize ?? null, encodedBodySize: item?.encodedBodySize ?? null, decodedBodySize: item?.decodedBodySize ?? null, nextHopProtocol: item?.nextHopProtocol ?? null, responseStatus: Number.isFinite(Number(item?.responseStatus)) ? Number(item.responseStatus) : null })); } function compactLoadingMetricsForOutput(value) { if (!value || typeof value !== "object") return null; return { summary: value.summary ?? null, longestSegments: Array.isArray(value.segments) ? value.segments.slice(0, 8) : [], owners: Array.isArray(value.owners) ? value.owners.slice(0, 8) : [], timeline: Array.isArray(value.timeline) ? value.timeline.slice(-12) : [], valuesRedacted: true }; } function compactSessionRailTitleMetricsForOutput(value) { if (!value || typeof value !== "object") return null; return { summary: value.summary ?? null, samples: Array.isArray(value.samples) ? value.samples.slice(0, 12) : [], examples: Array.isArray(value.examples) ? value.examples.slice(0, 12) : [], timeline: Array.isArray(value.timeline) ? value.timeline.slice(-12) : [], valuesRedacted: true }; } function compactWorkbenchTurnStateTriadForOutput(value) { if (!value || typeof value !== "object") return null; return { summary: value.summary ?? null, drilldown: value.drilldown ?? null, invalidFullTriads: Array.isArray(value.invalidFullTriads) ? value.invalidFullTriads.slice(0, 8) : [], cardFinalResponseMismatches: Array.isArray(value.cardFinalResponseMismatches) ? value.cardFinalResponseMismatches.slice(0, 8) : [], collectorMissingRows: Array.isArray(value.collectorMissingRows) ? value.collectorMissingRows.slice(0, 8) : [], 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 }; }); } `; }