open-webui / open-webui/computer

bug: messaging bridge drops final replies on send-only platforms (WhatsApp Cloud API)

Open
#264 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
569
Forks
79
PR merge metrics
No merged PRs in 30d

Description

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

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in cptr/utils/bridge.py at _dispatch_task and _stream_loop, then inspect BaseAdapter and WhatsAppAdapter. Trace the webhook handling for the inbound message id and the adapter's typing-indicator path. Done means send-only platforms deliver complete final replies, empty replies and failures are handled visibly, typing indicators use the inbound id, and interactive replies are surfaced as plain text.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
api, backend, observability
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.