diff --git a/src/acp/translator.stop-reason.test.ts b/src/acp/translator.stop-reason.test.ts index f4be4c135ba..5c4fda85a8f 100644 --- a/src/acp/translator.stop-reason.test.ts +++ b/src/acp/translator.stop-reason.test.ts @@ -295,6 +295,49 @@ describe("acp translator stop reason mapping", () => { } }); + it("keeps accepted prompts pending when the deadline recheck still reports timeout", async () => { + vi.useFakeTimers(); + try { + const sessionId = "session-1"; + const sessionKey = "agent:main:main"; + const request = vi.fn(async (method: string) => { + if (method === "chat.send") { + return {}; + } + if (method === "agent.wait") { + return { status: "timeout" }; + } + return {}; + }) as GatewayClient["request"]; + const sessionStore = createInMemorySessionStore(); + sessionStore.createSession({ + sessionId, + sessionKey, + cwd: "/tmp", + }); + const agent = new AcpGatewayAgent(createAcpConnection(), createAcpGateway(request), { + sessionStore, + }); + const promptPromise = agent.prompt({ + sessionId, + prompt: [{ type: "text", text: "hello" }], + _meta: {}, + } as unknown as PromptRequest); + + await Promise.resolve(); + agent.handleGatewayDisconnect("1006: connection lost"); + agent.handleGatewayReconnect(); + await Promise.resolve(); + await vi.advanceTimersByTimeAsync(5_000); + + await expect(Promise.race([promptPromise, Promise.resolve("pending")])).resolves.toBe( + "pending", + ); + } finally { + vi.useRealTimers(); + } + }); + it("does not clear a newer disconnect deadline while reconnect reconciliation is still running", async () => { vi.useFakeTimers(); try { @@ -353,7 +396,10 @@ describe("acp translator stop reason mapping", () => { expect(settleSpy).not.toHaveBeenCalled(); await vi.advanceTimersByTimeAsync(1); - await expect(promptPromise).rejects.toThrow("Gateway disconnected: 1006: second disconnect"); + expect(request).toHaveBeenCalledTimes(3); + await expect(Promise.race([promptPromise, Promise.resolve("pending")])).resolves.toBe( + "pending", + ); } finally { vi.useRealTimers(); } diff --git a/src/acp/translator.ts b/src/acp/translator.ts index 2694ce1c9b8..efb79a1058c 100644 --- a/src/acp/translator.ts +++ b/src/acp/translator.ts @@ -1052,6 +1052,11 @@ export class AcpGatewayAgent implements Agent { pending.reject(error); } + private clearPendingDisconnectState(pending: PendingPrompt): void { + pending.disconnectGeneration = undefined; + pending.disconnectReason = undefined; + } + private async reconcilePendingPrompts( observedDisconnectGeneration: number, deadlineExpired: boolean, @@ -1116,10 +1121,7 @@ export class AcpGatewayAgent implements Agent { } catch (err) { this.log(`agent.wait reconcile failed for ${pending.idempotencyKey}: ${String(err)}`); if (deadlineExpired) { - this.rejectPendingPrompt( - pending, - new Error(`Gateway disconnected: ${pending.disconnectReason}`), - ); + this.clearPendingDisconnectState(pending); return false; } return true; @@ -1138,10 +1140,7 @@ export class AcpGatewayAgent implements Agent { return false; } if (deadlineExpired) { - this.rejectPendingPrompt( - currentPending, - new Error(`Gateway disconnected: ${currentPending.disconnectReason}`), - ); + this.clearPendingDisconnectState(currentPending); return false; } return true;