fix: classify codex steer proxy failures
This commit is contained in:
@@ -47,7 +47,8 @@ CLI 可以从 `master` 快速演进,但必须兼容 `deploy.json` 固定的 CI
|
||||
- `codex read <taskId>` 在人工审阅后标记单个终态任务已读;列表、overview 和 supervisor 视图只返回这个命令字段,不得自动执行,也不得批量清空未读状态。
|
||||
- `codex dev-ready` 查询 Code Queue `/api/dev-ready` 并返回有界 readiness 摘要,包括工具、Docker、Codex config、SSH 和 `devReady.skills`。`devReady.skills` 只暴露 `UNIDESK_SKILLS_PATH`、是否存在、是否只读、skillCount、`cli-spec` 是否可见和修复建议,不输出宿主 auth/token 文件内容。
|
||||
- `codex judge <taskId> --attempt N [--dry-run] [--include-prompt]` 通过 Code Queue 私有代理按指定 attempt 单步复现 judge;这是执行面诊断入口,仍依赖 D601 scheduler/runner 侧的真实 judge builder、MiniMax 调用路径和执行环境。默认会真实调用 MiniMax,`--dry-run` 只返回 prompt/payload 大小、attempt 窗口和重建来源诊断,`--include-prompt` 仅用于本地深度排查。
|
||||
- `codex steer <taskId> [prompt|--prompt-file path|--prompt-stdin] [--dry-run]` 通过 Code Queue 私有代理向正在运行的 task 注入纠偏提示,正式替代底层 `microservice proxy code-queue /api/tasks/<taskId>/steer` 调用。prompt 必须且只能来自位置参数、文件或 stdin 之一;`--dry-run` 只输出将要发送的结构化请求和 prompt 字符数,不触碰运行中 session。真实执行仍走 `/api/microservices/code-queue/proxy/api/tasks/<taskId>/steer`,只能作用于 D601 scheduler 上存在 active steerable turn 的 running task;若任务不在 running/active-turn 状态,返回上游 409 和有界 task 摘要,不得静默成功。
|
||||
- `codex steer <taskId> [prompt|--prompt-file path|--prompt-stdin] [--dry-run]` 通过 Code Queue 私有代理向正在运行的 task 注入纠偏提示,正式替代底层 `microservice proxy code-queue /api/tasks/<taskId>/steer` 调用。prompt 必须且只能来自位置参数、文件或 stdin 之一;`--dry-run` 只输出 `method`、`path`、`stableProxyPath`、prompt 字符数、截断预览和 raw proxy 等价命令,不触碰运行中 session,也不得泄露超长 prompt 全文。真实执行复用与 `codex task/tasks/read` 相同的 backend-core stable proxy helper,路径固定为 `/api/microservices/code-queue/proxy/api/tasks/<taskId>/steer`,只能作用于 D601 scheduler 上存在 active steerable turn 的 running task。
|
||||
- `codex steer` 非 dry-run 失败仍输出 JSON 且退出非零;`.data.diagnostics.reason` 用于 runner 分流,当前包括 `backend-core-unreachable`、`code-queue-microservice-unregistered`、`proxy-unauthorized`、`proxy-404`、`steer-endpoint-404`、`upstream-runtime-rejected`、`stable-proxy-failed` 和 `invalid-proxy-response`。`scope` 区分 `backend-core`、`stable-proxy`、`code-queue-runtime` 或 `unknown`,并带 `status`、`exitCode`、`retryable`、有界 `upstreamBodyPreview` 和推荐交叉验证命令;若任务不在 running/active-turn 状态,通常归类为 `upstream-runtime-rejected`,不得静默成功。
|
||||
- `codex interrupt|cancel <taskId>` 通过 Code Queue 私有代理请求中断;running/judging 任务会请求 D601 当前 agent run 停止,queued/retry_wait 任务的取消也必须保持与 WebUI 相同代理路径,返回有界 task 摘要和后续查询命令。任何需要接触 active run 的动作仍属于 D601 执行面。
|
||||
- Code Queue 多队列 lane 由 `codex` 命令命名空间管理:`queues [--full|--all]` 列表、`queue create <queueId>` 创建、`queue merge <sourceQueueId> --into <targetQueueId>` 合并、`move <taskId> --queue <queueId>` 迁移;这些队列管理入口默认由主 server `code-queue-mgr` 直管 PostgreSQL,仍通过稳定 `code-queue` 用户服务代理路径访问。`codex queues` 默认只返回 active/nonempty/unread/runnable queue 摘要、全局 counts 和 execution diagnostics;完整队列数组必须显式 `--full` 或 `--all`。同一个 queue 内部串行执行,不同 queue 之间并行执行。迁移只允许尚未被 scheduler claim 的 `queued`/`retry_wait` 任务,必须满足 `startedAt=null`、`currentAttempt=0` 且没有 active thread/turn;已进入 `running`/`judging` 或已有 claim 标记的任务返回 409,不得被 move/merge 回写成 queued。合并会移动可迁移任务归属并自动删除源 queue 记录,只保留合并后的目标 queue;若 source 或 target queue 存在 active/claimed 任务,合并整体返回 409。合并后的目标 queue 按任务原 `queueEnteredAt`/`createdAt` 时间顺序串行,成功迁移 queued/retry_wait 任务后由 D601 scheduler 轮询推进。
|
||||
- 所有 `codex` 查询和管理命令必须走与 WebUI 相同的 backend-core 私有代理路径 `/api/microservices/code-queue/proxy/...`;CLI 不得为了提交、移动、中断、取消或队列管理直接调用 D601 内部 Service、数据库、pod curl 或 k3sctl scheduler 子服务。若该路径失败,应先修复 CLI/backend/provider tunnel 链路,而不是绕过控制面。
|
||||
|
||||
@@ -182,7 +182,7 @@ bun scripts/code-queue-pr-preflight-example.ts --repo pikasTech/unidesk --base m
|
||||
只有存在明确理由时才干预。
|
||||
|
||||
- 如果任务还在运行且 trace 或 scheduler heartbeat 新鲜,应引导而不是 interrupt。
|
||||
- 对运行中任务的引导应优先使用正式 CLI。若源码或 runtime 已有 `steer` 能力但 CLI 尚未暴露,临时通过受控 microservice proxy 调用可以作为现场恢复手段;这类临时绕行必须记录到指挥简报 issue #24 主体的常驻观察,并创建正式 issue 补齐 CLI 能力,避免长期依赖隐式 API。
|
||||
- 对运行中任务的引导应优先使用正式 CLI:`bun scripts/cli.ts codex steer <taskId> --prompt-file <path>`。该命令和 `codex task/tasks/read` 复用同一个 backend-core stable proxy helper;`--dry-run` 会显示 `method/path/stableProxyPath`、prompt 摘要和 raw proxy 等价命令但不发送。非 dry-run 失败时先看 `.data.diagnostics.reason`:`backend-core-unreachable` 属于本机到 core 的观察路径,`code-queue-microservice-unregistered`/`proxy-unauthorized`/`proxy-404` 属于 stable proxy 配置或权限,`steer-endpoint-404`/`upstream-runtime-rejected` 属于 D601 runtime 或任务状态,`stable-proxy-failed` 多为 provider/k3s/tunnel 链路问题。若正式 CLI 自身不可用,临时通过受控 microservice proxy 调用只能作为现场恢复手段;这类绕行必须记录到指挥简报 issue #24 主体的常驻观察,并创建正式 issue 补齐 CLI 能力,避免长期依赖隐式 API。
|
||||
- 如果任务进入终态但缺少必要验收证据,应使用聚焦 continuation prompt retry 同一任务。
|
||||
- 如果任务被可复用基础设施缺陷阻塞,应把该缺陷分配给合适的空闲或低风险队列,让原业务任务等待,或在修复后 retry。
|
||||
- 如果基础设施缺陷影响 Code Queue 控制面可用性,指挥官可以执行恢复队列所需的最小受控部署,然后验证原任务能继续。
|
||||
|
||||
+26
-2
@@ -23,7 +23,28 @@ import { isHelpToken, rootHelp, serverHelp, sshHelp, staticNamespaceHelp } from
|
||||
|
||||
const remoteOptions = extractRemoteCliOptions(process.argv.slice(2));
|
||||
const args = remoteOptions.args;
|
||||
const commandName = args.join(" ") || "help";
|
||||
const commandName = displayCommandName(args);
|
||||
|
||||
function displayCommandName(parts: string[]): string {
|
||||
if (parts.length === 0) return "help";
|
||||
if (parts[0] === "codex" && parts[1] === "steer" && parts[2] !== undefined) {
|
||||
const shown = ["codex", "steer", parts[2]];
|
||||
const hasPromptFile = parts.includes("--prompt-file") || parts.includes("--file");
|
||||
const hasPromptStdin = parts.includes("--prompt-stdin") || parts.includes("--stdin");
|
||||
if (!hasPromptFile && !hasPromptStdin) shown.push("<prompt:redacted>");
|
||||
for (let index = 3; index < parts.length; index += 1) {
|
||||
const part = parts[index] ?? "";
|
||||
if (!part.startsWith("--")) continue;
|
||||
shown.push(part);
|
||||
if (part === "--prompt-file" || part === "--file") {
|
||||
shown.push(parts[index + 1] ?? "<missing>");
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
return shown.join(" ");
|
||||
}
|
||||
return parts.join(" ");
|
||||
}
|
||||
|
||||
function numberOption(name: string, defaultValue: number): number {
|
||||
const index = args.indexOf(name);
|
||||
@@ -252,7 +273,10 @@ async function main(): Promise<void> {
|
||||
if (!ok) process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
emitJson(commandName, await runCodeQueueCommand(config, args.slice(1)));
|
||||
const result = await runCodeQueueCommand(config, args.slice(1));
|
||||
const ok = (result as { ok?: unknown }).ok !== false;
|
||||
emitJson(commandName, result, ok);
|
||||
if (!ok) process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { spawnSync } from "node:child_process";
|
||||
import { writeFileSync, unlinkSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { codexSteerTaskForTest } from "./src/code-queue";
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
@@ -48,15 +49,35 @@ function assertDryRunPrompt(response: JsonRecord, expectedText: string): void {
|
||||
assertCondition(response.ok === true, "CLI dry-run should succeed", response);
|
||||
const data = nestedRecord(response.data, []);
|
||||
assertCondition(data.dryRun === true, "dry-run response should expose dryRun=true", data);
|
||||
const request = nestedRecord(response.data, ["request"]);
|
||||
assertCondition(request.method === "POST", "dry-run should expose request method", request);
|
||||
assertCondition(request.path === "/api/tasks/codex_test_task/steer", "dry-run should expose request path", request);
|
||||
assertCondition(request.stableProxyPath === "/api/microservices/code-queue/proxy/api/tasks/codex_test_task/steer", "dry-run should expose stable proxy path", request);
|
||||
const prompt = nestedRecord(response.data, ["request", "body", "prompt"]);
|
||||
assertCondition(prompt.text === expectedText, "dry-run prompt text mismatch", prompt);
|
||||
assertCondition(prompt.chars === expectedText.length, "dry-run prompt char count mismatch", prompt);
|
||||
assertCondition(prompt.truncated === false, "dry-run prompt must not truncate", prompt);
|
||||
const bodySummary = nestedRecord(response.data, ["request", "bodySummary"]);
|
||||
assertCondition(bodySummary.promptChars === expectedText.length, "dry-run should expose body prompt char count", bodySummary);
|
||||
const commands = nestedRecord(response.data, ["commands"]);
|
||||
assertCondition(String(commands.rawProxy || "").includes("microservice proxy code-queue /api/tasks/codex_test_task/steer --method POST"), "dry-run should expose raw proxy equivalent", commands);
|
||||
}
|
||||
|
||||
function assertReason(result: unknown, reason: string, status: number | null): void {
|
||||
const data = nestedRecord({ data: result }, ["data"]);
|
||||
assertCondition(data.ok === false, "classified steer failure should be ok=false", data);
|
||||
const diagnostics = nestedRecord(data, ["diagnostics"]);
|
||||
assertCondition(diagnostics.reason === reason, "unexpected steer failure reason", diagnostics);
|
||||
assertCondition(diagnostics.status === status, "unexpected steer failure status", diagnostics);
|
||||
assertCondition(typeof diagnostics.retryable === "boolean", "diagnostics should expose retryable boolean", diagnostics);
|
||||
assertCondition(Array.isArray(diagnostics.recommendedCrossChecks), "diagnostics should expose cross-check commands", diagnostics);
|
||||
}
|
||||
|
||||
export function runCodeQueueCliSteerContract(): JsonRecord {
|
||||
const positional = runCli(["codex", "steer", "codex_test_task", "correct the running task", "--dry-run"]);
|
||||
assertDryRunPrompt(positional.json ?? {}, "correct the running task");
|
||||
assertCondition(String(positional.json?.command || "").includes("<prompt:redacted>"), "outer command should redact positional steer prompt", positional.json ?? {});
|
||||
assertCondition(!String(positional.json?.command || "").includes("correct the running task"), "outer command must not echo positional steer prompt", positional.json ?? {});
|
||||
|
||||
const stdin = runCli(["codex", "steer", "codex_test_task", "--prompt-stdin", "--dry-run"], "stdin steer prompt\n");
|
||||
assertDryRunPrompt(stdin.json ?? {}, "stdin steer prompt\n");
|
||||
@@ -85,6 +106,51 @@ export function runCodeQueueCliSteerContract(): JsonRecord {
|
||||
const usage = stringArray(nestedRecord(help.json?.data, []).usage);
|
||||
assertCondition(usage.some((line) => line.includes("codex steer <taskId>")), "codex help should list steer", { usage });
|
||||
|
||||
let dryRunFetchCount = 0;
|
||||
const dryRunDirect = codexSteerTaskForTest("direct_task", ["do not send", "--dry-run"], () => {
|
||||
dryRunFetchCount += 1;
|
||||
return { ok: true, status: 200, body: { ok: true } };
|
||||
});
|
||||
assertCondition(dryRunFetchCount === 0, "dry-run must not call stable proxy helper", { dryRunFetchCount, dryRunDirect });
|
||||
|
||||
const longPrompt = `${"x".repeat(480)}-tail-secret-marker`;
|
||||
const longDryRun = codexSteerTaskForTest("direct_task", [longPrompt, "--dry-run"], () => {
|
||||
throw new Error("dry-run should not fetch");
|
||||
}) as JsonRecord;
|
||||
const longPreview = nestedRecord(longDryRun, ["request", "body", "prompt"]);
|
||||
assertCondition(longPreview.truncated === true, "long dry-run prompt should be truncated", longPreview);
|
||||
assertCondition(!String(longPreview.text || "").includes("tail-secret-marker"), "long dry-run must not leak prompt tail", longPreview);
|
||||
|
||||
let fetchPath = "";
|
||||
let fetchMethod = "";
|
||||
let fetchPrompt = "";
|
||||
const success = codexSteerTaskForTest("direct_task", ["send this"], (path, init) => {
|
||||
fetchPath = path;
|
||||
fetchMethod = String(init?.method || "");
|
||||
fetchPrompt = String((init?.body as JsonRecord | undefined)?.prompt || "");
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
body: {
|
||||
ok: true,
|
||||
task: { id: "direct_task", status: "running", prompt: "p" },
|
||||
queue: { activeTaskIds: ["direct_task"] },
|
||||
},
|
||||
};
|
||||
}) as JsonRecord;
|
||||
assertCondition(fetchPath === "/api/microservices/code-queue/proxy/api/tasks/direct_task/steer", "non-dry-run should use stable proxy path", { fetchPath });
|
||||
assertCondition(fetchMethod === "POST", "non-dry-run should POST", { fetchMethod });
|
||||
assertCondition(fetchPrompt === "send this", "non-dry-run should send raw prompt in body", { fetchPrompt });
|
||||
assertCondition(nestedRecord(success, ["steer"]).accepted === true, "successful steer should report accepted=true", success);
|
||||
|
||||
assertReason(codexSteerTaskForTest("direct_task", ["p"], () => ({ ok: false, exitCode: 1, stderrTail: "Cannot connect to the Docker daemon" })), "backend-core-unreachable", null);
|
||||
assertReason(codexSteerTaskForTest("direct_task", ["p"], () => ({ ok: false, status: 404, body: { ok: false, error: "microservice not found: code-queue" } })), "code-queue-microservice-unregistered", 404);
|
||||
assertReason(codexSteerTaskForTest("direct_task", ["p"], () => ({ ok: false, status: 401, body: { ok: false, error: "unauthorized" } })), "proxy-unauthorized", 401);
|
||||
assertReason(codexSteerTaskForTest("direct_task", ["p"], () => ({ ok: false, status: 404, body: { ok: false, error: "proxy route not found", path: "/api/microservices/code-queue/proxy/api/tasks/direct_task/steer" } })), "proxy-404", 404);
|
||||
assertReason(codexSteerTaskForTest("direct_task", ["p"], () => ({ ok: false, status: 404, body: { ok: false, error: "task not found" } })), "steer-endpoint-404", 404);
|
||||
assertReason(codexSteerTaskForTest("direct_task", ["p"], () => ({ ok: false, status: 409, body: { ok: false, error: "task does not have an active steerable turn" } })), "upstream-runtime-rejected", 409);
|
||||
assertReason(codexSteerTaskForTest("direct_task", ["p"], () => ({ ok: false, status: 504, body: { ok: false, error: "provider HTTP tunnel timed out or disconnected", stage: "http-tunnel-wait" } })), "stable-proxy-failed", 504);
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
checks: [
|
||||
@@ -94,6 +160,11 @@ export function runCodeQueueCliSteerContract(): JsonRecord {
|
||||
"duplicate prompt source failure",
|
||||
"unsupported option failure",
|
||||
"codex help lists steer",
|
||||
"outer command redacts positional steer prompt",
|
||||
"dry-run does not call stable proxy helper",
|
||||
"dry-run prompt preview is bounded",
|
||||
"non-dry-run uses stable proxy helper",
|
||||
"steer failure classification is JSON-consumable",
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
+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: {
|
||||
|
||||
@@ -32,6 +32,14 @@ export function coreInternalFetch(path: string, init?: { method?: string; body?:
|
||||
const command = dockerCoreFetchCommand(path, init);
|
||||
const result = runCommand(command, repoRoot);
|
||||
if (result.exitCode !== 0) {
|
||||
const parsedStdout = parseJsonRecord(result.stdout.trim());
|
||||
if (parsedStdout !== null) {
|
||||
return {
|
||||
...parsedStdout,
|
||||
commandExitCode: result.exitCode,
|
||||
commandStderrTail: result.stderr.slice(-1200),
|
||||
};
|
||||
}
|
||||
const codeQueueStableProxy = path.startsWith("/api/microservices/code-queue/");
|
||||
return {
|
||||
ok: false,
|
||||
@@ -55,6 +63,16 @@ export function coreInternalFetch(path: string, init?: { method?: string; body?:
|
||||
}
|
||||
}
|
||||
|
||||
function parseJsonRecord(text: string): Record<string, unknown> | null {
|
||||
if (text.length === 0) return null;
|
||||
try {
|
||||
const parsed = JSON.parse(text) as unknown;
|
||||
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed as Record<string, unknown> : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function requireId(value: string | undefined, command: string): string {
|
||||
if (value === undefined || value.length === 0) throw new Error(`${command} requires microservice id`);
|
||||
return value;
|
||||
|
||||
Reference in New Issue
Block a user