Symptom
A channel bot (bot-A) with a message-coalesce window configured (message_coalesce_window_ms = 3000), routed to an agent (agent-A) on a text-only model (DeepSeek, supports_vision = false), never receives a description of inbound photos. The agent only sees the redacted placeholder [image omitted: model has no vision support] (the #6010 text-only redaction) and replies that it cannot see the image.
An otherwise identical bot without a coalesce window, same model, does get a Groq description and can answer about the image. The only differing variable is message_coalesce_window_ms.
[media] image_description = true is set globally; the Groq provider/key work (verified by calling the vision model directly with the same image — it returns a correct description).
Root cause (read from current source, tag v2026.6.22-beta.22)
Inbound dispatch forks on the debounce window in BridgeManager::start_adapter (crates/librefang-channels/src/bridge.rs:1435-1438):
- Fast path (
debounce_ms == 0) → dispatch_message(). Its image branch downloads the photo, builds an ImageFile block, and calls the description step:
// crates/librefang-channels/src/bridge.rs:3959
if let Some(desc_block) = maybe_describe_inbound_image(handle, Some(saved)).await {
final_blocks.insert(0, desc_block); // prepend Groq description
}
// ...
// crates/librefang-channels/src/bridge.rs:3966
dispatch_with_blocks(final_blocks, message, /* ... */).await; // shared tail
- Debounce path (
debounce_ms > 0) → pre-downloads images into image_blocks and dispatches via flush_debounced() → dispatch_with_blocks() (bridge.rs:1223). This branch builds the image blocks itself and never calls maybe_describe_inbound_image. dispatch_with_blocks() (bridge.rs:6245) contains no description call in its entire body.
So maybe_describe_inbound_image (defined bridge.rs:5453) is reachable only from dispatch_message. The [media] image_description flag is consequently only ever consulted on the fast path (the flag check lives in describe_inbound_image at crates/librefang-api/src/channel_bridge.rs, reached only via maybe_describe_inbound_image).
Why it surfaces as it does
For a text-only model the un-described image block is then redacted to a placeholder by the #6010 vision-gate (redact_images_for_text_only, crates/librefang-runtime/src/agent_loop/mod.rs), so there is no HTTP 400 / crash — the failure is silent: the agent simply never receives any image content or description. Vision-capable models are unaffected (they would see the raw image either way). The bug is therefore invisible except as "the bot ignores photos" on text-only + coalesce setups.
Proposed fix
The two paths already converge on the shared tail dispatch_with_blocks(). The divergence is only that the content→blocks conversion (which includes description) is duplicated, and the debounced copy omits the description step.
- Minimal: in
flush_debounced (where the image blocks are assembled, around bridge.rs:1073-1083), mirror the ImageFile-extraction + maybe_describe_inbound_image prepend from bridge.rs:3950-3963 before calling dispatch_with_blocks.
- Proper (recommended): factor the content→blocks conversion including
maybe_describe_inbound_image into a single shared helper that both the immediate and the debounced paths call before the common dispatch_with_blocks. The fork should be only "coalesce or not" at ingest; all downstream media handling (description, future transcription/OCR) should live in one path so a parallel branch cannot drift out of parity again.
Setup (for reference)
- v2026.6.22-beta.22
- Pattern: sidecar/channel bot with
message_coalesce_window_ms > 0, default agent on a text-only model (supports_vision = false), [media] image_description = true.
- Repro: send a photo to a coalesced bot vs. a non-coalesced bot on the same model — only the non-coalesced one is described.
- Workaround: remove
message_coalesce_window_ms for affected bots (forces the fast path), at the cost of losing message coalescing.
Happy to test a patch.
Refs: #6010 (text-only image redaction — closed; this is the adjacent description gap on the debounce path)
Symptom
A channel bot (
bot-A) with a message-coalesce window configured (message_coalesce_window_ms = 3000), routed to an agent (agent-A) on a text-only model (DeepSeek,supports_vision = false), never receives a description of inbound photos. The agent only sees the redacted placeholder[image omitted: model has no vision support](the #6010 text-only redaction) and replies that it cannot see the image.An otherwise identical bot without a coalesce window, same model, does get a Groq description and can answer about the image. The only differing variable is
message_coalesce_window_ms.[media] image_description = trueis set globally; the Groq provider/key work (verified by calling the vision model directly with the same image — it returns a correct description).Root cause (read from current source, tag v2026.6.22-beta.22)
Inbound dispatch forks on the debounce window in
BridgeManager::start_adapter(crates/librefang-channels/src/bridge.rs:1435-1438):debounce_ms == 0) →dispatch_message(). Its image branch downloads the photo, builds anImageFileblock, and calls the description step:debounce_ms > 0) → pre-downloads images intoimage_blocksand dispatches viaflush_debounced()→dispatch_with_blocks()(bridge.rs:1223). This branch builds the image blocks itself and never callsmaybe_describe_inbound_image.dispatch_with_blocks()(bridge.rs:6245) contains no description call in its entire body.So
maybe_describe_inbound_image(definedbridge.rs:5453) is reachable only fromdispatch_message. The[media] image_descriptionflag is consequently only ever consulted on the fast path (the flag check lives indescribe_inbound_imageatcrates/librefang-api/src/channel_bridge.rs, reached only viamaybe_describe_inbound_image).Why it surfaces as it does
For a text-only model the un-described image block is then redacted to a placeholder by the #6010 vision-gate (
redact_images_for_text_only,crates/librefang-runtime/src/agent_loop/mod.rs), so there is no HTTP 400 / crash — the failure is silent: the agent simply never receives any image content or description. Vision-capable models are unaffected (they would see the raw image either way). The bug is therefore invisible except as "the bot ignores photos" on text-only + coalesce setups.Proposed fix
The two paths already converge on the shared tail
dispatch_with_blocks(). The divergence is only that the content→blocks conversion (which includes description) is duplicated, and the debounced copy omits the description step.flush_debounced(where the imageblocksare assembled, aroundbridge.rs:1073-1083), mirror theImageFile-extraction +maybe_describe_inbound_imageprepend frombridge.rs:3950-3963before callingdispatch_with_blocks.maybe_describe_inbound_imageinto a single shared helper that both the immediate and the debounced paths call before the commondispatch_with_blocks. The fork should be only "coalesce or not" at ingest; all downstream media handling (description, future transcription/OCR) should live in one path so a parallel branch cannot drift out of parity again.Setup (for reference)
message_coalesce_window_ms > 0, default agent on a text-only model (supports_vision = false),[media] image_description = true.message_coalesce_window_msfor affected bots (forces the fast path), at the cost of losing message coalescing.Happy to test a patch.
Refs: #6010 (text-only image redaction — closed; this is the adjacent description gap on the debounce path)