--- title: "1.25.2 — Manual job retry publishes a runnable, linked replacement" description: "Retrying a failed, canceled or expired job now publishes a replacement with a fresh expiration and leaves the original as a terminal record linked to it. Before, an expired job's retry inherited its past deadline and expired before running, and the original was reset to `pending` with its diagnostic…" date: "2026-09-11" tags: ["release"] canonical: "https://django-mojo.sitesmojo.com/changelog/1-25-2/" --- Retrying a failed, canceled or expired job now publishes a replacement with a fresh expiration and leaves the original as a terminal record linked to it. Before, an expired job's retry inherited its past deadline and expired before running, and the original was reset to `pending` with its diagnostics wiped. A second retry is refused while the replacement is still live. ### Changed - Manual retry (`retry_request` action, `POST /api/jobs/retry`, `JobManager.retry_job`, the assistant `retry_job` tool) publishes a replacement job and leaves the original untouched — its status, attempt count and error diagnostics are no longer reset to `pending`. - The replacement's expiration is the larger of `JOBS_DEFAULT_EXPIRES_SEC` and the window the original was published with, extended by `delay`; the original's `expires_at` is never reused. - Original and replacement reference each other: `metadata.retried_as` on the original, `metadata.retried_from` on the replacement, and `new_job_id` in the original's `retry` event. - A second retry is refused with `Job already retried as ` while that replacement is pending, running or completed; allowed again once it has failed, been canceled or expired. This is double-submit protection, not a locked exactly-once guarantee. - `JobManager.retry_job` accepts `expired` jobs and returns the replacement id (or `False`), matching the service path. - A negative retry `delay` is refused instead of silently shortening the replacement's lifetime. - A non-numeric `delay` sent through `retry_request` returns `status: false` instead of a 500. - Removed `JobActionsService.pause_job` / `resume_job` — undocumented, unreferenced, and carrying the same reset-and-copy-expiry defect; `cancel_job` + `retry_job` cover the same need. ### Fixed - Retrying an expired job produced a replacement that expired before it ran. - Retrying left the original row `pending` with its diagnostics erased — a zombie that a `requeue_db_pending` sweep would have executed a second time. - A delayed retry longer than the default job lifetime expired before it was due. ### Upgrade notes - No migration. Code that detected "retry in flight" by watching the original job flip to `pending` should read `metadata.retried_as` instead — the original now stays `failed` / `canceled` / `expired`.