diff --git a/src/infra/heartbeat-runner.ts b/src/infra/heartbeat-runner.ts index 71c41394a..5f4431722 100644 --- a/src/infra/heartbeat-runner.ts +++ b/src/infra/heartbeat-runner.ts @@ -35,7 +35,7 @@ import { } from "../config/sessions.js"; import type { AgentDefaultsConfig } from "../config/types.agent-defaults.js"; import { formatErrorMessage } from "../infra/errors.js"; -import { peekSystemEvents } from "../infra/system-events.js"; +import { hasSystemEvents, peekSystemEvents } from "../infra/system-events.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; import { getQueueSize } from "../process/command-queue.js"; import { CommandLane } from "../process/lanes.js"; @@ -460,15 +460,22 @@ export async function runHeartbeatOnce(opts: { return { status: "skipped", reason: "requests-in-flight" }; } + const { entry, sessionKey, storePath } = resolveHeartbeatSession(cfg, agentId, heartbeat); + // Skip heartbeat if HEARTBEAT.md exists but has no actionable content. // This saves API calls/costs when the file is effectively empty (only comments/headers). - // EXCEPTION: Don't skip for exec events - they have pending system events to process. + // EXCEPTION: Don't skip for exec events or if there are pending system events (from cron, etc.) const isExecEventReason = opts.reason === "exec-event"; + const hasPendingSystemEvents = hasSystemEvents(sessionKey); const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId); const heartbeatFilePath = path.join(workspaceDir, DEFAULT_HEARTBEAT_FILENAME); try { const heartbeatFileContent = await fs.readFile(heartbeatFilePath, "utf-8"); - if (isHeartbeatContentEffectivelyEmpty(heartbeatFileContent) && !isExecEventReason) { + if ( + isHeartbeatContentEffectivelyEmpty(heartbeatFileContent) && + !isExecEventReason && + !hasPendingSystemEvents + ) { emitHeartbeatEvent({ status: "skipped", reason: "empty-heartbeat-file", @@ -480,8 +487,6 @@ export async function runHeartbeatOnce(opts: { // File doesn't exist or can't be read - proceed with heartbeat. // The LLM prompt says "if it exists" so this is expected behavior. } - - const { entry, sessionKey, storePath } = resolveHeartbeatSession(cfg, agentId, heartbeat); const previousUpdatedAt = entry?.updatedAt; const delivery = resolveHeartbeatDeliveryTarget({ cfg, entry, heartbeat }); const visibility =