{"repo":"NousResearch/hermes-agent","public_id":"2234","display_label":"Placeholder text leaking to users and cron targets","summary":"Multiple bug reports and fix attempts address a problem where internal fallback strings like '(No response generated)' or '(empty)' are delivered to end users and cron job targets instead of being suppressed. The cluster covers both the detection of empty/think-only LLM responses and the separation of logging placeholders from actual delivery responses.","subsystem":"agent","counts":{"members":13,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":4,"merged_prs":2,"attempt_prs":7},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":2442},"resolution":{"state":"partially_resolved","by":"pr-2442,pr-11021"},"freshness":{"state":"stale","last_verified_at":"2026-04-16T13:10:40+00:00","is_stale":true},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.12923076923076923},"fix_edges":[{"issue_number":2234,"pr_number":2442,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":9270,"pr_number":11021,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true}],"members":[{"kind":"issue","number":2234,"title":"bug(cron): silent cron jobs deliver \"(No response generated)\" to Discord","url":null,"state":"closed"},{"kind":"issue","number":6559,"title":"[Bug] Ollama concurrency limit causes literal '(empty)' responses on Telegram","url":null,"state":"closed"},{"kind":"issue","number":9270,"title":"Empty LLM responses get replaced with literal \"(No response generated)\" and delivered to users, including cron targets","url":null,"state":"closed"},{"kind":"issue","number":11539,"title":"bug: literal (empty) string sent to users when model returns no content","url":null,"state":"closed"},{"kind":"pr","number":2238,"title":"fix: separate cron log placeholder from delivery response","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#1741","number":1741}],"category":"uncategorized","implication":"needs_llm"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":2241,"title":"fix(cron): separate log placeholder from delivery response for silent cron jobs","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#2442","number":2442}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":2255,"title":"fix(cron): treat empty agent response as silent instead of delivering fallback text","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#2442","number":2442}],"category":"uncategorized","implication":"needs_llm"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":2280,"title":"fix(cron): silent jobs return empty response for delivery skip","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#2442","number":2442}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":2407,"title":"fix: separate cron log placeholder from delivery response (salvage #2238)","url":null,"state":"closed","close_reason":{"category":"silent_close","implication":"active"},"merged":false},{"kind":"pr","number":2442,"title":"fix(cron): silent jobs return empty response for delivery skip","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"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":["@@ -412,9 +412,10 @@ def run_job(job: dict) -> tuple[bool, str, str, Optional[str]]:\n         \n         result = agent.run_conversation(prompt)\n         \n-        final_response = result.get(\"final_response\", \"\")\n-        if not final_response:\n-            final_response = \"(No response generated)\"\n+        final_response = result.get(\"final_response\", \"\") or \"\"\n+        # Use a separate variable for log display; keep final_response clean\n+        # for delivery logic (empty response = no delivery).\n+        logged_response = final_response if final_response else \"(No response generated)\"\n         \n         output = f\"\"\"# Cron Job: {job_name}\n ","@@ -428,7 +429,7 @@ def run_job(job: dict) -> tuple[bool, str, str, Optional[str]]:\n \n ## Response\n \n-{final_response}\n+{logged_response}\n \"\"\"\n         \n         logger.info(\"Job '%s' completed successfully\", job_name)"]},{"file":"tests/cron/test_scheduler.py","hunks":["@@ -234,6 +234,47 @@ def test_run_job_passes_session_db_and_cron_platform(self, tmp_path):\n         assert kwargs[\"session_id\"].startswith(\"cron_test-job_\")\n         fake_db.close.assert_called_once()\n \n+    def test_run_job_empty_response_returns_empty_not_placeholder(self, tmp_path):\n+        \"\"\"Empty final_response should stay empty for delivery logic (issue #2234).\n+        \n+        The placeholder '(No response generated)' should only appear in the\n+        output log, not in the returned final_response that's used for delivery.\n+        \"\"\"\n+        job = {\n+            \"id\": \"silent-job\",\n+            \"name\": \"silent test\",\n+            \"prompt\": \"do work via tools only\",\n+        }\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(\n+                 \"hermes_cli.runtime_provider.resolve_runtime_provider\",\n+                 return_value={\n+                     \"api_key\": \"test-key\",\n+                     \"base_url\": \"https://example.invalid/v1\",\n+                     \"provider\": \"openrouter\",\n+                     \"api_mode\": \"chat_completions\",\n+                 },\n+             ), \\\n+             patch(\"run_agent.AIAgent\") as mock_agent_cls:\n+            mock_agent = MagicMock()\n+            # Agent did work via tools but returned no text\n+            mock_agent.run_conversation.return_value = {\"final_response\": \"\"}\n+            mock_agent_cls.return_value = mock_agent\n+\n+            success, output, final_response, error = run_job(job)\n+\n+        assert success is True\n+        assert error is None\n+        # final_response should be empty for delivery logic to skip\n+        assert final_response == \"\"\n+        # But the output log should show the placeholder\n+        assert \"(No response generated)\" in output\n+\n     def test_run_job_sets_auto_delivery_env_from_dotenv_home_channel(self, tmp_path, monkeypatch):\n         job = {\n             \"id\": \"test-job\",\n"]}]},{"kind":"pr","number":9292,"title":"fix: stop leaking placeholder text to users and cron targets","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#11021","number":11021}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":11021,"title":"fix: empty response nudge crash + placeholder leak to cron targets","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["cron/scheduler.py","gateway/run.py","run_agent.py","scripts/release.py"],"fix_hunks":[{"file":"cron/scheduler.py","hunks":["@@ -837,6 +837,9 @@ def run_job(job: dict) -> tuple[bool, str, str, Optional[str]]:\n             )\n \n         final_response = result.get(\"final_response\", \"\") or \"\"\n+        # Strip leaked placeholder text that upstream may inject on empty completions.\n+        if final_response.strip() == \"(No response generated)\":\n+            final_response = \"\"\n         # Use a separate variable for log display; keep final_response clean\n         # for delivery logic (empty response = no delivery).\n         logged_response = final_response if final_response else \"(No response generated)\""]},{"file":"gateway/run.py","hunks":["@@ -8886,7 +8886,7 @@ def _approval_notify_sync(approval_data: dict) -> None:\n             _resolved_model = getattr(_agent, \"model\", None) if _agent else None\n \n             if not final_response:\n-                error_msg = f\"⚠️ {result['error']}\" if result.get(\"error\") else \"(No response generated)\"\n+                error_msg = f\"⚠️ {result['error']}\" if result.get(\"error\") else \"\"\n                 return {\n                     \"final_response\": error_msg,\n                     \"messages\": result.get(\"messages\", []),"]},{"file":"run_agent.py","hunks":["@@ -10945,8 +10945,9 @@ def _stop_spinner():\n                             #   tool(result) → assistant(\"(empty)\") → user(nudge)\n                             # Without this, we'd have tool → user which most\n                             # APIs reject as an invalid sequence.\n-                            assistant_msg[\"content\"] = \"(empty)\"\n-                            messages.append(assistant_msg)\n+                            _nudge_msg = self._build_assistant_message(assistant_message, finish_reason)\n+                            _nudge_msg[\"content\"] = \"(empty)\"\n+                            messages.append(_nudge_msg)\n                             messages.append({\n                                 \"role\": \"user\",\n                                 \"content\": ("]},{"file":"scripts/release.py","hunks":["@@ -179,6 +179,7 @@\n     \"[redacted:email]\": \"limars874\",\n     \"[redacted:email]\": \"lesterli\",\n     \"[redacted:email]\": \"MagicRay1217\",\n+    \"[redacted:email]\": \"GenKoKo\",\n     \"[redacted:email]\": \"spniyant\",\n     \"[redacted:email]\": \"olafthiele\",\n     \"[redacted:email]\": \"sprmn24\",\n"]}]},{"kind":"pr","number":11563,"title":"bug: literal (empty) string sent to users when model returns no content","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#11539","number":11539}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}