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,
} 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<ResolvedBlueBubblesAccount> = {
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,

View File

@@ -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,
});
}

View File

@@ -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)");
});

View File

@@ -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<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) 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,