From 26a2fda8d709122d432ab7bc0345303ff95e6f5d Mon Sep 17 00:00:00 2001 From: Yemi Date: Thu, 23 Jul 2026 08:02:02 -0700 Subject: [PATCH] fix(slack): surface thread images/files as markers in fetched thread context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Images and files posted in a Slack thread before the bot joins were invisible to the agent: _fetch_thread_context renders text only, and a caption-less image post was dropped from context entirely (empty text → skip). "@bot what do you think of the chart above?" read as a question about nothing. _render_message_text now appends a compact, sanitized marker per file attachment — [image: chart.png], [video: demo.mp4], [audio: note.m4a], [file: report.pdf (application/pdf)] — so the agent can SEE that prior thread messages carried attachments and ask for a re-share when it needs the bytes. Filenames are stripped of newlines/brackets so a hostile name can't fake context structure. Because both thread-context formatting and parent-text rendering go through _render_message_text, markers appear on the cold-start hydrate, the explicit-mention delta refresh, restart rehydration, and reply_to_text. Reapplied from #32315 onto the current adapter (original patched the pre-plugin gateway/platforms/slack.py, moved in the plugin migration; annotation labels reworked to per-file typed markers, download side handled separately). --- plugins/platforms/slack/adapter.py | 33 ++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/plugins/platforms/slack/adapter.py b/plugins/platforms/slack/adapter.py index 1ca4da6e3dcc..5b68b3e35948 100644 --- a/plugins/platforms/slack/adapter.py +++ b/plugins/platforms/slack/adapter.py @@ -101,6 +101,26 @@ _SLACK_SPECIAL_MENTION_RE = re.compile( r"\n]*)?>", re.IGNORECASE ) + +def _slack_file_marker(file_obj: Dict[str, Any]) -> str: + """Render a compact text marker for a Slack file attachment. + + Used by :meth:`SlackAdapter._render_message_text` so thread-context and + parent-text rendering surface that a message carried images/files even + though the context fetch is text-only. Name is sanitized (newlines and + brackets stripped) so a hostile filename can't fake context structure. + """ + name = str(file_obj.get("name") or file_obj.get("title") or file_obj.get("id") or "file") + name = re.sub(r"[\r\n\[\]]+", " ", name).strip() or "file" + mimetype = str(file_obj.get("mimetype") or "") + if mimetype.startswith("image/"): + return f"[image: {name}]" + if mimetype.startswith("video/"): + return f"[video: {name}]" + if mimetype.startswith("audio/"): + return f"[audio: {name}]" + return f"[file: {name} ({mimetype})]" if mimetype else f"[file: {name}]" + # ContextVar carrying the user_id of the slash-command invoker. # Set in _handle_slash_command, read in send() to match the correct # stashed response_url when multiple users issue commands on the same @@ -6131,6 +6151,19 @@ class SlackAdapter(BasePlatformAdapter): new_urls = [u for u in urls if u not in msg_text and all(u not in e for e in extras)] if new_urls: extras.append("URLs: " + ", ".join(new_urls)) + # Surface file/image attachments as compact text markers. The + # thread-context fetch is text-only, so without this the agent has + # no idea prior messages carried images/files at all (#69185, + # #32315): "@bot what do you think of the chart above?" reads as a + # question about nothing. Markers keep context bounded — the agent + # can ask for a re-share when it needs the actual bytes. + files = msg.get("files") + if isinstance(files, list): + markers = [ + _slack_file_marker(f) for f in files if isinstance(f, dict) + ] + if markers: + extras.append(" ".join(markers)) if extras: addendum = "\n".join(extras) msg_text = (msg_text + "\n" + addendum).strip() if msg_text else addendum