Merge pull request #443 from pikasTech/fix/code-agent-wait-state-224-143-99
fix: 收口 Code Agent 长请求等待体验
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user