Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions cron/scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,24 +108,30 @@ def _summarize_cron_failure_for_delivery(job: dict, error: str | None) -> str:
job_name = job.get("name") or job.get("id") or "cron job"
text = (error or "unknown error").strip()
lower = text.lower()
fallback_note = ""
if not job.get("no_agent"):
fallback_note = " Fallback chain was exhausted or unavailable."

# Provider/API failures are the common noisy path. Keep these short.
if "429" in text or "rate limit" in lower or "usage limit" in lower:
if re.search(r"\b429\b", text) or "rate limit" in lower or "usage limit" in lower:
reason = "rate limit"
if "weekly usage limit" in lower:
reason = "weekly usage limit"
elif "quota" in lower:
reason = "quota limit"
return (
f"⚠️ Cron '{job_name}' failed: provider {reason}. "
"Fallback chain was exhausted or unavailable. "
f"⚠️ Cron '{job_name}' failed: provider {reason}."
f"{fallback_note} "
"Full details saved in cron output."
)

if "readtimeout" in lower or "timed out" in lower or "timeout" in lower:
timeout_source = "provider"
if job.get("no_agent") and "script timed out" in lower:
timeout_source = "script"
return (
f"⚠️ Cron '{job_name}' failed: provider timeout. "
"Fallback chain was exhausted or unavailable. "
f"⚠️ Cron '{job_name}' failed: {timeout_source} timeout."
f"{fallback_note} "
"Full details saved in cron output."
)

Expand Down
53 changes: 51 additions & 2 deletions tests/cron/test_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,61 @@

import pytest

from cron.scheduler import _resolve_origin, _resolve_delivery_target, _deliver_result, _send_media_via_adapter, run_job, SILENT_MARKER, _build_job_prompt, _resolve_cron_enabled_toolsets, _merge_mcp_into_per_job_toolsets
from cron.scheduler import (
SILENT_MARKER,
_build_job_prompt,
_deliver_result,
_merge_mcp_into_per_job_toolsets,
_resolve_cron_enabled_toolsets,
_resolve_delivery_target,
_resolve_origin,
_send_media_via_adapter,
_summarize_cron_failure_for_delivery,
run_job,
)
from tools.env_passthrough import clear_env_passthrough
from tools.credential_files import clear_credential_files


class TestSummarizeCronFailureForDelivery:
def test_embedded_429_in_source_identifier_is_not_a_rate_limit(self):
summary = _summarize_cron_failure_for_delivery(
{"name": "LLM Wiki Incremental Index", "no_agent": True},
"Script failed: path/hash429abc.md source snapshot failure",
)

assert "provider rate limit" not in summary
assert "hash429abc.md" in summary

def test_http_429_is_still_classified_as_a_rate_limit(self):
summary = _summarize_cron_failure_for_delivery(
{"name": "provider-backed job"},
"HTTP 429: Too Many Requests",
)

assert "provider rate limit" in summary
assert "Fallback chain was exhausted or unavailable" in summary

def test_no_agent_rate_limit_does_not_claim_a_fallback_chain(self):
summary = _summarize_cron_failure_for_delivery(
{"name": "script job", "no_agent": True},
"HTTP 429: Too Many Requests",
)

assert "provider rate limit" in summary
assert "Fallback chain" not in summary

def test_no_agent_timeout_is_identified_as_a_script_timeout(self):
summary = _summarize_cron_failure_for_delivery(
{"name": "script job", "no_agent": True},
"Script timed out after 3600s",
)

assert "script timeout" in summary
assert "provider timeout" not in summary
assert "Fallback chain" not in summary


class TestPerJobToolsetMcpMerge:
"""A per-job enabled_toolsets allowlist must not silently drop MCP servers."""

Expand Down Expand Up @@ -1975,4 +2025,3 @@ def test_dedupes_on_duplicate_title(self):
assert out == "Nightly Synthesis #2"
db.get_next_title_in_lineage.assert_called_once_with("Nightly Synthesis")