From 16e189833992fd8a7c8fed0ca2ab518865a58c07 Mon Sep 17 00:00:00 2001 From: tanzhenxin Date: Wed, 10 Jun 2026 16:16:33 +0800 Subject: [PATCH] fix(core): prevent cron scheduler from firing on creation minute The CronScheduler fired jobs immediately when the current minute matched the cron expression, because lastFiredAt was undefined and the double-fire guard was bypassed. Combined with /loop's intentional immediate execution (SKILL.md step 3), this caused duplicate execution of the prompt. Initialize lastFiredAt to the creation minute-start so the scheduler treats it as already-fired and waits for the next matching cron window. --- .../core/src/services/cronScheduler.test.ts | 24 ++++++++++++++++++- packages/core/src/services/cronScheduler.ts | 2 ++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/core/src/services/cronScheduler.test.ts b/packages/core/src/services/cronScheduler.test.ts index 4863bbb90b9..164d966fe79 100644 --- a/packages/core/src/services/cronScheduler.test.ts +++ b/packages/core/src/services/cronScheduler.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { CronScheduler, type CronJob } from './cronScheduler.js'; describe('CronScheduler', () => { @@ -99,6 +99,28 @@ describe('CronScheduler', () => { expect(fired[0]!.prompt).toBe('match'); }); + it('does not fire on the same minute the job was created', () => { + vi.useFakeTimers(); + const localScheduler = new CronScheduler(); + try { + vi.setSystemTime(new Date(2025, 0, 15, 10, 30, 15)); + const fired: CronJob[] = []; + localScheduler.start((job) => fired.push(job)); + + localScheduler.create('*/1 * * * *', 'should not fire yet', true); + + localScheduler.tick(new Date(2025, 0, 15, 10, 30, 59)); + expect(fired).toHaveLength(0); + + localScheduler.tick(new Date(2025, 0, 15, 10, 31, 59)); + expect(fired).toHaveLength(1); + expect(fired[0]!.prompt).toBe('should not fire yet'); + } finally { + localScheduler.destroy(); + vi.useRealTimers(); + } + }); + it('does not fire when no match', () => { const fired: CronJob[] = []; scheduler.start((job) => fired.push(job)); diff --git a/packages/core/src/services/cronScheduler.ts b/packages/core/src/services/cronScheduler.ts index 9685dde5bf8..81ed519ca19 100644 --- a/packages/core/src/services/cronScheduler.ts +++ b/packages/core/src/services/cronScheduler.ts @@ -115,6 +115,8 @@ export class CronScheduler { recurring, createdAt: now, expiresAt: recurring ? now + THREE_DAYS_MS : Infinity, + // Prevent the scheduler from firing during the creation minute + lastFiredAt: now - (now % 60_000), jitterMs, };