langchain-ai/deepagents · error · CronJobError

cron retention window cannot be negative

Error message

cron retention window cannot be negative

What it means

CronJobStore.prune_completed deletes disabled, completed jobs older than a retention window. A negative retain_for timedelta is rejected up front with CronJobError because it would make the cutoff in the future and silently delete jobs that are not actually expired.

Source

Thrown at libs/talon/deepagents_talon/cron/jobs.py:601

        *,
        retain_for: timedelta,
        now: datetime | None = None,
    ) -> list[CronJob]:
        """Delete completed jobs older than the retention window.

        Args:
            retain_for: Duration to keep disabled jobs after completion.
            now: Current timestamp override for deterministic tests.

        Returns:
            Removed job records.

        Raises:
            CronJobError: If `retain_for` is negative.
        """
        if retain_for < timedelta(0):
            msg = "cron retention window cannot be negative"
            raise CronJobError(msg)

        cutoff = _coerce_utc(now) - retain_for
        kept: list[CronJob] = []
        removed: list[CronJob] = []
        for job in self.list_jobs():
            reference = job.last_run_at or job.created_at
            if not job.enabled and job.next_run_at is None and reference <= cutoff:
                removed.append(job)
            else:
                kept.append(job)
        if removed:
            self._write_jobs(kept)
        return removed

    def _read_jobs(self) -> list[CronJob]:
        self._ensure_store()
        if not self.path.exists():
            return []

View on GitHub (pinned to a1af029e6e)

Solutions

  1. Pass a non-negative timedelta, e.g. timedelta(days=7) or timedelta(0).
  2. Clamp computed windows: retain_for = max(computed, timedelta(0)) before calling.
  3. Fix the config/source value that produced the negative duration and re-run cleanup.

Example fix

// before
removed = store.prune_completed(retain_for=timedelta(seconds=expiry_ts - now_ts))
// after
window = timedelta(seconds=max(0, expiry_ts - now_ts))
removed = store.prune_completed(retain_for=window)
Defensive patterns

Strategy: validation

Validate before calling

if retain_for < timedelta(0):
    raise ValueError("retain_for must be >= 0")

Type guard

def is_non_negative(delta: timedelta) -> bool:
    return delta >= timedelta(0)

Try / catch

try:
    removed = store.prune_completed(retain_for=retain_for)
except CronJobError as exc:
    logger.error("bad retention window: %s", exc)
    removed = store.prune_completed(retain_for=timedelta(0))

Prevention

When it happens

Trigger: Calling store.prune_completed(retain_for=timedelta(days=-1)) or computing retain_for from a configuration value that can go negative (e.g., expiry_ts - now where expiry already passed).

Common situations: Config-driven retention where an env/config value is parsed with the wrong sign; deriving the window by subtracting timestamps in the wrong order; a caller passing timedelta(0) is fine, but a typo like timedelta(hours=-1) from arithmetic is common.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of langchain-ai/deepagents@a1af029e6e (2026-08-29). Data as JSON: /api/errors/9bdedd95574224e5. Report an issue: GitHub.