fix(cli): retry codex steer tunnel aborts
This commit is contained in:
@@ -52,7 +52,7 @@ UniDesk 是一个以主 server 为统一入口的分布式工作平台;本文
|
||||
- `bun scripts/cli.ts codex submit [prompt] [--prompt-file path|--prompt-stdin] [--queue <id>]` / `codex pr-preflight [--remote]`:前者通过 backend-core 私有代理提交 Code Queue 任务,`--dry-run` 会给出 MiniMax/GPT/人工路由建议但不改写 payload,真实提交成功只返回写入确认、task id 和后续查看命令,不回显 prompt;后者只读检查 D601 scheduler/runner 的 GitHub token、egress 和 PR 能力,PR 型派单前必须使用,规则见 `docs/reference/cli.md` 和 `docs/reference/code-queue-supervision.md`。
|
||||
- `bun scripts/cli.ts codex task <taskId>`:按 Code Queue 任务 ID 查询默认审阅摘要,只返回原始 prompt、最终 response、最后错误和渐进披露命令;`--detail`、`codex output` 和 supervisor 大 `--limit` 仍默认有界,完整内容需显式 `--full`/`--full-text`/分页展开;`codex queues [--full] [--limit N] [--page N|--offset N]` 默认分页低噪声输出队列摘要,完整 upstream 只通过 raw command 显式获取。
|
||||
- `bun scripts/cli.ts codex judge <taskId> --attempt <n> [--dry-run]`:按指定 task/attempt 用与队列 worker 相同的上下文构建和 MiniMax judge 调用路径单步复现完成判定;`--dry-run` 只输出 prompt/payload 诊断。
|
||||
- `bun scripts/cli.ts codex steer <taskId> [prompt|--prompt-file path|--prompt-stdin] [--dry-run]`:通过 Code Queue 私有代理向运行中的 active turn 注入纠偏提示,真实成功只确认写入并返回后续查看命令,不回显 prompt 或完整 task state。
|
||||
- `bun scripts/cli.ts codex steer <taskId> [prompt|--prompt-file path|--prompt-stdin] [--dry-run] [--no-retry|--retry-attempts N]`:通过 Code Queue 私有代理向运行中的 active turn 注入纠偏提示,对 retryable tunnel abort 做有界重试诊断,真实成功只确认写入并返回后续查看命令,不回显 prompt 或完整 task state。
|
||||
- `bun scripts/cli.ts codex interrupt|cancel <taskId>`:通过 Code Queue 私有代理中断运行任务或取消 queued/retry_wait 任务,规则见 `docs/reference/cli.md`。
|
||||
- `bun scripts/cli.ts server stop`:以异步 job 停止固定 Compose 项目中的全部 UniDesk 服务,停止后用 `server status` 复核。
|
||||
- `bun scripts/cli.ts job list [--limit N]` / `bun scripts/cli.ts job status latest [--tail-bytes N]`:分页查询 `.state/jobs/` 中的异步任务状态,状态输出只读日志尾部并保留完整日志路径,job 机制见 `docs/reference/cli.md`。
|
||||
|
||||
@@ -53,8 +53,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` 只输出 `method`、`path`、`stableProxyPath`、prompt 字符数、截断预览和 raw proxy 等价命令,不触碰运行中 session,也不得泄露超长 prompt 全文。真实执行是写入操作,成功只返回 `accepted=true`、task id、prompt 字符数、`promptOmitted=true`、有界 task/queue 确认和后续查看命令,不回显 prompt 或完整 task state;路径固定为 `/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 steer <taskId> [prompt|--prompt-file path|--prompt-stdin] [--dry-run] [--no-retry|--retry-attempts N]` 通过 Code Queue 私有代理向正在运行的 task 注入纠偏提示,正式替代底层 `microservice proxy code-queue /api/tasks/<taskId>/steer` 调用。prompt 必须且只能来自位置参数、文件或 stdin 之一;`--dry-run` 只输出 `method`、`path`、`stableProxyPath`、retry policy、prompt 字符数、截断预览和 raw proxy 等价命令,不触碰运行中 session,也不得泄露超长 prompt 全文。真实执行是写入操作,成功只返回 `accepted=true`、task id、prompt 字符数、`promptOmitted=true`、有界 task/queue 确认、attempt summary 和后续查看命令,不回显 prompt 或完整 task state;路径固定为 `/api/microservices/code-queue/proxy/api/tasks/<taskId>/steer`,只能作用于 D601 scheduler 上存在 active steerable turn 的 running task。默认对 `stable-proxy-failed` 和 `backend-core-unreachable` 这类 retryable control-plane failures 做一次有界重试;`--retry-attempts N` 最大为 3,`--retry-delay-ms N` 最大为 5000,`--no-retry` 用于复现单次失败。
|
||||
- `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`、`attempts`、`retryPolicy` 和推荐交叉验证命令;若任务不在 running/active-turn 状态,通常归类为 `upstream-runtime-rejected`,不得静默成功。`502 provider HTTP tunnel failed`、`provider-gateway-http-fetch`、`The operation was aborted` 或约 30 秒 tunnel wait abort 会归类为 `stable-proxy-failed`,CLI 会先按 retry policy 重试;如果仍失败,`.data.diagnostics.operatorGuidance.rawProxyEquivalentIsFallback=false` 表示 raw proxy 等价命令走同一条 tunnel,只能用于对照诊断,不应被当作更低噪声 fallback。此时 `.data.steer.deliveryUnconfirmed=true`,指挥官应先看 `codex tasks --view supervisor`、`codex task <taskId>` 和 `microservice health code-queue`,再从主 server CLI 或显式 SSH transport 重试同一个 `codex steer`。
|
||||
- `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] [--limit N] [--page N|--offset N]` 列表、`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` 只切换为完整队列行视图的一页,仍受 `--limit`/`--page`/`--offset` 分页约束,不再默认携带 deprecated full array。summary 和 full 的稳定机读路径都是 `.data.queues.items[]`,全局元数据固定在 `.data.queues.counts`、`.data.queues.executionDiagnostics`、`.data.queues.activeTaskIds` 和 `.data.queues.queuedTaskIds`;需要完整 upstream 时使用输出中的 raw command。旧 full 顶层数组语义已作为 deprecated 兼容信息记录,不再作为 `.data.queues` 主形态。同一个 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 链路,而不是绕过控制面。
|
||||
|
||||
@@ -283,7 +283,7 @@ D601 artifact registry 的 systemd unit inactive 不等于 D601 全局离线。
|
||||
只有存在明确理由时才干预。
|
||||
|
||||
- 如果任务还在运行且 trace 或 scheduler heartbeat 新鲜,应引导而不是 interrupt。
|
||||
- 对运行中任务的引导应优先使用正式 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。
|
||||
- 对运行中任务的引导应优先使用正式 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`、retry policy、prompt 摘要和 raw proxy 等价命令但不发送。非 dry-run 默认对 `stable-proxy-failed` 和 `backend-core-unreachable` 做一次有界重试,失败时先看 `.data.diagnostics.reason`、`.data.diagnostics.attempts` 和 `.data.diagnostics.operatorGuidance`:`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 链路问题。`502 provider HTTP tunnel failed`、`The operation was aborted`、约 30 秒 provider tunnel wait abort 仍失败时,`.data.steer.deliveryUnconfirmed=true`;指挥官应先用 `codex tasks --view supervisor --limit 20`、`codex task <taskId>` 和 `microservice health code-queue` 交叉确认任务活性,再从主 server CLI 或显式 SSH transport 重试同一个 steer。raw proxy 等价命令走同一条 tunnel,`rawProxyEquivalentIsFallback=false`,只能做诊断对照,不应作为正式 fallback。若正式 CLI 自身不可用,临时通过受控 microservice proxy 调用只能作为现场恢复手段;这类绕行必须记录到指挥简报 issue #24 主体的常驻观察,并创建正式 issue 补齐 CLI 能力,避免长期依赖隐式 API。
|
||||
- 如果任务进入终态但缺少必要验收证据,应使用聚焦 continuation prompt retry 同一任务。
|
||||
- 如果任务被可复用基础设施缺陷阻塞,应把该缺陷分配给合适的空闲或低风险队列,让原业务任务等待,或在修复后 retry。
|
||||
- 如果基础设施缺陷影响 Code Queue 控制面可用性,指挥官可以执行恢复队列所需的最小受控部署,然后验证原任务能继续。
|
||||
|
||||
+2
-1
@@ -67,6 +67,7 @@ function displayCommandName(parts: string[]): string {
|
||||
}
|
||||
if (parts[0] === "codex" && parts[1] === "steer" && parts[2] !== undefined) {
|
||||
const shown = ["codex", "steer", parts[2]];
|
||||
const shownValueOptions = new Set(["--prompt-file", "--file", "--retry-attempts", "--retry-delay-ms"]);
|
||||
const hasPromptFile = parts.includes("--prompt-file") || parts.includes("--file");
|
||||
const hasPromptStdin = parts.includes("--prompt-stdin") || parts.includes("--stdin");
|
||||
const hasHelp = parts.slice(3).some(isHelpToken);
|
||||
@@ -75,7 +76,7 @@ function displayCommandName(parts: string[]): string {
|
||||
const part = parts[index] ?? "";
|
||||
if (!part.startsWith("--")) continue;
|
||||
shown.push(part);
|
||||
if (part === "--prompt-file" || part === "--file") {
|
||||
if (shownValueOptions.has(part)) {
|
||||
shown.push(parts[index + 1] ?? "<missing>");
|
||||
index += 1;
|
||||
}
|
||||
|
||||
@@ -147,13 +147,58 @@ export function runCodeQueueCliSteerContract(): JsonRecord {
|
||||
assertCondition(!successJson.includes("send this"), "successful steer must not echo prompt text", success);
|
||||
assertCondition(!successJson.includes("promptPreview"), "successful steer must not include promptPreview", 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);
|
||||
assertReason(codexSteerTaskForTest("direct_task", ["p", "--no-retry"], () => ({ ok: false, exitCode: 1, stderrTail: "Cannot connect to the Docker daemon" })), "backend-core-unreachable", null);
|
||||
assertReason(codexSteerTaskForTest("direct_task", ["p", "--no-retry"], () => ({ ok: false, status: 404, body: { ok: false, error: "microservice not found: code-queue" } })), "code-queue-microservice-unregistered", 404);
|
||||
assertReason(codexSteerTaskForTest("direct_task", ["p", "--no-retry"], () => ({ ok: false, status: 401, body: { ok: false, error: "unauthorized" } })), "proxy-unauthorized", 401);
|
||||
assertReason(codexSteerTaskForTest("direct_task", ["p", "--no-retry"], () => ({ 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", "--no-retry"], () => ({ ok: false, status: 404, body: { ok: false, error: "task not found" } })), "steer-endpoint-404", 404);
|
||||
assertReason(codexSteerTaskForTest("direct_task", ["p", "--no-retry"], () => ({ 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", "--no-retry"], () => ({ ok: false, status: 504, body: { ok: false, error: "provider HTTP tunnel timed out or disconnected", stage: "http-tunnel-wait" } })), "stable-proxy-failed", 504);
|
||||
|
||||
const abortedTunnelBody = {
|
||||
ok: false,
|
||||
error: "provider HTTP tunnel failed",
|
||||
stage: "provider-gateway-http-fetch",
|
||||
providerId: "D601",
|
||||
serviceId: "code-queue",
|
||||
providerError: "The operation was aborted",
|
||||
retryable: false,
|
||||
attempts: [{ attempt: 1, ok: false, durationMs: 30003, timeoutMs: 30000, result: { ok: false, error: "The operation was aborted" } }],
|
||||
};
|
||||
let retryCalls = 0;
|
||||
const retryThenSuccess = codexSteerTaskForTest("direct_task", ["transient correction", "--retry-delay-ms", "0"], () => {
|
||||
retryCalls += 1;
|
||||
if (retryCalls === 1) return { ok: false, status: 502, body: abortedTunnelBody };
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
body: {
|
||||
ok: true,
|
||||
task: { id: "direct_task", status: "running", prompt: "hidden" },
|
||||
queue: { activeTaskIds: ["direct_task"] },
|
||||
},
|
||||
};
|
||||
}) as JsonRecord;
|
||||
assertCondition(retryCalls === 2, "retryable 502 tunnel abort should be retried once by default", { retryCalls, retryThenSuccess });
|
||||
assertCondition(nestedRecord(retryThenSuccess, ["steer"]).accepted === true, "retry success should accept steer", retryThenSuccess);
|
||||
const retrySuccessAttempts = nestedRecord(retryThenSuccess, ["steer"]).attempts;
|
||||
assertCondition(Array.isArray(retrySuccessAttempts) && retrySuccessAttempts.length === 2, "retry success should expose both attempts", retryThenSuccess);
|
||||
assertCondition(String(JSON.stringify(retryThenSuccess)).includes("The operation was aborted"), "retry attempts should preserve aborted tunnel evidence", retryThenSuccess);
|
||||
assertCondition(!String(JSON.stringify(retryThenSuccess)).includes("transient correction"), "retry success must not echo steer prompt", retryThenSuccess);
|
||||
|
||||
let exhaustedCalls = 0;
|
||||
const exhausted = codexSteerTaskForTest("direct_task", ["final correction", "--retry-attempts", "2", "--retry-delay-ms", "0"], () => {
|
||||
exhaustedCalls += 1;
|
||||
return { ok: false, status: 502, body: abortedTunnelBody };
|
||||
}) as JsonRecord;
|
||||
assertCondition(exhaustedCalls === 2, "retryable 502 tunnel abort should honor retry-attempts", { exhaustedCalls, exhausted });
|
||||
assertReason(exhausted, "stable-proxy-failed", 502);
|
||||
const exhaustedDiagnostics = nestedRecord(exhausted, ["diagnostics"]);
|
||||
const exhaustedAttempts = exhaustedDiagnostics.attempts;
|
||||
assertCondition(Array.isArray(exhaustedAttempts) && exhaustedAttempts.length === 2, "exhausted retry diagnostics should expose attempts", exhaustedDiagnostics);
|
||||
assertCondition(String(exhaustedDiagnostics.message || "").includes("The operation was aborted"), "diagnostics should include provider abort message", exhaustedDiagnostics);
|
||||
assertCondition(nestedRecord(exhaustedDiagnostics, ["operatorGuidance"]).rawProxyEquivalentIsFallback === false, "raw proxy equivalent should be diagnostic, not fallback", exhaustedDiagnostics);
|
||||
assertCondition(String(nestedRecord(exhausted, ["commands"]).rawProxy || "").includes("microservice proxy code-queue /api/tasks/direct_task/steer"), "failure should still expose raw proxy diagnostic command", exhausted);
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
@@ -170,6 +215,7 @@ export function runCodeQueueCliSteerContract(): JsonRecord {
|
||||
"non-dry-run uses stable proxy helper",
|
||||
"successful steer confirms write without echoing prompt",
|
||||
"steer failure classification is JSON-consumable",
|
||||
"retryable tunnel aborts are retried with bounded diagnostics",
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
+135
-9
@@ -33,6 +33,10 @@ const submitLockWaitMs = 60_000;
|
||||
const submitLockPollMs = 250;
|
||||
const submitLockStaleMs = 120_000;
|
||||
const submitThrottleMs = nonNegativeIntegerEnv("UNIDESK_CODEX_SUBMIT_THROTTLE_MS", 2000);
|
||||
const defaultSteerRetryAttempts = 2;
|
||||
const maxSteerRetryAttempts = 3;
|
||||
const defaultSteerRetryDelayMs = 750;
|
||||
const maxSteerRetryDelayMs = 5_000;
|
||||
|
||||
interface CodexTaskOptions {
|
||||
detail: boolean;
|
||||
@@ -138,6 +142,8 @@ interface SubmitRoutingRecommendation {
|
||||
interface CodexSteerOptions {
|
||||
prompt: string;
|
||||
dryRun: boolean;
|
||||
retryAttempts: number;
|
||||
retryDelayMs: number;
|
||||
}
|
||||
|
||||
type CodexSteerFailureReason =
|
||||
@@ -164,6 +170,19 @@ interface ClassifiedCodexSteerError {
|
||||
recommendedCrossChecks: string[];
|
||||
}
|
||||
|
||||
interface CodexSteerAttemptSummary {
|
||||
attempt: number;
|
||||
ok: boolean;
|
||||
durationMs: number;
|
||||
status: number | null;
|
||||
exitCode: number | null;
|
||||
reason: CodexSteerFailureReason | null;
|
||||
scope: ClassifiedCodexSteerError["scope"] | null;
|
||||
retryable: boolean;
|
||||
message: string | null;
|
||||
upstreamBodyPreview?: unknown;
|
||||
}
|
||||
|
||||
interface CompactTaskMutationResponseOptions {
|
||||
fullPrompt?: boolean;
|
||||
}
|
||||
@@ -497,7 +516,10 @@ function responseBody(response: Record<string, unknown> | null): Record<string,
|
||||
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;
|
||||
const providerError = body?.providerError;
|
||||
if (typeof bodyError === "string" && bodyError.length > 0) {
|
||||
return typeof providerError === "string" && providerError.length > 0 ? `${bodyError}: ${providerError}` : 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;
|
||||
@@ -577,6 +599,41 @@ function unwrapSteerResponse(response: unknown, targetPath: string, stableProxyP
|
||||
return { ok: false, diagnostics: classifySteerFailure(response, targetPath, stableProxyPath, rawProxyEquivalent) };
|
||||
}
|
||||
|
||||
function steerSuccessAttempt(attempt: number, durationMs: number, upstream: { ok: unknown; status: unknown }): CodexSteerAttemptSummary {
|
||||
const status = typeof upstream.status === "number" && Number.isFinite(upstream.status) ? upstream.status : null;
|
||||
return {
|
||||
attempt,
|
||||
ok: true,
|
||||
durationMs,
|
||||
status,
|
||||
exitCode: null,
|
||||
reason: null,
|
||||
scope: null,
|
||||
retryable: false,
|
||||
message: "steer accepted by Code Queue",
|
||||
};
|
||||
}
|
||||
|
||||
function steerFailureAttempt(attempt: number, durationMs: number, diagnostics: ClassifiedCodexSteerError): CodexSteerAttemptSummary {
|
||||
return {
|
||||
attempt,
|
||||
ok: false,
|
||||
durationMs,
|
||||
status: diagnostics.status,
|
||||
exitCode: diagnostics.exitCode,
|
||||
reason: diagnostics.reason,
|
||||
scope: diagnostics.scope,
|
||||
retryable: diagnostics.retryable,
|
||||
message: diagnostics.message,
|
||||
upstreamBodyPreview: diagnostics.upstreamBodyPreview,
|
||||
};
|
||||
}
|
||||
|
||||
function shouldRetrySteerFailure(diagnostics: ClassifiedCodexSteerError, attempt: number, maxAttempts: number): boolean {
|
||||
if (attempt >= maxAttempts) return false;
|
||||
return diagnostics.retryable === true && (diagnostics.reason === "stable-proxy-failed" || diagnostics.reason === "backend-core-unreachable");
|
||||
}
|
||||
|
||||
function positiveIntegerOption(args: string[], names: string[], defaultValue: number, maxValue = Number.MAX_SAFE_INTEGER): number {
|
||||
for (const name of names) {
|
||||
const index = args.indexOf(name);
|
||||
@@ -2705,6 +2762,8 @@ const submitPromptValueOptions = new Set([
|
||||
const steerPromptValueOptions = new Set([
|
||||
"--prompt-file",
|
||||
"--file",
|
||||
"--retry-attempts",
|
||||
"--retry-delay-ms",
|
||||
]);
|
||||
|
||||
function referenceTaskIdsFromOptions(args: string[]): string[] {
|
||||
@@ -2758,12 +2817,17 @@ function parseSubmitOptions(args: string[]): CodexSubmitOptions {
|
||||
|
||||
function parseSteerOptions(args: string[]): CodexSteerOptions {
|
||||
assertKnownOptions(args, {
|
||||
flags: ["--prompt-stdin", "--stdin", "--dry-run"],
|
||||
valueOptions: ["--prompt-file", "--file"],
|
||||
flags: ["--prompt-stdin", "--stdin", "--dry-run", "--no-retry"],
|
||||
valueOptions: ["--prompt-file", "--file", "--retry-attempts", "--retry-delay-ms"],
|
||||
}, "codex steer");
|
||||
const retryAttempts = hasFlag(args, "--no-retry")
|
||||
? 1
|
||||
: positiveIntegerOption(args, ["--retry-attempts"], defaultSteerRetryAttempts, maxSteerRetryAttempts);
|
||||
return {
|
||||
prompt: promptFromArgs(args, "codex steer", steerPromptValueOptions),
|
||||
dryRun: hasFlag(args, "--dry-run"),
|
||||
retryAttempts,
|
||||
retryDelayMs: nonNegativeIntegerOption(args, ["--retry-delay-ms"], defaultSteerRetryDelayMs, maxSteerRetryDelayMs),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3887,6 +3951,13 @@ function codexSteerTask(taskId: string, args: string[], fetcher: CodexResponseFe
|
||||
path: targetPath,
|
||||
stableProxyPath,
|
||||
method: "POST",
|
||||
retryPolicy: {
|
||||
defaultAttempts: defaultSteerRetryAttempts,
|
||||
maxAttempts: options.retryAttempts,
|
||||
delayMs: options.retryDelayMs,
|
||||
retryableReasons: ["stable-proxy-failed", "backend-core-unreachable"],
|
||||
deliveryConfirmation: "success confirms Code Queue accepted the steer request; repeated stable-proxy failures mean delivery is unconfirmed",
|
||||
},
|
||||
bodySummary: {
|
||||
promptChars: options.prompt.length,
|
||||
promptPreviewChars: steerPromptPreviewChars,
|
||||
@@ -3901,22 +3972,70 @@ function codexSteerTask(taskId: string, args: string[], fetcher: CodexResponseFe
|
||||
request,
|
||||
commands: {
|
||||
run: `bun scripts/cli.ts codex steer ${taskId} --prompt-file <path>`,
|
||||
noRetry: `bun scripts/cli.ts codex steer ${taskId} --prompt-file <path> --no-retry`,
|
||||
rawProxy: rawProxyEquivalent,
|
||||
},
|
||||
};
|
||||
}
|
||||
const response = unwrapSteerResponse(fetcher(stableProxyPath, { method: "POST", body: { prompt: options.prompt } }), targetPath, stableProxyPath, rawProxyEquivalent);
|
||||
if (!response.ok) {
|
||||
const attempts: CodexSteerAttemptSummary[] = [];
|
||||
let failedResponse: { ok: false; diagnostics: ClassifiedCodexSteerError } | null = null;
|
||||
let successfulResponse: { ok: true; upstream: { ok: unknown; status: unknown }; body: Record<string, unknown> } | null = null;
|
||||
for (let attempt = 1; attempt <= options.retryAttempts; attempt += 1) {
|
||||
const startedAt = Date.now();
|
||||
const response = unwrapSteerResponse(fetcher(stableProxyPath, { method: "POST", body: { prompt: options.prompt } }), targetPath, stableProxyPath, rawProxyEquivalent);
|
||||
const durationMs = Date.now() - startedAt;
|
||||
if (response.ok) {
|
||||
attempts.push(steerSuccessAttempt(attempt, durationMs, response.upstream));
|
||||
successfulResponse = response;
|
||||
break;
|
||||
}
|
||||
attempts.push(steerFailureAttempt(attempt, durationMs, response.diagnostics));
|
||||
failedResponse = response;
|
||||
if (!shouldRetrySteerFailure(response.diagnostics, attempt, options.retryAttempts)) break;
|
||||
sleepSync(options.retryDelayMs);
|
||||
}
|
||||
if (successfulResponse === null) {
|
||||
const diagnostics = failedResponse?.diagnostics ?? classifySteerFailure(null, targetPath, stableProxyPath, rawProxyEquivalent);
|
||||
const transportDeliveryUnconfirmed = diagnostics.reason === "stable-proxy-failed" || diagnostics.reason === "backend-core-unreachable";
|
||||
return {
|
||||
ok: false,
|
||||
steer: {
|
||||
accepted: false,
|
||||
prompt,
|
||||
attempts,
|
||||
deliveryUnconfirmed: transportDeliveryUnconfirmed,
|
||||
retryPolicy: {
|
||||
attempted: attempts.length,
|
||||
maxAttempts: options.retryAttempts,
|
||||
retryDelayMs: options.retryDelayMs,
|
||||
retried: attempts.length > 1,
|
||||
exhausted: transportDeliveryUnconfirmed && attempts.length >= options.retryAttempts,
|
||||
note: "codex steer retried only retryable stable-proxy/backend-core failures; raw microservice proxy uses the same tunnel and is diagnostic, not a lower-noise fallback.",
|
||||
},
|
||||
},
|
||||
request,
|
||||
diagnostics: response.diagnostics,
|
||||
diagnostics: {
|
||||
...diagnostics,
|
||||
attempts,
|
||||
retryPolicy: {
|
||||
attempted: attempts.length,
|
||||
maxAttempts: options.retryAttempts,
|
||||
retryDelayMs: options.retryDelayMs,
|
||||
retried: attempts.length > 1,
|
||||
exhausted: transportDeliveryUnconfirmed && attempts.length >= options.retryAttempts,
|
||||
},
|
||||
operatorGuidance: {
|
||||
rawProxyEquivalentIsFallback: false,
|
||||
deliveryUnconfirmed: transportDeliveryUnconfirmed,
|
||||
nextStep: transportDeliveryUnconfirmed
|
||||
? "Check task liveness and retry codex steer from the main-server CLI or explicit SSH transport; do not treat a raw proxy failure as separate evidence that the task rejected the correction."
|
||||
: "Inspect the non-retryable reason before resubmitting the correction.",
|
||||
},
|
||||
},
|
||||
commands: {
|
||||
dryRun: `bun scripts/cli.ts codex steer ${taskId} --prompt-file <path> --dry-run`,
|
||||
retry: `bun scripts/cli.ts codex steer ${taskId} --prompt-file <path>`,
|
||||
noRetry: `bun scripts/cli.ts codex steer ${taskId} --prompt-file <path> --no-retry`,
|
||||
rawProxy: rawProxyEquivalent,
|
||||
tasks: "bun scripts/cli.ts codex tasks --view supervisor --limit 20",
|
||||
health: "bun scripts/cli.ts microservice health code-queue",
|
||||
@@ -3925,12 +4044,19 @@ function codexSteerTask(taskId: string, args: string[], fetcher: CodexResponseFe
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
upstream: response.upstream,
|
||||
upstream: successfulResponse.upstream,
|
||||
steer: {
|
||||
accepted: true,
|
||||
taskId,
|
||||
promptChars: options.prompt.length,
|
||||
promptOmitted: true,
|
||||
attempts,
|
||||
retryPolicy: {
|
||||
attempted: attempts.length,
|
||||
maxAttempts: options.retryAttempts,
|
||||
retryDelayMs: options.retryDelayMs,
|
||||
retried: attempts.length > 1,
|
||||
},
|
||||
outputPolicy: {
|
||||
default: "write-confirmation",
|
||||
promptEchoed: false,
|
||||
@@ -3938,8 +4064,8 @@ function codexSteerTask(taskId: string, args: string[], fetcher: CodexResponseFe
|
||||
reason: "codex steer is a write operation; default output confirms delivery and provides drill-down commands without echoing prompt text or full task state.",
|
||||
},
|
||||
},
|
||||
task: compactSubmitTaskConfirmation(response.body.task),
|
||||
queue: compactSubmitQueueConfirmation(response.body.queue),
|
||||
task: compactSubmitTaskConfirmation(successfulResponse.body.task),
|
||||
queue: compactSubmitQueueConfirmation(successfulResponse.body.queue),
|
||||
commands: {
|
||||
show: `bun scripts/cli.ts codex task ${taskId}`,
|
||||
detail: `bun scripts/cli.ts codex task ${taskId} --detail`,
|
||||
|
||||
+2
-2
@@ -60,7 +60,7 @@ export function rootHelp(): unknown {
|
||||
{ command: "codex read <taskId>", description: "Mark one reviewed terminal task read; never run automatically as part of listing." },
|
||||
{ command: "codex dev-ready", description: "Fetch execution-container readiness, including sanitized skill injection status from /api/dev-ready." },
|
||||
{ command: "codex judge <taskId> --attempt N [--dry-run] [--include-prompt]", description: "Replay one stored Code Queue attempt through the same judge context builder and MiniMax judge call path used by the live queue worker." },
|
||||
{ command: "codex steer <taskId> [prompt|--prompt-file path|--prompt-stdin] [--dry-run]", description: "Push a corrective prompt into a running Code Queue task; real success only confirms the write and does not echo prompt text." },
|
||||
{ command: "codex steer <taskId> [prompt|--prompt-file path|--prompt-stdin] [--dry-run] [--no-retry|--retry-attempts N]", description: "Push a corrective prompt into a running Code Queue task; retryable tunnel aborts get bounded retry diagnostics, and real success does not echo prompt text." },
|
||||
{ command: "codex interrupt|cancel <taskId>", description: "Request interrupt for a running Code Queue task, or cancel a queued/retry_wait task, through the same private proxy." },
|
||||
{ command: "codex (queues [--full|--all] | queue create <queueId> | queue merge <sourceQueueId> --into <targetQueueId> | move <taskId> --queue <queueId>)", description: "List low-noise queue summaries by default; full queue rows require --full/--all." },
|
||||
{ command: "job list [--limit N] [--include-command]", description: "List async jobs from .state/jobs with a bounded default page." },
|
||||
@@ -258,7 +258,7 @@ function codexHelp(): unknown {
|
||||
"bun scripts/cli.ts codex dev-ready",
|
||||
"bun scripts/cli.ts codex pr-preflight [--remote] [--push-dry-run --push-dry-run-ref refs/heads/probe/<name>] [--pr-create-dry-run --pr-create-dry-run-head <head>] [--issue N]",
|
||||
"bun scripts/cli.ts codex judge <taskId> --attempt N [--dry-run] [--include-prompt]",
|
||||
"bun scripts/cli.ts codex steer <taskId> [prompt|--prompt-file path|--prompt-stdin] [--dry-run]",
|
||||
"bun scripts/cli.ts codex steer <taskId> [prompt|--prompt-file path|--prompt-stdin] [--dry-run] [--no-retry|--retry-attempts N]",
|
||||
"bun scripts/cli.ts codex interrupt|cancel <taskId>",
|
||||
"bun scripts/cli.ts codex queues [--full|--all] [--limit N] [--page N|--offset N] | queue create <queueId> | queue merge <sourceQueueId> --into <targetQueueId> | move <taskId> --queue <queueId>",
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user