{"repo":"NousResearch/hermes-agent","public_id":"27853","display_label":"Telegram typing indicator after inline approval","summary":"These members address a bug where the typing indicator does not resume after a user clicks an inline approval button in Telegram. A fix is proposed and referenced across the related pull requests and issue.","subsystem":"gateway","counts":{"members":3,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":28501,"merged_at":"2026-05-19T05:20:57+00:00"},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"verified","last_verified_at":"2026-05-19T05:20:57+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":27853,"pr_number":28501,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-05-19T05:20:57+00:00"}],"members":[{"kind":"issue","number":27853,"title":"Telegram inline approval buttons do not resume typing indicator after command approval","url":null,"state":"closed"},{"kind":"pr","number":27874,"title":"fix(telegram): resume typing indicator after inline approval click (#27853)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#28501","number":28501}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":28501,"title":"fix(telegram): resume typing indicator after inline approval click (#27853)","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["gateway/platforms/telegram.py","tests/gateway/test_telegram_approval_buttons.py"],"fix_hunks":[{"file":"gateway/platforms/telegram.py","hunks":["@@ -2814,6 +2814,15 @@ async def _handle_callback_query(\n                     )\n                 except Exception as exc:\n                     logger.error(\"Failed to resolve gateway approval from Telegram button: %s\", exc)\n+                    count = 0\n+\n+                # Resume the typing indicator — paused when the approval was\n+                # sent (gateway/run.py).  The text /approve and /deny paths\n+                # call resume_typing_for_chat here too; without it, typing\n+                # stays paused for the rest of the turn after an inline\n+                # button click.\n+                if count and query_chat_id is not None:\n+                    self.resume_typing_for_chat(str(query_chat_id))\n             return\n \n         # --- Slash-confirm callbacks (sc:choice:confirm_id) ---"]},{"file":"tests/gateway/test_telegram_approval_buttons.py","hunks":["@@ -271,6 +271,67 @@ async def test_resolves_approval_on_click(self):\n         # State should be cleaned up\n         assert 1 not in adapter._approval_state\n \n+    @pytest.mark.asyncio\n+    async def test_resume_typing_after_inline_approval(self):\n+        \"\"\"Clicking an inline approval button must un-pause the chat's typing.\n+\n+        Regression for #27853: the text /approve path resumed typing, but the\n+        ea: callback path did not, so the typing indicator stayed gone for the\n+        rest of a long-running turn after a button click.\n+        \"\"\"\n+        adapter = _make_adapter()\n+        adapter._approval_state[5] = \"agent:main:telegram:group:12345:99\"\n+        adapter.pause_typing_for_chat(\"12345\")\n+        assert \"12345\" in adapter._typing_paused\n+\n+        query = AsyncMock()\n+        query.data = \"ea:once:5\"\n+        query.message = MagicMock()\n+        query.message.chat_id = 12345\n+        query.from_user = MagicMock()\n+        query.from_user.first_name = \"Norbert\"\n+        query.from_user.id = \"12345\"\n+        query.answer = AsyncMock()\n+        query.edit_message_text = AsyncMock()\n+\n+        update = MagicMock()\n+        update.callback_query = query\n+        context = MagicMock()\n+\n+        with patch.dict(os.environ, {\"TELEGRAM_ALLOWED_USERS\": \"*\"}, clear=False):\n+            with patch(\"tools.approval.resolve_gateway_approval\", return_value=1):\n+                await adapter._handle_callback_query(update, context)\n+\n+        assert \"12345\" not in adapter._typing_paused\n+\n+    @pytest.mark.asyncio\n+    async def test_typing_stays_paused_when_resolve_returns_zero(self):\n+        \"\"\"If resolve_gateway_approval reports 0 resolves, the agent thread\n+        was never unblocked, so typing should NOT be force-resumed.\"\"\"\n+        adapter = _make_adapter()\n+        adapter._approval_state[6] = \"agent:main:telegram:group:12345:99\"\n+        adapter.pause_typing_for_chat(\"12345\")\n+\n+        query = AsyncMock()\n+        query.data = \"ea:once:6\"\n+        query.message = MagicMock()\n+        query.message.chat_id = 12345\n+        query.from_user = MagicMock()\n+        query.from_user.first_name = \"Norbert\"\n+        query.from_user.id = \"12345\"\n+        query.answer = AsyncMock()\n+        query.edit_message_text = AsyncMock()\n+\n+        update = MagicMock()\n+        update.callback_query = query\n+        context = MagicMock()\n+\n+        with patch.dict(os.environ, {\"TELEGRAM_ALLOWED_USERS\": \"*\"}, clear=False):\n+            with patch(\"tools.approval.resolve_gateway_approval\", return_value=0):\n+                await adapter._handle_callback_query(update, context)\n+\n+        assert \"12345\" in adapter._typing_paused\n+\n     @pytest.mark.asyncio\n     async def test_approval_callback_escapes_dynamic_user_name(self):\n         adapter = _make_adapter()\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}