Skip to content

Commit a841778

Browse files
committed
fix(acp): cap turn timeout timers
1 parent 522d0f7 commit a841778

2 files changed

Lines changed: 45 additions & 2 deletions

File tree

src/acp/control-plane/manager.core.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { logVerbose } from "../../globals.js";
55
import { formatErrorMessage } from "../../infra/errors.js";
66
import { normalizeAgentId } from "../../routing/session-key.js";
77
import { isAcpSessionKey } from "../../sessions/session-key-utils.js";
8+
import { clampTimerTimeoutMs } from "../../shared/number-coercion.js";
89
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
910
import {
1011
createRunningTaskRun,
@@ -1082,7 +1083,7 @@ export class AcpSessionManager {
10821083
Number.isFinite(runtimeTimeoutSeconds) &&
10831084
runtimeTimeoutSeconds > 0
10841085
) {
1085-
return Math.max(1_000, Math.round(runtimeTimeoutSeconds * 1_000));
1086+
return clampTimerTimeoutMs(Math.round(runtimeTimeoutSeconds * 1_000), 1_000) ?? 1_000;
10861087
}
10871088
return resolveAgentTimeoutMs({
10881089
cfg: params.cfg,
@@ -1125,10 +1126,19 @@ export class AcpSessionManager {
11251126
return outcome.value;
11261127
}
11271128

1129+
const timeoutMs = clampTimerTimeoutMs(params.timeoutMs, 1);
1130+
if (timeoutMs === undefined) {
1131+
const outcome = await observedTurnPromise;
1132+
if (outcome.kind === "error") {
1133+
throw outcome.error;
1134+
}
1135+
return outcome.value;
1136+
}
1137+
11281138
const timeoutToken = Symbol("acp-turn-timeout");
11291139
let timer: NodeJS.Timeout | undefined;
11301140
const timeoutPromise = new Promise<typeof timeoutToken>((resolve) => {
1131-
timer = setTimeout(() => resolve(timeoutToken), params.timeoutMs);
1141+
timer = setTimeout(() => resolve(timeoutToken), timeoutMs);
11321142
timer.unref?.();
11331143
});
11341144

src/acp/control-plane/manager.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
44
import type { OpenClawConfig } from "../../config/config.js";
55
import type { AcpSessionRuntimeOptions, SessionAcpMeta } from "../../config/sessions/types.js";
66
import { resetHeartbeatWakeStateForTests } from "../../infra/heartbeat-wake.js";
7+
import { MAX_TIMER_TIMEOUT_MS } from "../../shared/number-coercion.js";
78
import { withTempDir } from "../../test-helpers/temp-dir.js";
89
import type { AcpRuntime, AcpRuntimeCapabilities } from "../runtime/types.js";
910

@@ -807,6 +808,38 @@ describe("AcpSessionManager", () => {
807808
}
808809
});
809810

811+
it("caps ACP runtime option turn timeouts before arming the watchdog", async () => {
812+
const runtimeState = createRuntime();
813+
hoisted.requireAcpRuntimeBackendMock.mockReturnValue({
814+
id: "acpx",
815+
runtime: runtimeState.runtime,
816+
});
817+
hoisted.readAcpSessionEntryMock.mockReturnValue({
818+
sessionKey: "agent:codex:acp:session-1",
819+
storeSessionKey: "agent:codex:acp:session-1",
820+
acp: readySessionMeta({
821+
runtimeOptions: {
822+
timeoutSeconds: Number.MAX_SAFE_INTEGER,
823+
},
824+
}),
825+
});
826+
const timeoutSpy = vi.spyOn(globalThis, "setTimeout");
827+
try {
828+
const manager = new AcpSessionManager();
829+
await manager.runTurn({
830+
cfg: baseCfg,
831+
sessionKey: "agent:codex:acp:session-1",
832+
text: "first",
833+
mode: "prompt",
834+
requestId: "r1",
835+
});
836+
837+
expect(timeoutSpy).toHaveBeenCalledWith(expect.any(Function), MAX_TIMER_TIMEOUT_MS);
838+
} finally {
839+
timeoutSpy.mockRestore();
840+
}
841+
});
842+
810843
it("keeps timed-out runtime handles counted until timeout cleanup finishes", async () => {
811844
vi.useFakeTimers();
812845
try {

0 commit comments

Comments
 (0)