Cron jobs misreporting agent API failures as successful
Cluster 17855. Data as of
Nothing open on GitHub. Fix merged in #17859 on 30 Apr 2026. Check it covers your case.
The short answer
- Known?
- Yes. 3 related items grouped here, 1 issue already closed.
- Fix event
- A fix was merged to main in PR #17859. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
resolved, deciding PR #17859. That is separate from the fix event. - Still open
- Nothing open on GitHub.
One issue reports that a cron job's API failure was recorded as last_status=ok and that no error notification was delivered. Two related pull requests propose changes so agent failures, including run_conversation failure flags, are reported as job failures and error notifications are not hidden.
Open issues0
Open PRs0
Fixes on record
-
#17859 fix(cron): surface agent run_conversation failure flags as job failure
files:
cron/scheduler.py,tests/cron/test_scheduler.pyShow the diff
-
cron/scheduler.py@@ -1150,6 +1150,21 @@ def run_job(job: dict) -> tuple[bool, str, str, Optional[str]]: f"agent.run_conversation returned {type(result).__name__} instead of dict: {result!r}" ) + # If the agent itself reported failure (e.g. all retries exhausted on + # API errors, model abort, mid-run interrupt), do not silently mark the + # job as successful. run_agent populates `failed=True`/`completed=False` + # on these paths and may put the error into `final_response`, which + # would otherwise be delivered as if it were the agent's reply and the + # job's `last_status` set to "ok". Raise so the except handler below + # builds the proper failure tuple. (issue #17855) + if result.get("failed") is True or result.get("completed") is False: + _err_text = ( + result.get("error") + or (result.get("final_response") or "").strip() + or "agent reported failure" + ) + raise RuntimeError(_err_text) + final_response = result.get("final_response", "") or "" # Strip leaked placeholder text that upstream may inject on empty completions. if final_response.strip() == "(No response generated)": -
tests/cron/test_scheduler.py@@ -935,6 +935,120 @@ def test_run_job_empty_response_returns_empty_not_placeholder(self, tmp_path): # But the output log should show the placeholder assert "(No response generated)" in output + @pytest.mark.parametrize( + "agent_result,expected_err_substring", + [ + ( + { + "final_response": "API call failed after 3 retries: Request timed out.", + "failed": True, + "completed": False, + "error": "API call failed after 3 retries: Request timed out.", + }, + "API call failed", + ), + ( + {"final_response": None, "completed": False, "failed": True}, + "agent reported failure", + ), + ( + {"final_response": "", "completed": False}, + "agent reported failure", + ), + ( + { + "final_response": "partial reply before crash", + "failed": True, + "completed": False, + "error": "model abort: connection reset", + }, + "model abort", + ), + ], + ) + def test_run_job_treats_agent_failure_flag_as_failure( + self, tmp_path, agent_result, expected_err_substring + ): + """Issue #17855: run_conversation returns ``failed=True``/``completed=False`` + when the agent's API call exhausts retries or aborts mid-run. run_job + must surface this as success=False so cron's last_status reflects the + failure and the user gets an error notification, instead of treating + the (often non-empty) error string in final_response as a legitimate + agent reply. + """ + job = { + "id": "failing-api-job", + "name": "failing api", + "prompt": "do something", + } + fake_db = MagicMock() + + with patch("cron.scheduler._hermes_home", tmp_path), \ + patch("cron.scheduler._resolve_origin", return_value=None), \ + patch("dotenv.load_dotenv"), \ + patch("hermes_state.SessionDB", return_value=fake_db), \ + patch( + "hermes_cli.runtime_provider.resolve_runtime_provider", + return_value={ + "api_key": "***", + "base_url": "https://example.invalid/v1", + "provider": "openrouter", + "api_mode": "chat_completions", + }, + ), \ + patch("run_agent.AIAgent") as mock_agent_cls: + mock_agent = MagicMock() + mock_agent.run_conversation.return_value = agent_result + mock_agent_cls.return_value = mock_agent + + success, output, final_response, error = run_job(job) + + assert success is False + assert final_response == "" + assert error is not None and expected_err_substring in error + # Output should be the FAILED template, not the success template. + assert "(FAILED)" in output + # Ephemeral cron agent must still be closed even on agent-flagged failure. + mock_agent.close.assert_called_once() + + def test_run_job_completed_true_without_failed_flag_succeeds(self, tmp_path): + """Regression guard: a normal success result (``completed=True``, + ``failed`` absent) must not trip the failure-flag check. + """ + job = { + "id": "ok-job", + "name": "ok", + "prompt": "hello", + } + fake_db = MagicMock() + + with patch("cron.scheduler._hermes_home", tmp_path), \ + patch("cron.scheduler._resolve_origin", return_value=None), \ + patch("dotenv.load_dotenv"), \ + patch("hermes_state.SessionDB", return_value=fake_db), \ + patch( + "hermes_cli.runtime_provider.resolve_runtime_provider", + return_value={ + "api_key": "***", + "base_url": "https://example.invalid/v1", + "provider": "openrouter", + "api_mode": "chat_completions", + }, + ), \ + patch("run_agent.AIAgent") as mock_agent_cls: + mock_agent = MagicMock() + mock_agent.run_conversation.return_value = { + "final_response": "all good", + "completed": True, + } + mock_agent_cls.return_value = mock_agent + + success, output, final_response, error = run_job(job) + + assert success is True + assert error is None + assert final_response == "all good" + def test_tick_marks_empty_response_as_error(self, tmp_path): """When run_job returns success=True but final_response is empty, tick() should mark the job as error so last_status != 'ok'.
-
1 PR closed without merging
From the close comments: other 1
-
#17882 fix(cron): report agent API failure correctly, never hide error notif… · other →
#17859,#17855
Closed issues · 1
- Closed#17855Cron: API failure incorrectly reported as last_status=ok, no error notification delivered
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
- #17859
- Closed issues
- 1
- Attempts
- 1 PR closed without merging
- Fix link checked
- 30 Apr 2026. This confirmation is old and may be out of date (a revert or reopen would not have invalidated it here); re-verify.