2537 lines
106 KiB
TypeScript
2537 lines
106 KiB
TypeScript
// 重构前 index.ts 只读参考:commit 6a04144d3f5103014f75b637d7e6bc2f45bf007f,blob 56e590c1a6b5ca7ad128bf2c992f60e46c355a58;可用 `git show 6a04144d3f5103014f75b637d7e6bc2f45bf007f:src/components/microservices/code-queue/src/index.ts` 查看。
|
||
|
||
import { existsSync, readdirSync, readFileSync, statSync, type Dirent } from "node:fs";
|
||
import { resolve } from "node:path";
|
||
import type {
|
||
AttemptSummary,
|
||
FeedbackPromptRecord,
|
||
JudgeDecision,
|
||
JudgeResult,
|
||
JsonValue,
|
||
LiveOutput,
|
||
PromptHistoryItem,
|
||
QueueTask,
|
||
QueuedStatusReason,
|
||
RuntimeConfig,
|
||
SessionCommandOutput,
|
||
SessionFileChange,
|
||
TranscriptKind,
|
||
TranscriptLine,
|
||
} from "./types";
|
||
import { codeAgentPortForModel, codeAgentPortInfo, codeExecutionModeInfo, extractRecord } from "./code-agent/common";
|
||
import { currentTaskPromptMarker, resolvedReferenceContextTitle, stripCodeQueueEnvironmentHint, userPromptForDisplay } from "./prompts";
|
||
import { outputArchiveSignature, taskFullOutput } from "./task-output";
|
||
import { retryPrompt } from "./judge";
|
||
import { readOaTraceStepsForTask, type OaTraceStepSummary } from "./oa-events";
|
||
|
||
export interface TaskViewContext {
|
||
config: Pick<RuntimeConfig, "codexHome">;
|
||
errorToJson: (error: unknown) => JsonValue;
|
||
jsonResponse: (body: unknown, status?: number) => Response;
|
||
logger: (level: "debug" | "info" | "warn" | "error", message: string, data?: JsonValue) => void;
|
||
mergePromptHistory: (items: PromptHistoryItem[]) => PromptHistoryItem[];
|
||
nowIso: () => string;
|
||
outputPromptHistory: (task: QueueTask) => PromptHistoryItem[];
|
||
pageBySeq: <T extends { seq: number }>(items: T[], url: URL, limit: number) => {
|
||
mode: "tail" | "after" | "before";
|
||
afterSeq: number;
|
||
beforeSeq: number | null;
|
||
nextAfterSeq: number;
|
||
previousBeforeSeq: number | null;
|
||
hasMore: boolean;
|
||
hasBefore: boolean;
|
||
chunk: T[];
|
||
};
|
||
parseLimit: (url: URL) => number;
|
||
parseSeqParam: (url: URL, name: string, defaultValue: number | null) => number | null;
|
||
queueIdOf: (task: QueueTask) => string;
|
||
queuedStatusReason: (task: QueueTask) => QueuedStatusReason | null;
|
||
queuedTaskPromptEditable: (task: QueueTask) => boolean;
|
||
taskQueueEnteredAt: (task: QueueTask) => string;
|
||
}
|
||
|
||
function isCodexToolLifecycleOutput(output: Pick<LiveOutput, "channel" | "method" | "itemId" | "text">): boolean {
|
||
if (output.channel !== "tool" || typeof output.itemId !== "string" || output.itemId.length === 0) return false;
|
||
const method = String(output.method || "");
|
||
if (method !== "item/started" && method !== "item/completed") return false;
|
||
return /\b(?:webSearch|mcpToolCall|dynamicToolCall)\b/u.test(String(output.text || ""));
|
||
}
|
||
|
||
function codexToolLifecycleStartedBeforeIn(outputs: Pick<LiveOutput, "channel" | "method" | "itemId" | "text">[], output: Pick<LiveOutput, "channel" | "method" | "itemId" | "text">): boolean {
|
||
if (!isCodexToolLifecycleOutput(output)) return false;
|
||
return outputs.some((item) => item !== output && isCodexToolLifecycleOutput(item) && item.itemId === output.itemId && item.method === "item/started");
|
||
}
|
||
|
||
const judgeFailRetryLimit = 3;
|
||
const transcriptCache = new Map<string, { signature: string; previewTranscript?: TranscriptLine[]; fullTranscript?: TranscriptLine[] }>();
|
||
const codexSessionPathCache = new Map<string, string>();
|
||
const codexSessionFileChangeCache = new Map<string, { signature: string; changes: Map<string, SessionFileChange> }>();
|
||
const codexSessionCommandOutputCache = new Map<string, { signature: string; outputs: Map<string, SessionCommandOutput> }>();
|
||
let context: TaskViewContext | null = null;
|
||
|
||
export function configureTaskView(runtimeContext: TaskViewContext): void {
|
||
context = runtimeContext;
|
||
}
|
||
|
||
function ctx(): TaskViewContext {
|
||
if (context === null) throw new Error("task-view module is not configured");
|
||
return context;
|
||
}
|
||
|
||
function taskTimestamp(value: string | null): string | null {
|
||
if (value === null || value.length === 0) return null;
|
||
const time = Date.parse(value);
|
||
return Number.isFinite(time) ? new Date(time).toISOString() : null;
|
||
}
|
||
|
||
function terminalTask(task: QueueTask): boolean {
|
||
return task.status === "succeeded" || task.status === "failed" || task.status === "canceled";
|
||
}
|
||
|
||
function terminalTaskUnread(task: QueueTask): boolean {
|
||
return terminalTask(task) && task.readAt === null;
|
||
}
|
||
|
||
function queuedStatusPayload(task: QueueTask): { queuedReason: QueuedStatusReason | null; queuedReasonLabel: string | null } {
|
||
const reason = ctx().queuedStatusReason(task);
|
||
return {
|
||
queuedReason: reason,
|
||
queuedReasonLabel: reason?.label ?? null,
|
||
};
|
||
}
|
||
|
||
function durationMsBetween(startAt: string | null | undefined, endAt: string | null | undefined): number | null {
|
||
const start = typeof startAt === "string" ? Date.parse(startAt) : NaN;
|
||
const end = typeof endAt === "string" ? Date.parse(endAt) : NaN;
|
||
if (!Number.isFinite(start) || !Number.isFinite(end)) return null;
|
||
return Math.max(0, end - start);
|
||
}
|
||
|
||
function safePreview(value: string, max = 900): string {
|
||
const compact = value.replace(/\s+/gu, " ").trim();
|
||
return compact.length > max ? `${compact.slice(0, max)}...` : compact;
|
||
}
|
||
|
||
function prefixPreview(value: string, max = 900): string {
|
||
const trimmed = value.trim();
|
||
return trimmed.length > max ? `${trimmed.slice(0, max)}...` : trimmed;
|
||
}
|
||
|
||
function linePreview(text: string, maxLines: number, maxChars: number): { text: string; omittedLines: number } {
|
||
const clean = text.replace(/\u001b\[[0-9;]*m/gu, "").trimEnd();
|
||
if (clean.length === 0) return { text: "", omittedLines: 0 };
|
||
const lines = clean.split(/\r?\n/u);
|
||
const kept: string[] = [];
|
||
let chars = 0;
|
||
for (const line of lines) {
|
||
if (kept.length >= maxLines || chars + line.length > maxChars) break;
|
||
kept.push(line);
|
||
chars += line.length + 1;
|
||
}
|
||
return { text: kept.join("\n"), omittedLines: Math.max(0, lines.length - kept.length) };
|
||
}
|
||
|
||
function completeTraceText(text: string): { text: string; omittedLines: number } {
|
||
return { text: text.replace(/\u001b\[[0-9;]*m/gu, "").trimEnd(), omittedLines: 0 };
|
||
}
|
||
|
||
function editedOutputPreview(text: string): { text: string; omittedLines: number } {
|
||
return linePreview(compactTranscriptBody(text), 120, 24_000);
|
||
}
|
||
|
||
function codexSessionDateDir(value: string | null | undefined): string | null {
|
||
if (typeof value !== "string" || value.length === 0) return null;
|
||
const date = new Date(value);
|
||
if (Number.isNaN(date.getTime())) return null;
|
||
return resolve(
|
||
ctx().config.codexHome,
|
||
"sessions",
|
||
String(date.getUTCFullYear()),
|
||
String(date.getUTCMonth() + 1).padStart(2, "0"),
|
||
String(date.getUTCDate()).padStart(2, "0"),
|
||
);
|
||
}
|
||
|
||
function codexSessionSignature(path: string): string | null {
|
||
try {
|
||
const stat = statSync(path);
|
||
return `${stat.size}:${Math.floor(stat.mtimeMs)}`;
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function findCodexSessionFile(task: QueueTask): string | null {
|
||
const threadId = task.codexThreadId;
|
||
if (threadId === null || threadId.length === 0) return null;
|
||
const cached = codexSessionPathCache.get(threadId);
|
||
if (cached !== undefined && existsSync(cached)) return cached;
|
||
|
||
const roots = Array.from(new Set([
|
||
codexSessionDateDir(task.startedAt),
|
||
codexSessionDateDir(task.createdAt),
|
||
codexSessionDateDir(task.updatedAt),
|
||
resolve(ctx().config.codexHome, "sessions"),
|
||
].filter((value): value is string => value !== null && existsSync(value))));
|
||
const matches: string[] = [];
|
||
let scanned = 0;
|
||
const scan = (dir: string, depth: number): void => {
|
||
if (depth < 0 || scanned > 4000) return;
|
||
scanned += 1;
|
||
let entries: Dirent[];
|
||
try {
|
||
entries = readdirSync(dir, { withFileTypes: true });
|
||
} catch {
|
||
return;
|
||
}
|
||
for (const entry of entries) {
|
||
const path = resolve(dir, entry.name);
|
||
if (entry.isDirectory()) {
|
||
scan(path, depth - 1);
|
||
} else if (entry.isFile() && entry.name.endsWith(".jsonl") && entry.name.includes(threadId)) {
|
||
matches.push(path);
|
||
}
|
||
}
|
||
};
|
||
for (const root of roots) scan(root, root.endsWith("/sessions") ? 4 : 1);
|
||
matches.sort((left, right) => (statMtimeMs(right) - statMtimeMs(left)) || right.localeCompare(left));
|
||
const match = matches[0] ?? null;
|
||
if (match !== null) codexSessionPathCache.set(threadId, match);
|
||
return match;
|
||
}
|
||
|
||
function statMtimeMs(path: string): number {
|
||
try {
|
||
return statSync(path).mtimeMs;
|
||
} catch {
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
function parseCodexToolOutputText(raw: string): string {
|
||
const text = String(raw || "");
|
||
try {
|
||
const parsed = JSON.parse(text) as Record<string, unknown>;
|
||
if (typeof parsed.output === "string") return parsed.output;
|
||
} catch {
|
||
// Keep the raw tool output if it is not the JSON wrapper used by Codex.
|
||
}
|
||
return text;
|
||
}
|
||
|
||
function recordStringField(record: Record<string, unknown> | null, keys: string[]): string {
|
||
if (record === null) return "";
|
||
for (const key of keys) {
|
||
const value = record[key];
|
||
if (typeof value === "string") return value;
|
||
}
|
||
return "";
|
||
}
|
||
|
||
function recordNumberField(record: Record<string, unknown> | null, keys: string[]): number | null {
|
||
if (record === null) return null;
|
||
for (const key of keys) {
|
||
const value = Number(record[key]);
|
||
if (Number.isFinite(value)) return value;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function parseToolOutputStreams(raw: string): { stdout: string; stderr: string; output: string; exitCode: number | null } {
|
||
const text = String(raw || "");
|
||
try {
|
||
const parsed = JSON.parse(text) as unknown;
|
||
const record = extractRecord(parsed);
|
||
const stdout = recordStringField(record, ["stdout", "stdoutText"]);
|
||
const stderr = recordStringField(record, ["stderr", "stderrText"]);
|
||
const output = recordStringField(record, ["output", "text"]);
|
||
const exitCode = recordNumberField(record, ["exitCode", "code", "status"]);
|
||
if (stdout.length > 0 || stderr.length > 0 || output.length > 0) {
|
||
return { stdout: stdout || output, stderr, output: output || [stdout, stderr].filter(Boolean).join("\n"), exitCode };
|
||
}
|
||
} catch {
|
||
// Not a JSON wrapper; parse the Codex CLI tool-result envelope below.
|
||
}
|
||
|
||
const outputMatch = /(?:^|\n)Output:\n([\s\S]*)$/u.exec(text);
|
||
const exitMatch = /Process exited with code\s+(-?\d+)/u.exec(text);
|
||
if (outputMatch !== null) {
|
||
const output = (outputMatch[1] ?? "").trimEnd();
|
||
return {
|
||
stdout: output,
|
||
stderr: "",
|
||
output,
|
||
exitCode: exitMatch === null ? null : Number(exitMatch[1]),
|
||
};
|
||
}
|
||
return { stdout: text, stderr: "", output: text, exitCode: exitMatch === null ? null : Number(exitMatch[1]) };
|
||
}
|
||
|
||
function formatCommandOutput(output: SessionCommandOutput | null | undefined): string {
|
||
if (output === null || output === undefined) return "";
|
||
const parts: string[] = [];
|
||
const stdout = output.stdout.trimEnd();
|
||
const stderr = output.stderr.trimEnd();
|
||
if (stdout.length > 0) parts.push(stderr.length > 0 ? `[stdout]\n${stdout}` : stdout);
|
||
if (stderr.length > 0) parts.push(`[stderr]\n${stderr}`);
|
||
if (parts.length > 0) return parts.join("\n");
|
||
return output.output.trimEnd();
|
||
}
|
||
|
||
function addCommandOutputStreams(line: TranscriptLine, output: SessionCommandOutput | null | undefined, fullText: boolean): TranscriptLine {
|
||
if (output === null || output === undefined) return line;
|
||
const stdout = output.stdout.trimEnd();
|
||
const stderr = output.stderr.trimEnd();
|
||
if (stdout.length > 0) {
|
||
const preview = fullText ? completeTraceText(stdout) : outputPreview(stdout);
|
||
if (preview.text.length > 0) {
|
||
line.stdoutPreview = preview.text;
|
||
line.stdoutOmittedLines = preview.omittedLines || undefined;
|
||
}
|
||
}
|
||
if (stderr.length > 0) {
|
||
const preview = fullText ? completeTraceText(stderr) : outputPreview(stderr);
|
||
if (preview.text.length > 0) {
|
||
line.stderrPreview = preview.text;
|
||
line.stderrOmittedLines = preview.omittedLines || undefined;
|
||
}
|
||
}
|
||
return line;
|
||
}
|
||
|
||
function parseCodexSessionCommandOutputs(path: string): Map<string, SessionCommandOutput> {
|
||
const signature = codexSessionSignature(path);
|
||
const cached = codexSessionCommandOutputCache.get(path);
|
||
if (signature !== null && cached?.signature === signature) return cached.outputs;
|
||
|
||
const outputs = new Map<string, SessionCommandOutput>();
|
||
try {
|
||
const text = readFileSync(path, "utf8");
|
||
for (const line of text.split(/\r?\n/u)) {
|
||
if (line.trim().length === 0) continue;
|
||
let record: Record<string, unknown>;
|
||
try {
|
||
record = JSON.parse(line) as Record<string, unknown>;
|
||
} catch {
|
||
continue;
|
||
}
|
||
const payload = extractRecord(record.payload);
|
||
if (payload === null) continue;
|
||
const type = String(payload.type || "");
|
||
if (type !== "function_call_output" && type !== "custom_tool_call_output") continue;
|
||
const callId = typeof payload.call_id === "string" ? payload.call_id : "";
|
||
if (callId.length === 0) continue;
|
||
const parsed = parseToolOutputStreams(String(payload.output || ""));
|
||
if (parsed.output.trim().length === 0 && parsed.stdout.trim().length === 0 && parsed.stderr.trim().length === 0) continue;
|
||
outputs.set(callId, {
|
||
callId,
|
||
at: typeof record.timestamp === "string" ? record.timestamp : "",
|
||
stdout: parsed.stdout,
|
||
stderr: parsed.stderr,
|
||
output: parsed.output,
|
||
exitCode: parsed.exitCode,
|
||
});
|
||
}
|
||
} catch (error) {
|
||
ctx().logger("warn", "codex_session_command_output_parse_failed", { path, error: ctx().errorToJson(error) });
|
||
}
|
||
if (signature !== null) codexSessionCommandOutputCache.set(path, { signature, outputs });
|
||
if (codexSessionCommandOutputCache.size > 40) {
|
||
const firstKey = codexSessionCommandOutputCache.keys().next().value;
|
||
if (typeof firstKey === "string") codexSessionCommandOutputCache.delete(firstKey);
|
||
}
|
||
return outputs;
|
||
}
|
||
|
||
function codexSessionCommandOutputsByCallId(task: QueueTask): Map<string, SessionCommandOutput> {
|
||
const path = findCodexSessionFile(task);
|
||
return path === null ? new Map() : parseCodexSessionCommandOutputs(path);
|
||
}
|
||
|
||
function isInlineFileChangeInput(name: string, input: string): boolean {
|
||
return name === "apply_patch" && /^\*\*\* Begin Patch/mu.test(input);
|
||
}
|
||
|
||
function trimmedPatchForTrace(input: string): string {
|
||
const normalized = input.replace(/\r\n/gu, "\n").replace(/\r/gu, "\n").trimEnd();
|
||
const maxChars = 120_000;
|
||
if (normalized.length <= maxChars) return normalized;
|
||
return `${normalized.slice(0, maxChars)}\n...[patch content truncated: ${normalized.length - maxChars} chars omitted]`;
|
||
}
|
||
|
||
function parseCodexSessionFileChanges(path: string): Map<string, SessionFileChange> {
|
||
const signature = codexSessionSignature(path);
|
||
const cached = codexSessionFileChangeCache.get(path);
|
||
if (signature !== null && cached?.signature === signature) return cached.changes;
|
||
|
||
const changes = new Map<string, SessionFileChange>();
|
||
try {
|
||
const text = readFileSync(path, "utf8");
|
||
for (const line of text.split(/\r?\n/u)) {
|
||
if (line.trim().length === 0) continue;
|
||
let record: Record<string, unknown>;
|
||
try {
|
||
record = JSON.parse(line) as Record<string, unknown>;
|
||
} catch {
|
||
continue;
|
||
}
|
||
const payload = extractRecord(record.payload);
|
||
if (payload === null) continue;
|
||
const type = String(payload.type || "");
|
||
const callId = typeof payload.call_id === "string" ? payload.call_id : "";
|
||
if (callId.length === 0) continue;
|
||
if (type === "custom_tool_call") {
|
||
const name = String(payload.name || "");
|
||
const input = typeof payload.input === "string" ? payload.input : "";
|
||
if (!isInlineFileChangeInput(name, input)) continue;
|
||
changes.set(callId, {
|
||
callId,
|
||
at: typeof record.timestamp === "string" ? record.timestamp : "",
|
||
name,
|
||
input: trimmedPatchForTrace(input),
|
||
output: "",
|
||
});
|
||
continue;
|
||
}
|
||
if (type === "custom_tool_call_output") {
|
||
const existing = changes.get(callId);
|
||
if (existing === undefined) continue;
|
||
existing.output = parseCodexToolOutputText(String(payload.output || ""));
|
||
}
|
||
}
|
||
} catch (error) {
|
||
ctx().logger("warn", "codex_session_file_change_parse_failed", { path, error: ctx().errorToJson(error) });
|
||
}
|
||
if (signature !== null) codexSessionFileChangeCache.set(path, { signature, changes });
|
||
if (codexSessionFileChangeCache.size > 40) {
|
||
const firstKey = codexSessionFileChangeCache.keys().next().value;
|
||
if (typeof firstKey === "string") codexSessionFileChangeCache.delete(firstKey);
|
||
}
|
||
return changes;
|
||
}
|
||
|
||
function codexSessionFileChangesByCallId(task: QueueTask): Map<string, SessionFileChange> {
|
||
const path = findCodexSessionFile(task);
|
||
return path === null ? new Map() : parseCodexSessionFileChanges(path);
|
||
}
|
||
|
||
function fileChangeTextWithInlinePatch(item: LiveOutput, changes: Map<string, SessionFileChange>): string {
|
||
if (item.method !== "item/fileChange/outputDelta" || typeof item.itemId !== "string") return item.text;
|
||
const change = changes.get(item.itemId);
|
||
if (change === undefined || change.input.length === 0 || item.text.includes(change.input)) return item.text;
|
||
return `${item.text.trimEnd()}\n\n${change.input}\n`;
|
||
}
|
||
|
||
function compactNoisyLine(line: string): string {
|
||
const compact = line.replace(/\s+/gu, " ").trimEnd();
|
||
const hasEncodedBlob = /[A-Za-z0-9+/=]{220,}/u.test(compact);
|
||
const hasSshWrapper = compact.includes("UNIDESK_SSH_TOOL_DIR") || compact.includes("apply_patch") || compact.includes("base64 -d");
|
||
if (compact.length > 420 && (hasEncodedBlob || hasSshWrapper)) {
|
||
return `${compact.slice(0, 220)} ... [omitted noisy wrapper, ${compact.length - 220} chars]`;
|
||
}
|
||
return compact.length > 1200 ? `${compact.slice(0, 900)} ... [omitted ${compact.length - 900} chars]` : line;
|
||
}
|
||
|
||
function compactTranscriptBody(text: string): string {
|
||
return text.split(/\r?\n/u).map(compactNoisyLine).join("\n");
|
||
}
|
||
|
||
function parseCommandLine(text: string): { command: string; status?: string } | null {
|
||
const match = text.match(/^item\/(?:started|completed):\s+([\s\S]*?)\s+status=([A-Za-z0-9_-]+)/u);
|
||
if (match === null) return null;
|
||
return { command: match[1]?.trim() ?? "", status: match[2] };
|
||
}
|
||
|
||
function extractOuterQuotedShellArg(text: string): { body: string; trailing: string } | null {
|
||
const quote = text[0];
|
||
if (quote !== "\"" && quote !== "'") return null;
|
||
let escaped = false;
|
||
for (let index = 1; index < text.length; index += 1) {
|
||
const char = text[index] ?? "";
|
||
if (quote === "\"" && escaped) {
|
||
escaped = false;
|
||
continue;
|
||
}
|
||
if (quote === "\"" && char === "\\") {
|
||
escaped = true;
|
||
continue;
|
||
}
|
||
if (char === quote) return { body: text.slice(1, index), trailing: text.slice(index + 1).trim() };
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function decodeShellDoubleQuoted(text: string): string {
|
||
return text
|
||
.replace(/\\(["\\$`])/gu, "$1")
|
||
.replace(/\\\r?\n/gu, "");
|
||
}
|
||
|
||
function displayCommand(command: string): string {
|
||
const normalized = command.trim().replace(/\\n/gu, "\n");
|
||
const match = normalized.match(/^(?:\/usr\/bin\/env\s+)?(?:\/(?:usr\/)?bin\/)?(?:bash|sh)\s+-lc\s+([\s\S]+)$/u);
|
||
if (match === null) return normalized;
|
||
const shellText = (match[1] ?? "").trim();
|
||
const shellArg = extractOuterQuotedShellArg(shellText);
|
||
if (shellArg === null) return (match[1] ?? normalized).trim();
|
||
const quote = shellText[0];
|
||
const body = quote === "\"" ? decodeShellDoubleQuoted(shellArg.body) : shellArg.body;
|
||
return shellArg.trailing.length > 0 ? `${body} ${shellArg.trailing}` : body;
|
||
}
|
||
|
||
function shortCommandTitle(command: string): string {
|
||
const firstLine = displayCommand(command).split(/\r?\n/u).find((line) => line.trim().length > 0)?.trim() ?? command.trim();
|
||
return safePreview(firstLine, 180);
|
||
}
|
||
|
||
function commandPreview(command: string): { text: string; omittedLines: number } {
|
||
return linePreview(compactTranscriptBody(displayCommand(command)), 10, 8000);
|
||
}
|
||
|
||
function outputPreview(text: string): { text: string; omittedLines: number } {
|
||
return linePreview(compactTranscriptBody(text), 4, 1600);
|
||
}
|
||
|
||
function fullMessageBody(text: string): { text: string; omittedLines: number } {
|
||
return linePreview(text.replace(/\u001b\[[0-9;]*m/gu, "").trimEnd(), 12, 4000);
|
||
}
|
||
|
||
function timestampMs(value: string | null | undefined): number | null {
|
||
if (typeof value !== "string" || value.length === 0) return null;
|
||
const time = Date.parse(value);
|
||
return Number.isFinite(time) ? time : null;
|
||
}
|
||
|
||
function nonNegativeElapsed(startMs: number | null, endMs: number | null): number | null {
|
||
if (startMs === null || endMs === null) return null;
|
||
return Math.max(0, endMs - startMs);
|
||
}
|
||
|
||
function taskTiming(task: QueueTask): JsonValue {
|
||
const nowMs = Date.now();
|
||
const createdMs = timestampMs(task.createdAt);
|
||
const startedMs = timestampMs(task.startedAt);
|
||
const finishedMs = timestampMs(task.finishedAt);
|
||
const updatedMs = timestampMs(task.updatedAt);
|
||
const terminal = task.status === "succeeded" || task.status === "failed" || task.status === "canceled";
|
||
const effectiveEndMs = finishedMs ?? (terminal ? updatedMs : nowMs);
|
||
return {
|
||
queueWaitMs: nonNegativeElapsed(createdMs, startedMs ?? (terminal ? effectiveEndMs : nowMs)),
|
||
durationMs: nonNegativeElapsed(startedMs, effectiveEndMs),
|
||
totalElapsedMs: nonNegativeElapsed(createdMs, effectiveEndMs),
|
||
effectiveEndAt: finishedMs !== null ? task.finishedAt : terminal ? task.updatedAt : null,
|
||
running: !terminal && startedMs !== null,
|
||
};
|
||
}
|
||
|
||
const codexStatsTimeZone = "Asia/Shanghai";
|
||
const codexStatsDateFormatter = new Intl.DateTimeFormat("en-CA", {
|
||
timeZone: codexStatsTimeZone,
|
||
year: "numeric",
|
||
month: "2-digit",
|
||
day: "2-digit",
|
||
});
|
||
|
||
interface DailyTaskStatsBucket {
|
||
date: string;
|
||
executedTasks: number;
|
||
completedTasks: number;
|
||
retryAttempts: number;
|
||
succeededTasks: number;
|
||
failedTasks: number;
|
||
canceledTasks: number;
|
||
totalDurationMs: number;
|
||
durationSamples: number;
|
||
}
|
||
|
||
function codexStatsDateKey(value: string | Date | null | undefined): string | null {
|
||
const date = value instanceof Date ? value : typeof value === "string" && value.length > 0 ? new Date(value) : null;
|
||
if (date === null || Number.isNaN(date.getTime())) return null;
|
||
const parts = codexStatsDateFormatter.formatToParts(date).reduce<Record<string, string>>((memo, part) => {
|
||
if (part.type !== "literal") memo[part.type] = part.value;
|
||
return memo;
|
||
}, {});
|
||
const year = parts.year;
|
||
const month = parts.month;
|
||
const day = parts.day;
|
||
return year !== undefined && month !== undefined && day !== undefined ? `${year}-${month}-${day}` : null;
|
||
}
|
||
|
||
function shiftDateKey(dateKey: string, offsetDays: number): string {
|
||
const [year = "1970", month = "01", day = "01"] = dateKey.split("-");
|
||
const shifted = new Date(Date.UTC(Number(year), Number(month) - 1, Number(day) + offsetDays));
|
||
return shifted.toISOString().slice(0, 10);
|
||
}
|
||
|
||
function statsDaysFromUrl(url: URL): number {
|
||
const value = Number(url.searchParams.get("days") ?? 14);
|
||
return Number.isInteger(value) && value > 0 ? Math.min(90, value) : 14;
|
||
}
|
||
|
||
function emptyDailyTaskStatsBucket(date: string): DailyTaskStatsBucket {
|
||
return {
|
||
date,
|
||
executedTasks: 0,
|
||
completedTasks: 0,
|
||
retryAttempts: 0,
|
||
succeededTasks: 0,
|
||
failedTasks: 0,
|
||
canceledTasks: 0,
|
||
totalDurationMs: 0,
|
||
durationSamples: 0,
|
||
};
|
||
}
|
||
|
||
function retryAttemptDates(task: QueueTask): Array<string | null> {
|
||
const attempts = Array.isArray(task.attempts) ? task.attempts : [];
|
||
const retryAttempts = attempts.filter((attempt, index) => attempt.mode === "retry" || Number(attempt.index || 0) > 1 || index > 0);
|
||
const fallbackRetryCount = Math.max(0, Math.floor(Number(task.currentAttempt || 0)) - 1);
|
||
const fallbackAt = taskTimestamp(task.updatedAt) ?? taskTimestamp(task.startedAt) ?? taskTimestamp(task.createdAt);
|
||
return [
|
||
...retryAttempts.map((attempt) => taskTimestamp(attempt.startedAt) ?? taskTimestamp(attempt.finishedAt) ?? fallbackAt),
|
||
...Array.from({ length: Math.max(0, fallbackRetryCount - retryAttempts.length) }, () => fallbackAt),
|
||
];
|
||
}
|
||
|
||
function completedTaskDurationMs(task: QueueTask, finishedAt: string): number | null {
|
||
const startedAt = taskTimestamp(task.startedAt) ?? taskTimestamp(task.attempts[0]?.startedAt ?? null) ?? taskTimestamp(task.createdAt);
|
||
return durationMsBetween(startedAt, finishedAt);
|
||
}
|
||
|
||
function taskStatisticsSummary(tasks: QueueTask[], days = 14): JsonValue {
|
||
const generatedAt = ctx().nowIso();
|
||
const endDate = codexStatsDateKey(new Date()) ?? generatedAt.slice(0, 10);
|
||
const safeDays = Math.max(1, Math.min(90, Math.floor(days)));
|
||
const startDate = shiftDateKey(endDate, 1 - safeDays);
|
||
const buckets = new Map<string, DailyTaskStatsBucket>();
|
||
for (let offset = 0; offset < safeDays; offset += 1) {
|
||
const date = shiftDateKey(startDate, offset);
|
||
buckets.set(date, emptyDailyTaskStatsBucket(date));
|
||
}
|
||
|
||
const bucketFor = (value: string | null): DailyTaskStatsBucket | null => {
|
||
const date = codexStatsDateKey(value);
|
||
return date === null ? null : buckets.get(date) ?? null;
|
||
};
|
||
|
||
for (const task of tasks) {
|
||
const executedBucket = bucketFor(taskTimestamp(task.startedAt) ?? taskTimestamp(task.attempts[0]?.startedAt ?? null));
|
||
if (executedBucket !== null) executedBucket.executedTasks += 1;
|
||
|
||
for (const retryAt of retryAttemptDates(task)) {
|
||
const retryBucket = bucketFor(retryAt);
|
||
if (retryBucket !== null) retryBucket.retryAttempts += 1;
|
||
}
|
||
|
||
if (!terminalTask(task)) continue;
|
||
const finishedAt = taskTimestamp(task.finishedAt) ?? taskTimestamp(task.updatedAt);
|
||
const completedBucket = bucketFor(finishedAt);
|
||
if (finishedAt === null || completedBucket === null) continue;
|
||
completedBucket.completedTasks += 1;
|
||
if (task.status === "succeeded") completedBucket.succeededTasks += 1;
|
||
if (task.status === "failed") completedBucket.failedTasks += 1;
|
||
if (task.status === "canceled") completedBucket.canceledTasks += 1;
|
||
const durationMs = completedTaskDurationMs(task, finishedAt);
|
||
if (durationMs !== null) {
|
||
completedBucket.totalDurationMs += durationMs;
|
||
completedBucket.durationSamples += 1;
|
||
}
|
||
}
|
||
|
||
const daily = Array.from(buckets.values()).map((bucket) => ({
|
||
date: bucket.date,
|
||
executedTasks: bucket.executedTasks,
|
||
completedTasks: bucket.completedTasks,
|
||
retryAttempts: bucket.retryAttempts,
|
||
succeededTasks: bucket.succeededTasks,
|
||
failedTasks: bucket.failedTasks,
|
||
canceledTasks: bucket.canceledTasks,
|
||
avgDurationMs: bucket.durationSamples > 0 ? Math.round(bucket.totalDurationMs / bucket.durationSamples) : null,
|
||
totalDurationMs: bucket.totalDurationMs,
|
||
durationSamples: bucket.durationSamples,
|
||
}));
|
||
const totals = daily.reduce((memo, day) => {
|
||
memo.executedTasks += day.executedTasks;
|
||
memo.completedTasks += day.completedTasks;
|
||
memo.retryAttempts += day.retryAttempts;
|
||
memo.succeededTasks += day.succeededTasks;
|
||
memo.failedTasks += day.failedTasks;
|
||
memo.canceledTasks += day.canceledTasks;
|
||
memo.totalDurationMs += day.totalDurationMs;
|
||
memo.durationSamples += day.durationSamples;
|
||
return memo;
|
||
}, {
|
||
executedTasks: 0,
|
||
completedTasks: 0,
|
||
retryAttempts: 0,
|
||
succeededTasks: 0,
|
||
failedTasks: 0,
|
||
canceledTasks: 0,
|
||
totalDurationMs: 0,
|
||
durationSamples: 0,
|
||
});
|
||
return {
|
||
generatedAt,
|
||
timezone: codexStatsTimeZone,
|
||
days: safeDays,
|
||
range: { startDate, endDate },
|
||
totals: {
|
||
...totals,
|
||
avgDurationMs: totals.durationSamples > 0 ? Math.round(totals.totalDurationMs / totals.durationSamples) : null,
|
||
},
|
||
daily,
|
||
} as unknown as JsonValue;
|
||
}
|
||
|
||
function transcriptLine(kind: TranscriptKind, at: string, seq: number, title: string, rawSeqs: number[], body = "", command = "", status?: string, fullText = false): TranscriptLine {
|
||
const bodyInfo = fullText ? completeTraceText(body) : kind === "message" ? fullMessageBody(body) : kind === "edited" ? editedOutputPreview(body) : outputPreview(body);
|
||
const commandInfo = command.length > 0 ? fullText ? completeTraceText(displayCommand(command)) : commandPreview(command) : { text: "", omittedLines: 0 };
|
||
return {
|
||
seq,
|
||
at,
|
||
kind,
|
||
title,
|
||
status,
|
||
commandPreview: commandInfo.text || undefined,
|
||
commandOmittedLines: commandInfo.omittedLines || undefined,
|
||
bodyPreview: bodyInfo.text || undefined,
|
||
bodyOmittedLines: bodyInfo.omittedLines || undefined,
|
||
rawSeqs,
|
||
};
|
||
}
|
||
|
||
function pushUniqueRawSeq(rawSeqs: number[], seq: number): void {
|
||
if (!rawSeqs.includes(seq)) rawSeqs.push(seq);
|
||
}
|
||
|
||
function messageFragmentMergeKey(line: TranscriptLine): string | null {
|
||
if (line.kind !== "message") return null;
|
||
const title = String(line.title || "").trim().toLowerCase();
|
||
if (title !== "assistant message" && title !== "reasoning") return null;
|
||
return title;
|
||
}
|
||
|
||
function appendMessageText(left: string, right: string): string {
|
||
if (left.length === 0) return right;
|
||
if (right.length === 0) return left;
|
||
if (!/\s$/u.test(left) && !/^\s|^[,.;:!?)}\]]/u.test(right) && /[A-Za-z0-9]/u.test(left.at(-1) || "") && /[A-Za-z0-9]/u.test(right[0] || "")) return `${left} ${right}`;
|
||
return `${left}${right}`;
|
||
}
|
||
|
||
function mergeTranscriptMessageGroup(group: TranscriptLine[]): TranscriptLine {
|
||
const first = group[0];
|
||
const last = group[group.length - 1] || first;
|
||
const body = group.reduce((text, line) => appendMessageText(text, String(line.bodyPreview || "")), "");
|
||
const rawSeqs: number[] = [];
|
||
for (const line of group) {
|
||
for (const seq of Array.isArray(line.rawSeqs) ? line.rawSeqs : []) pushUniqueRawSeq(rawSeqs, seq);
|
||
}
|
||
return {
|
||
...first,
|
||
seq: Number.isFinite(Number(last.seq)) ? Number(last.seq) : Number(first.seq),
|
||
at: last.at || first.at,
|
||
status: last.status || first.status,
|
||
bodyPreview: body.length > 0 ? body : undefined,
|
||
bodyOmittedLines: group.reduce((sum, line) => sum + Number(line.bodyOmittedLines || 0), 0) || undefined,
|
||
rawSeqs,
|
||
};
|
||
}
|
||
|
||
function coalesceTranscriptMessageFragments(entries: TranscriptLine[]): TranscriptLine[] {
|
||
const rows = sortTranscript([...entries]);
|
||
const merged: TranscriptLine[] = [];
|
||
let group: TranscriptLine[] = [];
|
||
let groupKey: string | null = null;
|
||
const flush = () => {
|
||
if (group.length === 0) return;
|
||
merged.push(group.length === 1 ? group[0] : mergeTranscriptMessageGroup(group));
|
||
group = [];
|
||
groupKey = null;
|
||
};
|
||
for (const line of rows) {
|
||
const key = messageFragmentMergeKey(line);
|
||
if (key !== null && key === groupKey) {
|
||
group.push(line);
|
||
continue;
|
||
}
|
||
flush();
|
||
if (key !== null) {
|
||
group = [line];
|
||
groupKey = key;
|
||
} else {
|
||
merged.push(line);
|
||
}
|
||
}
|
||
flush();
|
||
return merged;
|
||
}
|
||
|
||
function traceMessageOverlayKey(line: TranscriptLine): string | null {
|
||
return messageFragmentMergeKey(line);
|
||
}
|
||
|
||
function traceMessageOverlayCandidate(line: TranscriptLine): boolean {
|
||
return traceMessageOverlayKey(line) !== null && String(line.bodyPreview || "").trim().length > 0;
|
||
}
|
||
|
||
function overlayTraceMessagesFromRawTranscript(oaLines: TranscriptLine[], rawLines: TranscriptLine[]): TranscriptLine[] {
|
||
const rawMessages = rawLines.filter(traceMessageOverlayCandidate);
|
||
if (rawMessages.length === 0) return oaLines;
|
||
const messageSeqs = new Set<number>();
|
||
for (const line of oaLines) {
|
||
if (traceMessageOverlayKey(line) === null) continue;
|
||
for (const seq of Array.isArray(line.rawSeqs) && line.rawSeqs.length > 0 ? line.rawSeqs : [line.seq]) {
|
||
if (Number.isFinite(Number(seq))) messageSeqs.add(Number(seq));
|
||
}
|
||
}
|
||
const result = oaLines.filter((line) => traceMessageOverlayKey(line) === null);
|
||
for (const raw of rawMessages) {
|
||
const seqs = Array.isArray(raw.rawSeqs) && raw.rawSeqs.length > 0 ? raw.rawSeqs : [raw.seq];
|
||
if (!seqs.some((seq) => messageSeqs.has(Number(seq)))) continue;
|
||
result.push(raw);
|
||
}
|
||
return coalesceTranscriptMessageFragments(result);
|
||
}
|
||
|
||
|
||
function commandKind(command: string): TranscriptKind {
|
||
if (/\bwebSearch\b/u.test(command)) return "explored";
|
||
if (/\b(apply_patch|git apply|cat >|tee .*<<|sed -i|python3? .*write_text)\b/u.test(command)) return "edited";
|
||
if (/\b(rg|grep|find|ls|cat|sed -n|tail|head|git status|git diff|ps)\b/u.test(command)) return "explored";
|
||
return "ran";
|
||
}
|
||
|
||
function commandKindLabel(kind: TranscriptKind): string {
|
||
if (kind === "edited") return "Edited";
|
||
if (kind === "explored") return "Explored";
|
||
return "Ran";
|
||
}
|
||
|
||
function jsonPreview(value: unknown, max = 4000): string {
|
||
try {
|
||
return safePreview(JSON.stringify(value), max);
|
||
} catch {
|
||
return safePreview(String(value ?? ""), max);
|
||
}
|
||
}
|
||
|
||
function recordDisplayField(record: Record<string, unknown> | null, keys: string[], max = 3000): string {
|
||
if (record === null) return "";
|
||
for (const key of keys) {
|
||
const value = record[key];
|
||
if (typeof value === "string") return value;
|
||
if (value !== undefined && value !== null) return jsonPreview(value, max);
|
||
}
|
||
return "";
|
||
}
|
||
|
||
function decodeJsonStringFragment(value: string): string {
|
||
try {
|
||
return JSON.parse(`"${value}"`) as string;
|
||
} catch {
|
||
return value
|
||
.replace(/\\"/gu, "\"")
|
||
.replace(/\\\\/gu, "\\")
|
||
.replace(/\\n/gu, "\n")
|
||
.replace(/\\r/gu, "\r")
|
||
.replace(/\\t/gu, "\t");
|
||
}
|
||
}
|
||
|
||
function partialJsonStringField(text: string, key: string): string {
|
||
const match = new RegExp(`"${key}"\\s*:\\s*"`, "u").exec(text);
|
||
if (match === null) return "";
|
||
const start = match.index + match[0].length;
|
||
let escaped = false;
|
||
for (let index = start; index < text.length; index += 1) {
|
||
const char = text[index] ?? "";
|
||
if (escaped) {
|
||
escaped = false;
|
||
continue;
|
||
}
|
||
if (char === "\\") {
|
||
escaped = true;
|
||
continue;
|
||
}
|
||
if (char === "\"") return decodeJsonStringFragment(text.slice(start, index));
|
||
}
|
||
return decodeJsonStringFragment(text.slice(start).replace(/\s*\.\.\.\s*$/u, ""));
|
||
}
|
||
|
||
function partialJsonNumberField(text: string, key: string): number | null {
|
||
const match = new RegExp(`"${key}"\\s*:\\s*(-?\\d+(?:\\.\\d+)?)`, "u").exec(text);
|
||
if (match === null) return null;
|
||
const value = Number(match[1]);
|
||
return Number.isFinite(value) ? value : null;
|
||
}
|
||
|
||
function openCodePartialRecordFromOutput(item: LiveOutput): Record<string, unknown> | null {
|
||
if (!String(item.method || "").startsWith("opencode/")) return null;
|
||
const text = item.text.trim();
|
||
if (!text.includes("\"part\"") && !text.includes("\"tool\"")) return null;
|
||
const tool = partialJsonStringField(text, "tool");
|
||
const command = partialJsonStringField(text, "command") || partialJsonStringField(text, "cmd") || partialJsonStringField(text, "script");
|
||
const output = partialJsonStringField(text, "output") || partialJsonStringField(text, "result");
|
||
if (tool.length === 0 && command.length === 0 && output.length === 0) return null;
|
||
const input: Record<string, unknown> = {};
|
||
for (const key of ["command", "cmd", "script", "filePath", "filepath", "path", "pattern", "query", "title", "description"]) {
|
||
const value = partialJsonStringField(text, key);
|
||
if (value.length > 0) input[key] = value;
|
||
}
|
||
for (const key of ["offset", "limit"]) {
|
||
const value = partialJsonNumberField(text, key);
|
||
if (value !== null) input[key] = value;
|
||
}
|
||
return {
|
||
type: partialJsonStringField(text, "type") || "tool_use",
|
||
part: {
|
||
type: "tool",
|
||
tool: tool || "tool",
|
||
state: {
|
||
status: partialJsonStringField(text, "status"),
|
||
input,
|
||
output,
|
||
},
|
||
},
|
||
};
|
||
}
|
||
|
||
function openCodeRecordFromOutput(item: LiveOutput): Record<string, unknown> | null {
|
||
if (!String(item.method || "").startsWith("opencode/")) return null;
|
||
try {
|
||
return extractRecord(JSON.parse(item.text) as unknown);
|
||
} catch {
|
||
return openCodePartialRecordFromOutput(item);
|
||
}
|
||
}
|
||
|
||
function openCodeToolInputCommand(tool: string, input: Record<string, unknown> | null): string {
|
||
const command = recordStringField(input, ["command", "cmd", "script"]);
|
||
if (command.length > 0) return command;
|
||
const path = recordStringField(input, ["filePath", "filepath", "path"]);
|
||
const pattern = recordStringField(input, ["pattern", "query"]);
|
||
const offset = recordNumberField(input, ["offset"]);
|
||
const limit = recordNumberField(input, ["limit"]);
|
||
const args: string[] = [tool];
|
||
if (pattern.length > 0) args.push(pattern);
|
||
if (path.length > 0) args.push(path);
|
||
if (offset !== null) args.push(`offset=${offset}`);
|
||
if (limit !== null) args.push(`limit=${limit}`);
|
||
if (args.length > 1) return args.join(" ");
|
||
return input === null ? tool : `${tool} ${jsonPreview(input, 1200)}`;
|
||
}
|
||
|
||
function openCodeToolKind(tool: string, command: string): TranscriptKind {
|
||
const normalized = `${tool} ${command}`.toLowerCase();
|
||
if (/\b(read|grep|glob|list|ls|find|search|view|cat|sed|rg)\b/u.test(normalized)) return "explored";
|
||
if (/\b(edit|write|patch|apply|update|create|delete|apply_patch|git apply|sed -i)\b/u.test(normalized)) return "edited";
|
||
return commandKind(command);
|
||
}
|
||
|
||
function openCodeToolTitle(tool: string, command: string, input: Record<string, unknown> | null): string {
|
||
const title = recordStringField(input, ["title", "description"]);
|
||
if (title.length > 0) return safePreview(title, 180);
|
||
const path = recordStringField(input, ["filePath", "filepath", "path"]);
|
||
if (path.length > 0) return `${commandKindLabel(openCodeToolKind(tool, command))} ${path}`;
|
||
return shortCommandTitle(command || tool);
|
||
}
|
||
|
||
function openCodeToolDurationMs(part: Record<string, unknown> | null, state: Record<string, unknown> | null): number | undefined {
|
||
const explicit = recordNumberField(part, ["durationMs", "elapsedMs"]) ?? recordNumberField(state, ["durationMs", "elapsedMs"]);
|
||
if (explicit !== null && explicit >= 0) return explicit;
|
||
const time = extractRecord(state?.time) ?? extractRecord(part?.time);
|
||
const start = recordNumberField(time, ["start", "startedAt"]);
|
||
const end = recordNumberField(time, ["end", "finishedAt", "completedAt"]);
|
||
return start !== null && end !== null && end >= start ? end - start : undefined;
|
||
}
|
||
|
||
function isOpenCodeStepBoundaryMethod(method: string | undefined): boolean {
|
||
return method === "opencode/step-start" || method === "opencode/step-finish";
|
||
}
|
||
|
||
function openCodeVisibleAssistantText(text: string): string {
|
||
return String(text || "").replace(/<(?:think|thinking)\b[^>]*>[\s\S]*?<\/(?:think|thinking)>/giu, "").trim();
|
||
}
|
||
|
||
function openCodeToolTranscriptLine(item: LiveOutput, fullText: boolean): TranscriptLine | null {
|
||
const record = openCodeRecordFromOutput(item);
|
||
const part = extractRecord(record?.part);
|
||
if (record === null || part === null) return null;
|
||
const recordType = recordStringField(record, ["type", "event", "name"]).toLowerCase();
|
||
const partType = recordStringField(part, ["type"]).toLowerCase();
|
||
if (recordType === "step_start" || recordType === "step-start" || partType === "step-start") return null;
|
||
if (recordType === "step_finish" || recordType === "step-finish" || partType === "step-finish") return null;
|
||
const state = extractRecord(part.state);
|
||
const input = extractRecord(state?.input) ?? extractRecord(part.input);
|
||
const tool = recordStringField(part, ["tool", "title"]) || recordStringField(record, ["type", "event", "name"]) || "tool";
|
||
const command = openCodeToolInputCommand(tool, input);
|
||
const metadata = extractRecord(state?.metadata);
|
||
const filediff = extractRecord(metadata?.filediff);
|
||
const rawOutput = recordDisplayField(state, ["output", "result"]) || recordDisplayField(part, ["output", "text", "content"]) || recordDisplayField(record, ["output", "text", "content"]);
|
||
const diffOutput = recordDisplayField(metadata, ["diff"]) || recordDisplayField(filediff, ["patch"]);
|
||
const status = recordStringField(state, ["status"]) || recordStringField(part, ["status"]) || recordStringField(record, ["status"]);
|
||
const kind = openCodeToolKind(tool, command);
|
||
const body = kind === "edited" && diffOutput.length > 0 ? diffOutput : rawOutput.length > 0 ? rawOutput : jsonPreview(record, 3000);
|
||
const line = transcriptLine(kind, item.at, item.seq, openCodeToolTitle(tool, command, input), [item.seq], body, command, status || item.method, fullText);
|
||
const durationMs = openCodeToolDurationMs(part, state);
|
||
if (durationMs !== undefined) line.durationMs = durationMs;
|
||
return line;
|
||
}
|
||
|
||
function promptLineCount(text: string): number {
|
||
return text.length > 0 ? text.split(/\r\n|\r|\n/u).length : 0;
|
||
}
|
||
|
||
function promptSnapshot(text: string, maxPreviewChars = 1200): { text: string; preview: string; chars: number; lines: number; truncated: boolean } {
|
||
const normalized = text.trimEnd();
|
||
const preview = safePreview(normalized, maxPreviewChars);
|
||
return {
|
||
text: normalized,
|
||
preview,
|
||
chars: normalized.length,
|
||
lines: promptLineCount(normalized),
|
||
truncated: preview.length < normalized.length,
|
||
};
|
||
}
|
||
|
||
function setAttemptInputPrompt(attempt: AttemptSummary, prompt: string): void {
|
||
const snapshot = promptSnapshot(prompt, 1200);
|
||
if (snapshot.chars === 0) return;
|
||
attempt.inputPrompt = snapshot.text;
|
||
attempt.inputPromptPreview = snapshot.preview;
|
||
attempt.inputPromptChars = snapshot.chars;
|
||
attempt.inputPromptLines = snapshot.lines;
|
||
}
|
||
|
||
function setAttemptFeedbackPrompt(attempt: AttemptSummary | undefined, prompt: string, source: string, forAttempt: number | null): void {
|
||
if (attempt === undefined) return;
|
||
const snapshot = promptSnapshot(prompt, 1600);
|
||
if (snapshot.chars === 0) return;
|
||
attempt.feedbackPrompt = snapshot.text;
|
||
attempt.feedbackPromptPreview = snapshot.preview;
|
||
attempt.feedbackPromptChars = snapshot.chars;
|
||
attempt.feedbackPromptLines = snapshot.lines;
|
||
attempt.feedbackPromptSource = source;
|
||
attempt.feedbackPromptForAttempt = forAttempt;
|
||
}
|
||
|
||
function taskInitialPromptLine(task: QueueTask, fullText = false): TranscriptLine | null {
|
||
const prompt = (task.basePrompt || userPromptForDisplay(task.prompt)).trimEnd();
|
||
if (prompt.length === 0) return null;
|
||
const line = transcriptLine("message", task.createdAt, 0.5, "Submitted prompt", [], prompt, "", "enqueue", fullText);
|
||
const fullPrompt = task.prompt.trimEnd();
|
||
if (fullText && fullPrompt.length > 0 && fullPrompt !== prompt) {
|
||
line.fullPrompt = fullPrompt;
|
||
line.fullPromptLines = promptLineCount(fullPrompt);
|
||
line.fullPromptChars = fullPrompt.length;
|
||
line.foldedReferencePrompt = true;
|
||
}
|
||
return line;
|
||
}
|
||
|
||
function promptHistoryTranscriptLines(task: QueueTask, fullText = false): TranscriptLine[] {
|
||
return ctx().mergePromptHistory([...(Array.isArray(task.promptHistory) ? task.promptHistory : []), ...ctx().outputPromptHistory(task)])
|
||
.map((item) => transcriptLine("message", item.at, item.seq, "Steer prompt", [item.seq], item.text, "", item.method, fullText));
|
||
}
|
||
|
||
function sortTranscript(entries: TranscriptLine[]): TranscriptLine[] {
|
||
return entries.sort((left, right) => Number(left.seq) - Number(right.seq));
|
||
}
|
||
|
||
function boundedTranscript(entries: TranscriptLine[], limit: number): TranscriptLine[] {
|
||
sortTranscript(entries);
|
||
if (entries.length <= limit) return entries;
|
||
const first = entries[0];
|
||
if (first?.title === "Submitted prompt" && first.rawSeqs.length === 0 && limit > 1) {
|
||
return [first, ...entries.slice(-(limit - 1))];
|
||
}
|
||
return entries.slice(-limit);
|
||
}
|
||
|
||
function buildTaskTranscript(task: QueueTask, limit = 180, rawOutputWindow = 0, fullText = false): TranscriptLine[] {
|
||
const entries: TranscriptLine[] = [];
|
||
const initialPrompt = taskInitialPromptLine(task, fullText);
|
||
if (initialPrompt !== null) entries.push(initialPrompt);
|
||
const promptHistoryLines = promptHistoryTranscriptLines(task, fullText);
|
||
const promptHistorySeqs = new Set(promptHistoryLines.map((line) => line.seq));
|
||
entries.push(...promptHistoryLines);
|
||
type ActiveCommand = { seq: number; at: string; command: string; status?: string; body: string; rawSeqs: number[]; itemId?: string };
|
||
let activeCommand: ActiveCommand | null = null;
|
||
const activeCommandsByItemId = new Map<string, ActiveCommand>();
|
||
|
||
const pushRawSeq = (command: ActiveCommand, seq: number): void => {
|
||
if (!command.rawSeqs.includes(seq)) command.rawSeqs.push(seq);
|
||
};
|
||
|
||
const flushCommand = (command: ActiveCommand | null = activeCommand): void => {
|
||
if (command === null) return;
|
||
const kind = commandKind(command.command);
|
||
const output = command.itemId === undefined ? null : commandOutputs.get(command.itemId) ?? null;
|
||
const body = command.body.length > 0 ? command.body : formatCommandOutput(output);
|
||
const title = command.command.trim().length > 0 ? shortCommandTitle(command.command) : "Command output";
|
||
entries.push(addCommandOutputStreams(transcriptLine(
|
||
kind,
|
||
command.at,
|
||
command.seq,
|
||
title,
|
||
command.rawSeqs,
|
||
body,
|
||
command.command,
|
||
command.status,
|
||
fullText,
|
||
), output, fullText));
|
||
if (command === activeCommand) activeCommand = null;
|
||
if (command.itemId !== undefined && activeCommandsByItemId.get(command.itemId) === command) {
|
||
activeCommandsByItemId.delete(command.itemId);
|
||
}
|
||
};
|
||
|
||
const itemIdCommand = (item: LiveOutput, parsed: { command: string; status?: string } | null = null): ActiveCommand | null => {
|
||
if (typeof item.itemId !== "string" || item.itemId.length === 0) return null;
|
||
let command = activeCommandsByItemId.get(item.itemId);
|
||
if (command === undefined) {
|
||
command = {
|
||
seq: item.seq,
|
||
at: item.at,
|
||
command: parsed?.command ?? "",
|
||
status: parsed?.status,
|
||
body: "",
|
||
rawSeqs: [],
|
||
itemId: item.itemId,
|
||
};
|
||
activeCommandsByItemId.set(item.itemId, command);
|
||
}
|
||
return command;
|
||
};
|
||
|
||
const outputSource = rawOutputWindow > 0 ? task.output : taskFullOutput(task);
|
||
const outputItems = rawOutputWindow > 0 && outputSource.length > rawOutputWindow
|
||
? outputSource.slice(-rawOutputWindow)
|
||
: outputSource;
|
||
const commandOutputs = outputItems.some((item) => item.channel === "command" && typeof item.itemId === "string")
|
||
? codexSessionCommandOutputsByCallId(task)
|
||
: new Map<string, SessionCommandOutput>();
|
||
const fileChangeInputs = outputItems.some((item) => item.channel === "diff" && item.method === "item/fileChange/outputDelta" && typeof item.itemId === "string")
|
||
? codexSessionFileChangesByCallId(task)
|
||
: new Map<string, SessionFileChange>();
|
||
type ActiveMessage = { seq: number; at: string; title: string; status?: string; body: string; rawSeqs: number[] };
|
||
type ActiveCodexTool = { seq: number; at: string; text: string; status?: string; rawSeqs: number[]; itemId?: string };
|
||
let activeMessage: ActiveMessage | null = null;
|
||
const activeCodexToolsByItemId = new Map<string, ActiveCodexTool>();
|
||
|
||
const flushMessage = (): void => {
|
||
if (activeMessage === null) return;
|
||
entries.push(transcriptLine("message", activeMessage.at, activeMessage.seq, activeMessage.title, activeMessage.rawSeqs, activeMessage.body, "", activeMessage.status, fullText));
|
||
activeMessage = null;
|
||
};
|
||
|
||
const appendMessage = (item: LiveOutput, title: string, body: string, status?: string): void => {
|
||
if (body.length === 0) return;
|
||
if (activeMessage !== null && activeMessage.title === title && activeMessage.status === status) {
|
||
activeMessage.body += body;
|
||
activeMessage.at = item.at;
|
||
activeMessage.seq = item.seq;
|
||
pushUniqueRawSeq(activeMessage.rawSeqs, item.seq);
|
||
return;
|
||
}
|
||
flushMessage();
|
||
activeMessage = { seq: item.seq, at: item.at, title, status, body, rawSeqs: [item.seq] };
|
||
};
|
||
|
||
const parseCodexToolLifecycle = (item: LiveOutput): { status: string | undefined; text: string } => {
|
||
const status = /\bstatus=([A-Za-z0-9_-]+)/u.exec(item.text)?.[1];
|
||
return { status, text: String(item.text || "").trimEnd() };
|
||
};
|
||
|
||
const codexToolLifecycleLine = (tool: ActiveCodexTool): TranscriptLine => {
|
||
const kind = commandKind(tool.text);
|
||
return transcriptLine(kind, tool.at, tool.seq, shortCommandTitle(tool.text), tool.rawSeqs, "", tool.text, tool.status, fullText);
|
||
};
|
||
|
||
const flushCodexTool = (tool: ActiveCodexTool): void => {
|
||
entries.push(codexToolLifecycleLine(tool));
|
||
if (tool.itemId !== undefined && activeCodexToolsByItemId.get(tool.itemId) === tool) activeCodexToolsByItemId.delete(tool.itemId);
|
||
};
|
||
|
||
for (const item of outputItems) {
|
||
if (initialPrompt !== null && item.channel === "user" && item.method === "enqueue") continue;
|
||
if (item.channel === "user" && item.method === "turn/steer" && promptHistorySeqs.has(item.seq)) continue;
|
||
if (isOpenCodeStepBoundaryMethod(item.method)) continue;
|
||
if (item.channel === "command" && item.method === "item/started") {
|
||
flushMessage();
|
||
const parsed = parseCommandLine(item.text);
|
||
const groupedCommand = itemIdCommand(item, parsed);
|
||
if (groupedCommand !== null) {
|
||
flushCommand();
|
||
groupedCommand.seq = item.seq;
|
||
groupedCommand.at = item.at;
|
||
groupedCommand.command = parsed?.command || item.text;
|
||
groupedCommand.status = parsed?.status;
|
||
pushRawSeq(groupedCommand, item.seq);
|
||
continue;
|
||
}
|
||
flushCommand();
|
||
activeCommand = {
|
||
seq: item.seq,
|
||
at: item.at,
|
||
command: parsed?.command || item.text,
|
||
status: parsed?.status,
|
||
body: "",
|
||
rawSeqs: [item.seq],
|
||
...(typeof item.itemId === "string" ? { itemId: item.itemId } : {}),
|
||
};
|
||
continue;
|
||
}
|
||
if (item.channel === "command" && item.method === "item/commandExecution/outputDelta") {
|
||
flushMessage();
|
||
const groupedCommand = itemIdCommand(item);
|
||
if (groupedCommand !== null) {
|
||
groupedCommand.body += item.text;
|
||
pushRawSeq(groupedCommand, item.seq);
|
||
} else if (activeCommand !== null) {
|
||
activeCommand.body += item.text;
|
||
pushRawSeq(activeCommand, item.seq);
|
||
} else {
|
||
const output = typeof item.itemId === "string" ? commandOutputs.get(item.itemId) ?? null : null;
|
||
entries.push(addCommandOutputStreams(transcriptLine("ran", item.at, item.seq, "Command output", [item.seq], item.text || formatCommandOutput(output), "", undefined, fullText), output, fullText));
|
||
}
|
||
continue;
|
||
}
|
||
if (item.channel === "command" && item.method === "item/completed") {
|
||
flushMessage();
|
||
const parsed = parseCommandLine(item.text);
|
||
const groupedCommand = itemIdCommand(item, parsed);
|
||
if (groupedCommand !== null) {
|
||
if (parsed?.command) groupedCommand.command = parsed.command;
|
||
groupedCommand.status = parsed?.status ?? groupedCommand.status;
|
||
pushRawSeq(groupedCommand, item.seq);
|
||
flushCommand(groupedCommand);
|
||
} else if (activeCommand !== null) {
|
||
activeCommand.status = parsed?.status ?? activeCommand.status;
|
||
pushRawSeq(activeCommand, item.seq);
|
||
flushCommand();
|
||
} else {
|
||
const command = parsed?.command || item.text;
|
||
const kind = commandKind(command);
|
||
const output = typeof item.itemId === "string" ? commandOutputs.get(item.itemId) ?? null : null;
|
||
entries.push(addCommandOutputStreams(transcriptLine(kind, item.at, item.seq, shortCommandTitle(command), [item.seq], formatCommandOutput(output), command, parsed?.status, fullText), output, fullText));
|
||
}
|
||
continue;
|
||
}
|
||
|
||
if (item.channel !== "assistant" && item.channel !== "reasoning") flushMessage();
|
||
flushCommand();
|
||
if (isCodexToolLifecycleOutput(item)) {
|
||
const parsed = parseCodexToolLifecycle(item);
|
||
const itemId = item.itemId || "";
|
||
const existing = activeCodexToolsByItemId.get(itemId);
|
||
if (item.method === "item/started") {
|
||
if (existing !== undefined) flushCodexTool(existing);
|
||
activeCodexToolsByItemId.set(itemId, {
|
||
seq: item.seq,
|
||
at: item.at,
|
||
text: parsed.text,
|
||
status: parsed.status ?? item.method,
|
||
rawSeqs: [item.seq],
|
||
itemId,
|
||
});
|
||
} else if (existing !== undefined) {
|
||
existing.at = item.at;
|
||
existing.status = parsed.status ?? existing.status;
|
||
existing.text = parsed.text.length > 0 ? parsed.text : existing.text;
|
||
pushUniqueRawSeq(existing.rawSeqs, item.seq);
|
||
flushCodexTool(existing);
|
||
} else {
|
||
entries.push(codexToolLifecycleLine({
|
||
seq: item.seq,
|
||
at: item.at,
|
||
text: parsed.text,
|
||
status: parsed.status ?? item.method,
|
||
rawSeqs: [item.seq],
|
||
itemId,
|
||
}));
|
||
}
|
||
} else if (item.channel === "diff") {
|
||
const text = fileChangeTextWithInlinePatch(item, fileChangeInputs);
|
||
entries.push(transcriptLine("edited", item.at, item.seq, "Edited files", [item.seq], text, "", item.method, fullText));
|
||
} else if (item.channel === "error") {
|
||
entries.push(transcriptLine("error", item.at, item.seq, "Error", [item.seq], item.text, "", item.method, fullText));
|
||
} else if (item.channel === "assistant") {
|
||
const body = String(item.method || "").startsWith("opencode/") ? openCodeVisibleAssistantText(item.text) : item.text;
|
||
appendMessage(item, "Assistant message", body, item.method);
|
||
} else if (item.channel === "reasoning") {
|
||
appendMessage(item, "Reasoning", item.text, item.method);
|
||
} else if (item.channel === "user") {
|
||
entries.push(transcriptLine("message", item.at, item.seq, item.method === "enqueue" ? "Submitted prompt" : "User prompt", [item.seq], item.text, "", item.method, fullText));
|
||
} else if (item.channel === "tool" && String(item.method || "").startsWith("opencode/")) {
|
||
entries.push(openCodeToolTranscriptLine(item, fullText) ?? transcriptLine("system", item.at, item.seq, "OpenCode tool", [item.seq], item.text, "", item.method, fullText));
|
||
} else {
|
||
const title = item.method === "queue" && item.text.startsWith("attempt ")
|
||
? "Attempt started"
|
||
: item.method === "startup" || item.method === "shutdown"
|
||
? "Recovered thread execution"
|
||
: item.method === "judge"
|
||
? "Judge result"
|
||
: "System";
|
||
entries.push(transcriptLine("system", item.at, item.seq, title, [item.seq], item.text, "", item.method, fullText));
|
||
}
|
||
}
|
||
flushMessage();
|
||
flushCommand();
|
||
for (const command of Array.from(activeCommandsByItemId.values()).sort((left, right) => left.seq - right.seq)) {
|
||
flushCommand(command);
|
||
}
|
||
for (const tool of Array.from(activeCodexToolsByItemId.values()).sort((left, right) => left.seq - right.seq)) {
|
||
flushCodexTool(tool);
|
||
}
|
||
return boundedTranscript(coalesceTranscriptMessageFragments(entries), limit);
|
||
}
|
||
|
||
function buildCompactTaskTranscript(task: QueueTask, limit = 12, rawOutputWindow = 24): TranscriptLine[] {
|
||
return buildTaskTranscript(task, limit, rawOutputWindow, false);
|
||
}
|
||
|
||
function transcriptSignature(task: QueueTask): string {
|
||
const last = task.output.at(-1);
|
||
return `${task.updatedAt}:${task.output.length}:${last?.seq ?? 0}:${last?.text.length ?? 0}:${outputArchiveSignature(task)}:${task.status}:${task.createdAt}:${task.basePrompt.length}:${task.prompt.length}:${task.promptHistory.length}:${task.promptHistory.at(-1)?.seq ?? 0}`;
|
||
}
|
||
|
||
function cachedTranscript(task: QueueTask, fullText: boolean): TranscriptLine[] {
|
||
const signature = transcriptSignature(task);
|
||
const cached = transcriptCache.get(task.id);
|
||
if (cached?.signature === signature) {
|
||
const cachedTranscript = fullText ? cached.fullTranscript : cached.previewTranscript;
|
||
if (cachedTranscript !== undefined) return cachedTranscript;
|
||
}
|
||
const transcript = buildTaskTranscript(task, Number.MAX_SAFE_INTEGER, 0, fullText);
|
||
const next: { signature: string; previewTranscript?: TranscriptLine[]; fullTranscript?: TranscriptLine[] } = cached?.signature === signature ? cached : { signature };
|
||
if (fullText) next.fullTranscript = transcript;
|
||
else next.previewTranscript = transcript;
|
||
transcriptCache.set(task.id, next);
|
||
if (transcriptCache.size > 80) {
|
||
const firstKey = transcriptCache.keys().next().value;
|
||
if (typeof firstKey === "string") transcriptCache.delete(firstKey);
|
||
}
|
||
return transcript;
|
||
}
|
||
|
||
function cachedFullTranscript(task: QueueTask): TranscriptLine[] {
|
||
return cachedTranscript(task, true);
|
||
}
|
||
|
||
function cachedPreviewTranscript(task: QueueTask): TranscriptLine[] {
|
||
return cachedTranscript(task, false);
|
||
}
|
||
|
||
function outputForResponse(task: QueueTask, includeRaw: boolean): LiveOutput[] {
|
||
if (includeRaw) return taskFullOutput(task);
|
||
return task.output.slice(-80).map((item) => ({ ...item, text: safePreview(item.text, 4000) }));
|
||
}
|
||
|
||
function attemptForResponse(attempt: AttemptSummary, full = false): JsonValue {
|
||
const finalResponse = String(attempt.finalResponse ?? "");
|
||
const inputPrompt = String(attempt.inputPrompt ?? "");
|
||
const feedbackPrompt = String(attempt.feedbackPrompt ?? "");
|
||
return {
|
||
...attempt,
|
||
inputPrompt: full ? inputPrompt : undefined,
|
||
inputPromptPreview: safePreview(String(attempt.inputPromptPreview || inputPrompt), full ? 3000 : 1200),
|
||
inputPromptChars: Number(attempt.inputPromptChars ?? inputPrompt.length),
|
||
inputPromptLines: Number(attempt.inputPromptLines ?? promptLineCount(inputPrompt)),
|
||
finalResponse: full ? finalResponse : safePreview(finalResponse, 1200),
|
||
finalResponsePreview: safePreview(String(attempt.finalResponsePreview || finalResponse), full ? 3000 : 1200),
|
||
finalResponseChars: Number(attempt.finalResponseChars ?? finalResponse.length),
|
||
feedbackPrompt: full ? feedbackPrompt : undefined,
|
||
feedbackPromptPreview: safePreview(String(attempt.feedbackPromptPreview || feedbackPrompt), full ? 3000 : 1200),
|
||
feedbackPromptChars: Number(attempt.feedbackPromptChars ?? feedbackPrompt.length),
|
||
feedbackPromptLines: Number(attempt.feedbackPromptLines ?? promptLineCount(feedbackPrompt)),
|
||
stderrTail: full ? attempt.stderrTail : safePreview(attempt.stderrTail, 1200),
|
||
} as unknown as JsonValue;
|
||
}
|
||
|
||
function taskForResponse(task: QueueTask, full = false, includeRaw = full): JsonValue {
|
||
const displayPrompt = task.basePrompt || userPromptForDisplay(task.prompt);
|
||
return {
|
||
...task,
|
||
agentPort: codeAgentPortForModel(task.model),
|
||
agentPortInfo: codeAgentPortInfo(codeAgentPortForModel(task.model)),
|
||
judgeFailRetryLimit,
|
||
stepCount: null,
|
||
llmStepCount: null,
|
||
traceStats: null,
|
||
statsSource: "unavailable",
|
||
prompt: full ? task.prompt : safePreview(task.prompt, 2000),
|
||
basePrompt: full ? task.basePrompt : safePreview(task.basePrompt, 2000),
|
||
displayPrompt: full ? displayPrompt : safePreview(displayPrompt, 2000),
|
||
promptEditable: ctx().queuedTaskPromptEditable(task),
|
||
...queuedStatusPayload(task),
|
||
referenceTaskIds: task.referenceTaskIds,
|
||
referenceInjection: task.referenceInjection,
|
||
finalResponse: full ? task.finalResponse : safePreview(task.finalResponse, 5000),
|
||
terminalUnread: terminalTaskUnread(task),
|
||
attempts: task.attempts.map((attempt) => attemptForResponse(attempt, full)),
|
||
output: outputForResponse(task, includeRaw),
|
||
events: includeRaw ? task.events : task.events.slice(-120),
|
||
transcript: full ? fullTranscript(task) : buildTaskTranscript(task, 120, 0),
|
||
} as unknown as JsonValue;
|
||
}
|
||
|
||
function fullTranscript(task: QueueTask): TranscriptLine[] {
|
||
return cachedFullTranscript(task);
|
||
}
|
||
|
||
function nonNegativeInteger(value: unknown): number | null {
|
||
const parsed = Number(value);
|
||
return Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : null;
|
||
}
|
||
|
||
function retainedOutputStartsTraceStep(output: LiveOutput): boolean {
|
||
if (output.channel === "diff" || output.channel === "tool") return true;
|
||
if (output.channel !== "command") return false;
|
||
const method = String(output.method || "");
|
||
return method === "item/started" || (method !== "item/commandExecution/outputDelta" && method !== "item/completed");
|
||
}
|
||
|
||
function retainedTaskStepCount(task: QueueTask): number {
|
||
return (Array.isArray(task.output) ? task.output : []).reduce((count, output) => count + (retainedOutputStartsTraceStep(output) ? 1 : 0), 0);
|
||
}
|
||
|
||
function taskStoredStepCount(task: QueueTask): number | null {
|
||
return nonNegativeInteger(task.stepCount) ?? nonNegativeInteger(task.llmStepCount);
|
||
}
|
||
|
||
// Used only as a producer-side seed for OA stats snapshots; frontend/API
|
||
// display authority is the oa_trace_stats projection applied at route edges.
|
||
function taskListStepCount(task: QueueTask): number {
|
||
return taskStoredStepCount(task) ?? retainedTaskStepCount(task);
|
||
}
|
||
|
||
function taskOutputMaxSeq(task: QueueTask): number {
|
||
const output = Array.isArray(task.output) ? task.output : [];
|
||
const promptHistory = Array.isArray(task.promptHistory) ? task.promptHistory : [];
|
||
const attempts = Array.isArray(task.attempts) ? task.attempts : [];
|
||
const values = [
|
||
nonNegativeInteger(task.outputMaxSeq),
|
||
nonNegativeInteger(output.at(-1)?.seq),
|
||
nonNegativeInteger(promptHistory.at(-1)?.seq),
|
||
...attempts.map((attempt) => nonNegativeInteger(attempt.outputEndSeq)),
|
||
].filter((value): value is number => value !== null);
|
||
return values.length > 0 ? Math.max(...values) : 0;
|
||
}
|
||
|
||
function taskForMetaResponse(task: QueueTask): JsonValue {
|
||
const lastOutputSeq = taskOutputMaxSeq(task);
|
||
const displayPrompt = task.basePrompt || userPromptForDisplay(task.prompt);
|
||
return {
|
||
id: task.id,
|
||
queueId: ctx().queueIdOf(task),
|
||
queueEnteredAt: ctx().taskQueueEnteredAt(task),
|
||
prompt: task.prompt,
|
||
basePrompt: task.basePrompt,
|
||
displayPrompt,
|
||
promptChars: task.prompt.length,
|
||
basePromptChars: task.basePrompt.length,
|
||
displayPromptChars: displayPrompt.length,
|
||
promptEditable: ctx().queuedTaskPromptEditable(task),
|
||
finalResponseChars: task.finalResponse.length,
|
||
stepCount: null,
|
||
llmStepCount: null,
|
||
traceStats: null,
|
||
statsSource: "unavailable",
|
||
summaryOnly: false,
|
||
referenceTaskIds: task.referenceTaskIds,
|
||
referenceInjection: task.referenceInjection,
|
||
providerId: task.providerId,
|
||
executionMode: task.executionMode,
|
||
executionModeInfo: codeExecutionModeInfo(task.executionMode),
|
||
cwd: task.cwd,
|
||
model: task.model,
|
||
agentPort: codeAgentPortForModel(task.model),
|
||
agentPortInfo: codeAgentPortInfo(codeAgentPortForModel(task.model)),
|
||
reasoningEffort: task.reasoningEffort,
|
||
maxAttempts: task.maxAttempts,
|
||
status: task.status,
|
||
...queuedStatusPayload(task),
|
||
createdAt: task.createdAt,
|
||
updatedAt: task.updatedAt,
|
||
startedAt: task.startedAt,
|
||
finishedAt: task.finishedAt,
|
||
readAt: task.readAt,
|
||
currentAttempt: task.currentAttempt,
|
||
currentMode: task.currentMode,
|
||
judgeFailCount: task.judgeFailCount,
|
||
judgeFailRetryLimit,
|
||
codexThreadId: task.codexThreadId,
|
||
activeTurnId: task.activeTurnId,
|
||
finalResponse: task.finalResponse,
|
||
lastError: task.lastError,
|
||
lastJudge: task.lastJudge,
|
||
promptHistory: task.promptHistory,
|
||
attempts: task.attempts,
|
||
cancelRequested: task.cancelRequested,
|
||
terminalUnread: terminalTaskUnread(task),
|
||
nextMode: task.nextMode,
|
||
outputCount: task.output.length,
|
||
retainedOutputCount: task.output.length,
|
||
eventCount: task.events.length,
|
||
transcriptCount: null,
|
||
transcriptMaxSeq: lastOutputSeq,
|
||
timing: taskTiming(task),
|
||
transcript: [],
|
||
output: [],
|
||
events: [],
|
||
} as unknown as JsonValue;
|
||
}
|
||
|
||
function taskForCompactMetaResponse(task: QueueTask): JsonValue {
|
||
const displayPrompt = task.basePrompt || userPromptForDisplay(task.prompt);
|
||
const lastOutputSeq = taskOutputMaxSeq(task);
|
||
return {
|
||
id: task.id,
|
||
queueId: ctx().queueIdOf(task),
|
||
queueEnteredAt: ctx().taskQueueEnteredAt(task),
|
||
prompt: prefixPreview(task.prompt, 900),
|
||
basePrompt: prefixPreview(task.basePrompt, 900),
|
||
displayPrompt: prefixPreview(displayPrompt, 900),
|
||
promptChars: task.prompt.length,
|
||
basePromptChars: task.basePrompt.length,
|
||
displayPromptChars: displayPrompt.length,
|
||
promptEditable: ctx().queuedTaskPromptEditable(task),
|
||
finalResponseChars: task.finalResponse.length,
|
||
stepCount: null,
|
||
llmStepCount: null,
|
||
traceStats: null,
|
||
statsSource: "unavailable",
|
||
summaryOnly: true,
|
||
referenceTaskIds: task.referenceTaskIds,
|
||
referenceInjection: task.referenceInjection === null ? null : {
|
||
injectedAt: task.referenceInjection.injectedAt,
|
||
itemCount: task.referenceInjection.itemCount,
|
||
directReferenceTaskIds: task.referenceInjection.directReferenceTaskIds,
|
||
maxRounds: task.referenceInjection.maxRounds,
|
||
truncated: task.referenceInjection.truncated,
|
||
},
|
||
providerId: task.providerId,
|
||
executionMode: task.executionMode,
|
||
executionModeInfo: codeExecutionModeInfo(task.executionMode),
|
||
cwd: task.cwd,
|
||
model: task.model,
|
||
agentPort: codeAgentPortForModel(task.model),
|
||
agentPortInfo: codeAgentPortInfo(codeAgentPortForModel(task.model)),
|
||
reasoningEffort: task.reasoningEffort,
|
||
maxAttempts: task.maxAttempts,
|
||
status: task.status,
|
||
...queuedStatusPayload(task),
|
||
createdAt: task.createdAt,
|
||
updatedAt: task.updatedAt,
|
||
startedAt: task.startedAt,
|
||
finishedAt: task.finishedAt,
|
||
readAt: task.readAt,
|
||
currentAttempt: task.currentAttempt,
|
||
currentMode: task.currentMode,
|
||
judgeFailCount: task.judgeFailCount,
|
||
judgeFailRetryLimit,
|
||
codexThreadId: task.codexThreadId,
|
||
activeTurnId: task.activeTurnId,
|
||
finalResponse: prefixPreview(task.finalResponse, 1200),
|
||
lastError: task.lastError,
|
||
lastJudge: task.lastJudge,
|
||
promptHistory: task.promptHistory.slice(-8),
|
||
attempts: task.attempts.slice(-3).map((attempt) => attemptForResponse(attempt, false)),
|
||
cancelRequested: task.cancelRequested,
|
||
terminalUnread: terminalTaskUnread(task),
|
||
nextMode: task.nextMode,
|
||
outputCount: task.output.length,
|
||
eventCount: task.events.length,
|
||
transcriptCount: null,
|
||
transcriptMaxSeq: lastOutputSeq,
|
||
timing: taskTiming(task),
|
||
transcript: [],
|
||
output: [],
|
||
events: [],
|
||
} as unknown as JsonValue;
|
||
}
|
||
|
||
function lastAssistantMessage(task: QueueTask, transcript = fullTranscript(task)): JsonValue {
|
||
const assistantLine = transcript.slice().reverse().find((line) => line.kind === "message" && line.title === "Assistant message");
|
||
const text = task.finalResponse.trim().length > 0 ? task.finalResponse.trim() : String(assistantLine?.bodyPreview ?? "").trim();
|
||
return {
|
||
text,
|
||
at: assistantLine?.at ?? task.finishedAt ?? task.updatedAt,
|
||
seq: assistantLine?.seq ?? null,
|
||
source: task.finalResponse.trim().length > 0 ? "finalResponse" : assistantLine !== undefined ? "transcript" : "none",
|
||
};
|
||
}
|
||
|
||
function parseToolLimit(url: URL): number {
|
||
const value = Number(url.searchParams.get("toolLimit") ?? 160);
|
||
return Number.isInteger(value) && value > 0 ? Math.min(500, value) : 160;
|
||
}
|
||
|
||
function taskToolSummaryForLimit(task: QueueTask, limit: number, transcript = fullTranscript(task)): JsonValue {
|
||
const allTools = transcript.filter((line) => line.kind === "ran" || line.kind === "explored" || line.kind === "edited");
|
||
const boundedLimit = Math.max(1, Math.min(500, Math.floor(limit)));
|
||
const start = Math.max(0, allTools.length - boundedLimit);
|
||
return {
|
||
count: allTools.length,
|
||
returned: allTools.length - start,
|
||
limit: boundedLimit,
|
||
truncated: start > 0,
|
||
items: allTools.slice(start).map((line) => ({
|
||
seq: line.seq,
|
||
at: line.at,
|
||
kind: line.kind,
|
||
title: line.title,
|
||
status: line.status ?? null,
|
||
commandPreview: line.commandPreview ?? "",
|
||
commandOmittedLines: line.commandOmittedLines ?? 0,
|
||
outputPreview: line.bodyPreview ?? "",
|
||
outputOmittedLines: line.bodyOmittedLines ?? 0,
|
||
rawSeqs: line.rawSeqs,
|
||
})),
|
||
} as unknown as JsonValue;
|
||
}
|
||
|
||
function taskToolSummary(task: QueueTask, url: URL, transcript = fullTranscript(task)): JsonValue {
|
||
return taskToolSummaryForLimit(task, parseToolLimit(url), transcript);
|
||
}
|
||
|
||
function uniqueStrings(values: string[], limit = 20): string[] {
|
||
const result: string[] = [];
|
||
for (const value of values) {
|
||
const normalized = value.trim();
|
||
if (normalized.length === 0 || result.includes(normalized)) continue;
|
||
result.push(normalized);
|
||
if (result.length >= limit) break;
|
||
}
|
||
return result;
|
||
}
|
||
|
||
function cleanTracePath(value: string): string {
|
||
return value
|
||
.replace(/^['"`([{<]+/u, "")
|
||
.replace(/['"`)\]}>.,;:]+$/u, "")
|
||
.replace(/:\d+(?::\d+)?$/u, "")
|
||
.replace(/^[ab]\//u, "")
|
||
.trim();
|
||
}
|
||
|
||
function extractTracePaths(text: string): string[] {
|
||
const source = String(text || "");
|
||
const matches = source.match(/(?:~|\.{1,2}|\/)?(?:[A-Za-z0-9_.@+-]+\/)+[A-Za-z0-9_.@+-]+|[A-Za-z0-9_.@+-]+\.(?:c|cc|cpp|h|hpp|js|jsx|ts|tsx|json|md|py|sh|toml|ya?ml|txt|log|lock)/gu) || [];
|
||
return uniqueStrings(matches.map(cleanTracePath).filter((path) => path.length >= 2 && !path.includes("...") && !/^(http|https|status|method)$/iu.test(path)), 40);
|
||
}
|
||
|
||
function parseEditedFilesFromText(text: string): string[] {
|
||
const files: string[] = [];
|
||
const addFile = (path: string): void => {
|
||
const cleanPath = cleanTracePath(path);
|
||
if (cleanPath.length === 0 || cleanPath === "/dev/null" || files.includes(cleanPath)) return;
|
||
files.push(cleanPath);
|
||
};
|
||
for (const line of text.split(/\r?\n/u)) {
|
||
const statusMatch = /^([AMDRCU?]{1,2})\s+(.+)$/u.exec(line);
|
||
if (statusMatch) {
|
||
addFile(statusMatch[2] || "");
|
||
continue;
|
||
}
|
||
const patchMatch = /^\*\*\*\s+(?:Add|Update|Delete)\s+File:\s+(.+)$/u.exec(line);
|
||
if (patchMatch) {
|
||
addFile(patchMatch[1] || "");
|
||
continue;
|
||
}
|
||
const moveMatch = /^\*\*\*\s+Move to:\s+(.+)$/u.exec(line);
|
||
if (moveMatch) {
|
||
addFile(moveMatch[1] || "");
|
||
continue;
|
||
}
|
||
const diffMatch = /^diff --git a\/(.+?) b\/(.+)$/u.exec(line);
|
||
if (diffMatch) {
|
||
addFile(diffMatch[2] || diffMatch[1] || "");
|
||
continue;
|
||
}
|
||
const plusMatch = /^\+\+\+ b\/(.+)$/u.exec(line);
|
||
if (plusMatch) addFile(plusMatch[1] || "");
|
||
}
|
||
return files.length > 0 ? files : extractTracePaths(text);
|
||
}
|
||
|
||
function transcriptPreviewLines(text: string, maxLines: number, maxChars: number): string[] {
|
||
const preview = linePreview(text, maxLines, maxChars).text;
|
||
return preview.length === 0 ? [] : preview.split(/\r?\n/u).slice(0, maxLines);
|
||
}
|
||
|
||
function transcriptLineSummaryLines(line: TranscriptLine): string[] {
|
||
const lines: string[] = [];
|
||
const command = String(line.commandPreview || "").trim();
|
||
if (command.length > 0) {
|
||
for (const item of transcriptPreviewLines(command, 2, 420)) lines.push(`$ ${item}`);
|
||
}
|
||
const body = String(line.bodyPreview || "").trim();
|
||
const remaining = Math.max(0, 4 - lines.length);
|
||
if (body.length > 0 && remaining > 0) {
|
||
for (const item of transcriptPreviewLines(body, remaining, 700)) lines.push(item);
|
||
}
|
||
if (lines.length === 0) lines.push(line.status ? `${line.title} (${line.status})` : line.title);
|
||
return lines.slice(0, 4);
|
||
}
|
||
|
||
function isToolActionLine(line: TranscriptLine): boolean {
|
||
return line.kind === "ran" || line.kind === "explored" || line.kind === "edited";
|
||
}
|
||
|
||
function toolStepCountsFromTranscript(lines: TranscriptLine[]): { toolCallCount: number; readCount: number; editCount: number; runCount: number } {
|
||
const counts = lines.reduce((memo, line) => {
|
||
if (line.kind === "explored") memo.readCount += 1;
|
||
else if (line.kind === "edited") memo.editCount += 1;
|
||
else if (line.kind === "ran") memo.runCount += 1;
|
||
return memo;
|
||
}, { readCount: 0, editCount: 0, runCount: 0 });
|
||
return {
|
||
...counts,
|
||
toolCallCount: counts.readCount + counts.editCount + counts.runCount,
|
||
};
|
||
}
|
||
|
||
function realAttemptWindows(task: QueueTask, transcript: TranscriptLine[]): TraceAttemptWindow[] {
|
||
return traceAttemptWindows(task, transcript).filter((window) => window.synthetic !== true && window.index > 0);
|
||
}
|
||
|
||
function taskExecutionTranscript(task: QueueTask, transcript: TranscriptLine[]): TranscriptLine[] {
|
||
const windows = realAttemptWindows(task, transcript);
|
||
if (windows.length === 0) return transcript;
|
||
return sortTranscript(windows.flatMap((window) => executionLinesForAttempt(window.lines)));
|
||
}
|
||
|
||
function executionSummaryFromTranscript(
|
||
task: QueueTask,
|
||
transcript: TranscriptLine[],
|
||
timing: Record<string, JsonValue>,
|
||
outputCount = taskFullOutput(task).length,
|
||
retainedOutputCount = task.output.length,
|
||
): JsonValue {
|
||
const toolLines = transcript.filter(isToolActionLine);
|
||
const editedFiles = uniqueStrings(toolLines.flatMap((line) => line.kind === "edited" ? parseEditedFilesFromText(`${line.title}\n${line.bodyPreview ?? ""}`) : []), 40);
|
||
const commands = uniqueStrings(toolLines
|
||
.map((line) => String(line.commandPreview || "").split(/\r?\n/u).find((row) => row.trim().length > 0)?.trim() ?? "")
|
||
.filter(Boolean), 30);
|
||
const counts = toolStepCountsFromTranscript(transcript);
|
||
return {
|
||
durationMs: timing.durationMs ?? timing.totalElapsedMs ?? null,
|
||
totalElapsedMs: timing.totalElapsedMs ?? null,
|
||
queueWaitMs: timing.queueWaitMs ?? null,
|
||
toolCallCount: counts.toolCallCount,
|
||
readCount: counts.readCount,
|
||
editCount: counts.editCount,
|
||
runCount: counts.runCount,
|
||
editedFiles,
|
||
commands,
|
||
stepCount: counts.toolCallCount,
|
||
llmStepCount: counts.toolCallCount,
|
||
traceLineCount: transcript.filter((line) => line.title !== "Submitted prompt").length,
|
||
transcriptMaxSeq: transcript.at(-1)?.seq ?? 0,
|
||
outputCount,
|
||
retainedOutputCount,
|
||
} as unknown as JsonValue;
|
||
}
|
||
|
||
function taskExecutionSummary(task: QueueTask, transcript = cachedPreviewTranscript(task)): JsonValue {
|
||
return executionSummaryFromTranscript(task, taskExecutionTranscript(task, transcript), taskTiming(task) as Record<string, JsonValue>);
|
||
}
|
||
|
||
function parseAttemptIndex(text: string): number | null {
|
||
const match = /\battempt\s+(\d+)\s*\/\s*\d+/iu.exec(text);
|
||
if (match === null) return null;
|
||
const value = Number(match[1]);
|
||
return Number.isInteger(value) && value > 0 ? value : null;
|
||
}
|
||
|
||
function traceAttemptIndexFromLine(line: TranscriptLine): number | null {
|
||
return parseAttemptIndex(`${line.bodyPreview ?? ""}\n${line.commandPreview ?? ""}\n${line.title}`);
|
||
}
|
||
|
||
function parseJudgeLine(text: string): JudgeResult | null {
|
||
const match = /\bjudge=(complete|retry|fail)\s+confidence=([0-9.]+)\s+source=([A-Za-z0-9_-]+):\s*([\s\S]*)$/u.exec(text.trim());
|
||
if (match === null) return null;
|
||
const confidence = Number(match[2]);
|
||
const source = match[3] === "minimax" ? "minimax" : "fallback";
|
||
const rawReason = (match[4] ?? "").trim();
|
||
const detailsMatch = /\bMiniMax failure details:\s+([\s\S]*)$/u.exec(rawReason);
|
||
const detailsText = detailsMatch?.[1] ?? "";
|
||
const detailValue = (key: string): string | null => new RegExp(`\\b${key}=([^\\s]+)`, "u").exec(detailsText)?.[1] ?? null;
|
||
const detailNumber = (key: string): number | undefined => {
|
||
const value = Number(detailValue(key));
|
||
return Number.isFinite(value) ? value : undefined;
|
||
};
|
||
const errorMessage = /\berror=([\s\S]*)$/u.exec(detailsText)?.[1]?.trim() ?? "";
|
||
const failureDetails = detailsText.length === 0 ? null : {
|
||
provider: "minimax" as const,
|
||
stage: (detailValue("stage") || "unknown") as JudgeResult["failureDetails"] extends infer T ? T extends { stage: infer S } ? S : never : never,
|
||
model: "",
|
||
apiBase: "",
|
||
occurredAt: "",
|
||
durationMs: detailNumber("durationMs") ?? 0,
|
||
timeoutMs: detailNumber("timeoutMs") ?? 0,
|
||
timedOut: detailValue("timedOut") === "true",
|
||
errorName: detailValue("errorName") || "",
|
||
errorMessage,
|
||
promptChars: detailNumber("promptChars"),
|
||
promptLines: detailNumber("promptLines"),
|
||
payloadBytes: detailNumber("payloadBytes"),
|
||
responseStatus: detailNumber("responseStatus") ?? null,
|
||
};
|
||
return {
|
||
decision: match[1] as JudgeDecision,
|
||
confidence: Number.isFinite(confidence) ? confidence : 0,
|
||
source,
|
||
reason: detailsMatch === null ? rawReason : rawReason.slice(0, detailsMatch.index).trim(),
|
||
failureDetails,
|
||
};
|
||
}
|
||
|
||
function judgeFromAttemptLines(lines: TranscriptLine[]): { judge: JudgeResult; at: string | null; seq: number | null } | null {
|
||
for (const line of lines.slice().reverse()) {
|
||
if (line.title !== "Judge result" && line.status !== "judge") continue;
|
||
const judge = parseJudgeLine(String(line.bodyPreview || ""));
|
||
if (judge === null) continue;
|
||
return { judge, at: line.at || null, seq: Number.isFinite(Number(line.seq)) ? Number(line.seq) : null };
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function attemptFeedbackPromptRecord(task: QueueTask, attemptIndex: number, attempt: AttemptSummary | null, judge: JudgeResult | null = attempt?.judge ?? null): FeedbackPromptRecord | null {
|
||
const directPrompt = String(attempt?.feedbackPrompt ?? "").trimEnd();
|
||
if (directPrompt.length > 0) {
|
||
const snapshot = promptSnapshot(directPrompt, 1600);
|
||
return {
|
||
text: snapshot.text,
|
||
preview: String(attempt?.feedbackPromptPreview || snapshot.preview),
|
||
chars: Number(attempt?.feedbackPromptChars ?? snapshot.chars),
|
||
lines: Number(attempt?.feedbackPromptLines ?? snapshot.lines),
|
||
source: String(attempt?.feedbackPromptSource || "judge-feedback"),
|
||
forAttempt: Number.isFinite(Number(attempt?.feedbackPromptForAttempt)) ? Number(attempt?.feedbackPromptForAttempt) : attemptIndex + 1,
|
||
truncated: snapshot.truncated,
|
||
};
|
||
}
|
||
|
||
const hasPendingRetry = task.status === "retry_wait" || task.status === "queued";
|
||
if (attemptIndex === task.attempts.length && hasPendingRetry && task.nextMode === "retry" && task.nextPrompt !== null && task.nextPrompt.trim().length > 0) {
|
||
const snapshot = promptSnapshot(task.nextPrompt, 1600);
|
||
return { ...snapshot, source: "pending-retry", forAttempt: attemptIndex + 1 };
|
||
}
|
||
|
||
const nextAttempt = task.attempts.find((item) => Number(item.index) === attemptIndex + 1) ?? task.attempts[attemptIndex] ?? null;
|
||
const nextInput = String(nextAttempt?.inputPrompt ?? "").trimEnd();
|
||
if (nextInput.length > 0) {
|
||
const snapshot = promptSnapshot(nextInput, 1600);
|
||
return { ...snapshot, source: "attempt-input", forAttempt: attemptIndex + 1 };
|
||
}
|
||
|
||
if (judge?.decision === "retry") {
|
||
const generated = retryPrompt(task, judge);
|
||
const snapshot = promptSnapshot(generated, 1600);
|
||
return { ...snapshot, source: judge.continuePrompt?.trim() ? "judge-continue-prompt" : "judge-retry-generated", forAttempt: attemptIndex + 1 };
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
function attemptTimingSummary(attempt: AttemptSummary | null, lines: TranscriptLine[]): Record<string, JsonValue> {
|
||
const startedAt = attempt?.startedAt || lines[0]?.at || null;
|
||
const finishedAt = attempt?.finishedAt || lines.at(-1)?.at || null;
|
||
const startedMs = timestampMs(startedAt);
|
||
const finishedMs = timestampMs(finishedAt);
|
||
const durationMs = nonNegativeElapsed(startedMs, finishedMs);
|
||
return {
|
||
durationMs,
|
||
totalElapsedMs: durationMs,
|
||
queueWaitMs: null,
|
||
effectiveEndAt: finishedAt,
|
||
};
|
||
}
|
||
|
||
interface TraceAttemptWindow {
|
||
index: number;
|
||
attempt: AttemptSummary | null;
|
||
startSeq: number | null;
|
||
endSeq: number | null;
|
||
lines: TranscriptLine[];
|
||
synthetic?: boolean;
|
||
label?: string;
|
||
}
|
||
|
||
type TraceStatsLookup = Map<string, JsonValue> | Record<string, JsonValue> | null | undefined;
|
||
|
||
function transcriptLineSeq(line: TranscriptLine | undefined): number | null {
|
||
const value = Number(line?.seq ?? NaN);
|
||
return Number.isFinite(value) ? value : null;
|
||
}
|
||
|
||
function traceWindowFirstSeq(window: TraceAttemptWindow): number {
|
||
return transcriptLineSeq(window.lines[0]) ?? window.startSeq ?? Number.POSITIVE_INFINITY;
|
||
}
|
||
|
||
function isRecoveredThreadLine(line: TranscriptLine): boolean {
|
||
const status = String(line.status || "");
|
||
const text = `${line.title}\n${line.bodyPreview ?? ""}\n${line.commandPreview ?? ""}`;
|
||
return line.kind === "system" && (
|
||
line.title === "Recovered thread execution"
|
||
|| line.title === "Queue recovered"
|
||
|| status === "startup"
|
||
|| status === "shutdown"
|
||
|| /\btask queued for retry\b|Service (?:re)?started while task was active|Service stopping while task was active/iu.test(text)
|
||
);
|
||
}
|
||
|
||
function traceSystemLineIsError(line: TranscriptLine): boolean {
|
||
const text = [
|
||
line.title,
|
||
line.status,
|
||
line.bodyPreview,
|
||
line.commandPreview,
|
||
line.stderrPreview,
|
||
line.stdoutPreview,
|
||
].map((value) => String(value || "")).join("\n");
|
||
return /\b(error|failed|failure|interrupt|interrupted|cancell?ed|watchdog|timeout|closed|refused|aborted|exception)\b/iu.test(text);
|
||
}
|
||
|
||
function traceLineVisibleInTraceView(line: TranscriptLine): boolean {
|
||
if (line.title === "Submitted prompt") return false;
|
||
return line.kind !== "system" || traceSystemLineIsError(line);
|
||
}
|
||
|
||
function oaTraceStepKind(kind: string): TranscriptKind {
|
||
const normalized = String(kind || "").trim().toLowerCase();
|
||
if (normalized === "read" || normalized === "explored" || normalized === "explore") return "explored";
|
||
if (normalized === "edit" || normalized === "edited") return "edited";
|
||
if (normalized === "run" || normalized === "ran" || normalized === "command") return "ran";
|
||
if (normalized === "error" || normalized === "failed") return "error";
|
||
if (normalized === "message" || normalized === "assistant" || normalized === "user") return "message";
|
||
return "system";
|
||
}
|
||
|
||
function oaTraceStepToTranscriptLine(step: OaTraceStepSummary): TranscriptLine {
|
||
const kind = oaTraceStepKind(step.kind);
|
||
const summaryText = step.summaryLines.join("\n").trimEnd();
|
||
const commandText = summaryText.startsWith("item/") ? summaryText : "";
|
||
return {
|
||
seq: step.seq,
|
||
at: step.at,
|
||
kind,
|
||
title: step.title || (kind === "explored" ? "Read" : kind === "edited" ? "Edit" : kind === "ran" ? "Run" : "Trace step"),
|
||
status: step.status || undefined,
|
||
commandPreview: commandText.length > 0 ? commandText : undefined,
|
||
bodyPreview: commandText.length > 0 ? undefined : summaryText || undefined,
|
||
rawSeqs: step.rawSeqs,
|
||
};
|
||
}
|
||
|
||
function isCodexToolLifecycleTranscriptLine(line: TranscriptLine): boolean {
|
||
const text = `${line.commandPreview ?? ""}\n${line.bodyPreview ?? ""}\n${line.title}`;
|
||
const status = String(line.status || "");
|
||
return (line.kind === "explored" || line.kind === "ran")
|
||
&& (status === "item/started" || status === "item/completed" || /^item\/(?:started|completed):/u.test(text))
|
||
&& /\b(?:webSearch|mcpToolCall|dynamicToolCall)\b/u.test(text);
|
||
}
|
||
|
||
function mergeCodexToolLifecycleGroup(group: TranscriptLine[]): TranscriptLine {
|
||
if (group.length <= 1) return group[0];
|
||
const first = group[0];
|
||
const last = group.at(-1) || first;
|
||
const rawSeqs: number[] = [];
|
||
for (const line of group) {
|
||
for (const seq of Array.isArray(line.rawSeqs) ? line.rawSeqs : [line.seq]) pushUniqueRawSeq(rawSeqs, Number(seq));
|
||
}
|
||
const command = String(last.commandPreview || first.commandPreview || last.bodyPreview || first.bodyPreview || last.title || first.title || "");
|
||
return {
|
||
...first,
|
||
seq: Number.isFinite(Number(last.seq)) ? Number(last.seq) : Number(first.seq),
|
||
at: last.at || first.at,
|
||
kind: commandKind(command),
|
||
title: shortCommandTitle(command) || String(last.title || first.title || "WebSearch"),
|
||
status: last.status || first.status,
|
||
commandPreview: command || undefined,
|
||
commandOmittedLines: Number(first.commandOmittedLines || 0) + Number(last.commandOmittedLines || 0) || undefined,
|
||
bodyPreview: last.bodyPreview || first.bodyPreview,
|
||
bodyOmittedLines: Number(first.bodyOmittedLines || 0) + Number(last.bodyOmittedLines || 0) || undefined,
|
||
rawSeqs,
|
||
};
|
||
}
|
||
|
||
function coalesceCodexToolLifecycleTranscriptLines(lines: TranscriptLine[]): TranscriptLine[] {
|
||
const rows = sortTranscript([...lines]);
|
||
const merged: TranscriptLine[] = [];
|
||
let group: TranscriptLine[] = [];
|
||
const flush = () => {
|
||
if (group.length > 0) merged.push(mergeCodexToolLifecycleGroup(group));
|
||
group = [];
|
||
};
|
||
for (const line of rows) {
|
||
if (isCodexToolLifecycleTranscriptLine(line)) {
|
||
const text = String(line.commandPreview || line.bodyPreview || "");
|
||
if ((line.status === "item/started" || /^item\/started:/u.test(text)) && group.length > 0) flush();
|
||
group.push(line);
|
||
if (line.status === "item/completed" || /^item\/completed:/u.test(text)) flush();
|
||
continue;
|
||
}
|
||
flush();
|
||
merged.push(line);
|
||
}
|
||
flush();
|
||
return merged;
|
||
}
|
||
|
||
async function oaTraceTranscriptForTask(task: QueueTask, attemptIndex: number | null): Promise<TranscriptLine[]> {
|
||
const taskId = task.id;
|
||
const steps = await readOaTraceStepsForTask(taskId, attemptIndex);
|
||
const oaLines = coalesceCodexToolLifecycleTranscriptLines(coalesceTranscriptMessageFragments(steps.map(oaTraceStepToTranscriptLine).filter(traceLineVisibleInTraceView)));
|
||
const rawLines = coalesceCodexToolLifecycleTranscriptLines(fullTranscript(task).filter(traceLineVisibleInTraceView));
|
||
return coalesceCodexToolLifecycleTranscriptLines(overlayTraceMessagesFromRawTranscript(oaLines, rawLines));
|
||
}
|
||
|
||
function mergeTraceWindowLines(left: TranscriptLine[], right: TranscriptLine[]): TranscriptLine[] {
|
||
const seen = new Set<string>();
|
||
const merged: TranscriptLine[] = [];
|
||
for (const line of [...left, ...right]) {
|
||
const key = `${line.seq}:${line.title}:${line.status ?? ""}`;
|
||
if (seen.has(key)) continue;
|
||
seen.add(key);
|
||
merged.push(line);
|
||
}
|
||
return sortTranscript(merged);
|
||
}
|
||
|
||
function minNullableSeq(left: number | null, right: number | null): number | null {
|
||
if (left === null) return right;
|
||
if (right === null) return left;
|
||
return Math.min(left, right);
|
||
}
|
||
|
||
function isSystemOnlyOrphanGroup(lines: TranscriptLine[]): boolean {
|
||
const executionLines = executionLinesForAttempt(lines);
|
||
return executionLines.length > 0 && executionLines.every((line) => line.kind === "system");
|
||
}
|
||
|
||
function mergeSystemGroupIntoNextAttempt(windows: TraceAttemptWindow[], lines: TranscriptLine[]): boolean {
|
||
if (!isSystemOnlyOrphanGroup(lines)) return false;
|
||
const groupEndSeq = transcriptLineSeq(lines.at(-1));
|
||
if (groupEndSeq === null) return false;
|
||
const target = windows
|
||
.filter((window) => window.synthetic !== true && traceWindowFirstSeq(window) > groupEndSeq)
|
||
.sort((left, right) => traceWindowFirstSeq(left) - traceWindowFirstSeq(right))[0];
|
||
if (target === undefined) return false;
|
||
target.lines = mergeTraceWindowLines(lines, target.lines);
|
||
target.startSeq = minNullableSeq(target.startSeq, transcriptLineSeq(lines[0]));
|
||
return true;
|
||
}
|
||
|
||
function traceAttemptWindows(task: QueueTask, transcript: TranscriptLine[]): TraceAttemptWindow[] {
|
||
const starts = transcript
|
||
.map((line, position) => ({ line, position, index: line.title === "Attempt started" ? traceAttemptIndexFromLine(line) : null }))
|
||
.filter((item): item is { line: TranscriptLine; position: number; index: number } => item.index !== null)
|
||
.sort((left, right) => Number(left.line.seq) - Number(right.line.seq));
|
||
const maxStartIndex = starts.reduce((max, item) => Math.max(max, item.index), 0);
|
||
const maxIndex = Math.max(task.attempts.length, task.currentAttempt || 0, maxStartIndex);
|
||
const windows: TraceAttemptWindow[] = [];
|
||
for (let index = 1; index <= maxIndex; index += 1) {
|
||
const attempt = task.attempts.find((item) => Number(item.index) === index) ?? task.attempts[index - 1] ?? null;
|
||
const start = starts.find((item) => item.index === index) ?? starts[index - 1] ?? null;
|
||
const nextStart = starts.find((item) => item.index > index) ?? null;
|
||
const explicitStartSeq = Number((attempt as AttemptSummary | null)?.outputStartSeq ?? NaN);
|
||
const explicitEndSeq = Number((attempt as AttemptSummary | null)?.outputEndSeq ?? NaN);
|
||
const startSeq = Number.isFinite(explicitStartSeq) ? explicitStartSeq : start?.line.seq ?? null;
|
||
const hasExplicitEndSeq = Number.isFinite(explicitEndSeq);
|
||
const endSeq = hasExplicitEndSeq ? explicitEndSeq : nextStart !== null ? nextStart.line.seq : null;
|
||
let lines = startSeq === null
|
||
? []
|
||
: transcript.filter((line) => Number(line.seq) >= startSeq && (endSeq === null || (hasExplicitEndSeq ? Number(line.seq) <= endSeq : Number(line.seq) < endSeq)));
|
||
if (lines.length === 0 && attempt !== null) {
|
||
const startedMs = timestampMs(attempt.startedAt);
|
||
const finishedMs = timestampMs(attempt.finishedAt);
|
||
lines = transcript.filter((line) => {
|
||
const lineMs = timestampMs(line.at);
|
||
return lineMs !== null
|
||
&& (startedMs === null || lineMs >= startedMs)
|
||
&& (finishedMs === null || lineMs <= finishedMs);
|
||
});
|
||
}
|
||
windows.push({ index, attempt, startSeq, endSeq, lines });
|
||
}
|
||
const coveredSeqs = new Set<number>();
|
||
for (const window of windows) {
|
||
for (const line of window.lines) coveredSeqs.add(Number(line.seq));
|
||
}
|
||
const orphanedGroups: TranscriptLine[][] = [];
|
||
let group: TranscriptLine[] = [];
|
||
const flushGroup = (): void => {
|
||
if (group.length > 0 && executionLinesForAttempt(group).length > 0) orphanedGroups.push(group);
|
||
group = [];
|
||
};
|
||
for (const line of transcript) {
|
||
const seq = Number(line.seq);
|
||
if (line.title === "Submitted prompt" || coveredSeqs.has(seq)) {
|
||
flushGroup();
|
||
continue;
|
||
}
|
||
group.push(line);
|
||
}
|
||
flushGroup();
|
||
|
||
let syntheticIndex = -1;
|
||
for (const lines of orphanedGroups) {
|
||
if (mergeSystemGroupIntoNextAttempt(windows, lines)) continue;
|
||
const hasSteer = lines.some((line) => line.title === "Steer prompt" || line.status === "turn/steer");
|
||
const hasRecovery = lines.some(isRecoveredThreadLine);
|
||
const startSeq = Number(lines[0]?.seq ?? NaN);
|
||
const endSeq = Number(lines.at(-1)?.seq ?? NaN);
|
||
windows.push({
|
||
index: syntheticIndex,
|
||
attempt: null,
|
||
startSeq: Number.isFinite(startSeq) ? startSeq : null,
|
||
endSeq: Number.isFinite(endSeq) ? endSeq : null,
|
||
lines,
|
||
synthetic: true,
|
||
label: hasRecovery
|
||
? hasSteer ? "Recovered thread execution with steer prompt" : "Recovered thread execution"
|
||
: hasSteer ? "Recovered thread execution with steer prompt" : "System events",
|
||
});
|
||
syntheticIndex -= 1;
|
||
}
|
||
|
||
return windows.sort((left, right) => Number(left.lines[0]?.seq ?? left.startSeq ?? 0) - Number(right.lines[0]?.seq ?? right.startSeq ?? 0));
|
||
}
|
||
|
||
function executionLinesForAttempt(lines: TranscriptLine[]): TranscriptLine[] {
|
||
return lines.filter((line) => line.title !== "Submitted prompt" && line.title !== "Attempt started" && line.title !== "Judge result");
|
||
}
|
||
|
||
function taskTraceAttemptSummaries(task: QueueTask, transcript: TranscriptLine[], oaTraceStatsByScope: TraceStatsLookup = null): JsonValue[] {
|
||
const windows = traceAttemptWindows(task, transcript);
|
||
return windows.map((window) => {
|
||
const attempt = window.attempt;
|
||
const parsedJudge = judgeFromAttemptLines(window.lines);
|
||
const storedJudge = attempt?.judge ?? null;
|
||
const synthetic = window.synthetic === true;
|
||
const judge = synthetic ? null : storedJudge ?? parsedJudge?.judge ?? (window.index === task.attempts.length && task.lastJudge !== null ? task.lastJudge : null);
|
||
const finalResponse = synthetic ? "" : String(attempt?.finalResponse ?? attempt?.finalResponsePreview ?? (window.index === task.attempts.length ? task.finalResponse : ""));
|
||
const finalResponseChars = Number(attempt?.finalResponseChars ?? finalResponse.length);
|
||
const executionLines = executionLinesForAttempt(window.lines);
|
||
const attemptStats = synthetic || window.index <= 0 ? null : traceStatsForScope(oaTraceStatsByScope, taskAttemptScopeId(task.id, window.index));
|
||
const stepCount = traceStatsNumber(attemptStats, "stepCount");
|
||
const readCount = traceStatsNumber(attemptStats, "readCount");
|
||
const editCount = traceStatsNumber(attemptStats, "editCount");
|
||
const runCount = traceStatsNumber(attemptStats, "runCount");
|
||
const errorCount = traceStatsNumber(attemptStats, "errorCount");
|
||
const feedbackPrompt = synthetic ? null : attemptFeedbackPromptRecord(task, window.index, attempt, judge);
|
||
const inputPrompt = promptSnapshot(String(attempt?.inputPrompt ?? ""), 1200);
|
||
return {
|
||
...(attempt ?? {}),
|
||
index: attempt?.index ?? window.index,
|
||
synthetic,
|
||
label: window.label ?? null,
|
||
mode: attempt?.mode ?? (window.index <= 1 ? "initial" : "retry"),
|
||
startedAt: attempt?.startedAt ?? window.lines[0]?.at ?? task.startedAt,
|
||
finishedAt: attempt?.finishedAt ?? null,
|
||
terminalStatus: attempt?.terminalStatus ?? null,
|
||
transportClosedBeforeTerminal: attempt?.transportClosedBeforeTerminal ?? false,
|
||
appServerExitCode: attempt?.appServerExitCode ?? null,
|
||
appServerSignal: attempt?.appServerSignal ?? null,
|
||
error: attempt?.error ?? null,
|
||
stderrTail: attempt?.stderrTail ?? "",
|
||
startSeq: window.startSeq,
|
||
endSeq: window.endSeq,
|
||
inputPrompt: undefined,
|
||
inputPromptPreview: inputPrompt.preview,
|
||
inputPromptChars: Number(attempt?.inputPromptChars ?? inputPrompt.chars),
|
||
inputPromptLines: Number(attempt?.inputPromptLines ?? inputPrompt.lines),
|
||
finalResponse,
|
||
finalResponsePreview: attempt?.finalResponsePreview ?? safePreview(finalResponse, 3000),
|
||
finalResponseChars,
|
||
finalResponseTruncated: finalResponse.length < finalResponseChars,
|
||
judge,
|
||
judgeAt: attempt?.judgeAt ?? parsedJudge?.at ?? null,
|
||
judgeSeq: attempt?.judgeSeq ?? parsedJudge?.seq ?? null,
|
||
feedbackPrompt: undefined,
|
||
feedbackPromptPreview: feedbackPrompt?.preview ?? "",
|
||
feedbackPromptChars: feedbackPrompt?.chars ?? 0,
|
||
feedbackPromptLines: feedbackPrompt?.lines ?? 0,
|
||
feedbackPromptSource: feedbackPrompt?.source ?? null,
|
||
feedbackPromptForAttempt: feedbackPrompt?.forAttempt ?? null,
|
||
feedbackPromptTruncated: feedbackPrompt?.truncated ?? false,
|
||
attemptScopeId: synthetic || window.index <= 0 ? null : taskAttemptScopeId(task.id, window.index),
|
||
stepCount,
|
||
readCount,
|
||
editCount,
|
||
runCount,
|
||
errorCount,
|
||
statsSource: attemptStats === null ? "unavailable" : "oa-event-flow",
|
||
traceStats: attemptStats,
|
||
execution: executionSummaryWithOaStats(
|
||
executionSummaryFromTranscript(
|
||
task,
|
||
executionLines,
|
||
attemptTimingSummary(attempt, executionLines.length > 0 ? executionLines : window.lines),
|
||
window.lines.length,
|
||
window.lines.length,
|
||
),
|
||
attemptStats,
|
||
),
|
||
};
|
||
}) as unknown as JsonValue[];
|
||
}
|
||
|
||
function resolvedReferencePromptParts(prompt: string): { reference: string; userPrompt: string } {
|
||
const withoutEnvironment = stripCodeQueueEnvironmentHint(prompt);
|
||
const trimmed = withoutEnvironment.trimStart();
|
||
if (!trimmed.startsWith(resolvedReferenceContextTitle)) {
|
||
return { reference: "", userPrompt: userPromptForDisplay(prompt) };
|
||
}
|
||
const offset = withoutEnvironment.length - trimmed.length;
|
||
const index = withoutEnvironment.lastIndexOf(currentTaskPromptMarker);
|
||
if (index < offset) return { reference: "", userPrompt: userPromptForDisplay(prompt) };
|
||
return {
|
||
reference: withoutEnvironment.slice(offset, index).trimEnd(),
|
||
userPrompt: withoutEnvironment.slice(index + currentTaskPromptMarker.length).trimStart(),
|
||
};
|
||
}
|
||
|
||
function taskTracePromptSummary(task: QueueTask): JsonValue {
|
||
const displayPrompt = task.basePrompt || userPromptForDisplay(task.prompt);
|
||
const parts = resolvedReferencePromptParts(task.prompt);
|
||
return {
|
||
basePrompt: displayPrompt,
|
||
basePromptChars: displayPrompt.length,
|
||
basePromptLines: promptLineCount(displayPrompt),
|
||
promptChars: task.prompt.length,
|
||
promptLines: promptLineCount(task.prompt),
|
||
referencePromptChars: parts.reference.length,
|
||
referencePromptLines: promptLineCount(parts.reference),
|
||
hasReferenceInjection: parts.reference.length > 0 || task.referenceInjection !== null,
|
||
referenceTaskIds: task.referenceTaskIds,
|
||
referenceInjectionSummary: task.referenceInjection === null ? null : {
|
||
injectedAt: task.referenceInjection.injectedAt,
|
||
itemCount: task.referenceInjection.itemCount,
|
||
directReferenceTaskIds: task.referenceInjection.directReferenceTaskIds,
|
||
maxRounds: task.referenceInjection.maxRounds,
|
||
truncated: task.referenceInjection.truncated,
|
||
},
|
||
} as unknown as JsonValue;
|
||
}
|
||
|
||
function traceStatsRecord(value: JsonValue | null | undefined): Record<string, JsonValue> | null {
|
||
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, JsonValue> : null;
|
||
}
|
||
|
||
function traceStatsNumber(stats: Record<string, JsonValue> | null, key: string): number | null {
|
||
const value = Number(stats?.[key]);
|
||
return Number.isFinite(value) && value >= 0 ? Math.floor(value) : null;
|
||
}
|
||
|
||
function taskAttemptScopeId(taskId: string, attemptIndex: number): string {
|
||
return `task:${taskId}:attempt:${Math.floor(attemptIndex)}`;
|
||
}
|
||
|
||
function traceStatsForScope(lookup: TraceStatsLookup, scopeId: string): Record<string, JsonValue> | null {
|
||
if (lookup === null || lookup === undefined || scopeId.length === 0) return null;
|
||
const value = lookup instanceof Map ? lookup.get(scopeId) : lookup[scopeId];
|
||
return traceStatsRecord(value);
|
||
}
|
||
|
||
function executionSummaryWithOaStats(execution: JsonValue, stats: Record<string, JsonValue> | null): JsonValue {
|
||
if (typeof execution !== "object" || execution === null || Array.isArray(execution)) return execution;
|
||
const record = execution as Record<string, JsonValue>;
|
||
if (stats === null) {
|
||
const {
|
||
stepCount: _stepCount,
|
||
llmStepCount: _llmStepCount,
|
||
toolCallCount: _toolCallCount,
|
||
readCount: _readCount,
|
||
editCount: _editCount,
|
||
runCount: _runCount,
|
||
errorCount: _errorCount,
|
||
traceLineCount: _traceLineCount,
|
||
outputMaxSeq: _outputMaxSeq,
|
||
transcriptMaxSeq: _transcriptMaxSeq,
|
||
...rest
|
||
} = record;
|
||
return {
|
||
...rest,
|
||
statsSource: "unavailable",
|
||
traceStats: null,
|
||
statsUnavailable: true,
|
||
} as unknown as JsonValue;
|
||
}
|
||
const stepCount = traceStatsNumber(stats, "stepCount");
|
||
const llmStepCount = traceStatsNumber(stats, "llmStepCount") ?? stepCount;
|
||
const readCount = traceStatsNumber(stats, "readCount");
|
||
const editCount = traceStatsNumber(stats, "editCount");
|
||
const runCount = traceStatsNumber(stats, "runCount");
|
||
const errorCount = traceStatsNumber(stats, "errorCount");
|
||
const traceLineCount = traceStatsNumber(stats, "traceLineCount");
|
||
const outputMaxSeq = traceStatsNumber(stats, "outputMaxSeq");
|
||
const toolCallCount = readCount === null && editCount === null && runCount === null ? null : (readCount ?? 0) + (editCount ?? 0) + (runCount ?? 0);
|
||
return {
|
||
...record,
|
||
...(stepCount === null ? {} : { stepCount }),
|
||
...(llmStepCount === null ? {} : { llmStepCount }),
|
||
...(toolCallCount === null ? {} : { toolCallCount }),
|
||
...(readCount === null ? {} : { readCount }),
|
||
...(editCount === null ? {} : { editCount }),
|
||
...(runCount === null ? {} : { runCount }),
|
||
...(errorCount === null ? {} : { errorCount }),
|
||
...(traceLineCount === null ? {} : { traceLineCount }),
|
||
...(outputMaxSeq === null ? {} : { outputMaxSeq, transcriptMaxSeq: outputMaxSeq }),
|
||
statsSource: "oa-event-flow",
|
||
traceStats: stats,
|
||
} as unknown as JsonValue;
|
||
}
|
||
|
||
function taskTraceSummaryResponse(task: QueueTask, oaTraceStats: JsonValue | null = null, oaTraceStatsByScope: TraceStatsLookup = null): JsonValue {
|
||
const transcript = cachedPreviewTranscript(task);
|
||
const attempts = taskTraceAttemptSummaries(task, transcript, oaTraceStatsByScope);
|
||
const stats = traceStatsRecord(oaTraceStats);
|
||
const stepCount = traceStatsNumber(stats, "stepCount");
|
||
const llmStepCount = traceStatsNumber(stats, "llmStepCount") ?? stepCount;
|
||
const errorCount = traceStatsNumber(stats, "errorCount");
|
||
const execution = executionSummaryWithOaStats(taskExecutionSummary(task, transcript), stats);
|
||
return {
|
||
id: task.id,
|
||
queueId: ctx().queueIdOf(task),
|
||
status: task.status,
|
||
providerId: task.providerId,
|
||
executionMode: task.executionMode,
|
||
executionModeInfo: codeExecutionModeInfo(task.executionMode),
|
||
model: task.model,
|
||
agentPort: codeAgentPortForModel(task.model),
|
||
agentPortInfo: codeAgentPortInfo(codeAgentPortForModel(task.model)),
|
||
cwd: task.cwd,
|
||
reasoningEffort: task.reasoningEffort,
|
||
createdAt: task.createdAt,
|
||
startedAt: task.startedAt,
|
||
finishedAt: task.finishedAt,
|
||
updatedAt: task.updatedAt,
|
||
currentAttempt: task.currentAttempt,
|
||
maxAttempts: task.maxAttempts,
|
||
stepCount,
|
||
llmStepCount,
|
||
promptEditable: ctx().queuedTaskPromptEditable(task),
|
||
prompt: taskTracePromptSummary(task),
|
||
execution,
|
||
traceStats: stats,
|
||
statsSource: stats === null ? "unavailable" : "oa-event-flow",
|
||
finalResponse: task.finalResponse,
|
||
finalResponseChars: task.finalResponse.length,
|
||
lastJudge: task.lastJudge,
|
||
lastError: task.lastError,
|
||
errorCount,
|
||
attempts,
|
||
timing: taskTiming(task),
|
||
} as unknown as JsonValue;
|
||
}
|
||
|
||
function taskFeedbackPromptDetail(task: QueueTask, attemptIndex: number | null): FeedbackPromptRecord | null {
|
||
const index = attemptIndex ?? (task.attempts.length > 0 ? task.attempts.length : task.currentAttempt || 1);
|
||
if (!Number.isInteger(index) || index <= 0) return null;
|
||
const attempt = task.attempts.find((item) => Number(item.index) === index) ?? task.attempts[index - 1] ?? null;
|
||
let judge = attempt?.judge ?? null;
|
||
if (judge === null) {
|
||
const window = traceAttemptWindows(task, cachedPreviewTranscript(task)).find((item) => item.index === index) ?? null;
|
||
judge = judgeFromAttemptLines(window?.lines ?? [])?.judge ?? null;
|
||
}
|
||
return attemptFeedbackPromptRecord(task, index, attempt, judge);
|
||
}
|
||
|
||
function taskPromptDetailResponse(task: QueueTask, url: URL): Response {
|
||
const part = String(url.searchParams.get("part") || "full");
|
||
const parts = resolvedReferencePromptParts(task.prompt);
|
||
const basePrompt = task.basePrompt || userPromptForDisplay(task.prompt);
|
||
if (part === "feedback" || part === "judge-feedback") {
|
||
const attemptIndex = ctx().parseSeqParam(url, "attempt", null);
|
||
const detail = taskFeedbackPromptDetail(task, attemptIndex);
|
||
const text = detail?.text ?? "";
|
||
return ctx().jsonResponse({
|
||
ok: true,
|
||
taskId: task.id,
|
||
queueId: ctx().queueIdOf(task),
|
||
status: task.status,
|
||
updatedAt: task.updatedAt,
|
||
part: "feedback",
|
||
attempt: attemptIndex,
|
||
forAttempt: detail?.forAttempt ?? null,
|
||
source: detail?.source ?? null,
|
||
text,
|
||
preview: detail?.preview ?? "",
|
||
chars: detail?.chars ?? text.length,
|
||
lines: detail?.lines ?? promptLineCount(text),
|
||
truncated: detail?.truncated ?? false,
|
||
});
|
||
}
|
||
const text = part === "base"
|
||
? basePrompt
|
||
: part === "reference"
|
||
? parts.reference
|
||
: task.prompt;
|
||
return ctx().jsonResponse({
|
||
ok: true,
|
||
taskId: task.id,
|
||
queueId: ctx().queueIdOf(task),
|
||
status: task.status,
|
||
updatedAt: task.updatedAt,
|
||
part,
|
||
text,
|
||
chars: text.length,
|
||
lines: promptLineCount(text),
|
||
promptChars: task.prompt.length,
|
||
basePromptChars: basePrompt.length,
|
||
referencePromptChars: parts.reference.length,
|
||
});
|
||
}
|
||
|
||
async function taskTraceStepsResponse(task: QueueTask, url: URL): Promise<Response> {
|
||
const limit = ctx().parseLimit(url);
|
||
const attemptIndex = ctx().parseSeqParam(url, "attempt", null);
|
||
const agentPort = codeAgentPortForModel(task.model);
|
||
const transcript = await oaTraceTranscriptForTask(task, attemptIndex);
|
||
const page = ctx().pageBySeq(transcript, url, limit);
|
||
return ctx().jsonResponse({
|
||
ok: true,
|
||
taskId: task.id,
|
||
queueId: ctx().queueIdOf(task),
|
||
status: task.status,
|
||
updatedAt: task.updatedAt,
|
||
agentPort,
|
||
agentPortInfo: codeAgentPortInfo(agentPort),
|
||
attempt: attemptIndex,
|
||
source: "oa-event-flow",
|
||
steps: page.chunk.map((line) => ({
|
||
seq: line.seq,
|
||
at: line.at,
|
||
kind: line.kind,
|
||
title: line.title,
|
||
status: line.status ?? null,
|
||
durationMs: line.durationMs ?? null,
|
||
rawSeqs: line.rawSeqs,
|
||
summaryLines: transcriptLineSummaryLines(line),
|
||
hasDetail: true,
|
||
})),
|
||
mode: page.mode,
|
||
afterSeq: page.afterSeq,
|
||
nextAfterSeq: page.nextAfterSeq,
|
||
beforeSeq: page.beforeSeq,
|
||
previousBeforeSeq: page.previousBeforeSeq,
|
||
hasMore: page.hasMore,
|
||
hasBefore: page.hasBefore,
|
||
total: transcript.length,
|
||
maxSeq: transcript.at(-1)?.seq ?? 0,
|
||
});
|
||
}
|
||
|
||
async function taskTraceStepDetailResponse(task: QueueTask, url: URL): Promise<Response> {
|
||
const seq = Number(url.searchParams.get("seq"));
|
||
if (!Number.isFinite(seq)) return ctx().jsonResponse({ ok: false, error: "seq is required" }, 400);
|
||
const agentPort = codeAgentPortForModel(task.model);
|
||
const transcript = await oaTraceTranscriptForTask(task, null);
|
||
const line = transcript.find((item) => Number(item.seq) === seq || item.rawSeqs.includes(seq));
|
||
if (line === undefined) return ctx().jsonResponse({ ok: false, error: "trace step not found", seq }, 404);
|
||
return ctx().jsonResponse({
|
||
ok: true,
|
||
taskId: task.id,
|
||
queueId: ctx().queueIdOf(task),
|
||
status: task.status,
|
||
updatedAt: task.updatedAt,
|
||
agentPort,
|
||
agentPortInfo: codeAgentPortInfo(agentPort),
|
||
seq,
|
||
line,
|
||
});
|
||
}
|
||
|
||
function taskSummaryResponse(task: QueueTask, url: URL): JsonValue {
|
||
const transcript = fullTranscript(task);
|
||
const fullOutput = taskFullOutput(task);
|
||
return {
|
||
id: task.id,
|
||
queueId: ctx().queueIdOf(task),
|
||
status: task.status,
|
||
providerId: task.providerId,
|
||
executionMode: task.executionMode,
|
||
executionModeInfo: codeExecutionModeInfo(task.executionMode),
|
||
model: task.model,
|
||
agentPort: codeAgentPortForModel(task.model),
|
||
agentPortInfo: codeAgentPortInfo(codeAgentPortForModel(task.model)),
|
||
cwd: task.cwd,
|
||
reasoningEffort: task.reasoningEffort,
|
||
maxAttempts: task.maxAttempts,
|
||
currentAttempt: task.currentAttempt,
|
||
currentMode: task.currentMode,
|
||
judgeFailCount: task.judgeFailCount,
|
||
judgeFailRetryLimit,
|
||
codexThreadId: task.codexThreadId,
|
||
activeTurnId: task.activeTurnId,
|
||
createdAt: task.createdAt,
|
||
startedAt: task.startedAt,
|
||
finishedAt: task.finishedAt,
|
||
updatedAt: task.updatedAt,
|
||
timing: taskTiming(task),
|
||
initialPrompt: task.prompt,
|
||
basePrompt: task.basePrompt,
|
||
prompt: task.prompt,
|
||
promptEditable: ctx().queuedTaskPromptEditable(task),
|
||
referenceTaskIds: task.referenceTaskIds,
|
||
referenceInjection: task.referenceInjection,
|
||
lastAssistantMessage: lastAssistantMessage(task, transcript),
|
||
toolSummary: taskToolSummary(task, url, transcript),
|
||
attempts: task.attempts,
|
||
lastJudge: task.lastJudge,
|
||
lastError: task.lastError,
|
||
cancelRequested: task.cancelRequested,
|
||
transcriptCount: transcript.length,
|
||
transcriptMaxSeq: transcript.at(-1)?.seq ?? 0,
|
||
outputCount: fullOutput.length,
|
||
retainedOutputCount: task.output.length,
|
||
outputMaxSeq: fullOutput.at(-1)?.seq ?? 0,
|
||
eventCount: task.events.length,
|
||
cliHint: `bun scripts/cli.ts codex task ${task.id}`,
|
||
traceHint: `bun scripts/cli.ts codex task ${task.id} --trace --tail --limit 80`,
|
||
rawOutputHint: `bun scripts/cli.ts codex output ${task.id} --tail --limit 20`,
|
||
} as unknown as JsonValue;
|
||
}
|
||
|
||
|
||
export {
|
||
buildCompactTaskTranscript,
|
||
buildTaskTranscript,
|
||
cachedPreviewTranscript,
|
||
codexToolLifecycleStartedBeforeIn,
|
||
isCodexToolLifecycleOutput,
|
||
formatCommandOutput,
|
||
fullTranscript,
|
||
lastAssistantMessage,
|
||
promptLineCount,
|
||
promptSnapshot,
|
||
recordNumberField,
|
||
recordStringField,
|
||
safePreview,
|
||
prefixPreview,
|
||
setAttemptFeedbackPrompt,
|
||
setAttemptInputPrompt,
|
||
statsDaysFromUrl,
|
||
taskExecutionSummary,
|
||
taskListStepCount,
|
||
taskOutputMaxSeq,
|
||
taskForCompactMetaResponse,
|
||
taskForMetaResponse,
|
||
taskForResponse,
|
||
taskStatisticsSummary,
|
||
taskSummaryResponse,
|
||
taskTiming,
|
||
timestampMs,
|
||
nonNegativeElapsed,
|
||
taskToolSummary,
|
||
taskTraceStepDetailResponse,
|
||
taskTraceStepsResponse,
|
||
taskTraceSummaryResponse,
|
||
taskPromptDetailResponse,
|
||
transcriptLineSummaryLines,
|
||
};
|