feat(cron): support name-based lookup for job operations

Cron mutation operations (run/pause/resume/remove) and 'hermes cron edit'
now accept a job name in addition to the hex ID, with case-insensitive
matching. Before this, 'hermes cron run my_job_name' died with
'Job with ID my_job_name not found' and forced the user to look up the
hex ID first.

The original PR matched by name but silently picked the first match when
two jobs shared a name. This version refuses to act on an ambiguous name
and surfaces every matching job (id, name, schedule, next_run_at) so the
caller can pick a specific ID.

- cron/jobs.py:
  - get_job() stays ID-only (preserves existing call-site semantics for
    web_server/api_server/curator/scheduler/test code that always passes
    real IDs).
  - resolve_job_ref() is the new name-or-ID resolver, used by pause/
    resume/trigger/remove_job. Exact ID match wins over a name match
    even if a different job's name happens to equal that ID. Ambiguous
    name match raises AmbiguousJobReference with all candidate IDs.
- tools/cronjob_tools.py: dispatch site uses resolve_job_ref, surfaces
  ambiguous matches as a structured error with the matching IDs.
- hermes_cli/cron.py: 'cron edit' uses resolve_job_ref so editing by
  name works and ambiguous names are reported with IDs.
- tests/cron/test_jobs.py: new TestResolveJobRef covering ID match,
  case-insensitive name match, ID-wins-over-name, ambiguous refusal,
  and that pause/resume/trigger/remove all refuse on ambiguity.

Closes #2627
This commit is contained in:
buntingszn 2026-05-15 01:33:12 -07:00 committed by Teknium
parent 05d9f641c0
commit 6682f91b80
4 changed files with 176 additions and 16 deletions

View file

@ -21,12 +21,14 @@ logger = logging.getLogger(__name__)
sys.path.insert(0, str(Path(__file__).parent.parent))
from cron.jobs import (
AmbiguousJobReference,
create_job,
get_job,
list_jobs,
parse_schedule,
pause_job,
remove_job,
resolve_job_ref,
resume_job,
trigger_job,
update_job,
@ -393,12 +395,32 @@ def cronjob(
if not job_id:
return tool_error(f"job_id is required for action '{normalized}'", success=False)
job = get_job(job_id)
if not job:
try:
job = resolve_job_ref(job_id)
except AmbiguousJobReference as exc:
return json.dumps(
{"success": False, "error": f"Job with ID '{job_id}' not found. Use cronjob(action='list') to inspect jobs."},
{
"success": False,
"error": str(exc),
"matches": [
{
"id": m["id"],
"name": m.get("name"),
"schedule": m.get("schedule_display"),
"next_run_at": m.get("next_run_at"),
}
for m in exc.matches
],
},
indent=2,
)
if not job:
return json.dumps(
{"success": False, "error": f"Job with ID or name '{job_id}' not found. Use cronjob(action='list') to inspect jobs."},
indent=2,
)
# Resolve to canonical ID (supports name-based lookup)
job_id = job["id"]
if normalized == "remove":
removed = remove_job(job_id)