Skip to content
Merged
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
7 changes: 5 additions & 2 deletions cron/scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -1842,7 +1842,10 @@ def _process_job(job: dict) -> bool:
# If the agent responded with [SILENT], skip delivery (but
# output is already saved above). Failed jobs always deliver.
deliver_content = final_response if success else f"⚠️ Cron job '{job.get('name', job['id'])}' failed:\n{error}"
should_deliver = bool(deliver_content)
# Treat whitespace-only final responses the same as empty
# responses: do not deliver a blank message, and let the
# empty-response guard below mark the run as a soft failure.
should_deliver = bool(deliver_content.strip())
if should_deliver and success and SILENT_MARKER in deliver_content.strip().upper():
logger.info("Job '%s': agent returned %s — skipping delivery", job["id"], SILENT_MARKER)
should_deliver = False
Expand All @@ -1858,7 +1861,7 @@ def _process_job(job: dict) -> bool:
# Treat empty final_response as a soft failure so last_status
# is not "ok" — the agent ran but produced nothing useful.
# (issue #8585)
if success and not final_response:
if success and not final_response.strip():
success = False
error = "Agent completed but produced empty response (model error, timeout, or misconfiguration)"

Expand Down
18 changes: 18 additions & 0 deletions tests/cron/test_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -1773,6 +1773,24 @@ def test_output_saved_even_when_delivery_suppressed(self):
save_mock.assert_called_once_with("monitor-job", "# full output")
deliver_mock.assert_not_called()

def test_whitespace_only_response_is_marked_failed_not_delivered(self):
"""Whitespace-only final responses should behave like empty responses."""
with patch("cron.scheduler.get_due_jobs", return_value=[self._make_job()]), \
patch("cron.scheduler.run_job", return_value=(True, "# output", " \n\t ", None)), \
patch("cron.scheduler.save_job_output", return_value="/tmp/out.md"), \
patch("cron.scheduler._deliver_result") as deliver_mock, \
patch("cron.scheduler.mark_job_run") as mark_mock:
from cron.scheduler import tick
tick(verbose=False)

deliver_mock.assert_not_called()
mark_mock.assert_called_once_with(
"monitor-job",
False,
"Agent completed but produced empty response (model error, timeout, or misconfiguration)",
delivery_error=None,
)


class TestBuildJobPromptSilentHint:
"""Verify _build_job_prompt always injects [SILENT] guidance."""
Expand Down
Loading