fix: sync code queue retry trace display

This commit is contained in:
Codex
2026-05-20 03:54:02 +00:00
parent 00d5066009
commit 4bb33d0d52
4 changed files with 615 additions and 110 deletions
File diff suppressed because one or more lines are too long
+16
View File
@@ -2983,6 +2983,22 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); }
background: rgba(78, 183, 168, 0.08);
letter-spacing: 0.04em;
}
.codex-task-step-count.syncing,
.codex-task-step-count.unavailable {
border-color: rgba(215, 161, 58, 0.38);
color: #f0cf8a;
background: rgba(215, 161, 58, 0.08);
}
.codex-task-step-count.failed {
border-color: rgba(207, 106, 84, 0.52);
color: var(--danger);
background: rgba(207, 106, 84, 0.08);
}
.codex-task-step-count.fallback {
border-color: rgba(148, 190, 255, 0.40);
color: #b9d0ff;
background: rgba(148, 190, 255, 0.08);
}
.codex-output-panel .panel-body {
padding: 0;
}
+182 -29
View File
@@ -681,6 +681,82 @@ function canonicalTaskStepCount(task: any): number | null {
return null;
}
function rawTraceStepCountFallback(...values: any[]): number | null {
for (const value of values) {
const count = statCount(value);
if (count !== null) return count;
}
return null;
}
function traceStepCountLabel(state: AnyRecord): string {
if (state.state === "ready" || state.state === "fallback") return `STEP ${state.count}${state.state === "fallback" ? " raw" : ""}`;
if (state.state === "syncing") return "STEP sync";
if (state.state === "failed") return "STEP failed";
return "STEP N/A";
}
function traceStepCountState(task: any): AnyRecord {
const oaStats = taskOaTraceStats(task);
const oaCount = statCount(oaStats?.stepCount ?? oaStats?.llmStepCount);
if (oaCount !== null) {
return {
state: "ready",
count: oaCount,
label: `STEP ${oaCount}`,
title: "STEP 来自 OA Event Flow 统计中心",
source: "oa-event-flow",
};
}
const summary = taskTraceSummary(task);
const activeWithoutRawRows = taskIsActive(task)
&& task?._traceSummaryLoaded !== true
&& statCount(task?.outputCount ?? task?.retainedOutputCount) === 0;
const rawCount = rawTraceStepCountFallback(
summary?.retainedStepCount,
summary?.execution?.traceLineCount,
summary?.execution?.stepCount,
task?.outputCount,
task?.retainedOutputCount,
activeWithoutRawRows ? null : task?.stepCount,
activeWithoutRawRows ? null : task?.llmStepCount,
);
if (rawCount !== null) {
return {
state: "fallback",
count: rawCount,
label: `STEP ${rawCount} raw`,
title: "OA STEP 统计不可用,当前显示 raw trace fallback 行数",
source: "raw-trace",
};
}
if (summary?.traceStatsError || task?.traceStatsError) {
return {
state: "failed",
count: null,
label: "STEP failed",
title: "STEP 统计读取失败;请查看 trace summary 原始 JSON",
source: "stats-error",
};
}
if (taskIsActive(task) || task?._traceSummaryLoaded !== true) {
return {
state: "syncing",
count: null,
label: "STEP sync",
title: "STEP 统计同步中;Trace Summary 或 OA 统计尚未返回",
source: "syncing",
};
}
return {
state: "unavailable",
count: null,
label: "STEP N/A",
title: "当前任务没有可用 STEP 统计或 raw trace fallback",
source: "unavailable",
};
}
function traceSummaryStepCount(summary: AnyRecord | null): number | null {
const stats = objectRecord(summary?.traceStats);
if (!stats || (summary?.statsSource !== "oa-event-flow" && stats.source !== "oa-event-flow")) return null;
@@ -764,12 +840,12 @@ function JudgeFailureDetails({ judge, testId = "codex-judge-failure-details" }:
function taskProgressiveAttempts(task: any): any[] {
const summaryAttempts = taskTraceSummary(task)?.attempts;
if (Array.isArray(summaryAttempts) && summaryAttempts.length > 0) return summaryAttempts;
if (Array.isArray(summaryAttempts) && summaryAttempts.length > 0) return currentFirstAttempts(task, withCurrentAttemptSegment(task, summaryAttempts));
const execution = taskExecutionSummary(task);
const finalResponse = taskFinalResponseText(task);
const judge = taskLastJudge(task);
if (Object.keys(execution).length === 0 && finalResponse.length === 0 && judge === null) return [];
return [{
if (Object.keys(execution).length === 0 && finalResponse.length === 0 && judge === null) return currentFirstAttempts(task, withCurrentAttemptSegment(task, []));
return currentFirstAttempts(task, withCurrentAttemptSegment(task, [{
index: Number(task?.currentAttempt || 1),
mode: task?.currentMode || "initial",
startedAt: task?.startedAt,
@@ -779,7 +855,56 @@ function taskProgressiveAttempts(task: any): any[] {
finalResponse,
finalResponseChars: finalResponse.length,
judge,
}];
}]));
}
function attemptIsCurrent(task: any, attemptIndex: any): boolean {
const summary = taskTraceSummary(task);
const currentAttempt = Number(summary?.currentAttempt || task?.currentAttempt || 0);
const numericAttempt = Number(attemptIndex);
return Number.isFinite(currentAttempt) && currentAttempt > 0 && Number.isFinite(numericAttempt) && numericAttempt === currentAttempt;
}
function attemptIsActiveCurrent(task: any, attemptIndex: any): boolean {
return taskIsActive(task) && attemptIsCurrent(task, attemptIndex);
}
function withCurrentAttemptSegment(task: any, attempts: any[]): any[] {
const rows = Array.isArray(attempts) ? attempts.filter(Boolean) : [];
const summary = taskTraceSummary(task);
const currentAttempt = Number(summary?.currentAttempt || task?.currentAttempt || 0);
if (!taskIsActive(task) || !Number.isFinite(currentAttempt) || currentAttempt <= 0) return rows;
if (rows.some((attempt) => Number(attempt?.index) === currentAttempt)) return rows;
const activeAt = latestTimestampValue(task?.startedAt, summary?.startedAt, task?.updatedAt, summary?.updatedAt);
return [
...rows,
{
index: currentAttempt,
mode: task?.currentMode || summary?.currentMode || (currentAttempt <= 1 ? "initial" : "retry"),
startedAt: activeAt || task?.startedAt || summary?.startedAt,
finishedAt: null,
terminalStatus: null,
execution: {},
finalResponse: "",
finalResponsePreview: "",
finalResponseChars: 0,
judge: null,
currentPlaceholder: true,
},
];
}
function currentFirstAttempts(task: any, attempts: any[]): any[] {
const rows = [...(Array.isArray(attempts) ? attempts : [])];
if (!taskIsActive(task)) return rows;
return rows.sort((left, right) => {
const leftIndex = Number(left?.index);
const rightIndex = Number(right?.index);
const leftCurrent = attemptIsCurrent(task, leftIndex) ? 0 : 1;
const rightCurrent = attemptIsCurrent(task, rightIndex) ? 0 : 1;
if (leftCurrent !== rightCurrent) return leftCurrent - rightCurrent;
return leftIndex - rightIndex;
});
}
function attemptExecutionSummary(task: any, attempt: any): AnyRecord {
@@ -807,11 +932,13 @@ function attemptExecutionUpdatedAt(task: any, attempt: any, attemptIndex: any, e
function attemptFinalResponseText(task: any, attempt: any): string {
const text = String(attempt?.finalResponse || attempt?.finalResponsePreview || "");
if (attemptIsActiveCurrent(task, attempt?.index)) return "";
if (Object.prototype.hasOwnProperty.call(attempt || {}, "finalResponse") || Object.prototype.hasOwnProperty.call(attempt || {}, "finalResponsePreview")) return text.trimEnd();
return text.length > 0 ? text.trimEnd() : taskFinalResponseText(task);
}
function attemptJudge(task: any, attempt: any): AnyRecord | null {
function attemptJudge(task: any, attempt: any, attemptIndex: any = attempt?.index): AnyRecord | null {
if (attemptIsActiveCurrent(task, attemptIndex)) return null;
if (Object.prototype.hasOwnProperty.call(attempt || {}, "judge")) return objectRecord(attempt?.judge);
return taskLastJudge(task);
}
@@ -821,12 +948,9 @@ function attemptExecutionIsRunning(task: any, attempt: any, attemptIndex: any):
if (isSyntheticAttemptSegment(attempt, attemptIndex)) return false;
if (attempt?.finishedAt) return false;
if (["succeeded", "failed", "canceled"].includes(String(attempt?.terminalStatus || ""))) return false;
const summary = taskTraceSummary(task);
const currentAttempt = Number(summary?.currentAttempt || task?.currentAttempt || 0);
const numericAttempt = Number(attemptIndex);
if (Number.isFinite(numericAttempt) && numericAttempt > 0 && Number.isFinite(currentAttempt) && currentAttempt > 0) {
return numericAttempt === currentAttempt;
}
if (attemptIsCurrent(task, attemptIndex)) return true;
const currentAttempt = Number(taskTraceSummary(task)?.currentAttempt || task?.currentAttempt || 0);
if (Number.isFinite(currentAttempt) && currentAttempt > 0) return false;
return true;
}
@@ -850,7 +974,7 @@ function attemptFeedbackPrompt(task: any, attempt: any, attemptIndex: any): AnyR
truncated: Boolean(attempt?.feedbackPromptTruncated),
};
}
const judge = attemptJudge(task, attempt);
const judge = attemptJudge(task, attempt, attemptIndex);
const continuePrompt = String(judge?.continuePrompt || "").trimEnd();
if (judge?.decision === "retry" && continuePrompt.length > 0) {
return {
@@ -1485,9 +1609,8 @@ function TaskCard({ task, selected, onSelect, onCopy, onReference, onMarkRead, c
const unread = taskIsUnreadTerminal(task);
const updatedAt = latestTimestampValue(task?.updatedAt, taskTraceSummary(task)?.updatedAt);
const recentUpdateLabel = `最近更新: ${fmtRelativeAge(updatedAt)}`;
const stepCount = taskStepCount(task);
const stepLabel = stepCount === null ? "--" : String(stepCount);
const stepTitle = stepCount === null ? "STEP 统计中心同步中" : "STEP 来自 OA Event Flow 统计中心";
const stepState = traceStepCountState(task);
const stepTitle = stepState.title;
return h("article", {
role: "button",
tabIndex: 0,
@@ -1551,7 +1674,7 @@ function TaskCard({ task, selected, onSelect, onCopy, onReference, onMarkRead, c
h("span", null, taskDurationLabel(task)),
),
h("div", { className: "codex-task-meta codex-task-update-meta" },
h("span", { className: "codex-task-recent-update codex-task-step-count", title: stepTitle, "data-testid": `codex-task-step-count-${taskId || "unknown"}` }, `STEP ${stepLabel}`),
h("span", { className: `codex-task-recent-update codex-task-step-count ${stepState.state}`, title: stepTitle, "data-testid": `codex-task-step-count-${taskId || "unknown"}`, "data-step-state": stepState.state, "data-step-source": stepState.source }, traceStepCountLabel(stepState)),
h("span", { className: "codex-task-recent-update", title: updatedAt ? `更新时间: ${fmtDate(updatedAt)}` : recentUpdateLabel, "data-testid": `codex-task-recent-update-${taskId || "unknown"}` }, recentUpdateLabel),
h("span", null, fmtDate(updatedAt || task?.updatedAt)),
),
@@ -1871,13 +1994,30 @@ function ProgressiveExecutionSummary({ task, attempt, attemptIndex, loading, onL
const stepDetails = taskTraceStepDetails(task);
const stepsLoaded = taskTraceStepsLoaded(task, attemptIndex);
const errorCount = statCount(stats?.errorCount);
const toolCount = statCount(stats?.stepCount ?? stats?.llmStepCount);
const fallbackStepCount = rawTraceStepCountFallback(
steps.length > 0 ? steps.length : null,
attempt?.stepCount,
attempt?.execution?.stepCount,
execution.traceLineCount,
execution.stepCount,
attempt?.outputCount,
);
const statsStepCount = statCount(stats?.stepCount ?? stats?.llmStepCount);
const toolCount = statsStepCount ?? fallbackStepCount;
const readCount = statCount(stats?.readCount);
const editCount = statCount(stats?.editCount);
const runCount = statCount(stats?.runCount);
const stepCount = statCount(stats?.stepCount ?? stats?.llmStepCount);
const toolsLabel = toolCount === null ? "--" : String(toolCount);
const stepLabel = stepCount === null ? "--" : String(stepCount);
const stepCount = statsStepCount ?? fallbackStepCount;
const countSource = statsStepCount !== null ? "oa-event-flow" : fallbackStepCount !== null ? "raw-trace" : taskIsActive(task) ? "syncing" : "unavailable";
const toolsLabel = toolCount === null ? (countSource === "syncing" ? "sync" : "N/A") : String(toolCount);
const stepLabel = stepCount === null ? (countSource === "syncing" ? "sync" : "N/A") : `${stepCount}${countSource === "raw-trace" ? " raw" : ""}`;
const stepTitle = countSource === "oa-event-flow"
? "STEP 来自 OA Event Flow 统计中心"
: countSource === "raw-trace"
? "OA STEP 统计不可用,当前显示 raw trace fallback 行数"
: countSource === "syncing"
? "STEP 统计同步中;展开后会按需读取当前 attempt trace"
: "当前 attempt 没有可用 STEP 统计";
const editedFiles = Array.isArray(execution.editedFiles) ? execution.editedFiles : [];
const commands = Array.isArray(execution.commands) ? execution.commands : [];
const synthetic = isSyntheticAttemptSegment(attempt, attemptIndex);
@@ -1899,14 +2039,14 @@ function ProgressiveExecutionSummary({ task, attempt, attemptIndex, loading, onL
h("span", { className: "codex-output-channel" }, "Summary"),
h("strong", null, `执行过程摘要${labelSuffix}`),
running ? h("span", { className: "codex-summary-running-pill", "data-testid": `${testId}-running` }, "执行中") : null,
h("code", { title: updatedAt ? `最近更新: ${fmtDate(updatedAt)}` : recentUpdateLabel },
h("code", { title: `${updatedAt ? `最近更新: ${fmtDate(updatedAt)}` : recentUpdateLabel}${stepTitle}` },
`${fmtDuration(execution.durationMs ?? execution.totalElapsedMs)} / ${toolsLabel} tools / ${recentUpdateLabel}`),
),
h("div", { className: "codex-execution-digest" },
h("span", { title: "来自 OA Event Flow 统计中心" }, `read ${readCount === null ? "--" : readCount}`),
h("span", { title: "来自 OA Event Flow 统计中心" }, `edit ${editCount === null ? "--" : editCount}`),
h("span", { title: "来自 OA Event Flow 统计中心" }, `run ${runCount === null ? "--" : runCount}`),
h("span", { title: "来自 OA Event Flow 统计中心" }, `STEP ${stepLabel}`),
h("span", { title: stepTitle, "data-testid": `${testId}-step-count`, "data-step-state": countSource }, `STEP ${stepLabel}`),
errorCount !== null && errorCount > 0 ? h("span", { className: "codex-execution-error-pill", "data-testid": `${testId}-error-count` }, `Error ${errorCount}`) : null,
),
),
@@ -1982,7 +2122,7 @@ function ProgressiveFinalResponse({ task, attempt, attemptIndex, testId = "codex
}
function ProgressiveJudge({ task, attempt, attemptIndex, testId = "codex-progressive-judge" }: AnyRecord) {
const judge = attemptJudge(task, attempt);
const judge = attemptJudge(task, attempt, attemptIndex);
if (!judge?.decision) return null;
const labelSuffix = attemptIndex ? ` #${attemptIndex}` : "";
return h("section", { className: "codex-progressive-card codex-progressive-judge", "data-testid": testId, "data-attempt-index": attemptDataIndex(attemptIndex) },
@@ -2036,7 +2176,8 @@ function ProgressiveJudgeFeedbackPrompt({ task, attempt, attemptIndex, loading,
function ProgressiveAttemptCycle({ task, attempt, position, loading, onLoadPromptPart, onLoadSteps, onLoadStep }: AnyRecord) {
const attemptIndex = attemptSegmentIndex(attempt, position);
const first = position === 0;
const current = attemptIsActiveCurrent(task, attemptIndex);
const first = position === 0 && !current;
const synthetic = isSyntheticAttemptSegment(attempt, attemptIndex);
const title = synthetic ? String(attempt?.label || "Recovered thread execution") : `Attempt ${attemptIndex}`;
return h("section", { className: "codex-attempt-cycle", "data-testid": `codex-attempt-cycle-${attemptIndex}` },
@@ -2053,19 +2194,19 @@ function ProgressiveAttemptCycle({ task, attempt, position, loading, onLoadPromp
loading,
onLoadSteps,
onLoadStep,
testId: first ? "codex-execution-summary" : `codex-execution-summary-attempt-${attemptIndex}`,
testId: current ? "codex-execution-summary-current" : first ? "codex-execution-summary" : `codex-execution-summary-attempt-${attemptIndex}`,
}),
synthetic ? null : h(ProgressiveFinalResponse, {
task,
attempt,
attemptIndex,
testId: first ? "codex-final-response" : `codex-final-response-attempt-${attemptIndex}`,
testId: current ? "codex-final-response-current" : first ? "codex-final-response" : `codex-final-response-attempt-${attemptIndex}`,
}),
synthetic ? null : h(ProgressiveJudge, {
task,
attempt,
attemptIndex,
testId: first ? "codex-progressive-judge" : `codex-progressive-judge-attempt-${attemptIndex}`,
testId: current ? "codex-progressive-judge-current" : first ? "codex-progressive-judge" : `codex-progressive-judge-attempt-${attemptIndex}`,
}),
synthetic ? null : h(ProgressiveJudgeFeedbackPrompt, {
task,
@@ -2073,7 +2214,7 @@ function ProgressiveAttemptCycle({ task, attempt, position, loading, onLoadPromp
attemptIndex,
loading,
onLoadPromptPart,
testId: first ? "codex-judge-feedback-prompt" : `codex-judge-feedback-prompt-attempt-${attemptIndex}`,
testId: current ? "codex-judge-feedback-prompt-current" : first ? "codex-judge-feedback-prompt" : `codex-judge-feedback-prompt-attempt-${attemptIndex}`,
}),
);
}
@@ -3682,7 +3823,7 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi
if (selectedIdRef.current === taskId && (normalizedEvent?.type === "trace-step-created" || normalizedEvent?.type === "task-updated" || normalizedEvent?.type === "trace-stats-updated")) {
const previousStepCount = canonicalTaskStepCount(previousTask);
const shouldRefreshSteps = normalizedEvent?.type === "trace-step-created"
|| (normalizedEvent?.type === "trace-stats-updated" && !isAttemptStats && Number.isFinite(Number(normalizedEvent?.stepCount)) && (previousStepCount === null || stepCount > previousStepCount));
|| (normalizedEvent?.type === "trace-stats-updated" && Number.isFinite(Number(normalizedEvent?.stepCount)) && (isAttemptStats || previousStepCount === null || stepCount > previousStepCount));
scheduleSelectedTraceEventRefresh(taskId, shouldRefreshSteps);
}
}
@@ -3741,6 +3882,18 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi
void ensureTraceSummary(taskId, true).catch((err) => setError(errorText(err, "自动加载 Trace Summary 失败")));
}, [service?.id, selectedTask?.id, selectedTask?.updatedAt, selectedTask?.traceStats?.statsRevision, selectedTask?._traceSummaryUpdatedAt, selectedTask?._traceSummaryLoaded, selectedDetailLoading]);
useEffect(() => {
if (!service || !selectedTask || selectedDetailLoading) return;
const taskId = String(selectedTask.id || "");
const currentAttempt = Number(taskTraceSummary(selectedTask)?.currentAttempt || selectedTask?.currentAttempt || 0);
if (!taskId || !taskIsExecuting(selectedTask) || !Number.isFinite(currentAttempt) || currentAttempt <= 0) return;
if (taskTraceStepsLoaded(selectedTask, currentAttempt)) return;
const key = `${taskId}:current-attempt:${currentAttempt}:${String(selectedTask?.updatedAt || "")}`;
if (autoTraceLoadKeysRef.current.has(key)) return;
autoTraceLoadKeysRef.current.add(key);
void ensureTraceSteps(currentAttempt).catch((err) => setError(errorText(err, "自动加载当前 Attempt Trace Steps 失败")));
}, [service?.id, selectedTask?.id, selectedTask?.status, selectedTask?.currentAttempt, selectedTask?.updatedAt, selectedTask?._traceSummaryUpdatedAt, selectedDetailLoading]);
useEffect(() => {
if (!service) return undefined;
void loadWorkdirs().catch((err) => setError(errorText(err, "加载工作目录失败")));