Merge pull request #443 from pikasTech/fix/code-agent-wait-state-224-143-99

fix: 收口 Code Agent 长请求等待体验
This commit is contained in:
Lyon
2026-05-24 16:59:57 +08:00
committed by GitHub
5 changed files with 180 additions and 15 deletions
+20 -1
View File
@@ -449,6 +449,25 @@ test("Code Agent browser classifier distinguishes runner busy, session blocked,
assert.equal(sessionBlocked.blocker, "session-blocked");
assert.equal(sessionBlocked.category, "session_blocked");
const codexRuntimeBlocked = classifyCodeAgentBrowserJourney({
responseSummary: sanitizeAgentChatBody({
...base,
error: {
code: "codex_stdio_failed",
message: "Codex app-server turn finished with status=failed"
}
}, { httpStatus: 200 }),
httpOk: true,
httpStatus: 200,
ui: {
agentChatStatus: "Runner 受阻",
completedMessageVisible: false,
failedMessageVisible: true
}
});
assert.equal(codexRuntimeBlocked.blocker, "runner-blocked");
assert.equal(codexRuntimeBlocked.category, "runner_blocked");
const apiError = classifyCodeAgentBrowserJourney({
responseSummary: sanitizeAgentChatBody({
status: "failed",
@@ -461,7 +480,7 @@ test("Code Agent browser classifier distinguishes runner busy, session blocked,
httpOk: false,
httpStatus: 502,
ui: {
agentChatStatus: "发送失败",
agentChatStatus: "后端失败",
completedMessageVisible: false,
failedMessageVisible: true
}
+15 -2
View File
@@ -4,7 +4,7 @@ const partialRunnerProviders = new Set(["codex-readonly-runner"]);
const readonlySessionCapabilityLevels = new Set(["read-only-tools", "read-only-session-tools"]);
const untrustedProviderPattern = /\b(?:echo|mock|fixture|stub|sample)\b/iu;
const blockedCodeAgentUiLabels = new Set([
"发送失败",
"后端失败",
"服务受阻",
"等待超时",
"Provider 不可用",
@@ -484,7 +484,20 @@ export function classifyCodeAgentRuntimeBlock(payload, { httpStatus = null } = {
};
}
if (["runner_unavailable", "tool_unavailable", "skills_unavailable", "security_blocked", "external_network_blocked", "network_tool_unavailable", "network_timeout"].includes(errorCode)) {
if ([
"runner_unavailable",
"tool_unavailable",
"skills_unavailable",
"security_blocked",
"codex_stdio_blocked",
"codex_stdio_failed",
"codex_stdio_protocol_blocked",
"codex_stdio_empty_response",
"codex_stdio_command_probe_failed",
"external_network_blocked",
"network_tool_unavailable",
"network_timeout"
].includes(errorCode)) {
return {
blocked: true,
status: "blocked",
+78 -5
View File
@@ -329,6 +329,7 @@ const requiredCodeAgentEvidenceTerms = Object.freeze([
]);
const blockedCodeAgentUiLabels = Object.freeze([
"后端失败",
"服务受阻",
"等待超时",
"Provider 不可用",
@@ -654,7 +655,7 @@ function runStaticSmoke() {
evidence: [
"DEV-LIVE 回复",
"SOURCE 回复",
"等待超时/API 错误/发送失败",
"等待超时/API 错误/后端失败",
"服务受阻 or legacy BLOCKED 凭证缺口",
"message-trace/details/events"
]
@@ -4222,6 +4223,31 @@ async function inspectJourneyControls(page) {
async function inspectJourneyUi(page) {
return page.evaluate(() => {
const summarizeActions = (statusClass) => {
const cards = [...document.querySelectorAll(`.message-card.${statusClass}`)];
const labels = cards.flatMap((card) =>
[...card.querySelectorAll(".message-action")].map((button) => button.textContent?.replace(/\s+/gu, " ").trim() ?? "")
);
const panels = cards.flatMap((card) =>
[...card.querySelectorAll(".message-actions")].map((panel) => {
const box = panel.getBoundingClientRect();
return {
width: box.width,
left: box.left,
right: box.right,
scrollWidth: panel.scrollWidth,
clientWidth: panel.clientWidth,
contained: box.left >= -1 && box.right <= window.innerWidth + 1 && panel.scrollWidth <= panel.clientWidth + 2
};
})
);
return {
retryVisible: labels.includes("重试上一条"),
traceVisible: labels.includes("回放 trace"),
cancelVisible: labels.includes("取消当前请求"),
contained: panels.length > 0 && panels.every((panel) => panel.contained)
};
};
const failedText = [...document.querySelectorAll(".message-card.status-failed")]
.map((element) => element.textContent ?? "")
.join("\n");
@@ -4250,6 +4276,9 @@ async function inspectJourneyUi(page) {
contained: box.left >= -1 && box.right <= window.innerWidth + 1 && card.scrollWidth <= card.clientWidth + 2
};
});
const runningActions = summarizeActions("status-running");
const failedActions = summarizeActions("status-failed");
const timeoutActions = summarizeActions("status-timeout");
return {
title: document.title,
workspaceHidden: document.querySelector('[data-view="workspace"]')?.hidden ?? null,
@@ -4271,6 +4300,16 @@ async function inspectJourneyUi(page) {
pendingMessageHasConversation: /conversation\s*cnv_|conversation=cnv_|cnv_/u.test(pendingText),
pendingMessageHasSession: /session\s*(等待后端分配|cnv_|ses_)/u.test(pendingText),
pendingTraceCopyVisible: pendingRows.some((row) => /traceId/u.test(row.text) && /复制/u.test(row.text)),
pendingRetryActionVisible: runningActions.retryVisible,
pendingTraceActionVisible: runningActions.traceVisible,
pendingCancelActionVisible: runningActions.cancelVisible,
pendingActionPanelContained: runningActions.contained,
failedRetryActionVisible: failedActions.retryVisible,
failedTraceActionVisible: failedActions.traceVisible,
failedActionPanelContained: failedActions.contained,
timeoutRetryActionVisible: timeoutActions.retryVisible,
timeoutTraceActionVisible: timeoutActions.traceVisible,
timeoutActionPanelContained: timeoutActions.contained,
pendingContextRowsContained: pendingRows.length > 0 && pendingRows.every((row) => row.contained),
runningCardsContained: runningCardBoxes.length > 0 && runningCardBoxes.every((box) => box.contained),
messageCount: document.querySelectorAll(".message-card").length,
@@ -4324,6 +4363,16 @@ async function inspectLegacyFailureWindow(page, { beforeMessageCount }) {
contained: row.scrollWidth <= row.clientWidth + 2
}))
: [];
const pendingActionLabels = latestAgent
? [...latestAgent.querySelectorAll(".message-action")].map((button) => button.textContent?.replace(/\s+/gu, " ").trim() ?? "")
: [];
const pendingActionPanels = latestAgent
? [...latestAgent.querySelectorAll(".message-actions")].map((panel) => ({
scrollWidth: panel.scrollWidth,
clientWidth: panel.clientWidth,
contained: panel.scrollWidth <= panel.clientWidth + 2
}))
: [];
const latestAgentBox = latestAgent?.getBoundingClientRect();
const pendingStillRunning =
latestAgent?.classList.contains("status-running") === true ||
@@ -4342,6 +4391,9 @@ async function inspectLegacyFailureWindow(page, { beforeMessageCount }) {
pendingConversationVisible: /conversation\s*cnv_|conversation=cnv_|cnv_/u.test(pendingText),
pendingSessionVisible: /session\s*(等待后端分配|cnv_|ses_)/u.test(pendingText),
pendingTraceCopyVisible: pendingRows.some((row) => /traceId/u.test(row.text) && /复制/u.test(row.text)),
pendingRetryActionVisible: pendingActionLabels.includes("重试上一条"),
pendingTraceActionVisible: pendingActionLabels.includes("回放 trace"),
pendingActionsContained: pendingActionPanels.length > 0 && pendingActionPanels.every((panel) => panel.contained),
pendingRowsContained: pendingRows.length > 0 && pendingRows.every((row) => row.contained),
runningCardContained: latestAgentBox
? latestAgentBox.left >= -1 &&
@@ -4678,7 +4730,7 @@ async function runLocalAgentFixtureSmoke({
await page.locator("#command-send").click();
await page.waitForTimeout(Math.min(legacyFailureWindowMs + 350, Math.max(80, timeoutConfigMs ?? 0) + 80));
await page.waitForFunction(
() => ["等待超时", "发送失败"].includes(document.querySelector("#agent-chat-status")?.textContent?.trim() ?? ""),
() => document.querySelector("#agent-chat-status")?.textContent?.trim() === "等待超时",
null,
{ timeout: 8000 }
);
@@ -4711,10 +4763,13 @@ async function runLocalAgentFixtureSmoke({
responseSummary = response ? sanitizeAgentChatBody(responseBody, { httpStatus: response.status() }) : null;
const pass = expectTimeout
? (
["等待超时", "发送失败"].includes(ui.agentChatStatus) &&
ui.agentChatStatus === "等待超时" &&
ui.retryInputPreserved &&
(ui.failedMessageHasTrace || ui.traceHasTraceId) &&
(ui.failedMessageHasChineseTimeout || ui.agentChatStatus === "等待超时") &&
ui.timeoutRetryActionVisible &&
ui.timeoutTraceActionVisible &&
ui.timeoutActionPanelContained &&
!ui.completedMessageVisible
)
: agentFixtureMode === "slow-blocker" || agentFixtureMode === "external-network-blocker"
@@ -4730,6 +4785,9 @@ async function runLocalAgentFixtureSmoke({
ui.failedMessageVisible &&
ui.failedMessageHasTrace &&
ui.retryInputPreserved &&
ui.failedRetryActionVisible &&
ui.failedTraceActionVisible &&
ui.failedActionPanelContained &&
!ui.completedMessageVisible
)
: (
@@ -4746,6 +4804,9 @@ async function runLocalAgentFixtureSmoke({
result.legacyWindow?.pendingConversationVisible === true &&
result.legacyWindow?.pendingSessionVisible === true &&
result.legacyWindow?.pendingTraceCopyVisible === true &&
result.legacyWindow?.pendingRetryActionVisible === true &&
result.legacyWindow?.pendingTraceActionVisible === true &&
result.legacyWindow?.pendingActionsContained === true &&
result.legacyWindow?.pendingRowsContained === true &&
result.legacyWindow?.runningCardContained === true
) &&
@@ -4805,6 +4866,9 @@ async function runLocalAgentFixtureSmoke({
result.legacyWindow?.pendingConversationVisible === true &&
result.legacyWindow?.pendingSessionVisible === true &&
result.legacyWindow?.pendingTraceCopyVisible === true &&
result.legacyWindow?.pendingRetryActionVisible === true &&
result.legacyWindow?.pendingTraceActionVisible === true &&
result.legacyWindow?.pendingActionsContained === true &&
result.legacyWindow?.pendingRowsContained === true &&
result.legacyWindow?.runningCardContained === true
) ? "pass" : "blocked",
@@ -4835,7 +4899,7 @@ async function runLocalAgentFixtureSmoke({
status: pass ? "pass" : "blocked",
summary: agentFixtureMode === "external-network-blocker"
? "External network blocker for a GitHub prompt displays Runner 受阻 with network trace, keeps the user prompt, and does not restore main evidence/facts noise."
: "Delayed structured blocker arrives after the legacy 4500ms window, displays Runner 受阻 instead of 发送失败, keeps trace visible, and leaves the user prompt for continuing the same conversation.",
: "Delayed structured blocker arrives after the legacy 4500ms window, displays Runner 受阻 instead of a generic 后端失败 state, keeps trace visible, and leaves the user prompt for continuing the same conversation.",
observations: {
legacyFailureWindowMs,
fixtureDelayMs: responseDelayMs,
@@ -4946,10 +5010,16 @@ async function inspectLocalAgentPendingViewport(browser, url, { width, height, r
legacyWindow.pendingConversationVisible === true &&
legacyWindow.pendingSessionVisible === true &&
legacyWindow.pendingTraceCopyVisible === true &&
legacyWindow.pendingRetryActionVisible === true &&
legacyWindow.pendingTraceActionVisible === true &&
legacyWindow.pendingActionsContained === true &&
legacyWindow.pendingRowsContained === true &&
legacyWindow.runningCardContained === true &&
ui.runningCardsContained === true &&
ui.pendingContextRowsContained === true,
ui.pendingContextRowsContained === true &&
ui.pendingRetryActionVisible === true &&
ui.pendingTraceActionVisible === true &&
ui.pendingActionPanelContained === true,
legacyWindow,
ui: {
agentChatStatus: ui.agentChatStatus,
@@ -4959,6 +5029,9 @@ async function inspectLocalAgentPendingViewport(browser, url, { width, height, r
pendingMessageHasConversation: ui.pendingMessageHasConversation,
pendingMessageHasSession: ui.pendingMessageHasSession,
pendingTraceCopyVisible: ui.pendingTraceCopyVisible,
pendingRetryActionVisible: ui.pendingRetryActionVisible,
pendingTraceActionVisible: ui.pendingTraceActionVisible,
pendingActionPanelContained: ui.pendingActionPanelContained,
pendingContextRowsContained: ui.pendingContextRowsContained,
runningCardsContained: ui.runningCardsContained
},
+62 -7
View File
@@ -792,8 +792,12 @@ async function submitAgentMessage(value, options = {}) {
renderAgentChatStatus(failedStatus, state.chatMessages[index]);
renderCodeAgentSummary();
} finally {
if (state.currentRequest?.traceId === traceId) state.currentRequest = null;
state.chatPending = false;
if (state.currentRequest?.traceId === traceId) {
state.currentRequest = null;
state.chatPending = false;
} else if (!state.currentRequest) {
state.chatPending = false;
}
stopTraceStream();
renderCodeAgentSummary();
renderConversation();
@@ -2575,7 +2579,7 @@ function renderAgentChatStatus(status, result = null) {
running: "处理中",
completed: "DEV-LIVE 回复",
source: "SOURCE 回复",
failed: "发送失败",
failed: "后端失败",
timeout: "等待超时",
canceled: "已取消",
error: "请求错误",
@@ -2927,7 +2931,20 @@ function agentFailurePresentation(error, { result = null, traceId = null } = {})
};
}
if (["runner_unavailable", "tool_unavailable", "skills_unavailable", "security_blocked"].includes(code)) {
if ([
"runner_unavailable",
"tool_unavailable",
"skills_unavailable",
"security_blocked",
"codex_stdio_blocked",
"codex_stdio_failed",
"codex_stdio_protocol_blocked",
"codex_stdio_empty_response",
"codex_stdio_command_probe_failed",
"external_network_blocked",
"network_tool_unavailable",
"network_timeout"
].includes(code)) {
return {
category: "runner_blocked",
title: "Code Agent Runner 受阻",
@@ -2969,6 +2986,8 @@ function titleForBlockerCategory(category, code) {
if (value === "canceled" || code === "codex_stdio_canceled") return "Code Agent 已取消";
if (value === "runner_busy") return "Code Agent Runner 忙碌";
if (value === "session_blocked") return "Code Agent Session 受阻";
if (value === "runner_blocked") return "Code Agent Runner 受阻";
if (value === "api_error") return "Code Agent API 错误";
if (value === "capability_unavailable") return "Code Agent 能力未开放";
if (value === "retryable") return "Code Agent 可重试";
return "Code Agent 服务受阻";
@@ -3013,8 +3032,11 @@ function messageActionsPanel(message) {
const actions = [];
if (message.status === "running") {
actions.push(actionButton("取消当前请求", "cancel", () => cancelAgentMessage(message.id), "取消当前 in-flight Codex stdio 请求"));
if (message.retryInput) {
actions.push(actionButton("重试上一条", "retry", () => restartRunningAgentMessage(message.id), "先取消当前 trace,再用同一输入重新发送"));
}
}
if (canRetryAgentMessage(message)) {
if (canRetryTerminalAgentMessage(message)) {
actions.push(actionButton("重试上一条", "retry", () => retryAgentMessage(message.id), "保留 conversation/trace 记录并重新发送上一条输入"));
}
if (message.traceId) {
@@ -3040,16 +3062,38 @@ function actionButton(label, action, onClick, title) {
return button;
}
function canRetryAgentMessage(message) {
function canRetryTerminalAgentMessage(message) {
return Boolean(
message?.retryInput &&
["failed", "timeout", "error", "canceled"].includes(String(message.status ?? "").toLowerCase())
);
}
async function restartRunningAgentMessage(messageId) {
const message = state.chatMessages.find((item) => item.id === messageId);
if (!message?.retryInput || message.status !== "running") return;
await cancelAgentMessage(messageId);
const canceled = state.chatMessages.find((item) => item.id === messageId);
if (canceled?.status !== "canceled" || state.chatPending) {
setAgentActionStatus(messageId, "取消未确认,当前 trace 仍在等待后端;已保留输入,可等待真实超时或后端结果后再重试。");
return;
}
await submitAgentMessage(message.retryInput, {
conversationId: canceled.conversationId || state.conversationId || undefined,
sessionId: isTerminalSessionStatus(canceled.session?.status ?? canceled.runnerTrace?.sessionStatus ?? canceled.status)
? undefined
: canceled.sessionId,
retryOf: canceled.traceId
});
}
async function retryAgentMessage(messageId) {
const message = state.chatMessages.find((item) => item.id === messageId);
if (!message?.retryInput || state.chatPending) return;
if (!message?.retryInput) return;
if (state.chatPending) {
setAgentActionStatus(messageId, "当前仍有请求处理中;请先取消当前请求,或等待真实超时/后端结果后再重试。");
return;
}
await submitAgentMessage(message.retryInput, {
conversationId: message.conversationId || state.conversationId || undefined,
sessionId: isTerminalSessionStatus(message.session?.status ?? message.runnerTrace?.sessionStatus ?? message.status)
@@ -3059,6 +3103,17 @@ async function retryAgentMessage(messageId) {
});
}
function setAgentActionStatus(messageId, text) {
const index = state.chatMessages.findIndex((message) => message.id === messageId);
if (index < 0) return;
state.chatMessages[index] = {
...state.chatMessages[index],
traceReplayStatus: text,
updatedAt: new Date().toISOString()
};
renderConversation();
}
async function cancelAgentMessage(messageId) {
const index = state.chatMessages.findIndex((message) => message.id === messageId);
if (index < 0) return;
+5
View File
@@ -901,6 +901,9 @@ assert.match(app, /sourceKind:\s*"PENDING"/);
assert.match(app, /旧 4500ms/);
assert.match(app, /function messagePendingContextPanel/);
assert.match(app, /message-pending-context/);
assert.match(app, /function\s+restartRunningAgentMessage/);
assert.match(app, /先取消当前 trace,再用同一输入重新发送/);
assert.match(app, /canRetryTerminalAgentMessage/);
assert.match(app, /结构化 blocker/);
assert.match(app, /Code Agent 请求超过/);
assert.match(app, /agentFailurePresentation/);
@@ -914,6 +917,8 @@ assert.match(app, /Provider 不可用/);
assert.match(app, /Runner 忙碌/);
assert.match(app, /Session 受阻/);
assert.match(app, /API 错误/);
assert.match(app, /后端失败/);
assert.doesNotMatch(app, /发送失败/);
assert.doesNotMatch(html, /M3 Diagnostics Console/);
assert.match(styles, /html,\s*\nbody\s*{[^}]*height:\s*100%;[^}]*overflow:\s*hidden;/s);
assert.match(styles, /body\s*>\s*\[data-app-shell\]\s*{[^}]*min-height:\s*0;/s);