feat: tighten code queue supervision cli
This commit is contained in:
+401
-29
@@ -60,7 +60,10 @@ interface CompactTaskMutationResponseOptions {
|
||||
interface CodexTasksOptions {
|
||||
queueId: string | undefined;
|
||||
limit: number;
|
||||
beforeId: string | undefined;
|
||||
unreadOnly: boolean;
|
||||
statusFilter: string[] | null;
|
||||
view: "supervisor" | "full";
|
||||
}
|
||||
|
||||
interface CodexTasksEntry {
|
||||
@@ -73,10 +76,15 @@ interface CodexTasksEntry {
|
||||
readAt: string | null;
|
||||
unread: boolean;
|
||||
unreadTerminal: boolean;
|
||||
promptPreview: Record<string, unknown>;
|
||||
queuedReason: Record<string, unknown> | null;
|
||||
lastAssistantMessage: Record<string, unknown> | null;
|
||||
commands: {
|
||||
show: string;
|
||||
trace: string;
|
||||
output: string;
|
||||
read: string;
|
||||
full: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -84,15 +92,26 @@ interface CodexTasksSection {
|
||||
count: number;
|
||||
returned: number;
|
||||
truncated: boolean;
|
||||
hasMore: boolean;
|
||||
commands: {
|
||||
next: string | null;
|
||||
full: string;
|
||||
};
|
||||
items: CodexTasksEntry[];
|
||||
}
|
||||
|
||||
interface CodexTasksDegraded {
|
||||
summaryFetchFailedTaskIds: string[];
|
||||
summaryFetchErrorCount: number;
|
||||
summaryFetchOmittedTaskCount: number;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
interface CodexQueuesOptions {
|
||||
full: boolean;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
type CodexRequestInit = { method?: string; body?: unknown };
|
||||
type CodexResponseFetcher = (path: string, init?: CodexRequestInit) => unknown;
|
||||
type AsyncCodexResponseFetcher = (path: string, init?: CodexRequestInit) => Promise<unknown>;
|
||||
@@ -300,6 +319,23 @@ function hasFlag(args: string[], name: string): boolean {
|
||||
return args.includes(name);
|
||||
}
|
||||
|
||||
function assertKnownOptions(args: string[], spec: { flags?: string[]; valueOptions?: string[] }, command: string): void {
|
||||
const flags = new Set(spec.flags ?? []);
|
||||
const valueOptions = new Set(spec.valueOptions ?? []);
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index] ?? "";
|
||||
if (!arg.startsWith("--")) continue;
|
||||
if (flags.has(arg)) continue;
|
||||
if (valueOptions.has(arg)) {
|
||||
const value = args[index + 1];
|
||||
if (value === undefined || value.startsWith("--")) throw new Error(`${arg} requires a value`);
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
throw new Error(`unsupported ${command} option: ${arg}`);
|
||||
}
|
||||
}
|
||||
|
||||
function textView(text: string, full: boolean, maxChars: number): Record<string, unknown> {
|
||||
const truncated = !full && text.length > maxChars;
|
||||
return {
|
||||
@@ -324,6 +360,21 @@ function compactLastAssistant(value: unknown, full: boolean): Record<string, unk
|
||||
};
|
||||
}
|
||||
|
||||
function compactQueuedReason(value: unknown): Record<string, unknown> | null {
|
||||
const record = asRecord(value);
|
||||
if (record === null) return null;
|
||||
return {
|
||||
code: record.code ?? null,
|
||||
label: record.label ?? null,
|
||||
message: textPreview(asString(record.message), 280),
|
||||
blockerTaskId: record.blockerTaskId ?? null,
|
||||
blockerQueueId: record.blockerQueueId ?? null,
|
||||
waitPosition: record.waitPosition ?? null,
|
||||
activeRunSlotCount: record.activeRunSlotCount ?? null,
|
||||
maxActiveQueues: record.maxActiveQueues ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function compactSchedulerHeartbeat(value: unknown): Record<string, unknown> | null {
|
||||
const record = asRecord(value);
|
||||
if (record === null) return null;
|
||||
@@ -667,6 +718,10 @@ function compactTracePage(body: Record<string, unknown>, taskId: string, limit:
|
||||
}
|
||||
|
||||
function parseTaskOptions(args: string[]): CodexTaskOptions {
|
||||
assertKnownOptions(args, {
|
||||
flags: ["--trace", "--tail", "--from-start", "--first", "--full", "--raw-summary"],
|
||||
valueOptions: ["--before-seq", "--beforeSeq", "--after-seq", "--afterSeq", "--trace-limit", "--limit", "--tool-limit"],
|
||||
}, "codex task");
|
||||
const beforeSeq = nullablePositiveNumberOption(args, ["--before-seq", "--beforeSeq"]);
|
||||
const afterSeq = nonNegativeNumberOption(args, ["--after-seq", "--afterSeq"], 0);
|
||||
const fromStart = hasFlag(args, "--from-start") || hasFlag(args, "--first");
|
||||
@@ -685,6 +740,10 @@ function parseTaskOptions(args: string[]): CodexTaskOptions {
|
||||
}
|
||||
|
||||
function parseOutputOptions(args: string[]): CodexOutputOptions {
|
||||
assertKnownOptions(args, {
|
||||
flags: ["--tail", "--from-start", "--first", "--full-text", "--raw"],
|
||||
valueOptions: ["--before-seq", "--beforeSeq", "--after-seq", "--afterSeq", "--limit", "--max-text-chars"],
|
||||
}, "codex output");
|
||||
const beforeSeq = nullablePositiveNumberOption(args, ["--before-seq", "--beforeSeq"]);
|
||||
const afterSeq = nonNegativeNumberOption(args, ["--after-seq", "--afterSeq"], 0);
|
||||
const fromStart = hasFlag(args, "--from-start") || hasFlag(args, "--first");
|
||||
@@ -700,14 +759,47 @@ function parseOutputOptions(args: string[]): CodexOutputOptions {
|
||||
}
|
||||
|
||||
function parseTasksOptions(args: string[]): CodexTasksOptions {
|
||||
assertKnownOptions(args, {
|
||||
flags: ["--unread-only", "--unread", "--full", "--all"],
|
||||
valueOptions: ["--queue", "--queue-id", "--limit", "--status", "--view", "--before-id", "--beforeId"],
|
||||
}, "codex tasks");
|
||||
const viewValue = optionValue(args, ["--view"]) ?? "supervisor";
|
||||
if (viewValue !== "supervisor" && viewValue !== "full") throw new Error(`--view must be supervisor or full; got ${viewValue}`);
|
||||
const statusRaw = optionValue(args, ["--status"]);
|
||||
const statusFilter = statusRaw === undefined
|
||||
? null
|
||||
: statusRaw.split(/[,\s]+/u).map((item) => item.trim()).filter(Boolean);
|
||||
if (statusFilter !== null) {
|
||||
const supported = new Set(["queued", "running", "judging", "retry_wait", "succeeded", "failed", "canceled"]);
|
||||
const unsupported = statusFilter.filter((status) => !supported.has(status));
|
||||
if (unsupported.length > 0) throw new Error(`unsupported --status value: ${unsupported.join(", ")}; supported: ${Array.from(supported).join(", ")}`);
|
||||
}
|
||||
return {
|
||||
queueId: optionValue(args, ["--queue", "--queue-id"]),
|
||||
limit: positiveIntegerOption(args, ["--limit"], defaultTasksLimit, maxTasksLimit),
|
||||
unreadOnly: hasFlag(args, "--unread-only"),
|
||||
beforeId: optionValue(args, ["--before-id", "--beforeId"]),
|
||||
unreadOnly: hasFlag(args, "--unread-only") || hasFlag(args, "--unread"),
|
||||
statusFilter,
|
||||
view: hasFlag(args, "--full") || hasFlag(args, "--all") ? "full" : viewValue,
|
||||
};
|
||||
}
|
||||
|
||||
function parseQueuesOptions(args: string[]): CodexQueuesOptions {
|
||||
assertKnownOptions(args, {
|
||||
flags: ["--full", "--all"],
|
||||
valueOptions: ["--limit"],
|
||||
}, "codex queues");
|
||||
return {
|
||||
full: hasFlag(args, "--full") || hasFlag(args, "--all"),
|
||||
limit: positiveIntegerOption(args, ["--limit"], defaultTasksLimit, maxTasksLimit),
|
||||
};
|
||||
}
|
||||
|
||||
function parseJudgeOptions(args: string[]): CodexJudgeOptions {
|
||||
assertKnownOptions(args, {
|
||||
flags: ["--dry-run", "--no-call", "--include-prompt"],
|
||||
valueOptions: ["--attempt", "--attempt-id", "--attemptIndex"],
|
||||
}, "codex judge");
|
||||
const rawAttempt = optionValue(args, ["--attempt", "--attempt-id", "--attemptIndex"]) ?? positionalArgs(args)[0];
|
||||
let attempt: number | null = null;
|
||||
if (rawAttempt !== undefined) {
|
||||
@@ -823,7 +915,7 @@ function isTerminalTaskStatus(status: unknown): boolean {
|
||||
}
|
||||
|
||||
function isActiveTaskStatus(status: unknown): boolean {
|
||||
return status === "running" || status === "judging" || status === "retry_wait" || status === "queued";
|
||||
return status === "running" || status === "judging";
|
||||
}
|
||||
|
||||
function taskStatusRank(status: unknown): number {
|
||||
@@ -855,16 +947,24 @@ function taskUnreadTerminal(task: Record<string, unknown>): boolean {
|
||||
return isTerminalTaskStatus(status) && (readAt === null || readAt === undefined || readAt === "");
|
||||
}
|
||||
|
||||
function taskQueuedRunnable(task: Record<string, unknown>): boolean {
|
||||
const status = asString(task.status);
|
||||
if (status === "queued" || status === "retry_wait") return true;
|
||||
return asRecord(task.queuedReason)?.code === "ready";
|
||||
}
|
||||
|
||||
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 promptPreview = textPreview(asString(task.displayPrompt ?? task.basePrompt ?? task.prompt), 360);
|
||||
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}`;
|
||||
const outputCommand = `bun scripts/cli.ts codex output ${taskId} --tail --limit ${defaultOutputLimit}`;
|
||||
return {
|
||||
taskId,
|
||||
queueId: asString(task.queueId) || null,
|
||||
@@ -875,31 +975,51 @@ function taskWatchEntry(task: Record<string, unknown>, summary: Record<string, u
|
||||
readAt: asString(task.readAt) || null,
|
||||
unread: taskUnreadTerminal(task),
|
||||
unreadTerminal: taskUnreadTerminal(task),
|
||||
promptPreview,
|
||||
queuedReason: compactQueuedReason(task.queuedReason),
|
||||
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,
|
||||
output: outputCommand,
|
||||
read: `bun scripts/cli.ts codex read ${taskId}`,
|
||||
full: `bun scripts/cli.ts codex task ${taskId} --full`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function buildTaskWatchSection(tasks: Record<string, unknown>[], summaries: Map<string, Record<string, unknown>>, limit: number): CodexTasksSection {
|
||||
function buildTaskWatchSection(
|
||||
tasks: Record<string, unknown>[],
|
||||
summaries: Map<string, Record<string, unknown>>,
|
||||
limit: number,
|
||||
nextCommand: string | null,
|
||||
fullCommand: string,
|
||||
): CodexTasksSection {
|
||||
const visibleTasks = tasks.slice(0, limit);
|
||||
const items = visibleTasks.map((task) => taskWatchEntry(task, summaries.get(taskOverviewCandidateKey(task)) ?? null));
|
||||
const truncated = tasks.length > limit;
|
||||
return {
|
||||
count: tasks.length,
|
||||
returned: items.length,
|
||||
truncated: tasks.length > limit,
|
||||
truncated,
|
||||
hasMore: truncated,
|
||||
commands: {
|
||||
next: truncated ? nextCommand : null,
|
||||
full: fullCommand,
|
||||
},
|
||||
items,
|
||||
};
|
||||
}
|
||||
|
||||
function collectTaskWatchDegraded(summaryErrors: Array<{ taskId: string; message: string }>): CodexTasksDegraded | null {
|
||||
if (summaryErrors.length === 0) return null;
|
||||
function collectTaskWatchDegraded(summaryErrors: Array<{ taskId: string; message: string }>, omittedTaskCount = 0): CodexTasksDegraded | null {
|
||||
if (summaryErrors.length === 0 && omittedTaskCount === 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",
|
||||
summaryFetchOmittedTaskCount: omittedTaskCount,
|
||||
reason: summaryErrors.length > 0
|
||||
? "task summary fetch failed for one or more entries; unread state still comes from task-level overview data"
|
||||
: "task summary fetch was intentionally omitted for bounded full view; use commands.show for per-task detail",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -925,10 +1045,46 @@ function sortCompletedWatchTasks(tasks: Record<string, unknown>[]): Record<strin
|
||||
});
|
||||
}
|
||||
|
||||
function sortQueuedWatchTasks(tasks: Record<string, unknown>[]): Record<string, unknown>[] {
|
||||
return [...tasks]
|
||||
.filter((task) => taskQueuedRunnable(task))
|
||||
.sort((left, right) => {
|
||||
const rankDelta = taskStatusRank(asString(left.status)) - taskStatusRank(asString(right.status));
|
||||
if (rankDelta !== 0) return rankDelta;
|
||||
const timeDelta = taskTimelineMs(left) - taskTimelineMs(right);
|
||||
if (timeDelta !== 0) return timeDelta;
|
||||
return asString(left.id).localeCompare(asString(right.id));
|
||||
});
|
||||
}
|
||||
|
||||
function taskMatchesStatusFilter(task: Record<string, unknown>, statusFilter: string[] | null): boolean {
|
||||
return statusFilter === null || statusFilter.includes(asString(task.status));
|
||||
}
|
||||
|
||||
function filterTasksForOptions(tasks: Record<string, unknown>[], options: CodexTasksOptions): Record<string, unknown>[] {
|
||||
return tasks
|
||||
.filter((task) => options.queueId === undefined || asString(task.queueId) === options.queueId)
|
||||
.filter((task) => taskMatchesStatusFilter(task, options.statusFilter))
|
||||
.filter((task) => !options.unreadOnly || taskUnreadTerminal(task));
|
||||
}
|
||||
|
||||
function taskListCommand(options: CodexTasksOptions, extra: string[] = []): string {
|
||||
const args = ["codex", "tasks"];
|
||||
if (options.view !== "supervisor") args.push("--view", options.view);
|
||||
if (options.queueId !== undefined) args.push("--queue", options.queueId);
|
||||
if (options.unreadOnly) args.push("--unread");
|
||||
if (options.statusFilter !== null) args.push("--status", options.statusFilter.join(","));
|
||||
if (options.limit !== defaultTasksLimit) args.push("--limit", String(options.limit));
|
||||
if (options.beforeId !== undefined) args.push("--before-id", options.beforeId);
|
||||
args.push(...extra);
|
||||
return `bun scripts/cli.ts ${args.join(" ")}`;
|
||||
}
|
||||
|
||||
function fetchTaskSummaries(taskIds: string[], fetcher: CodexResponseFetcher): { summaries: Map<string, Record<string, unknown>>; degraded: CodexTasksDegraded | null } {
|
||||
const boundedIds = taskIds.slice(0, maxTasksLimit);
|
||||
const summaries = new Map<string, Record<string, unknown>>();
|
||||
const errors: Array<{ taskId: string; message: string }> = [];
|
||||
for (const taskId of taskIds) {
|
||||
for (const taskId of boundedIds) {
|
||||
try {
|
||||
const response = unwrapCodexResponse(fetcher(codeQueueProxyPath(`/api/tasks/${encodeURIComponent(taskId)}/summary${queryString({ toolLimit: 1 })}`)));
|
||||
const summary = asRecord(response.body.summary) ?? {};
|
||||
@@ -937,13 +1093,14 @@ function fetchTaskSummaries(taskIds: string[], fetcher: CodexResponseFetcher): {
|
||||
errors.push({ taskId, message: error instanceof Error ? error.message : String(error) });
|
||||
}
|
||||
}
|
||||
return { summaries, degraded: collectTaskWatchDegraded(errors) };
|
||||
return { summaries, degraded: collectTaskWatchDegraded(errors, Math.max(0, taskIds.length - boundedIds.length)) };
|
||||
}
|
||||
|
||||
async function fetchTaskSummariesAsync(taskIds: string[], fetcher: AsyncCodexResponseFetcher): Promise<{ summaries: Map<string, Record<string, unknown>>; degraded: CodexTasksDegraded | null }> {
|
||||
const boundedIds = taskIds.slice(0, maxTasksLimit);
|
||||
const summaries = new Map<string, Record<string, unknown>>();
|
||||
const errors: Array<{ taskId: string; message: string }> = [];
|
||||
await Promise.all(taskIds.map(async (taskId) => {
|
||||
await Promise.all(boundedIds.map(async (taskId) => {
|
||||
try {
|
||||
const response = unwrapCodexResponse(await fetcher(codeQueueProxyPath(`/api/tasks/${encodeURIComponent(taskId)}/summary${queryString({ toolLimit: 1 })}`)));
|
||||
const summary = asRecord(response.body.summary) ?? {};
|
||||
@@ -952,7 +1109,7 @@ async function fetchTaskSummariesAsync(taskIds: string[], fetcher: AsyncCodexRes
|
||||
errors.push({ taskId, message: error instanceof Error ? error.message : String(error) });
|
||||
}
|
||||
}));
|
||||
return { summaries, degraded: collectTaskWatchDegraded(errors) };
|
||||
return { summaries, degraded: collectTaskWatchDegraded(errors, Math.max(0, taskIds.length - boundedIds.length)) };
|
||||
}
|
||||
|
||||
type CodexTasksTaskPage = {
|
||||
@@ -966,6 +1123,7 @@ function tasksListQueryString(options: CodexTasksOptions): string {
|
||||
limit: 200,
|
||||
priorityLimit: 200,
|
||||
queueId: options.queueId,
|
||||
beforeId: options.beforeId,
|
||||
includeActive: 1,
|
||||
selected: 0,
|
||||
compact: 1,
|
||||
@@ -1000,21 +1158,32 @@ function codexTasksOverviewResult(
|
||||
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);
|
||||
if (options.view === "full") return codexTasksFullResult(taskPage, upstream, options, summaries, degraded);
|
||||
const allTasks = filterTasksForOptions(taskPage.tasks, options);
|
||||
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 queuedTasks = options.unreadOnly ? [] : sortQueuedWatchTasks(allTasks);
|
||||
const nextBeforeId = asString(taskPage.pagination.nextBeforeId) || null;
|
||||
const sourceHasMore = asBoolean(taskPage.pagination.hasMore);
|
||||
const nextCommand = sourceHasMore && nextBeforeId !== null ? taskListCommand({ ...options, beforeId: nextBeforeId }) : null;
|
||||
const fullCommand = taskListCommand({ ...options, view: "full" });
|
||||
const runningSection = buildTaskWatchSection(runningTasks, summaries, options.limit, nextCommand, fullCommand);
|
||||
const unreadSection = buildTaskWatchSection(unreadCompletedTasks, summaries, options.limit, nextCommand, fullCommand);
|
||||
const recentSection = buildTaskWatchSection(recentCompletedTasks, summaries, options.limit, nextCommand, fullCommand);
|
||||
const queuedSection = buildTaskWatchSection(queuedTasks, summaries, options.limit, nextCommand, fullCommand);
|
||||
const pagination = taskPage.pagination;
|
||||
const diagnostics = compactExecutionDiagnostics(asRecord(taskPage.queue)?.executionDiagnostics);
|
||||
return {
|
||||
upstream,
|
||||
overview: {
|
||||
supervisor: {
|
||||
filters: {
|
||||
view: options.view,
|
||||
queueId: options.queueId ?? null,
|
||||
limit: options.limit,
|
||||
unreadOnly: options.unreadOnly,
|
||||
status: options.statusFilter,
|
||||
beforeId: options.beforeId ?? null,
|
||||
},
|
||||
source: {
|
||||
endpoint: "/api/tasks/overview",
|
||||
@@ -1026,29 +1195,92 @@ function codexTasksOverviewResult(
|
||||
nextBeforeId: asString(pagination.nextBeforeId) || null,
|
||||
includeActive: asBoolean(pagination.includeActive),
|
||||
},
|
||||
queue: taskPage.queue,
|
||||
bounded: true,
|
||||
disclosure: {
|
||||
defaultView: "supervisor",
|
||||
fullCommand,
|
||||
next: nextCommand,
|
||||
rawOverview: `bun scripts/cli.ts microservice proxy code-queue /api/tasks/overview${tasksListQueryString(options)} --raw`,
|
||||
},
|
||||
counts: {
|
||||
scanned: allTasks.length,
|
||||
running: runningSection.count,
|
||||
completedUnread: unreadSection.count,
|
||||
recentCompleted: recentSection.count,
|
||||
queued: queuedSection.count,
|
||||
},
|
||||
executionDiagnostics: diagnostics,
|
||||
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}`}`,
|
||||
refresh: taskListCommand(options),
|
||||
unread: taskListCommand({ ...options, unreadOnly: true }),
|
||||
byStatus: `bun scripts/cli.ts codex tasks --status <status>${options.queueId === undefined ? "" : ` --queue ${options.queueId}`}${options.limit === defaultTasksLimit ? "" : ` --limit ${options.limit}`}`,
|
||||
full: fullCommand,
|
||||
next: nextCommand,
|
||||
},
|
||||
running: runningSection,
|
||||
completedUnread: unreadSection,
|
||||
recentCompleted: recentSection,
|
||||
queued: queuedSection,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function codexTasksFullResult(
|
||||
taskPage: CodexTasksTaskPage,
|
||||
upstream: { ok: unknown; status: unknown },
|
||||
options: CodexTasksOptions,
|
||||
summaries: Map<string, Record<string, unknown>>,
|
||||
degraded: CodexTasksDegraded | null,
|
||||
): Record<string, unknown> {
|
||||
const filtered = filterTasksForOptions(taskPage.tasks, options);
|
||||
const visible = filtered.slice(0, options.limit);
|
||||
const pagination = taskPage.pagination;
|
||||
const nextBeforeId = asString(pagination.nextBeforeId) || null;
|
||||
const sourceHasMore = asBoolean(pagination.hasMore);
|
||||
const nextCommand = sourceHasMore && nextBeforeId !== null ? taskListCommand({ ...options, beforeId: nextBeforeId }) : null;
|
||||
return {
|
||||
upstream,
|
||||
tasks: {
|
||||
filters: {
|
||||
view: "full",
|
||||
queueId: options.queueId ?? null,
|
||||
limit: options.limit,
|
||||
unreadOnly: options.unreadOnly,
|
||||
status: options.statusFilter,
|
||||
beforeId: options.beforeId ?? null,
|
||||
},
|
||||
source: {
|
||||
endpoint: "/api/tasks/overview",
|
||||
returned: asNumber(pagination.returned, 0) || null,
|
||||
total: asNumber(pagination.total, 0) || null,
|
||||
hasMore: asBoolean(pagination.hasMore),
|
||||
nextBeforeId,
|
||||
},
|
||||
count: filtered.length,
|
||||
returned: visible.length,
|
||||
hasMore: filtered.length > visible.length || sourceHasMore,
|
||||
degraded,
|
||||
commands: {
|
||||
supervisor: taskListCommand({ ...options, view: "supervisor" }),
|
||||
next: nextCommand,
|
||||
rawOverview: `bun scripts/cli.ts microservice proxy code-queue /api/tasks/overview${tasksListQueryString(options)} --raw`,
|
||||
},
|
||||
items: visible.map((task) => taskWatchEntry(task, summaries.get(taskOverviewCandidateKey(task)) ?? null)),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function visibleTaskIdsForOverview(tasks: Record<string, unknown>[], options: CodexTasksOptions): string[] {
|
||||
const filtered = filterTasksForOptions(tasks, options);
|
||||
if (options.view === "full") {
|
||||
return filtered.slice(0, options.limit).map((task) => taskOverviewCandidateKey(task)).filter((taskId) => taskId.length > 0);
|
||||
}
|
||||
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),
|
||||
...sortRunningWatchTasks(filtered).slice(0, options.limit),
|
||||
...sortCompletedWatchTasks(filtered).filter((task) => taskUnreadTerminal(task)).slice(0, options.limit),
|
||||
...sortCompletedWatchTasks(filtered).slice(0, options.limit),
|
||||
...sortQueuedWatchTasks(filtered).slice(0, options.limit),
|
||||
].map((task) => taskOverviewCandidateKey(task))))
|
||||
.filter((taskId) => taskId.length > 0);
|
||||
}
|
||||
@@ -1211,15 +1443,77 @@ function requireMergeTargetQueueId(args: string[], command: string): string {
|
||||
return raw.trim();
|
||||
}
|
||||
|
||||
function codeQueues(): unknown {
|
||||
const response = unwrapCodexResponse(coreInternalFetch(codeQueueProxyPath("/api/queues")));
|
||||
function compactQueueRow(value: unknown): Record<string, unknown> {
|
||||
const record = asRecord(value) ?? {};
|
||||
return {
|
||||
upstream: response.upstream,
|
||||
queues: response.body.queues ?? [],
|
||||
queue: compactQueueMutationSummary(response.body.queue),
|
||||
id: record.id ?? null,
|
||||
name: record.name ?? null,
|
||||
total: record.total ?? null,
|
||||
counts: record.counts ?? {},
|
||||
unreadTerminal: record.unreadTerminal ?? 0,
|
||||
activeTaskId: record.activeTaskId ?? null,
|
||||
runnableTaskId: record.runnableTaskId ?? null,
|
||||
processing: record.processing ?? null,
|
||||
updatedAt: record.updatedAt ?? null,
|
||||
commands: {
|
||||
tasks: record.id === undefined || record.id === null ? null : `bun scripts/cli.ts codex tasks --queue ${String(record.id)} --limit ${defaultTasksLimit}`,
|
||||
unread: record.id === undefined || record.id === null ? null : `bun scripts/cli.ts codex tasks --queue ${String(record.id)} --unread --limit ${defaultTasksLimit}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function compactQueuesResponse(body: Record<string, unknown>, options: CodexQueuesOptions, upstream: { ok: unknown; status: unknown }): Record<string, unknown> {
|
||||
const queue = asRecord(body.queue) ?? asRecord(body.summary) ?? {};
|
||||
const queues = asArray(body.queues).map(compactQueueRow);
|
||||
const activeIds = stringList(queue.activeQueueIds);
|
||||
const nonemptyQueues = queues.filter((row) => asNumber(row.total, 0) > 0);
|
||||
const unreadQueues = queues.filter((row) => asNumber(row.unreadTerminal, 0) > 0);
|
||||
const runnableQueues = queues.filter((row) => row.runnableTaskId !== null && row.runnableTaskId !== undefined);
|
||||
const activeQueues = queues.filter((row) => typeof row.id === "string" && activeIds.includes(row.id));
|
||||
const selected = options.full ? queues : Array.from(new Map([...activeQueues, ...unreadQueues, ...runnableQueues, ...nonemptyQueues].map((row) => [String(row.id), row])).values());
|
||||
const visible = selected.slice(0, options.limit);
|
||||
const diagnostics = compactExecutionDiagnostics(queue.executionDiagnostics);
|
||||
return {
|
||||
upstream,
|
||||
queues: {
|
||||
view: options.full ? "full" : "summary",
|
||||
bounded: true,
|
||||
count: selected.length,
|
||||
returned: visible.length,
|
||||
hasMore: selected.length > visible.length,
|
||||
totals: {
|
||||
totalTasks: queue.total ?? null,
|
||||
queueCount: queue.queueCount ?? queues.length,
|
||||
activeQueueCount: activeIds.length,
|
||||
nonemptyQueueCount: nonemptyQueues.length,
|
||||
unreadQueueCount: unreadQueues.length,
|
||||
runnableQueueCount: runnableQueues.length,
|
||||
},
|
||||
activeQueueIds: queue.activeQueueIds ?? [],
|
||||
activeTaskIds: queue.activeTaskIds ?? [],
|
||||
queuedTaskIds: queue.queuedTaskIds ?? [],
|
||||
counts: queue.counts ?? {},
|
||||
unreadTerminal: queue.unreadTerminal ?? 0,
|
||||
executionDiagnostics: diagnostics,
|
||||
items: visible,
|
||||
commands: {
|
||||
refresh: `bun scripts/cli.ts codex queues${options.limit === defaultTasksLimit ? "" : ` --limit ${options.limit}`}`,
|
||||
full: `bun scripts/cli.ts codex queues --full${options.limit === defaultTasksLimit ? "" : ` --limit ${options.limit}`}`,
|
||||
tasks: `bun scripts/cli.ts codex tasks --view supervisor --limit ${Math.min(options.limit, defaultTasksLimit)}`,
|
||||
unread: `bun scripts/cli.ts codex tasks --unread --limit ${Math.min(options.limit, defaultTasksLimit)}`,
|
||||
raw: "bun scripts/cli.ts microservice proxy code-queue /api/queues --raw",
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function codeQueues(optionArgs: string[] = []): unknown {
|
||||
const options = parseQueuesOptions(optionArgs);
|
||||
const response = unwrapCodexResponse(coreInternalFetch(codeQueueProxyPath("/api/queues")));
|
||||
if (options.full) return { upstream: response.upstream, queues: response.body.queues ?? [], queue: compactQueueMutationSummary(response.body.queue), commands: { summary: "bun scripts/cli.ts codex queues" } };
|
||||
return compactQueuesResponse(response.body, options, response.upstream);
|
||||
}
|
||||
|
||||
function codexCreateQueue(queueId: string): unknown {
|
||||
return unwrapCodexResponse(coreInternalFetch(codeQueueProxyPath("/api/queues"), { method: "POST", body: { queueId } }));
|
||||
}
|
||||
@@ -1275,6 +1569,27 @@ function referenceTaskIdsFromOptions(args: string[]): string[] {
|
||||
}
|
||||
|
||||
function parseSubmitOptions(args: string[]): CodexSubmitOptions {
|
||||
assertKnownOptions(args, {
|
||||
flags: ["--prompt-stdin", "--stdin", "--dry-run"],
|
||||
valueOptions: [
|
||||
"--prompt-file",
|
||||
"--file",
|
||||
"--queue",
|
||||
"--queue-id",
|
||||
"--provider-id",
|
||||
"--provider",
|
||||
"--cwd",
|
||||
"--workdir",
|
||||
"--model",
|
||||
"--reasoning-effort",
|
||||
"--execution-mode",
|
||||
"--mode",
|
||||
"--max-attempts",
|
||||
"--reference-task-id",
|
||||
"--reference",
|
||||
"--ref",
|
||||
],
|
||||
}, "codex submit");
|
||||
const maxAttempts = args.some((arg) => arg === "--max-attempts")
|
||||
? positiveIntegerOption(args, ["--max-attempts"], 99, 99)
|
||||
: undefined;
|
||||
@@ -1339,6 +1654,19 @@ function compactTaskMutationResponse(task: unknown, options: CompactTaskMutation
|
||||
};
|
||||
}
|
||||
|
||||
function codexReadTask(taskId: string): unknown {
|
||||
const response = unwrapCodexResponse(coreInternalFetch(codeQueueProxyPath(`/api/tasks/${encodeURIComponent(taskId)}/read`), { method: "POST", body: {} }));
|
||||
return {
|
||||
upstream: response.upstream,
|
||||
task: compactTaskMutationResponse(response.body.task),
|
||||
queue: compactQueueMutationSummary(response.body.queue),
|
||||
commands: {
|
||||
show: `bun scripts/cli.ts codex task ${taskId}`,
|
||||
unread: `bun scripts/cli.ts codex tasks --unread --limit ${defaultTasksLimit}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function compactQueueMutationSummary(value: unknown): Record<string, unknown> | null {
|
||||
const record = asRecord(value);
|
||||
if (record === null) return null;
|
||||
@@ -1355,6 +1683,42 @@ function compactQueueMutationSummary(value: unknown): Record<string, unknown> |
|
||||
};
|
||||
}
|
||||
|
||||
function compactSkillsStatus(value: unknown): Record<string, unknown> | null {
|
||||
const record = asRecord(value);
|
||||
if (record === null) return null;
|
||||
return {
|
||||
path: record.path ?? null,
|
||||
mountPoint: record.mountPoint ?? null,
|
||||
exists: record.exists ?? false,
|
||||
available: record.available ?? false,
|
||||
readonly: record.readonly ?? false,
|
||||
skillCount: record.skillCount ?? 0,
|
||||
cliSpecAvailable: record.cliSpecAvailable ?? false,
|
||||
repairHint: record.repairHint ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function codeQueueDevReady(): unknown {
|
||||
const response = unwrapCodexResponse(coreInternalFetch(codeQueueProxyPath("/api/dev-ready")));
|
||||
const devReady = asRecord(response.body.devReady) ?? {};
|
||||
return {
|
||||
upstream: response.upstream,
|
||||
devReady: {
|
||||
ok: devReady.ok ?? false,
|
||||
missingTools: devReady.missingTools ?? [],
|
||||
workdir: devReady.workdir ?? null,
|
||||
docker: devReady.docker ?? null,
|
||||
codexConfig: devReady.codexConfig ?? null,
|
||||
ssh: devReady.ssh ?? null,
|
||||
skills: compactSkillsStatus(devReady.skills),
|
||||
},
|
||||
commands: {
|
||||
health: "bun scripts/cli.ts codex dev-ready",
|
||||
raw: "bun scripts/cli.ts microservice proxy code-queue /api/dev-ready --raw",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function codexSubmitTask(args: string[]): unknown {
|
||||
const options = parseSubmitOptions(args);
|
||||
const payload = submitPayload(options);
|
||||
@@ -1399,6 +1763,10 @@ export async function runCodeQueueCommand(_config: UniDeskConfig, args: string[]
|
||||
if (action === "tasks" || action === "overview") {
|
||||
return codexTasksQuery(args.slice(1));
|
||||
}
|
||||
if (action === "dev-ready" || action === "health") {
|
||||
assertKnownOptions(args.slice(1), {}, `codex ${action}`);
|
||||
return codeQueueDevReady();
|
||||
}
|
||||
if (action === "output") {
|
||||
const taskId = requireTaskId(taskIdArg, "codex output");
|
||||
return codexOutputQuery(taskId, args.slice(2));
|
||||
@@ -1407,10 +1775,14 @@ export async function runCodeQueueCommand(_config: UniDeskConfig, args: string[]
|
||||
const taskId = requireTaskId(taskIdArg, "codex judge");
|
||||
return codexJudgeQuery(taskId, args.slice(2));
|
||||
}
|
||||
if (action === "queues") return codeQueues();
|
||||
if (action === "read" || action === "mark-read") {
|
||||
const taskId = requireTaskId(taskIdArg, `codex ${action}`);
|
||||
return codexReadTask(taskId);
|
||||
}
|
||||
if (action === "queues") return codeQueues(args.slice(1));
|
||||
if (action === "queue") {
|
||||
const sub = taskIdArg ?? "list";
|
||||
if (sub === "list") return codeQueues();
|
||||
if (sub === "list") return codeQueues(args.slice(2));
|
||||
if (sub === "create") return codexCreateQueue(requireQueueId(args.slice(2), "queue create"));
|
||||
if (sub === "merge") {
|
||||
const mergeArgs = args.slice(2);
|
||||
@@ -1425,5 +1797,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, tasks, overview, 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, read, mark-read, dev-ready, health, queues, queue list, queue create, queue merge, move, interrupt, cancel");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user