diff --git a/extensions/bluebubbles/src/channel.ts b/extensions/bluebubbles/src/channel.ts index b78f41659..da594e763 100644 --- a/extensions/bluebubbles/src/channel.ts +++ b/extensions/bluebubbles/src/channel.ts @@ -20,6 +20,7 @@ import { resolveDefaultBlueBubblesAccountId, } from "./accounts.js"; import { BlueBubblesConfigSchema } from "./config-schema.js"; +import { resolveBlueBubblesMessageId } from "./monitor.js"; import { probeBlueBubbles, type BlueBubblesProbe } from "./probe.js"; import { sendMessageBlueBubbles } from "./send.js"; import { @@ -237,7 +238,9 @@ export const bluebubblesPlugin: ChannelPlugin = { return { ok: true, to: trimmed }; }, sendText: async ({ cfg, to, text, accountId, replyToId }) => { - const replyToMessageGuid = typeof replyToId === "string" ? replyToId.trim() : ""; + const rawReplyToId = typeof replyToId === "string" ? replyToId.trim() : ""; + // Resolve short ID (e.g., "5") to full UUID + const replyToMessageGuid = rawReplyToId ? resolveBlueBubblesMessageId(rawReplyToId) : ""; const result = await sendMessageBlueBubbles(to, text, { cfg: cfg as ClawdbotConfig, accountId: accountId ?? undefined, diff --git a/extensions/bluebubbles/src/media-send.ts b/extensions/bluebubbles/src/media-send.ts index 254cecafd..5fc9895e3 100644 --- a/extensions/bluebubbles/src/media-send.ts +++ b/extensions/bluebubbles/src/media-send.ts @@ -4,8 +4,9 @@ import { fileURLToPath } from "node:url"; import { resolveChannelMediaMaxBytes, type ClawdbotConfig } from "clawdbot/plugin-sdk"; import { sendBlueBubblesAttachment } from "./attachments.js"; -import { sendMessageBlueBubbles } from "./send.js"; +import { resolveBlueBubblesMessageId } from "./monitor.js"; import { getBlueBubblesRuntime } from "./runtime.js"; +import { sendMessageBlueBubbles } from "./send.js"; const HTTP_URL_RE = /^https?:\/\//i; const MB = 1024 * 1024; @@ -134,12 +135,17 @@ export async function sendBlueBubblesMedia(params: { } } + // Resolve short ID (e.g., "5") to full UUID + const replyToMessageGuid = replyToId?.trim() + ? resolveBlueBubblesMessageId(replyToId.trim()) + : undefined; + const attachmentResult = await sendBlueBubblesAttachment({ to, buffer, filename: resolvedFilename ?? "attachment", contentType: resolvedContentType ?? undefined, - replyToMessageGuid: replyToId?.trim() || undefined, + replyToMessageGuid, opts: { cfg, accountId, @@ -151,7 +157,7 @@ export async function sendBlueBubblesMedia(params: { await sendMessageBlueBubbles(to, trimmedCaption, { cfg, accountId, - replyToMessageGuid: replyToId?.trim() || undefined, + replyToMessageGuid, }); } diff --git a/extensions/bluebubbles/src/monitor.test.ts b/extensions/bluebubbles/src/monitor.test.ts index 14c896427..f3be7d6ed 100644 --- a/extensions/bluebubbles/src/monitor.test.ts +++ b/extensions/bluebubbles/src/monitor.test.ts @@ -1175,8 +1175,8 @@ describe("BlueBubbles webhook monitor", () => { expect(callArgs.ctx.ReplyToId).toBe("msg-0"); expect(callArgs.ctx.ReplyToBody).toBe("original message"); expect(callArgs.ctx.ReplyToSender).toBe("+15550000000"); - // Body still uses the full UUID since it wasn't cached - expect(callArgs.ctx.Body).toContain("[Replying to +15550000000 id:msg-0]"); + // Body uses just the ID (no sender) for token savings + expect(callArgs.ctx.Body).toContain("[Replying to id:msg-0]"); expect(callArgs.ctx.Body).toContain("original message"); }); @@ -1245,8 +1245,8 @@ describe("BlueBubbles webhook monitor", () => { expect(callArgs.ctx.ReplyToId).toBe("1"); expect(callArgs.ctx.ReplyToBody).toBe("original message (cached)"); expect(callArgs.ctx.ReplyToSender).toBe("+15550000000"); - // Body uses short ID for token savings - expect(callArgs.ctx.Body).toContain("[Replying to +15550000000 id:1]"); + // Body uses just the short ID (no sender) for token savings + expect(callArgs.ctx.Body).toContain("[Replying to id:1]"); expect(callArgs.ctx.Body).toContain("original message (cached)"); }); diff --git a/extensions/bluebubbles/src/monitor.ts b/extensions/bluebubbles/src/monitor.ts index 4c2b4a225..c7a4188c0 100644 --- a/extensions/bluebubbles/src/monitor.ts +++ b/extensions/bluebubbles/src/monitor.ts @@ -376,6 +376,8 @@ function buildMessagePlaceholder(message: NormalizedWebhookMessage): string { return ""; } +const REPLY_BODY_TRUNCATE_LENGTH = 60; + function formatReplyContext(message: { replyToId?: string; replyToShortId?: string; @@ -383,15 +385,20 @@ function formatReplyContext(message: { replyToSender?: string; }): string | null { if (!message.replyToId && !message.replyToBody && !message.replyToSender) return null; - const sender = message.replyToSender?.trim() || "unknown sender"; // Prefer short ID for token savings const displayId = message.replyToShortId || message.replyToId; - const idPart = displayId ? ` id:${displayId}` : ""; - const body = message.replyToBody?.trim(); - if (!body) { - return `[Replying to ${sender}${idPart}]\n[/Replying]`; + // Only include sender if we don't have an ID (fallback) + const label = displayId ? `id:${displayId}` : (message.replyToSender?.trim() || "unknown"); + const rawBody = message.replyToBody?.trim(); + if (!rawBody) { + return `[Replying to ${label}]\n[/Replying]`; } - return `[Replying to ${sender}${idPart}]\n${body}\n[/Replying]`; + // Truncate long reply bodies for token savings + const body = + rawBody.length > REPLY_BODY_TRUNCATE_LENGTH + ? `${rawBody.slice(0, REPLY_BODY_TRUNCATE_LENGTH)}…` + : rawBody; + return `[Replying to ${label}]\n${body}\n[/Replying]`; } function readNumberLike(record: Record | null, key: string): number | undefined { @@ -1661,8 +1668,12 @@ async function processMessage( if (!chunks.length && payload.text) chunks.push(payload.text); if (!chunks.length) return; for (const chunk of chunks) { - const replyToMessageGuid = + const rawReplyToId = typeof payload.replyToId === "string" ? payload.replyToId.trim() : ""; + // Resolve short ID (e.g., "5") to full UUID + const replyToMessageGuid = rawReplyToId + ? resolveBlueBubblesMessageId(rawReplyToId) + : ""; const result = await sendMessageBlueBubbles(outboundTarget, chunk, { cfg: config, accountId: account.accountId,