#15240: fix(bluebubbles): URL dropped when sent in same iMessage bubble as text (#15065)
channel: bluebubbles
stale
size: M
Cluster:
Bluebubbles iMessage Fixes
## Summary
When a user sends text + URL in the same iMessage bubble, macOS splits it into two `chat.db` rows with different GUIDs and no `associatedMessageGuid`. BlueBubbles delivers both as separate webhooks ~870ms apart, but the debouncer keyed each by `messageId`, so they got different keys and never merged. The assistant only saw the text — the URL got dropped.
Closes #15065
lobster-biscuit
## Root Cause
`buildKey` in `getOrCreateDebouncer` (`extensions/bluebubbles/src/monitor.ts`) fell through to the `msg:<messageId>` path for URL balloons that lack `associatedMessageGuid`. Since both the text and the balloon have different GUIDs, they flushed independently. The default debounce window (500ms) was also too short — URL balloons arrive ~870ms after the text.
## Changes
- Inbound (not from-me) messages now use sender+chat keys instead of message-id keys, so text and subsequent URL balloon land in the same debounce bucket. This matches the iMessage native channel's debounce strategy (`monitorIMessageProvider` in `src/imessage/monitor/monitor-provider.ts`).
- Extracted `buildChatKey` helper with type prefixes (`chat_guid:`, `chat_identifier:`, `chat_id:`) consistent with `formatGroupAllowlistEntry`, avoiding cross-identifier-type collisions.
- `DEFAULT_INBOUND_DEBOUNCE_MS` bumped from 500 to 1000 (covers the observed ~870ms balloon delay with margin, well below typical human inter-message gaps of 2s+).
- From-me messages keep message-id keying to avoid merging distinct outbound messages.
### Before / After
- Before: text + URL in same iMessage bubble → assistant sees only the text, URL is lost
- After: both coalesce into a single dispatch containing text and URL
## Tests
- `monitor.test.ts` — new test: sends text then URL balloon (different GUIDs, no `associatedMessageGuid`, `balloonBundleId` set) 870ms apart; verifies they coalesce into a single dispatch. Fails before fix, passes after.
- `monitor.test.ts` — new test: two separate inbound messages from the same sender outside the debounce window are dispatched independently (proves the window doesn't over-merge).
- Existing debounce test timer adjusted from 600ms to 1100ms to match the new default window.
- All 53 tests in `monitor.test.ts` pass.
- `pnpm build && pnpm check` pass (lint/format clean on changed files).
<!-- greptile_comment -->
<h2>Greptile Overview</h2>
<h3>Greptile Summary</h3>
Fixes a bug where a URL sent in the same iMessage bubble as text was dropped by the BlueBubbles extension. macOS splits such messages into two `chat.db` rows with different GUIDs and no `associatedMessageGuid`, causing the debouncer to bucket them separately and lose the URL.
- **Debounce key change**: Inbound (not from-me) messages now use `sender + chat` keying instead of `messageId` keying, matching the iMessage native channel's debounce strategy. This ensures text and subsequent URL balloon land in the same debounce bucket.
- **`buildChatKey` helper**: Extracted a type-prefixed helper (`chat_guid:`, `chat_id:`, `chat_identifier:`) consistent with `formatGroupAllowlistEntry`, preventing cross-identifier-type collisions.
- **Debounce window**: `DEFAULT_INBOUND_DEBOUNCE_MS` increased from 500ms to 1000ms to cover the observed ~870ms balloon delay.
- **From-me messages**: Retain message-id keying to avoid merging distinct outbound messages.
- **Tests**: Two new tests verify coalescing behavior and independent dispatch outside the debounce window. Existing test timer adjusted for the new window.
<h3>Confidence Score: 4/5</h3>
- This PR is safe to merge — the fix is well-scoped, addresses a real bug with the correct approach, and has good test coverage.
- Score of 4 reflects a clean, well-tested fix that aligns with established patterns (iMessage native channel debounce strategy, `formatGroupAllowlistEntry` prefix convention). The only minor concern is the theoretical risk of over-merging two genuinely distinct rapid-fire inbound messages within the 1000ms window, but this is acknowledged in comments, mitigated by the typical 2s+ human inter-message gap, and `combineDebounceEntries` preserves all text content even if coalescing occurs. Previous review comments about key collisions and over-merging have been addressed.
- No files require special attention. Both changed files are well-structured and the test coverage is solid.
<sub>Last reviewed commit: 691cffd</sub>
<!-- greptile_other_comments_section -->
<!-- /greptile_comment -->
Most Similar PRs
#23483: fix(bluebubbles): key debounce by chat+sender instead of messageId
by saucesteals · 2026-02-22
92.1%
#22564: fix(bluebubbles): include iMessage subject in message text
by lailoo · 2026-02-21
79.2%
#14429: feat(bluebubbles): handle iMessage edit events in webhook
by westhechiang · 2026-02-12
78.3%
#16304: fix(bluebubbles): accept webhook message fields at top level
by MisterGuy420 · 2026-02-14
77.7%
#21174: fix(bluebubbles): trim leading newlines from message text
by cosmopax · 2026-02-19
76.5%
#19522: feat(bluebubbles): send TTS as native iMessage voice memos
by mwmacmahon · 2026-02-17
76.4%
#16733: fix(ui): avoid injected newlines when tool output is hidden
by jp117 · 2026-02-15
76.2%
#23705: BlueBubbles: enrich webhook group participants from chat metadata
by marc726 · 2026-02-22
76.2%
#11600: fix(bluebubbles): always use private-api method for sending
by coletebou · 2026-02-08
75.2%
#2799: fix(imessage): prevent self-chat and outbound echo loops (#2585)
by Tfh-Yqf · 2026-01-27
75.0%