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
+322 -1
View File
@@ -155,6 +155,9 @@ const FRONTEND_CHECK_NAMES = [
"frontend:code-queue-trace-full-load",
"frontend:code-queue-judge-wrap",
"frontend:code-queue-error-red-markers",
"frontend:code-queue-retry-attempt-trace-current",
"frontend:code-queue-step-missing-diagnostic",
"frontend:code-queue-judge-feedback-attempt-order",
"frontend:claudeqq-integrated-visible",
"frontend:url-route-deeplink",
"frontend:pipeline-integrated-visible",
@@ -978,6 +981,288 @@ async function runCodeQueueLongPromptObservation(page: Page): Promise<any> {
};
}
async function runCodeQueueRetryTraceFixture(page: Page, frontendUrl: string): Promise<any> {
const taskId = "codex_2000000000000_issue12_retry_fixture";
const now = new Date().toISOString();
const attempt1Started = "2026-05-20T03:00:00.000Z";
const attempt1Finished = "2026-05-20T03:04:00.000Z";
const attempt2Started = "2026-05-20T03:05:00.000Z";
const traceRequests: string[] = [];
const promptRequests: string[] = [];
const task = {
id: taskId,
queueId: "issue12-fixture",
queueEnteredAt: attempt2Started,
displayPrompt: "Issue #12 retry current attempt fixture",
basePrompt: "Issue #12 retry current attempt fixture",
displayPromptPreview: "Issue #12 retry current attempt fixture",
providerId: "D601-dev",
executionMode: "default",
cwd: "/workspace-dev",
model: "gpt-5.5",
maxAttempts: 99,
status: "running",
createdAt: "2026-05-20T02:59:00.000Z",
updatedAt: now,
startedAt: attempt1Started,
finishedAt: null,
currentAttempt: 2,
currentMode: "retry",
codexThreadId: "thread_issue12_fixture",
activeTurnId: "turn_issue12_attempt_2",
finalResponse: "",
lastJudge: {
decision: "retry",
confidence: 0.82,
reason: "Attempt 1 judge: missing live verification.",
continuePrompt: "Continue with attempt 2 and gather live trace evidence.",
source: "fixture",
},
stepCount: 0,
llmStepCount: 0,
outputCount: 0,
eventCount: 0,
attemptCount: 1,
attempts: [{
index: 1,
mode: "initial",
startedAt: attempt1Started,
finishedAt: attempt1Finished,
terminalStatus: "completed",
finalResponsePreview: "Attempt 1 stopped after planning.",
finalResponseChars: 33,
judge: {
decision: "retry",
confidence: 0.82,
reason: "Attempt 1 judge: missing live verification.",
continuePrompt: "Continue with attempt 2 and gather live trace evidence.",
source: "fixture",
},
feedbackPromptPreview: "Continue with attempt 2 and gather live trace evidence.",
feedbackPromptChars: 56,
feedbackPromptLines: 1,
feedbackPromptSource: "judge-continue-prompt",
feedbackPromptForAttempt: 2,
statsSource: "unavailable",
traceStats: null,
execution: {
durationMs: 240000,
traceLineCount: 5,
stepCount: 5,
editedFiles: [],
commands: ["bun test"],
statsSource: "unavailable",
traceStats: null,
},
}],
summaryOnly: true,
promptEditable: false,
timing: { durationMs: 330000, totalElapsedMs: 390000 },
};
const summary = {
id: taskId,
taskId,
queueId: "issue12-fixture",
status: "running",
providerId: "D601-dev",
executionMode: "default",
model: "gpt-5.5",
cwd: "/workspace-dev",
createdAt: task.createdAt,
startedAt: task.startedAt,
finishedAt: null,
updatedAt: now,
currentAttempt: 2,
maxAttempts: 99,
stepCount: null,
llmStepCount: null,
retainedStepCount: 0,
traceStats: null,
statsSource: "unavailable",
prompt: {
basePrompt: task.basePrompt,
basePromptLines: 1,
promptChars: task.basePrompt.length,
promptLines: 1,
},
execution: {
durationMs: 390000,
traceLineCount: 0,
stepCount: null,
editedFiles: [],
commands: [],
statsSource: "unavailable",
traceStats: null,
},
finalResponse: "",
lastJudge: task.lastJudge,
attempts: task.attempts,
timing: task.timing,
};
const overview = {
ok: true,
queue: {
activeTaskId: taskId,
activeTaskIds: [taskId],
total: 1,
unreadTerminal: 0,
counts: { running: 1, queued: 0, retry_wait: 0, judging: 0 },
queues: [{ id: "issue12-fixture", name: "Issue 12 Fixture", total: 1, activeTaskId: taskId, counts: { running: 1 } }],
mainProviderId: "D601-dev",
defaultProviderId: "D601-dev",
defaultWorkdir: "/workspace-dev",
defaultWorkdirByProvider: { "D601-dev": "/workspace-dev" },
codeModels: ["gpt-5.5", "gpt-5.4-mini", "gpt-5.4"],
executionProviders: [{ id: "D601-dev", label: "D601 dev", defaultWorkdir: "/workspace-dev" }],
executionModes: [{ id: "default", label: "默认容器/本机" }],
},
statistics: { timezone: "Asia/Shanghai", range: { startDate: "2026-05-20", endDate: "2026-05-20" }, totals: {}, daily: [] },
tasks: [task],
pagination: { returned: 1, total: 1, hasMore: false },
selected: { task, transcript: [], hasMore: false, preview: true },
};
const attempt2Steps = [
{
seq: 201,
at: "2026-05-20T03:05:20.000Z",
kind: "message",
title: "Assistant message",
status: "streaming",
rawSeqs: [201],
summaryLines: ["Attempt 2 running: fixture trace is visible."],
hasDetail: true,
},
{
seq: 202,
at: "2026-05-20T03:05:40.000Z",
kind: "ran",
title: "Ran bun --check target files",
status: "running",
rawSeqs: [202],
summaryLines: ["Attempt 2 running command trace should appear before old judge."],
hasDetail: true,
},
];
const routePattern = "**/api/microservices/code-queue/proxy/api/**";
const handler = async (route: any, request: any): Promise<void> => {
if (request.method() !== "GET") {
await route.continue();
return;
}
const url = new URL(request.url());
const path = url.pathname.replace(/^\/api\/microservices\/code-queue\/proxy/u, "");
if (path === "/api/tasks/overview") {
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(overview) });
return;
}
if (path === "/api/workdirs") {
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ ok: true, workdirs: [{ providerId: "D601-dev", executionMode: "default", path: "/workspace-dev", source: "fixture" }] }) });
return;
}
if (path === "/api/queues") {
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ ok: true, queues: overview.queue.queues, summary: overview.queue }) });
return;
}
if (path === `/api/tasks/${encodeURIComponent(taskId)}/trace-summary` || path === `/api/tasks/${taskId}/trace-summary`) {
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ ok: true, summary }) });
return;
}
if (path === `/api/tasks/${encodeURIComponent(taskId)}/trace-steps` || path === `/api/tasks/${taskId}/trace-steps`) {
traceRequests.push(url.search);
const attempt = url.searchParams.get("attempt") || "";
const steps = attempt === "2" ? attempt2Steps : [];
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ ok: true, taskId, attempt: attempt ? Number(attempt) : null, source: "fixture", steps, total: steps.length, hasMore: false, nextAfterSeq: steps.at(-1)?.seq || 0 }) });
return;
}
if (path === `/api/tasks/${encodeURIComponent(taskId)}/prompt` || path === `/api/tasks/${taskId}/prompt`) {
promptRequests.push(url.search);
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ ok: true, part: url.searchParams.get("part"), attempt: Number(url.searchParams.get("attempt") || 0), text: "Continue with attempt 2 and gather live trace evidence.", chars: 56, lines: 1, forAttempt: 2 }) });
return;
}
await route.continue();
};
await page.route(routePattern, handler);
try {
await page.goto(`${frontendUrl}/app/code-queue/`, { waitUntil: "domcontentloaded", timeout: 15000 });
await page.waitForSelector('[data-testid="code-queue-page"]', { timeout: 15000 });
await page.waitForSelector(`[data-testid="codex-task-${taskId}"]`, { timeout: 15000 });
await page.waitForSelector('[data-testid="codex-execution-summary-current"]', { timeout: 15000 });
await page.waitForFunction(() => {
const current = document.querySelector('[data-testid="codex-execution-summary-current"]') as HTMLDetailsElement | null;
const text = current?.textContent || "";
return current?.getAttribute("data-attempt-index") === "2"
&& current?.getAttribute("data-running") === "true"
&& text.includes("Attempt 2 running");
}, undefined, { timeout: 15000 });
const beforeExpand = await page.evaluate((id) => {
const card = document.querySelector(`[data-testid="codex-task-${id}"]`) as HTMLElement | null;
const current = document.querySelector('[data-testid="codex-execution-summary-current"]') as HTMLElement | null;
const attempt1Judge = document.querySelector('[data-testid="codex-progressive-judge-attempt-1"]') as HTMLElement | null;
const feedback = document.querySelector('[data-testid="codex-judge-feedback-prompt-attempt-1"]') as HTMLElement | null;
const currentStep = document.querySelector('[data-testid="codex-execution-summary-current-step-count"]') as HTMLElement | null;
const root = document.querySelector('[data-testid="codex-output"]') as HTMLElement | null;
const order = ["codex-execution-summary-current", "codex-progressive-judge-attempt-1", "codex-judge-feedback-prompt-attempt-1"]
.map((testId) => {
const element = document.querySelector(`[data-testid="${testId}"]`);
return element && root ? Array.prototype.indexOf.call(root.querySelectorAll("[data-testid]"), element) : -1;
});
return {
cardText: card?.innerText || "",
cardAttemptVisible: (card?.innerText || "").includes("2/99"),
cardStepText: card?.querySelector('[data-testid^="codex-task-step-count-"]')?.textContent || "",
cardStepState: card?.querySelector('[data-testid^="codex-task-step-count-"]')?.getAttribute("data-step-state") || "",
currentText: current?.textContent || "",
currentAttempt: current?.getAttribute("data-attempt-index") || "",
currentRunning: current?.getAttribute("data-running") || "",
currentStepText: currentStep?.textContent || "",
currentStepState: currentStep?.getAttribute("data-step-state") || "",
attempt1JudgeText: attempt1Judge?.textContent || "",
feedbackText: feedback?.textContent || "",
currentBeforeJudge: order[0] >= 0 && order[1] >= 0 && order[0] < order[1],
judgeBeforeFeedback: order[1] >= 0 && order[2] >= 0 && order[1] < order[2],
currentJudgeMissing: document.querySelector('[data-testid="codex-progressive-judge-current"]') === null,
legacyCurrentAliasMissing: document.querySelector('[data-testid="codex-progressive-judge"]') === null || (document.querySelector('[data-testid="codex-progressive-judge"]') as HTMLElement | null)?.getAttribute("data-attempt-index") !== "2",
};
}, taskId);
await page.locator('[data-testid="codex-judge-feedback-prompt-attempt-1"] summary').click();
await page.waitForFunction(() => (document.querySelector('[data-testid="codex-judge-feedback-prompt-attempt-1"]') as HTMLDetailsElement | null)?.open === true, undefined, { timeout: 5000 });
const feedbackText = await page.getByTestId("codex-judge-feedback-prompt-attempt-1-text").innerText({ timeout: 5000 });
const currentStepRows = await page.locator('[data-testid="codex-execution-summary-current"] .codex-trace-step').count();
return {
checked: true,
taskId,
fixture: {
cardAttempt: "2/99",
summaryAttempts: summary.attempts.length,
attempt2Steps: attempt2Steps.length,
},
beforeExpand,
currentStepRows,
traceRequests,
promptRequests,
feedbackText,
ok: beforeExpand.cardAttemptVisible === true
&& beforeExpand.currentAttempt === "2"
&& beforeExpand.currentRunning === "true"
&& beforeExpand.currentText.includes("Attempt 2 running")
&& beforeExpand.currentBeforeJudge === true
&& beforeExpand.judgeBeforeFeedback === true
&& beforeExpand.currentJudgeMissing === true
&& beforeExpand.cardStepText !== "STEP --"
&& beforeExpand.currentStepText !== "STEP --"
&& ["syncing", "raw-trace"].includes(beforeExpand.cardStepState)
&& ["syncing", "raw-trace"].includes(beforeExpand.currentStepState)
&& currentStepRows >= 2
&& traceRequests.some((search) => search.includes("attempt=2"))
&& promptRequests.some((search) => search.includes("part=feedback") && search.includes("attempt=1"))
&& feedbackText.includes("Continue with attempt 2"),
};
} finally {
await page.unroute(routePattern, handler).catch(() => undefined);
}
}
function runPsql(config: UniDeskConfig, sql: string): { ok: boolean; stdout: string; stderr: string; exitCode: number | null } {
const result = runCommand([
"docker",
@@ -1885,6 +2170,9 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
"frontend:code-queue-trace-full-load",
"frontend:code-queue-judge-wrap",
"frontend:code-queue-error-red-markers",
"frontend:code-queue-retry-attempt-trace-current",
"frontend:code-queue-step-missing-diagnostic",
"frontend:code-queue-judge-feedback-attempt-order",
]);
const needCodeQueueFullSurface = wantsAny([
"frontend:code-queue-integrated-visible",
@@ -1895,6 +2183,11 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
"frontend:code-queue-judge-wrap",
"frontend:code-queue-error-red-markers",
]);
const needCodeQueueRetryTraceFixture = wantsAny([
"frontend:code-queue-retry-attempt-trace-current",
"frontend:code-queue-step-missing-diagnostic",
"frontend:code-queue-judge-feedback-attempt-order",
]);
const needClaudeqq = wants("frontend:claudeqq-integrated-visible");
const needRouteDeepLink = wants("frontend:url-route-deeplink");
const needPipeline = wantsAny([
@@ -2004,6 +2297,7 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
let codexTraceFullMetrics: any = { candidateFound: false };
let codexJudgeWrapMetrics: any = { checked: false };
let codeQueueErrorHighlightMetrics: any = { checked: false, candidateFound: false };
let codeQueueRetryTraceFixtureMetrics: any = { checked: false };
let claudeqqText = "";
let routeDeepLinkText = "";
let routeInitialPath = "";
@@ -3002,10 +3296,13 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
const cards = Array.from(document.querySelectorAll('[data-testid^="codex-task-codex_"]')).map((node) => (node as HTMLElement).innerText);
return cards.length === 0 || cards.every((text) => text.includes(`queue=${queueId}`));
}, targetQueueId, { timeout: 15000 });
codeQueueSwitchMetrics = { ...codeQueueSwitchMetrics, targetQueueId, switched: true };
codeQueueSwitchMetrics = { ...codeQueueSwitchMetrics, targetQueueId, switched: true };
await page.getByTestId("code-queue-filter-select").selectOption("__all__");
}
}
if (needCodeQueueRetryTraceFixture) {
codeQueueRetryTraceFixtureMetrics = await runCodeQueueRetryTraceFixture(page, urls.frontendUrl);
}
if (needClaudeqq) {
await page.getByRole("button", { name: /ClaudeQQ/ }).click();
await page.waitForSelector('[data-testid="claudeqq-page"]', { timeout: 10000 });
@@ -3664,6 +3961,30 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
addSelectedCheck(checks, options, "frontend:code-queue-judge-wrap",
codexJudgeWrapMetrics.checked === true && codexJudgeWrapMetrics.ok === true,
{ codexJudgeWrapMetrics });
addSelectedCheck(checks, options, "frontend:code-queue-retry-attempt-trace-current",
codeQueueRetryTraceFixtureMetrics.checked === true
&& codeQueueRetryTraceFixtureMetrics.ok === true
&& codeQueueRetryTraceFixtureMetrics.beforeExpand?.cardAttemptVisible === true
&& codeQueueRetryTraceFixtureMetrics.beforeExpand?.currentAttempt === "2"
&& codeQueueRetryTraceFixtureMetrics.beforeExpand?.currentRunning === "true"
&& Number(codeQueueRetryTraceFixtureMetrics.currentStepRows || 0) >= 2
&& (codeQueueRetryTraceFixtureMetrics.traceRequests || []).some((search: string) => String(search).includes("attempt=2")),
{ codeQueueRetryTraceFixtureMetrics });
addSelectedCheck(checks, options, "frontend:code-queue-step-missing-diagnostic",
codeQueueRetryTraceFixtureMetrics.checked === true
&& codeQueueRetryTraceFixtureMetrics.beforeExpand?.cardStepText !== "STEP --"
&& codeQueueRetryTraceFixtureMetrics.beforeExpand?.currentStepText !== "STEP --"
&& ["syncing", "raw-trace"].includes(String(codeQueueRetryTraceFixtureMetrics.beforeExpand?.cardStepState || ""))
&& ["syncing", "raw-trace"].includes(String(codeQueueRetryTraceFixtureMetrics.beforeExpand?.currentStepState || "")),
{ codeQueueRetryTraceFixtureMetrics });
addSelectedCheck(checks, options, "frontend:code-queue-judge-feedback-attempt-order",
codeQueueRetryTraceFixtureMetrics.checked === true
&& codeQueueRetryTraceFixtureMetrics.beforeExpand?.currentBeforeJudge === true
&& codeQueueRetryTraceFixtureMetrics.beforeExpand?.judgeBeforeFeedback === true
&& codeQueueRetryTraceFixtureMetrics.beforeExpand?.currentJudgeMissing === true
&& String(codeQueueRetryTraceFixtureMetrics.feedbackText || "").includes("Continue with attempt 2")
&& (codeQueueRetryTraceFixtureMetrics.promptRequests || []).some((search: string) => String(search).includes("part=feedback") && String(search).includes("attempt=1")),
{ codeQueueRetryTraceFixtureMetrics });
addSelectedCheck(checks, options, "frontend:claudeqq-integrated-visible", claudeqqTextLower.includes("claudeqq 工作台") && claudeqqText.includes("D601") && claudeqqText.includes("D601 k3s Service") && claudeqqText.includes("k3s://unidesk/claudeqq") && claudeqqText.includes("QQ 事件订阅") && claudeqqText.includes("消息推送") && claudeqqText.includes("事件缓存") && claudeqqText.includes("主用户私聊账号") && claudeqqText.includes("645275593") && claudeqqTextLower.includes("napcat 容器登录") && claudeqqText.includes("已登录") && /health\s+ok/i.test(claudeqqText) && claudeqqText.includes("仅 UniDesk frontend 代理访问") && !claudeqqText.includes("{\n"), { claudeqqTextPreview: claudeqqText.slice(0, 1400) });
addSelectedCheck(checks, options, "frontend:url-route-deeplink", routeInitialPath === "/app/pipeline/" && routeDockerPath === "/nodes/docker/" && routeBackPath === "/app/pipeline/" && routeOverviewPath === "/ops/status/" && routeCodexPath === "/app/code-queue/" && routeDeepLinkText.toLowerCase().includes("pipeline v2 工作台".toLowerCase()) && routeOverviewText.includes("核心指标") && routeCodexShellMetrics.appShell === true && routeCodexShellMetrics.standalone === false && routeCodexShellMetrics.topbar === true && routeCodexShellMetrics.codexPage === true && String(routeCodexShellMetrics.railText || "").includes("用户服务") && String(routeCodexShellMetrics.tabsText || "").includes("Code Queue"), { routeInitialPath, routeDockerPath, routeBackIntermediatePath, routeBackPath, routeOverviewPath, routeCodexPath, routeCodexShellMetrics, routeDeepLinkPreview: routeDeepLinkText.slice(0, 1200), routeOverviewPreview: routeOverviewText.slice(0, 800) });
addSelectedCheck(checks, options, "frontend:pipeline-integrated-visible",