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