diff --git a/gateway/platforms/telegram.py b/gateway/platforms/telegram.py index 51b2bc848a..83e8173687 100644 --- a/gateway/platforms/telegram.py +++ b/gateway/platforms/telegram.py @@ -353,7 +353,10 @@ class TelegramAdapter(BasePlatformAdapter): @classmethod def _message_thread_id_for_typing(cls, thread_id: Optional[str]) -> Optional[int]: - if not thread_id: + # Mirrors _message_thread_id_for_send: the General forum topic (thread id + # "1") is represented as "no thread id" on the wire. User-created topics + # keep their real id so typing stays scoped to that topic. + if not thread_id or str(thread_id) == cls._GENERAL_TOPIC_THREAD_ID: return None return int(thread_id) @@ -2508,22 +2511,16 @@ class TelegramAdapter(BasePlatformAdapter): try: _typing_thread = self._metadata_thread_id(metadata) message_thread_id = self._message_thread_id_for_typing(_typing_thread) - try: - await self._bot.send_chat_action( - chat_id=int(chat_id), - action="typing", - message_thread_id=message_thread_id, - ) - except Exception as e: - if message_thread_id is not None and self._is_thread_not_found_error(e): - if str(_typing_thread) == self._GENERAL_TOPIC_THREAD_ID: - await self._bot.send_chat_action( - chat_id=int(chat_id), - action="typing", - message_thread_id=None, - ) - else: - raise + # No retry-without-thread fallback here: _message_thread_id_for_typing + # already maps the forum General topic to None, so any non-None value + # reaching this call is a user-created topic. If Telegram rejects it + # (e.g. topic deleted mid-session), we swallow the failure rather than + # showing a typing indicator in the wrong chat/All Messages. + await self._bot.send_chat_action( + chat_id=int(chat_id), + action="typing", + message_thread_id=message_thread_id, + ) except Exception as e: # Typing failures are non-fatal; log at debug level only. logger.debug( diff --git a/tests/gateway/test_telegram_thread_fallback.py b/tests/gateway/test_telegram_thread_fallback.py index 3b7069d6fa..b8330822b3 100644 --- a/tests/gateway/test_telegram_thread_fallback.py +++ b/tests/gateway/test_telegram_thread_fallback.py @@ -159,22 +159,24 @@ async def test_send_omits_general_topic_thread_id(): @pytest.mark.asyncio -async def test_send_typing_retries_without_general_thread_when_not_found(): - """Typing for forum General should fall back if Telegram rejects thread 1.""" +async def test_send_typing_general_topic_uses_none_thread_id(): + """Typing for forum General should hit the API with message_thread_id=None directly. + + _message_thread_id_for_typing() maps the General topic (thread id "1") to None + the same way _message_thread_id_for_send() does, so there's no retry path — the + first call is already correct. + """ adapter = _make_adapter() call_log = [] async def mock_send_chat_action(**kwargs): call_log.append(dict(kwargs)) - if kwargs.get("message_thread_id") == 1: - raise FakeBadRequest("Message thread not found") adapter._bot = SimpleNamespace(send_chat_action=mock_send_chat_action) await adapter.send_typing("-100123", metadata={"thread_id": "1"}) assert call_log == [ - {"chat_id": -100123, "action": "typing", "message_thread_id": 1}, {"chat_id": -100123, "action": "typing", "message_thread_id": None}, ]