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; 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 | null { return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record : 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 { 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 } { 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 { 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 { return textView(asString(text), full, maxChars); } function compactLastAssistant(value: unknown, full: boolean): Record { 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 { 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 { 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 --limit ${defaultTraceLimit}`, previousPageTemplate: `bun scripts/cli.ts codex task ${taskId} --trace --before-seq --limit ${defaultTraceLimit}`, rawOutputTemplate: `bun scripts/cli.ts codex output ${taskId} --after-seq --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 { 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 { 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 { 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, steps: Record[]): 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, steps: Record[], taskId: string, full: boolean): Record { 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, taskId: string, limit: number, summaryBody: Record | null, options: CodexTaskOptions): Record { 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= --raw`, rawOutput: omittedInPage || hasRawSeqs ? `Use rawSeqs on each trace step, e.g. bun scripts/cli.ts codex output ${taskId} --after-seq --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 { 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 = { upstream: summaryResponse.upstream, summary: compactSummary(summary, options, taskId), }; if (options.rawSummary) result.rawSummary = summary; if (options.trace) { const traceParams: Record = { 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, taskId: string, limit: number): Record { 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 --limit ${limit} --full-text`, }, }; } function codexTaskOutput(taskId: string, options: CodexOutputOptions, fetcher: CodexResponseFetcher): unknown { const params: Record = { 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 { 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 = { upstream: summaryResponse.upstream, summary: compactSummary(summary, options, taskId), }; if (options.rawSummary) result.rawSummary = summary; if (options.trace) { const traceParams: Record = { 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 { const params: Record = { 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 { 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 { return codexTaskSummaryAsync(taskId, parseTaskOptions(optionArgs), fetcher); } export async function codexOutputQueryAsync(taskId: string, optionArgs: string[], fetcher: AsyncCodexResponseFetcher): Promise { return codexTaskOutputAsync(taskId, parseOutputOptions(optionArgs), fetcher); } export async function codexJudgeQueryAsync(taskId: string, optionArgs: string[], fetcher: AsyncCodexResponseFetcher): Promise { 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[] { 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 { 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 { 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 `, }, }; } function compactQueueMutationSummary(value: unknown): Record | 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 { 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"); }