Skip to content

Commit fa2d38e

Browse files
committed
fix(cron): skip stale delayed deliveries
1 parent 5b1a5e9 commit fa2d38e

File tree

2 files changed

+77
-0
lines changed

2 files changed

+77
-0
lines changed

src/cron/isolated-agent/delivery-dispatch.double-announce.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ describe("dispatchCronDelivery — double-announce guard", () => {
143143
});
144144

145145
afterEach(() => {
146+
vi.useRealTimers();
146147
vi.unstubAllEnvs();
147148
});
148149

@@ -255,6 +256,39 @@ describe("dispatchCronDelivery — double-announce guard", () => {
255256
).toBe(false);
256257
});
257258

259+
it("skips stale cron deliveries while still suppressing fallback main summary", async () => {
260+
vi.useFakeTimers();
261+
vi.setSystemTime(new Date("2026-03-18T17:00:00.000Z"));
262+
vi.mocked(countActiveDescendantRuns).mockReturnValue(0);
263+
vi.mocked(isLikelyInterimCronMessage).mockReturnValue(false);
264+
265+
const params = makeBaseParams({ synthesizedText: "Yesterday's morning briefing." });
266+
(params.job as { state?: { nextRunAtMs?: number } }).state = {
267+
nextRunAtMs: Date.now() - (3 * 60 * 60_000 + 1),
268+
};
269+
270+
const state = await dispatchCronDelivery(params);
271+
272+
expect(state.result).toEqual(
273+
expect.objectContaining({
274+
status: "ok",
275+
delivered: false,
276+
deliveryAttempted: true,
277+
}),
278+
);
279+
expect(deliverOutboundPayloads).not.toHaveBeenCalled();
280+
expect(
281+
shouldEnqueueCronMainSummary({
282+
summaryText: "Yesterday's morning briefing.",
283+
deliveryRequested: true,
284+
delivered: state.result?.delivered,
285+
deliveryAttempted: state.result?.deliveryAttempted,
286+
suppressMainSummary: false,
287+
isCronSystemEvent: () => true,
288+
}),
289+
).toBe(false);
290+
});
291+
258292
it("text delivery fires exactly once (no double-deliver)", async () => {
259293
vi.mocked(countActiveDescendantRuns).mockReturnValue(0);
260294
vi.mocked(isLikelyInterimCronMessage).mockReturnValue(false);

src/cron/isolated-agent/delivery-dispatch.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@ const PERMANENT_DIRECT_CRON_DELIVERY_ERROR_PATTERNS: readonly RegExp[] = [
134134
/outbound not configured for channel/i,
135135
];
136136

137+
const STALE_CRON_DELIVERY_MAX_AGE_MS = 3 * 60 * 60_000;
138+
137139
type CompletedDirectCronDelivery = {
138140
ts: number;
139141
results: OutboundDeliveryResult[];
@@ -174,6 +176,23 @@ function pruneCompletedDirectCronDeliveries(now: number) {
174176
}
175177
}
176178

179+
function resolveCronDeliveryScheduledAtMs(params: { job: CronJob; runStartedAt: number }): number {
180+
const scheduledAt = params.job.state?.nextRunAtMs;
181+
return typeof scheduledAt === "number" && Number.isFinite(scheduledAt)
182+
? scheduledAt
183+
: params.runStartedAt;
184+
}
185+
186+
function isStaleCronDelivery(params: {
187+
job: CronJob;
188+
runStartedAt: number;
189+
nowMs?: number;
190+
}): boolean {
191+
const nowMs = params.nowMs ?? Date.now();
192+
const scheduledAtMs = resolveCronDeliveryScheduledAtMs(params);
193+
return nowMs - scheduledAtMs > STALE_CRON_DELIVERY_MAX_AGE_MS;
194+
}
195+
177196
function rememberCompletedDirectCronDelivery(
178197
idempotencyKey: string,
179198
results: readonly OutboundDeliveryResult[],
@@ -331,6 +350,30 @@ export async function dispatchCronDelivery(
331350
...params.telemetry,
332351
});
333352
}
353+
if (
354+
params.deliveryRequested &&
355+
isStaleCronDelivery({
356+
job: params.job,
357+
runStartedAt: params.runStartedAt,
358+
})
359+
) {
360+
deliveryAttempted = true;
361+
const scheduledAtMs = resolveCronDeliveryScheduledAtMs({
362+
job: params.job,
363+
runStartedAt: params.runStartedAt,
364+
});
365+
logWarn(
366+
`[cron:${params.job.id}] skipping stale delivery scheduled at ${new Date(scheduledAtMs).toISOString()}, age ${Math.round((Date.now() - scheduledAtMs) / 60_000)}m`,
367+
);
368+
return params.withRunSession({
369+
status: "ok",
370+
summary,
371+
outputText,
372+
deliveryAttempted,
373+
delivered: false,
374+
...params.telemetry,
375+
});
376+
}
334377
deliveryAttempted = true;
335378
const cachedResults = getCompletedDirectCronDelivery(deliveryIdempotencyKey);
336379
if (cachedResults) {

0 commit comments

Comments
 (0)