diff --git a/cron/scheduler.py b/cron/scheduler.py index c6941c763d8..be9bab41215 100644 --- a/cron/scheduler.py +++ b/cron/scheduler.py @@ -1842,7 +1842,10 @@ def tick(verbose: bool = True, adapters=None, loop=None) -> int: # 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 @@ -1858,7 +1861,7 @@ def tick(verbose: bool = True, adapters=None, loop=None) -> int: # 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)" diff --git a/tests/cron/test_scheduler.py b/tests/cron/test_scheduler.py index e0cb1cc155e..bbb0343088e 100644 --- a/tests/cron/test_scheduler.py +++ b/tests/cron/test_scheduler.py @@ -1773,6 +1773,24 @@ class TestSilentDelivery: 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."""