From ba2572e54ccb3169ba3a6dff0809912810e57c60 Mon Sep 17 00:00:00 2001 From: briandevans <252620095+briandevans@users.noreply.github.com> Date: Mon, 18 May 2026 01:33:13 -0700 Subject: [PATCH] fix(telegram): resume typing indicator after inline approval click (#27853) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The text /approve and /deny paths in gateway/run.py call resume_typing_for_chat() after resolve_gateway_approval() succeeds, but the Telegram inline-button (ea:*) callback in _handle_callback_query did not. Typing is paused when the approval is sent (gateway/run.py:15658), so without a matching resume the typing indicator stayed gone for the remainder of a long-running turn after a button click. Symmetry-match the text path: after a successful resolve, call self.resume_typing_for_chat(str(query_chat_id)). Guarded by count > 0 to match /approve's "if not count" early-return — if nothing was actually resolved, the agent thread was never unblocked, so typing should remain paused. Co-Authored-By: Claude Opus 4.7 (1M context) --- gateway/platforms/telegram.py | 9 +++ .../gateway/test_telegram_approval_buttons.py | 61 +++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/gateway/platforms/telegram.py b/gateway/platforms/telegram.py index 7d63de1fdba..d68296e12cb 100644 --- a/gateway/platforms/telegram.py +++ b/gateway/platforms/telegram.py @@ -2814,6 +2814,15 @@ class TelegramAdapter(BasePlatformAdapter): ) except Exception as exc: logger.error("Failed to resolve gateway approval from Telegram button: %s", exc) + count = 0 + + # Resume the typing indicator — paused when the approval was + # sent (gateway/run.py). The text /approve and /deny paths + # call resume_typing_for_chat here too; without it, typing + # stays paused for the rest of the turn after an inline + # button click. + if count and query_chat_id is not None: + self.resume_typing_for_chat(str(query_chat_id)) return # --- Slash-confirm callbacks (sc:choice:confirm_id) --- diff --git a/tests/gateway/test_telegram_approval_buttons.py b/tests/gateway/test_telegram_approval_buttons.py index f439d97250f..c939b08c09d 100644 --- a/tests/gateway/test_telegram_approval_buttons.py +++ b/tests/gateway/test_telegram_approval_buttons.py @@ -271,6 +271,67 @@ class TestTelegramApprovalCallback: # State should be cleaned up assert 1 not in adapter._approval_state + @pytest.mark.asyncio + async def test_resume_typing_after_inline_approval(self): + """Clicking an inline approval button must un-pause the chat's typing. + + Regression for #27853: the text /approve path resumed typing, but the + ea: callback path did not, so the typing indicator stayed gone for the + rest of a long-running turn after a button click. + """ + adapter = _make_adapter() + adapter._approval_state[5] = "agent:main:telegram:group:12345:99" + adapter.pause_typing_for_chat("12345") + assert "12345" in adapter._typing_paused + + query = AsyncMock() + query.data = "ea:once:5" + query.message = MagicMock() + query.message.chat_id = 12345 + query.from_user = MagicMock() + query.from_user.first_name = "Norbert" + query.from_user.id = "12345" + query.answer = AsyncMock() + query.edit_message_text = AsyncMock() + + update = MagicMock() + update.callback_query = query + context = MagicMock() + + with patch.dict(os.environ, {"TELEGRAM_ALLOWED_USERS": "*"}, clear=False): + with patch("tools.approval.resolve_gateway_approval", return_value=1): + await adapter._handle_callback_query(update, context) + + assert "12345" not in adapter._typing_paused + + @pytest.mark.asyncio + async def test_typing_stays_paused_when_resolve_returns_zero(self): + """If resolve_gateway_approval reports 0 resolves, the agent thread + was never unblocked, so typing should NOT be force-resumed.""" + adapter = _make_adapter() + adapter._approval_state[6] = "agent:main:telegram:group:12345:99" + adapter.pause_typing_for_chat("12345") + + query = AsyncMock() + query.data = "ea:once:6" + query.message = MagicMock() + query.message.chat_id = 12345 + query.from_user = MagicMock() + query.from_user.first_name = "Norbert" + query.from_user.id = "12345" + query.answer = AsyncMock() + query.edit_message_text = AsyncMock() + + update = MagicMock() + update.callback_query = query + context = MagicMock() + + with patch.dict(os.environ, {"TELEGRAM_ALLOWED_USERS": "*"}, clear=False): + with patch("tools.approval.resolve_gateway_approval", return_value=0): + await adapter._handle_callback_query(update, context) + + assert "12345" in adapter._typing_paused + @pytest.mark.asyncio async def test_approval_callback_escapes_dynamic_user_name(self): adapter = _make_adapter()