From f8cfd54f41becfb752138cab247ee10bd002dc59 Mon Sep 17 00:00:00 2001 From: Peter Lee Date: Mon, 10 Aug 2026 03:21:14 -0500 Subject: [PATCH] fix(infra): heartbeat runs honor timeoutSeconds 0 as unlimited (#119297) * fix(infra): preserve timeoutSeconds 0 as unlimited for heartbeat runs * test(infra): consolidate heartbeat timeout coverage --------- Co-authored-by: Dallin Romney --- src/infra/heartbeat-runner-config.ts | 5 ++++- .../heartbeat-runner.model-override.test.ts | 17 ++++++++++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/infra/heartbeat-runner-config.ts b/src/infra/heartbeat-runner-config.ts index 6fc2cef44742..50c158b6a7ec 100644 --- a/src/infra/heartbeat-runner-config.ts +++ b/src/infra/heartbeat-runner-config.ts @@ -52,7 +52,10 @@ export function resolveHeartbeatTimeoutOverrideSeconds( typeof agentDefaultTimeoutSeconds === "number" && Number.isFinite(agentDefaultTimeoutSeconds) ) { - return Math.max(1, Math.floor(agentDefaultTimeoutSeconds)); + // Preserve the unlimited sentinel consumed by resolveAgentTimeoutMs. + return agentDefaultTimeoutSeconds === 0 + ? 0 + : Math.max(1, Math.floor(agentDefaultTimeoutSeconds)); } // The wake dispatcher awaits heartbeat turns serially. Keep unset heartbeat // timeouts tied to the cadence instead of the 48h built-in agent default. diff --git a/src/infra/heartbeat-runner.model-override.test.ts b/src/infra/heartbeat-runner.model-override.test.ts index a3c83d5ece11..120899ff7621 100644 --- a/src/infra/heartbeat-runner.model-override.test.ts +++ b/src/infra/heartbeat-runner.model-override.test.ts @@ -227,13 +227,16 @@ describe("runHeartbeatOnce – heartbeat model override", () => { }); }); - it("preserves explicit default agent timeout for heartbeat runs", async () => { - const replyOpts = await runDefaultsHeartbeat({ defaultTimeoutSeconds: 60, every: "30m" }); - expectReplyOptions(replyOpts, { - isHeartbeat: true, - timeoutOverrideSeconds: 60, - }); - }); + it.each([0, 60])( + "preserves explicit default agent timeout %d for heartbeat runs", + async (defaultTimeoutSeconds) => { + const replyOpts = await runDefaultsHeartbeat({ defaultTimeoutSeconds, every: "30m" }); + expectReplyOptions(replyOpts, { + isHeartbeat: true, + timeoutOverrideSeconds: defaultTimeoutSeconds, + }); + }, + ); it("passes bootstrapContextMode when heartbeat lightContext is enabled", async () => { const replyOpts = await runDefaultsHeartbeat({ lightContext: true });