fix(cli): recover child json from dumps and artifacts

This commit is contained in:
Codex
2026-07-02 18:27:49 +00:00
parent fa4e1d236d
commit e2500ba418
3 changed files with 429 additions and 11 deletions
+58 -11
View File
@@ -11,6 +11,7 @@ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "
import { dirname, join } from "node:path";
import { repoRoot, rootPath, type Config } from "../config";
import { runCommand, type CommandResult } from "../command";
import { resolveCliChildJsonObject } from "../cli-child-json-recovery";
import { startJob } from "../jobs";
import { classifySshTcpPoolFailure } from "../ssh";
import { HWLAB_NODE_CONTROL_PLANE_CONFIG_PATH, hwlabNodeControlPlaneInfraHelp, runHwlabNodeControlPlaneInfra } from "../hwlab-node-control-plane";
@@ -2493,20 +2494,48 @@ export function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOption
"exit \"$analyzer_exit\"",
].join("\n");
const result = runTransWorkspaceStdinScript(options.node, spec.workspace, script, options.commandTimeoutSeconds);
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 analysisArtifactDir = options.stateDir ? join(options.stateDir, "analysis") : "";
const reportJsonPath = analysisArtifactDir ? join(analysisArtifactDir, "report.json") : null;
const collectReportCommand = webObserveAnalyzeCollectCommand(options, "analysis/report.json");
const stdoutResolution = resolveCliChildJsonObject({
stdout: result.stdout,
stderr: result.stderr,
requestedStdoutType: "web-probe observe analyze compact JSON",
acceptParsed: isWebObserveAnalyzeJsonContract,
forceArtifactFallbackReason: result.timedOut ? "remote-command-timeout" : result.exitCode !== 0 ? "remote-command-failed" : null,
artifactFallback: {
path: reportJsonPath,
nextCommand: collectReportCommand,
read: () => {
const recovered = recoverWebObserveAnalyzeFromArtifacts(options, spec, result);
if (recovered !== null) {
return {
ok: true,
value: recovered,
path: stringOrNullValue(recordValue(recovered).reportJsonPath) ?? reportJsonPath,
reason: "analysis-artifact-contract",
};
}
return { ok: false, reason: "analysis-artifact-missing-or-invalid", path: reportJsonPath };
},
},
});
const artifactAnalysis = stdoutResolution.source === "artifact" ? stdoutResolution.parsed : null;
const analysis = recoverWebObserveAnalyzeTurnDetails(options, spec, stdoutResolution.parsed);
const analysisOk = analysis?.ok === true;
const stdoutDiagnostics = stdoutResolution.diagnostics;
const failureReason = analysis === null
? stringOrNullValue(recordValue(stdoutDiagnostics).fallbackReason) ?? "analyzer-output-not-json"
: "analyzer-reported-not-ok";
const analysisFailure = analysisOk
? null
: {
reason: analysis === null ? "analyzer-output-not-json" : "analyzer-reported-not-ok",
reason: failureReason,
exitCode: result.exitCode,
timedOut: result.timedOut,
parsedJson: analysis !== null,
recoveredFromArtifacts: artifactAnalysis !== null,
stdoutRecovery: stdoutDiagnostics,
stdoutBytes: result.stdout.length,
stderrBytes: result.stderr.length,
stdoutTail: result.stdout.trim().slice(-1200),
@@ -2515,12 +2544,11 @@ export function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOption
observerId: webObserveIdFromOptions(options),
valuesRedacted: true,
};
const analysisArtifactDir = options.stateDir ? join(options.stateDir, "analysis") : "";
const failureAnalysis = analysis ?? (analysisFailure ? {
ok: false,
command: "web-probe-observe analyze",
stateDir: options.stateDir,
reportJsonPath: analysisArtifactDir ? join(analysisArtifactDir, "report.json") : null,
reportJsonPath,
reportMdPath: analysisArtifactDir ? join(analysisArtifactDir, "report.md") : null,
analyzer: {
exitCode: result.exitCode,
@@ -2532,6 +2560,7 @@ export function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOption
stdoutTail: result.stdout.trim().slice(-1200),
stderrTail: result.stderr.trim().slice(-1200),
recoveredFrom: "analyzer-timeout-failure-contract",
stdoutRecovery: stdoutDiagnostics,
valuesRedacted: true,
},
findings: [{
@@ -2543,12 +2572,15 @@ export function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOption
: "observe analyze failed before producing a compact report; inspect analyzer stdout/stderr artifacts under the observer analysis directory",
}],
next: {
collectAnalyzerStdout: options.stateDir ? `bun scripts/cli.ts web-probe observe collect --node ${options.node} --lane ${options.lane} --state-dir ${options.stateDir} --file analysis/analyzer-stdout.json` : null,
collectAnalyzerStderr: options.stateDir ? `bun scripts/cli.ts web-probe observe collect --node ${options.node} --lane ${options.lane} --state-dir ${options.stateDir} --file analysis/analyzer-stderr.log` : null,
collectReportJson: collectReportCommand,
collectAnalyzerStdout: webObserveAnalyzeCollectCommand(options, "analysis/analyzer-stdout.json"),
collectAnalyzerStderr: webObserveAnalyzeCollectCommand(options, "analysis/analyzer-stderr.log"),
valuesRedacted: true,
},
valuesRedacted: true,
} : null);
const compactResult = analysis === null ? compactCommandResultWithStdoutTail(result) : compactCommandResult(result);
compactResult.stdoutRecovery = stdoutDiagnostics;
const payload = {
ok: analysisOk,
status: analysisOk ? "analyzed" : "blocked",
@@ -2562,13 +2594,28 @@ export function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOption
alertThresholds,
browserFreezePolicy,
wrapper: buildWebObserveWrapperForObserveOptions("analyze", options, spec.workspace),
result: analysis === null ? compactCommandResultWithStdoutTail(result) : compactCommandResult(result),
result: compactResult,
full: options.full,
valuesRedacted: true,
};
return options.raw ? compactWebObserveAnalyzePayloadForRaw(payload, options.compactRaw) : withWebObserveAnalyzeRendered(payload);
}
function webObserveAnalyzeCollectCommand(options: NodeWebProbeObserveOptions, file: string): string {
const selector = options.stateDir ? `--state-dir ${shellQuote(options.stateDir)}` : webObserveIdFromOptions(options);
return `bun scripts/cli.ts web-probe observe collect ${selector} --node ${options.node} --lane ${options.lane} --view files --file ${shellQuote(file)}`;
}
function isWebObserveAnalyzeJsonContract(value: Record<string, unknown>): boolean {
return value.ok === true
|| value.ok === false
|| typeof value.reportJsonPath === "string"
|| typeof value.reportMdPath === "string"
|| typeof value.error === "string"
|| Object.keys(recordValue(value.analyzer)).length > 0
|| Object.keys(recordValue(value.counts)).length > 0;
}
function compactWebObserveAnalyzePayloadForRaw(payload: Record<string, unknown>, compactRaw: boolean): Record<string, unknown> {
if (!compactRaw) return payload;
const analysis = payload.analysis && typeof payload.analysis === "object" && !Array.isArray(payload.analysis)