Skip to content

feat(bluebubbles): add reply-context API fallback for cache misses#71820

Merged
omarshahine merged 7 commits intoopenclaw:mainfrom
coletebou:feat/bluebubbles-reply-context-api-fallback
May 1, 2026
Merged

feat(bluebubbles): add reply-context API fallback for cache misses#71820
omarshahine merged 7 commits intoopenclaw:mainfrom
coletebou:feat/bluebubbles-reply-context-api-fallback

Conversation

@coletebou
Copy link
Copy Markdown
Contributor

Summary

Adds an opt-in best-effort fallback: when an inbound BlueBubbles reply arrives without replyToBody/replyToSender and the in-memory reply-context cache misses, fetch the original message from the BlueBubbles HTTP API (GET /api/v1/message/{id}) and populate the cache so subsequent replies amortize. Off by default (channels.bluebubbles.replyContextApiFallback: false).

When the cache hits — the common case — there is zero additional cost. The fallback only fires when the existing in-RAM lookup comes up empty.

Motivation

The current in-memory reply cache is fast but lives in one process's RAM. Several real deployment patterns drop reply context today even when the original message exists in BlueBubbles:

  1. Multi-instance deployments sharing one BB account — the message being replied to was sent by a different OpenClaw process whose RAM is unreachable.
  2. Container/process restart — the cache is wiped; replies to messages older than the restart land context-less.
  3. Cross-tenant participation in shared chats — multiple agents in the same group; only one had the original cached.
  4. Long-lived chats with TTL/LRU eviction — the message's cache entry expired before the reply arrived.

