fix: classify codex steer proxy failures

This commit is contained in:
Codex
2026-05-20 15:01:35 +00:00
parent cab85fcdfb
commit 3b6a420d09
6 changed files with 280 additions and 9 deletions
+162 -5
View File
@@ -1,6 +1,7 @@
import { mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
import { type UniDeskConfig, repoRoot, rootPath } from "./config";
import { coreInternalFetch } from "./microservices";
import { previewJson } from "./preview";
const defaultToolLimit = 8;
const defaultTraceLimit = 80;
@@ -9,6 +10,7 @@ const defaultOutputLimit = 20;
const defaultTextPreviewChars = 12_000;
const defaultTasksLimit = 20;
const maxTasksLimit = 100;
const steerPromptPreviewChars = 320;
const submitLockWaitMs = 60_000;
const submitLockPollMs = 250;
const submitLockStaleMs = 120_000;
@@ -58,6 +60,30 @@ interface CodexSteerOptions {
dryRun: boolean;
}
type CodexSteerFailureReason =
| "backend-core-unreachable"
| "code-queue-microservice-unregistered"
| "proxy-unauthorized"
| "proxy-404"
| "steer-endpoint-404"
| "upstream-runtime-rejected"
| "stable-proxy-failed"
| "invalid-proxy-response";
interface ClassifiedCodexSteerError {
reason: CodexSteerFailureReason;
scope: "backend-core" | "stable-proxy" | "code-queue-runtime" | "unknown";
status: number | null;
exitCode: number | null;
retryable: boolean;
message: string;
requestPath: string;
stableProxyPath: string;
upstreamBodyPreview: unknown;
rawProxyEquivalent: string;
recommendedCrossChecks: string[];
}
interface CompactTaskMutationResponseOptions {
fullPrompt?: boolean;
}
@@ -129,6 +155,10 @@ function codeQueueProxyPath(path: string): string {
return `${codeQueueProxyPrefix}${path}`;
}
function codeQueueProxyEquivalentCommand(targetPath: string, bodyJson: string): string {
return `bun scripts/cli.ts microservice proxy code-queue ${targetPath} --method POST --body-json '${bodyJson}'`;
}
function nonNegativeIntegerEnv(name: string, fallback: number): number {
const raw = process.env[name];
if (raw === undefined || raw.trim().length === 0) return fallback;
@@ -285,6 +315,103 @@ function unwrapCodexResponse(response: unknown): { upstream: { ok: unknown; stat
return { upstream: { ok: record.ok, status: record.status }, body };
}
function responseStatus(response: Record<string, unknown> | null): number | null {
const value = response?.status;
return typeof value === "number" && Number.isFinite(value) ? value : null;
}
function responseExitCode(response: Record<string, unknown> | null): number | null {
const value = response?.exitCode ?? response?.commandExitCode;
return typeof value === "number" && Number.isFinite(value) ? value : null;
}
function responseBody(response: Record<string, unknown> | null): Record<string, unknown> | null {
return asRecord(response?.body);
}
function responseErrorMessage(response: Record<string, unknown> | null): string {
const body = responseBody(response);
const bodyError = body?.error;
if (typeof bodyError === "string" && bodyError.length > 0) return bodyError;
if (typeof response?.codeQueueObservationNote === "string") return response.codeQueueObservationNote;
if (typeof response?.stderrTail === "string" && response.stderrTail.length > 0) return response.stderrTail;
if (typeof response?.commandStderrTail === "string" && response.commandStderrTail.length > 0) return response.commandStderrTail;
return "Code Queue steer request failed";
}
function classifySteerFailure(response: unknown, targetPath: string, stableProxyPath: string, rawProxyEquivalent: string): ClassifiedCodexSteerError {
const record = asRecord(response);
const status = responseStatus(record);
const exitCode = responseExitCode(record);
const body = responseBody(record);
const message = responseErrorMessage(record);
const lowerMessage = message.toLowerCase();
const bodyPath = typeof body?.path === "string" ? body.path : "";
const bodyStage = typeof body?.stage === "string" ? body.stage : "";
const bodyServiceId = typeof body?.serviceId === "string" ? body.serviceId : "";
let reason: CodexSteerFailureReason = "invalid-proxy-response";
let scope: ClassifiedCodexSteerError["scope"] = "unknown";
let retryable = false;
if (record === null || status === null && exitCode !== null) {
reason = "backend-core-unreachable";
scope = "backend-core";
retryable = true;
} else if (status === 401 || status === 403) {
reason = "proxy-unauthorized";
scope = "stable-proxy";
retryable = false;
} else if (status === 404 && /microservice not found: code-queue/u.test(lowerMessage)) {
reason = "code-queue-microservice-unregistered";
scope = "stable-proxy";
retryable = false;
} else if (status === 404 && (lowerMessage === "task not found" || lowerMessage === "not found" || bodyPath === targetPath || bodyServiceId === "code-queue")) {
reason = "steer-endpoint-404";
scope = "code-queue-runtime";
retryable = false;
} else if (status === 404) {
reason = "proxy-404";
scope = "stable-proxy";
retryable = false;
} else if (status === 400 || status === 405 || status === 409 || /active run|steerable|scheduler-only|read-only|prompt is required/u.test(lowerMessage)) {
reason = "upstream-runtime-rejected";
scope = "code-queue-runtime";
retryable = status === 409;
} else if (status === 502 || status === 503 || status === 504 || /proxy|tunnel|provider|k3sctl|adapter|timed out|timeout|unavailable|disconnected/u.test(lowerMessage) || bodyStage.length > 0) {
reason = "stable-proxy-failed";
scope = "stable-proxy";
retryable = true;
}
return {
reason,
scope,
status,
exitCode,
retryable,
message,
requestPath: targetPath,
stableProxyPath,
upstreamBodyPreview: previewJson(body ?? record, { maxDepth: 3, maxArrayItems: 3, maxObjectKeys: 16, maxStringLength: 320 }),
rawProxyEquivalent,
recommendedCrossChecks: [
"bun scripts/cli.ts codex queues",
"bun scripts/cli.ts codex tasks --view supervisor --limit 20",
"bun scripts/cli.ts codex task <taskId>",
"bun scripts/cli.ts microservice health code-queue",
"bun scripts/cli.ts microservice diagnostics code-queue",
],
};
}
function unwrapSteerResponse(response: unknown, targetPath: string, stableProxyPath: string, rawProxyEquivalent: string): { ok: true; upstream: { ok: unknown; status: unknown }; body: Record<string, unknown> } | { ok: false; diagnostics: ClassifiedCodexSteerError } {
const record = asRecord(response);
const body = responseBody(record);
if (record?.ok === true && body?.ok === true) return { ok: true, upstream: { ok: record.ok, status: record.status }, body };
return { ok: false, diagnostics: classifySteerFailure(response, targetPath, stableProxyPath, rawProxyEquivalent) };
}
function positiveIntegerOption(args: string[], names: string[], defaultValue: number, maxValue = Number.MAX_SAFE_INTEGER): number {
for (const name of names) {
const index = args.indexOf(name);
@@ -917,6 +1044,10 @@ export function codexJudgeQuery(taskId: string, optionArgs: string[], fetcher: C
return codexTaskJudge(taskId, parseJudgeOptions(optionArgs), fetcher);
}
export function codexSteerTaskForTest(taskId: string, optionArgs: string[], fetcher: CodexResponseFetcher): unknown {
return codexSteerTask(taskId, optionArgs, fetcher);
}
function isTerminalTaskStatus(status: unknown): boolean {
return status === "succeeded" || status === "failed" || status === "canceled";
}
@@ -1779,12 +1910,21 @@ function codexInterruptTask(taskId: string): unknown {
};
}
function codexSteerTask(taskId: string, args: string[]): unknown {
function codexSteerTask(taskId: string, args: string[], fetcher: CodexResponseFetcher = coreInternalFetch): unknown {
const options = parseSteerOptions(args);
const prompt = textView(options.prompt, true, 3000);
const targetPath = `/api/tasks/${encodeURIComponent(taskId)}/steer`;
const stableProxyPath = codeQueueProxyPath(targetPath);
const rawProxyEquivalent = codeQueueProxyEquivalentCommand(targetPath, "{\"prompt\":\"...\"}");
const prompt = textView(options.prompt, false, steerPromptPreviewChars);
const request = {
path: `/api/tasks/${taskId}/steer`,
path: targetPath,
stableProxyPath,
method: "POST",
bodySummary: {
promptChars: options.prompt.length,
promptPreviewChars: steerPromptPreviewChars,
promptTruncated: prompt.truncated,
},
body: { prompt },
};
if (options.dryRun) {
@@ -1794,11 +1934,28 @@ function codexSteerTask(taskId: string, args: string[]): unknown {
request,
commands: {
run: `bun scripts/cli.ts codex steer ${taskId} --prompt-file <path>`,
rawProxy: `bun scripts/cli.ts microservice proxy code-queue /api/tasks/${encodeURIComponent(taskId)}/steer --method POST --body-json '{"prompt":"..."}'`,
rawProxy: rawProxyEquivalent,
},
};
}
const response = unwrapSteerResponse(fetcher(stableProxyPath, { method: "POST", body: { prompt: options.prompt } }), targetPath, stableProxyPath, rawProxyEquivalent);
if (!response.ok) {
return {
ok: false,
steer: {
accepted: false,
prompt,
},
request,
diagnostics: response.diagnostics,
commands: {
dryRun: `bun scripts/cli.ts codex steer ${taskId} --prompt-file <path> --dry-run`,
rawProxy: rawProxyEquivalent,
tasks: "bun scripts/cli.ts codex tasks --view supervisor --limit 20",
health: "bun scripts/cli.ts microservice health code-queue",
},
};
}
const response = unwrapCodexResponse(coreInternalFetch(codeQueueProxyPath(`/api/tasks/${encodeURIComponent(taskId)}/steer`), { method: "POST", body: { prompt: options.prompt } }));
return {
upstream: response.upstream,
steer: {