[GH-ISSUE #264] bug: messaging bridge drops final replies on send-only platforms (WhatsApp Cloud API) #177

Open
opened 2026-10-18 14:03:35 +00:00 by bsakaguchi · 2 comments
Owner

Originally created by @1-kabir on GitHub (Sep 5, 2026).
Original GitHub issue: https://github.com/open-webui/computer/issues/264

Bug description

When a bot is configured on a platform whose API cannot edit sent messages (WhatsApp via the Meta Cloud API), the messaging bridge processes inbound messages and runs the agent task correctly, but the final reply is never delivered. The user only ever receives the initial "Thinking..." placeholder message. The reply text exists in the cptr chat thread — only the platform-side delivery is missing, and the failure is logged at DEBUG level only, so it is invisible in normal operation.

Root cause

cptr/utils/bridge.py → _stream_loop() implements reply delivery with an edit-based model: it sends a placeholder message, streams progress via adapter.edit(), and delivers the final response via adapter.edit() on that placeholder.

WhatsApp Cloud API is send-only — the Messages API exposes exactly one endpoint (POST /{Phone-Number-ID}/messages); sent messages cannot be edited (Meta's "edit messages" docs cover inbound user-edit webhook events only). WhatsAppAdapter.edit() is therefore a no-op. Since the placeholder send always succeeds, the send-based fallback branch (reachable only when there is no platform_msg_id) never runs, so the final edit silently drops every normal-length reply.

Two related defects in the same code path:

  1. Multi-chunk replies are truncated on all edit-fallback platforms. The overflow-delivery branch ends its chunk loop with an unconditional break, so everything after the first chunk is silently discarded.
  2. The empty-reply branch ("task complete, no text output") calls edit() directly, which is also a no-op on send-only platforms.
  3. Typing indicator sends HTTP 400 on every call. WhatsAppAdapter.send_typing() posts a hardcoded fake message_id; the Cloud API requires the real inbound wamid from the messages webhook for the read-receipt/typing-indicator endpoint. Because the bridge calls send_typing() each poll iteration, every agent run produces a burst of 400s (exception swallowed).

To Reproduce

  1. Create a bot with platform: "whatsapp" and a valid access_token|phone_number_id token, with an allowed sender.
  2. Message the bot's WhatsApp number from that sender.
  3. Observe: the "Thinking..." placeholder arrives; the final agent reply never does (any reply under the chunk limit). The reply text is present in the cptr chat thread.
  4. Optionally watch the server logs: repeated 400s from the typing-indicator endpoint during the run.

Expected behavior

  • Platforms that cannot edit messages should receive the final reply via send() (in chunks), with no placeholder message (it could never be updated in place).
  • The WhatsApp typing indicator should use the documented payload: POST /{phone_number_id}/messages with {"messaging_product": "whatsapp", "status": "read", "message_id": "<inbound wamid>", "typing_indicator": {"type": "text"}} (requires Graph API ≥ v23.0; the indicator auto-dismisses after 25 seconds or on reply, so periodic refresh is intended usage). This requires tracking the last inbound message.id from the webhook payload.
  • Failed final deliveries should be logged at WARNING+ with the exception, not DEBUG.

Suggested approach

  • Add a capability flag on BaseAdapter (e.g. supports_edit, default True; WhatsAppAdapter sets False).
  • In _dispatch_task: skip the placeholder when the adapter cannot edit.
  • In _stream_loop: when the adapter cannot edit, skip streaming edits entirely, refresh the typing indicator each poll, and deliver the final reply via adapter.send() chunks (also fixing the overflow break and the empty-reply branch).
  • If a final edit fails on an edit-capable platform, fall back to send() instead of dropping the reply.
  • Adapter: bump API_BASE to v23.0 (first GA version with typing indicators), implement the real typing indicator, track the last inbound message id from the webhook, and surface interactive button/list replies (currently dropped silently) as plain text.

Additional context

  • The same fix also applies the observability correction: a dropped final reply is currently a DEBUG-level event; it should be impossible to lose a reply silently.
  • Implemented and tested on a fork (capability flag + send-based delivery + real typing indicator + webhook hardening); happy to share implementation notes if useful.

Environment

  • cptr 0.9.21 (latest main at time of writing)
  • WhatsApp Cloud API (Meta), Graph API v21.0 as shipped in the adapter
Originally created by @1-kabir on GitHub (Sep 5, 2026). Original GitHub issue: https://github.com/open-webui/computer/issues/264 ### Bug description When a bot is configured on a platform whose API cannot edit sent messages (WhatsApp via the Meta Cloud API), the messaging bridge processes inbound messages and runs the agent task correctly, but the **final reply is never delivered**. The user only ever receives the initial "Thinking..." placeholder message. The reply text exists in the cptr chat thread — only the platform-side delivery is missing, and the failure is logged at DEBUG level only, so it is invisible in normal operation. ### Root cause `cptr/utils/bridge.py` → `_stream_loop()` implements reply delivery with an edit-based model: it sends a placeholder message, streams progress via `adapter.edit()`, and delivers the final response via `adapter.edit()` on that placeholder. WhatsApp Cloud API is send-only — the Messages API exposes exactly one endpoint (`POST /{Phone-Number-ID}/messages`); sent messages cannot be edited (Meta's "edit messages" docs cover inbound user-edit webhook events only). `WhatsAppAdapter.edit()` is therefore a no-op. Since the placeholder send always succeeds, the send-based fallback branch (reachable only when there is no `platform_msg_id`) never runs, so the final edit silently drops every normal-length reply. Two related defects in the same code path: 1. **Multi-chunk replies are truncated on all edit-fallback platforms.** The overflow-delivery branch ends its chunk loop with an unconditional `break`, so everything after the first chunk is silently discarded. 2. **The empty-reply branch** ("task complete, no text output") calls `edit()` directly, which is also a no-op on send-only platforms. 3. **Typing indicator sends HTTP 400 on every call.** `WhatsAppAdapter.send_typing()` posts a hardcoded fake `message_id`; the Cloud API requires the real inbound `wamid` from the messages webhook for the read-receipt/typing-indicator endpoint. Because the bridge calls `send_typing()` each poll iteration, every agent run produces a burst of 400s (exception swallowed). ### To Reproduce 1. Create a bot with `platform: "whatsapp"` and a valid `access_token|phone_number_id` token, with an allowed sender. 2. Message the bot's WhatsApp number from that sender. 3. Observe: the "Thinking..." placeholder arrives; the final agent reply never does (any reply under the chunk limit). The reply text is present in the cptr chat thread. 4. Optionally watch the server logs: repeated 400s from the typing-indicator endpoint during the run. ### Expected behavior - Platforms that cannot edit messages should receive the final reply via `send()` (in chunks), with no placeholder message (it could never be updated in place). - The WhatsApp typing indicator should use the documented payload: `POST /{phone_number_id}/messages` with `{"messaging_product": "whatsapp", "status": "read", "message_id": "<inbound wamid>", "typing_indicator": {"type": "text"}}` (requires Graph API ≥ v23.0; the indicator auto-dismisses after 25 seconds or on reply, so periodic refresh is intended usage). This requires tracking the last inbound `message.id` from the webhook payload. - Failed final deliveries should be logged at WARNING+ with the exception, not DEBUG. ### Suggested approach - Add a capability flag on `BaseAdapter` (e.g. `supports_edit`, default `True`; `WhatsAppAdapter` sets `False`). - In `_dispatch_task`: skip the placeholder when the adapter cannot edit. - In `_stream_loop`: when the adapter cannot edit, skip streaming edits entirely, refresh the typing indicator each poll, and deliver the final reply via `adapter.send()` chunks (also fixing the overflow `break` and the empty-reply branch). - If a final edit fails on an edit-capable platform, fall back to `send()` instead of dropping the reply. - Adapter: bump `API_BASE` to v23.0 (first GA version with typing indicators), implement the real typing indicator, track the last inbound message id from the webhook, and surface interactive button/list replies (currently dropped silently) as plain text. ### Additional context - The same fix also applies the observability correction: a dropped final reply is currently a DEBUG-level event; it should be impossible to lose a reply silently. - Implemented and tested on a fork (capability flag + send-based delivery + real typing indicator + webhook hardening); happy to share implementation notes if useful. ### Environment - cptr 0.9.21 (latest main at time of writing) - WhatsApp Cloud API (Meta), Graph API v21.0 as shipped in the adapter
Author
Owner

@owui-terminator[bot] commented on GitHub (Sep 5, 2026):

🔍 Related Issues Found

I found some existing issues that might be related. Please check if any of these are duplicates or contain helpful solutions:

  1. 🟢 #112 bug: Response output repeats infinitely due to missing stream termination
    This issue is in the same streaming/delivery path and reports incorrect end-of-response handling. While it is about repeated output rather than WhatsApp delivery, it is the closest related issue found around stream termination in the bridge.
    by JKGeovision

💡 If your issue is a duplicate, please close it and add any additional details to the existing issue instead.

This comment was generated automatically. React with 👍 if helpful, 👎 if not.

<!-- gh-comment-id:5552078829 --> @owui-terminator[bot] commented on GitHub (Sep 5, 2026): <!-- terminator-bot:related-issues-reply --> 🔍 **Related Issues Found** I found some existing issues that might be related. Please check if any of these are duplicates or contain helpful solutions: 1. 🟢 [#112](https://github.com/open-webui/computer/issues/112) **bug: Response output repeats infinitely due to missing stream termination** *This issue is in the same streaming/delivery path and reports incorrect end-of-response handling. While it is about repeated output rather than WhatsApp delivery, it is the closest related issue found around stream termination in the bridge.* *by JKGeovision* --- 💡 If your issue is a duplicate, please close it and add any additional details to the existing issue instead. *This comment was generated automatically.* React with 👍 if helpful, 👎 if not.
Author
Owner

@1-kabir commented on GitHub (Sep 5, 2026):

Update: this is implemented and tested on a fork (1-kabir/computer, branch fix/whatsapp-bridge-delivery, merged to its main) in case the approach is useful as a reference:

  • BaseAdapter.supports_edit capability flag; send-only platforms skip the placeholder and streaming edits entirely
  • final reply delivered via send() chunks (also fixes the multi-chunk overflow break and the empty-reply branch)
  • edit-failure on edit-capable platforms falls back to send() instead of dropping the reply
  • real typing indicator per the v23+ docs (real inbound wamid + typing_indicator: {"type": "text"})
  • delivery failures logged with logger.exception instead of DEBUG-only

15 tests covering the adapter and bridge loop ship with it. No PR will be opened against this repo per its contribution policy — sharing in case the maintainers want to implement it here.

<!-- gh-comment-id:5552538548 --> @1-kabir commented on GitHub (Sep 5, 2026): Update: this is implemented and tested on a fork ([1-kabir/computer](https://github.com/1-kabir/computer), branch `fix/whatsapp-bridge-delivery`, merged to its `main`) in case the approach is useful as a reference: - `BaseAdapter.supports_edit` capability flag; send-only platforms skip the placeholder and streaming edits entirely - final reply delivered via `send()` chunks (also fixes the multi-chunk overflow `break` and the empty-reply branch) - edit-failure on edit-capable platforms falls back to `send()` instead of dropping the reply - real typing indicator per the v23+ docs (real inbound `wamid` + `typing_indicator: {"type": "text"}`) - delivery failures logged with `logger.exception` instead of DEBUG-only 15 tests covering the adapter and bridge loop ship with it. No PR will be opened against this repo per its contribution policy — sharing in case the maintainers want to implement it here.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
open-webui/computer#177
No description provided.