diff --git a/scripts/code-queue-pr-preflight-contract-test.ts b/scripts/code-queue-pr-preflight-contract-test.ts index 4ea13d8b..25c88d38 100644 --- a/scripts/code-queue-pr-preflight-contract-test.ts +++ b/scripts/code-queue-pr-preflight-contract-test.ts @@ -661,6 +661,135 @@ async function main(): Promise { assertCondition(githubTransientPreflight.retryable === true, "GitHub transient full preflight should be retryable", githubTransientPreflight); assertCondition(githubTransientPreflight.commanderAction === "retry-backoff-or-keep-running-if-heartbeat-fresh", "GitHub transient full preflight should expose bounded commander action", githubTransientPreflight); + const k3sTunnelTimeout = { + ok: false, + status: 504, + body: { + ok: false, + error: "provider HTTP tunnel timed out or disconnected", + stage: "http-tunnel-wait", + serviceId: "k3sctl-adapter", + providerId: "D601", + requestId: "req-k3s-timeout", + detail: { retryable: true }, + }, + }; + const fallbackRuntime = rawRuntimePreflightFixture({ + pullRequestDelivery: { + ...asRecord(rawRuntimePreflightFixture().pullRequestDelivery), + ok: true, + credentials: { + ghTokenPresent: true, + githubTokenPresent: false, + ghHostsConfigPresent: false, + gitCredentialsPresent: false, + }, + }, + }); + const fallbackCalls: string[] = []; + const k3sFallbackRecord = asRecord(await codexPrPreflightQueryForTest(["--remote", "--issue", "20"], { + coreFetch: (path) => { + fallbackCalls.push(path); + if (path.includes("remote=1")) return k3sTunnelTimeout; + return { ok: true, status: 200, body: { runtimePreflight: fallbackRuntime } }; + }, + })); + assertCondition(fallbackCalls.length === 2, "k3s tunnel timeout should trigger exactly one fallback runtime-preflight fetch", fallbackCalls); + assertCondition(fallbackCalls[0] === "/api/microservices/code-queue/proxy/api/runtime-preflight?remote=1&issue=20", "primary path should request remote runtime probes", fallbackCalls); + assertCondition(fallbackCalls[1] === "/api/microservices/code-queue/proxy/api/runtime-preflight?issue=20", "fallback path should omit remote=1 but keep issue query", fallbackCalls); + assertCondition(k3sFallbackRecord.ok === true, "transport-degraded fallback should preserve observed runtime preflight result", k3sFallbackRecord); + assertCondition(k3sFallbackRecord.failureKind === null, "healthy fallback runtime should not be misclassified as infra-blocked", k3sFallbackRecord); + const k3sFallbackSummary = asRecord(k3sFallbackRecord.summary); + const k3sFallbackTransport = asRecord(k3sFallbackRecord.transportObservation); + const k3sFallbackRuntime = asRecord(k3sFallbackRecord.runtimeObservation); + assertCondition(k3sFallbackSummary.transportState === "transport-degraded", "summary should distinguish degraded transport from runtime observation", k3sFallbackSummary); + assertCondition(k3sFallbackSummary.runtimePreflightState === "runtime-preflight-observed", "summary should expose observed runtime preflight after fallback", k3sFallbackSummary); + assertCondition(k3sFallbackTransport.fallbackUsed === true && k3sFallbackTransport.state === "transport-degraded", "transport observation should mark fallback used", k3sFallbackTransport); + assertCondition(asRecord(k3sFallbackTransport.primary).stage === "http-tunnel-wait", "primary transport summary should retain bounded tunnel stage", k3sFallbackTransport.primary); + assertCondition(asRecord(k3sFallbackTransport.primary).serviceId === "k3sctl-adapter", "primary transport summary should retain service id", k3sFallbackTransport.primary); + assertCondition(k3sFallbackRuntime.source === "fallback-runtime-preflight" && k3sFallbackRuntime.observed === true, "runtime observation should identify fallback source", k3sFallbackRuntime); + assertCondition(k3sFallbackRecord.preflight === undefined, "transport fallback default output should omit detailed preflight", k3sFallbackRecord); + assertCondition(JSON.stringify(k3sFallbackRecord).length < 14000, "transport fallback default output should stay compact", { chars: JSON.stringify(k3sFallbackRecord).length }); + + const bothPathsFailed = asRecord(await codexPrPreflightQueryForTest(["--remote"], { + coreFetch: (path) => path.includes("remote=1") + ? k3sTunnelTimeout + : { + ok: false, + status: 503, + body: { + ok: false, + error: "code-queue runtime unavailable", + stage: "runtime-preflight", + serviceId: "code-queue", + }, + }, + })); + assertCondition(bothPathsFailed.ok === false, "both unavailable paths should fail", bothPathsFailed); + assertCondition(bothPathsFailed.failureKind === "proxy-gap", "both unavailable paths should remain an infra/proxy observation gap", bothPathsFailed); + assertCondition(bothPathsFailed.degradedReason === "runtime-preflight-transport-unavailable", "both unavailable paths should expose stable degraded reason", bothPathsFailed); + assertCondition(bothPathsFailed.runnerDisposition === "infra-blocked", "both unavailable paths remain infra-blocked", bothPathsFailed); + assertCondition(bothPathsFailed.blockingDisposition === "control-plane-observation-gap", "both unavailable paths should not pretend runner capability was observed", bothPathsFailed); + const bothSummary = asRecord(bothPathsFailed.summary); + const bothTransport = asRecord(bothPathsFailed.transportObservation); + const bothRuntime = asRecord(bothPathsFailed.runtimeObservation); + const bothScheduler = asRecord(bothPathsFailed.schedulerPreflight); + assertCondition(bothSummary.transportState === "transport-blocked", "both paths failed summary should report transport-blocked", bothSummary); + assertCondition(bothSummary.runtimePreflightState === "runtime-preflight-unobserved", "both paths failed summary should report runtime unobserved", bothSummary); + assertCondition(bothSummary.schedulerPreflightAuthReady === null, "transport-blocked should not infer scheduler auth missing", bothSummary); + assertCondition(bothTransport.fallbackUsed === false && bothRuntime.observed === false, "both paths failed should expose fallback attempted but runtime unobserved", { bothTransport, bothRuntime }); + assertCondition(bothScheduler.authReady === null && Array.isArray(bothScheduler.missing) && bothScheduler.missing.length === 0, "transport-blocked scheduler preflight should be unknown, not auth-missing", bothScheduler); + + const skillsFallbackRecord = asRecord(await codexPrPreflightQueryForTest(["--remote"], { + coreFetch: (path) => path.includes("remote=1") + ? k3sTunnelTimeout + : { + ok: true, + status: 200, + body: { + runtimePreflight: rawRuntimePreflightFixture({ + ok: false, + skills: { + ok: false, + runnerUsable: false, + contractOk: false, + blocker: "skills-target-missing", + degradedReason: "skills-target-missing", + resolution: { hostRolloutRequired: true }, + valuesPrinted: false, + }, + skillsSync: { + ok: false, + degraded: true, + blocker: "skills-target-missing", + dryRun: true, + mutation: false, + valuesPrinted: false, + }, + pullRequestDelivery: { + ...asRecord(rawRuntimePreflightFixture().pullRequestDelivery), + ok: true, + credentials: { + ghTokenPresent: true, + githubTokenPresent: false, + ghHostsConfigPresent: false, + gitCredentialsPresent: false, + }, + }, + }), + }, + }, + })); + assertCondition(skillsFallbackRecord.ok === false, "skills blocker observed through fallback should fail preflight", skillsFallbackRecord); + assertCondition(skillsFallbackRecord.failureKind === "runner-skills-blocker", "skills-target-missing should classify as runner capability degraded, not proxy failure", skillsFallbackRecord); + assertCondition(skillsFallbackRecord.degradedReason === "skills-target-missing", "skills degraded reason should preserve skills-target-missing", skillsFallbackRecord); + assertCondition(skillsFallbackRecord.blockingDisposition === "runner-capability-degraded", "skills blocker should expose runner capability degraded disposition", skillsFallbackRecord); + assertCondition(asRecord(skillsFallbackRecord.summary).transportState === "transport-degraded", "skills fallback should still expose degraded transport", skillsFallbackRecord.summary); + assertCondition(asRecord(skillsFallbackRecord.summary).status === "runner-capability-degraded", "skills fallback summary should separate runner capability degradation", skillsFallbackRecord.summary); + assertCondition(asRecord(skillsFallbackRecord.transportObservation).fallbackUsed === true, "skills fallback should mark transport fallback used", skillsFallbackRecord.transportObservation); + assertCondition(asRecord(skillsFallbackRecord.runtimeObservation).observed === true, "skills fallback should report runtime observed", skillsFallbackRecord.runtimeObservation); + assertCondition(asRecord(skillsFallbackRecord.skillsContract).degradedReason === "skills-target-missing", "skills contract should expose missing target", skillsFallbackRecord.skillsContract); + let observedDryRunPath = ""; const dryRunContract = await codexPrPreflightQueryForTest([ "--remote", @@ -934,6 +1063,9 @@ async function main(): Promise { "auth missing returns auth-missing with broker/auth-broker-needed", "proxy failures return proxy-gap", "GitHub DNS/API failures return github-transient with retry/backoff guidance", + "k3sctl HTTP tunnel 504 falls back to lightweight runtime-preflight observation", + "double runtime-preflight transport failure remains infra-blocked without inferring auth gaps", + "skills-target-missing fallback is runner-capability-degraded, not proxy unavailable", "git remote failures return git-remote-gap", "combined push/PR create dry-run contract stays read-only and separates system gh from UniDesk gh CLI", "broker-issued token, env-token, and missing-token branches expose authBroker source/capability/nextAction", diff --git a/scripts/src/code-queue.ts b/scripts/src/code-queue.ts index ece16db9..56732af6 100644 --- a/scripts/src/code-queue.ts +++ b/scripts/src/code-queue.ts @@ -6869,38 +6869,49 @@ function compactPrPreflightCommanderView(record: Record, option const prCreateDryRun = asRecord(capability?.prCreateDryRun); const unsupportedMergeBoundary = asRecord(capability?.unsupportedMergeBoundary); const commands = prPreflightCommandSet(record, options); + const schedulerAuthObserved = tokenCoverage !== null; const schedulerAuthReady = tokenCoverage?.ok === true; const activeRunnerTokenCandidatePresent = activeRunnerDevContainer.ok === true; const failureKind = record.failureKind ?? (schedulerAuthReady ? null : "auth-missing"); const degradedReason = record.degradedReason ?? (schedulerAuthReady ? null : "auth-broker-needed"); + const blockingDisposition = record.blockingDisposition ?? (failureKind === "runner-skills-blocker" ? "runner-capability-degraded" : undefined); const githubTransient = asRecord(record.githubTransient); + const transportObservation = asRecord(record.transportObservation); + const runtimeObservation = asRecord(record.runtimeObservation); + const runtimePreflightObserved = runtimeObservation?.observed === true || record.preflight !== undefined; return { ok: record.ok === true, runnerDisposition: record.runnerDisposition ?? (record.ok === true ? "ready" : "infra-blocked"), failureKind, degradedReason, + ...(blockingDisposition === undefined ? {} : { blockingDisposition }), + ...(typeof record.message === "string" ? { message: record.message } : {}), ...(record.retryable === true ? { retryable: true } : {}), ...(typeof record.commanderAction === "string" ? { commanderAction: record.commanderAction } : {}), ...(githubTransient === null ? {} : { githubTransient }), summary: { - status: record.ok === true ? "ready" : "blocked", - schedulerPreflightAuthReady: schedulerAuthReady, + status: record.ok === true ? "ready" : failureKind === "runner-skills-blocker" ? "runner-capability-degraded" : "blocked", + transportState: transportObservation?.state ?? "primary-ok", + runtimePreflightState: runtimeObservation?.state ?? (record.preflight === undefined ? "runtime-preflight-unobserved" : "runtime-preflight-observed"), + schedulerPreflightAuthReady: schedulerAuthObserved ? schedulerAuthReady : null, schedulerPreflightScope: tokenCoverage?.scope ?? "scheduler-runner-env", activeRunnerTokenCandidatePresent, activeRunnerScope: activeRunnerDevContainer.scope ?? "current-cli-process", - interpretation: schedulerAuthReady + interpretation: !runtimePreflightObserved + ? "runtime preflight was not observed; do not infer scheduler auth or active runner PR capability from this transport failure" + : schedulerAuthReady ? "scheduler preflight auth is ready; still use active-runner dry-runs before writes" : "scheduler auth is missing for preflight admission only; this does not prove the active runner/dev container cannot create or comment PRs", }, schedulerPreflight: { scope: tokenCoverage?.scope ?? "scheduler-runner-env", - authReady: schedulerAuthReady, + authReady: schedulerAuthObserved ? schedulerAuthReady : null, authSource: tokenCoverage?.source ?? null, credentialSource: tokenCoverage?.credentialSource ?? null, - missing: Array.isArray(tokenCoverage?.missing) ? tokenCoverage.missing.map(String) : [], - failureKind: schedulerAuthReady ? null : failureKind, - degradedReason: schedulerAuthReady ? null : degradedReason, + missing: schedulerAuthObserved && Array.isArray(tokenCoverage?.missing) ? tokenCoverage.missing.map(String) : [], + failureKind: schedulerAuthObserved ? schedulerAuthReady ? null : failureKind : null, + degradedReason: schedulerAuthObserved ? schedulerAuthReady ? null : degradedReason : null, authBroker: authBroker === null ? null : { ok: authBroker.ok ?? schedulerAuthReady, source: authBroker.source ?? null, @@ -6947,6 +6958,8 @@ function compactPrPreflightCommanderView(record: Record, option recommendedActions: compactRecommendedActions(record), upstream: record.upstream ?? null, controlPlane: record.controlPlane ?? null, + transportObservation: transportObservation ?? undefined, + runtimeObservation: runtimeObservation ?? undefined, observationGap: record.observationGap ?? undefined, localObservationGap: record.localObservationGap ?? undefined, localObservationSummary: record.localObservationSummary ?? undefined, @@ -7064,6 +7077,188 @@ function maybeFullPrPreflightObservations(options: CodexPrPreflightOptions, loca }; } +function prPreflightRuntimeProxyPath(options: CodexPrPreflightOptions, includeRemote: boolean): string { + return codeQueueProxyPath(`/api/runtime-preflight${queryString({ + remote: includeRemote ? 1 : undefined, + pushDryRun: options.pushDryRun ? 1 : undefined, + pushDryRunRef: options.pushDryRunRef, + prCreateDryRun: options.prCreateDryRun ? 1 : undefined, + prCreateDryRunHead: options.prCreateDryRunHead, + issue: options.issueNumber, + })}`); +} + +function runtimePreflightFromResponse(response: unknown): { response: Record; body: Record; preflight: Record } | null { + const record = asRecord(response); + const body = asRecord(record?.body); + const preflight = asRecord(body?.runtimePreflight); + if (record?.ok !== true || preflight === null) return null; + return { response: record, body: body ?? {}, preflight }; +} + +function compactTransportErrorText(record: Record | null, body: Record | null): string { + const candidates = [ + body?.error, + body?.providerError, + body?.message, + record?.message, + record?.commandStderrTail, + record?.stderrTail, + record?.stdoutTail, + ]; + return candidates.find((item): item is string => typeof item === "string" && item.length > 0) ?? ""; +} + +function compactPrPreflightTransportResponse(path: string, response: unknown): Record { + const record = asRecord(response); + const body = responseBody(record); + const detail = asRecord(body?.detail) ?? asRecord(body?.details); + const text = compactTransportErrorText(record, body); + return { + path, + ok: record?.ok === true, + status: responseStatus(record), + bodyOk: body?.ok ?? null, + stage: typeof body?.stage === "string" ? body.stage : null, + serviceId: typeof body?.serviceId === "string" ? body.serviceId : typeof body?.service === "string" ? body.service : null, + providerId: typeof body?.providerId === "string" ? body.providerId : null, + requestId: typeof body?.requestId === "string" ? body.requestId : null, + retryable: body?.retryable ?? detail?.retryable ?? null, + runtimePreflightPresent: asRecord(body?.runtimePreflight) !== null, + errorPreview: text.length === 0 ? null : compactInlinePreview(text, 240), + }; +} + +function prPreflightRuntimeTransportDegraded(response: unknown): boolean { + const record = asRecord(response); + if (record?.ok === true) return false; + const status = responseStatus(record); + const body = responseBody(record); + const text = [ + compactTransportErrorText(record, body), + typeof body?.stage === "string" ? body.stage : "", + typeof body?.serviceId === "string" ? body.serviceId : "", + typeof body?.providerId === "string" ? body.providerId : "", + ].join(" ").toLowerCase(); + if (status === 504) return true; + return (status === 502 || status === 503) + && /\b(?:http-tunnel-wait|tunnel|timed out|timeout|disconnected|provider|k3sctl-adapter)\b/u.test(text); +} + +function prPreflightTransportObservation( + state: "transport-degraded" | "transport-blocked", + primaryPath: string, + primaryResponse: unknown, + fallbackPath: string, + fallbackResponse: unknown, + fallbackUsed: boolean, +): Record { + return { + state, + degraded: true, + primary: compactPrPreflightTransportResponse(primaryPath, primaryResponse), + fallback: compactPrPreflightTransportResponse(fallbackPath, fallbackResponse), + fallbackAttempted: true, + fallbackUsed, + interpretation: fallbackUsed + ? "transport degraded on the remote runtime probe; runtime preflight was observed through the lightweight Code Queue runtime path" + : "transport blocked on both runtime-preflight observation paths; runner PR capability was not observed", + }; +} + +function prPreflightRuntimeObservation( + options: CodexPrPreflightOptions, + preflight: Record | null, + source: "primary-runtime-preflight" | "fallback-runtime-preflight" | null, + primaryPath: string, + fallbackPath: string | null, +): Record { + const pull = asRecord(preflight?.pullRequestDelivery); + return { + state: preflight === null ? "runtime-preflight-unobserved" : "runtime-preflight-observed", + observed: preflight !== null, + source, + remoteProbesRequested: options.remote, + remoteProbesObserved: asRecord(pull?.remote) !== null, + primaryPath, + fallbackPath, + }; +} + +function prPreflightObservedRuntimeResult( + options: CodexPrPreflightOptions, + response: Record, + preflight: Record, + controlPlaneMode: "local-backend-core" | "remote-frontend", + runtimeSource: "primary-runtime-preflight" | "fallback-runtime-preflight", + primaryPath: string, + fallbackPath: string | null, + transportObservation?: Record, +): Record { + const compact = compactPrRuntimePreflight(preflight, options); + return compactPrPreflightCommanderView({ + ok: compact.ok, + runnerDisposition: compact.runnerDisposition, + blockingDisposition: compact.blockingDisposition, + failureKind: compact.failureKind ?? null, + degradedReason: compact.degradedReason ?? null, + ...(compact.retryable === true ? { retryable: true } : {}), + ...(typeof compact.commanderAction === "string" ? { commanderAction: compact.commanderAction } : {}), + ...(asRecord(compact.githubTransient) === null ? {} : { githubTransient: compact.githubTransient }), + authScopeSummary: compact.authScopeSummary, + scopeBoundary: compact.scopeBoundary, + activeRunnerDevContainer: compact.activeRunnerDevContainer, + recommendedActions: compact.recommendedActions, + skillsContract: compact.skillsContract, + upstream: { ok: response.ok, status: response.status }, + controlPlane: { + mode: controlPlaneMode, + localBackendCoreMissing: false, + remoteFallbackUsed: false, + transportFallbackUsed: transportObservation?.fallbackUsed === true, + }, + ...(transportObservation === undefined ? {} : { transportObservation }), + runtimeObservation: prPreflightRuntimeObservation(options, preflight, runtimeSource, primaryPath, fallbackPath), + preflight: compact, + }, options); +} + +function prPreflightTransportBlockedResult( + options: CodexPrPreflightOptions, + controlPlaneMode: "local-backend-core" | "remote-frontend", + primaryPath: string, + primaryResponse: unknown, + fallbackPath: string, + fallbackResponse: unknown, +): Record { + const transportObservation = prPreflightTransportObservation("transport-blocked", primaryPath, primaryResponse, fallbackPath, fallbackResponse, false); + return compactPrPreflightCommanderView({ + ok: false, + runnerDisposition: "infra-blocked", + blockingDisposition: "control-plane-observation-gap", + failureKind: "proxy-gap", + degradedReason: "runtime-preflight-transport-unavailable", + message: "Code Queue runtime preflight could not be observed through either the remote probe path or the lightweight runtime fallback", + upstream: { + ok: false, + status: responseStatus(asRecord(fallbackResponse)) ?? responseStatus(asRecord(primaryResponse)), + }, + controlPlane: { + mode: controlPlaneMode, + localBackendCoreMissing: false, + remoteFallbackUsed: false, + transportFallbackUsed: false, + }, + transportObservation, + runtimeObservation: prPreflightRuntimeObservation(options, null, null, primaryPath, fallbackPath), + commands: { + retry: "bun scripts/cli.ts codex pr-preflight --remote", + fallbackRaw: "bun scripts/cli.ts microservice proxy code-queue /api/runtime-preflight --raw", + fullDetail: "bun scripts/cli.ts codex pr-preflight --remote --full", + }, + }, options); +} + function compactPrRuntimePreflight(preflight: Record, options: CodexPrPreflightOptions): Record { const pull = asRecord(preflight.pullRequestDelivery) ?? {}; const skills = compactSkillsStatus(preflight.skills); @@ -7116,10 +7311,12 @@ function compactPrRuntimePreflight(preflight: Record, options: const activeRunnerDevContainer = activeRunnerDevContainerCapability(); const authScopeSummary = prPreflightAuthScopeSummary(tokenCoverage, activeRunnerDevContainer); const recommendedActions = prPreflightRecommendedActions(tokenCoverage); + const blockingDisposition = failureKind === "runner-skills-blocker" ? "runner-capability-degraded" : undefined; const result: Record = { ok, failureKind, degradedReason, + ...(blockingDisposition === undefined ? {} : { blockingDisposition }), checkedAt: preflight.checkedAt ?? pull.checkedAt ?? null, runner: { serviceId: "code-queue", @@ -7344,15 +7541,18 @@ function codeQueuePrPreflight(optionArgs: string[] = [], transport: CodeQueuePrP const options = parsePrPreflightOptions(optionArgs); const config = transport.config ?? null; const fetcher = transport.coreFetch ?? coreInternalFetch; - const path = codeQueueProxyPath(`/api/runtime-preflight${queryString({ - remote: options.remote ? 1 : undefined, - pushDryRun: options.pushDryRun ? 1 : undefined, - pushDryRunRef: options.pushDryRunRef, - prCreateDryRun: options.prCreateDryRun ? 1 : undefined, - prCreateDryRunHead: options.prCreateDryRunHead, - issue: options.issueNumber, - })}`); - const localResponse = fetcher(path); + const primaryPath = prPreflightRuntimeProxyPath(options, options.remote); + const localResponse = fetcher(primaryPath); + if (options.remote && prPreflightRuntimeTransportDegraded(localResponse)) { + const fallbackPath = prPreflightRuntimeProxyPath(options, false); + const fallbackResponse = fetcher(fallbackPath); + const fallbackRuntime = runtimePreflightFromResponse(fallbackResponse); + if (fallbackRuntime !== null) { + const transportObservation = prPreflightTransportObservation("transport-degraded", primaryPath, localResponse, fallbackPath, fallbackResponse, true); + return prPreflightObservedRuntimeResult(options, fallbackRuntime.response, fallbackRuntime.preflight, "local-backend-core", "fallback-runtime-preflight", primaryPath, fallbackPath, transportObservation); + } + return prPreflightTransportBlockedResult(options, "local-backend-core", primaryPath, localResponse, fallbackPath, fallbackResponse); + } const localRecord = asRecord(localResponse); const localTargetStackMissing = localRecord?.ok === false && localRecord.failureKind === "target-stack-not-running" @@ -7495,28 +7695,7 @@ function codeQueuePrPreflight(optionArgs: string[] = [], transport: CodeQueuePrP }, }; } - const compact = compactPrRuntimePreflight(preflight, options); - return compactPrPreflightCommanderView({ - ok: compact.ok, - runnerDisposition: compact.runnerDisposition, - failureKind: compact.failureKind ?? null, - degradedReason: compact.degradedReason ?? null, - ...(compact.retryable === true ? { retryable: true } : {}), - ...(typeof compact.commanderAction === "string" ? { commanderAction: compact.commanderAction } : {}), - ...(asRecord(compact.githubTransient) === null ? {} : { githubTransient: compact.githubTransient }), - authScopeSummary: compact.authScopeSummary, - scopeBoundary: compact.scopeBoundary, - activeRunnerDevContainer: compact.activeRunnerDevContainer, - recommendedActions: compact.recommendedActions, - skillsContract: compact.skillsContract, - upstream: { ok: response.ok, status: response.status }, - controlPlane: { - mode: "local-backend-core", - localBackendCoreMissing: false, - remoteFallbackUsed: false, - }, - preflight: compact, - }, options); + return prPreflightObservedRuntimeResult(options, response, preflight, "local-backend-core", "primary-runtime-preflight", primaryPath, null); } export function codexPrPreflightQueryForTest(optionArgs: string[], transport: CodeQueuePrPreflightTransport = {}): unknown { @@ -7525,15 +7704,19 @@ export function codexPrPreflightQueryForTest(optionArgs: string[], transport: Co export async function codexPrPreflightQueryAsync(optionArgs: string[], fetcher: AsyncCodexResponseFetcher): Promise { const options = parsePrPreflightOptions(optionArgs); - const path = codeQueueProxyPath(`/api/runtime-preflight${queryString({ - remote: options.remote ? 1 : undefined, - pushDryRun: options.pushDryRun ? 1 : undefined, - pushDryRunRef: options.pushDryRunRef, - prCreateDryRun: options.prCreateDryRun ? 1 : undefined, - prCreateDryRunHead: options.prCreateDryRunHead, - issue: options.issueNumber, - })}`); - const response = asRecord(await fetcher(path)); + const primaryPath = prPreflightRuntimeProxyPath(options, options.remote); + const primaryResponse = await fetcher(primaryPath); + if (options.remote && prPreflightRuntimeTransportDegraded(primaryResponse)) { + const fallbackPath = prPreflightRuntimeProxyPath(options, false); + const fallbackResponse = await fetcher(fallbackPath); + const fallbackRuntime = runtimePreflightFromResponse(fallbackResponse); + if (fallbackRuntime !== null) { + const transportObservation = prPreflightTransportObservation("transport-degraded", primaryPath, primaryResponse, fallbackPath, fallbackResponse, true); + return prPreflightObservedRuntimeResult(options, fallbackRuntime.response, fallbackRuntime.preflight, "remote-frontend", "fallback-runtime-preflight", primaryPath, fallbackPath, transportObservation); + } + return prPreflightTransportBlockedResult(options, "remote-frontend", primaryPath, primaryResponse, fallbackPath, fallbackResponse); + } + const response = asRecord(primaryResponse); if (response?.ok !== true) throw new Error(upstreamError(response)); const body = asRecord(response.body); const preflight = asRecord(body?.runtimePreflight); @@ -7555,28 +7738,7 @@ export async function codexPrPreflightQueryAsync(optionArgs: string[], fetcher: }, }; } - const compact = compactPrRuntimePreflight(preflight, options); - return compactPrPreflightCommanderView({ - ok: compact.ok, - runnerDisposition: compact.runnerDisposition, - failureKind: compact.failureKind ?? null, - degradedReason: compact.degradedReason ?? null, - ...(compact.retryable === true ? { retryable: true } : {}), - ...(typeof compact.commanderAction === "string" ? { commanderAction: compact.commanderAction } : {}), - ...(asRecord(compact.githubTransient) === null ? {} : { githubTransient: compact.githubTransient }), - authScopeSummary: compact.authScopeSummary, - scopeBoundary: compact.scopeBoundary, - activeRunnerDevContainer: compact.activeRunnerDevContainer, - recommendedActions: compact.recommendedActions, - skillsContract: compact.skillsContract, - upstream: { ok: response.ok, status: response.status }, - controlPlane: { - mode: "remote-frontend", - localBackendCoreMissing: false, - remoteFallbackUsed: false, - }, - preflight: compact, - }, options); + return prPreflightObservedRuntimeResult(options, response, preflight, "remote-frontend", "primary-runtime-preflight", primaryPath, null); } export function codexSubmitRoutingRecommendationForTest(prompt: string, model?: string): SubmitRoutingRecommendation {