diff --git a/src/cron/service.runs-one-shot-main-job-disables-it.test.ts b/src/cron/service.runs-one-shot-main-job-disables-it.test.ts index 1cc3eca03c..bbee9cf7e8 100644 --- a/src/cron/service.runs-one-shot-main-job-disables-it.test.ts +++ b/src/cron/service.runs-one-shot-main-job-disables-it.test.ts @@ -200,6 +200,49 @@ describe("CronService", () => { await store.cleanup(); }); + it("passes agentId to runHeartbeatOnce for main-session wakeMode now jobs", async () => { + const store = await makeStorePath(); + const enqueueSystemEvent = vi.fn(); + const requestHeartbeatNow = vi.fn(); + const runHeartbeatOnce = vi.fn(async () => ({ status: "ran" as const, durationMs: 1 })); + + const cron = new CronService({ + storePath: store.storePath, + cronEnabled: true, + log: noopLogger, + enqueueSystemEvent, + requestHeartbeatNow, + runHeartbeatOnce, + runIsolatedAgentJob: vi.fn(async () => ({ status: "ok" })), + }); + + await cron.start(); + const job = await cron.add({ + name: "wakeMode now with agent", + agentId: "ops", + enabled: true, + schedule: { kind: "at", at: new Date(1).toISOString() }, + sessionTarget: "main", + wakeMode: "now", + payload: { kind: "systemEvent", text: "hello" }, + }); + + await cron.run(job.id, "force"); + + expect(runHeartbeatOnce).toHaveBeenCalledTimes(1); + expect(runHeartbeatOnce).toHaveBeenCalledWith( + expect.objectContaining({ + reason: `cron:${job.id}`, + agentId: "ops", + }), + ); + expect(requestHeartbeatNow).not.toHaveBeenCalled(); + expect(enqueueSystemEvent).toHaveBeenCalledWith("hello", { agentId: "ops" }); + + cron.stop(); + await store.cleanup(); + }); + it("wakeMode now falls back to queued heartbeat when main lane stays busy", async () => { const store = await makeStorePath(); const enqueueSystemEvent = vi.fn(); diff --git a/src/cron/service/state.ts b/src/cron/service/state.ts index c51103f339..025da7b3fa 100644 --- a/src/cron/service/state.ts +++ b/src/cron/service/state.ts @@ -37,7 +37,7 @@ export type CronServiceDeps = { sessionStorePath?: string; enqueueSystemEvent: (text: string, opts?: { agentId?: string }) => void; requestHeartbeatNow: (opts?: { reason?: string }) => void; - runHeartbeatOnce?: (opts?: { reason?: string }) => Promise; + runHeartbeatOnce?: (opts?: { reason?: string; agentId?: string }) => Promise; runIsolatedAgentJob: (params: { job: CronJob; message: string }) => Promise<{ status: "ok" | "error" | "skipped"; summary?: string; diff --git a/src/cron/service/timer.ts b/src/cron/service/timer.ts index 3b446848a3..802ff63b70 100644 --- a/src/cron/service/timer.ts +++ b/src/cron/service/timer.ts @@ -440,7 +440,7 @@ async function executeJobCore( let heartbeatResult: HeartbeatRunResult; for (;;) { - heartbeatResult = await state.deps.runHeartbeatOnce({ reason }); + heartbeatResult = await state.deps.runHeartbeatOnce({ reason, agentId: job.agentId }); if ( heartbeatResult.status !== "skipped" || heartbeatResult.reason !== "requests-in-flight" diff --git a/src/gateway/server-cron.ts b/src/gateway/server-cron.ts index 10ce4200a6..07fd2831cb 100644 --- a/src/gateway/server-cron.ts +++ b/src/gateway/server-cron.ts @@ -69,9 +69,11 @@ export function buildGatewayCronService(params: { requestHeartbeatNow, runHeartbeatOnce: async (opts) => { const runtimeConfig = loadConfig(); + const agentId = opts?.agentId ? resolveCronAgent(opts.agentId).agentId : undefined; return await runHeartbeatOnce({ cfg: runtimeConfig, reason: opts?.reason, + agentId, deps: { ...params.deps, runtime: defaultRuntime }, }); },