fix(cron): wait for heartbeat to complete when wakeMode is "now"

Fixes #652

When cron jobs with sessionTarget:"main" have wakeMode:"now",
they were being marked as completed immediately without waiting for the
agent to actually process the system event.

The issue was that requestHeartbeatNow() is fire-and-forget and
doesn't wait for the heartbeat to complete. The job would finish
with durationMs: 0 before the agent had a chance to run.

This fix:
- Adds runHeartbeatOnce to CronServiceDeps
- Wires it up in gateway/server.ts to load config and pass runtime
- Modifies executeJob() to call runHeartbeatOnce when wakeMode:"now"
- Waits for heartbeat to complete and maps status to cron result:
  * "ran" → "ok"
  * "skipped" → "skipped"
  * "failed" → "error"
- Falls back to old behavior for wakeMode:"next-heartbeat" or if
  runHeartbeatOnce is not available (backward compatibility)

Benefits:
- Jobs now have accurate durationMs reflecting actual processing time
- Jobs are correctly marked with "error" status if heartbeat fails
- Prevents race condition where job completes before agent runs

[AI-assisted] - Generated with z.ai GLM-4.7
[Tested: Lightly tested - Logic validated with test scenarios, code quality checks passed, integration testing requires live Clawdbot instance]
This commit is contained in:
Roshan Singh
2026-01-10 16:24:46 +00:00
committed by Peter Steinberger
parent 5a57cbe571
commit 91c870a0c4
2 changed files with 40 additions and 3 deletions

View File

@@ -61,7 +61,10 @@ import { startNodeBridgeServer } from "../infra/bridge/server.js";
import { resolveCanvasHostUrl } from "../infra/canvas-host-url.js";
import { GatewayLockError } from "../infra/gateway-lock.js";
import { onHeartbeatEvent } from "../infra/heartbeat-events.js";
import { startHeartbeatRunner } from "../infra/heartbeat-runner.js";
import {
runHeartbeatOnce,
startHeartbeatRunner,
} from "../infra/heartbeat-runner.js";
import { requestHeartbeatNow } from "../infra/heartbeat-wake.js";
import { getMachineDisplayName } from "../infra/machine-name.js";
import { resolveOutboundTarget } from "../infra/outbound/targets.js";
@@ -715,6 +718,14 @@ export async function startGatewayServer(
enqueueSystemEvent(text, { sessionKey: resolveMainSessionKey(cfg) });
},
requestHeartbeatNow,
runHeartbeatOnce: async (opts) => {
const runtimeConfig = loadConfig();
return await runHeartbeatOnce({
cfg: runtimeConfig,
reason: opts?.reason,
deps: { runtime },
});
},
runIsolatedAgentJob: async ({ job, message }) => {
const runtimeConfig = loadConfig();
return await runCronIsolatedAgentTurn({