Skip to content

fix(bluebubbles): remove updated-message from webhook subscription to prevent duplicate processing - #34378

Open
liuhao1024 wants to merge 2 commits into
NousResearch:mainfrom
liuhao1024:fix/bluebubbles-webhook-duplicate-messages
Open

fix(bluebubbles): remove updated-message from webhook subscription to prevent duplicate processing#34378
liuhao1024 wants to merge 2 commits into
NousResearch:mainfrom
liuhao1024:fix/bluebubbles-webhook-duplicate-messages

Conversation

@liuhao1024

@liuhao1024 liuhao1024 commented May 29, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

The BlueBubbles adapter subscribes to both new-message and updated-message webhook events. The updated-message event fires for delivery receipts, read state changes, and attachment finalization — so every inbound iMessage triggers two handle_message invocations.

Worse: the two events deliver slightly different chatGuid formats (any;-;+47… and +47…), which create separate session keys, so session-level dedup doesn't catch the duplicate.

Net result: every iMessage to the bot is processed twice, producing two replies.

Related Issue

Fixes #34372

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)

Changes Made

  • See commit messages for detailed changes

How to Test

  1. Run pytest tests/ -q — all tests should pass
  2. Verify the specific scenario described above is resolved

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run pytest tests/ -q and all tests pass
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: macOS 26.4.1

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — or N/A
  • I've updated cli-config.yaml.example if I added/changed config keys — or N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture and workflows — or N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — or N/A

Code Intelligence

  • Analyzed: gateway/platforms/bluebubbles.py_MESSAGE_EVENTS (2 references), _register_webhook (called during adapter startup), _handle_webhook (inbound webhook handler)
  • Blast radius: LOW — change is scoped to BlueBubbles platform adapter only; no cross-module impact
  • Related patterns: other platform adapters (telegram, discord) use similar webhook event filtering

Fixes #34372

… prevent duplicate processing

The BlueBubbles adapter subscribed to both 'new-message' and 'updated-message'
webhook events. The 'updated-message' event fires for delivery receipts, read
state changes, and attachment finalization — causing every inbound iMessage to
trigger two handle_message invocations with different chatGuid formats
(any;-;+47... vs +47...), which bypasses session-level dedup.

Fix: remove 'updated-message' from both _MESSAGE_EVENTS and the webhook
registration payload, so only genuine new messages are processed.

Fixes NousResearch#34372
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/gateway Gateway runner, session dispatch, delivery labels May 29, 2026
@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:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 13, 2026
JoshHobbs added a commit to JoshHobbs/hermes-agent that referenced this pull request Jul 18, 2026
BlueBubbles surfaces a single 1:1 conversation under more than one chat_id,
and build_session_key used the raw value, so one thread split across several
session keys:

  1. The adapter sets `session_chat_id = chat_guid or chat_identifier`
     (gateway/platforms/bluebubbles.py), so a webhook carrying no chat GUID
     falls back to the bare handle. The two forms key differently:
     `any;-;+1555…` vs `+1555…`.
  2. The GUID form recorded for one conversation is not stable over time. On
     the deployment this was found on, sessions carry `iMessage;-;+1555…` from
     May and `any;-;+1555…` since July, while the server today reports exactly
     one chat for that handle (`any;-;+1555…`, chatIdentifier `+1555…`) and
     uses the `any` prefix for every chat it knows about. Whatever drove that
     change server-side, the routing key should not depend on it.

The usual report of this is duplicate replies (NousResearch#30708, NousResearch#34372): two chat-id
variants defeat the in-flight guard, so a message gets answered twice. The
split has a second and worse consequence that has not been reported. Each
variant is a separate SessionEntry with its own updated_at, so a variant that
has not been messaged recently goes stale while the conversation continues
under another. When a webhook eventually routes to the stale variant,
_should_reset() finds it idle and clears an actively-used conversation.
Observed in production: a thread whose live session held 298 messages was
reset because a GUID-less webhook landed on a sibling key last touched 20 days
earlier. The notice reads "inactive for 3h" because it renders
policy.idle_minutes rather than measured elapsed time, so it does not point at
the real cause.

Canonicalize the DM chat_id the way WhatsApp already canonicalizes JID/LID
aliases: unwrap the `<service>;-;` prefix so every form of one conversation
maps to the bare handle. Group GUIDs use `;+;` and carry an opaque chat id
rather than a participant handle, so they are returned untouched, as is every
other platform. BlueBubbles needs no group-participant equivalent of the
WhatsApp fix: the adapter already sets user_id from handle.address, which is
a bare handle.

Where a deployment does have distinct iMessage and SMS chats for the same
handle, those now share one session key. That is intended — one human, one
agent conversation — and replies to an inbound message are unaffected, since
they route on the live event's source.chat_id rather than on the key.

Existing sessions are not orphaned. Canonicalization rewrites only the routing
key, never source.chat_id, so when the exact-key lookup misses after upgrade,
find_latest_gateway_session_for_peer's peer-tuple fallback still matches the
stored row on (source, user_id, chat_id, chat_type, thread_id) and adopts the
transcript under the new key. The regression test drives build_session_key
rather than hardcoding the key, so it fails both if the canonicalization is
dropped and if source.chat_id is ever canonicalized too.

This is the session-key half of NousResearch#30708, complementary to the open adapter-side
PRs (NousResearch#45717, NousResearch#34378, NousResearch#18395, NousResearch#19976, NousResearch#27985) that suppress the duplicate-event
trigger. Those do not make the key stable on their own: the form drift in (2)
puts one conversation under two keys with no duplicate event involved, so the
reset stays reachable with any of them merged.
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 P2 Medium — degraded but workaround exists sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform 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.

BlueBubbles: webhook auto-registration includes 'updated-message', causing every iMessage to be processed twice (with different chat-id variants)

3 participants