macroclaw 0.23.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cron.test.ts +43 -0
- package/src/cron.ts +7 -2
package/package.json
CHANGED
package/src/cron.test.ts
CHANGED
|
@@ -39,6 +39,12 @@ function minutesAgoCron(minutesAgo: number): string {
|
|
|
39
39
|
return `${past.getMinutes()} ${past.getHours()} ${past.getDate()} ${past.getMonth() + 1} *`;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
// Build a cron expression that matched N days ago
|
|
43
|
+
function daysAgoCron(daysAgo: number): string {
|
|
44
|
+
const past = new Date(Date.now() - daysAgo * 24 * 60 * 60_000);
|
|
45
|
+
return `${past.getMinutes()} ${past.getHours()} ${past.getDate()} ${past.getMonth() + 1} *`;
|
|
46
|
+
}
|
|
47
|
+
|
|
42
48
|
beforeEach(() => {
|
|
43
49
|
mkdirSync(SCHEDULE_DIR, { recursive: true });
|
|
44
50
|
});
|
|
@@ -337,4 +343,41 @@ describe("missed non-recurring events", () => {
|
|
|
337
343
|
const updated = readScheduleConfig();
|
|
338
344
|
expect(updated.jobs).toHaveLength(0);
|
|
339
345
|
});
|
|
346
|
+
|
|
347
|
+
it("discards non-recurring job missed by more than a week without firing", () => {
|
|
348
|
+
writeScheduleConfig({
|
|
349
|
+
jobs: [
|
|
350
|
+
{ name: "stale", cron: daysAgoCron(10), prompt: "too old", recurring: false },
|
|
351
|
+
{ name: "keeper", cron: nonMatchingCron(), prompt: "stay" },
|
|
352
|
+
],
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
const onJob = makeOnJob();
|
|
356
|
+
const cron = new CronScheduler(TEST_DIR, { onJob });
|
|
357
|
+
cron.start();
|
|
358
|
+
cron.stop();
|
|
359
|
+
|
|
360
|
+
expect(onJob).not.toHaveBeenCalled();
|
|
361
|
+
|
|
362
|
+
const updated = readScheduleConfig();
|
|
363
|
+
expect(updated.jobs).toHaveLength(1);
|
|
364
|
+
expect(updated.jobs[0].name).toBe("keeper");
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it("fires non-recurring job missed by 6 days (within week window)", () => {
|
|
368
|
+
writeScheduleConfig({
|
|
369
|
+
jobs: [{ name: "recent", cron: daysAgoCron(6), prompt: "still valid", recurring: false }],
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
const onJob = makeOnJob();
|
|
373
|
+
const cron = new CronScheduler(TEST_DIR, { onJob });
|
|
374
|
+
cron.start();
|
|
375
|
+
cron.stop();
|
|
376
|
+
|
|
377
|
+
expect(onJob).toHaveBeenCalledTimes(1);
|
|
378
|
+
const call = onJob.mock.calls[0];
|
|
379
|
+
expect(call[0]).toBe("recent");
|
|
380
|
+
expect(call[1]).toContain("[missed event, should have fired");
|
|
381
|
+
expect(call[1]).toContain("still valid");
|
|
382
|
+
});
|
|
340
383
|
});
|
package/src/cron.ts
CHANGED
|
@@ -25,6 +25,7 @@ export interface CronSchedulerConfig {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
const TICK_INTERVAL = 10_000; // 10 seconds
|
|
28
|
+
const MAX_MISSED_MS = 7 * 24 * 60 * 60 * 1000; // 1 week
|
|
28
29
|
|
|
29
30
|
export class CronScheduler {
|
|
30
31
|
#lastMinute = -1;
|
|
@@ -87,14 +88,18 @@ export class CronScheduler {
|
|
|
87
88
|
if (job.recurring === false) {
|
|
88
89
|
firedNonRecurring.push(i);
|
|
89
90
|
}
|
|
90
|
-
} else if (job.recurring === false && diff >= 60_000) {
|
|
91
|
-
// Non-recurring job in the past — fire
|
|
91
|
+
} else if (job.recurring === false && diff >= 60_000 && diff <= MAX_MISSED_MS) {
|
|
92
|
+
// Non-recurring job in the past — fire if missed within the last week
|
|
92
93
|
const missedMinutes = Math.round(diff / 60_000);
|
|
93
94
|
const firedAt = prev.toISOString();
|
|
94
95
|
const missedPrompt = `[missed event, should have fired ${missedMinutes} min ago at ${firedAt}] ${job.prompt}`;
|
|
95
96
|
log.info({ name: job.name, missedMinutes, firedAt }, "Firing missed non-recurring job");
|
|
96
97
|
this.#config.onJob(job.name, missedPrompt, job.model);
|
|
97
98
|
firedNonRecurring.push(i);
|
|
99
|
+
} else if (job.recurring === false && diff > MAX_MISSED_MS) {
|
|
100
|
+
// Non-recurring job missed by more than a week — discard without firing
|
|
101
|
+
log.warn({ name: job.name, missedMinutes: Math.round(diff / 60_000) }, "Discarding stale non-recurring job");
|
|
102
|
+
firedNonRecurring.push(i);
|
|
98
103
|
}
|
|
99
104
|
} catch (err) {
|
|
100
105
|
log.warn({ cron: job.cron, err: err instanceof Error ? err.message : err }, "Invalid cron expression");
|