In all four cases the agent currently loses thread context and the user has to re-explain. With this opt-in, BlueBubbles becomes the source-of-truth fallback. (Related to the Private-API-status-cache class of degradation discussed in #43764 — different cache, complementary fix path.)

Design choices

  • Opt-in, default false — strictly backwards-compatible. Path: channels.bluebubbles.replyContextApiFallback (also accepted per-account under accounts.<id>.). Schema in extensions/bluebubbles/src/config-schema.ts, type in extensions/bluebubbles/src/types.ts.
  • Extracted helper fetchBlueBubblesReplyContext in extensions/bluebubbles/src/monitor-reply-fetch.ts — single responsibility, fully testable in isolation, no deep dependencies.
  • In-flight dedupe keyed by ${accountId}:${replyToId} — concurrent webhooks for replies to the same original message coalesce into one HTTP fetch. Released on settle.
  • Best-effort, never throws — any non-2xx, network error, parse error, or empty payload returns null; the caller proceeds with empty reply context, matching pre-fallback behavior.
  • Reuses existing helpersbuildBlueBubblesApiUrl, blueBubblesFetchWithTimeout, isPrivateNetworkOptInEnabled, normalizeOptionalString, normalizeBlueBubblesHandle, rememberBlueBubblesReplyCache. No duplication.
  • Defensive response parsing — accepts both {data: {...}}-wrapped and flat shapes. Body extracted from text ?? body ?? subject. Sender from handle.address ?? handle.id ?? senderId ?? sender.
  • 5-second default timeout (overridable per call) — short enough not to stall the inbound pipeline, long enough for typical local BB latency.
  • SSRF policy propagated from account config via the existing isPrivateNetworkOptInEnabled seam.

Why not always-on?

Three reasons to keep the default off in this PR:

  1. No measured baseline for the cost-on-miss in single-instance deployments. The cache miss rate for a single OpenClaw on stable network is very low; the upside is only meaningful when the patterns above are present.
  2. Discoverability — explicit opt-in makes the behavior change visible at config-load time so an operator knows their replies started incurring an extra HTTP roundtrip.
  3. Conservative landing surface — minimizes the chance of an unexpected behavior change for the long tail of existing deployments.

Happy to flip the default once there's a comfortable baseline. The implementation is identical either way.

Why not push notifications from BB Server?

Considered. BlueBubbles Server's webhook payload doesn't include the full original message body for reply events — only selectedMessageGuid (an ID). The HTTP API is the only authoritative source upstream of the cache. Asking BB Server to denormalize would be a separate cross-project ask.

Test plan

pnpm test extensions/bluebubbles/src/monitor-reply-fetch.test.ts — 18 cases covering:

  • Cache hit equivalent (no replyToId / missing creds → no fetch made)
  • Cache miss + successful BB fetch → fields filled, cache populated
  • Body field cascade (textbodysubject)
  • Sender field cascade (handle.addresshandle.idsenderIdsender)
  • Email handle lowercasing via normalizeBlueBubblesHandle
  • Both wrapped and flat response envelopes
  • 404 / network error / JSON parse error / empty-payload → null, no throw
  • In-flight dedupe (same accountId + replyToId → one fetch)
  • Cross-account isolation (different accountId → independent fetches)
  • Slot release after settle (sequential calls re-fetch)
  • SSRF policy propagation (opt-in on/off)
  • Custom timeout passthrough

Plus the full pnpm check:changed lane (528 BB extension tests + typecheck core/extensions, lint, import-cycles) and pnpm build. All green.

Backward compatibility

With replyContextApiFallback defaulting to false, every existing deployment behaves identically to current main. To opt in:

{
  "channels": {
    "bluebubbles": {
      "replyContextApiFallback": true
    }
  }
}

(or under channels.bluebubbles.accounts.<accountId>.replyContextApiFallback for per-account control.)

Open questions

  1. Default flip — keep opt-in indefinitely, or default-on once burned in?
  2. Config nestingreplyContextApiFallback matches the enrichGroupParticipantsFromContacts precedent. If you'd prefer a richer surface for future tuning knobs (timeout override, max-age cap), happy to nest under replyContext: { apiFallback: true, ... }.

Marking as draft for maintainer review per the "larger behavior" guidance in CLAUDE.md. Happy to slim or restructure based on feedback before flipping out of draft.

@openclaw-barnacle openclaw-barnacle Bot added channel: bluebubbles Channel integration: bluebubbles size: L labels Apr 25, 2026
@coletebou
Copy link
Copy Markdown
Contributor Author

@greptile

@coletebou
Copy link
Copy Markdown
Contributor Author

@codex

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 25, 2026

Greptile Summary

Adds an opt-in best-effort fallback (replyContextApiFallback, default false) that fetches the original BlueBubbles message via GET /api/v1/message/{id} when the in-memory reply-context cache misses, covering multi-instance, post-restart, and TTL-eviction scenarios. The helper is cleanly extracted into monitor-reply-fetch.ts with in-flight deduplication, full SSRF-policy propagation, and graceful error swallowing; the config schema correctly uses .optional() (no per-account default) to allow channel-level inheritance.

Confidence Score: 5/5

Safe to merge — no P0 or P1 issues found; behaviour is strictly additive behind an opt-in flag.

No correctness or security bugs identified. The previous dead-code finding (redundant if (body || sender) guard) was already fixed in the head commit. SSRF policy is correctly propagated through the existing three-mode resolver, inflight deduplication is correct, error handling is exhaustive, and the config-schema .optional() choice is the right call to avoid clobbering channel-level inheritance.

No files require special attention.

Reviews (2): Last reviewed commit: "fix(bluebubbles): address PR #71820 revi..." | Re-trigger Greptile

Comment on lines +115 to +126
if (body || sender) {
rememberBlueBubblesReplyCache({
accountId: params.accountId,
messageId: replyToId,
chatGuid: params.chatGuid,
chatIdentifier: params.chatIdentifier,
chatId: params.chatId,
senderLabel: sender,
body,
timestamp: Date.now(),
});
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Redundant guard is unreachable dead code

The if (body || sender) condition on line 115 can never be false at that point: the early-return two lines above (if (!body && !sender) { return null; }) already guarantees that at least one of them is truthy. The wrapper if is dead code and can be removed to make the intent clearer.

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/bluebubbles/src/monitor-reply-fetch.ts
Line: 115-126

Comment:
**Redundant guard is unreachable dead code**

The `if (body || sender)` condition on line 115 can never be false at that point: the early-return two lines above (`if (!body && !sender) { return null; }`) already guarantees that at least one of them is truthy. The wrapper `if` is dead code and can be removed to make the intent clearer.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Codex Fix in Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 75c4ac1dd5612a35247e198894d46ce0347401b7. Removed the if (body || sender) wrapper — the if (!body && !sender) return null two lines above already guarantees one is truthy by the time we reach the cache write.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5a3c29d598

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +92 to +95
const ssrfPolicy =
params.accountConfig && isPrivateNetworkOptInEnabled(params.accountConfig)
? ({ allowPrivateNetwork: true as const } as const)
: undefined;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Always apply SSRF guard on reply-context fetches

When fallback is enabled but network.dangerouslyAllowPrivateNetwork is not explicitly true, this sets ssrfPolicy to undefined and passes it to blueBubblesFetchWithTimeout; that helper only uses fetchWithSsrFGuard when a policy is present, so this new path skips SSRF hostname/pinning protections entirely. In practice, a compromised or redirecting BlueBubbles endpoint can pivot this request to unintended internal targets, unlike the rest of the BlueBubbles client paths that stay guarded.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 75c4ac1dd5612a35247e198894d46ce0347401b7. Routed through resolveBlueBubblesClientSsrfPolicy — the same three-mode helper every other BB client request uses. The resolver never returns undefined, so blueBubblesFetchWithTimeout always engages fetchWithSsrFGuard:

  • mode 1: explicit private-network opt-in → { allowPrivateNetwork: true }
  • mode 2: parseable trusted hostname → { allowedHostnames: [hostname] }
  • mode 3: explicit opt-out on a private host or unparseable URL → {} (default-deny)

Same bug class as #68234. Added an explicit "never passes undefined to the underlying fetch (regression for #71820 codex review)" test plus mode-by-mode coverage in extensions/bluebubbles/src/monitor-reply-fetch.test.ts. Thanks for the catch.

* cache eviction), opt in to fetching the original message from the
* BlueBubbles HTTP API as a best-effort fallback. Off by default.
*/
replyContextApiFallback: z.boolean().optional().default(false),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve channel-level fallback setting for account configs

Defaulting replyContextApiFallback to false inside each account schema causes account entries that omit the field to still materialize false, which then overrides any channel-level channels.bluebubbles.replyContextApiFallback: true during account merge. In multi-account deployments, operators enabling the global toggle will silently get no fallback unless they duplicate the flag under every accounts.<id> block.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 75c4ac1dd5612a35247e198894d46ce0347401b7. Dropped .default(false) from the per-account schema field — accounts that omit the flag now inherit the channel-level value through mergeAccountConfig instead of clobbering it.

Added two end-to-end tests in setup-surface.test.ts that exercise the runtime merge path through resolveBlueBubblesAccount:

  • channel-level replyContextApiFallback: true survives when an account omits the flag
  • account-level false still overrides channel-level true

Plus a schema-level test confirming the per-account default no longer materializes for missing fields.

@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented Apr 26, 2026

Codex review: needs changes before merge.

What this changes:

This PR adds a default-off BlueBubbles replyContextApiFallback config, fetches missing inbound reply context through the typed BlueBubbles client, documents/tests the setting, updates the changelog, and promotes attachment download failures to runtime error logging.

Required change before merge:

There is one narrow mechanical repair suitable for automation: make the reply-context fallback cache lookup/write-back handle part-index-prefixed IDs consistently and add a focused regression test.

Security review:

Security review cleared: The diff adds an opt-in HTTP fetch path, but it routes through the existing typed BlueBubbles client and SSRF policy, encodes the validated message id, remains best-effort, and does not add new dependencies, workflow changes, or secret-handling expansion.

Review findings:

  • [P3] Preserve cache hits for prefixed reply ids — extensions/bluebubbles/src/monitor-reply-fetch.ts:155-158
Review details

Best possible solution:

Land the BlueBubbles fallback after preserving cache hits for part-index-prefixed reply ids, keeping the feature default-off, extension-owned, typed-client routed, and documented, with any future default-on decision left to a separate product review.

Do we have a high-confidence way to reproduce the issue?

Yes. Main can be reproduced by processing an inbound BlueBubbles reply with a replyToId but no replyToBody or replyToSender after the in-memory reply cache is empty; current main only checks RAM. The remaining P3 is reproducible with a p:0/<guid> reply id because the helper caches the bare guid while the caller keeps looking up the prefixed id.

Is this the best way to solve the issue?

Yes, with one small repair. The PR's default-off, BlueBubbles-owned typed-client fallback is the narrow maintainable direction; the safer final shape is to canonicalize or alias prefixed reply ids consistently so the promised cache amortization works.

Full review comments:

  • [P3] Preserve cache hits for prefixed reply ids — extensions/bluebubbles/src/monitor-reply-fetch.ts:155-158
    sanitizeReplyToId strips p:0/<guid> before fetching, and the successful response is cached only under the bare guid. The caller still looks up the reply cache and short id using the original prefixed replyToId, so repeated prefixed webhooks miss RAM and re-fetch instead of amortizing. Canonicalize the caller lookup or store an alias for the original id.
    Confidence: 0.82

Overall correctness: patch is correct
Overall confidence: 0.84

Acceptance criteria:

  • pnpm test extensions/bluebubbles/src/monitor-reply-fetch.test.ts
  • pnpm test extensions/bluebubbles/src/setup-surface.test.ts
  • pnpm docs:list
  • git diff --check

What I checked:

Likely related people:

Remaining risk / open question:

  • Exact-head CI was still pending during review, so merge should wait for the final required check set to finish on 04f6a8740a0b1aa1f40d14b492cb11c11d67f8f1.
  • The changelog entry still mentions sanitizeForLog redaction even though current main already contains that redaction behavior; maintainers may want to trim the release note to only the final diff's remaining user-visible changes.

Codex review notes: model gpt-5.5, reasoning high; reviewed against eabab1f64f9e.

@coletebou
Copy link
Copy Markdown
Contributor Author

Addressed all three review findings in 75c4ac1dd5612a35247e198894d46ce0347401b7:

  1. P1 SSRF (Codex)monitor-reply-fetch.ts now routes through resolveBlueBubblesClientSsrfPolicy, the canonical three-mode helper every other BB client request uses. The resolver never returns undefined, so the SSRF guard is always engaged. Regression test + per-mode coverage added.
  2. P2 Account inheritance (Codex) — Dropped .default(false) from the per-account schema. Channel-level replyContextApiFallback: true now propagates to accounts that omit the flag through mergeAccountConfig. Two end-to-end tests added through the runtime resolveBlueBubblesAccount merge path.
  3. P2 Dead code (Greptile) — Redundant if (body || sender) wrapper removed.

Verified locally: pnpm test extensions/bluebubbles (535/535 green), pnpm test:changed (all extension lanes green), pnpm check:changed typecheck / lint / runtime-import-cycles all green. Flipping out of draft.

@coletebou coletebou marked this pull request as ready for review April 27, 2026 04:21
@coletebou coletebou force-pushed the feat/bluebubbles-reply-context-api-fallback branch from 75c4ac1 to f1247c6 Compare April 27, 2026 17:41
@coletebou
Copy link
Copy Markdown
Contributor Author

coletebou commented Apr 27, 2026

Rebased onto latest main (now at f1247c6373c568dee505ce0bff68ef965136ed84) — the only conflict was CHANGELOG.md (the layout flipped from ## YYYY.M.D (Unreleased) to a top-level ## Unreleased section while this PR was open). My entry is now at the top of ## Unreleased### Changes in the new single-line style.

Branch is now 0 commits behind upstream/main; BB-targeted tests still 76/76 green post-rebase.

@coletebou
Copy link
Copy Markdown
Contributor Author

Pushed 205b89fe6c — added Thanks @coletebou. attribution to the BlueBubbles changelog entry under ## Unreleased### Changes, matching the surrounding (#PR) Thanks @user. convention. That was the only outstanding ask from the codex meta-review; all three review findings (P1 SSRF guard, P2 channel-level inheritance, P2 dead code) were already addressed in 75c4ac1d.

@openclaw-barnacle openclaw-barnacle Bot added channel: slack Channel integration: slack app: web-ui App: web-ui gateway Gateway runtime commands Command implementations channel: feishu Channel integration: feishu labels Apr 28, 2026
@coletebou
Copy link
Copy Markdown
Contributor Author

Resolved CHANGELOG.md merge conflict in 706de15b54. Upstream cut release 2026.4.27 between my last sync and now, which moved 6 entries out of ## Unreleased and added 4 new ones (iOS/Gateway, Android, Gateway/chat, Security/networking). Kept upstream's new entries verbatim and re-prepended the BlueBubbles entry with the Thanks @coletebou. attribution at the top of ### Changes. mergeable: MERGEABLE confirmed; only the BB diff vs main remains.

@coletebou coletebou force-pushed the feat/bluebubbles-reply-context-api-fallback branch from a2ba44d to 1f690de Compare April 28, 2026 10:53
@openclaw-barnacle openclaw-barnacle Bot removed channel: slack Channel integration: slack app: web-ui App: web-ui gateway Gateway runtime commands Command implementations channel: feishu Channel integration: feishu labels Apr 28, 2026
@coletebou
Copy link
Copy Markdown
Contributor Author

Force-pushed a clean rebase onto current upstream/main (1f690dec35). Two-commit history, 7 changed files (all BlueBubbles + CHANGELOG.md), +739/-0 — strictly scoped to the feature.

Addressed all three codex review items:

  1. Unrelated formatting churn dropped. The earlier merge artifact carried drifted snapshots of feishu/slack/gateway/commands/web-ui files that survived the merge-base anchor. Rebasing instead of merging moves the merge-base forward to upstream's current tip, so the PR diff is now exactly the contribution. git diff --stat upstream/main...HEAD confirms zero non-BB files, zero deletions.

  2. CI parity gate failure resolved structurally. That workflow's paths: filter requires src/gateway/**/src/commands/**/ui/src/** to trigger. Those paths are no longer in the diff, so the parity gate (which is also currently failing on upstream main itself — sha d2fe119) won't run on this PR.

  3. Competing PR feat(bluebubbles): fetch quoted message via SSRF-guarded API on reply cache miss + surface attachment download errors #73241 acknowledged. zqchris's PR opened today proposes a similar fallback with the cleaner architectural pattern of routing through the typed BlueBubblesClient surface (createBlueBubblesClientFromParts), which is what your meta-review originally suggested as a follow-up. Happy to defer to that one if maintainers prefer the typed-client approach; this PR has the longer review history (SSRF guard, channel-level inheritance, dedupe) but theirs is structurally tighter.

mergeable should settle back to MERGEABLE within ~30s of GitHub recomputing the graph after the force-push. Bot review pointers will re-target the new SHAs.

@coletebou
Copy link
Copy Markdown
Contributor Author

Two follow-ups pushed (27b6105990, 9d081574a3) that fold in @zqchris's #73241 architectural improvements while preserving this PR's production wins.

Refactor: route through typed BlueBubblesClient (27b6105990)

The reply-context fetch now uses createBlueBubblesClientFromParts + client.request() instead of resolving the SSRF policy by hand and threading it to blueBubblesFetchWithTimeout. This is exactly the suggestion from the codex meta-review ("consider whether the fetch helper should be folded into the typed BlueBubblesClient surface for consistency") and matches the pattern @zqchris adopted in #73241. The typed client owns SSRF policy resolution internally and cannot produce an undefined policy, which is structurally stronger than the hand-rolled three-mode resolver call.

Also picked up @zqchris's reply-id shape validation as defense-in-depth: sanitizeReplyToId strips the p:0/<guid> part-index prefix and validates the bare GUID against /^[A-Za-z0-9._:-]+$/ with a 128-char cap before it reaches the API path (CWE-20).

Test seam swapped from raw fetch (fetchImpl) to typed client (clientFactory); SSRF mode tests now assert factory-call params, which is the correct level since the typed client owns policy resolution.

Pickup: attachment-error visibility + log redaction (9d081574a3)

Both behavioral changes originated in @zqchris's #73241 and are preserved with attribution in the CHANGELOG:

  1. Attachment download failures promoted from logVerbose to runtime.error. Previously, BB attachment download failures (pinned-dispatcher compat bugs, server 500s, transient network errors) were invisible at default log level — agents only saw the <media:image> (1 image) placeholder with nothing in the log pointing at the actual failure.
  2. sanitizeForLog redacts secret-bearing patterns. BB uses ?password=… query-string auth by default, so error chains carrying captured request URLs could leak the API password into log aggregators (CWE-532). Now redacts password / token / api_key / secret query params plus Authorization: Bearer … headers.

Production wins this PR has and #73241 doesn't

  • In-flight dedupe — concurrent webhooks for the same replyToId (group chats with multiple recipients replying near-simultaneously) coalesce into one fetch.
  • Reply-cache write-back — successful responses populate monitor-reply-cache so subsequent webhooks for the same message resolve from RAM without another round-trip.
  • End-to-end account-inheritance tests through resolveBlueBubblesAccount (covers channel-level → account merge for the new opt-in flag).
  • Per-mode SSRF coverage (mode 1 explicit, mode 1 implicit-private, mode 2 hostname allowlist, mode 3 explicit opt-out, plus the never-undefined regression).

Net diff

7 files, +815/-7 (was +739/-0). Strictly BB-scoped. Tests: 78 BB-targeted (incl. setup-surface end-to-end), 0 changes outside the BlueBubbles extension and CHANGELOG.

CHANGELOG entry moved to the bottom of ## Unreleased### Changes so subsequent upstream additions at the top don't trigger conflicts on this PR — long-lived PR hygiene.

CC @zqchris — appreciated the architectural pointer and the observability fix; happy to defer to your PR if maintainers prefer a fresh review surface, or close mine and land yours.

@coletebou
Copy link
Copy Markdown
Contributor Author

Rebased onto de0f54b54a. Two mechanical conflicts:

  • CHANGELOG.md — re-prepended the BlueBubbles entry under ## Unreleased > ### Changes, matching the convention from the prior rebase.
  • extensions/bluebubbles/src/monitor-processing.ts — layered fix(bluebubbles): tighten DM-vs-group routing across session route, reactions, chatGuid fallback, and short message ids #73235's routing-guard sanitization (wrap attachment.guid/err in sanitizeForLog per call) with this PR's promotion of the same log line to runtime.error?.(). Final shape uses local safeGuid/safeErr (maxLen=80 on the guid) and emits both the visible runtime.error and the verbose log, so neither side's win regresses. The sanitizeForLog body itself had a superset-vs-precursor conflict between two of this PR's own commits (60427fabd1 added guid to the redaction allowlist; a7f8f1d6e1 carried a simpler precursor) — kept the superset.

First pnpm check:changed pass hit two unrelated typecheck errors in extensions/github-copilot/index.ts. Re-fetching and rebasing onto the new tip after d8b9ace39c fix(ci): repair github copilot setup types landed cleared them.

Current branch tip is a7f8f1d6e1 on feat/bluebubbles-reply-context-api-fallback. Full check:changed lane green: typecheck (tsgo --noEmit), lint (oxlint, 0/0 across 13,650 files), import-cycles (0), all 574 BlueBubbles extension tests pass.

Open to maintainer review whenever cycles exist — happy to flip the default, slim the config nesting (replyContext: { apiFallback: true, ... }), or split the sanitizeForLog redaction additions and the attachment-error log promotion into a separate PR if any of those land cleaner. Three separable wins in here.

@openclaw-barnacle openclaw-barnacle Bot added the docs Improvements or additions to documentation label May 1, 2026
@omarshahine omarshahine force-pushed the feat/bluebubbles-reply-context-api-fallback branch from c388b55 to 7506d2b Compare May 1, 2026 04:38
@omarshahine omarshahine force-pushed the feat/bluebubbles-reply-context-api-fallback branch from 7506d2b to b41b207 Compare May 1, 2026 04:41
coletebou and others added 7 commits May 1, 2026 04:51
…nt merge, dead code)

- Route reply-context fetch through resolveBlueBubblesClientSsrfPolicy so
  the SSRF guard is always engaged. The three-mode resolver never returns
  undefined; previously we passed undefined when private-network opt-in was
  off, which silently skipped fetchWithSsrFGuard. Same bug class as openclaw#68234.
- Drop .default(false) from per-account replyContextApiFallback so accounts
  that omit the flag inherit the channel-level value through
  mergeAccountConfig instead of clobbering it with false.
- Remove redundant if (body || sender) guard left after the early-return
  on the previous line.
- Add three-mode SSRF coverage in monitor-reply-fetch.test.ts (mode 1
  opt-in, mode 2 hostname allowlist, mode 3 default-deny) plus an explicit
  regression test, and end-to-end inheritance tests in setup-surface.test.ts
  through resolveBlueBubblesAccount.
…eBubblesClient

Replaces the manual SSRF-policy-resolve + raw-fetch path with
createBlueBubblesClientFromParts, so the request goes through the same
typed client surface every other BlueBubbles call uses. Picks up the
folding-into-typed-client suggestion the codex meta-review left as a
follow-up on the original PR review, and matches the architectural
pattern in openclaw#73241.

- Drop direct imports of buildBlueBubblesApiUrl, blueBubblesFetchWithTimeout,
  and resolveBlueBubblesClientSsrfPolicy. The typed client owns SSRF policy
  resolution internally and cannot produce an undefined policy.
- Add sanitizeReplyToId: strip part-index prefix (`p:0/<guid>` → `<guid>`)
  and validate the bare GUID against the known charset + 128-char cap
  before it reaches the API path. Defense-in-depth alongside the typed
  client's URL construction (CWE-20).
- Swap the test seam from fetchImpl (raw fetch) to clientFactory (typed
  client constructor). New tests assert factory-call params for SSRF
  modes 1/2/3 plus the regression invariant from the original review.
- Keep in-flight dedupe and reply-cache write-back (production wins for
  high-volume group chats — concurrent webhooks for the same replyToId
  coalesce into one fetch, and successful responses populate the cache
  so subsequent webhooks resolve from RAM).

Tests: 23 in monitor-reply-fetch.test.ts (+1 new pathological-id case,
+1 part-index-strip case), 78 BB-targeted total green.
…r and redact secrets in logs

Carries forward two BlueBubbles observability/security fixes from openclaw#73241
so this PR is a strict superset of the competing implementation.

- Promote attachment download failures from logVerbose to runtime.error
  alongside a verbose copy. Previously, BB attachment download failures
  (pinned-dispatcher compat bugs, BB server 500s, transient network
  errors) were invisible at default log level — agents only saw the
  `<media:image> (1 image)` placeholder text with nothing in the log
  pointing at the actual failure.
- Extend sanitizeForLog to redact `?password=…` / `?token=…` /
  `?api_key=…` / `?secret=…` query params and `Authorization: Bearer …`
  headers before they reach the log sink. BB uses query-string auth by
  default, so error chains carrying captured request URLs were
  vulnerable to leaking the API password into log aggregators
  (CWE-532).
- Move the changelog entry to the bottom of `## Unreleased` →
  `### Changes`. Insertions at the top of that section are the most
  common cause of CHANGELOG conflicts on long-lived PRs; bottom
  position keeps the diff context stable.

Both behavioral changes originated in @zqchris's PR openclaw#73241; attribution
preserved in the CHANGELOG entry.
@omarshahine omarshahine force-pushed the feat/bluebubbles-reply-context-api-fallback branch from b41b207 to 04f6a87 Compare May 1, 2026 04:51
@omarshahine omarshahine merged commit 76930da into openclaw:main May 1, 2026
95 checks passed
@omarshahine
Copy link
Copy Markdown
Contributor

Merged via squash.

Thanks @coletebou!

lxe pushed a commit to lxe/openclaw that referenced this pull request May 6, 2026
…penclaw#71820)

Merged via squash.

Prepared head SHA: 04f6a87
Co-authored-by: coletebou <12384893+coletebou@users.noreply.github.com>
Co-authored-by: omarshahine <10343873+omarshahine@users.noreply.github.com>
Reviewed-by: @omarshahine
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
…penclaw#71820)

Merged via squash.

Prepared head SHA: 04f6a87
Co-authored-by: coletebou <12384893+coletebou@users.noreply.github.com>
Co-authored-by: omarshahine <10343873+omarshahine@users.noreply.github.com>
Reviewed-by: @omarshahine
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: bluebubbles Channel integration: bluebubbles docs Improvements or additions to documentation size: L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants