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