{"repo":"NousResearch/hermes-agent","public_id":"18620","display_label":"Telegram image files sent as documents misrouted","summary":"A bug where image files (PNG, JPG, WebP, GIF) sent as Telegram documents were rejected or mishandled instead of being routed through image/vision handling. Multiple fix attempts address routing these document-type images correctly.","subsystem":"gateway","counts":{"members":7,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":2,"merged_prs":1,"attempt_prs":4},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":28519},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"verified","last_verified_at":"2026-05-19T05:37:29+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.35047619047619044},"fix_edges":[{"issue_number":18620,"pr_number":28519,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":20128,"pr_number":28519,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false}],"members":[{"kind":"issue","number":18620,"title":"[Bug]: Telegram incoming image files treated as document uploads","url":null,"state":"closed"},{"kind":"issue","number":20128,"title":"Telegram: image documents like .webp are rejected as unsupported documents instead of being routed through image handling","url":null,"state":"closed"},{"kind":"pr","number":16710,"title":"fix(telegram): treat image documents (PNG/JPG/WebP/GIF) as images","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"77c4675a50db7abbfd191d4fba4746b4f3e1559e","number":0},{"kind":"pr","value":"#18620","number":18620},{"kind":"pr","value":"#28519","number":28519}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":20147,"title":"fix(telegram): route image documents (.webp, .png, .jpg, .gif) through image handling (#20128)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"77c4675a50db7abbfd191d4fba4746b4f3e1559e","number":0},{"kind":"commit","value":"77c4675a50db7abbfd191d4fba4746b4f3e1559e","number":0},{"kind":"pr","value":"#20128","number":20128},{"kind":"pr","value":"#16710","number":16710}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":20756,"title":"fix(gateway): route image documents through vision pipeline in Telegram","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"77c4675a50db7abbfd191d4fba4746b4f3e1559e","number":0},{"kind":"pr","value":"#20128","number":20128}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":24815,"title":"fix(telegram): route image documents (.png/.jpg/.webp/.gif) through vision pipeline","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#28519","number":28519}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":28519,"title":"fix(telegram): route image documents (.png/.jpg/.webp/.gif) through vision pipeline","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["gateway/platforms/base.py","gateway/platforms/telegram.py","scripts/release.py","tests/gateway/test_telegram_documents.py"],"fix_hunks":[{"file":"gateway/platforms/base.py","hunks":["@@ -837,6 +837,26 @@ def cache_video_from_bytes(data: bytes, ext: str = \".mp4\") -> str:\n }\n \n \n+# ---------------------------------------------------------------------------\n+# Image document types\n+#\n+# Image extensions that platforms may deliver as \"documents\" rather than\n+# native photo attachments (Telegram users uploading via the file picker,\n+# clients that wrap stickers/screenshots as files, etc.). When we see one\n+# of these, we route the bytes through the image cache and the normal\n+# vision/photo handling path instead of rejecting them as unsupported\n+# documents.\n+# ---------------------------------------------------------------------------\n+\n+SUPPORTED_IMAGE_DOCUMENT_TYPES = {\n+    \".jpg\": \"image/jpeg\",\n+    \".jpeg\": \"image/jpeg\",\n+    \".png\": \"image/png\",\n+    \".webp\": \"image/webp\",\n+    \".gif\": \"image/gif\",\n+}\n+\n+\n def get_document_cache_dir() -> Path:\n     \"\"\"Return the document cache directory, creating it if it doesn't exist.\"\"\"\n     DOCUMENT_CACHE_DIR.mkdir(parents=True, exist_ok=True)"]},{"file":"gateway/platforms/telegram.py","hunks":["@@ -76,6 +76,7 @@ class _MockContextTypes:\n     resolve_proxy_url,\n     SUPPORTED_VIDEO_TYPES,\n     SUPPORTED_DOCUMENT_TYPES,\n+    SUPPORTED_IMAGE_DOCUMENT_TYPES,\n     utf16_len,\n )\n from gateway.platforms.telegram_network import (","@@ -4624,6 +4625,14 @@ async def _handle_media_message(self, update: Update, context: ContextTypes.DEFA\n                     video_mime_to_ext = {v: k for k, v in SUPPORTED_VIDEO_TYPES.items()}\n                     ext = video_mime_to_ext.get(doc.mime_type, \"\")\n \n+                if not ext and doc.mime_type:\n+                    # SUPPORTED_IMAGE_DOCUMENT_TYPES has duplicate values (.jpg + .jpeg\n+                    # both map to image/jpeg); keep the first ext we encounter.\n+                    image_mime_to_ext: dict[str, str] = {}\n+                    for _ext, _mime in SUPPORTED_IMAGE_DOCUMENT_TYPES.items():\n+                        image_mime_to_ext.setdefault(_mime, _ext)\n+                    ext = image_mime_to_ext.get(doc.mime_type, \"\")\n+\n                 if ext in SUPPORTED_VIDEO_TYPES:\n                     file_obj = await doc.get_file()\n                     video_bytes = await file_obj.download_as_bytearray()","@@ -4635,6 +4644,21 @@ async def _handle_media_message(self, update: Update, context: ContextTypes.DEFA\n                     await self.handle_message(event)\n                     return\n \n+                if ext in SUPPORTED_IMAGE_DOCUMENT_TYPES:\n+                    file_obj = await doc.get_file()\n+                    image_bytes = await file_obj.download_as_bytearray()\n+                    cached_path = cache_image_from_bytes(bytes(image_bytes), ext=ext)\n+                    event.media_urls = [cached_path]\n+                    event.media_types = [SUPPORTED_IMAGE_DOCUMENT_TYPES[ext]]\n+                    event.message_type = MessageType.PHOTO\n+                    logger.info(\"[Telegram] Cached user image document at %s\", cached_path)\n+                    media_group_id = getattr(msg, \"media_group_id\", None)\n+                    if media_group_id:\n+                        await self._queue_media_group_event(str(media_group_id), event)\n+                    else:\n+                        await self.handle_message(event)\n+                    return\n+\n                 # Check if supported\n                 if ext not in SUPPORTED_DOCUMENT_TYPES:\n                     supported_list = \", \".join(sorted(SUPPORTED_DOCUMENT_TYPES.keys()))"]},{"file":"scripts/release.py","hunks":["@@ -179,6 +179,7 @@\n     \"[redacted:email]\": \"kunci115\",  # PR #27098 salvage (thread-not-found retry)\n     \"[redacted:email]\": \"chromalinx\",  # PR #27014 salvage (commands for groups+DM)\n     \"[redacted:email]\": \"booker1207\",  # PR #25132 salvage (gate profile bots by allowed topics)\n+    \"[redacted:email]\": \"kiranvk-2011\",  # PR #24815 salvage (image documents → vision)\n     \"[redacted:email]\": \"eliteworkstation94-ai\",  # PR #28157 salvage (group reply session splits)\n     \"[redacted:email]\": \"hllqkb\",\n     \"[redacted:email]\": \"Bongulielmi\","]},{"file":"tests/gateway/test_telegram_documents.py","hunks":["@@ -134,6 +134,11 @@ def adapter():\n     a = TelegramAdapter(config)\n     # Capture events instead of processing them\n     a.handle_message = AsyncMock()\n+    # After PR #28494 made the empty-allowlist callback auth fail-closed\n+    # (and #28492 wired _is_callback_user_authorized into _should_process_message),\n+    # document-routing tests need to bypass the new gate so messages from fake\n+    # senders reach handle_message.\n+    a._is_callback_user_authorized = lambda user_id, **_kw: True\n     return a\n \n \n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}