Files
pikasTech-unidesk/scripts/src/code-queue.ts
T
2026-05-17 16:20:20 +00:00

918 lines
39 KiB
TypeScript

import { readFileSync } from "node:fs";
import { type UniDeskConfig } from "./config";
import { coreInternalFetch } from "./microservices";
const defaultToolLimit = 8;
const defaultTraceLimit = 80;
const maxTraceLimit = 500;
const defaultOutputLimit = 20;
const defaultTextPreviewChars = 12_000;
interface CodexTaskOptions {
trace: boolean;
traceLimit: number;
traceMode: "tail" | "after" | "before";
afterSeq: number;
beforeSeq: number | null;
toolLimit: number;
full: boolean;
rawSummary: boolean;
}
interface CodexOutputOptions {
limit: number;
mode: "tail" | "after" | "before";
afterSeq: number;
beforeSeq: number | null;
fullText: boolean;
maxTextChars: number;
}
interface CodexJudgeOptions {
attempt: number | null;
dryRun: boolean;
includePrompt: boolean;
}
interface CodexSubmitOptions {
prompt: string;
queueId: string | undefined;
providerId: string | undefined;
cwd: string | undefined;
model: string | undefined;
reasoningEffort: string | undefined;
executionMode: string | undefined;
maxAttempts: number | undefined;
referenceTaskIds: string[];
dryRun: boolean;
}
interface CompactTaskMutationResponseOptions {
fullPrompt?: boolean;
}
type CodexRequestInit = { method?: string; body?: unknown };
type CodexResponseFetcher = (path: string, init?: CodexRequestInit) => unknown;
type AsyncCodexResponseFetcher = (path: string, init?: CodexRequestInit) => Promise<unknown>;
const codeQueueProxyPrefix = "/api/microservices/code-queue/proxy";
function codeQueueProxyPath(path: string): string {
if (!path.startsWith("/")) throw new Error("Code Queue proxy path must start with /");
return `${codeQueueProxyPrefix}${path}`;
}
function requireTaskId(value: string | undefined, command: string): string {
if (value === undefined || value.trim().length === 0) throw new Error(`${command} requires task id`);
return value.trim();
}
function asRecord(value: unknown): Record<string, unknown> | null {
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : null;
}
function asArray(value: unknown): unknown[] {
return Array.isArray(value) ? value : [];
}
function asString(value: unknown): string {
return typeof value === "string" ? value : "";
}
function asNumber(value: unknown, fallback = 0): number {
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
}
function asBoolean(value: unknown): boolean {
return value === true || value === 1 || value === "1" || value === "true";
}
function stringList(value: unknown): string[] {
return asArray(value).map((item) => String(item ?? "")).filter((item) => item.length > 0);
}
function textPreview(value: string, maxChars: number): Record<string, unknown> {
const truncated = value.length > maxChars;
return {
text: truncated ? value.slice(0, maxChars) : value,
chars: value.length,
truncated,
omittedChars: truncated ? value.length - maxChars : 0,
};
}
function fmtDuration(ms: unknown): string {
const value = Number(ms);
if (!Number.isFinite(value) || value < 0) return "--";
const totalSeconds = Math.floor(value / 1000);
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
if (minutes > 0) return `${minutes}m ${String(seconds).padStart(2, "0")}s`;
return `${seconds}s`;
}
function upstreamError(response: unknown): string {
const record = asRecord(response);
if (record === null) return String(response);
const body = asRecord(record.body);
const bodyError = body?.error;
if (typeof bodyError === "string") {
const requestId = typeof body?.requestId === "string" ? ` requestId=${body.requestId}` : "";
const providerId = typeof body?.providerId === "string" ? ` providerId=${body.providerId}` : "";
return `${bodyError}${providerId}${requestId}`;
}
const status = typeof record.status === "number" ? `HTTP ${record.status}` : "upstream request failed";
return `${status}: ${JSON.stringify(response).slice(0, 1200)}`;
}
function unwrapCodexResponse(response: unknown): { upstream: { ok: unknown; status: unknown }; body: Record<string, unknown> } {
const record = asRecord(response);
if (record?.ok !== true) throw new Error(upstreamError(response));
const body = asRecord(record.body);
if (body?.ok !== true) throw new Error(upstreamError(response));
return { upstream: { ok: record.ok, status: record.status }, body };
}
function positiveIntegerOption(args: string[], names: string[], defaultValue: number, maxValue = Number.MAX_SAFE_INTEGER): number {
for (const name of names) {
const index = args.indexOf(name);
if (index === -1) continue;
const raw = args[index + 1];
const value = Number(raw);
if (!Number.isInteger(value) || value <= 0) throw new Error(`${name} must be a positive integer`);
return Math.min(value, maxValue);
}
return defaultValue;
}
function nonNegativeNumberOption(args: string[], names: string[], defaultValue: number): number {
for (const name of names) {
const index = args.indexOf(name);
if (index === -1) continue;
const raw = args[index + 1];
const value = Number(raw);
if (!Number.isFinite(value) || value < 0) throw new Error(`${name} must be a non-negative number`);
return value;
}
return defaultValue;
}
function nullablePositiveNumberOption(args: string[], names: string[]): number | null {
for (const name of names) {
const index = args.indexOf(name);
if (index === -1) continue;
const raw = args[index + 1];
const value = Number(raw);
if (!Number.isFinite(value) || value <= 0) throw new Error(`${name} must be a positive number`);
return value;
}
return null;
}
function hasFlag(args: string[], name: string): boolean {
return args.includes(name);
}
function textView(text: string, full: boolean, maxChars: number): Record<string, unknown> {
const truncated = !full && text.length > maxChars;
return {
text: truncated ? text.slice(0, maxChars) : text,
chars: text.length,
truncated,
omittedChars: truncated ? text.length - maxChars : 0,
};
}
function compactText(text: unknown, full: boolean, maxChars: number): Record<string, unknown> {
return textView(asString(text), full, maxChars);
}
function compactLastAssistant(value: unknown, full: boolean): Record<string, unknown> {
const record = asRecord(value) ?? {};
return {
at: record.at ?? null,
seq: record.seq ?? null,
source: record.source ?? "none",
...textView(asString(record.text), full, 4000),
};
}
function compactToolSummary(value: unknown, full: boolean): Record<string, unknown> {
const record = asRecord(value) ?? {};
const items = asArray(record.items).map((item) => {
const line = asRecord(item) ?? {};
return {
seq: line.seq ?? null,
at: line.at ?? null,
kind: line.kind ?? null,
title: line.title ?? "",
status: line.status ?? null,
commandPreview: compactText(line.commandPreview, full, 1200),
commandOmittedLines: line.commandOmittedLines ?? 0,
outputPreview: compactText(line.outputPreview, full, 800),
outputOmittedLines: line.outputOmittedLines ?? 0,
rawSeqs: line.rawSeqs ?? [],
};
});
return {
count: record.count ?? 0,
returned: record.returned ?? items.length,
limit: record.limit ?? items.length,
truncated: record.truncated ?? false,
items,
};
}
function compactSummary(summary: unknown, options: CodexTaskOptions, taskId: string): Record<string, unknown> {
const record = asRecord(summary) ?? {};
const transcriptCount = asNumber(record.transcriptCount, 0);
const transcriptMaxSeq = transcriptCount > 0 ? record.transcriptMaxSeq ?? null : null;
const initialPrompt = asString(record.initialPrompt ?? record.prompt);
return {
id: record.id ?? taskId,
queueId: record.queueId ?? null,
status: record.status ?? null,
providerId: record.providerId ?? null,
model: record.model ?? null,
agentPort: record.agentPort ?? null,
agentPortInfo: record.agentPortInfo ?? null,
reasoningEffort: record.reasoningEffort ?? null,
cwd: record.cwd ?? null,
attempts: {
currentAttempt: record.currentAttempt ?? null,
maxAttempts: record.maxAttempts ?? null,
currentMode: record.currentMode ?? null,
judgeFailCount: record.judgeFailCount ?? null,
judgeFailRetryLimit: record.judgeFailRetryLimit ?? null,
attemptRecords: asArray(record.attempts).map((attempt) => compactAttemptCycle(attempt, options.full)),
},
thread: {
codexThreadId: record.codexThreadId ?? null,
activeTurnId: record.activeTurnId ?? null,
cancelRequested: record.cancelRequested ?? null,
},
timing: record.timing ?? null,
createdAt: record.createdAt ?? null,
startedAt: record.startedAt ?? null,
updatedAt: record.updatedAt ?? null,
finishedAt: record.finishedAt ?? null,
initialPrompt: textView(initialPrompt, options.full, 3000),
basePrompt: textView(asString(record.basePrompt), options.full, 2000),
referenceTaskIds: record.referenceTaskIds ?? [],
referenceInjection: record.referenceInjection ?? null,
lastAssistantMessage: compactLastAssistant(record.lastAssistantMessage, options.full),
lastJudge: record.lastJudge ?? null,
lastError: record.lastError ?? null,
toolSummary: compactToolSummary(record.toolSummary, options.full),
counts: {
transcript: record.transcriptCount ?? null,
output: record.outputCount ?? null,
events: record.eventCount ?? null,
},
traceDisclosure: {
included: options.trace,
renderer: "shared trace-summary/trace-steps progressive abstraction; CLI and WebUI diverge only at final rendering",
total: record.transcriptCount ?? null,
maxSeq: transcriptMaxSeq,
defaultPage: `bun scripts/cli.ts codex task ${taskId} --trace --limit ${defaultTraceLimit}`,
firstPage: `bun scripts/cli.ts codex task ${taskId} --trace --from-start --limit ${defaultTraceLimit}`,
nextPageTemplate: `bun scripts/cli.ts codex task ${taskId} --trace --after-seq <nextAfterSeq> --limit ${defaultTraceLimit}`,
previousPageTemplate: `bun scripts/cli.ts codex task ${taskId} --trace --before-seq <previousBeforeSeq> --limit ${defaultTraceLimit}`,
rawOutputTemplate: `bun scripts/cli.ts codex output ${taskId} --after-seq <rawSeq> --limit ${defaultOutputLimit}`,
fullTextSummary: `bun scripts/cli.ts codex task ${taskId} --full --tool-limit ${Math.max(options.toolLimit, defaultToolLimit)}`,
},
};
}
function traceKindLabel(kind: unknown): string {
const value = String(kind || "");
if (value === "ran") return "Ran";
if (value === "explored") return "Explored";
if (value === "edited") return "Edited";
if (value === "error") return "Error";
if (value === "system") return "System";
return "Message";
}
function transcriptLineToStep(line: unknown): Record<string, unknown> {
const record = asRecord(line) ?? {};
const summaryLines = stringList(record.summaryLines);
const fallbackSummary = [record.commandPreview, record.bodyPreview, record.title].map((value) => asString(value).trim()).filter(Boolean);
return {
seq: record.seq ?? null,
at: record.at ?? null,
kind: record.kind ?? "message",
title: record.title ?? "",
status: record.status ?? null,
durationMs: record.durationMs ?? null,
rawSeqs: record.rawSeqs ?? [],
summaryLines: summaryLines.length > 0 ? summaryLines : fallbackSummary.slice(0, 4),
hasDetail: true,
};
}
function compactTraceStep(step: unknown, taskId: string): Record<string, unknown> {
const record = asRecord(step) ?? {};
const seq = record.seq ?? null;
return {
seq,
at: record.at ?? null,
kind: record.kind ?? "message",
label: traceKindLabel(record.kind),
title: record.title ?? "",
status: record.status ?? null,
durationMs: record.durationMs ?? null,
duration: fmtDuration(record.durationMs),
rawSeqs: record.rawSeqs ?? [],
summaryLines: stringList(record.summaryLines),
hasDetail: asBoolean(record.hasDetail),
detailCommand: seq === null ? null : `bun scripts/cli.ts microservice proxy code-queue /api/tasks/${encodeURIComponent(taskId)}/trace-step?seq=${encodeURIComponent(String(seq))} --raw`,
};
}
function compactAttemptCycle(value: unknown, full: boolean): Record<string, unknown> {
const record = asRecord(value) ?? {};
return {
index: record.index ?? null,
synthetic: record.synthetic ?? false,
label: record.label ?? null,
mode: record.mode ?? null,
terminalStatus: record.terminalStatus ?? null,
appServerExitCode: record.appServerExitCode ?? null,
appServerSignal: record.appServerSignal ?? null,
error: record.error ?? null,
stderrTail: textView(asString(record.stderrTail), full, 1200),
startedAt: record.startedAt ?? null,
finishedAt: record.finishedAt ?? null,
startSeq: record.startSeq ?? null,
endSeq: record.endSeq ?? null,
execution: record.execution ?? null,
finalResponse: textView(asString(record.finalResponsePreview ?? record.finalResponse), full, 3000),
judge: record.judge ?? null,
feedbackPrompt: textView(asString(record.feedbackPromptPreview), full, 1800),
};
}
function renderTraceConsoleRows(summary: Record<string, unknown>, steps: Record<string, unknown>[]): string[] {
const rows: string[] = [];
const execution = asRecord(summary.execution) ?? {};
const stats = asRecord(execution.traceStats) ?? asRecord(summary.traceStats);
const statsSource = String(execution.statsSource || summary.statsSource || "");
const stat = (key: string): string | number => {
if (!stats || statsSource !== "oa-event-flow") return "--";
const value = Number(stats[key]);
return Number.isFinite(value) && value >= 0 ? Math.floor(value) : "--";
};
rows.push([
`task=${summary.id ?? ""}`,
`status=${summary.status ?? ""}`,
`port=${summary.agentPort ?? "codex"}`,
`model=${summary.model ?? ""}`,
`updated=${summary.updatedAt ?? ""}`,
].filter((item) => !item.endsWith("=")).join(" "));
const read = stat("readCount");
const edit = stat("editCount");
const run = stat("runCount");
const toolCount = typeof read === "number" || typeof edit === "number" || typeof run === "number" ? Number(read === "--" ? 0 : read) + Number(edit === "--" ? 0 : edit) + Number(run === "--" ? 0 : run) : "--";
rows.push(`progressive steps=${steps.length} tools=${toolCount} read=${read} edit=${edit} run=${run} errors=${stat("errorCount")}`);
for (const step of steps) {
const head = [`#${step.seq ?? "?"}`, `[${step.label ?? traceKindLabel(step.kind)}]`, step.title ?? ""]
.filter((item) => String(item).length > 0)
.join(" ");
rows.push(`${head}${step.status ? ` (${step.status})` : ""}${step.duration && step.duration !== "--" ? ` ${step.duration}` : ""}`);
for (const line of stringList(step.summaryLines).slice(0, 4)) rows.push(` ${line}`);
}
return rows;
}
function compactProgressiveTrace(summaryBody: Record<string, unknown>, steps: Record<string, unknown>[], taskId: string, full: boolean): Record<string, unknown> {
const summary = asRecord(summaryBody.summary) ?? summaryBody;
return {
taskId: summary.id ?? taskId,
queueId: summary.queueId ?? null,
status: summary.status ?? null,
updatedAt: summary.updatedAt ?? null,
model: summary.model ?? null,
agentPort: summary.agentPort ?? null,
agentPortInfo: summary.agentPortInfo ?? null,
prompt: summary.prompt ?? null,
execution: summary.execution ?? null,
attempts: asArray(summary.attempts).map((attempt) => compactAttemptCycle(attempt, full)),
displayedStepSeqs: steps.map((step) => step.seq ?? null),
renderedRows: renderTraceConsoleRows(summary, steps),
};
}
function compactTracePage(body: Record<string, unknown>, taskId: string, limit: number, summaryBody: Record<string, unknown> | null, options: CodexTaskOptions): Record<string, unknown> {
const stepsSource = asArray(body.steps).length > 0 ? asArray(body.steps) : asArray(body.transcript).map(transcriptLineToStep);
const steps = stepsSource.map((step) => compactTraceStep(step, taskId));
const summary = summaryBody === null ? null : asRecord(summaryBody.summary) ?? summaryBody;
const nextAfterSeq = body.nextAfterSeq ?? null;
const previousBeforeSeq = body.previousBeforeSeq ?? null;
const omittedInPage = steps.some((item) => {
const line = asRecord(item) ?? {};
return asNumber(line.bodyOmittedLines, 0) > 0 || asNumber(line.commandOmittedLines, 0) > 0;
});
const hasRawSeqs = steps.some((item) => asArray(item.rawSeqs).length > 0);
return {
taskId: body.taskId ?? taskId,
queueId: body.queueId ?? null,
status: body.status ?? null,
updatedAt: body.updatedAt ?? null,
agentPort: body.agentPort ?? summary?.agentPort ?? null,
agentPortInfo: body.agentPortInfo ?? summary?.agentPortInfo ?? null,
mode: body.mode ?? null,
limit,
returned: steps.length,
total: body.total ?? null,
maxSeq: body.maxSeq ?? null,
afterSeq: body.afterSeq ?? null,
nextAfterSeq,
beforeSeq: body.beforeSeq ?? null,
previousBeforeSeq,
hasMore: body.hasMore ?? false,
hasBefore: body.hasBefore ?? false,
steps,
progressive: summaryBody === null ? null : compactProgressiveTrace(summaryBody, steps, taskId, options.full),
commands: {
next: body.hasMore === true && nextAfterSeq !== null ? `bun scripts/cli.ts codex task ${taskId} --trace --after-seq ${nextAfterSeq} --limit ${limit}` : null,
previous: body.hasBefore === true && previousBeforeSeq !== null ? `bun scripts/cli.ts codex task ${taskId} --trace --before-seq ${previousBeforeSeq} --limit ${limit}` : null,
tail: `bun scripts/cli.ts codex task ${taskId} --trace --tail --limit ${limit}`,
first: `bun scripts/cli.ts codex task ${taskId} --trace --from-start --limit ${limit}`,
stepDetailTemplate: `bun scripts/cli.ts microservice proxy code-queue /api/tasks/${taskId}/trace-step?seq=<seq> --raw`,
rawOutput: omittedInPage || hasRawSeqs ? `Use rawSeqs on each trace step, e.g. bun scripts/cli.ts codex output ${taskId} --after-seq <rawSeqBefore> --limit ${defaultOutputLimit}` : null,
},
};
}
function parseTaskOptions(args: string[]): CodexTaskOptions {
const beforeSeq = nullablePositiveNumberOption(args, ["--before-seq", "--beforeSeq"]);
const afterSeq = nonNegativeNumberOption(args, ["--after-seq", "--afterSeq"], 0);
const fromStart = hasFlag(args, "--from-start") || hasFlag(args, "--first");
const trace = hasFlag(args, "--trace") || beforeSeq !== null || args.some((arg) => arg === "--after-seq" || arg === "--afterSeq") || fromStart || hasFlag(args, "--tail");
const traceMode = beforeSeq !== null ? "before" : fromStart || args.some((arg) => arg === "--after-seq" || arg === "--afterSeq") ? "after" : "tail";
return {
trace,
traceLimit: positiveIntegerOption(args, ["--trace-limit", "--limit"], defaultTraceLimit, maxTraceLimit),
traceMode,
afterSeq,
beforeSeq,
toolLimit: positiveIntegerOption(args, ["--tool-limit"], defaultToolLimit, 500),
full: hasFlag(args, "--full") || hasFlag(args, "--raw-summary"),
rawSummary: hasFlag(args, "--raw-summary"),
};
}
function parseOutputOptions(args: string[]): CodexOutputOptions {
const beforeSeq = nullablePositiveNumberOption(args, ["--before-seq", "--beforeSeq"]);
const afterSeq = nonNegativeNumberOption(args, ["--after-seq", "--afterSeq"], 0);
const fromStart = hasFlag(args, "--from-start") || hasFlag(args, "--first");
const mode = beforeSeq !== null ? "before" : fromStart || args.some((arg) => arg === "--after-seq" || arg === "--afterSeq") ? "after" : "tail";
return {
limit: positiveIntegerOption(args, ["--limit"], defaultOutputLimit, maxTraceLimit),
mode,
afterSeq,
beforeSeq,
fullText: hasFlag(args, "--full-text") || hasFlag(args, "--raw"),
maxTextChars: positiveIntegerOption(args, ["--max-text-chars"], defaultTextPreviewChars, 500_000),
};
}
function parseJudgeOptions(args: string[]): CodexJudgeOptions {
const rawAttempt = optionValue(args, ["--attempt", "--attempt-id", "--attemptIndex"]) ?? positionalArgs(args)[0];
let attempt: number | null = null;
if (rawAttempt !== undefined) {
const value = Number(rawAttempt);
if (!Number.isInteger(value) || value <= 0) throw new Error("--attempt must be a positive integer");
attempt = value;
}
return {
attempt,
dryRun: hasFlag(args, "--dry-run") || hasFlag(args, "--no-call"),
includePrompt: hasFlag(args, "--include-prompt"),
};
}
function queryString(params: Record<string, string | number | boolean | null | undefined>): string {
const search = new URLSearchParams();
for (const [key, value] of Object.entries(params)) {
if (value !== undefined && value !== null) search.set(key, String(value));
}
const text = search.toString();
return text.length > 0 ? `?${text}` : "";
}
function codexTaskSummary(taskId: string, options: CodexTaskOptions, fetcher: CodexResponseFetcher): unknown {
const summaryPath = codeQueueProxyPath(`/api/tasks/${encodeURIComponent(taskId)}/summary${queryString({ toolLimit: options.toolLimit })}`);
const summaryResponse = unwrapCodexResponse(fetcher(summaryPath));
const summary = summaryResponse.body.summary;
const result: Record<string, unknown> = {
upstream: summaryResponse.upstream,
summary: compactSummary(summary, options, taskId),
};
if (options.rawSummary) result.rawSummary = summary;
if (options.trace) {
const traceParams: Record<string, string | number | boolean | null> = { limit: options.traceLimit };
if (options.traceMode === "tail") traceParams.tail = 1;
if (options.traceMode === "after") traceParams.afterSeq = options.afterSeq;
if (options.traceMode === "before") traceParams.beforeSeq = options.beforeSeq;
const traceSummaryResponse = unwrapCodexResponse(fetcher(codeQueueProxyPath(`/api/tasks/${encodeURIComponent(taskId)}/trace-summary`)));
const traceResponse = unwrapCodexResponse(fetcher(codeQueueProxyPath(`/api/tasks/${encodeURIComponent(taskId)}/trace-steps${queryString(traceParams)}`)));
result.trace = compactTracePage(traceResponse.body, taskId, options.traceLimit, traceSummaryResponse.body, options);
}
return result;
}
function compactOutputPage(body: Record<string, unknown>, taskId: string, limit: number): Record<string, unknown> {
const output = asArray(body.output);
const nextAfterSeq = body.nextAfterSeq ?? null;
const previousBeforeSeq = body.previousBeforeSeq ?? null;
return {
taskId: body.taskId ?? taskId,
queueId: body.queueId ?? null,
status: body.status ?? null,
updatedAt: body.updatedAt ?? null,
mode: body.mode ?? null,
limit,
returned: output.length,
total: body.total ?? null,
maxSeq: body.maxSeq ?? null,
afterSeq: body.afterSeq ?? null,
nextAfterSeq,
beforeSeq: body.beforeSeq ?? null,
previousBeforeSeq,
hasMore: body.hasMore ?? false,
hasBefore: body.hasBefore ?? false,
output,
commands: {
next: body.hasMore === true && nextAfterSeq !== null ? `bun scripts/cli.ts codex output ${taskId} --after-seq ${nextAfterSeq} --limit ${limit}` : null,
previous: body.hasBefore === true && previousBeforeSeq !== null ? `bun scripts/cli.ts codex output ${taskId} --before-seq ${previousBeforeSeq} --limit ${limit}` : null,
tail: `bun scripts/cli.ts codex output ${taskId} --tail --limit ${limit}`,
first: `bun scripts/cli.ts codex output ${taskId} --from-start --limit ${limit}`,
fullText: `bun scripts/cli.ts codex output ${taskId} --after-seq <rawSeqBefore> --limit ${limit} --full-text`,
},
};
}
function codexTaskOutput(taskId: string, options: CodexOutputOptions, fetcher: CodexResponseFetcher): unknown {
const params: Record<string, string | number | boolean | null> = {
limit: options.limit,
fullText: options.fullText ? 1 : 0,
maxTextChars: options.maxTextChars,
};
if (options.mode === "tail") params.tail = 1;
if (options.mode === "after") params.afterSeq = options.afterSeq;
if (options.mode === "before") params.beforeSeq = options.beforeSeq;
const response = unwrapCodexResponse(fetcher(codeQueueProxyPath(`/api/tasks/${encodeURIComponent(taskId)}/output${queryString(params)}`)));
return { upstream: response.upstream, outputPage: compactOutputPage(response.body, taskId, options.limit) };
}
function codexTaskJudge(taskId: string, options: CodexJudgeOptions, fetcher: CodexResponseFetcher): unknown {
const params = queryString({
attempt: options.attempt,
dryRun: options.dryRun ? 1 : undefined,
includePrompt: options.includePrompt ? 1 : undefined,
});
const response = unwrapCodexResponse(fetcher(codeQueueProxyPath(`/api/tasks/${encodeURIComponent(taskId)}/judge${params}`), { method: "POST" }));
return { upstream: response.upstream, judgeReplay: response.body };
}
export function codexTaskQuery(taskId: string, optionArgs: string[], fetcher: CodexResponseFetcher = coreInternalFetch): unknown {
return codexTaskSummary(taskId, parseTaskOptions(optionArgs), fetcher);
}
export function codexOutputQuery(taskId: string, optionArgs: string[], fetcher: CodexResponseFetcher = coreInternalFetch): unknown {
return codexTaskOutput(taskId, parseOutputOptions(optionArgs), fetcher);
}
export function codexJudgeQuery(taskId: string, optionArgs: string[], fetcher: CodexResponseFetcher = coreInternalFetch): unknown {
return codexTaskJudge(taskId, parseJudgeOptions(optionArgs), fetcher);
}
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));
const summary = summaryResponse.body.summary;
const result: Record<string, unknown> = {
upstream: summaryResponse.upstream,
summary: compactSummary(summary, options, taskId),
};
if (options.rawSummary) result.rawSummary = summary;
if (options.trace) {
const traceParams: Record<string, string | number | boolean | null> = { limit: options.traceLimit };
if (options.traceMode === "tail") traceParams.tail = 1;
if (options.traceMode === "after") traceParams.afterSeq = options.afterSeq;
if (options.traceMode === "before") traceParams.beforeSeq = options.beforeSeq;
const traceSummaryResponse = unwrapCodexResponse(await fetcher(codeQueueProxyPath(`/api/tasks/${encodeURIComponent(taskId)}/trace-summary`)));
const traceResponse = unwrapCodexResponse(await fetcher(codeQueueProxyPath(`/api/tasks/${encodeURIComponent(taskId)}/trace-steps${queryString(traceParams)}`)));
result.trace = compactTracePage(traceResponse.body, taskId, options.traceLimit, traceSummaryResponse.body, options);
}
return result;
}
async function codexTaskOutputAsync(taskId: string, options: CodexOutputOptions, fetcher: AsyncCodexResponseFetcher): Promise<unknown> {
const params: Record<string, string | number | boolean | null> = {
limit: options.limit,
fullText: options.fullText ? 1 : 0,
maxTextChars: options.maxTextChars,
};
if (options.mode === "tail") params.tail = 1;
if (options.mode === "after") params.afterSeq = options.afterSeq;
if (options.mode === "before") params.beforeSeq = options.beforeSeq;
const response = unwrapCodexResponse(await fetcher(codeQueueProxyPath(`/api/tasks/${encodeURIComponent(taskId)}/output${queryString(params)}`)));
return { upstream: response.upstream, outputPage: compactOutputPage(response.body, taskId, options.limit) };
}
async function codexTaskJudgeAsync(taskId: string, options: CodexJudgeOptions, fetcher: AsyncCodexResponseFetcher): Promise<unknown> {
const params = queryString({
attempt: options.attempt,
dryRun: options.dryRun ? 1 : undefined,
includePrompt: options.includePrompt ? 1 : undefined,
});
const response = unwrapCodexResponse(await fetcher(codeQueueProxyPath(`/api/tasks/${encodeURIComponent(taskId)}/judge${params}`), { method: "POST" }));
return { upstream: response.upstream, judgeReplay: response.body };
}
export async function codexTaskQueryAsync(taskId: string, optionArgs: string[], fetcher: AsyncCodexResponseFetcher): Promise<unknown> {
return codexTaskSummaryAsync(taskId, parseTaskOptions(optionArgs), fetcher);
}
export async function codexOutputQueryAsync(taskId: string, optionArgs: string[], fetcher: AsyncCodexResponseFetcher): Promise<unknown> {
return codexTaskOutputAsync(taskId, parseOutputOptions(optionArgs), fetcher);
}
export async function codexJudgeQueryAsync(taskId: string, optionArgs: string[], fetcher: AsyncCodexResponseFetcher): Promise<unknown> {
return codexTaskJudgeAsync(taskId, parseJudgeOptions(optionArgs), fetcher);
}
function requireQueueId(args: string[], command: string): string {
const index = args.indexOf("--queue");
const raw = index === -1 ? args[0] : args[index + 1];
if (raw === undefined || raw.trim().length === 0) throw new Error(`${command} requires queue id, for example --queue default`);
return raw.trim();
}
function optionValue(args: string[], names: string[]): string | undefined {
for (const name of names) {
const index = args.indexOf(name);
if (index === -1) continue;
const raw = args[index + 1];
if (raw === undefined || raw.trim().length === 0) throw new Error(`${name} requires a non-empty value`);
return raw.trim();
}
return undefined;
}
function optionValues(args: string[], names: string[]): string[] {
const values: string[] = [];
for (let index = 0; index < args.length; index += 1) {
const name = args[index] ?? "";
if (!names.includes(name)) continue;
const raw = args[index + 1];
if (raw === undefined || raw.trim().length === 0) throw new Error(`${name} requires a non-empty value`);
values.push(raw.trim());
index += 1;
}
return values;
}
function positionalArgs(args: string[]): string[] {
const positions: string[] = [];
for (let index = 0; index < args.length; index += 1) {
const value = args[index] ?? "";
if (value.startsWith("--")) {
index += 1;
continue;
}
positions.push(value);
}
return positions;
}
function positionalArgsWithValueOptions(args: string[], valueOptions: Set<string>): string[] {
const positions: string[] = [];
for (let index = 0; index < args.length; index += 1) {
const value = args[index] ?? "";
if (value.startsWith("--")) {
if (valueOptions.has(value)) index += 1;
continue;
}
positions.push(value);
}
return positions;
}
function requireMergeSourceQueueId(args: string[], command: string): string {
const raw = optionValue(args, ["--source", "--from", "--queue"]) ?? positionalArgs(args)[0];
if (raw === undefined || raw.trim().length === 0) throw new Error(`${command} requires source queue id, for example: codex queue merge old --into default`);
return raw.trim();
}
function requireMergeTargetQueueId(args: string[], command: string): string {
const raw = optionValue(args, ["--into", "--target", "--to"]) ?? positionalArgs(args)[1];
if (raw === undefined || raw.trim().length === 0) throw new Error(`${command} requires target queue id, for example: codex queue merge old --into default`);
return raw.trim();
}
function codeQueues(): unknown {
return unwrapCodexResponse(coreInternalFetch(codeQueueProxyPath("/api/queues")));
}
function codexCreateQueue(queueId: string): unknown {
return unwrapCodexResponse(coreInternalFetch(codeQueueProxyPath("/api/queues"), { method: "POST", body: { queueId } }));
}
function codexMergeQueue(sourceQueueId: string, targetQueueId: string): unknown {
return unwrapCodexResponse(coreInternalFetch(codeQueueProxyPath(`/api/queues/${encodeURIComponent(targetQueueId)}/merge`), { method: "POST", body: { sourceQueueId } }));
}
function codexMoveTask(taskId: string, queueId: string): unknown {
return unwrapCodexResponse(coreInternalFetch(codeQueueProxyPath(`/api/tasks/${encodeURIComponent(taskId)}/move`), { method: "POST", body: { queueId } }));
}
function promptFromSubmitArgs(args: string[]): string {
const promptFile = optionValue(args, ["--prompt-file", "--file"]);
const promptStdin = hasFlag(args, "--prompt-stdin") || hasFlag(args, "--stdin");
const promptArgs = positionalArgsWithValueOptions(args, new Set([
"--prompt-file",
"--file",
"--queue",
"--queue-id",
"--provider",
"--provider-id",
"--cwd",
"--workdir",
"--model",
"--reasoning-effort",
"--execution-mode",
"--mode",
"--max-attempts",
"--reference-task-id",
"--reference",
"--ref",
]));
const sources = [promptFile !== undefined, promptStdin, promptArgs.length > 0].filter(Boolean).length;
if (sources !== 1) throw new Error("codex submit requires exactly one prompt source: positional prompt, --prompt-file, or --prompt-stdin");
const text = promptFile !== undefined
? (promptFile === "-" ? readFileSync(0, "utf8") : readFileSync(promptFile, "utf8"))
: promptStdin
? readFileSync(0, "utf8")
: promptArgs.join(" ");
if (text.trim().length === 0) throw new Error("codex submit prompt must not be empty");
return text;
}
function referenceTaskIdsFromOptions(args: string[]): string[] {
const values = optionValues(args, ["--reference-task-id", "--reference", "--ref"]);
const ids: string[] = [];
for (const value of values.flatMap((item) => item.split(/[,\s]+/u))) {
const id = value.trim();
if (id.length > 0 && !ids.includes(id)) ids.push(id);
}
return ids;
}
function parseSubmitOptions(args: string[]): CodexSubmitOptions {
const maxAttempts = args.some((arg) => arg === "--max-attempts")
? positiveIntegerOption(args, ["--max-attempts"], 99, 99)
: undefined;
return {
prompt: promptFromSubmitArgs(args),
queueId: optionValue(args, ["--queue", "--queue-id"]),
providerId: optionValue(args, ["--provider-id", "--provider"]),
cwd: optionValue(args, ["--cwd", "--workdir"]),
model: optionValue(args, ["--model"]),
reasoningEffort: optionValue(args, ["--reasoning-effort"]),
executionMode: optionValue(args, ["--execution-mode", "--mode"]),
maxAttempts,
referenceTaskIds: referenceTaskIdsFromOptions(args),
dryRun: hasFlag(args, "--dry-run"),
};
}
function submitPayload(options: CodexSubmitOptions): Record<string, unknown> {
return {
prompt: options.prompt,
...(options.queueId === undefined ? {} : { queueId: options.queueId }),
...(options.providerId === undefined ? {} : { providerId: options.providerId }),
...(options.cwd === undefined ? {} : { cwd: options.cwd }),
...(options.model === undefined ? {} : { model: options.model }),
...(options.reasoningEffort === undefined ? {} : { reasoningEffort: options.reasoningEffort }),
...(options.executionMode === undefined ? {} : { executionMode: options.executionMode }),
...(options.maxAttempts === undefined ? {} : { maxAttempts: options.maxAttempts }),
...(options.referenceTaskIds.length === 0 ? {} : { referenceTaskIds: options.referenceTaskIds }),
};
}
function compactTaskMutationResponse(task: unknown, options: CompactTaskMutationResponseOptions = {}): Record<string, unknown> {
const record = asRecord(task) ?? {};
const taskId = asString(record.id);
const prompt = asString(record.displayPrompt ?? record.basePrompt ?? record.prompt);
return {
id: taskId || null,
queueId: record.queueId ?? null,
status: record.status ?? null,
queuedReason: record.queuedReason ?? null,
providerId: record.providerId ?? null,
model: record.model ?? null,
reasoningEffort: record.reasoningEffort ?? null,
cwd: record.cwd ?? null,
executionMode: record.executionMode ?? null,
maxAttempts: record.maxAttempts ?? null,
currentAttempt: record.currentAttempt ?? null,
cancelRequested: record.cancelRequested ?? null,
createdAt: record.createdAt ?? null,
startedAt: record.startedAt ?? null,
updatedAt: record.updatedAt ?? null,
finishedAt: record.finishedAt ?? null,
prompt: textView(prompt, options.fullPrompt === true, 1200),
commands: taskId.length === 0 ? null : {
show: `bun scripts/cli.ts codex task ${taskId}`,
trace: `bun scripts/cli.ts codex task ${taskId} --trace --tail --limit ${defaultTraceLimit}`,
output: `bun scripts/cli.ts codex output ${taskId} --tail --limit ${defaultOutputLimit}`,
interrupt: `bun scripts/cli.ts codex interrupt ${taskId}`,
move: `bun scripts/cli.ts codex move ${taskId} --queue <queueId>`,
},
};
}
function compactQueueMutationSummary(value: unknown): Record<string, unknown> | null {
const record = asRecord(value);
if (record === null) return null;
return {
activeQueueIds: record.activeQueueIds ?? null,
activeTaskIds: record.activeTaskIds ?? null,
queuedTaskIds: record.queuedTaskIds ?? null,
counts: record.counts ?? null,
byQueue: Array.isArray(record.byQueue) ? record.byQueue : undefined,
};
}
function codexSubmitTask(args: string[]): unknown {
const options = parseSubmitOptions(args);
const payload = submitPayload(options);
if (options.dryRun) {
return {
ok: true,
dryRun: true,
request: {
...payload,
prompt: textView(options.prompt, true, 3000),
},
};
}
const response = unwrapCodexResponse(coreInternalFetch(codeQueueProxyPath("/api/tasks"), { method: "POST", body: payload }));
return {
upstream: response.upstream,
tasks: asArray(response.body.tasks).map((task) => compactTaskMutationResponse(task, { fullPrompt: true })),
queue: compactQueueMutationSummary(response.body.queue),
};
}
function codexInterruptTask(taskId: string): unknown {
const response = unwrapCodexResponse(coreInternalFetch(codeQueueProxyPath(`/api/tasks/${encodeURIComponent(taskId)}/interrupt`), { method: "POST" }));
return {
upstream: response.upstream,
task: compactTaskMutationResponse(response.body.task),
queue: compactQueueMutationSummary(response.body.queue),
};
}
export async function runCodeQueueCommand(_config: UniDeskConfig, args: string[]): Promise<unknown> {
const [action = "task", taskIdArg] = args;
if (action === "submit" || action === "enqueue") {
return codexSubmitTask(args.slice(1));
}
if (action === "task" || action === "summary" || action === "show") {
const taskId = requireTaskId(taskIdArg, `codex ${action}`);
return codexTaskQuery(taskId, args.slice(2));
}
if (action === "output") {
const taskId = requireTaskId(taskIdArg, "codex output");
return codexOutputQuery(taskId, args.slice(2));
}
if (action === "judge") {
const taskId = requireTaskId(taskIdArg, "codex judge");
return codexJudgeQuery(taskId, args.slice(2));
}
if (action === "queues") return codeQueues();
if (action === "queue") {
const sub = taskIdArg ?? "list";
if (sub === "list") return codeQueues();
if (sub === "create") return codexCreateQueue(requireQueueId(args.slice(2), "queue create"));
if (sub === "merge") {
const mergeArgs = args.slice(2);
return codexMergeQueue(requireMergeSourceQueueId(mergeArgs, "queue merge"), requireMergeTargetQueueId(mergeArgs, "queue merge"));
}
}
if (action === "move") {
const taskId = requireTaskId(taskIdArg, "codex move");
return codexMoveTask(taskId, requireQueueId(args.slice(2), "codex move"));
}
if (action === "interrupt" || action === "cancel") {
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");
}