fix: keep active ACP runs alive after reconnect timeout

This commit is contained in:
Ayaan Zaidi
2026-04-02 14:48:23 +05:30
parent e48a7b9be8
commit 73c1b45819
2 changed files with 54 additions and 9 deletions

View File

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

View File

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