fix(cron): share isolated announce flow + harden cron scheduling/delivery (#11641)

* fix(cron): comprehensive cron scheduling and delivery fixes

- Fix delivery target resolution for isolated agent cron jobs
- Improve schedule parsing and validation
- Add job retry logic and error handling
- Enhance cron ops with better state management
- Add timer improvements for more reliable cron execution
- Add cron event type to protocol schema
- Support cron events in heartbeat runner (skip empty-heartbeat check,
  use dedicated CRON_EVENT_PROMPT for relay)

* fix: remove cron debug test and add changelog/docs notes (#11641) (thanks @tyler6204)
This commit is contained in:
Tyler Yust
2026-02-07 19:46:01 -08:00
committed by GitHub
parent ebe5730401
commit 8fae55e8e0
19 changed files with 488 additions and 150 deletions

View File

@@ -52,6 +52,12 @@ export function stop(state: CronServiceState) {
export async function status(state: CronServiceState) {
return await locked(state, async () => {
await ensureLoaded(state, { skipRecompute: true });
if (state.store) {
const changed = recomputeNextRuns(state);
if (changed) {
await persist(state);
}
}
return {
enabled: state.deps.cronEnabled,
storePath: state.deps.storePath,
@@ -64,6 +70,12 @@ export async function status(state: CronServiceState) {
export async function list(state: CronServiceState, opts?: { includeDisabled?: boolean }) {
return await locked(state, async () => {
await ensureLoaded(state, { skipRecompute: true });
if (state.store) {
const changed = recomputeNextRuns(state);
if (changed) {
await persist(state);
}
}
const includeDisabled = opts?.includeDisabled === true;
const jobs = (state.store?.jobs ?? []).filter((j) => includeDisabled || j.enabled);
return jobs.toSorted((a, b) => (a.state.nextRunAtMs ?? 0) - (b.state.nextRunAtMs ?? 0));
@@ -76,8 +88,25 @@ export async function add(state: CronServiceState, input: CronJobCreate) {
await ensureLoaded(state);
const job = createJob(state, input);
state.store?.jobs.push(job);
// Defensive: recompute all next-run times to ensure consistency
recomputeNextRuns(state);
await persist(state);
armTimer(state);
state.deps.log.info(
{
jobId: job.id,
jobName: job.name,
nextRunAtMs: job.state.nextRunAtMs,
schedulerNextWakeAtMs: nextWakeAtMs(state) ?? null,
timerArmed: state.timer !== null,
cronEnabled: state.deps.cronEnabled,
},
"cron: job added",
);
emit(state, {
jobId: job.id,
action: "added",
@@ -110,12 +139,17 @@ export async function update(state: CronServiceState, id: string, patch: CronJob
};
}
}
const scheduleChanged = patch.schedule !== undefined;
const enabledChanged = patch.enabled !== undefined;
job.updatedAtMs = now;
if (job.enabled) {
job.state.nextRunAtMs = computeJobNextRunAtMs(job, now);
} else {
job.state.nextRunAtMs = undefined;
job.state.runningAtMs = undefined;
if (scheduleChanged || enabledChanged) {
if (job.enabled) {
job.state.nextRunAtMs = computeJobNextRunAtMs(job, now);
} else {
job.state.nextRunAtMs = undefined;
job.state.runningAtMs = undefined;
}
}
await persist(state);