fix: stabilize code queue runtime and trace flow
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -3181,6 +3181,15 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); }
|
||||
color: var(--accent-2);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.codex-trace-step-inline-summary {
|
||||
min-width: 80px;
|
||||
flex: 1 1 180px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: #a7c7c3;
|
||||
font-size: 11px;
|
||||
}
|
||||
.codex-trace-step.error > summary .codex-output-channel {
|
||||
color: var(--danger);
|
||||
border-color: rgba(207, 106, 84, 0.52);
|
||||
|
||||
@@ -655,6 +655,40 @@ function objectRecord(value: any): AnyRecord | null {
|
||||
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
||||
}
|
||||
|
||||
function judgeFailureDetailsText(judge: any): string {
|
||||
const raw = objectRecord(judge?.raw);
|
||||
const details = objectRecord(judge?.failureDetails) || objectRecord(raw?.minimaxFailure);
|
||||
if (details === null) return "";
|
||||
const repairAttempt = details.repairAttempt === undefined ? "" : `${details.repairAttempt}/${details.maxRepairAttempts ?? "?"}`;
|
||||
const rows = [
|
||||
["provider", details.provider || "minimax"],
|
||||
["stage", details.stage],
|
||||
["model", details.model],
|
||||
["timedOut", details.timedOut],
|
||||
["durationMs", details.durationMs],
|
||||
["timeoutMs", details.timeoutMs],
|
||||
["promptChars", details.promptChars],
|
||||
["promptLines", details.promptLines],
|
||||
["payloadBytes", details.payloadBytes],
|
||||
["responseStatus", details.responseStatus],
|
||||
["repairAttempt", repairAttempt],
|
||||
["errorName", details.errorName],
|
||||
["error", details.errorMessage],
|
||||
["responseContentPreview", details.responseContentPreview],
|
||||
["responseTextPreview", details.responseTextPreview],
|
||||
].filter(([, value]) => value !== undefined && value !== null && String(value).length > 0);
|
||||
return rows.map(([key, value]) => `${key}: ${String(value)}`).join("\n");
|
||||
}
|
||||
|
||||
function JudgeFailureDetails({ judge, testId = "codex-judge-failure-details" }: AnyRecord) {
|
||||
const text = judgeFailureDetailsText(judge);
|
||||
if (text.length === 0) return null;
|
||||
return h("details", { className: "codex-judge-failure-details", "data-testid": testId },
|
||||
h("summary", null, "MiniMax failure details"),
|
||||
h("pre", null, text),
|
||||
);
|
||||
}
|
||||
|
||||
function taskProgressiveAttempts(task: any): any[] {
|
||||
const summaryAttempts = taskTraceSummary(task)?.attempts;
|
||||
if (Array.isArray(summaryAttempts) && summaryAttempts.length > 0) return summaryAttempts;
|
||||
@@ -1214,6 +1248,8 @@ function codexProviderOptions(queue: any, currentProviderId: string): any[] {
|
||||
id: String(item?.id || "").trim(),
|
||||
label: String(item?.label || item?.id || "").trim(),
|
||||
defaultWorkdir: String(item?.defaultWorkdir || "").trim(),
|
||||
supportsWindowsNativeCodex: item?.supportsWindowsNativeCodex === true,
|
||||
windowsNativeDefaultWorkdir: String(item?.windowsNativeDefaultWorkdir || "").trim(),
|
||||
kind: String(item?.kind || "").trim(),
|
||||
}))
|
||||
.filter((item: any) => item.id.length > 0);
|
||||
@@ -1221,14 +1257,44 @@ function codexProviderOptions(queue: any, currentProviderId: string): any[] {
|
||||
const byId = new Map<string, any>();
|
||||
for (const item of [
|
||||
...rows,
|
||||
{ id: fallbackMain, label: `${fallbackMain} (master)`, defaultWorkdir: String(queue?.defaultWorkdir || "/root/unidesk"), kind: "local" },
|
||||
currentProviderId ? { id: currentProviderId, label: currentProviderId, defaultWorkdir: providerDefaultWorkdir(queue, currentProviderId), kind: "" } : null,
|
||||
{ id: fallbackMain, label: `${fallbackMain} (master)`, defaultWorkdir: String(queue?.defaultWorkdir || "/root/unidesk"), supportsWindowsNativeCodex: false, windowsNativeDefaultWorkdir: "", kind: "local" },
|
||||
currentProviderId ? { id: currentProviderId, label: currentProviderId, defaultWorkdir: providerDefaultWorkdir(queue, currentProviderId), supportsWindowsNativeCodex: currentProviderId !== fallbackMain, windowsNativeDefaultWorkdir: String(queue?.windowsNativeCodexDefaultWorkdir || "/mnt/f/Work/ConStart"), kind: "" } : null,
|
||||
].filter(Boolean) as any[]) {
|
||||
if (!byId.has(item.id)) byId.set(item.id, item);
|
||||
}
|
||||
return Array.from(byId.values());
|
||||
}
|
||||
|
||||
function codexExecutionModeOptions(queue: any, currentMode: string): any[] {
|
||||
const configured = Array.isArray(queue?.executionModes) ? queue.executionModes : [];
|
||||
const rows = configured
|
||||
.map((item: any) => ({
|
||||
id: String(item?.id || item?.kind || "").trim(),
|
||||
label: String(item?.label || item?.id || item?.kind || "").trim(),
|
||||
description: String(item?.description || "").trim(),
|
||||
defaultWorkdir: String(item?.defaultWorkdir || "").trim(),
|
||||
requiresProvider: item?.requiresProvider === true,
|
||||
requiresWindowsCwd: item?.requiresWindowsCwd === true,
|
||||
}))
|
||||
.filter((item: any) => item.id.length > 0);
|
||||
const fallback = [
|
||||
{ id: "default", label: "默认容器/本机", description: "主 server 用本机 Codex;远程 Provider 用执行容器 Codex。", defaultWorkdir: "", requiresProvider: false, requiresWindowsCwd: false },
|
||||
{ id: "windows-native", label: "Windows 原生 Codex", description: "启动执行容器,但容器只做 stdio relay,Codex 运行在 Provider 的 Windows 宿主。", defaultWorkdir: String(queue?.windowsNativeCodexDefaultWorkdir || "/mnt/f/Work/ConStart"), requiresProvider: true, requiresWindowsCwd: true },
|
||||
];
|
||||
const byId = new Map<string, any>();
|
||||
for (const item of [...rows, ...fallback, currentMode ? { id: currentMode, label: currentMode, description: "", defaultWorkdir: "", requiresProvider: currentMode === "windows-native", requiresWindowsCwd: currentMode === "windows-native" } : null].filter(Boolean) as any[]) {
|
||||
if (!byId.has(item.id)) byId.set(item.id, item);
|
||||
}
|
||||
return Array.from(byId.values());
|
||||
}
|
||||
|
||||
function executionModeDefaultWorkdir(queue: any, mode: string, providerId: string): string {
|
||||
if (mode !== "windows-native") return providerDefaultWorkdir(queue, providerId);
|
||||
const option = Array.isArray(queue?.executionModes) ? queue.executionModes.find((item: any) => String(item?.id || item?.kind || "") === "windows-native") : null;
|
||||
const provider = Array.isArray(queue?.executionProviders) ? queue.executionProviders.find((item: any) => String(item?.id || "") === providerId) : null;
|
||||
return String(provider?.windowsNativeDefaultWorkdir || option?.defaultWorkdir || queue?.windowsNativeCodexDefaultWorkdir || "/mnt/f/Work/ConStart");
|
||||
}
|
||||
|
||||
function providerDefaultWorkdir(queue: any, providerId: string): string {
|
||||
const id = String(providerId || "").trim();
|
||||
const map = queue?.defaultWorkdirByProvider && typeof queue.defaultWorkdirByProvider === "object" ? queue.defaultWorkdirByProvider : {};
|
||||
@@ -1310,6 +1376,7 @@ function TaskCard({ task, selected, onSelect, onCopy, onReference, onMarkRead, c
|
||||
h("div", { className: "codex-task-meta" },
|
||||
h("span", null, `queue=${taskQueueLabel(task)}`),
|
||||
h("span", null, `provider=${task?.providerId || "main-server"}`),
|
||||
h("span", null, `mode=${task?.executionMode || "default"}`),
|
||||
h("span", null, task?.model || "--"),
|
||||
h("span", null, taskDurationLabel(task)),
|
||||
),
|
||||
@@ -1622,6 +1689,7 @@ function ProgressiveExecutionSummary({ task, attempt, attemptIndex, loading, onL
|
||||
const seq = String(step?.seq ?? "");
|
||||
const detail = stepDetails[seq];
|
||||
const summaryLines = Array.isArray(step?.summaryLines) ? step.summaryLines.slice(0, 4) : [];
|
||||
const inlineSummary = summaryLines.find((line: any) => String(line || "").trim().length > 0);
|
||||
return h("details", {
|
||||
key: seq || `${step?.title}-${step?.at}`,
|
||||
className: `codex-trace-step ${String(step?.kind || "message")} ${traceStepIsError(step) ? "error" : ""}`,
|
||||
@@ -1634,6 +1702,7 @@ function ProgressiveExecutionSummary({ task, attempt, attemptIndex, loading, onL
|
||||
h("span", { className: "codex-output-channel" }, traceStepKindLabel(step?.kind)),
|
||||
h("strong", null, String(step?.title || "Trace step")),
|
||||
step?.status ? h("code", null, String(step.status)) : null,
|
||||
inlineSummary ? h("span", { className: "codex-trace-step-inline-summary", title: String(inlineSummary) }, String(inlineSummary)) : null,
|
||||
h("time", null, fmtDate(step?.at)),
|
||||
),
|
||||
h("div", { className: "codex-trace-step-summary" },
|
||||
@@ -1695,6 +1764,7 @@ function ProgressiveJudge({ task, attempt, attemptIndex, testId = "codex-progres
|
||||
h(StatusBadge, { status: judge.decision }, judge.decision),
|
||||
h("strong", null, `${Math.round(Number(judge.confidence || 0) * 100)}% confidence`),
|
||||
h("p", { "data-testid": `${testId}-reason` }, judge.reason || "--"),
|
||||
h(JudgeFailureDetails, { judge, testId: `${testId}-failure-details` }),
|
||||
judge.continuePrompt ? h("pre", { "data-testid": `${testId}-continue-prompt` }, String(judge.continuePrompt || "")) : null,
|
||||
),
|
||||
);
|
||||
@@ -1889,6 +1959,7 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi
|
||||
const [mergeDialogOpen, setMergeDialogOpen] = useState(false);
|
||||
const [mergeSourceQueueId, setMergeSourceQueueId] = useState("");
|
||||
const [providerId, setProviderId] = useState("main-server");
|
||||
const [executionMode, setExecutionMode] = useState("default");
|
||||
const [model, setModel] = useState("gpt-5.5");
|
||||
const [cwd, setCwd] = useState("/root/unidesk");
|
||||
const [maxAttempts, setMaxAttempts] = useState(99);
|
||||
@@ -1967,7 +2038,8 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi
|
||||
const submitDisabled = submitting || busy || enqueueCount === 0 || batchNeedsConfirmation;
|
||||
const codexModels = codexModelOptions(queue, model);
|
||||
const providerOptions = codexProviderOptions(queue, providerId);
|
||||
const currentProviderDefaultWorkdir = providerDefaultWorkdir(queue, providerId);
|
||||
const executionModeRows = codexExecutionModeOptions(queue, executionMode);
|
||||
const currentProviderDefaultWorkdir = executionModeDefaultWorkdir(queue, executionMode, providerId);
|
||||
const selectedCanSteer = selectedTask?.id && selectedTask?.activeTurnId && String(selectedTask?.status) === "running";
|
||||
const selectedCanInterrupt = selectedTask?.id && !["succeeded", "failed", "canceled"].includes(String(selectedTask?.status || ""));
|
||||
const selectedCanRetry = selectedTask?.id && ["succeeded", "failed", "canceled"].includes(String(selectedTask?.status || ""));
|
||||
@@ -2038,7 +2110,21 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi
|
||||
function changeSubmitProvider(nextProviderId: string): void {
|
||||
const next = String(nextProviderId || queue?.mainProviderId || "main-server").trim() || "main-server";
|
||||
setProviderId(next);
|
||||
setCwd(providerDefaultWorkdir(queue, next));
|
||||
setCwd(executionModeDefaultWorkdir(queue, executionMode, next));
|
||||
}
|
||||
|
||||
function changeSubmitExecutionMode(nextMode: string): void {
|
||||
const next = String(nextMode || "default").trim() || "default";
|
||||
let nextProvider = providerId;
|
||||
if (next === "windows-native") {
|
||||
const current = providerOptions.find((provider: any) => provider.id === providerId);
|
||||
if (!current?.supportsWindowsNativeCodex) {
|
||||
nextProvider = String(providerOptions.find((provider: any) => provider.supportsWindowsNativeCodex)?.id || providerId || "D601");
|
||||
setProviderId(nextProvider);
|
||||
}
|
||||
}
|
||||
setExecutionMode(next);
|
||||
setCwd(executionModeDefaultWorkdir(queue, next, nextProvider));
|
||||
}
|
||||
|
||||
function patchLoadedReadState(taskIds: string[], readAt: string, queuePatch: any = null, taskPatch: any = null): void {
|
||||
@@ -2872,6 +2958,7 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi
|
||||
prompt: text,
|
||||
queueId: submitQueueId,
|
||||
providerId,
|
||||
executionMode,
|
||||
model,
|
||||
cwd,
|
||||
maxAttempts: Number(maxAttempts),
|
||||
@@ -3298,6 +3385,7 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi
|
||||
h("span", { className: `codex-trace-status-chip unread ${overallUnreadTerminalCount > 0 ? "warn" : ""}` }, h("b", null, "结束未读"), String(overallUnreadTerminalCount)),
|
||||
h("span", { className: "codex-trace-status-chip service" }, h("b", null, "服务"), `${runtime.providerStatus || "unknown"} · ${service?.providerId || "main-server"} · ${backend.public ? "公网暴露" : "仅 UniDesk frontend 代理访问"}`),
|
||||
h("span", { className: "codex-trace-status-chip" }, h("b", null, "执行节点"), providerOptions.map((provider: any) => provider.id).join(" / ")),
|
||||
h("span", { className: "codex-trace-status-chip" }, h("b", null, "执行模式"), executionModeRows.map((mode: any) => mode.id).join(" / ")),
|
||||
h("span", { className: "codex-trace-status-chip" }, h("b", null, "模型"), codexModels.join(" / ")),
|
||||
h("span", { className: "codex-trace-status-chip" }, h("b", null, "加载"), loadStats?.phase === "complete" ? fmtPreciseMs(loadStats?.totalMs) : String(loadStats?.phase || "idle")),
|
||||
h("span", { className: "codex-trace-status-chip" }, h("b", null, "刷新"), refreshedAt ? fmtClock(refreshedAt) : "--"),
|
||||
@@ -3305,7 +3393,7 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi
|
||||
|
||||
const sessionPanel = h(Panel, {
|
||||
title: selectedTask ? `Trace ${String(selectedTask.id).slice(0, 22)}` : "Trace 输出",
|
||||
eyebrow: selectedTask ? `${selectedTask.status} / view=${selectedQueueName} / task queue=${taskQueueLabel(selectedTask)} / provider=${selectedTask.providerId || "main-server"} / ${selectedTask.model} / agent loop trace` : `Agent loop trace / view=${selectedQueueName}`,
|
||||
eyebrow: selectedTask ? `${selectedTask.status} / view=${selectedQueueName} / task queue=${taskQueueLabel(selectedTask)} / provider=${selectedTask.providerId || "main-server"} / mode=${selectedTask.executionMode || "default"} / ${selectedTask.model} / agent loop trace` : `Agent loop trace / view=${selectedQueueName}`,
|
||||
summary: traceStatusSummary,
|
||||
loading: selectedDetailLoading || loadingMoreTasks || searchLoading || searchLoadingMoreTasks || loadStats?.phase === "loading",
|
||||
actions: h("div", { className: "panel-actions" },
|
||||
@@ -3433,7 +3521,12 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi
|
||||
),
|
||||
h("label", null, "执行 Provider",
|
||||
h("select", { value: providerId, disabled: submitting, onChange: (event: any) => changeSubmitProvider(String(event.target.value || "main-server")), "data-testid": "codex-provider-select" },
|
||||
providerOptions.map((provider: any) => h("option", { key: provider.id, value: provider.id }, `${provider.label || provider.id} · ${provider.defaultWorkdir || providerDefaultWorkdir(queue, provider.id)}`)),
|
||||
providerOptions.map((provider: any) => h("option", { key: provider.id, value: provider.id }, `${provider.label || provider.id} · ${provider.defaultWorkdir || providerDefaultWorkdir(queue, provider.id)}${provider.supportsWindowsNativeCodex ? " · Windows native" : ""}`)),
|
||||
),
|
||||
),
|
||||
h("label", null, "执行模式",
|
||||
h("select", { value: executionMode, disabled: submitting, onChange: (event: any) => changeSubmitExecutionMode(String(event.target.value || "default")), "data-testid": "codex-execution-mode-select" },
|
||||
executionModeRows.map((mode: any) => h("option", { key: mode.id, value: mode.id }, `${mode.label || mode.id}${mode.id === "windows-native" ? " · 宿主 Codex" : ""}`)),
|
||||
),
|
||||
),
|
||||
h("label", null, "工作目录", h("input", { value: cwd, disabled: submitting, onChange: (event: any) => setCwd(event.target.value), placeholder: currentProviderDefaultWorkdir || queue?.defaultWorkdir || "/root/unidesk", "data-testid": "codex-cwd-input" })),
|
||||
@@ -3530,6 +3623,7 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi
|
||||
h(StatusBadge, { status: selectedTask.lastJudge.decision }, selectedTask.lastJudge.decision),
|
||||
h("strong", null, `${Math.round(Number(selectedTask.lastJudge.confidence || 0) * 100)}% confidence`),
|
||||
h("p", { "data-testid": "codex-task-judge-reason" }, shortText(selectedTask.lastJudge.reason || "--", 180)),
|
||||
h(JudgeFailureDetails, { judge: selectedTask.lastJudge, testId: "codex-task-judge-failure-details" }),
|
||||
selectedTask.lastJudge.continuePrompt ? h("code", { "data-testid": "codex-task-judge-continue-prompt" }, shortText(selectedTask.lastJudge.continuePrompt, 160)) : null,
|
||||
) : h(EmptyState, { title: "尚未判定", text: "Codex turn 结束后会由 MiniMax M2.7 或 fallback judge 判定 complete/retry/fail;retry 会在已有 thread 追加继续执行 prompt。" }),
|
||||
),
|
||||
|
||||
@@ -864,6 +864,28 @@ function renderToolGroup(item: TraceItem): any {
|
||||
);
|
||||
}
|
||||
|
||||
function traceSystemItemIsError(item: TraceItem): boolean {
|
||||
const text = [
|
||||
item.title,
|
||||
item.status,
|
||||
item.bodyPreview,
|
||||
item.commandPreview,
|
||||
item.stderrPreview,
|
||||
item.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 filterTraceSystemEvents(items: TraceItem[], showSystemEvents: boolean): TraceItem[] {
|
||||
if (showSystemEvents) return items;
|
||||
return items.flatMap((item) => {
|
||||
if (String(item.kind || "") === "system" && !traceSystemItemIsError(item)) return [];
|
||||
if (String(item.kind || "") !== "toolGroup" || !Array.isArray(item.items)) return [item];
|
||||
const visibleItems = filterTraceSystemEvents(item.items, showSystemEvents);
|
||||
return [{ ...item, items: visibleItems }];
|
||||
});
|
||||
}
|
||||
|
||||
const traceAutoScrollBottomThresholdPx = 16;
|
||||
|
||||
function traceIsScrolledToBottom(element: HTMLElement): boolean {
|
||||
@@ -871,10 +893,10 @@ function traceIsScrolledToBottom(element: HTMLElement): boolean {
|
||||
return distanceToBottom <= traceAutoScrollBottomThresholdPx;
|
||||
}
|
||||
|
||||
export function TraceView({ items, input, port, autoScroll = false, loading = false, hasDetail = true, emptyText = "等待 Trace 输出...", loadingText = "正在加载完整 Trace...", testId = "trace-output", className = "codex-transcript", keepRecentToolCalls = 3, collapseTools = true }: AnyRecord) {
|
||||
export function TraceView({ items, input, port, autoScroll = false, loading = false, hasDetail = true, emptyText = "等待 Trace 输出...", loadingText = "正在加载完整 Trace...", testId = "trace-output", className = "codex-transcript", keepRecentToolCalls = 3, collapseTools = true, showSystemEvents = false }: AnyRecord) {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const shouldFollowTailRef = useRef(true);
|
||||
const rawTrace = coalesceEditedFileChanges(port ? traceFromPort(port, input) : normalizeTraceItems(items));
|
||||
const rawTrace = filterTraceSystemEvents(coalesceEditedFileChanges(port ? traceFromPort(port, input) : normalizeTraceItems(items)), Boolean(showSystemEvents));
|
||||
const trace = collapseTools ? collapseToolTraceRuns(rawTrace, keepRecentToolCalls) : rawTrace;
|
||||
const maxSeq = traceMaxSeq(rawTrace);
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user