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 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:
feishu-provider.js
telegram-provider.js
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.
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.
There was a second detail that mattered.
CliGate runs locally on localhost
, and assistant-generated images can exist as:
data:
URLlocalhost
URLThat 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:
image_key
, send image messagesendPhoto
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 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:
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.
When an agent sends files, screenshots, or generated images into chat, do not debug only the provider.
Trace the whole delivery path:
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?