feat: add codex tasks overview
This commit is contained in:
@@ -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`,由它写入主 PostgreSQL;D601 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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user