From 173f959613b35195c8e4e662b9e0138831cfb4d2 Mon Sep 17 00:00:00 2001 From: "clawsweeper[bot]" <274271284+clawsweeper[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 22:44:21 -0700 Subject: [PATCH] fix(bluebubbles): cache prefixed reply context aliases * fix: BlueBubbles reply-context fallback cache-key regression * fix(clawsweeper): address review for clawsweeper-commit-openclaw-openclaw-76930da7ebc7 (1) --------- Co-authored-by: openclaw-clawsweeper[bot] <280122609+openclaw-clawsweeper[bot]@users.noreply.github.com> --- .../src/monitor-reply-fetch.test.ts | 48 +++++++++++++++++++ .../bluebubbles/src/monitor-reply-fetch.ts | 26 +++++++++- 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/extensions/bluebubbles/src/monitor-reply-fetch.test.ts b/extensions/bluebubbles/src/monitor-reply-fetch.test.ts index 96cf9cefc6f..ad4531740c2 100644 --- a/extensions/bluebubbles/src/monitor-reply-fetch.test.ts +++ b/extensions/bluebubbles/src/monitor-reply-fetch.test.ts @@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { BlueBubblesClient, createBlueBubblesClientFromParts } from "./client.js"; import { _resetBlueBubblesShortIdState, + getShortIdForUuid, resolveReplyContextFromCache, } from "./monitor-reply-cache.js"; import { @@ -138,6 +139,53 @@ describe("fetchBlueBubblesReplyContext", () => { expect(requestCalls[0]?.path).toBe("/api/v1/message/msg-bare-guid"); }); + it("populates the reply cache for the original prefixed reply id", async () => { + const { factory } = makeFakeClient([ + jsonResponse({ data: { text: "cached prefix", handle: { address: "+15551112222" } } }), + ]); + await fetchBlueBubblesReplyContext({ + ...baseParams, + replyToId: "p:0/msg-prefixed-cache", + chatGuid: "iMessage;-;+15551112222", + clientFactory: factory, + }); + const cached = resolveReplyContextFromCache({ + accountId: "default", + replyToId: "p:0/msg-prefixed-cache", + chatGuid: "iMessage;-;+15551112222", + }); + expect(cached?.body).toBe("cached prefix"); + expect(cached?.senderLabel).toBe("+15551112222"); + }); + + it("does not cache non-part-index slash prefixes as aliases", async () => { + const { factory, requestCalls } = makeFakeClient([ + jsonResponse({ data: { text: "cached bare only", handle: { address: "+15551112222" } } }), + ]); + await fetchBlueBubblesReplyContext({ + ...baseParams, + replyToId: "../etc/passwd", + chatGuid: "iMessage;-;+15551112222", + clientFactory: factory, + }); + expect(requestCalls[0]?.path).toBe("/api/v1/message/passwd"); + expect( + resolveReplyContextFromCache({ + accountId: "default", + replyToId: "passwd", + chatGuid: "iMessage;-;+15551112222", + })?.body, + ).toBe("cached bare only"); + expect( + resolveReplyContextFromCache({ + accountId: "default", + replyToId: "../etc/passwd", + chatGuid: "iMessage;-;+15551112222", + }), + ).toBeNull(); + expect(getShortIdForUuid("../etc/passwd")).toBeUndefined(); + }); + it("fetches the BB API and returns body + normalized sender on success", async () => { const { factory, requestCalls } = makeFakeClient([ jsonResponse({ diff --git a/extensions/bluebubbles/src/monitor-reply-fetch.ts b/extensions/bluebubbles/src/monitor-reply-fetch.ts index 0da09f5a2cf..05713594ced 100644 --- a/extensions/bluebubbles/src/monitor-reply-fetch.ts +++ b/extensions/bluebubbles/src/monitor-reply-fetch.ts @@ -17,6 +17,8 @@ const DEFAULT_REPLY_FETCH_TIMEOUT_MS = 5_000; // punctuation set below; 128 chars is comfortable headroom (CWE-20). const REPLY_TO_ID_PATTERN = /^[A-Za-z0-9._:-]+$/; const REPLY_TO_ID_MAX_LENGTH = 128; +const PART_INDEX_REPLY_TO_ID_PATTERN = /^p:\d{1,10}\/([A-Za-z0-9._:-]+)$/; +const PART_INDEX_REPLY_TO_ID_MAX_LENGTH = REPLY_TO_ID_MAX_LENGTH + "p:".length + 10 + "/".length; export type BlueBubblesReplyFetchResult = { body?: string; @@ -110,6 +112,18 @@ function sanitizeReplyToId(raw: string): string | null { return bare; } +function normalizePartIndexReplyToIdAlias(raw: string, bareReplyToId: string): string | null { + const trimmed = raw.trim(); + if (trimmed.length > PART_INDEX_REPLY_TO_ID_MAX_LENGTH) { + return null; + } + const match = PART_INDEX_REPLY_TO_ID_PATTERN.exec(trimmed); + if (!match || match[1] !== bareReplyToId) { + return null; + } + return trimmed; +} + async function runFetch( params: FetchBlueBubblesReplyContextParams, replyToId: string, @@ -152,7 +166,7 @@ async function runFetch( if (!body && !sender) { return null; } - rememberBlueBubblesReplyCache({ + const cacheEntry = { accountId: params.accountId, messageId: replyToId, chatGuid: params.chatGuid, @@ -161,7 +175,15 @@ async function runFetch( senderLabel: sender, body, timestamp: Date.now(), - }); + }; + rememberBlueBubblesReplyCache(cacheEntry); + const partIndexReplyToId = normalizePartIndexReplyToIdAlias(params.replyToId, replyToId); + if (partIndexReplyToId) { + rememberBlueBubblesReplyCache({ + ...cacheEntry, + messageId: partIndexReplyToId, + }); + } return { body, sender }; } catch { // Best-effort: swallow network/parse errors. Caller proceeds with empty