# NousResearch/hermes-agent · Cron jobs deleted after one run; repeat=-1 normalization

## Open now

- Nothing open on GitHub.

Three items concern cron jobs being deleted after a single run, apparently when an LLM passes repeat=-1 to mean 'forever'. Two titles propose normalizing repeat values of -1 or less to None, and the third is a bug report describing the deletion.

## TL;DR

- 3 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 1 PR(s) closed without merging.
- A fix was merged to main in PR #2612. Verify it addresses your case.
- **Recommended:** read PR #2612 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `resolved` · deciding PR(s): `pr-2612`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.67`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #2611: \[Bug\]: Cron jobs getting deleted after one run

## Prior & competing PRs

- **#2612: fix: normalize repeat<=0 to None — cron jobs deleted after first run when LLM passes -1**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `cron/jobs.py`, `tests/cron/test_jobs.py`, `tools/cronjob_tools.py`
  - fix hunks:
  - `cron/jobs.py`

    ```diff
    @@ -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
    ```

    ```diff
    @@ -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)
    ```
  - `tests/cron/test_jobs.py`

    ```diff
    @@ -313,6 +313,24 @@ def test_repeat_limit_removes_job(self, tmp_cron_dir):
             # 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")
    ```
  - `tools/cronjob_tools.py`

    ```diff
    @@ -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)
    ```
- **#2616: fix(cron): normalize repeat=-1 (LLM 'forever' convention) to None to prevent auto-deletion**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#2612`

## Fix status

- Merged to main in PR #2612 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-03-23`. This confirmation is old and may be out of date (a revert/reopen would not have invalidated it here); re-verify.
- Subsystem: `cron`
