fix(gateway): harden BlueBubbles delivery and acknowledgments - #18395
Open
benjaminsehl wants to merge 2 commits into
Open
fix(gateway): harden BlueBubbles delivery and acknowledgments#18395benjaminsehl wants to merge 2 commits into
benjaminsehl wants to merge 2 commits into
Conversation
benjaminsehl
force-pushed
the
fix/bluebubbles-webhook-dedupe
branch
from
May 1, 2026 16:09
5bc2429 to
d25ac89
Compare
This was referenced May 5, 2026
This was referenced May 23, 2026
13 tasks
teknium1
reviewed
Jul 12, 2026
teknium1
left a comment
Contributor
There was a problem hiding this comment.
Thanks for the focused BlueBubbles hardening. Current main still subscribes to both events at gateway/platforms/bluebubbles.py:374-377 and dispatches each accepted webhook at :1040, so inbound deduplication remains needed.
Problems
gateway/platforms/bluebubbles.py:885-895includeschatGuidin the key but does not usechatIdentifier. The linked #30708 report documents same-GUID deliveries withany;-;+addresson one event and only a bare chat identifier on another. Those forms become different keys, so both deliveries can reach the new guard at:1050and dispatch.
Suggested changes
- Use a GUID-first key, or canonicalize the fallback chat forms, and add an async two-webhook regression covering those differing payload shapes.
- Preserve main's existing log-safe registration URL property at current
gateway/platforms/bluebubbles.py:334-340while salvaging the branch.
Automated hermes-sweeper review.
| chats = record.get("chats") or [] | ||
| if chats and isinstance(chats[0], dict): | ||
| chat_guid = self._value(chats[0].get("guid"), chats[0].get("chatGuid")) | ||
| return f"{chat_guid or 'unknown-chat'}:{msg_guid}" |
Contributor
There was a problem hiding this comment.
This key changes when duplicate BlueBubbles events expose the same message GUID under different chat forms. The linked #30708 report shows any;-;+address on one event and only a bare chat identifier on the other; because chatIdentifier is not considered here, they become different keys and both dispatch. Please key GUID-first or canonicalize the fallback identifiers, with an end-to-end two-webhook regression.
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.
10 tasks
benjaminsehl
force-pushed
the
fix/bluebubbles-webhook-dedupe
branch
from
July 19, 2026 22:20
d25ac89 to
c3c8647
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
BlueBubbles can deliver one iMessage as both
new-messageandupdated-message, including overlapping deliveries and later attachment enrichment. This replaces the original persistent-hash approach with bounded in-memory reservation semantics and adds a contextual, turn-local quick acknowledgment for substantive iMessages.Duplicate-delivery protection
Contextual quick acknowledgment
ReadTimeoutas an ambiguous send so an already-delivered message does not trigger a duplicate fallback bubble.Verification
git diff --checkpass.mainpasses the same 192-test gate.Test command