122bfad535
The 2026-08-14 incident (t_20e23f84): 4 recurring no_agent interval jobs EAGAIN-failed at 12:50 and recorded ZERO executions for ~1h47m, surviving a gateway restart, cleared only by operator `cron resume` / force-run. The in-memory stale-claim sweep (t_3778a491, already on origin/main) heals a leaked `_running_job_ids` claim in-process, but a recurring job whose PERSISTED state shows last_status=error and whose next_run_at was re-armed into the future by mark_job_run is invisible to that sweep: it is not in the running set and not due, so it just sits — the restart-surviving half. cron/jobs.py::_get_due_jobs_locked now re-arms such a recurring job to next_run_at=now when all hold: persisted last_status==error, last_run_at older than cadence+grace (so it is a real wedge, not a normal transient-error retry), next_run_at in the future, and not running in this process. The scheduler then re-dispatches it on the next tick without force-run/resume. Logs cron.persisted_error.recovered, bumps a probe-visible counter, appends a JSONL row. Within-cadence errors are never force-re-armed. Tests: tests/cron/test_recurring_persisted_error_recovery.py (clean behavioral RED on unfixed main / GREEN here; 2 consecutive auto-fires; within-cadence not re-armed). Full tests/cron/: 713 passed, 1 skipped.