fix(web-probe): recover observe analyze artifact output

This commit is contained in:
Codex
2026-06-24 11:59:11 +00:00
parent 66c1473375
commit 93ec87cdd1
+60 -2
View File
@@ -8843,8 +8843,12 @@ function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOptions, spec
"exit \"$analyzer_exit\"",
].join("\n");
const result = runTransWorkspaceStdinScript(options.node, spec.workspace, script, options.commandTimeoutSeconds);
const analysis = recoverWebObserveAnalyzeTurnDetails(options, spec, parseJsonObject(result.stdout));
const analysisOk = result.exitCode === 0 && analysis?.ok === true;
const primaryAnalysis = recoverWebObserveAnalyzeTurnDetails(options, spec, parseJsonObject(result.stdout));
const artifactAnalysis = (result.timedOut || result.exitCode !== 0 || primaryAnalysis === null)
? recoverWebObserveAnalyzeFromArtifacts(options, spec, result)
: null;
const analysis = recoverWebObserveAnalyzeTurnDetails(options, spec, artifactAnalysis ?? primaryAnalysis);
const analysisOk = analysis?.ok === true;
const analysisFailure = analysisOk
? null
: {
@@ -8852,6 +8856,7 @@ function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOptions, spec
exitCode: result.exitCode,
timedOut: result.timedOut,
parsedJson: analysis !== null,
recoveredFromArtifacts: artifactAnalysis !== null,
stdoutBytes: result.stdout.length,
stderrBytes: result.stderr.length,
stdoutTail: result.stdout.trim().slice(-1200),
@@ -8910,6 +8915,59 @@ function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOptions, spec
});
}
function recoverWebObserveAnalyzeFromArtifacts(options: NodeWebProbeObserveOptions, spec: HwlabRuntimeLaneSpec, result: { exitCode: number; timedOut: boolean }): Record<string, unknown> | null {
const recoverScript = [
"set -eu",
nodeWebObserveResolveStateDirShell(options),
"analysis_dir=\"$state_dir/analysis\"",
"analysis_stdout=\"$analysis_dir/analyzer-stdout.json\"",
"analysis_stderr=\"$analysis_dir/analyzer-stderr.log\"",
"report_json=\"$analysis_dir/report.json\"",
"report_md=\"$analysis_dir/report.md\"",
`transport_exit=${shellQuote(String(result.exitCode))}`,
`transport_timed_out=${shellQuote(result.timedOut ? "true" : "false")}`,
"node - \"$state_dir\" \"$analysis_stdout\" \"$analysis_stderr\" \"$report_json\" \"$report_md\" \"$transport_exit\" \"$transport_timed_out\" <<'UNIDESK_WEB_OBSERVE_RECOVER_ANALYZE_ARTIFACT'",
"const fs = require('fs');",
"const crypto = require('crypto');",
"const [stateDir, stdoutPath, stderrPath, reportJsonPath, reportMdPath, transportExitRaw, transportTimedOutRaw] = process.argv.slice(2);",
"const readText = (path) => { try { return fs.readFileSync(path, 'utf8'); } catch { return ''; } };",
"const readJson = (path) => { const text = readText(path); if (!text.trim()) return null; try { return JSON.parse(text); } catch { return null; } };",
"const statSize = (path) => { try { return fs.statSync(path).size; } catch { return 0; } };",
"const sha256 = (path) => { const text = readText(path); return text ? 'sha256:' + crypto.createHash('sha256').update(text).digest('hex') : null; };",
"const objectOrNull = (value) => value && typeof value === 'object' && !Array.isArray(value) ? value : null;",
"const arr = (value) => Array.isArray(value) ? value : [];",
"const clip = (value, limit = 160) => value === null || value === undefined ? null : String(value).slice(0, limit);",
"const numberish = (...values) => { for (const value of values) { const n = Number(value); if (Number.isFinite(n)) return value; } return null; };",
"const slimFinding = (item) => { const v = objectOrNull(item) || {}; return { kind: clip(v.kind ?? v.id ?? v.code, 48), code: clip(v.code ?? v.id ?? v.kind, 48), severity: clip(v.severity ?? v.level, 24), level: clip(v.level ?? v.severity, 24), count: v.count ?? v.sampleCount ?? null, sampleCount: v.sampleCount ?? v.count ?? null, summary: clip(v.summary ?? v.message, 180), message: clip(v.message ?? v.summary, 180) }; };",
"const slimRound = (item) => { const v = objectOrNull(item) || {}; return { promptIndex: v.promptIndex ?? null, promptTextHash: clip(v.promptTextHash, 80), sampleCount: v.sampleCount ?? null, firstSeq: v.firstSeq ?? null, lastSeq: v.lastSeq ?? null, lastTotalElapsedSeconds: v.lastTotalElapsedSeconds ?? null, lastRecentUpdateSeconds: v.lastRecentUpdateSeconds ?? null, loadingSamples: v.loadingSamples ?? null, maxLoadingCount: v.maxLoadingCount ?? null, loadingOwnerCount: v.loadingOwnerCount ?? null, diagnosticSamples: v.diagnosticSamples ?? null, terminalSamples: v.terminalSamples ?? null, finalTextSamples: v.finalTextSamples ?? null, turnTimingTotalElapsedZeroResetCount: v.turnTimingTotalElapsedZeroResetCount ?? null, turnTimingTotalElapsedForwardJumpCount: v.turnTimingTotalElapsedForwardJumpCount ?? null, turnTimingTotalElapsedForwardJumpMaxSeconds: v.turnTimingTotalElapsedForwardJumpMaxSeconds ?? null, turnTimingRecentUpdateJumpCount: v.turnTimingRecentUpdateJumpCount ?? null, turnTimingRecentUpdateMaxIncreaseSeconds: v.turnTimingRecentUpdateMaxIncreaseSeconds ?? null }; };",
"const slimTurnColumn = (item) => { const v = objectOrNull(item) || {}; return { label: clip(v.label, 24), source: clip(v.source, 48), pageRole: clip(v.pageRole, 24), pageId: clip(v.pageId, 32), pageEpoch: v.pageEpoch ?? null, promptIndex: v.promptIndex ?? null, lastPromptIndex: v.lastPromptIndex ?? null, firstSeq: v.firstSeq ?? null, lastSeq: v.lastSeq ?? null, traceId: clip(v.traceId, 64), messageId: clip(v.messageId, 64) }; };",
"const slimGroup = (item) => { const v = objectOrNull(item) || {}; return { count: v.count ?? null, method: clip(v.method, 12), status: v.status ?? null, path: clip(v.path ?? v.urlPath ?? v.route, 96), firstAt: v.firstAt ?? null, lastAt: v.lastAt ?? null, text: clip(v.text ?? v.preview, 180), failureKinds: arr(v.failureKinds).slice(0, 4).map((x) => clip(x, 48)), traceIds: arr(v.traceIds).slice(0, 3).map((x) => clip(x, 64)) }; };",
"const slimSlowSample = (item) => { const v = objectOrNull(item) || {}; return { ts: v.ts ?? null, seq: v.seq ?? null, path: clip(v.path ?? v.rawPath, 96), initiatorType: clip(v.initiatorType, 24), durationMs: v.durationMs ?? null, requestToResponseStartMs: v.requestToResponseStartMs ?? v.streamOpenMs ?? null, responseTransferMs: v.responseTransferMs ?? null, timingStatus: clip(v.timingStatus, 16), nextHopProtocol: clip(v.nextHopProtocol, 24), serverTimingNames: arr(v.serverTimingNames).slice(0, 4).map((x) => clip(x, 32)), otelTraceId: clip(v.otelTraceId, 32) }; };",
"const slimSlowApi = (item) => { const v = objectOrNull(item) || {}; return { path: clip(v.path ?? v.route, 96), route: clip(v.route ?? v.path, 96), sampleCount: v.sampleCount ?? null, p95Ms: v.p95Ms ?? v.p95 ?? null, maxMs: v.maxMs ?? v.max ?? null, budgetMs: v.budgetMs ?? null, overBudgetCount: v.overBudgetCount ?? null, overFiveSecondCount: v.overFiveSecondCount ?? null, slowSamples: arr(v.slowSamples).slice(0, 3).map(slimSlowSample) }; };",
"const compactLoading = (value) => { const v = objectOrNull(value); if (!v) return null; const s = objectOrNull(v.summary) || {}; return { summary: s, longestSegments: arr(v.longestSegments ?? v.segments).slice(0, 8), owners: arr(v.owners).slice(0, 8), timeline: arr(v.timeline).slice(-12) }; };",
"const compactSessionRailTitles = (value) => { const v = objectOrNull(value); if (!v) return null; return { summary: objectOrNull(v.summary) || {}, samples: arr(v.samples).slice(0, 8), examples: arr(v.examples).slice(0, 8) }; };",
"const compactTraceOrder = (value) => { const v = objectOrNull(value); if (!v) return null; const s = objectOrNull(v.summary) || {}; return { summary: { sampleCount: v.sampleCount ?? s.sampleCount ?? null, traceRowCount: v.traceRowCount ?? s.traceRowCount ?? null, orderAnomalyCount: v.orderAnomalyCount ?? s.orderAnomalyCount ?? arr(v.orderAnomalies).length, completionNotLastCount: v.completionNotLastCount ?? s.completionNotLastCount ?? arr(v.completionNotLast).length }, orderAnomalies: arr(v.orderAnomalies).slice(0, 8) }; };",
"const compactMetrics = (value) => { const v = objectOrNull(value) || {}; const s = objectOrNull(v.summary) || {}; return { sampleCount: numberish(v.sampleCount, s.sampleCount), rounds: arr(v.rounds ?? v.roundItems).slice(-8).map(slimRound), roundItems: arr(v.roundItems ?? v.rounds).slice(-8).map(slimRound), turnColumns: arr(v.turnColumns).slice(-12).map(slimTurnColumn), turnTimingRows: numberish(v.turnTimingRows, s.turnTimingRows), turnTimingNonMonotonicCount: numberish(v.turnTimingNonMonotonicCount, s.turnTimingNonMonotonicCount), turnTimingTotalElapsedDecreaseCount: numberish(v.turnTimingTotalElapsedDecreaseCount, s.turnTimingTotalElapsedDecreaseCount), turnTimingTotalElapsedZeroResetCount: numberish(v.turnTimingTotalElapsedZeroResetCount, s.turnTimingTotalElapsedZeroResetCount), turnTimingTotalElapsedForwardJumpCount: numberish(v.turnTimingTotalElapsedForwardJumpCount, s.turnTimingTotalElapsedForwardJumpCount), turnTimingTerminalElapsedGrowthCount: numberish(v.turnTimingTerminalElapsedGrowthCount, s.turnTimingTerminalElapsedGrowthCount), turnTimingRecentUpdateJumpCount: numberish(v.turnTimingRecentUpdateJumpCount, v.turnTimingRecentUpdateSawtoothJumpCount, s.turnTimingRecentUpdateJumpCount, s.turnTimingRecentUpdateSawtoothJumpCount), turnTimingRecentUpdateMaxIncreaseSeconds: numberish(v.turnTimingRecentUpdateMaxIncreaseSeconds, s.turnTimingRecentUpdateMaxIncreaseSeconds), loading: compactLoading(v.loading), traceOrder: compactTraceOrder(v.traceOrder), sessionRailTitles: compactSessionRailTitles(v.sessionRailTitles), promptSegments: numberish(v.promptSegments, s.promptSegments), loadingSampleCount: numberish(v.loadingSampleCount, s.loadingSampleCount), loadingMaxCount: numberish(v.loadingMaxCount, s.loadingMaxCount), loadingMaxOwnerCount: numberish(v.loadingMaxOwnerCount, s.loadingMaxOwnerCount), loadingOwnerCount: numberish(v.loadingOwnerCount, s.loadingOwnerCount), loadingLongestContinuousSeconds: numberish(v.loadingLongestContinuousSeconds, s.loadingLongestContinuousSeconds), loadingCurrentContinuousSeconds: numberish(v.loadingCurrentContinuousSeconds, s.loadingCurrentContinuousSeconds), loadingOverFiveSecondSegmentCount: numberish(v.loadingOverFiveSecondSegmentCount, s.loadingOverFiveSecondSegmentCount) }; };",
"const stdoutJson = readJson(stdoutPath);",
"const reportJson = readJson(reportJsonPath);",
"const source = objectOrNull(stdoutJson) || objectOrNull(reportJson);",
"if (!source) { console.log(JSON.stringify({ ok: false, command: 'web-probe-observe analyze', stateDir, error: 'web-probe-analyzer-artifacts-missing', analyzer: { recoveredFrom: 'analysis-artifact-after-transport-timeout', stdoutPath, stderrPath, reportJsonPath, reportMdPath, stdoutBytes: statSize(stdoutPath), stderrBytes: statSize(stderrPath), reportJsonBytes: statSize(reportJsonPath), reportMdBytes: statSize(reportMdPath), transportExitCode: Number(transportExitRaw), transportTimedOut: transportTimedOutRaw === 'true', valuesRedacted: true }, valuesRedacted: true })); process.exit(0); }",
"const recent = objectOrNull(source.windows?.recent) || {};",
"const srcMetrics = objectOrNull(source.sampleMetrics) || objectOrNull(recent.sampleMetrics) || {};",
"const pagePerformance = objectOrNull(source.pagePerformance) || objectOrNull(recent.pagePerformance) || {};",
"const runtimeAlerts = objectOrNull(source.runtimeAlerts) || objectOrNull(recent.runtimeAlerts) || {};",
"const promptNetwork = objectOrNull(source.promptNetwork) || objectOrNull(recent.promptNetwork) || {};",
"const archiveSummary = objectOrNull(source.archiveSummary) || {};",
"const archiveSampleMetrics = objectOrNull(archiveSummary.sampleMetrics) || objectOrNull(source.sampleMetrics?.summary) || objectOrNull(srcMetrics.summary) || {};",
"const slowApis = arr(source.pagePerformanceSlowApi).length > 0 ? arr(source.pagePerformanceSlowApi) : arr(pagePerformance.sameOriginApiByPath).filter((item) => Number(item?.overBudgetCount ?? item?.overFiveSecondCount ?? 0) > 0);",
"const compact = { ok: source.ok === true, command: source.command ?? 'web-probe-observe analyze', stateDir: source.stateDir ?? stateDir, jsonlScope: source.jsonlScope ?? null, alertThresholds: source.alertThresholds ?? null, counts: source.counts ?? null, archiveSummary: { ...archiveSummary, sampleMetrics: archiveSampleMetrics, pagePerformance: objectOrNull(archiveSummary.pagePerformance) || objectOrNull(pagePerformance.summary) || {}, runtimeAlerts: objectOrNull(archiveSummary.runtimeAlerts) || objectOrNull(runtimeAlerts.summary) || {}, redFindings: arr(archiveSummary.redFindings).slice(0, 12).map(slimFinding) }, analysisWindow: source.analysisWindow ?? objectOrNull(recent.summary), sampleMetrics: compactMetrics(srcMetrics), pageProvenance: objectOrNull(source.pageProvenance?.summary) || source.pageProvenance ?? null, pagePerformance: objectOrNull(pagePerformance.summary) || pagePerformance, promptNetwork: objectOrNull(promptNetwork.summary) || promptNetwork, runtimeAlerts: objectOrNull(runtimeAlerts.summary) || runtimeAlerts, runnerErrors: arr(source.runnerErrors).slice(-8), commandFailures: arr(source.commandFailures).slice(-8), httpErrorGroups: arr(source.httpErrorGroups ?? runtimeAlerts.networkHttpErrorsByPath).slice(0, 8).map(slimGroup), requestFailedGroups: arr(source.requestFailedGroups ?? runtimeAlerts.networkRequestFailedByPath).slice(0, 8).map(slimGroup), domDiagnosticGroups: arr(source.domDiagnosticGroups ?? runtimeAlerts.domDiagnosticsByText).slice(0, 5).map(slimGroup), domDiagnosticSamples: arr(source.domDiagnosticSamples ?? runtimeAlerts.domDiagnostics).slice(0, 8).map(slimGroup), consoleAlertGroups: arr(source.consoleAlertGroups ?? runtimeAlerts.consoleAlertsByPath).slice(0, 8).map(slimGroup), consoleAlertSamples: arr(source.consoleAlertSamples ?? runtimeAlerts.consoleAlerts).slice(0, 8).map(slimGroup), turnTimingRecentUpdateJumps: arr(source.turnTimingRecentUpdateJumps ?? srcMetrics.turnTimingRecentUpdateSawtoothJumps).slice(0, 8), turnTimingElapsedZeroResets: arr(source.turnTimingElapsedZeroResets ?? srcMetrics.turnTimingElapsedZeroResets).slice(0, 8), turnTimingTotalElapsedForwardJumps: arr(source.turnTimingTotalElapsedForwardJumps ?? srcMetrics.turnTimingTotalElapsedForwardJumps).slice(0, 8), pagePerformanceSlowApi: slowApis.slice(0, 8).map(slimSlowApi), archivePagePerformanceSlowApi: arr(source.archivePagePerformanceSlowApi).slice(0, 8).map(slimSlowApi), pagePerformancePartialApi: arr(source.pagePerformancePartialApi).slice(0, 8), pagePerformanceSseStreams: arr(source.pagePerformanceSseStreams).slice(0, 8), findings: arr(source.findings).slice(0, 12).map(slimFinding), archiveRedFindings: arr(source.archiveRedFindings ?? archiveSummary.redFindings).slice(0, 12).map(slimFinding), reportJsonPath: source.reportJsonPath ?? reportJsonPath, reportJsonSha256: source.reportJsonSha256 ?? sha256(reportJsonPath), reportMdPath: source.reportMdPath ?? reportMdPath, reportMdSha256: source.reportMdSha256 ?? sha256(reportMdPath), analyzer: { ...(objectOrNull(source.analyzer) || {}), recoveredFrom: 'analysis-artifact-after-transport-timeout', stdoutPath, stderrPath, stdoutBytes: statSize(stdoutPath), stderrBytes: statSize(stderrPath), reportJsonBytes: statSize(reportJsonPath), reportMdBytes: statSize(reportMdPath), transportExitCode: Number(transportExitRaw), transportTimedOut: transportTimedOutRaw === 'true', valuesRedacted: true }, valuesRedacted: true };",
"console.log(JSON.stringify(compact));",
"UNIDESK_WEB_OBSERVE_RECOVER_ANALYZE_ARTIFACT",
].join("\n");
const recovered = parseJsonObject(runTransWorkspaceStdinScript(options.node, spec.workspace, recoverScript, Math.min(options.commandTimeoutSeconds, 30)).stdout);
return recovered;
}
function recoverWebObserveAnalyzeTurnDetails(options: NodeWebProbeObserveOptions, spec: HwlabRuntimeLaneSpec, analysis: Record<string, unknown> | null): Record<string, unknown> | null {
if (analysis === null || typeof analysis !== "object") return analysis;
const sampleMetrics = record(analysis.sampleMetrics);