fix: harden agent prompt handling
This commit is contained in:
@@ -296,6 +296,40 @@ describe("AcpEngine.prompt", () => {
|
||||
const event = onPromptEnd.mock.calls.at(-1)?.[0];
|
||||
expect(event.reason).toBe("mcp_reconnecting");
|
||||
});
|
||||
|
||||
it("qimingcode prompt 长时间无响应时结束会话等待", async () => {
|
||||
const { engine, sessionId, session, acpConnection } =
|
||||
setupEngine("qimingcode");
|
||||
const onPromptEnd = vi.fn();
|
||||
const onSessionError = vi.fn();
|
||||
engine.on("computer:promptEnd", onPromptEnd);
|
||||
engine.on("session.error", onSessionError);
|
||||
|
||||
acpConnection.prompt.mockReturnValueOnce(new Promise(() => {}));
|
||||
|
||||
vi.useFakeTimers();
|
||||
const promptPromise = engine.prompt(
|
||||
sessionId,
|
||||
[{ type: "text", text: "hi" }],
|
||||
{ messageID: "rid-timeout-001" },
|
||||
);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(60_001);
|
||||
await expect(promptPromise).resolves.toBeDefined();
|
||||
|
||||
expect(onPromptEnd).toHaveBeenCalledTimes(1);
|
||||
expect(onPromptEnd.mock.calls[0][0]).toMatchObject({
|
||||
sessionId,
|
||||
reason: "error",
|
||||
});
|
||||
expect(onPromptEnd.mock.calls[0][0].description).toContain(
|
||||
"ACP prompt timed out",
|
||||
);
|
||||
expect(onSessionError).toHaveBeenCalledTimes(1);
|
||||
expect(session.status).toBe("idle");
|
||||
expect((engine as any).activePromptSessions.has(sessionId)).toBe(false);
|
||||
expect((engine as any).activePromptRejects.has(sessionId)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("AcpEngine.handleAcpSessionUpdate", () => {
|
||||
|
||||
@@ -79,7 +79,7 @@ import {
|
||||
import { processRegistry } from "../../system/processRegistry";
|
||||
import { t } from "../../i18n";
|
||||
import type { DetailedSession } from "@shared/types/sessions";
|
||||
import { ACP_ABORT_TIMEOUT } from "@shared/constants";
|
||||
import { ACP_ABORT_TIMEOUT, ACP_PROMPT_TIMEOUT } from "@shared/constants";
|
||||
import { APP_DATA_DIR_NAME } from "../../constants";
|
||||
import { perfEmitter } from "../perf/perfEmitter";
|
||||
import { firstTokenTrace } from "../perf/firstTokenTrace";
|
||||
@@ -1520,8 +1520,20 @@ export class AcpEngine extends EventEmitter {
|
||||
const maxAttempts = this.engineName === "qimingcode" ? 2 : 1;
|
||||
let attempt = 1;
|
||||
while (true) {
|
||||
let promptTimeout: ReturnType<typeof setTimeout> | undefined;
|
||||
try {
|
||||
const res = await this.acpConnection!.prompt(promptParams);
|
||||
const res = await Promise.race([
|
||||
this.acpConnection!.prompt(promptParams),
|
||||
new Promise<never>((_, reject) => {
|
||||
promptTimeout = setTimeout(() => {
|
||||
reject(
|
||||
new Error(
|
||||
`ACP prompt timed out after ${ACP_PROMPT_TIMEOUT}ms`,
|
||||
),
|
||||
);
|
||||
}, ACP_PROMPT_TIMEOUT);
|
||||
}),
|
||||
]);
|
||||
log.info(
|
||||
`${this.logTag} 📥 ACP prompt resolved (${Date.now() - promptStartTime}ms, attempt=${attempt}):`,
|
||||
safeStringify(res),
|
||||
@@ -1529,9 +1541,20 @@ export class AcpEngine extends EventEmitter {
|
||||
return res;
|
||||
} catch (err) {
|
||||
const errMsg = this.toErrorMessage(err);
|
||||
if (errMsg.includes("ACP prompt timed out")) {
|
||||
log.error(
|
||||
`${this.logTag} 📥 ACP prompt timed out (${Date.now() - promptStartTime}ms, attempt=${attempt})`,
|
||||
{
|
||||
sessionId,
|
||||
acpSessionId: session.acpSessionId,
|
||||
requestId: _opts?.messageID,
|
||||
},
|
||||
);
|
||||
}
|
||||
const canRetry =
|
||||
attempt < maxAttempts &&
|
||||
!this.isPromptCancellation(err) &&
|
||||
!errMsg.includes("ACP prompt timed out") &&
|
||||
this.isMcpReconnectFailure(errMsg);
|
||||
|
||||
if (!canRetry) {
|
||||
@@ -1569,6 +1592,10 @@ export class AcpEngine extends EventEmitter {
|
||||
);
|
||||
await this.sleep(MCP_RETRY_DELAY_MS);
|
||||
attempt += 1;
|
||||
} finally {
|
||||
if (promptTimeout !== undefined) {
|
||||
clearTimeout(promptTimeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1166,7 +1166,7 @@ export function getSetupRequiredDependencies(): LocalDependencyConfig[] {
|
||||
description: t(I18N_KEYS.Pages.Dependencies.DESC_QIMINGCODE),
|
||||
required: true,
|
||||
binName: "qimingcode",
|
||||
installVersion: "1.1.97",
|
||||
installVersion: "1.2.1",
|
||||
},
|
||||
{
|
||||
name: "claude-code-acp-ts",
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
CLEANUP_TIMEOUT,
|
||||
PROCESS_KILL_ESCALATION_TIMEOUT,
|
||||
ACP_ABORT_TIMEOUT,
|
||||
ACP_PROMPT_TIMEOUT,
|
||||
ENGINE_DESTROY_TIMEOUT,
|
||||
DEPS_SYNC_TIMEOUT,
|
||||
NPM_MIRRORS,
|
||||
@@ -151,6 +152,7 @@ describe("Constants", () => {
|
||||
expect(CLEANUP_TIMEOUT).toBeGreaterThan(0);
|
||||
expect(PROCESS_KILL_ESCALATION_TIMEOUT).toBeGreaterThan(0);
|
||||
expect(ACP_ABORT_TIMEOUT).toBeGreaterThan(0);
|
||||
expect(ACP_PROMPT_TIMEOUT).toBeGreaterThan(0);
|
||||
expect(ENGINE_DESTROY_TIMEOUT).toBeGreaterThan(0);
|
||||
expect(DEPS_SYNC_TIMEOUT).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
@@ -151,6 +151,9 @@ export const PROCESS_KILL_ESCALATION_TIMEOUT = 5000;
|
||||
/** ACP 会话取消超时 (ms) */
|
||||
export const ACP_ABORT_TIMEOUT = 15_000;
|
||||
|
||||
/** ACP prompt 等待响应超时 (ms) */
|
||||
export const ACP_PROMPT_TIMEOUT = 60_000;
|
||||
|
||||
/**
|
||||
* 用户主动取消会话时,挂在 `Error` 上的 `code`(与 `message` 语言无关)。
|
||||
* `message` 经主进程 `t()` 本地化后,`isPromptCancellation` 仍靠此字段识别,避免误触发 MCP 重试。
|
||||
|
||||
Reference in New Issue
Block a user