Skip to content

fix(cron): guard against None values in legacy job entries for cron list#23490

Closed
mtvgadgets wants to merge 1 commit into
NousResearch:mainfrom
mtvgadgets:fix/cron-list-attributeerror
Closed

fix(cron): guard against None values in legacy job entries for cron list#23490
mtvgadgets wants to merge 1 commit into
NousResearch:mainfrom
mtvgadgets:fix/cron-list-attributeerror

Conversation

@mtvgadgets

Copy link
Copy Markdown

Summary

hermes cron list raises AttributeError: 'NoneType' object has no attribute 'get' on any installation that has cron jobs predating the current repeat schema. A secondary cosmetic bug also displays Schedule: ? for those same legacy jobs because the schedule fallback references the wrong field name.

This PR fixes both with defensive or {} / fallback chain in hermes_cli/cron.py.

Reproduction

Any installation with cron jobs created on an older version (where repeat was serialized as null rather than {"times": null, "completed": 0}):

$ hermes cron list
┌─────────────────────────────────────────────────────────────────────────┐
│                         Scheduled Jobs                                  │
└─────────────────────────────────────────────────────────────────────────┘

Traceback (most recent call last):
  File ".../hermes_cli/main.py", line 10926, in main
    args.func(args)
  File ".../hermes_cli/cron.py", line 276, in cron_command
    cron_list(show_all)
  File ".../hermes_cli/cron.py", line 66, in cron_list
    repeat_times = repeat_info.get("times")
AttributeError: 'NoneType' object has no attribute 'get'

Sample ~/.hermes/cron/jobs.json that triggers the bug:

{
  "jobs": [
    {
      "id": "74bd2f6d0bce",
      "name": "Market Screener",
      "schedule": {"kind": "cron", "expr": "0 11,13,15 * * 1-5"},
      "repeat": null,
      ...
    }
  ]
}

(Compare to newer jobs created via hermes cron create, which emit "repeat": {"times": null, "completed": 0} and a top-level "schedule_display".)

Root cause

Two distinct issues in the same for job in jobs block:

1. repeat: nullAttributeError

repeat_info = job.get("repeat", {})   # returns None, not {}, when value IS None
repeat_times = repeat_info.get("times")  # boom

dict.get(key, default) returns the default only when the key is missing. When the key exists with value None, it returns None. Older jobs that serialized "repeat": null therefore bypass the default.

2. Schedule fallback references nonexistent value field

schedule = job.get("schedule_display", job.get("schedule", {}).get("value", "?"))

The serialized field is schedule.expr, not schedule.value. Newer jobs include a top-level schedule_display that masks this, but legacy jobs (without schedule_display) fall through to the missing value and render as Schedule: ?.

Fix

Two surgical changes, fully backwards compatible:

-        schedule = job.get("schedule_display", job.get("schedule", {}).get("value", "?"))
+        schedule_obj = job.get("schedule") or {}
+        schedule = (job.get("schedule_display")
+                    or schedule_obj.get("expr")
+                    or schedule_obj.get("value")
+                    or "?")
-        repeat_info = job.get("repeat", {})
+        repeat_info = job.get("repeat") or {}
  • or {} handles both missing key and None value uniformly.
  • The schedule fallback chain now includes expr (current field name) ahead of value (preserves legacy compatibility if any installation ever stored that shape).

Testing

Verified on a real installation with mixed-vintage jobs:

Before:

$ hermes cron list
[crashes with AttributeError]

After:

$ hermes cron list

┌─────────────────────────────────────────────────────────────────────────┐
│                         Scheduled Jobs                                  │
└─────────────────────────────────────────────────────────────────────────┘

  74bd2f6d0bce [active]
    Name:      Market Screener — Intraday
    Schedule:  0 11,13,15 * * 1-5
    Repeat:    ∞
    Next run:  2026-05-11T11:00:00-05:00
    ...

  2b6db102e070 [active]
    Name:      Sentiment Screener — Post-Market
    Schedule:  10 17 * * 1-5
    Repeat:    ∞
    Next run:  2026-05-11T17:10:00-05:00
    ...

Both legacy repeat: null jobs and newer repeat: {...} jobs render correctly with their schedule strings.

Notes

  • No changes to job creation logic — the repeat: null / repeat: {...} shape divergence is pre-existing and out of scope. A future migration pass could normalize on read, but defensive parsing is a faster and safer fix for the immediate UX regression.
  • No new dependencies, no schema changes, no behavior change for the happy path.

…list`

`hermes cron list` raised AttributeError on any installation with cron jobs
created on older versions, because two field shapes evolved:

1. `repeat` was stored as `None` in older jobs but as `{"times": null,
   "completed": 0}` in newer ones. `job.get("repeat", {})` returns `None`
   when the key exists with value `None`, so the subsequent
   `.get("times")` blew up with `AttributeError: 'NoneType' object has no
   attribute 'get'`.

2. The schedule fallback chain referenced `schedule.value`, but the
   serialized field is `schedule.expr`. Newer jobs include a top-level
   `schedule_display` so the bug was masked there, but legacy jobs (no
   `schedule_display`) fell through to the missing `value` key and
   rendered as `Schedule: ?`.

Both fixes are defensive: use `or {}` and add `expr` to the fallback
chain ahead of `value` to preserve compatibility with any historical
field naming.

Reproduction (any installation with cron jobs predating the
`repeat`/`schedule_display` schema change):

  $ hermes cron list
  Traceback (most recent call last):
    File ".../hermes_cli/main.py", line 10926, in main
      args.func(args)
    File ".../hermes_cli/cron.py", line 276, in cron_command
      cron_list(show_all)
    File ".../hermes_cli/cron.py", line 66, in cron_list
      repeat_times = repeat_info.get("times")
  AttributeError: 'NoneType' object has no attribute 'get'
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/cron Cron scheduler and job management comp/cli CLI entry point, hermes_cli/, setup wizard labels May 11, 2026
@teknium1

Copy link
Copy Markdown
Contributor

This appears to be implemented on current main. Thanks for the clear repro and root-cause writeup — this automated hermes-sweeper review found the same bug class has since been fixed.

Evidence:

  • hermes_cli/cron.py:87 now uses repeat_info = job.get("repeat") or {}, so persisted "repeat": null entries no longer crash before reading times.
  • tests/hermes_cli/test_cron.py:109 covers that exact legacy shape by forcing a saved job's repeat to None, running cron list, and asserting it renders Repeat: ∞.
  • cron/jobs.py:104 and cron/jobs.py:727 normalize job records before listing; _schedule_display_for_job() fills schedule_display from schedule display, value, expr, or run_at, covering the legacy schedule fallback case discussed here.
  • The null-repeat fix landed in b0d234f068952e7bc198759ae3ca2cda99bae491 and is contained in v2026.6.5.

@teknium1 teknium1 closed this Jun 11, 2026
@teknium1 teknium1 added the sweeper:implemented-on-main Sweeper: behavior already present on current main label Jun 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard comp/cron Cron scheduler and job management P2 Medium — degraded but workaround exists sweeper:implemented-on-main Sweeper: behavior already present on current main type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants