fix(telegram): resume typing indicator after inline approval click (#27853)

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) <noreply@anthropic.com>
This commit is contained in:
briandevans 2026-05-18 01:33:13 -07:00 committed by Teknium
parent 9a444a9355
commit ba2572e54c
2 changed files with 70 additions and 0 deletions

View file

@ -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) ---

View file

@ -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()