Nothing open on GitHub. Fix merged in #17456 on 29 Apr 2026. Check it covers your case.
The short answer
0open issues
0open PRs
3items grouped
- Known?
- Yes. 3 related items grouped here, 1 issue already closed.
- Fix event
- A fix was merged to main in PR #17456. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
resolved, deciding PR #17456. That is separate from the fix event. - Still open
- Nothing open on GitHub.
Auto-generated cluster label
Open issues0
Open PRs0
Fixes on record
-
#17456 fix(cron): accept list-form deliver so deliver=['telegram'] works
files:
cron/scheduler.py,tests/cron/test_scheduler.py,tests/tools/test_cronjob_tools.py,tools/cronjob_tools.pyShow the diff
-
cron/scheduler.py@@ -233,12 +233,32 @@ def _resolve_single_delivery_target(job: dict, deliver_value: str) -> Optional[d } +def _normalize_deliver_value(deliver) -> str: + """Normalize a stored/submitted ``deliver`` value to its canonical string form. + + The contract is that ``deliver`` is a string (``"local"``, ``"origin"``, + ``"telegram"``, ``"telegram:-1001:17"``, or comma-separated combinations). + Historically some callers — MCP clients passing an array, direct edits of + ``jobs.json``, or stale code paths — have stored a list/tuple like + ``["telegram"]``. ``str(["telegram"])`` would serialize to the literal + string ``"['telegram']"``, which is not a known platform and fails + resolution silently. Flatten lists/tuples into a comma-separated string + so both forms work. Returns ``"local"`` for anything falsy. + """ + if deliver is None or deliver == "": + return "local" + if isinstance(deliver, (list, tuple)): + parts = [str(p).strip() for p in deliver if str(p).strip()] + return ",".join(parts) if parts else "local" + return str(deliver) + + def _resolve_delivery_targets(job: dict) -> List[dict]: """Resolve all concrete auto-delivery targets for a cron job (supports comma-separated deliver).""" - deliver = job.get("deliver", "local") + deliver = _normalize_deliver_value(job.get("deliver", "local")) if deliver == "local": return [] - parts = [p.strip() for p in str(deliver).split(",") if p.strip()] + parts = [p.strip() for p in deliver.split(",") if p.strip()] seen = set() targets = [] for part in parts: -
tests/cron/test_scheduler.py@@ -279,6 +279,44 @@ def test_explicit_discord_channel_without_thread(self): "thread_id": None, } + def test_list_form_deliver_is_normalized(self, monkeypatch): + """deliver=['telegram'] (Python list) should resolve like 'telegram' string. + + Regression test for #17139: MCP clients / scripts that pass the deliver + field as an array-shaped value used to fail with "no delivery target + resolved for deliver=['telegram']" because ``str(['telegram'])`` was + passed through to ``split(',')`` verbatim. + """ + monkeypatch.setenv("TELEGRAM_HOME_CHANNEL", "-4004") + job = { + "deliver": ["telegram"], + "origin": None, + } + + assert _resolve_delivery_target(job) == { + "platform": "telegram", + "chat_id": "-4004", + "thread_id": None, + } + + def test_list_form_multiple_platforms_normalized(self, monkeypatch): + """deliver=['telegram', 'discord'] resolves to multiple targets.""" + from cron.scheduler import _resolve_delivery_targets + + monkeypatch.setenv("TELEGRAM_HOME_CHANNEL", "-111") + monkeypatch.setenv("DISCORD_HOME_CHANNEL", "-222") + job = {"deliver": ["telegram", "discord"], "origin": None} + + targets = _resolve_delivery_targets(job) + platforms = sorted(t["platform"] for t in targets) + assert platforms == ["discord", "telegram"] + + def test_empty_list_form_deliver_resolves_to_local(self): + """deliver=[] is treated as local (no delivery).""" + from cron.scheduler import _resolve_delivery_targets + + assert _resolve_delivery_targets({"deliver": []}) == [] + class TestDeliverResultWrapping: """Verify that cron deliveries are wrapped with header/footer and no longer mirrored.""" -
tests/tools/test_cronjob_tools.py@@ -231,3 +231,60 @@ def test_update_can_clear_skills(self): assert updated["success"] is True assert updated["job"]["skills"] == [] assert updated["job"]["skill"] is None + + def test_create_normalizes_list_form_deliver(self): + """deliver=['telegram'] (list) is stored as the string 'telegram'. + + Regression for #17139: MCP clients / scripts sometimes pass ``deliver`` + as an array. Prior to the fix, ``['telegram']`` was written verbatim + to ``jobs.json`` and the scheduler then tried to resolve the literal + string ``"['telegram']"`` as a platform, failing with + "no delivery target resolved". + """ + from cron.jobs import get_job + + created = json.loads( + cronjob( + action="create", + prompt="Daily briefing", + schedule="every 1h", + deliver=["telegram"], + ) + ) + assert created["success"] is True + stored = get_job(created["job_id"]) + assert stored["deliver"] == "telegram" + + def test_create_normalizes_multi_element_list_deliver(self): + """deliver=['telegram', 'discord'] is stored as 'telegram,discord'.""" + from cron.jobs import get_job + + created = json.loads( + cronjob( + action="create", + prompt="Daily briefing", + schedule="every 1h", + deliver=["telegram", "discord"], + ) + ) + assert created["success"] is True + stored = get_job(created["job_id"]) + assert stored["deliver"] == "telegram,discord" + + def test_update_normalizes_list_form_deliver(self): + """update with deliver=['telegram'] stores the canonical string.""" + from cron.jobs import get_job + + created = json.loads( + cronjob(action="create", prompt="x", schedule="every 1h") + ) + updated = json.loads( + cronjob( + action="update", + job_id=created["job_id"], + deliver=["telegram"], + ) + ) + assert updated["success"] is True + stored = get_job(created["job_id"]) + assert stored["deliver"] == "telegram" -
tools/cronjob_tools.py@@ -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
-
1 PR closed without merging
From the close comments: other 1
-
#17222 fix(cron): accept list-shaped deliver in delivery target resolver (#17139) · other →
#17139,#17456
Closed issues · 1
- Closed#17139Cron job deliver=['telegram'] fails: no delivery target resolved
How this cluster was grouped
-
Link evidence (
structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference. -
Pairwise link density
0.67: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the LabAt a glance
- Merged PRs
- #17456
- Closed issues
- 1
- Attempts
- 1 PR closed without merging
- Fix link checked
- 29 Apr 2026. This confirmation is old and may be out of date (a revert or reopen would not have invalidated it here); re-verify.