From ca2958ff98fd5a9b76d8586cbad0daa7a95a3ccf Mon Sep 17 00:00:00 2001 From: Mibay <97958526+Mibayy@users.noreply.github.com> Date: Mon, 23 Mar 2026 14:35:43 +0100 Subject: [PATCH] fix: normalize repeat<=0 to None to prevent cron jobs deleting after first run (#2612) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix: normalize repeat<=0 to None — cron jobs deleted after first run when LLM passes -1 --- cron/jobs.py | 6 +++++- tests/cron/test_jobs.py | 18 ++++++++++++++++++ tools/cronjob_tools.py | 4 +++- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/cron/jobs.py b/cron/jobs.py index 86a50f3b2..1dd6c680e 100644 --- a/cron/jobs.py +++ b/cron/jobs.py @@ -383,6 +383,10 @@ def create_job( """ parsed_schedule = parse_schedule(schedule) + # Normalize repeat: treat 0 or negative values as None (infinite) + if repeat is not None and repeat <= 0: + repeat = None + # Auto-set repeat=1 for one-shot schedules if not specified if parsed_schedule["kind"] == "once" and repeat is None: repeat = 1 @@ -571,7 +575,7 @@ def mark_job_run(job_id: str, success: bool, error: Optional[str] = None): # Check if we've hit the repeat limit times = job["repeat"].get("times") completed = job["repeat"]["completed"] - if times is not None and completed >= times: + if times is not None and times > 0 and completed >= times: # Remove the job (limit reached) jobs.pop(i) save_jobs(jobs) diff --git a/tests/cron/test_jobs.py b/tests/cron/test_jobs.py index a0dc8a89b..71883d158 100644 --- a/tests/cron/test_jobs.py +++ b/tests/cron/test_jobs.py @@ -313,6 +313,24 @@ class TestMarkJobRun: # Job should be removed after hitting repeat limit assert get_job(job["id"]) is None + def test_repeat_negative_one_is_infinite(self, tmp_cron_dir): + # LLMs often pass repeat=-1 to mean "infinite/forever". + # The job must NOT be deleted after runs when repeat <= 0. + job = create_job(prompt="Forever", schedule="every 1h", repeat=-1) + # -1 should be normalised to None (infinite) at create time + assert job["repeat"]["times"] is None + # Running it multiple times should never delete it + for _ in range(3): + mark_job_run(job["id"], success=True) + assert get_job(job["id"]) is not None, "job was deleted after run despite infinite repeat" + + def test_repeat_zero_is_infinite(self, tmp_cron_dir): + # repeat=0 should also be treated as None (infinite), not "run zero times". + job = create_job(prompt="ZeroRepeat", schedule="every 1h", repeat=0) + assert job["repeat"]["times"] is None + mark_job_run(job["id"], success=True) + assert get_job(job["id"]) is not None + def test_error_status(self, tmp_cron_dir): job = create_job(prompt="Fail", schedule="every 1h") mark_job_run(job["id"], success=False, error="timeout") diff --git a/tools/cronjob_tools.py b/tools/cronjob_tools.py index 62ea1bb71..0a023c904 100644 --- a/tools/cronjob_tools.py +++ b/tools/cronjob_tools.py @@ -266,8 +266,10 @@ def cronjob( if base_url is not None: updates["base_url"] = _normalize_optional_job_value(base_url, strip_trailing_slash=True) if repeat is not None: + # Normalize: treat 0 or negative as None (infinite) + normalized_repeat = None if repeat <= 0 else repeat repeat_state = dict(job.get("repeat") or {}) - repeat_state["times"] = repeat + repeat_state["times"] = normalized_repeat updates["repeat"] = repeat_state if schedule is not None: parsed_schedule = parse_schedule(schedule)