Skip to content

bluebubbles: de-duplicate re-delivered webhooks to stop double replies - #68726

Open
carljborg wants to merge 1 commit into
NousResearch:mainfrom
carljborg:fix/bluebubbles-duplicate-replies
Open

bluebubbles: de-duplicate re-delivered webhooks to stop double replies#68726
carljborg wants to merge 1 commit into
NousResearch:mainfrom
carljborg:fix/bluebubbles-duplicate-replies

Conversation

@carljborg

Copy link
Copy Markdown
Contributor

What

De-duplicate re-delivered BlueBubbles webhooks so the agent replies to each inbound iMessage exactly once.

Fixes #68718.

Why

BlueBubbles delivers a new-message webhook and one or more updated-message webhooks (delivery / read / edit state changes) for the same message. _handle_webhook processes every event in _MESSAGE_EVENTS without 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 the new-message + updated-message subscription intact (nothing that only surfaces via updated-message is lost) while making the handler idempotent per message.

  • The check is placed after the is_from_me and tapback filters, so those cheap skips still short-circuit first.
  • It keys on the same stable record["guid"] the adapter already relies on elsewhere.
  • No new dependencies; OrderedDict is already imported and used in this module.

An alternative one-line fix is to drop updated-message from 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.py passes.
  • Root cause confirmed on a live self-hosted deployment: the duplicate replies track exactly with the updated-message re-delivery, and suppressing that re-delivery removes them.

Note (separate, not in this PR)

send_typing / stop_typing also gate on a cached self._helper_connected snapshot, which BlueBubbles can report as false during 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.

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
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/gateway Gateway runner, session dispatch, delivery sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages needs-decision Awaiting maintainer decision before any implementation labels Jul 21, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Related to #45378 and #32313. This patch deduplicates by GUID, #45378 uses a content/media-aware fingerprint to retain attachment updates, and #32313 removes updated-message subscriptions. Please choose the desired BlueBubbles update-delivery contract.

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-941 runs before attachment handling at :950-979. A same-GUID updated-message carrying an attachment is acknowledged as a duplicate before its attachment can be downloaded or included in MessageEvent.
  • Proposed line :935 does not include the adapter's existing record["id"] fallback used for MessageEvent.message_id on 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"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@teknium1 teknium1 added the sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users label Jul 30, 2026
@GottZ

GottZ commented Aug 3, 2026

Copy link
Copy Markdown

This was generated by AI during triage.

Summary

Twenty-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 consolidation

Author 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 graph

flowchart 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"
Loading

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery needs-decision Awaiting maintainer decision before any implementation P2 Medium — degraded but workaround exists sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BlueBubbles adapter: duplicate iMessage replies — webhook subscribes to updated-message and handler doesn't dedupe by GUID

4 participants