fix: classify codex steer proxy failures
This commit is contained in:
+162
-5
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user