The most confusing delivery bug is not when an image upload fails loudly.
It is when the assistant can clearly see the screenshot, tells you it is sending it, and the chat still gets no image.
That is the bug I ran into while working on CliGate, my local control plane for a resident assistant, channel workflows, desktop automation, and AI tool routing.
The screenshot existed.
The assistant had the artifact.
DingTalk could already receive images.
But Feishu and Telegram still behaved like the image had vanished somewhere between "I have it" and "I sent it."
The annoying part was that the providers were not the whole problem
The obvious place to look was the channel provider layer.
That made sense at first.
Feishu needs image bytes uploaded first so it can return an image_key. Telegram can accept a file upload through sendPhoto. So the initial assumption was simple:
- add image sending to
feishu-provider.js - add image sending to
telegram-provider.js - done
But that was only half the path.
The real outbound flow looked more like this:
assistant tool
-> delivery sender
-> provider
-> channel chat
And the bug was hiding earlier than I expected.
The image was getting stripped before the provider ever saw it
CliGate has an assistant tool called send_message_to_channel.
That tool decides whether a channel is image-capable before it forwards payloads into the delivery path.
The critical line was basically this:
const effectiveImages = imageSupported ? images : [];
That sounds harmless until your capability map is wrong.
In my case, the tool-level allowlist still treated only DingTalk as image-capable.
So even if I taught Feishu and Telegram providers how to upload and send images, the assistant tool was silently stripping the image array before the providers ever received it.
That is why this kind of bug is so misleading.
The model is not hallucinating the screenshot.
The provider is not necessarily broken.
The payload is being amputated one layer upstream.
Local-first delivery made the fix a little more specific
There was a second detail that mattered.
CliGate runs locally on localhost, and assistant-generated images can exist as:
- a local file path
- a
data:URL - an external URL
- a
localhostURL
That matters because Feishu and Telegram do not treat image inputs the same way.
Feishu wants uploaded bytes first, then an image_key.
Telegram can fetch some public URLs remotely, but a localhost URL is useless to Telegram's servers.
So "just pass the URL through" is not a reliable local-first strategy.
The safer rule became:
resolve image bytes locally
-> upload from CliGate itself
-> send the provider-specific image payload
That means:
- Feishu: upload bytes, get
image_key, send image message - Telegram: upload bytes through multipart
sendPhoto
In other words, the fix was not only "mark these channels as image-capable."
It was also "make the provider implementation match local-first reality."
The real fix was end-to-end alignment
The lesson was that channel capability is not one boolean in one file.
It is an agreement across layers.
For this bug, the useful fix had three parts:
- expand the assistant tool's image-capable channel set beyond DingTalk
- make Feishu resolve image bytes locally and upload them through its image API
- make Telegram upload bytes directly instead of assuming the remote service can fetch the URL
Once those lined up, the behavior stopped being weird.
If the assistant has an image artifact, the delivery layer now keeps it intact long enough for the right provider to do the right thing.
That sounds small, but it changes the user experience a lot.
A screenshot is usually the proof that the task really happened.
If the text says "I sent the screenshot" but the image never arrives, trust drops immediately.
The rule I am keeping
When an agent sends files, screenshots, or generated images into chat, do not debug only the provider.
Trace the whole delivery path:
- did the assistant keep the artifact handle?
- did the tool layer preserve the image payload?
- does the provider support the right upload shape for a local-first app?
- is the channel receiving bytes, or just a URL it cannot actually reach?
That is now one of my standing checks for CliGate.
The bug looked like "Feishu and Telegram cannot send images."
The real problem was subtler:
the assistant pipeline and the provider capabilities had drifted apart.
The project is open source here: CliGate on GitHub.
If you are building local agents with chat delivery, where would you put the capability check: in the tool layer, the provider, or both?
Top comments (0)