{"repo":"NousResearch/hermes-agent","public_id":"36734","display_label":"Cron jobs failing due to undefined get_fallback_chain","summary":"All cron jobs were failing with a NameError because get_fallback_chain was not accessible in the job runner. Multiple fix attempts address wiring this function into the cron job runner correctly.","subsystem":"cron","counts":{"members":4,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":2},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":56166,"merged_at":"2026-07-01T08:23:21+00:00"},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"verified","last_verified_at":"2026-07-01T08:23:21+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.5},"fix_edges":[{"issue_number":36734,"pr_number":56166,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-07-01T08:23:21+00:00"}],"members":[{"kind":"issue","number":36734,"title":"[CRON] All cron jobs fail: NameError: get_fallback_chain is not defined","url":null,"state":"closed"},{"kind":"pr","number":36809,"title":"fix(cron): wire get_fallback_chain into job runner (#36734)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"32bc36522","number":0},{"kind":"pr","value":"#56166","number":56166},{"kind":"pr","value":"#36734","number":36734}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":38718,"title":"fix(cron): NameError get_fallback_chain + silent ticker death + tick() blocks (#36734 #36854 #37312)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#37312","number":37312},{"kind":"pr","value":"#39008","number":39008},{"kind":"pr","value":"#36734","number":36734},{"kind":"pr","value":"#36854","number":36854}],"category":"partial_fix","implication":"needs_llm"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":56166,"title":"fix(cron): use shared get_fallback_chain in job runner (#36734)","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["cron/scheduler.py","tests/cron/test_scheduler.py"],"fix_hunks":[{"file":"cron/scheduler.py","hunks":["@@ -41,6 +41,7 @@\n from hermes_constants import get_hermes_home\n from hermes_cli._subprocess_compat import windows_hide_flags\n from hermes_cli.config import load_config, _expand_env_vars\n+from hermes_cli.fallback_config import get_fallback_chain\n from hermes_time import now as _hermes_now\n \n logger = logging.getLogger(__name__)","@@ -2370,12 +2371,9 @@ def run_job(job: dict) -> tuple[bool, str, str, Optional[str]]:\n         except AuthError as auth_exc:\n             # Primary provider auth failed — try fallback chain before giving up.\n             logger.warning(\"Job '%s': primary auth failed (%s), trying fallback\", job_id, auth_exc)\n-            fb = _cfg.get(\"fallback_providers\") or _cfg.get(\"fallback_model\")\n-            fb_list = (fb if isinstance(fb, list) else [fb]) if fb else []\n+            fb_list = get_fallback_chain(_cfg)\n             runtime = None\n             for entry in fb_list:\n-                if not isinstance(entry, dict):\n-                    continue\n                 try:\n                     fb_kwargs = {\"requested\": entry.get(\"provider\")}\n                     if entry.get(\"base_url\"):","@@ -2447,7 +2445,7 @@ def run_job(job: dict) -> tuple[bool, str, str, Optional[str]]:\n                 f\"(or pin the original values to keep them). See #44585.\"\n             )\n \n-        fallback_model = _cfg.get(\"fallback_providers\") or _cfg.get(\"fallback_model\") or None\n+        fallback_model = get_fallback_chain(_cfg) or None\n         credential_pool = None\n         runtime_provider = str(runtime.get(\"provider\") or \"\").strip().lower()\n         if runtime_provider:"]},{"file":"tests/cron/test_scheduler.py","hunks":["@@ -1926,6 +1926,36 @@ def test_fallback_model_env_ref_in_config_yaml_is_expanded(self, tmp_path, monke\n             \"config.yaml ${VAR} in fallback_providers was not expanded.\"\n         )\n \n+    def test_fallback_chain_merges_providers_and_legacy_model(self, tmp_path, monkeypatch):\n+        \"\"\"Cron uses get_fallback_chain so legacy fallback_model is not dropped.\"\"\"\n+        (tmp_path / \"config.yaml\").write_text(\n+            \"fallback_providers:\\n\"\n+            \"  - provider: openrouter\\n\"\n+            \"    model: gpt-4o-mini\\n\"\n+            \"fallback_model:\\n\"\n+            \"  provider: anthropic\\n\"\n+            \"  model: claude-sonnet-4-6\\n\"\n+        )\n+\n+        job = {\"id\": \"fb-merge\", \"name\": \"fallback merge\", \"prompt\": \"hi\"}\n+        fake_db = MagicMock()\n+\n+        with patch(\"cron.scheduler._hermes_home\", tmp_path), \\\n+             patch(\"cron.scheduler._resolve_origin\", return_value=None), \\\n+             patch(\"dotenv.load_dotenv\"), \\\n+             patch(\"hermes_state.SessionDB\", return_value=fake_db), \\\n+             patch(\"hermes_cli.runtime_provider.resolve_runtime_provider\",\n+                   return_value=self._RUNTIME), \\\n+             patch(\"run_agent.AIAgent\") as mock_agent_cls:\n+            mock_agent = MagicMock()\n+            mock_agent.run_conversation.return_value = {\"final_response\": \"ok\"}\n+            mock_agent_cls.return_value = mock_agent\n+            run_job(job)\n+\n+        fb = mock_agent_cls.call_args.kwargs.get(\"fallback_model\") or []\n+        models = [e.get(\"model\") for e in fb if isinstance(e, dict)]\n+        assert models == [\"gpt-4o-mini\", \"claude-sonnet-4-6\"]\n+\n     def test_unexpanded_ref_passthrough_when_var_unset(self, tmp_path, monkeypatch):\n         \"\"\"When the env var is not set, the literal ${VAR} is kept verbatim (not crashed).\"\"\"\n         (tmp_path / \"config.yaml\").write_text(\"model: ${_HERMES_TEST_CRON_UNSET_VAR}\\n\")\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}