From 19fed71f093a4633faec3e245b3d603500f28d0e Mon Sep 17 00:00:00 2001 From: Jiang Bohan Date: Mon, 9 Feb 2026 15:08:04 +0800 Subject: [PATCH] fix(heartbeat): bypass empty-file check for cron-triggered wakes Cron reminders were silently skipped when heartbeat.md had no actionable content. Now cron: and exec-event reasons both bypass the empty-file guard so scheduled reminders always reach the agent. Co-Authored-By: Claude Opus 4.6 --- src/heartbeat/runner.test.ts | 12 ++++++++++++ src/heartbeat/runner.ts | 4 ++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/heartbeat/runner.test.ts b/src/heartbeat/runner.test.ts index 8a3570a3..1a04bc5f 100644 --- a/src/heartbeat/runner.test.ts +++ b/src/heartbeat/runner.test.ts @@ -65,6 +65,18 @@ describe("heartbeat runner", () => { } }); + it("bypasses empty-heartbeat-file check for cron-triggered wakes", async () => { + const dir = await mkdtemp(path.join(os.tmpdir(), "heartbeat-test-")); + try { + await writeFile(path.join(dir, "heartbeat.md"), "# keep empty\n", "utf-8"); + const agent = createStubAgent({ profileDir: dir, replyText: "HEARTBEAT_OK" }); + const result = await runHeartbeatOnce({ agent: agent as any, reason: "cron:test-job-id" }); + expect(result.status).toBe("ran"); + } finally { + await rm(dir, { recursive: true, force: true }); + } + }); + it("runs and returns ran for heartbeat acknowledgements", async () => { const agent = createStubAgent({ replyText: "HEARTBEAT_OK" }); const result = await runHeartbeatOnce({ agent: agent as any, reason: "manual" }); diff --git a/src/heartbeat/runner.ts b/src/heartbeat/runner.ts index f9ba6bfa..7e6dab53 100644 --- a/src/heartbeat/runner.ts +++ b/src/heartbeat/runner.ts @@ -156,8 +156,8 @@ export async function runHeartbeatOnce(opts: { } try { - const isExecEvent = opts.reason === "exec-event"; - if (!isExecEvent && (await isHeartbeatFileEmpty(agent))) { + const isForcedWake = opts.reason === "exec-event" || opts.reason?.startsWith("cron:"); + if (!isForcedWake && (await isHeartbeatFileEmpty(agent))) { emitHeartbeatEvent({ status: "skipped", reason: "empty-heartbeat-file",