feat: enhance message handling with short ID resolution and reply context improvements

- Implemented resolution of short message IDs to full UUIDs in both text and media sending functions.
- Updated reply context formatting to optimize token usage by including only necessary information.
- Introduced truncation for long reply bodies to further reduce token consumption.
- Adjusted tests to reflect changes in reply context handling and message ID resolution.
This commit is contained in:
Tyler Yust
2026-01-21 00:33:38 -08:00
parent b073deee20
commit 7bfc32fe33
4 changed files with 35 additions and 15 deletions

View File

@@ -20,6 +20,7 @@ import {
resolveDefaultBlueBubblesAccountId, resolveDefaultBlueBubblesAccountId,
} from "./accounts.js"; } from "./accounts.js";
import { BlueBubblesConfigSchema } from "./config-schema.js"; import { BlueBubblesConfigSchema } from "./config-schema.js";
import { resolveBlueBubblesMessageId } from "./monitor.js";
import { probeBlueBubbles, type BlueBubblesProbe } from "./probe.js"; import { probeBlueBubbles, type BlueBubblesProbe } from "./probe.js";
import { sendMessageBlueBubbles } from "./send.js"; import { sendMessageBlueBubbles } from "./send.js";
import { import {
@@ -237,7 +238,9 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
return { ok: true, to: trimmed }; return { ok: true, to: trimmed };
}, },
sendText: async ({ cfg, to, text, accountId, replyToId }) => { 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, { const result = await sendMessageBlueBubbles(to, text, {
cfg: cfg as ClawdbotConfig, cfg: cfg as ClawdbotConfig,
accountId: accountId ?? undefined, accountId: accountId ?? undefined,

View File

@@ -4,8 +4,9 @@ import { fileURLToPath } from "node:url";
import { resolveChannelMediaMaxBytes, type ClawdbotConfig } from "clawdbot/plugin-sdk"; import { resolveChannelMediaMaxBytes, type ClawdbotConfig } from "clawdbot/plugin-sdk";
import { sendBlueBubblesAttachment } from "./attachments.js"; import { sendBlueBubblesAttachment } from "./attachments.js";
import { sendMessageBlueBubbles } from "./send.js"; import { resolveBlueBubblesMessageId } from "./monitor.js";
import { getBlueBubblesRuntime } from "./runtime.js"; import { getBlueBubblesRuntime } from "./runtime.js";
import { sendMessageBlueBubbles } from "./send.js";
const HTTP_URL_RE = /^https?:\/\//i; const HTTP_URL_RE = /^https?:\/\//i;
const MB = 1024 * 1024; 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({ const attachmentResult = await sendBlueBubblesAttachment({
to, to,
buffer, buffer,
filename: resolvedFilename ?? "attachment", filename: resolvedFilename ?? "attachment",
contentType: resolvedContentType ?? undefined, contentType: resolvedContentType ?? undefined,
replyToMessageGuid: replyToId?.trim() || undefined, replyToMessageGuid,
opts: { opts: {
cfg, cfg,
accountId, accountId,
@@ -151,7 +157,7 @@ export async function sendBlueBubblesMedia(params: {
await sendMessageBlueBubbles(to, trimmedCaption, { await sendMessageBlueBubbles(to, trimmedCaption, {
cfg, cfg,
accountId, accountId,
replyToMessageGuid: replyToId?.trim() || undefined, replyToMessageGuid,
}); });
} }

View File

@@ -1175,8 +1175,8 @@ describe("BlueBubbles webhook monitor", () => {
expect(callArgs.ctx.ReplyToId).toBe("msg-0"); expect(callArgs.ctx.ReplyToId).toBe("msg-0");
expect(callArgs.ctx.ReplyToBody).toBe("original message"); expect(callArgs.ctx.ReplyToBody).toBe("original message");
expect(callArgs.ctx.ReplyToSender).toBe("+15550000000"); expect(callArgs.ctx.ReplyToSender).toBe("+15550000000");
// Body still uses the full UUID since it wasn't cached // Body uses just the ID (no sender) for token savings
expect(callArgs.ctx.Body).toContain("[Replying to +15550000000 id:msg-0]"); expect(callArgs.ctx.Body).toContain("[Replying to id:msg-0]");
expect(callArgs.ctx.Body).toContain("original message"); expect(callArgs.ctx.Body).toContain("original message");
}); });
@@ -1245,8 +1245,8 @@ describe("BlueBubbles webhook monitor", () => {
expect(callArgs.ctx.ReplyToId).toBe("1"); expect(callArgs.ctx.ReplyToId).toBe("1");
expect(callArgs.ctx.ReplyToBody).toBe("original message (cached)"); expect(callArgs.ctx.ReplyToBody).toBe("original message (cached)");
expect(callArgs.ctx.ReplyToSender).toBe("+15550000000"); expect(callArgs.ctx.ReplyToSender).toBe("+15550000000");
// Body uses short ID for token savings // Body uses just the short ID (no sender) for token savings
expect(callArgs.ctx.Body).toContain("[Replying to +15550000000 id:1]"); expect(callArgs.ctx.Body).toContain("[Replying to id:1]");
expect(callArgs.ctx.Body).toContain("original message (cached)"); expect(callArgs.ctx.Body).toContain("original message (cached)");
}); });

View File

@@ -376,6 +376,8 @@ function buildMessagePlaceholder(message: NormalizedWebhookMessage): string {
return ""; return "";
} }
const REPLY_BODY_TRUNCATE_LENGTH = 60;
function formatReplyContext(message: { function formatReplyContext(message: {
replyToId?: string; replyToId?: string;
replyToShortId?: string; replyToShortId?: string;
@@ -383,15 +385,20 @@ function formatReplyContext(message: {
replyToSender?: string; replyToSender?: string;
}): string | null { }): string | null {
if (!message.replyToId && !message.replyToBody && !message.replyToSender) return null; if (!message.replyToId && !message.replyToBody && !message.replyToSender) return null;
const sender = message.replyToSender?.trim() || "unknown sender";
// Prefer short ID for token savings // Prefer short ID for token savings
const displayId = message.replyToShortId || message.replyToId; const displayId = message.replyToShortId || message.replyToId;
const idPart = displayId ? ` id:${displayId}` : ""; // Only include sender if we don't have an ID (fallback)
const body = message.replyToBody?.trim(); const label = displayId ? `id:${displayId}` : (message.replyToSender?.trim() || "unknown");
if (!body) { const rawBody = message.replyToBody?.trim();
return `[Replying to ${sender}${idPart}]\n[/Replying]`; 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<string, unknown> | null, key: string): number | undefined { function readNumberLike(record: Record<string, unknown> | null, key: string): number | undefined {
@@ -1661,8 +1668,12 @@ async function processMessage(
if (!chunks.length && payload.text) chunks.push(payload.text); if (!chunks.length && payload.text) chunks.push(payload.text);
if (!chunks.length) return; if (!chunks.length) return;
for (const chunk of chunks) { for (const chunk of chunks) {
const replyToMessageGuid = const rawReplyToId =
typeof payload.replyToId === "string" ? payload.replyToId.trim() : ""; 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, { const result = await sendMessageBlueBubbles(outboundTarget, chunk, {
cfg: config, cfg: config,
accountId: account.accountId, accountId: account.accountId,