fix(cron): deliver max-iteration fallback reports

This commit is contained in:
helix4u 2026-06-22 12:49:23 -06:00 committed by Teknium
parent 3972701424
commit ae7e857420
2 changed files with 62 additions and 2 deletions

View file

@ -2189,13 +2189,27 @@ def run_job(job: dict) -> tuple[bool, str, str, Optional[str]]:
# 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:
turn_exit_reason = str(result.get("turn_exit_reason") or "")
final_response_text = (result.get("final_response") or "").strip()
max_iteration_summary = (
result.get("failed") is not True
and result.get("completed") is False
and turn_exit_reason.startswith("max_iterations_reached(")
and bool(final_response_text)
)
if result.get("failed") is True or (result.get("completed") is False and not max_iteration_summary):
_err_text = (
result.get("error")
or (result.get("final_response") or "").strip()
or final_response_text
or "agent reported failure"
)
raise RuntimeError(_err_text)
if max_iteration_summary:
logger.warning(
"Job '%s' reached the iteration limit but produced a final fallback response; "
"delivering the response instead of failing the cron run",
job_name,
)
final_response = result.get("final_response", "") or ""
# Strip leaked placeholder text that upstream may inject on empty completions.

View file

@ -1394,6 +1394,52 @@ class TestRunJobSessionPersistence:
assert error is None
assert final_response == "all good"
def test_run_job_delivers_max_iteration_fallback_summary(self, tmp_path):
"""Cron should deliver a usable max-iteration fallback summary.
A cron run can exhaust the iteration budget, get a final text summary
from the no-tools fallback call, and still have ``completed=False`` in
the generic agent result. That should not make cron raise the report
text as a RuntimeError.
"""
job = {
"id": "summary-job",
"name": "summary",
"prompt": "finish the report",
}
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": "final fallback report",
"completed": False,
"failed": False,
"turn_exit_reason": "max_iterations_reached(60/60)",
}
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 == "final fallback report"
assert "final fallback report" in output
assert "(FAILED)" not in output
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'.