Compare commits

...

8 Commits

Author SHA1 Message Date
6df40c52f3 feat(cron): Show error staleness in job list
Some checks failed
Forge CI / smoke-and-build (pull_request) Failing after 14s
Part of #349. Distinguishes current errors from stale/cleared errors.
2026-04-13 22:53:49 +00:00
479f5a615f feat(cron): Include error_cleared_at in job format
Part of #349. Shows when stale errors were cleared.
2026-04-13 22:53:07 +00:00
d7f4c0886e feat(cron): Add clear-error command handler
Part of #349. Enables `hermes cron clear-error JOB_ID` command.
2026-04-13 22:52:50 +00:00
aa1e460912 feat(cron): Add clear-error CLI subparser
Part of #349. Adds `hermes cron clear-error JOB_ID` command.
2026-04-13 22:51:31 +00:00
f2aef27d2e feat(cron): Add clear-error CLI action
Part of #349. Adds `hermes cron clear-error JOB_ID` command to clear stale error states.
2026-04-13 22:50:42 +00:00
9289734034 feat(cron): Add clear_error action to cronjob_tools
Part of #349. Enables clearing stale error states via CLI/API.
2026-04-13 22:50:13 +00:00
6fcd046e00 feat(cron): Add clear_job_error() to clear stale error states
Part of #349. Allows clearing stale revoked-session failure states after auth recovery.
2026-04-13 22:49:52 +00:00
906c3a4259 test update 2026-04-13 22:49:31 +00:00
4 changed files with 69 additions and 3 deletions

View File

@@ -656,6 +656,31 @@ def mark_job_run(job_id: str, success: bool, error: Optional[str] = None):
save_jobs(jobs)
def clear_job_error(job_id: str) -> Optional[Dict[str, Any]]:
"""
Clear stale error state for a job.
Resets last_status to 'ok', last_error to None, and
records when the error was cleared. Useful after auth
recovery when the job itself is healthy but stale error
state persists.
Returns:
Updated job dict, or None if not found.
"""
jobs = load_jobs()
for job in jobs:
if job["id"] == job_id:
job["last_status"] = "ok"
job["last_error"] = None
job["error_cleared_at"] = _hermes_now().isoformat()
save_jobs(jobs)
return job
save_jobs(jobs)
return None
def advance_next_run(job_id: str) -> bool:
"""Preemptively advance next_run_at for a recurring job before execution.

View File

@@ -93,6 +93,20 @@ def cron_list(show_all: bool = False):
script = job.get("script")
if script:
print(f" Script: {script}")
# Show error state with staleness
last_error = job.get("last_error")
last_status = job.get("last_status")
error_cleared_at = job.get("error_cleared_at")
if last_error:
if error_cleared_at:
print(color(f" Error (stale): {last_error[:100]}...", Colors.YELLOW))
print(color(f" Cleared at: {error_cleared_at}", Colors.DIM))
else:
print(color(f" Error: {last_error[:100]}...", Colors.RED))
elif last_status == "ok" and error_cleared_at:
print(color(f" Status: ok (error cleared at {error_cleared_at})", Colors.GREEN))
print()
from hermes_cli.gateway import find_gateway_pids
@@ -221,8 +235,20 @@ def cron_edit(args):
return 0
def _job_action(action: str, job_id: str, success_verb: str, now: bool = False) -> int:
if action == "run" and now:
if action == "clear_error":
result = _cron_api(action="clear_error", job_id=job_id)
if not result.get("success"):
print(color(f"Failed to clear error: {result.get('error', 'unknown error')}", Colors.RED))
return 1
job = result.get("job", {})
name = job.get("name", job_id)
print(color(f"Cleared stale error state for job '{name}'", Colors.GREEN))
if job.get("error_cleared_at"):
print(f" Cleared at: {job['error_cleared_at']}")
return 0
if action == "run" and now:
# Synchronous execution — run job immediately and show result
result = _cron_api(action="run_now", job_id=job_id)
if not result.get("success"):
@@ -291,10 +317,12 @@ def cron_command(args):
if subcmd == "run":
now = getattr(args, 'now', False)
return _job_action("run", args.job_id, "Triggered", now=now)
if subcmd == "clear-error":
return _job_action("clear_error", args.job_id, "Cleared")
if subcmd in {"remove", "rm", "delete"}:
return _job_action("remove", args.job_id, "Removed")
print(f"Unknown cron command: {subcmd}")
print("Usage: hermes cron [list|create|edit|pause|resume|run|remove|status|tick]")
print("Usage: hermes cron [list|create|edit|pause|resume|run|remove|clear-error|status|tick]")
sys.exit(1)

View File

@@ -4576,6 +4576,9 @@ For more help on a command:
cron_run.add_argument("job_id", help="Job ID to trigger")
cron_run.add_argument("--now", action="store_true", help="Execute immediately and wait for result (clears stale errors)")
cron_clear_error = cron_subparsers.add_parser("clear-error", help="Clear stale error state for a job")
cron_clear_error.add_argument("job_id", help="Job ID to clear error for")
cron_remove = cron_subparsers.add_parser("remove", aliases=["rm", "delete"], help="Remove a scheduled job")
cron_remove.add_argument("job_id", help="Job ID to remove")

View File

@@ -200,7 +200,10 @@ def _format_job(job: Dict[str, Any]) -> Dict[str, Any]:
"state": job.get("state", "scheduled" if job.get("enabled", True) else "paused"),
"paused_at": job.get("paused_at"),
"paused_reason": job.get("paused_reason"),
}
} if job.get("error_cleared_at"):
result["error_cleared_at"] = job["error_cleared_at"]
if job.get("script"):
result["script"] = job["script"]
return result
@@ -326,6 +329,13 @@ def cronjob(
if result is None:
return json.dumps({"success": False, "error": "Job not found"}, indent=2)
return json.dumps(result, indent=2)
if normalized == "clear_error":
from cron.jobs import clear_job_error
job = clear_job_error(job_id)
if job is None:
return json.dumps({"success": False, "error": "Job not found"}, indent=2)
return json.dumps({"success": True, "job": _format_job(job)}, indent=2)
if normalized == "update":
updates: Dict[str, Any] = {}