fix(bluebubbles): allow updated-message events with attachments through webhook filter#63983
Conversation
…gh webhook filter BlueBubbles fires new-message first, then updated-message ~2-3s later once attachment data is fully available. The webhook handler unconditionally discards updated-message events unless they carry a reaction, silently dropping all inbound images, PDFs, and files that arrive via the update path. Check for attachments in the updated-message payload before discarding. When attachments are present, let the event through for normal processing. Closes openclaw#4848
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3db31047e3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| !reaction && | ||
| !updatedMessageHasAttachments |
There was a problem hiding this comment.
Prevent duplicate turns from delayed attachment updates
Allowing updated-message events with attachments through this filter causes the same user message to be processed twice when BlueBubbles sends new-message first and the attachment-bearing update 2–3 seconds later (as noted in the comment). The default inbound debounce window is only 500ms in extensions/bluebubbles/src/monitor-debounce.ts, and processMessage has no inbound message-id dedupe path, so both events can trigger separate agent replies for a single message with media.
Useful? React with 👍 / 👎.
| const attachmentsFromRoot = Array.isArray(payloadRecord?.attachments) | ||
| ? (payloadRecord.attachments as unknown[]) | ||
| : []; |
There was a problem hiding this comment.
Require parseable message data before allowing root attachments
This branch explicitly treats payload.attachments as a signal to accept updated-message, but normalizeWebhookMessage only extracts message content from payload.message, payload.data, payload.payload, or payload.event (extractMessagePayload in extensions/bluebubbles/src/monitor-normalize.ts). For older payloads that expose attachments at the root without one of those message containers, this change turns a previously ignored event into a 400 "invalid payload" response, so attachments are still dropped and webhook retries can be triggered.
Useful? React with 👍 / 👎.
Greptile SummaryThis PR fixes a real gap:
Confidence Score: 4/5Safe to merge if BlueBubbles only fires updated-message for attachment-only sends; risky if it also fires for text+attachment sends due to missing messageId deduplication in processMessage. One P1 concern: the 500 ms debounce window is shorter than the ~2–3 s updated-message delay, and processMessage has no messageId-based deduplication, so text+attachment sends could produce duplicate message delivery. The fix is a clear improvement for attachment-only messages, but confirmation of BlueBubbles sequencing behavior or a deduplication guard would make it safe to merge without reservation. extensions/bluebubbles/src/monitor.ts — specifically the updated-message pass-through path and whether processMessage needs a short-lived seen-set guard by messageId.
|
| const updatedMessageHasAttachments = | ||
| eventType === "updated-message" && hasAttachments.length > 0; | ||
| if ( | ||
| (eventType === "updated-message" || | ||
| eventType === "message-reaction" || | ||
| eventType === "reaction") && | ||
| !reaction | ||
| !reaction && | ||
| !updatedMessageHasAttachments |
There was a problem hiding this comment.
Possible duplicate delivery for text-plus-attachment messages
When a sender sends a message that has both text and an attachment, BlueBubbles fires new-message (text only, attachment not yet available) and then updated-message ~2–3 s later (same text, full attachment). The fix lets the updated-message through, which is the right call for attachment-only sends. However, for text+attachment, normalizeWebhookMessage will extract the same non-empty text from both events, producing a non-empty rawBody each time. processMessage has no deduplication by messageId on the inbound path, so the same text body would be dispatched to the session twice — first without the attachment placeholder and then with it.
This is the crucial code path:
new-message → rawBody = "some text" → enqueued → delivered
updated-message (2–3 s later, outside 500 ms debounce window)
→ rawBody = "some text" → enqueued → delivered again
If BlueBubbles guarantees updated-message only fires for attachment-only sends (no accompanying text), this is fine. If it fires for text+attachment, users will see a duplicate line.
Consider checking the messageId against a short-lived seen-set before enqueueing (deduplication window of ~5 s would cover the updated-message delay), or restricting the pass-through to cases where text is empty so only attachment-only events are affected.
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/bluebubbles/src/monitor.ts
Line: 266-273
Comment:
**Possible duplicate delivery for text-plus-attachment messages**
When a sender sends a message that has both text and an attachment, BlueBubbles fires `new-message` (text only, attachment not yet available) and then `updated-message` ~2–3 s later (same text, full attachment). The fix lets the `updated-message` through, which is the right call for attachment-only sends. However, for text+attachment, `normalizeWebhookMessage` will extract the same non-empty text from both events, producing a non-empty `rawBody` each time. `processMessage` has no deduplication by `messageId` on the inbound path, so the same text body would be dispatched to the session twice — first without the attachment placeholder and then with it.
This is the crucial code path:
```
new-message → rawBody = "some text" → enqueued → delivered
updated-message (2–3 s later, outside 500 ms debounce window)
→ rawBody = "some text" → enqueued → delivered again
```
If BlueBubbles guarantees `updated-message` only fires for attachment-only sends (no accompanying text), this is fine. If it fires for text+attachment, users will see a duplicate line.
Consider checking the `messageId` against a short-lived seen-set before enqueueing (deduplication window of ~5 s would cover the `updated-message` delay), or restricting the pass-through to cases where `text` is empty so only attachment-only events are affected.
How can I resolve this? If you propose a fix, please make it concise.| const hasAttachments = | ||
| attachmentsFromData.length > 0 ? attachmentsFromData : attachmentsFromRoot; | ||
| const updatedMessageHasAttachments = | ||
| eventType === "updated-message" && hasAttachments.length > 0; |
There was a problem hiding this comment.
Misleading variable name and no-op alias
hasAttachments is named like a boolean but is assigned an array, which makes hasAttachments.length > 0 feel redundant on inspection. payloadRecord is also a pure alias of payload that adds no value. The full block can be simplified:
| const hasAttachments = | |
| attachmentsFromData.length > 0 ? attachmentsFromData : attachmentsFromRoot; | |
| const updatedMessageHasAttachments = | |
| eventType === "updated-message" && hasAttachments.length > 0; | |
| const dataRecord = (payload as Record<string, unknown> & { data?: Record<string, unknown> }).data; | |
| const attachmentsFromData = Array.isArray(dataRecord?.attachments) | |
| ? (dataRecord.attachments as unknown[]) | |
| : []; | |
| const attachmentsFromRoot = Array.isArray(payload.attachments) | |
| ? (payload.attachments as unknown[]) | |
| : []; | |
| const hasAttachments = attachmentsFromData.length > 0 || attachmentsFromRoot.length > 0; | |
| const updatedMessageHasAttachments = eventType === "updated-message" && hasAttachments; |
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/bluebubbles/src/monitor.ts
Line: 264-267
Comment:
**Misleading variable name and no-op alias**
`hasAttachments` is named like a boolean but is assigned an array, which makes `hasAttachments.length > 0` feel redundant on inspection. `payloadRecord` is also a pure alias of `payload` that adds no value. The full block can be simplified:
```suggestion
const dataRecord = (payload as Record<string, unknown> & { data?: Record<string, unknown> }).data;
const attachmentsFromData = Array.isArray(dataRecord?.attachments)
? (dataRecord.attachments as unknown[])
: [];
const attachmentsFromRoot = Array.isArray(payload.attachments)
? (payload.attachments as unknown[])
: [];
const hasAttachments = attachmentsFromData.length > 0 || attachmentsFromRoot.length > 0;
const updatedMessageHasAttachments = eventType === "updated-message" && hasAttachments;
```
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
|
Closing in favor of a rewrite that addresses review feedback: simplified variable naming, data-only attachment check (not root payload), and text stripping to prevent duplicate delivery. New PR incoming. |
Problem
BlueBubbles fires
new-messagefirst, thenupdated-message~2-3s later once attachment data is fully available. The webhook handler unconditionally discardsupdated-messageevents unless they carry a reaction, silently dropping all inbound images, PDFs, and files that arrive via the update path.Fix
Check for attachments in the
updated-messagepayload before discarding. When attachments are present, let the event through for normal message processing.Details
The attachment metadata lives in
payload.data.attachments(orpayload.attachmentsin older BB versions). The fix inspects both locations and only discardsupdated-messageevents that have neither a reaction nor attachments.Related: #4848