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:
committed by
Peter Steinberger
parent
5a57cbe571
commit
91c870a0c4
@@ -37,6 +37,11 @@ export type CronServiceDeps = {
|
||||
cronEnabled: boolean;
|
||||
enqueueSystemEvent: (text: string) => void;
|
||||
requestHeartbeatNow: (opts?: { reason?: string }) => void;
|
||||
runHeartbeatOnce?: (opts?: { reason?: string }) => Promise<{
|
||||
status: "ran" | "skipped" | "failed";
|
||||
durationMs: number;
|
||||
reason?: string;
|
||||
}>;
|
||||
runIsolatedAgentJob: (params: { job: CronJob; message: string }) => Promise<{
|
||||
status: "ok" | "error" | "skipped";
|
||||
summary?: string;
|
||||
@@ -514,10 +519,31 @@ export class CronService {
|
||||
return;
|
||||
}
|
||||
this.deps.enqueueSystemEvent(text);
|
||||
if (job.wakeMode === "now") {
|
||||
if (job.wakeMode === "now" && this.deps.runHeartbeatOnce) {
|
||||
const heartbeatResult = await this.deps.runHeartbeatOnce({
|
||||
reason: `cron:${job.id}`,
|
||||
});
|
||||
// Map heartbeat status to cron status
|
||||
if (heartbeatResult.status === "ran") {
|
||||
await finish("ok", undefined, text);
|
||||
} else if (heartbeatResult.status === "skipped") {
|
||||
await finish(
|
||||
"skipped",
|
||||
heartbeatResult.reason ?? "heartbeat skipped",
|
||||
text,
|
||||
);
|
||||
} else if (heartbeatResult.status === "failed") {
|
||||
await finish(
|
||||
"error",
|
||||
heartbeatResult.reason ?? "heartbeat failed",
|
||||
text,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// wakeMode is "next-heartbeat" or runHeartbeatOnce not available
|
||||
this.deps.requestHeartbeatNow({ reason: `cron:${job.id}` });
|
||||
await finish("ok", undefined, text);
|
||||
}
|
||||
await finish("ok", undefined, text);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user