bluebubbles: de-duplicate re-delivered webhooks to stop double replies - #68726
bluebubbles: de-duplicate re-delivered webhooks to stop double replies#68726carljborg wants to merge 1 commit into
Conversation
BlueBubbles fires a new-message webhook and one or more updated-message webhooks (delivery/read/edit state) for the same message. _handle_webhook processed every message event without deduplication, so a single inbound message was handled as multiple turns and the agent replied more than once. Track recently-seen message GUIDs (bounded OrderedDict) and process each message exactly once, keeping the updated-message subscription intact. Fixes NousResearch#68718
teknium1
left a comment
There was a problem hiding this comment.
Thanks for isolating the duplicate-dispatch path. The premise remains present on current main: BlueBubbles registers both events at gateway/platforms/bluebubbles.py:397-400, accepts updated-message at :94 / :926-929, and schedules every accepted message at :1061-1063.
Problems
- The proposed GUID check at
gateway/platforms/bluebubbles.py:935-941runs before attachment handling at:950-979. A same-GUIDupdated-messagecarrying an attachment is acknowledged as a duplicate before its attachment can be downloaded or included inMessageEvent. - Proposed line
:935does not include the adapter's existingrecord["id"]fallback used forMessageEvent.message_idon current main at:1049-1053. - The diff adds no regression tests for duplicate deliveries, attachment updates, or identifier fallback.
Suggested changes
- Please settle the update-delivery contract noted in the existing PR discussion. If retaining
updated-message, deduplicate after media extraction with the same stable-ID fallback order and a content/media-aware key; otherwise change the subscription/receive contract consistently. - Add focused webhook tests covering exact duplicate delivery, a same-GUID attachment update, and an
id-only payload.
Automated hermes-sweeper review.
| # for the same message; without this a single inbound message is | ||
| # processed as multiple turns and the agent replies more than once. | ||
| # Key on the stable message GUID so each message is handled exactly once. | ||
| msg_guid = self._value(record.get("guid"), record.get("messageGuid")) |
There was a problem hiding this comment.
This claims the GUID before attachment processing. A same-GUID updated-message that adds media will return as a duplicate before the attachment loop runs, so the user loses that update. Please choose the update-delivery contract; if updates remain subscribed, dedupe after media extraction with a content/media-aware key.
| # for the same message; without this a single inbound message is | ||
| # processed as multiple turns and the agent replies more than once. | ||
| # Key on the stable message GUID so each message is handled exactly once. | ||
| msg_guid = self._value(record.get("guid"), record.get("messageGuid")) |
There was a problem hiding this comment.
The eventual MessageEvent.message_id also falls back to record["id"], but this cache does not. Use the same fallback order so accepted id-only payloads receive the same duplicate protection.
SummaryTwenty-four PRs address or reference this BlueBubbles cluster: they cover updated-message classification or suppression, replay deduplication, nested group identity, outbound DM routing, chat admission, mention/context controls, webhook credentials, transport, and session-key stability. The duplicate-reply root cause is approached either by removing updated-message delivery, filtering it before routing, or retaining it with GUID/content-aware classification; several broader PRs combine those choices with distinct routing or lifecycle fixes. Related pull requests
Duplicates#24229 and #56132 implement the same participant-fallback removal, with #56132 merged; #35606 is superseded by merged #37091; #38342 is the nested-group-routing subset incorporated into #38379; #8266, #32313, and #34378 share the new-message-only strategy, while #30996, #18395, #33337, #45378, #47262, and #68726 overlap on replay dedup but differ materially on lifecycle/media semantics. Suggested consolidationAuthor action: rebase #68726 onto main, or split out the part that can merge: if updated-message remains supported, replace GUID-only pre-attachment suppression with a bounded content/media-aware key using the full guid/messageGuid/id fallback and add handler-level duplicate, attachment-update, and id-only regressions, explicitly addressing its keep-open review. Keep #38379 open with its routing/order salvage path and #8275 open for distinct edit-history semantics; treat merged #56132 and #37091 as reference implementations, leave #24229/#35606 closed as superseded, and keep the other recorded best-fix PRs open until their cited contributor-review gaps are resolved rather than collapsing them into #68726. Complex graphflowchart TD
classDef open fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
classDef merged fill:#dcfce7,stroke:#15803d,color:#14532d
classDef closed fill:#e5e7eb,stroke:#6b7280,color:#1f2937
classDef unverified fill:#f3f4f6,stroke:#9ca3af,color:#374151
classDef best stroke-width:3px,stroke:#b45309
classDef target stroke-width:3px,stroke:#4338ca
I30708(["issue #30708 (open)"])
I33327(["issue #33327 (closed)"])
I34372(["issue #34372 (open)"])
I68718(["issue #68718 (open)"])
subgraph Dup30996 ["PRs duplicating each other"]
P30996["PR #30996 (closed)"]
P33337["PR #33337 (open)"]
P45378["PR #45378 (open)"]
P47262["PR #47262 (closed)"]
P68726["PR #68726 (open)"]
end
P68726 -.->|partial| I30708
P68726 -.->|partial| I33327
P68726 -.->|partial| I34372
P68726 -->|fixes| I68718
class I30708 open
class I33327 closed
class I34372 open
class I68718 open
class P30996 closed
class P33337 open
class P45378 open
class P47262 closed
class P68726 open
class P33337 best
class P68726 target
click I30708 "https://github.com/NousResearch/hermes-agent/issues/30708"
click I33327 "https://github.com/NousResearch/hermes-agent/issues/33327"
click I34372 "https://github.com/NousResearch/hermes-agent/issues/34372"
click I68718 "https://github.com/NousResearch/hermes-agent/issues/68718"
click P30996 "https://github.com/NousResearch/hermes-agent/pull/30996"
click P33337 "https://github.com/NousResearch/hermes-agent/pull/33337"
click P45378 "https://github.com/NousResearch/hermes-agent/pull/45378"
click P47262 "https://github.com/NousResearch/hermes-agent/pull/47262"
click P68726 "https://github.com/NousResearch/hermes-agent/pull/68726"
Graph: solid arrow = fixes / best fix, dashed arrow = partial or unverified (see edge label); boxed group = PRs duplicating each other; amber border = best fix; indigo border = target; gray node = closed (state tag in the node label). Cross-PR triage: Reviewed 24 pull requests and 8 issues in this complex. Each diff was read against this issue; Assessment working set: 426 kB of PR diffs, 80 kB of issue/PR text, 64 kB of discussion (100 comments), 98 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch. |
What
De-duplicate re-delivered BlueBubbles webhooks so the agent replies to each inbound iMessage exactly once.
Fixes #68718.
Why
BlueBubbles delivers a
new-messagewebhook and one or moreupdated-messagewebhooks (delivery / read / edit state changes) for the same message._handle_webhookprocesses every event in_MESSAGE_EVENTSwithout deduplication, so a single inbound message is handled as multiple turns and the agent sends duplicate replies.How
Track recently-seen inbound message GUIDs in a bounded
OrderedDict(cap 2048, FIFO eviction) and process each GUID only once. This keeps thenew-message+updated-messagesubscription intact (nothing that only surfaces viaupdated-messageis lost) while making the handler idempotent per message.is_from_meand tapback filters, so those cheap skips still short-circuit first.record["guid"]the adapter already relies on elsewhere.OrderedDictis already imported and used in this module.An alternative one-line fix is to drop
updated-messagefrom the webhook subscription in_register_webhook; the dedup approach here is preferred because it doesn't remove the subscription and is robust to the same message arriving under either event.Testing
python -m py_compile gateway/platforms/bluebubbles.pypasses.updated-messagere-delivery, and suppressing that re-delivery removes them.Note (separate, not in this PR)
send_typing/stop_typingalso gate on a cachedself._helper_connectedsnapshot, which BlueBubbles can report asfalseduring gateway startup even when the Private API typing endpoint already works — dropping typing indicators in that window. Happy to send that as a follow-up if useful.