fix(cron): accept list-form deliver values so deliver=['telegram'] works (#17456)

The cron schema contracts deliver as a string ("local", "origin",
"telegram", "telegram:chat_id[:thread_id]", or comma-separated combos),
but MCP clients and scripts sometimes pass an array like ['telegram'].

Before this change, the list was written to jobs.json verbatim, and
the scheduler's str(deliver).split(',') then tried to resolve the
literal string "['telegram']" as a platform — returning None and
logging 'no delivery target resolved for deliver=[\'telegram\']'.

Fix on both ends:
- tools/cronjob_tools.py: normalize deliver at the API boundary on
  create and update, so storage is always a string.
- cron/scheduler.py: normalize deliver in _resolve_delivery_targets,
  so existing jobs.json entries with list-form deliver are handled
  gracefully without requiring users to edit the file.

Closes #17139
This commit is contained in:
Teknium
2026-04-29 06:35:34 -07:00
committed by GitHub
parent 7141cda967
commit 398945e7b1
4 changed files with 140 additions and 4 deletions

View File

@@ -150,6 +150,27 @@ def _normalize_optional_job_value(value: Optional[Any], *, strip_trailing_slash:
return text or None
def _normalize_deliver_param(value: Any) -> Optional[str]:
"""Normalize a user-supplied ``deliver`` value to the canonical string form.
The cron schema documents ``deliver`` as a string (``"local"``, ``"origin"``,
``"telegram"``, ``"telegram:chat_id[:thread_id]"``, or comma-separated combos).
Some callers — MCP clients passing arrays, scripts building the payload as a
list — supply ``["telegram"]``. ``create_job``/``update_job`` store it as-is,
and the scheduler's ``str(deliver).split(",")`` then serializes the list to
the literal ``"['telegram']"`` which is not a known platform. Flatten lists
/ tuples at the API boundary so storage is always a string. Returns ``None``
for ``None``/empty so callers can treat it as "not supplied".
"""
if value is None:
return None
if isinstance(value, (list, tuple)):
parts = [str(p).strip() for p in value if str(p).strip()]
return ",".join(parts) if parts else None
text = str(value).strip()
return text or None
def _validate_cron_script_path(script: Optional[str]) -> Optional[str]:
"""Validate a cron job script path at the API boundary.
@@ -283,7 +304,7 @@ def cronjob(
schedule=schedule,
name=name,
repeat=repeat,
deliver=deliver,
deliver=_normalize_deliver_param(deliver),
origin=_origin_from_env(),
skills=canonical_skills,
model=_normalize_optional_job_value(model),
@@ -364,7 +385,7 @@ def cronjob(
if name is not None:
updates["name"] = name
if deliver is not None:
updates["deliver"] = deliver
updates["deliver"] = _normalize_deliver_param(deliver)
if skills is not None or skill is not None:
canonical_skills = _canonical_skills(skill, skills)
updates["skills"] = canonical_skills