You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
On Agent Detail → Schedules, an Active daily schedule (0 4 * * *, Europe/Kiev) displays "→ Next: 1d ago" with 0 runs recorded (7d success —). A "next run" timestamp in the past on an enabled schedule is a contradiction: at least two cron windows passed with no execution row, no skip record, and no advance of the stored next_run_at. A sibling schedule on the same agent fired successfully inside that same window, so the scheduler container was up and its sync loop was running.
Context
agent_schedules.next_run_at is a stored projection, written at creation (src/backend/db/schedules.py:_calculate_next_run_at), at _add_job time (startup + sync), and on the post-fire paths. Several runtime paths let a cron fire pass without advancing the projection or recording anything, which leaves the UI showing a receding "Next: Xd ago":
Autonomy-disabled skip — src/scheduler/service.py:717-719 returns without creating an execution row and without advancing run times. Fires keep silently skipping every window; next_run_at stays frozen, runs stay 0. (Contrast: the pre-check fire=false path at service.py:773-800 correctly records a skipped execution AND advances run times — the autonomy skip predates that pattern.)
Lock-contention return — service.py:690-693 returns with no advance and no record.
Failure outcome branch — service.py:867-888: the comment says "update schedule run times" but the branch never calls update_schedule_run_times, so a repeatedly-failing schedule also shows a stale past "Next:" (runs > 0 in that variant).
Sync-loop add-failure permanence — if _add_job throws, the exception is swallowed (service.py:399-400) but _sync_agent_schedules still records the snapshot (service.py:473), so the schedule is never retried — no APScheduler job exists until a container restart, and next_run_at stays at its creation-time value.
Frontend: src/frontend/src/components/SchedulesPanel.vue:321 renders Next: {{ formatRelativeTime(schedule.next_run_at) }} with no overdue handling, so a past timestamp renders as the nonsensical "Next: 1d ago" instead of an explicit overdue/warning state.
This is the same projection-vs-authority class as #1082 (status-as-projection): next_run_at is a projection of APScheduler's in-memory trigger state, and the write discipline around it is leaky.
Acceptance Criteria
Root cause identified for the observed case (enabled daily schedule, 0 runs, next_run_at ~1 day in the past, sibling schedule on the same agent firing normally)
Every path where a due cron fire is skipped either advances next_run_at or records an observable skip (execution row / event) — in particular the autonomy-disabled skip at service.py:717
Failure outcome branch advances run times (or the projection is advanced at fire time regardless of outcome)
_sync_agent_schedules no longer permanently orphans a schedule whose _add_job failed (retry on next sync, or surface the failure)
UI renders an explicit overdue state (e.g. "Overdue by 1d") instead of "Next: Xd ago" when next_run_at < now on an enabled schedule
Consider a canary invariant: enabled, non-deleted schedule with next_run_at older than now − misfire_grace_time (fits the CANARY-001 catalog)
Summary
On Agent Detail → Schedules, an Active daily schedule (
0 4 * * *,Europe/Kiev) displays "→ Next: 1d ago" with 0 runs recorded (7d success—). A "next run" timestamp in the past on an enabled schedule is a contradiction: at least two cron windows passed with no execution row, no skip record, and no advance of the storednext_run_at. A sibling schedule on the same agent fired successfully inside that same window, so the scheduler container was up and its sync loop was running.Context
agent_schedules.next_run_atis a stored projection, written at creation (src/backend/db/schedules.py:_calculate_next_run_at), at_add_jobtime (startup + sync), and on the post-fire paths. Several runtime paths let a cron fire pass without advancing the projection or recording anything, which leaves the UI showing a receding "Next: Xd ago":src/scheduler/service.py:717-719returns without creating an execution row and without advancing run times. Fires keep silently skipping every window;next_run_atstays frozen, runs stay 0. (Contrast: the pre-checkfire=falsepath atservice.py:773-800correctly records a skipped execution AND advances run times — the autonomy skip predates that pattern.)service.py:690-693returns with no advance and no record.service.py:867-888: the comment says "update schedule run times" but the branch never callsupdate_schedule_run_times, so a repeatedly-failing schedule also shows a stale past "Next:" (runs > 0 in that variant)._add_jobthrows, the exception is swallowed (service.py:399-400) but_sync_agent_schedulesstill records the snapshot (service.py:473), so the schedule is never retried — no APScheduler job exists until a container restart, andnext_run_atstays at its creation-time value._get_missed_schedules) only runs atinitialize(); nothing detects or repairs a stalenext_run_atwhile the scheduler is running.Frontend:
src/frontend/src/components/SchedulesPanel.vue:321rendersNext: {{ formatRelativeTime(schedule.next_run_at) }}with no overdue handling, so a past timestamp renders as the nonsensical "Next: 1d ago" instead of an explicit overdue/warning state.This is the same projection-vs-authority class as #1082 (status-as-projection):
next_run_atis a projection of APScheduler's in-memory trigger state, and the write discipline around it is leaky.Acceptance Criteria
next_run_at~1 day in the past, sibling schedule on the same agent firing normally)next_run_ator records an observable skip (execution row / event) — in particular the autonomy-disabled skip atservice.py:717_sync_agent_schedulesno longer permanently orphans a schedule whose_add_jobfailed (retry on next sync, or surface the failure)next_run_at < nowon an enabled schedulenext_run_atolder thannow − misfire_grace_time(fits the CANARY-001 catalog)Technical Notes
src/scheduler/service.py—_execute_schedule(lock),_execute_schedule_with_lock(autonomy gate),_dispatch_and_record_outcome(success/dispatched advance; failure branch does not),_sync_agent_schedules+_add_job(silent add-failure),_get_missed_schedules(startup-only catch-up)src/backend/db/schedules.py(_calculate_next_run_at,update_schedule_run_times)src/frontend/src/components/SchedulesPanel.vue:321