From 9c0c5866dbf3b1e43f05bb0852196685008ca608 Mon Sep 17 00:00:00 2001 From: Tyler Yust Date: Mon, 26 Jan 2026 14:11:37 -0800 Subject: [PATCH] fix: coalesce BlueBubbles link previews (#1981) (thanks @tyler6204) --- CHANGELOG.md | 1 + extensions/bluebubbles/src/monitor.ts | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f1330931..2587a57b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,7 @@ Status: unreleased. - **BREAKING:** Gateway auth mode "none" is removed; gateway now requires token/password (Tailscale Serve identity still allowed). ### Fixes +- BlueBubbles: coalesce inbound URL link preview messages. (#1981) Thanks @tyler6204. - Agents: include memory.md when bootstrapping memory context. (#2318) Thanks @czekaj. - Telegram: wrap reasoning italics per line to avoid raw underscores. (#2181) Thanks @YuriNachos. - Voice Call: enforce Twilio webhook signature verification for ngrok URLs; disable ngrok free tier bypass by default. diff --git a/extensions/bluebubbles/src/monitor.ts b/extensions/bluebubbles/src/monitor.ts index ac248f5a7..98431775a 100644 --- a/extensions/bluebubbles/src/monitor.ts +++ b/extensions/bluebubbles/src/monitor.ts @@ -262,7 +262,7 @@ type BlueBubblesDebounceEntry = { /** * Default debounce window for inbound message coalescing (ms). * This helps combine URL text + link preview balloon messages that BlueBubbles - * sends as separate webhook events. + * sends as separate webhook events when no explicit inbound debounce config exists. */ const DEFAULT_INBOUND_DEBOUNCE_MS = 350; @@ -339,6 +339,17 @@ const targetDebouncers = new Map< ReturnType >(); +function resolveBlueBubblesDebounceMs( + config: ClawdbotConfig, + core: BlueBubblesCoreRuntime, +): number { + const inbound = config.messages?.inbound; + const hasExplicitDebounce = + typeof inbound?.debounceMs === "number" || typeof inbound?.byChannel?.bluebubbles === "number"; + if (!hasExplicitDebounce) return DEFAULT_INBOUND_DEBOUNCE_MS; + return core.channel.debounce.resolveInboundDebounceMs({ cfg: config, channel: "bluebubbles" }); +} + /** * Creates or retrieves a debouncer for a webhook target. */ @@ -349,7 +360,7 @@ function getOrCreateDebouncer(target: WebhookTarget) { const { account, config, runtime, core } = target; const debouncer = core.channel.debounce.createInboundDebouncer({ - debounceMs: DEFAULT_INBOUND_DEBOUNCE_MS, + debounceMs: resolveBlueBubblesDebounceMs(config, core), buildKey: (entry) => { const msg = entry.message; // Build key from account + chat + sender to coalesce messages from same source