From d5357f816d669084b9b7dc2da906100d2034212f Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Tue, 5 May 2026 13:27:27 -0700 Subject: [PATCH] refactor(telegram): make typing thread-id resolver symmetric with send MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirror _message_thread_id_for_typing() with _message_thread_id_for_send(): both now map the General forum topic (thread id "1") to None upfront. That removes the need for the retry-without-thread fallback in send_typing() entirely — if _message_thread_id_for_typing() returns a non-None value, it's a real user-created topic and falling back to the root chat is never correct. If Telegram rejects the typing action (e.g. topic deleted mid-session), we swallow it at debug level instead of bleeding the indicator into All Messages. Updates the General-topic typing regression test to assert the new single-call contract. --- gateway/platforms/telegram.py | 31 +++++++++---------- .../gateway/test_telegram_thread_fallback.py | 12 ++++--- 2 files changed, 21 insertions(+), 22 deletions(-) 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}, ]