Skip to content

fix(bluebubbles): add GUID-based dedup to prevent duplicate replies - #22116

Open
rswafford1980 wants to merge 1 commit into
NousResearch:mainfrom
rswafford1980:fix/bluebubbles-webhook-dedup
Open

fix(bluebubbles): add GUID-based dedup to prevent duplicate replies#22116
rswafford1980 wants to merge 1 commit into
NousResearch:mainfrom
rswafford1980:fix/bluebubbles-webhook-dedup

Conversation

@rswafford1980

Copy link
Copy Markdown

Problem

BlueBubbles sends multiple webhook events per incoming message (5-10 POSTs in rapid succession as iMessage status updates fire: delivered, read, etc.). The _handle_webhook method in bluebubbles.py spawns asyncio.create_task(self.handle_message(event)) for every webhook hit.

Although handle_message() in base.py has an _active_sessions guard, the tasks are spawned synchronously but run asynchronously, creating a race window where multiple tasks pass the guard before the first one sets it. Net result: every iMessage gets 2+ duplicate replies from the agent.

Fix

Three changes in gateway/platforms/bluebubbles.py:

  1. Add import time to imports
  2. Add _seen_message_guids dict and _DEDUP_TTL = 30.0 to __init__
  3. Add GUID-based dedup check in _handle_webhook — before the handle_message spawn, checks if the message GUID was seen within the last 30 seconds. If so, returns ok immediately. Periodically prunes expired entries.

The dedup runs after the existing isFromMe and tapback filters, so those are unaffected.

Test Plan

  • Verify single reply per iMessage on macOS with BlueBubbles private API enabled
  • Verify dedup cache prunes stale entries (no unbounded growth)
  • Verify isFromMe and tapback messages still silently dropped

BlueBubbles sends multiple webhook events per incoming message
(status updates: delivered, read, etc.). The _handle_webhook method
spawns handle_message() for every webhook POST, creating a race
window where multiple tasks pass the _active_sessions guard before
the first one sets it.

Fix adds a 30-second GUID dedup cache at the webhook level, so
only the first event per message GUID triggers handle_message.
Subsequent events within the TTL window return ok immediately.
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/gateway Gateway runner, session dispatch, delivery duplicate This issue or pull request already exists labels May 8, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

Duplicate of #18395 which implements BlueBubbles webhook dedup by message GUID+chat. Also overlaps with the dedup component of #11654.

@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 inbound webhook duplication path.

Problems

  • gateway/platforms/bluebubbles.py:70 deliberately admits updated-message events. The GUID-only check added by this PR would drop every same-GUID update, not only identical webhook replays; it needs an event/update fingerprint that preserves meaningful lifecycle updates.
  • The new local cache duplicates gateway/platforms/helpers.py:27-71, where MessageDeduplicator already provides TTL pruning and a hard size bound.
  • The diff has no tests, despite an existing webhook suite in tests/gateway/test_bluebubbles.py. Regression coverage should exercise exact replay suppression and same-GUID lifecycle updates.

Suggested changes

  • Reuse MessageDeduplicator and key it with a replay fingerprint rather than GUID alone.
  • Add focused async webhook tests for replay, distinct GUIDs, and same-GUID update/attachment behavior.

This is an automated hermes-sweeper review.

@@ -129,6 +130,8 @@ def __init__(self, config: PlatformConfig):
self._private_api_enabled: Optional[bool] = None

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.

Please reuse gateway.platforms.helpers.MessageDeduplicator rather than adding a second local TTL map. The shared helper already enforces a maximum cache size in addition to TTL pruning.

self._seen_message_guids = {
k: v for k, v in self._seen_message_guids.items()
if now - v < self._DEDUP_TTL
}

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 suppresses every same-GUID event, but the adapter explicitly accepts updated-message events. Use a replay fingerprint that distinguishes exact retries from meaningful same-GUID lifecycle updates such as edits or attachment completion.

@teknium1 teknium1 added sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 13, 2026
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 duplicate This issue or pull request already exists P2 Medium — degraded but workaround exists sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants