Skip to content

Commit 2af56d8

Browse files
committed
Honor no-delivery cron system events
1 parent e6f93ab commit 2af56d8

3 files changed

Lines changed: 43 additions & 3 deletions

File tree

src/agents/tools/cron-tool.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,9 @@ CRITICAL CONSTRAINTS:
560560
- sessionTarget="isolated" | "current" | "session:xxx" REQUIRES payload.kind="agentTurn"
561561
- Webhook: delivery.mode="webhook" and delivery.to URL.
562562
Default: prefer isolated agentTurn jobs unless the user explicitly wants current-session binding.
563+
For silent background automation that uses tools or stores memory, use
564+
sessionTarget="isolated" + payload.kind="agentTurn" + delivery.mode="none";
565+
do not use a main systemEvent job for tool-running background work.
563566
564567
RESTRICTED CRON RUNS:
565568
- Some isolated cron runs get narrow self-cleanup grant: status/list self-only, get/runs current job only, mutation only remove current job.

src/cron/service.runs-one-shot-main-job-disables-it.test.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,12 +191,18 @@ async function addWakeModeNowMainSystemEventJob(
191191

192192
async function addMainOneShotHelloJob(
193193
cron: CronService,
194-
params: { atMs: number; name: string; deleteAfterRun?: boolean },
194+
params: {
195+
atMs: number;
196+
name: string;
197+
deleteAfterRun?: boolean;
198+
delivery?: { mode: "none" };
199+
},
195200
) {
196201
return cron.add({
197202
name: params.name,
198203
enabled: true,
199204
...(params.deleteAfterRun === undefined ? {} : { deleteAfterRun: params.deleteAfterRun }),
205+
...(params.delivery ? { delivery: params.delivery } : {}),
200206
schedule: { kind: "at", at: new Date(params.atMs).toISOString() },
201207
sessionTarget: "main",
202208
wakeMode: "now",
@@ -255,13 +261,18 @@ function createStartedCronService(
255261
});
256262
}
257263

258-
async function createMainOneShotJobHarness(params: { name: string; deleteAfterRun?: boolean }) {
264+
async function createMainOneShotJobHarness(params: {
265+
name: string;
266+
deleteAfterRun?: boolean;
267+
delivery?: { mode: "none" };
268+
}) {
259269
const harness = await createMainOneShotHarness();
260270
const atMs = Date.parse("2025-12-13T00:00:02.000Z");
261271
const job = await addMainOneShotHelloJob(harness.cron, {
262272
atMs,
263273
name: params.name,
264274
deleteAfterRun: params.deleteAfterRun,
275+
delivery: params.delivery,
265276
});
266277
return { ...harness, atMs, job };
267278
}
@@ -306,6 +317,24 @@ describe("CronService", () => {
306317
await stopCronAndCleanup(cron, store);
307318
});
308319

320+
it("does not wake the assistant for explicit no-delivery main systemEvent jobs", async () => {
321+
const { store, cron, enqueueSystemEvent, requestHeartbeat, events, atMs, job } =
322+
await createMainOneShotJobHarness({
323+
name: "silent main system event",
324+
deleteAfterRun: false,
325+
delivery: { mode: "none" },
326+
});
327+
328+
vi.setSystemTime(new Date(atMs));
329+
await vi.runOnlyPendingTimersAsync();
330+
await events.waitFor((evt) => evt.jobId === job.id && evt.action === "finished");
331+
332+
expectMainSystemEventPosted(enqueueSystemEvent, { text: "hello", jobId: job.id });
333+
expect(requestHeartbeat).not.toHaveBeenCalled();
334+
335+
await stopCronAndCleanup(cron, store);
336+
});
337+
309338
it("runs a one-shot job and deletes it after success by default", async () => {
310339
const { store, cron, enqueueSystemEvent, requestHeartbeat, events, job } =
311340
await createMainOneShotJobHarness({

src/cron/service/timer.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,10 @@ function resolveMainSessionCronDeliveryContext(
471471
}
472472
}
473473

474+
function hasExplicitNoDelivery(job: CronJob): boolean {
475+
return normalizeOptionalLowercaseString(job.delivery?.mode) === "none";
476+
}
477+
474478
function resolveCronTaskChildSessionKey(params: {
475479
state: CronServiceState;
476480
job: CronJob;
@@ -1761,13 +1765,17 @@ async function executeMainSessionCronJob(
17611765
const cronStartedAt =
17621766
typeof job.state.runningAtMs === "number" ? job.state.runningAtMs : state.deps.nowMs();
17631767
const cronRunSessionKey = resolveMainSessionCronRunSessionKey(job, cronStartedAt);
1768+
const suppressHeartbeat = hasExplicitNoDelivery(job);
17641769
const deliveryContext = resolveMainSessionCronDeliveryContext(state, job);
17651770
state.deps.enqueueSystemEvent(text, {
17661771
agentId: job.agentId,
17671772
sessionKey: cronRunSessionKey,
17681773
contextKey: `cron:${job.id}`,
1769-
...(deliveryContext ? { deliveryContext } : {}),
1774+
...(!suppressHeartbeat && deliveryContext ? { deliveryContext } : {}),
17701775
});
1776+
if (suppressHeartbeat) {
1777+
return { status: "ok", summary: text, sessionKey: cronRunSessionKey };
1778+
}
17711779
if (job.wakeMode === "now" && state.deps.runHeartbeatOnce) {
17721780
const reason = `cron:${job.id}`;
17731781
const maxWaitMs = state.deps.wakeNowHeartbeatBusyMaxWaitMs ?? 2 * 60_000;

0 commit comments

Comments
 (0)