diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 80fc9140..3ebf9bb2 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -33,6 +33,7 @@ CLI 可以从 `master` 快速演进,但必须兼容 `deploy.json` 固定的 CI - `codex deploy ` 是旧 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`,由它写入主 PostgreSQL;D601 scheduler 只轮询并执行已入库任务。 - `codex task ` 通过 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 --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 --tail|--from-start|--after-seq N|--before-seq N --limit N [--full-text]` 按原始 output seq 分页读取底层记录;当 trace 行提示 `commandOmittedLines`、`bodyOmittedLines` 或 `rawSeqs` 时,用该命令按 seq 补取完整信息,默认仍有单条文本预览上限,显式 `--full-text` 才返回该页全文。 - `codex judge --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://:/` 获取 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 `、`codex output `、`codex judge --attempt N` 和 `ssh `。其中 `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 ` 或 `--main-server-transport ssh`,这时 CLI 会通过 SSH 登录主 server 的 `--main-server-root` 目录执行同一个 `bun scripts/cli.ts `。 +默认 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 `、`codex tasks`、`codex output `、`codex judge --attempt N` 和 `ssh `。其中 `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 ` 或 `--main-server-transport ssh`,这时 CLI 会通过 SSH 登录主 server 的 `--main-server-root` 目录执行同一个 `bun scripts/cli.ts `。 计算节点可以用该入口测试自身的远程升级闭环,而不需要在计算节点公开 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.upgrade --mode schedule --wait-ms 15000` 触发真实 `provider.upgrade`;随后再次运行 `debug health` 确认节点重新上线;最后运行 `bun scripts/cli.ts --main-server-ip 74.48.78.17 debug dispatch host.ssh --wait-ms 15000` 和 `bun scripts/cli.ts --main-server-ip 74.48.78.17 ssh hostname` 验证 SSH 透传能力。provider-gateway 新部署或升级后没有完成这组 remote CLI 自测,不能视为交付完成。 diff --git a/scripts/src/code-queue.ts b/scripts/src/code-queue.ts index ab1c81d2..359337a6 100644 --- a/scripts/src/code-queue.ts +++ b/scripts/src/code-queue.ts @@ -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 | 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; @@ -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): 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 { + return asString(task.id); +} + +function taskUnreadTerminal(task: Record): 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, summary: Record | 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[], summaries: Map>, 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[]): Record[] { + 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[]): Record[] { + 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>; degraded: CodexTasksDegraded | null } { + const summaries = new Map>(); + 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>; degraded: CodexTasksDegraded | null }> { + const summaries = new Map>(); + 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 | null; + pagination: Record; + tasks: Record[]; +}; + +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>(); + 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 => 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>, + degraded: CodexTasksDegraded | null, +): Record { + 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[], 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 { + const options = parseTasksOptions(taskArgs); + const byId = new Map>(); + 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 => 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 { 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"); } diff --git a/scripts/src/help.ts b/scripts/src/help.ts index 778c895a..fc90a531 100644 --- a/scripts/src/help.ts +++ b/scripts/src/help.ts @@ -44,6 +44,7 @@ export function rootHelp(): unknown { { command: "codex deploy [--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 [--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 [--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 --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 ", 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 [--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 [--tail|--from-start|--after-seq N|--before-seq N --limit N] [--full-text]", "bun scripts/cli.ts codex judge --attempt N [--dry-run] [--include-prompt]", "bun scripts/cli.ts codex interrupt|cancel ", diff --git a/scripts/src/remote.ts b/scripts/src/remote.ts index bfccb177..f319808d 100644 --- a/scripts/src/remote.ts +++ b/scripts/src/remote.ts @@ -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 { 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 , codex output , or codex judge --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 , codex tasks, codex output , or codex judge --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 => { 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 ", "ssh skills", "microservice list", "microservice status ", "microservice health ", "microservice diagnostics ", "microservice tunnel-self-test ", "microservice proxy ", "decision upload ", "decision list", "decision show ", "codex task ", "codex judge --attempt N", "network perf"], + commands: ["debug health", "debug dispatch", "debug task", "ssh ", "ssh skills", "microservice list", "microservice status ", "microservice health ", "microservice diagnostics ", "microservice tunnel-self-test ", "microservice proxy ", "decision upload ", "decision list", "decision show ", "codex task ", "codex tasks", "codex judge --attempt N", "network perf"], }); return 0; }