fix(heartbeat): run when HEARTBEAT.md is missing

This commit is contained in:
Gustavo Madeira Santana
2026-02-19 19:32:18 -05:00
parent 6bc9824735
commit cf4ffff3e1
3 changed files with 100 additions and 43 deletions

View File

@@ -41,7 +41,7 @@ import { CommandLane } from "../process/lanes.js";
import { normalizeAgentId, toAgentStoreSessionKey } from "../routing/session-key.js";
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
import { escapeRegExp } from "../utils.js";
import { formatErrorMessage } from "./errors.js";
import { formatErrorMessage, hasErrnoCode } from "./errors.js";
import { isWithinActiveHours } from "./heartbeat-active-hours.js";
import {
buildCronEventPrompt,
@@ -481,7 +481,7 @@ type HeartbeatReasonFlags = {
isWakeReason: boolean;
};
type HeartbeatSkipReason = "empty-heartbeat-file" | "no-heartbeat-file";
type HeartbeatSkipReason = "empty-heartbeat-file";
type HeartbeatPreflight = HeartbeatReasonFlags & {
session: ReturnType<typeof resolveHeartbeatSession>;
@@ -525,42 +525,39 @@ async function resolveHeartbeatPreflight(params: {
reasonFlags.isCronEventReason ||
reasonFlags.isWakeReason ||
hasTaggedCronEvents;
const workspaceDir = resolveAgentWorkspaceDir(params.cfg, params.agentId);
const heartbeatFilePath = path.join(workspaceDir, DEFAULT_HEARTBEAT_FILENAME);
try {
const heartbeatFileContent = await fs.readFile(heartbeatFilePath, "utf-8");
if (isHeartbeatContentEffectivelyEmpty(heartbeatFileContent) && !shouldBypassFileGates) {
return {
...reasonFlags,
session,
pendingEventEntries,
hasTaggedCronEvents,
shouldInspectPendingEvents,
skipReason: "empty-heartbeat-file",
};
}
} catch (err: unknown) {
if ((err as NodeJS.ErrnoException)?.code === "ENOENT" && !shouldBypassFileGates) {
return {
...reasonFlags,
session,
pendingEventEntries,
hasTaggedCronEvents,
shouldInspectPendingEvents,
skipReason: "no-heartbeat-file",
};
}
// For other read errors, proceed with heartbeat as before.
}
return {
const basePreflight = {
...reasonFlags,
session,
pendingEventEntries,
hasTaggedCronEvents,
shouldInspectPendingEvents,
};
} satisfies Omit<HeartbeatPreflight, "skipReason">;
if (shouldBypassFileGates) {
return basePreflight;
}
const workspaceDir = resolveAgentWorkspaceDir(params.cfg, params.agentId);
const heartbeatFilePath = path.join(workspaceDir, DEFAULT_HEARTBEAT_FILENAME);
try {
const heartbeatFileContent = await fs.readFile(heartbeatFilePath, "utf-8");
if (isHeartbeatContentEffectivelyEmpty(heartbeatFileContent)) {
return {
...basePreflight,
skipReason: "empty-heartbeat-file",
};
}
} catch (err: unknown) {
if (hasErrnoCode(err, "ENOENT")) {
// Missing HEARTBEAT.md is intentional in some setups (for example, when
// heartbeat instructions live outside the file), so keep the run active.
// The heartbeat prompt already says "if it exists".
return basePreflight;
}
// For other read errors, proceed with heartbeat as before.
}
return basePreflight;
}
export async function runHeartbeatOnce(opts: {