feat(heartbeat): skip API calls when HEARTBEAT.md is effectively empty (#1535)

* feat: skip heartbeat API calls when HEARTBEAT.md is effectively empty

- Added isHeartbeatContentEffectivelyEmpty() to detect files with only headers/comments
- Modified runHeartbeatOnce() to check HEARTBEAT.md content before polling the LLM
- Returns early with 'empty-heartbeat-file' reason when no actionable tasks exist
- Preserves existing behavior when file is missing (lets LLM decide)
- Added comprehensive test coverage for empty file detection
- Saves API calls/costs when heartbeat file has no meaningful content

* chore: update HEARTBEAT.md template to be effectively empty by default

Changed instruction text to comment format so new workspaces benefit from
heartbeat optimization immediately. Users still get clear guidance on usage.

* fix: only treat markdown headers (# followed by space) as comments, not #TODO etc

* refactor: simplify regex per code review suggestion

* docs: clarify heartbeat empty file behavior (#1535) (thanks @JustYannicc)

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
JustYannicc
2026-01-24 04:19:01 +00:00
committed by GitHub
parent 71203829d8
commit dd06028827
9 changed files with 357 additions and 3 deletions

View File

@@ -1,9 +1,18 @@
import { resolveAgentConfig, resolveDefaultAgentId } from "../agents/agent-scope.js";
import fs from "node:fs/promises";
import path from "node:path";
import {
resolveAgentConfig,
resolveAgentWorkspaceDir,
resolveDefaultAgentId,
} from "../agents/agent-scope.js";
import { resolveUserTimezone } from "../agents/date-time.js";
import { resolveEffectiveMessagesConfig } from "../agents/identity.js";
import { DEFAULT_HEARTBEAT_FILENAME } from "../agents/workspace.js";
import {
DEFAULT_HEARTBEAT_ACK_MAX_CHARS,
DEFAULT_HEARTBEAT_EVERY,
isHeartbeatContentEffectivelyEmpty,
resolveHeartbeatPrompt as resolveHeartbeatPromptText,
stripHeartbeatToken,
} from "../auto-reply/heartbeat.js";
@@ -440,6 +449,25 @@ export async function runHeartbeatOnce(opts: {
return { status: "skipped", reason: "requests-in-flight" };
}
// 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).
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)) {
emitHeartbeatEvent({
status: "skipped",
reason: "empty-heartbeat-file",
durationMs: Date.now() - startedAt,
});
return { status: "skipped", reason: "empty-heartbeat-file" };
}
} catch {
// 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 });