Bug Description
In the gateway post-stream media delivery path, Hermes can upload a local file even when the final visible assistant reply did not intentionally request an attachment.
The failure mode I hit on Slack was:
- the visible reply text sent to the user contained no
MEDIA: directive,
- but after the text message was sent, the gateway still uploaded an image based on a stale local path that had appeared in inspected content/tool output.
This is distinct from issues where a literal visible MEDIA:/... example in the assistant text gets treated as an attachment. In this case, the visible final reply was clean, but the post-stream helper rescanned the response and promoted a bare local filesystem path into a real upload.
Environment
- Platform: Slack gateway
- Repo area:
gateway/run.py
- Observed in a local git checkout after
hermes update
Steps to Reproduce
- Run Hermes via the Slack gateway.
- Have the agent inspect content that contains a local path or archived
MEDIA: reference to an existing image/file, e.g. tool output or searched content containing something like /absolute/path/to/file.png.
- Let the agent produce a normal streamed text reply that does not intentionally include an attachment directive in the visible final user-facing message.
- Observe gateway post-processing after the text has already been sent.
Expected Behavior
- Post-stream media delivery should only send attachments that were explicitly intended for user delivery.
- A normal streamed reply should not cause bare local paths from inspected/tool-output content to be uploaded as attachments.
- If the visible final reply has no explicit attachment directive, no attachment should be sent just because a local path string appeared somewhere in the assembled response context.
Actual Behavior
The gateway can attach a file after streaming the text reply.
In my case:
- Slack logs showed the normal text reply being sent first,
- then
files_upload_v2 ran afterward,
- and the uploaded image came from stale inspected content rather than an intentional attachment request in the visible reply.
Representative log pattern:
INFO gateway.run: response ready: platform=slack ... response=11235 chars
INFO gateway.platforms.base: [Slack] Sending response (11164 chars) to ...
INFO gateway.platforms.slack: [Slack] Sending 1 image(s) in single files_upload_v2 (chunk 1/1)
In a later reproduction, the gateway also logged:
WARNING gateway.platforms.slack: [Slack] Skipping missing image: /.../jonathan_brill_mockup.png
INFO gateway.platforms.slack: [Slack] Sending 1 image(s) in single files_upload_v2 (chunk 1/1)
Root Cause
GatewayRunner._deliver_media_from_response() rescans the response after streaming and currently mixes two behaviors:
- explicit attachment extraction (
extract_media(response))
- implicit local-path extraction from cleaned text (
extract_local_files(cleaned))
The problematic flow was:
media_files, _ = adapter.extract_media(response)
_, cleaned = adapter.extract_images(response)
local_files, _ = adapter.extract_local_files(cleaned)
That extract_local_files(cleaned) step lets bare local filesystem paths found in post-processed response text become real attachments, even when the final visible reply did not intentionally request upload.
This is especially dangerous in streaming mode because:
- the user-visible text has already been streamed/sanitized,
- then the post-stream helper reparses the full response,
- and hidden/internal/tool-derived path strings can be promoted into uploads.
Local Fix That Worked
I patched gateway/run.py so the post-stream helper stays explicit-only:
- keep explicit
MEDIA: extraction,
- stop auto-attaching bare local paths from the post-stream response text.
Specifically, I removed extract_local_files(cleaned) and the follow-on post-stream send logic for non-explicit local files from _deliver_media_from_response().
Conceptually:
media_files, _ = adapter.extract_media(response)
-_, cleaned = adapter.extract_images(response)
-local_files, _ = adapter.extract_local_files(cleaned)
+adapter.extract_images(response)
and removed the later block that sent non_image_local via send_video() / send_document().
Regression Test Added
I added a focused regression test file:
tests/gateway/test_post_stream_media_delivery.py
Coverage:
- post-stream delivery ignores a bare local path in the response text
- explicit
MEDIA: directives still work
Targeted test results on my checkout:
tests/gateway/test_post_stream_media_delivery.py: 2 passed
tests/gateway/test_extract_local_files.py: 37 passed
- combined targeted run with
tests/gateway/test_run_progress_topics.py: 63 passed
Why This Matters
This is more than log noise:
- it can leak internal/local file references into user-facing attachments,
- it can upload the wrong file after an otherwise normal reply,
- and it breaks the mental model that only explicit attachment directives produce uploads.
Related Issue
This seems related to #16434, but not identical.
A robust fix may want both:
- stricter
MEDIA: extraction semantics
- no implicit bare-local-path attachment behavior in the post-stream delivery helper
Bug Description
In the gateway post-stream media delivery path, Hermes can upload a local file even when the final visible assistant reply did not intentionally request an attachment.
The failure mode I hit on Slack was:
MEDIA:directive,This is distinct from issues where a literal visible
MEDIA:/...example in the assistant text gets treated as an attachment. In this case, the visible final reply was clean, but the post-stream helper rescanned the response and promoted a bare local filesystem path into a real upload.Environment
gateway/run.pyhermes updateSteps to Reproduce
MEDIA:reference to an existing image/file, e.g. tool output or searched content containing something like/absolute/path/to/file.png.Expected Behavior
Actual Behavior
The gateway can attach a file after streaming the text reply.
In my case:
files_upload_v2ran afterward,Representative log pattern:
In a later reproduction, the gateway also logged:
Root Cause
GatewayRunner._deliver_media_from_response()rescans the response after streaming and currently mixes two behaviors:extract_media(response))extract_local_files(cleaned))The problematic flow was:
That
extract_local_files(cleaned)step lets bare local filesystem paths found in post-processed response text become real attachments, even when the final visible reply did not intentionally request upload.This is especially dangerous in streaming mode because:
Local Fix That Worked
I patched
gateway/run.pyso the post-stream helper stays explicit-only:MEDIA:extraction,Specifically, I removed
extract_local_files(cleaned)and the follow-on post-stream send logic for non-explicit local files from_deliver_media_from_response().Conceptually:
and removed the later block that sent
non_image_localviasend_video()/send_document().Regression Test Added
I added a focused regression test file:
tests/gateway/test_post_stream_media_delivery.pyCoverage:
MEDIA:directives still workTargeted test results on my checkout:
tests/gateway/test_post_stream_media_delivery.py: 2 passedtests/gateway/test_extract_local_files.py: 37 passedtests/gateway/test_run_progress_topics.py: 63 passedWhy This Matters
This is more than log noise:
Related Issue
This seems related to #16434, but not identical.
MEDIA:examples in code/grep output are treated as attachmentsMEDIA:in the final sent reply, but post-stream bare-path extraction still causes an attachment uploadA robust fix may want both:
MEDIA:extraction semantics