A hand-edited "completed": null was patched with `.get("completed") or 0`
at five arithmetic readers. The display readers were still missed: the
cronjob tool listed a never-run one-shot as "1/1", and `hermes cron`
printed "None/3". Any new reader would bring the bug back.
Every reader gets its jobs through load_jobs: mark_job_run, update_job,
claim_dispatch, the due scan, merge_job_definition, list_jobs/get_job for
cronjob_job_args._repeat_display and hermes_cli/cron._job_rows. So reset a
null to 0 once in the load_jobs repair pass, which also persists the fix,
and change the per-site `or 0` patches back to their base form.
Co-authored-by: mochamgx <1114149@qq.com>
Co-authored-by: John Paul Soliva <soliva.johnpaul@icloud.com>
Co-authored-by: JoaoMarcos44 <joaomarcosdias444@gmail.com>
78 lines
3.4 KiB
Python
78 lines
3.4 KiB
Python
"""Job-definition schema shared by importers of a foreign cron store.
|
|
|
|
Cron owns which persisted fields are *authored* (create_job) versus *advanced by the
|
|
scheduler* (next_run_at, state, run counters...). Callers merging an authored store
|
|
into a live one (profile distributions) import this rather than duplicating the list.
|
|
"""
|
|
from typing import Any, Dict
|
|
|
|
from cron.jobs import _apply_schedule_update, _jobs_lock, is_job_runnable, load_jobs, parse_schedule, save_jobs
|
|
from cron.quota_hold import clear_state as _clear_quota_hold
|
|
from hermes_time import now as _hermes_now
|
|
|
|
JOB_DEFINITION_FIELDS = frozenset({
|
|
"name", "prompt", "skills", "skill", "model", "provider", "base_url",
|
|
"script", "no_agent", "monitor_script", "monitor_url", "context_from",
|
|
"schedule", "schedule_display", "deliver", "origin", "enabled_toolsets",
|
|
"workdir", "attach_to_session", "reasoning_effort", "failure_deliver",
|
|
})
|
|
|
|
|
|
def merge_job_definition(local: Dict[str, Any], authored: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Refresh authored fields while preserving this store's scheduler-owned state.
|
|
|
|
Raises ValueError when the authored schedule cannot be scheduled (unparseable string,
|
|
past one-shot for a live job)."""
|
|
merged = {key: value for key, value in local.items() if key not in JOB_DEFINITION_FIELDS}
|
|
merged.update((key, authored[key]) for key in JOB_DEFINITION_FIELDS if key in authored)
|
|
merged["repeat"] = {
|
|
"completed": (local.get("repeat") or {}).get("completed", 0),
|
|
"times": (authored.get("repeat") or {}).get("times"),
|
|
}
|
|
if isinstance(merged.get("schedule"), str):
|
|
merged["schedule"] = parse_schedule(merged["schedule"])
|
|
|
|
if local.get("schedule") != merged.get("schedule"):
|
|
merged.pop("pending_slot", None)
|
|
_clear_quota_hold(merged)
|
|
if is_job_runnable(merged):
|
|
updates = {"schedule": merged["schedule"]}
|
|
if "schedule_display" in authored:
|
|
updates["schedule_display"] = authored["schedule_display"]
|
|
_apply_schedule_update(merged, updates, str(merged.get("id") or "imported job"))
|
|
else:
|
|
merged["next_run_at"] = None
|
|
return merged
|
|
|
|
|
|
def import_job_definitions(shipped: Dict[str, Dict[str, Any]], *, paused_reason: str) -> None:
|
|
"""Merge *shipped* (job id -> authored record) into the active store under its lock.
|
|
|
|
Unknown ids arrive with the marker set ``create_job(paused=True)`` writes; known ids keep
|
|
their scheduler state. Nothing is written when a record cannot be merged: the ValueError
|
|
is re-raised naming the job."""
|
|
now = _hermes_now().isoformat()
|
|
seed = {
|
|
"enabled": False,
|
|
"state": "paused",
|
|
"paused_at": now,
|
|
"paused_reason": paused_reason,
|
|
"created_at": now,
|
|
"next_run_at": None,
|
|
}
|
|
pending = dict(shipped)
|
|
with _jobs_lock():
|
|
merged = []
|
|
for local in load_jobs():
|
|
incoming = pending.pop(local.get("id"), None)
|
|
merged.append(local if incoming is None else _merge_or_name(local, incoming))
|
|
merged.extend(_merge_or_name({"id": job_id, **seed}, incoming) for job_id, incoming in pending.items())
|
|
save_jobs(merged)
|
|
|
|
|
|
def _merge_or_name(local: Dict[str, Any], authored: Dict[str, Any]) -> Dict[str, Any]:
|
|
try:
|
|
return merge_job_definition(local, authored)
|
|
except ValueError as exc:
|
|
raise ValueError(f"cron job {authored.get('name') or local.get('id')!r}: {exc}") from exc
|