Signal document attachment classification fixes
Cluster 12845 · gateway. Data as of
1 open item. Fix merged in #44695 on 12 Jun 2026. Check it covers your case.
The short answer
- Known?
- Yes. 9 related items grouped here, 1 issue already closed.
- Fix event
- A fix was merged to main in PR #44695. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
resolved, deciding PR #44695. That is separate from the fix event. - Still open
- 0 issues and 1 PR.
Multiple fixes and bug reports addressing the misclassification of document attachments (PDFs, etc.) from Signal and other gateways. Work spans MIME type detection, shared attachment directories, and per-attachment type handling across Signal, Email, and SimpleX.
Open issues0
Open PRs1
- PR#20347fix(signal): classify inbound document attachments
Fixes on record
-
#44695 fix(gateway): classify document attachments as DOCUMENT on Signal, Email, and SimpleX
files:
gateway/platforms/email.py,gateway/platforms/signal.py,plugins/platforms/simplex/adapter.py,scripts/release.py,tests/gateway/test_email.py,tests/gateway/test_signal.py,tests/gateway/test_simplex_plugin.pyShow the diff
-
gateway/platforms/email.py@@ -470,8 +470,15 @@ async def _dispatch_message(self, msg_data: Dict[str, Any]) -> None: for att in attachments: media_urls.append(att["path"]) media_types.append(att["media_type"]) - if att["type"] == "image": + if att["type"] == "image" and msg_type == MessageType.TEXT: msg_type = MessageType.PHOTO + elif att["type"] == "document": + # Document wins over PHOTO for mixed attachments: run.py's + # image handling keys off the per-path image/* mime type + # regardless of message_type, but document-context injection + # gates strictly on MessageType.DOCUMENT — so DOCUMENT is the + # only classification that surfaces both. + msg_type = MessageType.DOCUMENT # Store thread context for reply threading self._thread_context[sender_addr] = { -
gateway/platforms/signal.py@@ -602,6 +602,14 @@ async def _handle_envelope(self, envelope: dict) -> None: msg_type = MessageType.VOICE elif any(mt.startswith("image/") for mt in media_types): msg_type = MessageType.PHOTO + elif any(mt.startswith("video/") for mt in media_types): + msg_type = MessageType.VIDEO + else: + # Catch-all: application/*, text/*, and unknown MIME types are + # treated as documents so run.py's document-context injection + # surfaces the cached file path to the agent (same pattern as + # WhatsApp/Slack/BlueBubbles/Mattermost). + msg_type = MessageType.DOCUMENT # Parse timestamp from envelope data (milliseconds since epoch) ts_ms = envelope_data.get("timestamp", 0) -
plugins/platforms/simplex/adapter.py@@ -625,6 +625,11 @@ async def _handle_chat_item(self, chat_item: dict) -> None: msg_type = MessageType.VOICE elif any(mt.startswith("image/") for mt in media_types): msg_type = MessageType.PHOTO + else: + # Catch-all: non-image/non-audio files (tagged + # application/octet-stream above) are documents so run.py's + # document-context injection surfaces the file to the agent. + msg_type = MessageType.DOCUMENT # Timestamp ts_str = meta.get("itemTs") or meta.get("createdAt", "") -
scripts/release.py@@ -76,6 +76,7 @@ "[email protected]": "HeLLGURD", "[email protected]": "synapsesx", "[email protected]": "dirtyren", + "[redacted:email]": "kdunn926", "[redacted:email]": "mvanhorn", "[redacted:email]": "youjunxiaji", "[redacted:email]": "NormallyGaussian", -
tests/gateway/test_email.py@@ -393,6 +393,68 @@ async def capture_handle(event): self.assertEqual(captured_events[0].message_type, MessageType.PHOTO) self.assertEqual(captured_events[0].media_urls, ["/tmp/img.jpg"]) + def test_document_attachment_sets_document_type(self): + """Email with a document attachment must set DOCUMENT so run.py injects file context.""" + import asyncio + from gateway.platforms.base import MessageType + adapter = self._make_adapter() + captured_events = [] + + async def capture_handle(event): + captured_events.append(event) + + adapter.handle_message = capture_handle + + msg_data = { + "uid": b"6", + "sender_addr": "[redacted:email]", + "sender_name": "User", + "subject": "Re: report", + "message_id": "<[redacted:email]>", + "in_reply_to": "", + "body": "See attached", + "attachments": [{"path": "/tmp/report.pdf", "filename": "report.pdf", "type": "document", "media_type": "application/pdf"}], + "date": "", + } + + asyncio.run(adapter._dispatch_message(msg_data)) + self.assertEqual(len(captured_events), 1) + self.assertEqual(captured_events[0].message_type, MessageType.DOCUMENT) + self.assertEqual(captured_events[0].media_urls, ["/tmp/report.pdf"]) + + def test_mixed_image_and_document_prefers_document(self): + """DOCUMENT wins for mixed attachments — image handling keys off per-path + mime types, but document injection gates strictly on MessageType.DOCUMENT.""" + import asyncio + from gateway.platforms.base import MessageType + adapter = self._make_adapter() + captured_events = [] + + async def capture_handle(event): + captured_events.append(event) + + adapter.handle_message = capture_handle + + msg_data = { + "uid": b"7", + "sender_addr": "[redacted:email]", + "sender_name": "User", + "subject": "Re: both", + "message_id": "<[redacted:email]>", + "in_reply_to": "", + "body": "Photo and PDF", + "attachments": [ + {"path": "/tmp/img.jpg", "filename": "img.jpg", "type": "image", "media_type": "image/jpeg"}, + {"path": "/tmp/report.pdf", "filename": "report.pdf", "type": "document", "media_type": "application/pdf"}, + ], + "date": "", + } + + asyncio.run(adapter._dispatch_message(msg_data)) + self.assertEqual(len(captured_events), 1) + self.assertEqual(captured_events[0].message_type, MessageType.DOCUMENT) + self.assertEqual(len(captured_events[0].media_urls), 2) + def test_source_built_correctly(self): """Session source should have correct chat_id and user info.""" import asyncio -
tests/gateway/test_signal.py@@ -770,6 +770,153 @@ def test_signal_has_all_media_methods(self, monkeypatch): assert type(adapter).send_image is not BasePlatformAdapter.send_image +# --------------------------------------------------------------------------- +# Inbound attachment message type classification +# --------------------------------------------------------------------------- + +def _make_dm_envelope(sender: str, attachments: list, text: str = "") -> dict: + """Build a minimal signal-cli DM envelope with the given attachments.""" + return { + "envelope": { + "sourceNumber": sender, + "sourceName": "Test User", + "sourceUuid": "aaaaaaaa-0000-0000-0000-000000000001", + "timestamp": 1700000000000, + "dataMessage": { + "timestamp": 1700000000000, + "message": text, + "expiresInSeconds": 0, + "viewOnce": False, + "attachments": attachments, + }, + } + } + + +class TestSignalInboundMessageTypeClassification: + """_handle_envelope must set MessageType.DOCUMENT for application/* and text/* attachments. + + Before the fix, PDFs and other documents left msg_type as MessageType.TEXT, + so run.py's document-context injection (which gates on MessageType.DOCUMENT) + silently dropped the file and the agent never saw it. + """ + + async def _dispatch_single_attachment(self, monkeypatch, content_type: str, + att_id: str, fetch_path: str, fetch_ext: str): + """Helper: run _handle_envelope with one attachment and return the dispatched event.""" + envelope = _make_dm_envelope( + sender="+15559876543", + attachments=[{ + "contentType": content_type, + "id": att_id, + "size": 1024, + "filename": None, + "width": None, + "height": None, + "caption": None, + "uploadTimestamp": 1700000000000, + }], + ) + adapter = _make_signal_adapter(monkeypatch) + adapter._rpc, _ = _stub_rpc(None) + dispatched = [] + + async def _fake_handle_message(event): + dispatched.append(event) + + adapter.handle_message = _fake_handle_message + adapter._fetch_attachment = AsyncMock(return_value=(fetch_path, fetch_ext)) + await adapter._handle_envelope(envelope) + assert dispatched, "_handle_envelope did not dispatch any event" + return dispatched[0] + + @pytest.mark.asyncio + async def test_pdf_attachment_sets_document_type(self, monkeypatch): + """A PDF attachment (application/pdf) must produce MessageType.DOCUMENT, not TEXT.""" + from gateway.platforms.base import MessageType + + event = await self._dispatch_single_attachment( + monkeypatch, + content_type="application/pdf", + att_id="6zLO3b-6Yf3zVWeLDctA.pdf", + fetch_path="/tmp/report.pdf", + fetch_ext=".pdf", + ) + + assert event.message_type == MessageType.DOCUMENT, ( + f"Expected DOCUMENT, got {event.message_type}. " + "PDFs must be classified as DOCUMENT so run.py injects file context." + ) + assert "/tmp/report.pdf" in event.media_urls + + @pytest.mark.asyncio + async def test_text_plain_attachment_sets_document_type(self, monkeypatch): + """A text/plain attachment must produce MessageType.DOCUMENT, not TEXT.""" + from gateway.platforms.base import MessageType + + event = await self._dispatch_single_attachment( + monkeypatch, + content_type="text/plain", + att_id="notes.txt", + fetch_path="/tmp/notes.txt", + fetch_ext=".txt", + ) + + assert event.message_type == MessageType.DOCUMENT, ( + f"Expected DOCUMENT, got {event.message_type}. " + "text/plain must be classified as DOCUMENT so run.py injects file context." + ) + + @pytest.mark.asyncio + async def test_text_html_attachment_sets_document_type(self, monkeypatch): + """A text/html attachment must produce MessageType.DOCUMENT (covers the text/* wildcard).""" + from gateway.platforms.base import MessageType + + event = await self._dispatch_single_attachment( + monkeypatch, + content_type="text/html", + att_id="page.html", + fetch_path="/tmp/page.html", + fetch_ext=".html", + ) + + assert event.message_type == MessageType.DOCUMENT, ( + f"Expected DOCUMENT, got {event.message_type}. " + "text/html must be classified as DOCUMENT so run.py injects file context." + ) + + @pytest.mark.asyncio + async def test_video_attachment_sets_video_type(self, monkeypatch): + """A video/mp4 attachment must produce MessageType.VIDEO.""" + from gateway.platforms.base import MessageType + + event = await self._dispatch_single_attachment( + monkeypatch, + content_type="video/mp4", + att_id="clip.mp4", + fetch_path="/tmp/clip.mp4", + fetch_ext=".mp4", + ) + + assert event.message_type == MessageType.VIDEO + + @pytest.mark.asyncio + async def test_unknown_mime_attachment_falls_back_to_document(self, monkeypatch): + """Unknown/exotic MIME types fall through to DOCUMENT (catch-all), + matching the WhatsApp/Slack/BlueBubbles classification pattern.""" + from gateway.platforms.base import MessageType + + event = await self._dispatch_single_attachment( + monkeypatch, + content_type="chemical/x-pdb", + att_id="molecule.pdb", + fetch_path="/tmp/molecule.pdb", + fetch_ext=".pdb", + ) + + assert event.message_type == MessageType.DOCUMENT + + # --------------------------------------------------------------------------- # send_document now routes through _send_attachment (#5105 bonus) # --------------------------------------------------------------------------- -
tests/gateway/test_simplex_plugin.py@@ -396,3 +396,74 @@ def test_register_calls_register_platform(): assert callable(kwargs["setup_fn"]) # SimpleX uses opaque IDs only — no PII to redact. assert kwargs["pii_safe"] is True + + +# --------------------------------------------------------------------------- +# Inbound attachment message type classification +# --------------------------------------------------------------------------- + +def _make_file_chat_item(file_path: str, file_name: str) -> dict: + """Minimal direct-chat rcvMsgContent item carrying a completed file.""" + return { + "chatInfo": { + "type": "direct", + "contact": {"contactId": 42, "localDisplayName": "tester"}, + }, + "chatItem": { + "chatDir": {"type": "directRcv"}, + "meta": {"itemTs": "2026-01-01T00:00:00Z"}, + "content": { + "type": "rcvMsgContent", + "msgContent": {"type": "file", "text": "here you go"}, + }, + "file": { + "fileId": 7, + "fileName": file_name, + "fileSource": {"filePath": file_path}, + }, + }, + } + + [redacted:email] +async def test_document_file_sets_document_type(): + """A non-image/non-audio file must classify as DOCUMENT, not TEXT, + so run.py's document-context injection surfaces the path to the agent.""" + from gateway.config import PlatformConfig + from gateway.platforms.base import MessageType + + cfg = PlatformConfig(enabled=True, extra={"ws_url": "ws://localhost:5225"}) + adapter = SimplexAdapter(cfg) + dispatched = [] + + async def _capture(event): + dispatched.append(event) + + adapter.handle_message = _capture + await adapter._handle_chat_item(_make_file_chat_item("/tmp/report.pdf", "report.pdf")) + + assert dispatched, "_handle_chat_item did not dispatch any event" + assert dispatched[0].message_type == MessageType.DOCUMENT + assert dispatched[0].media_urls == ["/tmp/report.pdf"] + assert dispatched[0].media_types == ["application/octet-stream"] + + [redacted:email] +async def test_image_file_still_sets_photo_type(): + """Regression guard: image files keep classifying as PHOTO after the + document catch-all was added.""" + from gateway.config import PlatformConfig + from gateway.platforms.base import MessageType + + cfg = PlatformConfig(enabled=True, extra={"ws_url": "ws://localhost:5225"}) + adapter = SimplexAdapter(cfg) + dispatched = [] + + async def _capture(event): + dispatched.append(event) + + adapter.handle_message = _capture + await adapter._handle_chat_item(_make_file_chat_item("/tmp/pic.jpg", "pic.jpg")) + + assert dispatched, "_handle_chat_item did not dispatch any event" + assert dispatched[0].message_type == MessageType.PHOTO
-
6 PRs closed without merging
From the close comments: duplicate 2 · closed without comment 1 · done elsewhere 1 · merged via another change 1 · other 1
- #7445 [codex] Fix Signal document attachments · done elsewhere
-
#12851 fix(gateway): add Signal message type classification for documents · merged via another change →
8207ae8,ffef9da,8e821cd,#44695 - #12859 fix(signal): classify document attachments as MessageType.DOCUMENT · other
- #13243 fix: detect Signal document attachments (PDF, etc.) as MessageType.DOCUMENT · duplicate
- #13512 fix(signal): detect document attachments via application/* and text/* MIME types · duplicate
Show 1 more
- #15671 fix(signal): support shared attachment directories · closed without comment
Closed issues · 1
- Closed#12845[Bug]: Document attachments from Signal not detected
How this cluster was grouped
-
Link evidence (
structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference. -
Pairwise link density
0.22: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the LabAt a glance
- Subsystem
- gateway
- Merged PRs
- #44695
- Closed issues
- 1
- Attempts
- 6 PRs closed without merging
- Fix link checked
- 12 Jun 2026