fix: harden agent prompt handling

This commit is contained in:
Codex
2026-05-30 13:52:18 +08:00
parent 9eb44acd18
commit 5a858f89b5
12 changed files with 212 additions and 69 deletions

View File

@@ -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", () => {

View File

@@ -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);
}
}
}
};