feat: add codex tasks overview

This commit is contained in:
Codex
2026-05-19 17:31:18 +00:00
parent 22a0b709f2
commit 2a21f6cda3
4 changed files with 338 additions and 13 deletions
+2 -1
View File
@@ -33,6 +33,7 @@ CLI 可以从 `master` 快速演进,但必须兼容 `deploy.json` 固定的 CI
- `codex deploy <commitId>` 是旧 Code Queue 兼容部署入口,已禁用以防止维护通道直连 D601 部署 Code Queue;当前 dev 自动化只做 `ci run-dev-e2e` smoke,不提供 Code Queue CD,详细规则见 `docs/reference/codex-deploy.md`
- `codex submit [prompt] [--prompt-file path|--prompt-stdin] [--queue queueId] [--provider-id id] [--cwd path] [--model model] [--reasoning-effort effort] [--execution-mode mode] [--max-attempts N] [--reference-task-id id] [--dry-run]` 通过 backend-core 私有代理向稳定 `code-queue` 用户服务路径提交任务;prompt 必须且只能来自位置参数、文件或 stdin 之一,`--dry-run` 只返回结构化请求且不实际入队。提交确认和 dry-run 必须返回完整 prompt、字符数和 `truncated=false`,不能套用任务详情的预览截断策略,否则长任务 prompt 无法被人工验收。backend-core 默认把提交、队列 CRUD、已读状态、历史摘要和轻量 Trace 读取分流到主 server `code-queue-mgr`,由它写入主 PostgreSQLD601 scheduler 只轮询并执行已入库任务。
- `codex task <taskId>` 通过 Code Queue 私有代理按任务 ID 查询结构化执行摘要;默认只返回有界 prompt/response 预览、执行 Provider、工作目录、最后 assistant message、最近工具调用摘要、attempt、judge、错误、耗时和 trace 翻页提示,适合在新队列任务中引用历史 session 且避免噪声爆炸。该摘要读取默认由主 server `code-queue-mgr` 从 PostgreSQL 返回,不依赖 D601 `code-queue-read` Service 可用。
- `codex tasks [--queue id] [--limit N] [--unread-only]` 通过同一私有代理输出一个只读聚合视图,按 `running``completedUnread``recentCompleted` 三个 section 汇总当前需要盯的任务;每个条目都带 `taskId``queueId``status``currentAttempt``updatedAt``finishedAt``unread`/`unreadTerminal``lastAssistantMessage` 摘要和可直接复制的 `commands.show` / `commands.trace``--queue` 限定单个队列,`--limit` 控制各 section 的最大条数,`--unread-only` 只保留未读终态和正在运行的任务。
- `codex task <taskId> --trace --tail|--from-start|--after-seq N|--before-seq N --limit N` 按页拉取 Code Queue 的逻辑 trace;响应会返回 `nextAfterSeq``previousBeforeSeq``hasMore``hasBefore` 和下一页/上一页命令,默认 `--trace` 取最新一页,需要完整 prompt/最后 response 时加 `--full`
- `codex output <taskId> --tail|--from-start|--after-seq N|--before-seq N --limit N [--full-text]` 按原始 output seq 分页读取底层记录;当 trace 行提示 `commandOmittedLines``bodyOmittedLines``rawSeqs` 时,用该命令按 seq 补取完整信息,默认仍有单条文本预览上限,显式 `--full-text` 才返回该页全文。
- `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` 仅用于本地深度排查。
@@ -129,7 +130,7 @@ bun scripts/cli.ts ssh D601 glob --root /home/ubuntu/pikapython --pattern '**/*-
`--main-server-ip` 是一个全局前缀,必须放在需要透传的命令同一次调用中,例如 `bun scripts/cli.ts --main-server-ip 74.48.78.17 debug health`。默认传输是公网 frontend:本地 CLI 读取本仓库 `config.json` 中的 frontend 登录账号密码,登录 `http://<ip>:<frontendPort>/` 获取 HttpOnly session cookie,然后通过 frontend 的 `/api/*` 同源代理访问 backend-core 内网 API;因此计算节点只需要能访问公网 frontend,不需要主 server SSH key,也不需要打开 backend-core REST API 或 PostgreSQL 端口。
默认 frontend 传输支持 `debug health``debug dispatch``debug task``microservice list/status/health/diagnostics/tunnel-self-test/proxy``decision upload/list/show/health``decision requirement list/upsert``decision diary import/list/months/show/edit/upsert``codex task <taskId>``codex output <taskId>``codex judge <taskId> --attempt N``ssh <PROVIDER_ID> <remote-command>`。其中 `ssh` 的 remote frontend 传输使用 `host.ssh` dispatch 执行有界远端命令,适合 `ssh D601 hostname``ssh D601 skills` 这类自测;交互式登录 shell 仍应在主 server 本机 CLI 使用,或显式切换到旧 SSH 传输后在主 server 上执行。frontend 远程透传不会流式转发本地 stdin,因此 `ssh py < script.py``ssh apply-patch < patch.diff` 这类 stdin-backed helper 必须在主 server 本机运行,或显式切换到 `--main-server-transport ssh`。若确实需要旧行为,可使用 `--main-server-key <key>``--main-server-transport ssh`,这时 CLI 会通过 SSH 登录主 server 的 `--main-server-root` 目录执行同一个 `bun scripts/cli.ts <command>`
默认 frontend 传输支持 `debug health``debug dispatch``debug task``microservice list/status/health/diagnostics/tunnel-self-test/proxy``decision upload/list/show/health``decision requirement list/upsert``decision diary import/list/months/show/edit/upsert``codex task <taskId>``codex tasks``codex output <taskId>``codex judge <taskId> --attempt N``ssh <PROVIDER_ID> <remote-command>`。其中 `ssh` 的 remote frontend 传输使用 `host.ssh` dispatch 执行有界远端命令,适合 `ssh D601 hostname``ssh D601 skills` 这类自测;交互式登录 shell 仍应在主 server 本机 CLI 使用,或显式切换到旧 SSH 传输后在主 server 上执行。frontend 远程透传不会流式转发本地 stdin,因此 `ssh py < script.py``ssh apply-patch < patch.diff` 这类 stdin-backed helper 必须在主 server 本机运行,或显式切换到 `--main-server-transport ssh`。若确实需要旧行为,可使用 `--main-server-key <key>``--main-server-transport ssh`,这时 CLI 会通过 SSH 登录主 server 的 `--main-server-root` 目录执行同一个 `bun scripts/cli.ts <command>`
计算节点可以用该入口测试自身的远程升级闭环,而不需要在计算节点公开 core REST API 或 database。标准顺序是:先运行 `bun scripts/cli.ts --main-server-ip 74.48.78.17 debug health` 确认主 server 看到当前 Provider 在线,且该 Provider labels 中 `unideskCapabilities` 包含 `host.ssh``hostSshConfigured=true``hostSshKeyPresent=true`;再运行 `bun scripts/cli.ts --main-server-ip 74.48.78.17 debug dispatch <PROVIDER_ID> provider.upgrade --mode schedule --wait-ms 15000` 触发真实 `provider.upgrade`;随后再次运行 `debug health` 确认节点重新上线;最后运行 `bun scripts/cli.ts --main-server-ip 74.48.78.17 debug dispatch <PROVIDER_ID> host.ssh --wait-ms 15000``bun scripts/cli.ts --main-server-ip 74.48.78.17 ssh <PROVIDER_ID> hostname` 验证 SSH 透传能力。provider-gateway 新部署或升级后没有完成这组 remote CLI 自测,不能视为交付完成。
+318 -1
View File
@@ -7,6 +7,8 @@ const defaultTraceLimit = 80;
const maxTraceLimit = 500;
const defaultOutputLimit = 20;
const defaultTextPreviewChars = 12_000;
const defaultTasksLimit = 20;
const maxTasksLimit = 100;
interface CodexTaskOptions {
trace: boolean;
@@ -51,6 +53,42 @@ interface CompactTaskMutationResponseOptions {
fullPrompt?: boolean;
}
interface CodexTasksOptions {
queueId: string | undefined;
limit: number;
unreadOnly: boolean;
}
interface CodexTasksEntry {
taskId: string;
queueId: string | null;
status: string | null;
currentAttempt: number | null;
updatedAt: string | null;
finishedAt: string | null;
readAt: string | null;
unread: boolean;
unreadTerminal: boolean;
lastAssistantMessage: Record<string, unknown> | null;
commands: {
show: string;
trace: string;
};
}
interface CodexTasksSection {
count: number;
returned: number;
truncated: boolean;
items: CodexTasksEntry[];
}
interface CodexTasksDegraded {
summaryFetchFailedTaskIds: string[];
summaryFetchErrorCount: number;
reason: string;
}
type CodexRequestInit = { method?: string; body?: unknown };
type CodexResponseFetcher = (path: string, init?: CodexRequestInit) => unknown;
type AsyncCodexResponseFetcher = (path: string, init?: CodexRequestInit) => Promise<unknown>;
@@ -528,6 +566,14 @@ function parseOutputOptions(args: string[]): CodexOutputOptions {
};
}
function parseTasksOptions(args: string[]): CodexTasksOptions {
return {
queueId: optionValue(args, ["--queue", "--queue-id"]),
limit: positiveIntegerOption(args, ["--limit"], defaultTasksLimit, maxTasksLimit),
unreadOnly: hasFlag(args, "--unread-only"),
};
}
function parseJudgeOptions(args: string[]): CodexJudgeOptions {
const rawAttempt = optionValue(args, ["--attempt", "--attempt-id", "--attemptIndex"]) ?? positionalArgs(args)[0];
let attempt: number | null = null;
@@ -639,6 +685,272 @@ export function codexJudgeQuery(taskId: string, optionArgs: string[], fetcher: C
return codexTaskJudge(taskId, parseJudgeOptions(optionArgs), fetcher);
}
function isTerminalTaskStatus(status: unknown): boolean {
return status === "succeeded" || status === "failed" || status === "canceled";
}
function isActiveTaskStatus(status: unknown): boolean {
return status === "running" || status === "judging" || status === "retry_wait" || status === "queued";
}
function taskStatusRank(status: unknown): number {
if (status === "running") return 0;
if (status === "judging") return 1;
if (status === "retry_wait") return 2;
if (status === "queued") return 3;
if (status === "succeeded") return 9;
if (status === "failed") return 10;
if (status === "canceled") return 11;
return 99;
}
function taskTimelineMs(task: Record<string, unknown>): number {
const value = asString(task.finishedAt ?? task.updatedAt ?? task.createdAt);
const timestamp = Date.parse(value);
return Number.isFinite(timestamp) ? timestamp : 0;
}
function taskOverviewCandidateKey(task: Record<string, unknown>): string {
return asString(task.id);
}
function taskUnreadTerminal(task: Record<string, unknown>): boolean {
const directUnread = task.terminalUnread ?? task.unreadTerminal ?? task.unread;
if (typeof directUnread === "boolean") return directUnread;
const status = asString(task.status);
const readAt = task.readAt;
return isTerminalTaskStatus(status) && (readAt === null || readAt === undefined || readAt === "");
}
function taskWatchEntry(task: Record<string, unknown>, summary: Record<string, unknown> | null): CodexTasksEntry {
const taskId = asString(task.id);
const summaryCommands = summary === null ? null : asRecord(summary.commands);
const summaryLastAssistant = summary?.lastAssistantMessage ?? task.lastAssistantMessage;
const showCommand = typeof summary?.cliHint === "string" && summary.cliHint.length > 0
? summary.cliHint
: `bun scripts/cli.ts codex task ${taskId}`;
const traceCommand = typeof summary?.traceHint === "string" && summary.traceHint.length > 0
? summary.traceHint
: `bun scripts/cli.ts codex task ${taskId} --trace --tail --limit ${defaultTraceLimit}`;
return {
taskId,
queueId: asString(task.queueId) || null,
status: asString(task.status) || null,
currentAttempt: typeof task.currentAttempt === "number" && Number.isFinite(task.currentAttempt) ? task.currentAttempt : null,
updatedAt: asString(task.updatedAt) || null,
finishedAt: asString(task.finishedAt) || null,
readAt: asString(task.readAt) || null,
unread: taskUnreadTerminal(task),
unreadTerminal: taskUnreadTerminal(task),
lastAssistantMessage: summaryLastAssistant === undefined || summaryLastAssistant === null ? null : compactLastAssistant(summaryLastAssistant, false),
commands: {
show: typeof summaryCommands?.show === "string" && summaryCommands.show.length > 0 ? summaryCommands.show : showCommand,
trace: typeof summaryCommands?.trace === "string" && summaryCommands.trace.length > 0 ? summaryCommands.trace : traceCommand,
},
};
}
function buildTaskWatchSection(tasks: Record<string, unknown>[], summaries: Map<string, Record<string, unknown>>, limit: number): CodexTasksSection {
const visibleTasks = tasks.slice(0, limit);
const items = visibleTasks.map((task) => taskWatchEntry(task, summaries.get(taskOverviewCandidateKey(task)) ?? null));
return {
count: tasks.length,
returned: items.length,
truncated: tasks.length > limit,
items,
};
}
function collectTaskWatchDegraded(summaryErrors: Array<{ taskId: string; message: string }>): CodexTasksDegraded | null {
if (summaryErrors.length === 0) return null;
return {
summaryFetchFailedTaskIds: summaryErrors.map((error) => error.taskId),
summaryFetchErrorCount: summaryErrors.length,
reason: "task summary fetch failed for one or more entries; unread state still comes from task-level overview data",
};
}
function sortRunningWatchTasks(tasks: Record<string, unknown>[]): Record<string, unknown>[] {
return [...tasks]
.filter((task) => isActiveTaskStatus(asString(task.status)))
.sort((left, right) => {
const rankDelta = taskStatusRank(asString(left.status)) - taskStatusRank(asString(right.status));
if (rankDelta !== 0) return rankDelta;
const timeDelta = taskTimelineMs(right) - taskTimelineMs(left);
if (timeDelta !== 0) return timeDelta;
return asString(left.id).localeCompare(asString(right.id));
});
}
function sortCompletedWatchTasks(tasks: Record<string, unknown>[]): Record<string, unknown>[] {
return [...tasks]
.filter((task) => isTerminalTaskStatus(asString(task.status)))
.sort((left, right) => {
const timeDelta = taskTimelineMs(right) - taskTimelineMs(left);
if (timeDelta !== 0) return timeDelta;
return asString(left.id).localeCompare(asString(right.id));
});
}
function fetchTaskSummaries(taskIds: string[], fetcher: CodexResponseFetcher): { summaries: Map<string, Record<string, unknown>>; degraded: CodexTasksDegraded | null } {
const summaries = new Map<string, Record<string, unknown>>();
const errors: Array<{ taskId: string; message: string }> = [];
for (const taskId of taskIds) {
try {
const response = unwrapCodexResponse(fetcher(codeQueueProxyPath(`/api/tasks/${encodeURIComponent(taskId)}/summary${queryString({ toolLimit: 1 })}`)));
const summary = asRecord(response.body.summary) ?? {};
summaries.set(taskId, summary);
} catch (error) {
errors.push({ taskId, message: error instanceof Error ? error.message : String(error) });
}
}
return { summaries, degraded: collectTaskWatchDegraded(errors) };
}
async function fetchTaskSummariesAsync(taskIds: string[], fetcher: AsyncCodexResponseFetcher): Promise<{ summaries: Map<string, Record<string, unknown>>; degraded: CodexTasksDegraded | null }> {
const summaries = new Map<string, Record<string, unknown>>();
const errors: Array<{ taskId: string; message: string }> = [];
await Promise.all(taskIds.map(async (taskId) => {
try {
const response = unwrapCodexResponse(await fetcher(codeQueueProxyPath(`/api/tasks/${encodeURIComponent(taskId)}/summary${queryString({ toolLimit: 1 })}`)));
const summary = asRecord(response.body.summary) ?? {};
summaries.set(taskId, summary);
} catch (error) {
errors.push({ taskId, message: error instanceof Error ? error.message : String(error) });
}
}));
return { summaries, degraded: collectTaskWatchDegraded(errors) };
}
type CodexTasksTaskPage = {
queue: Record<string, unknown> | null;
pagination: Record<string, unknown>;
tasks: Record<string, unknown>[];
};
function tasksListQueryString(options: CodexTasksOptions): string {
return queryString({
limit: 200,
priorityLimit: 200,
queueId: options.queueId,
includeActive: 1,
selected: 0,
compact: 1,
stats: 0,
skipTrace: 1,
});
}
function loadCodexTasks(taskArgs: CodexTasksOptions, fetcher: CodexResponseFetcher): { upstream: { ok: unknown; status: unknown }; page: CodexTasksTaskPage } {
const byId = new Map<string, Record<string, unknown>>();
const response = unwrapCodexResponse(fetcher(codeQueueProxyPath(`/api/tasks/overview${tasksListQueryString(taskArgs)}`)));
const pageTasks = asArray(response.body.tasks).map((task) => asRecord(task)).filter((task): task is Record<string, unknown> => task !== null);
for (const task of pageTasks) {
const taskId = taskOverviewCandidateKey(task);
if (taskId.length === 0) continue;
const existing = byId.get(taskId);
if (existing === undefined || taskTimelineMs(task) >= taskTimelineMs(existing)) byId.set(taskId, task);
}
const tasks = Array.from(byId.values()).sort((left, right) => {
const leftTime = taskTimelineMs(left);
const rightTime = taskTimelineMs(right);
if (leftTime !== rightTime) return rightTime - leftTime;
return asString(left.id).localeCompare(asString(right.id));
});
return { upstream: response.upstream, page: { queue: asRecord(response.body.queue), pagination: asRecord(response.body.pagination) ?? {}, tasks } };
}
function codexTasksOverviewResult(
taskPage: CodexTasksTaskPage,
upstream: { ok: unknown; status: unknown },
options: CodexTasksOptions,
summaries: Map<string, Record<string, unknown>>,
degraded: CodexTasksDegraded | null,
): Record<string, unknown> {
const allTasks = options.queueId === undefined ? taskPage.tasks : taskPage.tasks.filter((task) => asString(task.queueId) === options.queueId);
const runningTasks = sortRunningWatchTasks(allTasks);
const unreadCompletedTasks = sortCompletedWatchTasks(allTasks).filter((task) => taskUnreadTerminal(task));
const recentCompletedTasks = options.unreadOnly ? [] : sortCompletedWatchTasks(allTasks);
const runningSection = buildTaskWatchSection(runningTasks, summaries, options.limit);
const unreadSection = buildTaskWatchSection(unreadCompletedTasks, summaries, options.limit);
const recentSection = buildTaskWatchSection(recentCompletedTasks, summaries, options.limit);
const pagination = taskPage.pagination;
return {
upstream,
overview: {
filters: {
queueId: options.queueId ?? null,
limit: options.limit,
unreadOnly: options.unreadOnly,
},
source: {
endpoint: "/api/tasks/overview",
queueId: options.queueId ?? null,
limit: asNumber(pagination.limit, 0) || null,
returned: asNumber(pagination.returned, 0) || null,
total: asNumber(pagination.total, 0) || null,
hasMore: asBoolean(pagination.hasMore),
nextBeforeId: asString(pagination.nextBeforeId) || null,
includeActive: asBoolean(pagination.includeActive),
},
queue: taskPage.queue,
counts: {
scanned: allTasks.length,
running: runningSection.count,
completedUnread: unreadSection.count,
recentCompleted: recentSection.count,
},
degraded,
commands: {
refresh: `bun scripts/cli.ts codex tasks${options.queueId === undefined ? "" : ` --queue ${options.queueId}`}${options.unreadOnly ? " --unread-only" : ""}${options.limit === defaultTasksLimit ? "" : ` --limit ${options.limit}`}`,
},
running: runningSection,
completedUnread: unreadSection,
recentCompleted: recentSection,
},
};
}
function visibleTaskIdsForOverview(tasks: Record<string, unknown>[], options: CodexTasksOptions): string[] {
return Array.from(new Set([
...sortRunningWatchTasks(tasks).slice(0, options.limit),
...sortCompletedWatchTasks(tasks).filter((task) => taskUnreadTerminal(task)).slice(0, options.limit),
...sortCompletedWatchTasks(tasks).slice(0, options.limit),
].map((task) => taskOverviewCandidateKey(task))))
.filter((taskId) => taskId.length > 0);
}
function codexTasksQuery(taskArgs: string[], fetcher: CodexResponseFetcher = coreInternalFetch): unknown {
const options = parseTasksOptions(taskArgs);
const { upstream, page } = loadCodexTasks(options, fetcher);
const sectionTaskIds = visibleTaskIdsForOverview(page.tasks, options);
const { summaries, degraded } = fetchTaskSummaries(sectionTaskIds, fetcher);
return codexTasksOverviewResult(page, upstream, options, summaries, degraded);
}
async function codexTasksQueryAsync(taskArgs: string[], fetcher: AsyncCodexResponseFetcher): Promise<unknown> {
const options = parseTasksOptions(taskArgs);
const byId = new Map<string, Record<string, unknown>>();
const response = unwrapCodexResponse(await fetcher(codeQueueProxyPath(`/api/tasks/overview${tasksListQueryString(options)}`)));
const pageTasks = asArray(response.body.tasks).map((task) => asRecord(task)).filter((task): task is Record<string, unknown> => task !== null);
for (const task of pageTasks) {
const taskId = taskOverviewCandidateKey(task);
if (taskId.length === 0) continue;
const existing = byId.get(taskId);
if (existing === undefined || taskTimelineMs(task) >= taskTimelineMs(existing)) byId.set(taskId, task);
}
const tasks = Array.from(byId.values()).sort((left, right) => {
const leftTime = taskTimelineMs(left);
const rightTime = taskTimelineMs(right);
if (leftTime !== rightTime) return rightTime - leftTime;
return asString(left.id).localeCompare(asString(right.id));
});
const page: CodexTasksTaskPage = { queue: asRecord(response.body.queue), pagination: asRecord(response.body.pagination) ?? {}, tasks };
const visibleTaskIds = visibleTaskIdsForOverview(page.tasks, options);
const { summaries, degraded } = await fetchTaskSummariesAsync(visibleTaskIds, fetcher);
return codexTasksOverviewResult(page, response.upstream, options, summaries, degraded);
}
async function codexTaskSummaryAsync(taskId: string, options: CodexTaskOptions, fetcher: AsyncCodexResponseFetcher): Promise<unknown> {
const summaryPath = codeQueueProxyPath(`/api/tasks/${encodeURIComponent(taskId)}/summary${queryString({ toolLimit: options.toolLimit })}`);
const summaryResponse = unwrapCodexResponse(await fetcher(summaryPath));
@@ -695,6 +1007,8 @@ export async function codexJudgeQueryAsync(taskId: string, optionArgs: string[],
return codexTaskJudgeAsync(taskId, parseJudgeOptions(optionArgs), fetcher);
}
export { codexTasksQueryAsync };
function requireQueueId(args: string[], command: string): string {
const index = args.indexOf("--queue");
const raw = index === -1 ? args[0] : args[index + 1];
@@ -947,6 +1261,9 @@ export async function runCodeQueueCommand(_config: UniDeskConfig, args: string[]
const taskId = requireTaskId(taskIdArg, `codex ${action}`);
return codexTaskQuery(taskId, args.slice(2));
}
if (action === "tasks" || action === "overview") {
return codexTasksQuery(args.slice(1));
}
if (action === "output") {
const taskId = requireTaskId(taskIdArg, "codex output");
return codexOutputQuery(taskId, args.slice(2));
@@ -973,5 +1290,5 @@ export async function runCodeQueueCommand(_config: UniDeskConfig, args: string[]
const taskId = requireTaskId(taskIdArg, `codex ${action}`);
return codexInterruptTask(taskId);
}
throw new Error("codex command must be one of: submit, enqueue, task, summary, show, output, judge, queues, queue list, queue create, queue merge, move, interrupt, cancel");
throw new Error("codex command must be one of: submit, enqueue, task, summary, show, tasks, overview, output, judge, queues, queue list, queue create, queue merge, move, interrupt, cancel");
}
+3 -1
View File
@@ -44,6 +44,7 @@ export function rootHelp(): unknown {
{ command: "codex deploy <commitId> [--provider-id D601] [--timeout-ms N]", description: "Compatibility wrapper for deploy apply --service code-queue with a temporary repo+commit manifest." },
{ command: "codex submit [prompt] [--prompt-file path|--prompt-stdin] [--queue queueId] [--provider-id id] [--cwd path] [--model model] [--execution-mode mode] [--max-attempts N] [--reference-task-id id] [--dry-run]", description: "Submit a Code Queue task through backend-core -> code-queue proxy; --dry-run shows the structured request without enqueueing." },
{ command: "codex task <taskId> [--trace --tail|--from-start|--after-seq N|--before-seq N --limit N] [--full]", description: "Fetch a compact Code Queue task summary; trace rows are opt-in and paged with next/previous commands to avoid output explosion." },
{ command: "codex tasks [--queue id] [--limit N] [--unread-only]", description: "Show the current running, unread terminal, and recent completed Code Queue tasks in one JSON view." },
{ command: "codex output <taskId> [--tail|--from-start|--after-seq N|--before-seq N --limit N] [--full-text]", description: "Fetch paged raw Code Queue output records by seq when a trace row has omitted command/output text." },
{ 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 interrupt|cancel <taskId>", description: "Request interrupt for a running Code Queue task, or cancel a queued/retry_wait task, through the same private proxy." },
@@ -179,11 +180,12 @@ function scheduleHelp(): unknown {
function codexHelp(): unknown {
return {
command: "codex deploy|submit|task|output|judge|interrupt|cancel|queues|queue|move",
command: "codex deploy|submit|task|tasks|output|judge|interrupt|cancel|queues|queue|move",
output: "json",
usage: [
"bun scripts/cli.ts codex submit [prompt] [--prompt-file path|--prompt-stdin] [--queue id] [--dry-run]",
"bun scripts/cli.ts codex task <taskId> [--trace --tail|--from-start|--after-seq N|--before-seq N --limit N] [--full]",
"bun scripts/cli.ts codex tasks [--queue id] [--limit N] [--unread-only]",
"bun scripts/cli.ts codex output <taskId> [--tail|--from-start|--after-seq N|--before-seq N --limit N] [--full-text]",
"bun scripts/cli.ts codex judge <taskId> --attempt N [--dry-run] [--include-prompt]",
"bun scripts/cli.ts codex interrupt|cancel <taskId>",
+15 -10
View File
@@ -4,7 +4,7 @@ import { type DebugDispatchCommand, isDebugDispatchCommand } from "./debug";
import { summarizeMicroserviceProxyResponse } from "./microservices";
import { parseNetworkPerfOptions, runNetworkPerf } from "./network-perf";
import { isSshSkillDiscoveryArgs, parseSshArgs } from "./ssh";
import { codexJudgeQueryAsync, codexOutputQueryAsync, codexTaskQueryAsync } from "./code-queue";
import { codexJudgeQueryAsync, codexOutputQueryAsync, codexTaskQueryAsync, codexTasksQueryAsync } from "./code-queue";
import { runDecisionCenterCommandAsync } from "./decision-center";
export interface RemoteCliOptions {
@@ -519,11 +519,14 @@ async function remoteMicroservice(session: FrontendSession, args: string[]): Pro
async function remoteCodeQueue(session: FrontendSession, args: string[]): Promise<unknown> {
const action = args[1] ?? "task";
if (action !== "task" && action !== "summary" && action !== "show" && action !== "output" && action !== "judge") {
throw new Error("remote codex command must be: codex task <taskId>, codex output <taskId>, or codex judge <taskId> --attempt N");
if (action !== "task" && action !== "summary" && action !== "show" && action !== "tasks" && action !== "overview" && action !== "output" && action !== "judge") {
throw new Error("remote codex command must be: codex task <taskId>, codex tasks, codex output <taskId>, or codex judge <taskId> --attempt N");
}
const taskId = args[2];
if (taskId === undefined || taskId.length === 0) throw new Error(`codex ${action} requires task id`);
if ((action === "task" || action === "summary" || action === "show" || action === "output" || action === "judge") && (taskId === undefined || taskId.length === 0)) {
throw new Error(`codex ${action} requires task id`);
}
const requiredTaskId = taskId ?? "";
const fetcher = (path: string, init?: { method?: string; body?: unknown }): Promise<FetchJsonResult> => {
const requestInit = init === undefined
? undefined
@@ -535,11 +538,13 @@ async function remoteCodeQueue(session: FrontendSession, args: string[]): Promis
};
return {
transport: "frontend",
result: action === "output"
? await codexOutputQueryAsync(taskId, args.slice(3), fetcher)
: action === "judge"
? await codexJudgeQueryAsync(taskId, args.slice(3), fetcher)
: await codexTaskQueryAsync(taskId, args.slice(3), fetcher),
result: action === "tasks" || action === "overview"
? await codexTasksQueryAsync(args.slice(1), fetcher)
: action === "output"
? await codexOutputQueryAsync(requiredTaskId, args.slice(3), fetcher)
: action === "judge"
? await codexJudgeQueryAsync(requiredTaskId, args.slice(3), fetcher)
: await codexTaskQueryAsync(requiredTaskId, args.slice(3), fetcher),
};
}
@@ -605,7 +610,7 @@ async function runRemoteCliOverFrontend(options: RemoteCliOptions, config: UniDe
emitRemoteJson(name, {
transport: "frontend",
baseUrl: session.baseUrl,
commands: ["debug health", "debug dispatch", "debug task", "ssh <providerId> <command>", "ssh <providerId> skills", "microservice list", "microservice status <id>", "microservice health <id>", "microservice diagnostics <id>", "microservice tunnel-self-test <id>", "microservice proxy <id> <path>", "decision upload <markdown-file>", "decision list", "decision show <id>", "codex task <taskId>", "codex judge <taskId> --attempt N", "network perf"],
commands: ["debug health", "debug dispatch", "debug task", "ssh <providerId> <command>", "ssh <providerId> skills", "microservice list", "microservice status <id>", "microservice health <id>", "microservice diagnostics <id>", "microservice tunnel-self-test <id>", "microservice proxy <id> <path>", "decision upload <markdown-file>", "decision list", "decision show <id>", "codex task <taskId>", "codex tasks", "codex judge <taskId> --attempt N", "network perf"],
});
return 0;
}