{"repo":"NousResearch/hermes-agent","public_id":"38235","display_label":"LINE adapter inbound image message type mapping","summary":"These issues and pull requests concern a bug where the LINE adapter incorrectly parses or classifies inbound image messages, and the associated fixes to map LINE message types to the correct internal MessageType.","subsystem":"gateway","counts":{"members":3,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":39485,"merged_at":"2026-06-05T04:55:21+00:00"},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"verified","last_verified_at":"2026-06-05T04:55:21+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":38235,"pr_number":39485,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-06-05T04:55:21+00:00"}],"members":[{"kind":"issue","number":38235,"title":"[Bug]: LINE adapter can't parse image messages","url":null,"state":"closed"},{"kind":"pr","number":38528,"title":"fix(line): classify image messages as photos","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"7309f3bef","number":0},{"kind":"pr","value":"#39485","number":39485}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":39485,"title":"fix(line): map inbound message types to the correct MessageType","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["plugins/platforms/line/adapter.py","tests/gateway/test_line_plugin.py"],"fix_hunks":[{"file":"plugins/platforms/line/adapter.py","hunks":["@@ -133,6 +133,21 @@\n LINE_IMAGE_MAX_BYTES = 10 * 1024 * 1024  # 10 MB per LINE docs\n LINE_AV_MAX_BYTES = 200 * 1024 * 1024  # 200 MB for voice/video\n \n+# Map LINE webhook message types to the normalized MessageType the gateway\n+# routes on. LINE has no separate \"voice\" type — audio messages are recorded\n+# voice clips, so they map to VOICE (which the gateway sends through STT),\n+# mirroring how Telegram/WhatsApp classify voice notes. Anything unknown\n+# falls back to TEXT.\n+_LINE_MESSAGE_TYPES = {\n+    \"text\": MessageType.TEXT,\n+    \"image\": MessageType.PHOTO,\n+    \"video\": MessageType.VIDEO,\n+    \"audio\": MessageType.VOICE,\n+    \"file\": MessageType.DOCUMENT,\n+    \"location\": MessageType.LOCATION,\n+    \"sticker\": MessageType.STICKER,\n+}\n+\n # A 1×1 transparent PNG used as fallback video preview thumbnail when no\n # explicit preview is supplied — LINE requires ``previewImageUrl`` for\n # video messages. Sourced from the Python stdlib (no Pillow dependency).","@@ -968,7 +983,7 @@ async def _handle_message_event(self, event: Dict[str, Any]) -> None:\n \n         event_obj = MessageEvent(\n             text=text,\n-            message_type=MessageType.TEXT if msg_type == \"text\" else MessageType.IMAGE,\n+            message_type=_LINE_MESSAGE_TYPES.get(msg_type, MessageType.TEXT),\n             source=source_obj,\n             raw_message=event,\n             message_id=message_id,"]},{"file":"tests/gateway/test_line_plugin.py","hunks":["@@ -641,3 +641,36 @@ def test_get_chat_info_infers_type_from_prefix(self, monkeypatch):\n         assert asyncio.run(ad.get_chat_info(\"U123\"))[\"type\"] == \"dm\"\n         assert asyncio.run(ad.get_chat_info(\"C123\"))[\"type\"] == \"group\"\n         assert asyncio.run(ad.get_chat_info(\"R123\"))[\"type\"] == \"channel\"\n+\n+\n+# ---------------------------------------------------------------------------\n+# 9. Inbound message-type classification\n+# ---------------------------------------------------------------------------\n+\n+class TestMessageTypeMapping:\n+    \"\"\"LINE webhook message types must map to the right normalized\n+    MessageType so the gateway routes media correctly (e.g. voice → STT,\n+    files → document handling). Regression guard for the old code that\n+    referenced the non-existent ``MessageType.IMAGE`` and collapsed every\n+    non-text message onto a single type.\"\"\"\n+\n+    def test_image_event_not_attributeerror_regression(self):\n+        # The bug: MessageType.IMAGE doesn't exist on the enum.\n+        MessageType = _line.MessageType\n+        assert not hasattr(MessageType, \"IMAGE\")\n+\n+    def test_every_line_type_maps_to_correct_enum(self):\n+        MessageType = _line.MessageType\n+        mapping = _line._LINE_MESSAGE_TYPES\n+        assert mapping[\"text\"] == MessageType.TEXT\n+        assert mapping[\"image\"] == MessageType.PHOTO\n+        assert mapping[\"video\"] == MessageType.VIDEO\n+        # LINE has no separate voice type — audio clips are voice notes.\n+        assert mapping[\"audio\"] == MessageType.VOICE\n+        assert mapping[\"file\"] == MessageType.DOCUMENT\n+        assert mapping[\"location\"] == MessageType.LOCATION\n+        assert mapping[\"sticker\"] == MessageType.STICKER\n+\n+    def test_unknown_type_falls_back_to_text(self):\n+        MessageType = _line.MessageType\n+        assert _line._LINE_MESSAGE_TYPES.get(\"flex\", MessageType.TEXT) == MessageType.TEXT\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}