fix codex pr-preflight remote fallback
This commit is contained in:
+261
-11
@@ -1,4 +1,5 @@
|
||||
import { mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
||||
import { runCommand } from "./command";
|
||||
import { type UniDeskConfig, repoRoot, rootPath } from "./config";
|
||||
import { coreInternalFetch } from "./microservices";
|
||||
import { previewJson } from "./preview";
|
||||
@@ -220,6 +221,14 @@ interface CodexPrPreflightOptions {
|
||||
full: boolean;
|
||||
}
|
||||
|
||||
type CodeQueuePrPreflightFailureKind = "auth-missing" | "proxy-gap" | "git-remote-gap" | "target-stack-not-running";
|
||||
|
||||
interface CodeQueuePrPreflightTransport {
|
||||
config?: UniDeskConfig | null;
|
||||
coreFetch?: CodexResponseFetcher;
|
||||
remoteMainServerPrPreflight?: (optionArgs: string[], config: UniDeskConfig | null) => unknown;
|
||||
}
|
||||
|
||||
type CodexRequestInit = { method?: string; body?: unknown };
|
||||
type CodexResponseFetcher = (path: string, init?: CodexRequestInit) => unknown;
|
||||
type AsyncCodexResponseFetcher = (path: string, init?: CodexRequestInit) => Promise<unknown>;
|
||||
@@ -383,6 +392,16 @@ function upstreamError(response: unknown): string {
|
||||
return `${status}: ${JSON.stringify(response).slice(0, 1200)}`;
|
||||
}
|
||||
|
||||
function parseJsonRecord(text: string): Record<string, unknown> | null {
|
||||
if (text.trim().length === 0) return null;
|
||||
try {
|
||||
const parsed = JSON.parse(text) as unknown;
|
||||
return asRecord(parsed);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function unwrapCodexResponse(response: unknown): { upstream: { ok: unknown; status: unknown }; body: Record<string, unknown> } {
|
||||
const record = asRecord(response);
|
||||
if (record?.ok !== true) throw new Error(upstreamError(response));
|
||||
@@ -2468,6 +2487,11 @@ function compactPrRuntimePreflight(preflight: Record<string, unknown>, options:
|
||||
note: "UniDesk CLI intentionally does not merge PRs in this phase; runner handoff stops at PR creation and evidence.",
|
||||
},
|
||||
},
|
||||
controlPlane: {
|
||||
mode: "local-backend-core",
|
||||
localBackendCoreMissing: false,
|
||||
remoteFallbackUsed: false,
|
||||
},
|
||||
tools: {
|
||||
git: compactToolStatus(tools.git),
|
||||
gh: compactToolStatus(tools.gh),
|
||||
@@ -2538,7 +2562,211 @@ function compactPrRuntimePreflight(preflight: Record<string, unknown>, options:
|
||||
return result;
|
||||
}
|
||||
|
||||
function codeQueuePrPreflight(optionArgs: string[] = [], fetcher: CodexResponseFetcher = coreInternalFetch): unknown {
|
||||
function queryRemoteMainServerPrPreflight(optionArgs: string[], config: UniDeskConfig): unknown {
|
||||
const command = ["bun", "scripts/cli.ts", "--main-server-ip", config.network.publicHost, "codex", "pr-preflight", ...optionArgs];
|
||||
const result = runCommand(command, repoRoot, { timeoutMs: 120_000 });
|
||||
const parsed = parseJsonRecord(result.stdout);
|
||||
if (parsed !== null) {
|
||||
const data = asRecord(parsed.data);
|
||||
const dataResult = asRecord(data?.result);
|
||||
if (dataResult !== null) return dataResult;
|
||||
if (data !== null) return data;
|
||||
const error = asRecord(parsed.error);
|
||||
if (error !== null) {
|
||||
const message = typeof error.message === "string" && error.message.length > 0
|
||||
? error.message
|
||||
: result.stderr.trim() || result.stdout.trim() || "remote control plane unreachable";
|
||||
return {
|
||||
ok: false,
|
||||
runnerDisposition: "infra-blocked",
|
||||
failureKind: "proxy-gap",
|
||||
degradedReason: "remote-control-plane-unreachable",
|
||||
message,
|
||||
controlPlane: {
|
||||
mode: "remote-frontend",
|
||||
host: config.network.publicHost,
|
||||
frontendUrl: `http://${config.network.publicHost}:${config.network.frontend.port}`,
|
||||
localBackendCoreMissing: false,
|
||||
remoteFallbackUsed: true,
|
||||
},
|
||||
observed: {
|
||||
exitCode: result.exitCode,
|
||||
stdoutTail: result.stdout.slice(-2000),
|
||||
stderrTail: result.stderr.slice(-2000),
|
||||
},
|
||||
commands: {
|
||||
retry: `bun scripts/cli.ts --main-server-ip ${config.network.publicHost} codex pr-preflight --remote`,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
const message = result.stderr.trim() || result.stdout.trim() || `remote control plane unreachable: exitCode=${result.exitCode ?? "null"}`;
|
||||
return {
|
||||
ok: false,
|
||||
runnerDisposition: "infra-blocked",
|
||||
failureKind: "proxy-gap",
|
||||
degradedReason: "remote-control-plane-unreachable",
|
||||
message,
|
||||
controlPlane: {
|
||||
mode: "remote-frontend",
|
||||
host: config.network.publicHost,
|
||||
frontendUrl: `http://${config.network.publicHost}:${config.network.frontend.port}`,
|
||||
localBackendCoreMissing: false,
|
||||
remoteFallbackUsed: true,
|
||||
},
|
||||
observed: {
|
||||
exitCode: result.exitCode,
|
||||
stdoutTail: result.stdout.slice(-2000),
|
||||
stderrTail: result.stderr.slice(-2000),
|
||||
},
|
||||
commands: {
|
||||
retry: `bun scripts/cli.ts --main-server-ip ${config.network.publicHost} codex pr-preflight --remote`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function codeQueuePrPreflight(optionArgs: string[] = [], transport: CodeQueuePrPreflightTransport = {}): unknown {
|
||||
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 localRecord = asRecord(localResponse);
|
||||
const localTargetStackMissing = localRecord?.ok === false
|
||||
&& localRecord.failureKind === "target-stack-not-running"
|
||||
&& localRecord.degradedReason === "backend-core-container-missing";
|
||||
const remoteMainServerPrPreflight = transport.remoteMainServerPrPreflight
|
||||
?? (config === null ? null : (args: string[], _config: UniDeskConfig | null) => queryRemoteMainServerPrPreflight(args, config));
|
||||
if (options.remote && localTargetStackMissing && remoteMainServerPrPreflight !== null) {
|
||||
const remoteResponse = remoteMainServerPrPreflight(optionArgs, config);
|
||||
const remoteRecord = asRecord(remoteResponse);
|
||||
if (remoteRecord !== null) {
|
||||
if (remoteRecord.ok === false) {
|
||||
return {
|
||||
...remoteRecord,
|
||||
controlPlane: {
|
||||
...(asRecord(remoteRecord.controlPlane) ?? {}),
|
||||
mode: "remote-frontend",
|
||||
host: config?.network.publicHost ?? null,
|
||||
frontendUrl: config === null ? null : `http://${config.network.publicHost}:${config.network.frontend.port}`,
|
||||
localBackendCoreMissing: true,
|
||||
remoteFallbackUsed: true,
|
||||
},
|
||||
localObservation: localRecord,
|
||||
remoteObservation: remoteRecord,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...remoteRecord,
|
||||
controlPlane: {
|
||||
...(asRecord(remoteRecord.controlPlane) ?? {}),
|
||||
mode: "remote-frontend",
|
||||
host: config?.network.publicHost ?? null,
|
||||
frontendUrl: config === null ? null : `http://${config.network.publicHost}:${config.network.frontend.port}`,
|
||||
localBackendCoreMissing: true,
|
||||
remoteFallbackUsed: true,
|
||||
},
|
||||
localObservation: localRecord,
|
||||
remoteObservation: remoteRecord,
|
||||
};
|
||||
}
|
||||
}
|
||||
if (localRecord?.ok !== true) {
|
||||
if (options.remote && localTargetStackMissing) {
|
||||
const failureKind: CodeQueuePrPreflightFailureKind = "proxy-gap";
|
||||
const degradedReason = "remote-control-plane-unreachable";
|
||||
return {
|
||||
...(localRecord ?? {}),
|
||||
ok: false,
|
||||
runnerDisposition: "infra-blocked",
|
||||
failureKind,
|
||||
degradedReason,
|
||||
message: "remote control plane unreachable; local backend-core target-stack absence is evidence only",
|
||||
controlPlane: {
|
||||
mode: "local-backend-core",
|
||||
localBackendCoreMissing: true,
|
||||
remoteFallbackUsed: false,
|
||||
},
|
||||
commands: {
|
||||
retry: config !== null
|
||||
? `bun scripts/cli.ts --main-server-ip ${config.network.publicHost} codex pr-preflight --remote`
|
||||
: "bun scripts/cli.ts codex pr-preflight --remote",
|
||||
local: "bun scripts/cli.ts microservice proxy code-queue /api/runtime-preflight --raw",
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
...(localRecord ?? {}),
|
||||
ok: false,
|
||||
runnerDisposition: localRecord?.runnerDisposition ?? "infra-blocked",
|
||||
failureKind: (localRecord?.failureKind as CodeQueuePrPreflightFailureKind | undefined) ?? "proxy-gap",
|
||||
degradedReason: localRecord?.degradedReason ?? "backend-core-proxy-unavailable",
|
||||
message: localRecord?.message ?? localRecord?.stderrTail ?? localRecord?.stdoutTail ?? "Code Queue runtime preflight could not be observed",
|
||||
controlPlane: {
|
||||
mode: "local-backend-core",
|
||||
localBackendCoreMissing: localTargetStackMissing,
|
||||
remoteFallbackUsed: false,
|
||||
},
|
||||
commands: {
|
||||
retry: config !== null
|
||||
? `bun scripts/cli.ts --main-server-ip ${config.network.publicHost} codex pr-preflight --remote`
|
||||
: "bun scripts/cli.ts codex pr-preflight --remote",
|
||||
local: "bun scripts/cli.ts microservice proxy code-queue /api/runtime-preflight --raw",
|
||||
},
|
||||
};
|
||||
}
|
||||
const response = asRecord(localRecord);
|
||||
if (response?.ok !== true) {
|
||||
throw new Error(upstreamError(localRecord));
|
||||
}
|
||||
const body = asRecord(response.body);
|
||||
const preflight = asRecord(body?.runtimePreflight);
|
||||
if (preflight === null) {
|
||||
return {
|
||||
ok: false,
|
||||
runnerDisposition: "infra-blocked",
|
||||
failureKind: "proxy-gap",
|
||||
degradedReason: "runtime-preflight-missing",
|
||||
message: "Code Queue runtime-preflight response did not include runtimePreflight",
|
||||
controlPlane: {
|
||||
mode: "local-backend-core",
|
||||
localBackendCoreMissing: false,
|
||||
remoteFallbackUsed: false,
|
||||
},
|
||||
upstream: { ok: response.ok, status: response.status },
|
||||
commands: {
|
||||
retry: "bun scripts/cli.ts codex pr-preflight --remote",
|
||||
},
|
||||
};
|
||||
}
|
||||
const compact = compactPrRuntimePreflight(preflight, options);
|
||||
return {
|
||||
ok: compact.ok,
|
||||
runnerDisposition: compact.runnerDisposition,
|
||||
failureKind: compact.failureKind ?? null,
|
||||
degradedReason: compact.degradedReason ?? null,
|
||||
upstream: { ok: response.ok, status: response.status },
|
||||
controlPlane: {
|
||||
mode: "local-backend-core",
|
||||
localBackendCoreMissing: false,
|
||||
remoteFallbackUsed: false,
|
||||
},
|
||||
preflight: compact,
|
||||
};
|
||||
}
|
||||
|
||||
export function codexPrPreflightQueryForTest(optionArgs: string[], transport: CodeQueuePrPreflightTransport = {}): unknown {
|
||||
return codeQueuePrPreflight(optionArgs, transport);
|
||||
}
|
||||
|
||||
export async function codexPrPreflightQueryAsync(optionArgs: string[], fetcher: AsyncCodexResponseFetcher): Promise<unknown> {
|
||||
const options = parsePrPreflightOptions(optionArgs);
|
||||
const path = codeQueueProxyPath(`/api/runtime-preflight${queryString({
|
||||
remote: options.remote ? 1 : undefined,
|
||||
@@ -2548,22 +2776,44 @@ function codeQueuePrPreflight(optionArgs: string[] = [], fetcher: CodexResponseF
|
||||
prCreateDryRunHead: options.prCreateDryRunHead,
|
||||
issue: options.issueNumber,
|
||||
})}`);
|
||||
const response = unwrapCodexResponse(fetcher(path));
|
||||
const preflight = asRecord(response.body.runtimePreflight);
|
||||
if (preflight === null) throw new Error("Code Queue runtime-preflight response did not include runtimePreflight");
|
||||
const response = asRecord(await fetcher(path));
|
||||
if (response?.ok !== true) throw new Error(upstreamError(response));
|
||||
const body = asRecord(response.body);
|
||||
const preflight = asRecord(body?.runtimePreflight);
|
||||
if (preflight === null) {
|
||||
return {
|
||||
ok: false,
|
||||
runnerDisposition: "infra-blocked",
|
||||
failureKind: "proxy-gap",
|
||||
degradedReason: "runtime-preflight-missing",
|
||||
message: "Code Queue runtime-preflight response did not include runtimePreflight",
|
||||
controlPlane: {
|
||||
mode: "remote-frontend",
|
||||
localBackendCoreMissing: false,
|
||||
remoteFallbackUsed: false,
|
||||
},
|
||||
upstream: { ok: response.ok, status: response.status },
|
||||
commands: {
|
||||
retry: "bun scripts/cli.ts codex pr-preflight --remote",
|
||||
},
|
||||
};
|
||||
}
|
||||
const compact = compactPrRuntimePreflight(preflight, options);
|
||||
return {
|
||||
ok: compact.ok,
|
||||
runnerDisposition: compact.runnerDisposition,
|
||||
upstream: response.upstream,
|
||||
failureKind: compact.failureKind ?? null,
|
||||
degradedReason: compact.degradedReason ?? null,
|
||||
upstream: { ok: response.ok, status: response.status },
|
||||
controlPlane: {
|
||||
mode: "remote-frontend",
|
||||
localBackendCoreMissing: false,
|
||||
remoteFallbackUsed: false,
|
||||
},
|
||||
preflight: compact,
|
||||
};
|
||||
}
|
||||
|
||||
export function codexPrPreflightQueryForTest(optionArgs: string[], fetcher: CodexResponseFetcher): unknown {
|
||||
return codeQueuePrPreflight(optionArgs, fetcher);
|
||||
}
|
||||
|
||||
export function codexSubmitRoutingRecommendationForTest(prompt: string, model?: string): SubmitRoutingRecommendation {
|
||||
return submitRoutingRecommendation({
|
||||
prompt,
|
||||
@@ -2680,7 +2930,7 @@ function codexSteerTask(taskId: string, args: string[], fetcher: CodexResponseFe
|
||||
};
|
||||
}
|
||||
|
||||
export async function runCodeQueueCommand(_config: UniDeskConfig, args: string[]): Promise<unknown> {
|
||||
export async function runCodeQueueCommand(config: UniDeskConfig, args: string[]): Promise<unknown> {
|
||||
const [action = "task", taskIdArg] = args;
|
||||
if (action === "submit" || action === "enqueue") {
|
||||
return codexSubmitTask(args.slice(1));
|
||||
@@ -2696,7 +2946,7 @@ export async function runCodeQueueCommand(_config: UniDeskConfig, args: string[]
|
||||
assertKnownOptions(args.slice(1), {}, `codex ${action}`);
|
||||
return codeQueueDevReady();
|
||||
}
|
||||
if (action === "pr-preflight" || action === "runtime-preflight") return codeQueuePrPreflight(args.slice(1));
|
||||
if (action === "pr-preflight" || action === "runtime-preflight") return codeQueuePrPreflight(args.slice(1), { config });
|
||||
if (action === "output") {
|
||||
const taskId = requireTaskId(taskIdArg, "codex output");
|
||||
return codexOutputQuery(taskId, args.slice(2));
|
||||
|
||||
Reference in New Issue
Block a user