openclaw - ✅(Solved) Fix BlueBubbles: reply threading silently degrades when Private API cache expires [1 pull requests, 1 comments, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#43764Fetched 2026-04-08 00:17:50
View on GitHub
Comments
1
Participants
1
Timeline
9
Reactions
1
Participants
Timeline (top)
referenced ×6commented ×1cross-referenced ×1subscribed ×1

BlueBubbles reply threading (iMessage selectedMessageGuid) silently degrades to plain sends after the server info cache expires (~10 minutes). The message tool returns {"ok": true} with no indication that threading was dropped.

Error Message

  1. Message sends successfully as plain text — no error, no warning to the agent throw new Error("...");

Root Cause

  1. monitor.ts fetches fetchBlueBubblesServerInfo() once on startup, caching private_api: true with a 10-minute TTL (probe.ts:23)
  2. After expiry, getCachedBlueBubblesPrivateApiStatus() returns null
  3. isBlueBubblesPrivateApiStatusEnabled(null) returns false (requires === true)
  4. send.ts:438-442 — the if (wantsReplyThread && privateApiDecision.canUsePrivateApi) check fails silently
  5. selectedMessageGuid is never added to the BB API payload
  6. Message sends successfully as plain text — no error, no warning to the agent

Fix Action

Fixed

PR fix notes

PR #43773: fix(bluebubbles): lazy-refresh Private API status for reply threading

Description (problem / solution / changelog)

Summary

  • Lazy-refresh the BlueBubbles server info cache when Private API status is unknown (null) and a reply or message effect is requested
  • Prevents reply threading from silently degrading to plain sends after the 10-minute cache TTL expires

Problem

After #23393 correctly changed the Private API check from !== false to === true, expired cache (null) causes canUsePrivateApi to be false. The selectedMessageGuid is silently omitted from the BB API payload — the message sends successfully but without threading. No error is returned to the agent.

Fix

One lazy fetchBlueBubblesServerInfo() call in sendMessageBlueBubbles() when:

  1. privateApiStatus === null (cache expired)
  2. AND a reply or effect is requested

This adds at most one extra API call (~5s timeout) on the first reply after cache expiry. The fetched result repopulates the 10-minute cache for subsequent sends. Plain sends are unaffected.

Fixes #43764

Test plan

  • Gateway restart → reply threading works immediately (cache populated on startup)
  • Wait 10+ minutes → send a threaded reply → verify it still threads (lazy refresh kicks in)
  • Verify plain sends are unaffected when cache is expired
  • Verify no regression for Private API disabled servers (should still get proper error, not 500)

🤖 Generated with Claude Code

Changed files

  • extensions/bluebubbles/src/send.test.ts (modified, +184/-1)
  • extensions/bluebubbles/src/send.ts (modified, +26/-1)
  • extensions/bluebubbles/src/test-harness.ts (modified, +9/-0)

Code Example

[bluebubbles] Private API status unknown; sending without reply threading.
Run a status probe to restore private-api features.

---

if (getCachedBlueBubblesPrivateApiStatus(accountId) === false) {
  throw new Error("...");
}

---

// In sendMessageBlueBubbles(), after wantsReplyThread/wantsEffect are set:

if (privateApiStatus === null && (wantsReplyThread || wantsEffect)) {
  const serverInfo = await fetchBlueBubblesServerInfo({
    baseUrl,
    password,
    accountId: account.accountId,
    timeoutMs: 5000,
  }).catch(() => null);
  if (serverInfo) {
    privateApiStatus = getCachedBlueBubblesPrivateApiStatus(account.accountId);
  }
}
RAW_BUFFERClick to expand / collapse

Draft — validating fix before requesting upstream merge

Summary

BlueBubbles reply threading (iMessage selectedMessageGuid) silently degrades to plain sends after the server info cache expires (~10 minutes). The message tool returns {"ok": true} with no indication that threading was dropped.

Root Cause

  1. monitor.ts fetches fetchBlueBubblesServerInfo() once on startup, caching private_api: true with a 10-minute TTL (probe.ts:23)
  2. After expiry, getCachedBlueBubblesPrivateApiStatus() returns null
  3. isBlueBubblesPrivateApiStatusEnabled(null) returns false (requires === true)
  4. send.ts:438-442 — the if (wantsReplyThread && privateApiDecision.canUsePrivateApi) check fails silently
  5. selectedMessageGuid is never added to the BB API payload
  6. Message sends successfully as plain text — no error, no warning to the agent

Evidence

Gateway logs show this warning on every reply attempt after cache expiry:

[bluebubbles] Private API status unknown; sending without reply threading.
Run a status probe to restore private-api features.

But this warning only appears in gateway logs — the agent never sees it. The tool result returns success.

Inconsistency with Reactions

Reactions handle null differently (reactions.ts:153):

if (getCachedBlueBubblesPrivateApiStatus(accountId) === false) {
  throw new Error("...");
}

Reactions only block when Private API is explicitly disabled (false), allowing unknown (null) to proceed. Replies require === true, blocking on unknown.

Proposed Fix

Lazy-refresh the server info cache in send.ts when Private API status is null and a reply/effect is requested:

// In sendMessageBlueBubbles(), after wantsReplyThread/wantsEffect are set:

if (privateApiStatus === null && (wantsReplyThread || wantsEffect)) {
  const serverInfo = await fetchBlueBubblesServerInfo({
    baseUrl,
    password,
    accountId: account.accountId,
    timeoutMs: 5000,
  }).catch(() => null);
  if (serverInfo) {
    privateApiStatus = getCachedBlueBubblesPrivateApiStatus(account.accountId);
  }
}

This adds at most one extra API call (~5s timeout) on the first reply after cache expiry, and the fetched result repopulates the cache for subsequent sends.

Additional Issue: Built-in Skill Parameter Mismatch

The built-in skills/bluebubbles/SKILL.md documents replyTo as the parameter for action: "reply", but actions.ts:259 reads messageId. The generic action: "send" path in message-action-runner.ts correctly reads replyTo. This means:

  • action=send + replyTo → works (generic path)
  • action=reply + replyTosilently fails (BB path reads messageId)
  • action=reply + messageId → works (BB path)

The skill should either document messageId for action=reply, or actions.ts should also read replyTo as a fallback.

Environment

  • OpenClaw v2026.3.7
  • BlueBubbles with Private API enabled
  • macOS host

extent analysis

Problem Summary

Reply threading drops after the BlueBubbles server‑info cache expires because the send‑logic treats a null private‑API status as “disabled”. The same null handling is inconsistent with reactions, and the built‑in skill docs use the wrong parameter name for the reply action.

Root Cause Analysis

  1. monitor.ts caches private_api for 10 min. After expiry getCachedBlueBubblesPrivateApiStatus() returns null.
  2. send.ts checks privateApiDecision.canUsePrivateApi === true; null makes the check fail silently, so selectedMessageGuid is never added.
  3. Reactions treat null as “unknown → allowed”, while replies treat it as “disabled”.
  4. Skill documentation uses replyTo but the BB‑specific path reads messageId, causing silent failures for action=reply.

Fix Plan

1. Lazy‑refresh cache in the send path

Add a small helper that, when a reply/effect is requested and the cached status is null, forces a fresh probe.

// src/bluebubbles/send.ts  (around line 430)

import { fetchBlueBubblesServerInfo } from './monitor';
import { getCachedBlueBubblesPrivateApiStatus } from './cache';

// Existing variables
let privateApiStatus = getCachedBlueBubblesPrivateApiStatus(account.accountId);

// NEW: refresh if unknown and we need private‑API features
async function ensurePrivateApiStatus() {
  if (privateApiStatus !== null) return; // already known

  // Only hit the server when a reply or effect is requested
  if (!(wantsReplyThread || wantsEffect)) return;

  const serverInfo = await fetchBlueBubblesServerInfo({
    baseUrl,
    password,
    accountId: account.accountId,
    timeoutMs: 5_000,
  }).catch(() => null);

  // The fetch function updates the cache internally, so just read it again
  if (serverInfo) {
    privateApiStatus = getCachedBlueBubblesPrivateApiStatus(account.accountId);

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING