fix(cron): dual-case keys when loading jobs.json
jobs.json hand-edits and asdict-style snake_case for schedule intervals and runHistory crashed or silently disabled cron. Deserialize via Cron* from_store_dict and shared get_camel_snake (also used by local triggers).
This commit is contained in:
+1
-56
@@ -222,62 +222,7 @@ class CronService:
|
||||
jobs = []
|
||||
version = data.get("version", 1)
|
||||
for j in data.get("jobs", []):
|
||||
job = CronJob(
|
||||
id=j["id"],
|
||||
name=j["name"],
|
||||
enabled=j.get("enabled", True),
|
||||
schedule=CronSchedule(
|
||||
kind=j["schedule"]["kind"],
|
||||
at_ms=j["schedule"].get("atMs"),
|
||||
every_ms=j["schedule"].get("everyMs"),
|
||||
expr=j["schedule"].get("expr"),
|
||||
tz=j["schedule"].get("tz"),
|
||||
),
|
||||
payload=CronPayload(
|
||||
kind=j["payload"].get("kind", "agent_turn"),
|
||||
message=j["payload"].get("message", ""),
|
||||
deliver=j["payload"].get("deliver", False),
|
||||
channel=j["payload"].get("channel"),
|
||||
to=j["payload"].get("to"),
|
||||
channel_meta=(
|
||||
j["payload"].get("channelMeta")
|
||||
or j["payload"].get("channel_meta")
|
||||
or {}
|
||||
),
|
||||
session_key=j["payload"].get("sessionKey") or j["payload"].get("session_key"),
|
||||
origin_channel=(
|
||||
j["payload"].get("originChannel")
|
||||
or j["payload"].get("origin_channel")
|
||||
),
|
||||
origin_chat_id=(
|
||||
j["payload"].get("originChatId")
|
||||
or j["payload"].get("origin_chat_id")
|
||||
),
|
||||
origin_metadata=(
|
||||
j["payload"].get("originMetadata")
|
||||
or j["payload"].get("origin_metadata")
|
||||
or {}
|
||||
),
|
||||
),
|
||||
state=CronJobState(
|
||||
next_run_at_ms=j.get("state", {}).get("nextRunAtMs"),
|
||||
last_run_at_ms=j.get("state", {}).get("lastRunAtMs"),
|
||||
last_status=j.get("state", {}).get("lastStatus"),
|
||||
last_error=j.get("state", {}).get("lastError"),
|
||||
run_history=[
|
||||
CronRunRecord(
|
||||
run_at_ms=r["runAtMs"],
|
||||
status=r["status"],
|
||||
duration_ms=r.get("durationMs", 0),
|
||||
error=r.get("error"),
|
||||
)
|
||||
for r in j.get("state", {}).get("runHistory", [])
|
||||
],
|
||||
),
|
||||
created_at_ms=j.get("createdAtMs", 0),
|
||||
updated_at_ms=j.get("updatedAtMs", 0),
|
||||
delete_after_run=j.get("deleteAfterRun", False),
|
||||
)
|
||||
job = CronJob.from_store_dict(j)
|
||||
_normalize_agent_turn_job(job)
|
||||
jobs.append(job)
|
||||
except Exception:
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
"""Cron types."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Literal
|
||||
|
||||
from nanobot.utils.dict_keys import get_camel_snake
|
||||
|
||||
|
||||
@dataclass
|
||||
class CronSchedule:
|
||||
@@ -17,6 +21,16 @@ class CronSchedule:
|
||||
# Timezone for cron expressions
|
||||
tz: str | None = None
|
||||
|
||||
@classmethod
|
||||
def from_store_dict(cls, data: dict[str, Any]) -> CronSchedule:
|
||||
return cls(
|
||||
kind=data["kind"],
|
||||
at_ms=get_camel_snake(data, "atMs", "at_ms"),
|
||||
every_ms=get_camel_snake(data, "everyMs", "every_ms"),
|
||||
expr=data.get("expr"),
|
||||
tz=data.get("tz"),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class CronPayload:
|
||||
@@ -33,6 +47,25 @@ class CronPayload:
|
||||
origin_chat_id: str | None = None
|
||||
origin_metadata: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@classmethod
|
||||
def from_store_dict(cls, data: dict[str, Any]) -> CronPayload:
|
||||
return cls(
|
||||
kind=data.get("kind", "agent_turn"),
|
||||
message=data.get("message", ""),
|
||||
deliver=data.get("deliver", False),
|
||||
channel=data.get("channel"),
|
||||
to=data.get("to"),
|
||||
channel_meta=dict(
|
||||
get_camel_snake(data, "channelMeta", "channel_meta", {}) or {}
|
||||
),
|
||||
session_key=get_camel_snake(data, "sessionKey", "session_key"),
|
||||
origin_channel=get_camel_snake(data, "originChannel", "origin_channel"),
|
||||
origin_chat_id=get_camel_snake(data, "originChatId", "origin_chat_id"),
|
||||
origin_metadata=dict(
|
||||
get_camel_snake(data, "originMetadata", "origin_metadata", {}) or {}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class CronRunRecord:
|
||||
@@ -42,6 +75,15 @@ class CronRunRecord:
|
||||
duration_ms: int = 0
|
||||
error: str | None = None
|
||||
|
||||
@classmethod
|
||||
def from_store_dict(cls, data: dict[str, Any]) -> CronRunRecord:
|
||||
return cls(
|
||||
run_at_ms=int(get_camel_snake(data, "runAtMs", "run_at_ms", 0)),
|
||||
status=data["status"],
|
||||
duration_ms=int(get_camel_snake(data, "durationMs", "duration_ms", 0)),
|
||||
error=data.get("error"),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class CronJobState:
|
||||
@@ -52,6 +94,22 @@ class CronJobState:
|
||||
last_error: str | None = None
|
||||
run_history: list[CronRunRecord] = field(default_factory=list)
|
||||
|
||||
@classmethod
|
||||
def from_store_dict(cls, data: dict[str, Any]) -> CronJobState:
|
||||
history = get_camel_snake(data, "runHistory", "run_history", []) or []
|
||||
return cls(
|
||||
next_run_at_ms=get_camel_snake(data, "nextRunAtMs", "next_run_at_ms"),
|
||||
last_run_at_ms=get_camel_snake(data, "lastRunAtMs", "last_run_at_ms"),
|
||||
last_status=get_camel_snake(data, "lastStatus", "last_status"),
|
||||
last_error=get_camel_snake(data, "lastError", "last_error"),
|
||||
run_history=[
|
||||
record
|
||||
if isinstance(record, CronRunRecord)
|
||||
else CronRunRecord.from_store_dict(record)
|
||||
for record in history
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class CronJob:
|
||||
@@ -78,6 +136,23 @@ class CronJob:
|
||||
kwargs["state"] = CronJobState(**state_kwargs)
|
||||
return cls(**kwargs)
|
||||
|
||||
@classmethod
|
||||
def from_store_dict(cls, data: dict[str, Any]) -> CronJob:
|
||||
"""Load a job from jobs.json (camelCase with snake_case fallbacks)."""
|
||||
return cls(
|
||||
id=data["id"],
|
||||
name=data["name"],
|
||||
enabled=data.get("enabled", True),
|
||||
schedule=CronSchedule.from_store_dict(data["schedule"]),
|
||||
payload=CronPayload.from_store_dict(data.get("payload") or {}),
|
||||
state=CronJobState.from_store_dict(data.get("state") or {}),
|
||||
created_at_ms=int(get_camel_snake(data, "createdAtMs", "created_at_ms", 0)),
|
||||
updated_at_ms=int(get_camel_snake(data, "updatedAtMs", "updated_at_ms", 0)),
|
||||
delete_after_run=bool(
|
||||
get_camel_snake(data, "deleteAfterRun", "delete_after_run", False)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class CronStore:
|
||||
|
||||
Reference in New Issue
Block a user