Skip to content

BlueBubbles: reply threading silently degrades when Private API cache expires #43764

@omarshahine

Description

@omarshahine

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions