{"repo":"NousResearch/hermes-agent","public_id":"18529","display_label":"Title generator leaking think/reasoning blocks from LLM output","summary":"Multiple fixes address the title generator reading raw LLM output that includes <think> or <reasoning> blocks instead of using the existing extract_content_or_reasoning() helper. One member also adds multi-message context, noise filtering, retry logic, and provider override to title generation.","subsystem":"agent","counts":{"members":9,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":7},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":56298},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"verified","last_verified_at":"2026-07-01T11:18:48+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.2222222222222222},"fix_edges":[{"issue_number":18529,"pr_number":56298,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false}],"members":[{"kind":"issue","number":18529,"title":"[Bug]: title_generator.py read  response.choices[0].message.content directly instead of use extract_content_or_reasoning() to extract  <thinking> tag","url":null,"state":"closed"},{"kind":"pr","number":18537,"title":"Fix/title generation thinking leak","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"3b739b990b3f624a6cbd823c6b91bf27d83a694a","number":0},{"kind":"pr","value":"#18529","number":18529}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":18643,"title":"fix: use extract_content_or_reasoning() for title generation with reasoning models","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#17797","number":17797},{"kind":"pr","value":"#20266","number":20266}],"category":"uncategorized","implication":"needs_llm"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":19008,"title":"fix(title): strip thinking blocks from auto-generated session titles","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"3b739b990b3f624a6cbd823c6b91bf27d83a694a","number":0},{"kind":"pr","value":"#44126","number":44126},{"kind":"pr","value":"#56298","number":56298}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":20266,"title":"fix: support reasoning models in title generation","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"3b739b990","number":0},{"kind":"pr","value":"#18529","number":18529}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":22568,"title":"fix(title-generator): strip <think>/<reasoning> blocks before saving session title","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#19008","number":19008},{"kind":"pr","value":"#18529","number":18529}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":26676,"title":"feat(title-generator): multi-message context, noise filtering, retry, explicit provider override","url":null,"state":"closed","close_reason":{"category":"silent_close","implication":"active"},"merged":false},{"kind":"pr","number":27763,"title":"fix(title-gen): use extract_content_or_reasoning for reasoning models","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#18643","number":18643},{"kind":"pr","value":"#18643","number":18643},{"kind":"pr","value":"#20266","number":20266},{"kind":"pr","value":"#20754","number":20754},{"kind":"pr","value":"#18529","number":18529},{"kind":"pr","value":"#18643","number":18643}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":56298,"title":"fix(title_generator): strip think blocks from LLM output before extracting title","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["agent/title_generator.py","scripts/release.py","tests/agent/test_title_generator.py"],"fix_hunks":[{"file":"agent/title_generator.py","hunks":["@@ -87,7 +87,15 @@ def generate_title(\n             timeout=timeout,\n             main_runtime=main_runtime,\n         )\n-        title = (response.choices[0].message.content or \"\").strip()\n+        content = response.choices[0].message.content or \"\"\n+        # Strip thinking/reasoning blocks that think-enabled models\n+        # (MiniMax M2.7, DeepSeek, etc.) emit even for simple prompts like\n+        # title generation. Without this the raw <think>...</think> XML\n+        # leaks into session titles. Reuses the canonical scrubber so all\n+        # tag variants (unterminated blocks, orphan closes, mixed case)\n+        # are handled, not just a single literal <think> pair.\n+        from agent.agent_runtime_helpers import strip_think_blocks\n+        title = strip_think_blocks(None, content).strip()\n         # Clean up: remove quotes, trailing punctuation, prefixes like \"Title: \"\n         title = title.strip('\"\\'')\n         if title.lower().startswith(\"title:\"):"]},{"file":"scripts/release.py","hunks":["@@ -48,6 +48,7 @@\n     \"[redacted:email]\": \"bitcryptic-gw\",  # PR #53997 salvage (Matrix E2EE: resolve device_id via query_keys({mxid: []}) when whoami returns none; guard verification call sites so query_keys is never sent [null]; reset _device_id_unverified at connect() start; disconnect before reconnect)\n     \"[redacted:email]\": \"abchiaravalle\",  # PR #46997 salvage (recover resume_pending sessions: dual freshness signal + empty-turn safety net so restart auto-resume never sends a blank user turn)\n     \"[redacted:email]\": \"swissly\",  # PR #47167 salvage (wrap cron delivery thread-pool fallback in its own try/except so a per-target failure can't escape the except-RuntimeError block and crash the multi-target delivery loop; #47163)\n+    \"[redacted:email]\": \"shawchanshek\",  # PR #44126 salvage (strip <think>...</think> reasoning blocks from title-generator LLM output via the canonical strip_think_blocks scrubber so reasoning-model output can't leak into session titles)\n     \"[redacted:email]\": \"YLChen-007\",  # PR #27289 salvage (case-insensitive streaming reasoning-tag filter in cli.py _stream_delta + gateway stream_consumer so mixed-case variants like <Think>/<ThInK> are suppressed, not just the hardcoded case literals)\n     \"[redacted:email]\": \"kangsoo-bit\",  # PR #47508 salvage (keep Telegram gateway alive on transient bootstrap network errors: best-effort deleteWebhook + resilient start_polling degrade to background recovery instead of failing startup)\n     \"[redacted:email]\": \"testingbuddies24\",  # PR #43192 salvage (strip orphan think-tag close tags in progressive gateway stream so a bare </think> whose open was dropped upstream can't leak to the user)"]},{"file":"tests/agent/test_title_generator.py","hunks":["@@ -68,6 +68,37 @@ def test_strips_quotes(self):\n             title = generate_title(\"how do I set up docker\", \"First install...\")\n             assert title == \"Setting Up Docker Environment\"\n \n+    def test_strips_think_blocks(self):\n+        \"\"\"Reasoning-model output wrapped in <think>...</think> must not\n+        leak into the session title.\"\"\"\n+        mock_response = MagicMock()\n+        mock_response.choices = [MagicMock()]\n+        mock_response.choices[0].message.content = (\n+            \"<think>The user wants a title. I'll summarize the topic \"\n+            \"concisely.</think>Debugging Python Import Errors\"\n+        )\n+\n+        with patch(\"agent.title_generator.call_llm\", return_value=mock_response):\n+            title = generate_title(\"help me fix this import\", \"Sure...\")\n+            assert title == \"Debugging Python Import Errors\"\n+            assert \"<think>\" not in title\n+            assert \"summarize\" not in title\n+\n+    def test_strips_unterminated_think_block(self):\n+        \"\"\"An unterminated <think> block (no close tag) must still be\n+        stripped so the leaked reasoning doesn't become the title.\"\"\"\n+        mock_response = MagicMock()\n+        mock_response.choices = [MagicMock()]\n+        mock_response.choices[0].message.content = (\n+            \"<think>Let me reason about a good title for this session\"\n+        )\n+\n+        with patch(\"agent.title_generator.call_llm\", return_value=mock_response):\n+            title = generate_title(\"hello\", \"hi there\")\n+            # Everything from the unterminated open tag onward is stripped,\n+            # leaving nothing → None.\n+            assert title is None\n+\n     def test_strips_title_prefix(self):\n         mock_response = MagicMock()\n         mock_response.choices = [MagicMock()]\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}