diff --git a/extensions/bluebubbles/src/channel.ts b/extensions/bluebubbles/src/channel.ts index da594e763..f316b499f 100644 --- a/extensions/bluebubbles/src/channel.ts +++ b/extensions/bluebubbles/src/channel.ts @@ -66,7 +66,7 @@ export const bluebubblesPlugin: ChannelPlugin = { threading: { buildToolContext: ({ context, hasRepliedRef }) => ({ currentChannelId: context.To?.trim() || undefined, - currentThreadTs: context.ReplyToId, + currentThreadTs: context.ReplyToIdFull ?? context.ReplyToId, hasRepliedRef, }), }, diff --git a/extensions/bluebubbles/src/monitor.test.ts b/extensions/bluebubbles/src/monitor.test.ts index f3be7d6ed..2806bf82f 100644 --- a/extensions/bluebubbles/src/monitor.test.ts +++ b/extensions/bluebubbles/src/monitor.test.ts @@ -1243,6 +1243,7 @@ describe("BlueBubbles webhook monitor", () => { const callArgs = mockDispatchReplyWithBufferedBlockDispatcher.mock.calls[0][0]; // ReplyToId uses short ID "1" (first cached message) for token savings expect(callArgs.ctx.ReplyToId).toBe("1"); + expect(callArgs.ctx.ReplyToIdFull).toBe("cache-msg-0"); expect(callArgs.ctx.ReplyToBody).toBe("original message (cached)"); expect(callArgs.ctx.ReplyToSender).toBe("+15550000000"); // Body uses just the short ID (no sender) for token savings @@ -1812,6 +1813,7 @@ describe("BlueBubbles webhook monitor", () => { const callArgs = mockDispatchReplyWithBufferedBlockDispatcher.mock.calls[0][0]; // MessageSid should be short ID "1" instead of full UUID expect(callArgs.ctx.MessageSid).toBe("1"); + expect(callArgs.ctx.MessageSidFull).toBe("msg-uuid-12345"); }); it("resolves short ID back to UUID", async () => { diff --git a/extensions/bluebubbles/src/monitor.ts b/extensions/bluebubbles/src/monitor.ts index c7a4188c0..083587409 100644 --- a/extensions/bluebubbles/src/monitor.ts +++ b/extensions/bluebubbles/src/monitor.ts @@ -1110,11 +1110,10 @@ async function processMessage( const placeholder = buildMessagePlaceholder(message); const rawBody = text || placeholder; - // Cache messages (including fromMe) so later replies can resolve sender/body even when - // BlueBubbles webhook payloads omit nested reply metadata. const cacheMessageId = message.messageId?.trim(); let messageShortId: string | undefined; - if (cacheMessageId) { + const cacheInboundMessage = () => { + if (!cacheMessageId) return; const cacheEntry = rememberBlueBubblesReplyCache({ accountId: account.accountId, messageId: cacheMessageId, @@ -1126,9 +1125,13 @@ async function processMessage( timestamp: message.timestamp ?? Date.now(), }); messageShortId = cacheEntry.shortId; - } + }; - if (message.fromMe) return; + if (message.fromMe) { + // Cache from-me messages so reply context can resolve sender/body. + cacheInboundMessage(); + return; + } if (!rawBody) { logVerbose(core, runtime, `drop: empty text sender=${message.senderId}`); @@ -1370,6 +1373,10 @@ async function processMessage( return; } + // Cache allowed inbound messages so later replies can resolve sender/body without + // surfacing dropped content (allowlist/mention/command gating). + cacheInboundMessage(); + const baseUrl = account.config.serverUrl?.trim(); const password = account.config.password?.trim(); const maxBytes = @@ -1610,6 +1617,7 @@ async function processMessage( ConversationLabel: fromLabel, // Use short ID for token savings (agent can use this to reference the message) ReplyToId: replyToShortId || replyToId, + ReplyToIdFull: replyToId, ReplyToBody: replyToBody, ReplyToSender: replyToSender, GroupSubject: groupSubject, @@ -1620,6 +1628,7 @@ async function processMessage( Surface: "bluebubbles", // Use short ID for token savings (agent can use this to reference the message) MessageSid: messageShortId || message.messageId, + MessageSidFull: message.messageId, Timestamp: message.timestamp, OriginatingChannel: "bluebubbles", OriginatingTo: `bluebubbles:${outboundTarget}`, @@ -1634,6 +1643,11 @@ async function processMessage( cfg: config, dispatcherOptions: { deliver: async (payload) => { + const rawReplyToId = typeof payload.replyToId === "string" ? payload.replyToId.trim() : ""; + // Resolve short ID (e.g., "5") to full UUID + const replyToMessageGuid = rawReplyToId + ? resolveBlueBubblesMessageId(rawReplyToId) + : ""; const mediaList = payload.mediaUrls?.length ? payload.mediaUrls : payload.mediaUrl @@ -1649,7 +1663,7 @@ async function processMessage( to: outboundTarget, mediaUrl, caption: caption ?? undefined, - replyToId: payload.replyToId ?? null, + replyToId: replyToMessageGuid || null, accountId: account.accountId, }); const cachedBody = (caption ?? "").trim() || ""; @@ -1668,12 +1682,6 @@ async function processMessage( if (!chunks.length && payload.text) chunks.push(payload.text); if (!chunks.length) return; for (const chunk of chunks) { - 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, diff --git a/src/auto-reply/reply/agent-runner-execution.ts b/src/auto-reply/reply/agent-runner-execution.ts index bbdf6df0d..0e7dfa233 100644 --- a/src/auto-reply/reply/agent-runner-execution.ts +++ b/src/auto-reply/reply/agent-runner-execution.ts @@ -299,6 +299,8 @@ export async function runAgentTurnWithFallback(params: { const { text, skip } = normalizeStreamingText(payload); const hasPayloadMedia = (payload.mediaUrls?.length ?? 0) > 0; if (skip && !hasPayloadMedia) return; + const currentMessageId = + params.sessionCtx.MessageSidFull ?? params.sessionCtx.MessageSid; const taggedPayload = applyReplyTagsToPayload( { text, @@ -308,12 +310,12 @@ export async function runAgentTurnWithFallback(params: { replyToTag: payload.replyToTag, replyToCurrent: payload.replyToCurrent, }, - params.sessionCtx.MessageSid, + currentMessageId, ); // Let through payloads with audioAsVoice flag even if empty (need to track it) if (!isRenderablePayload(taggedPayload) && !payload.audioAsVoice) return; const parsed = parseReplyDirectives(taggedPayload.text ?? "", { - currentMessageId: params.sessionCtx.MessageSid, + currentMessageId, silentToken: SILENT_REPLY_TOKEN, }); const cleaned = parsed.text || undefined; diff --git a/src/auto-reply/reply/agent-runner.ts b/src/auto-reply/reply/agent-runner.ts index da73bf9de..1da6d3957 100644 --- a/src/auto-reply/reply/agent-runner.ts +++ b/src/auto-reply/reply/agent-runner.ts @@ -377,7 +377,7 @@ export async function runReplyAgent(params: { directlySentBlockKeys, replyToMode, replyToChannel, - currentMessageId: sessionCtx.MessageSid, + currentMessageId: sessionCtx.MessageSidFull ?? sessionCtx.MessageSid, messageProvider: followupRun.run.messageProvider, messagingToolSentTexts: runResult.messagingToolSentTexts, messagingToolSentTargets: runResult.messagingToolSentTargets, diff --git a/src/auto-reply/reply/get-reply-run.ts b/src/auto-reply/reply/get-reply-run.ts index aa2281de6..1b0bfd4eb 100644 --- a/src/auto-reply/reply/get-reply-run.ts +++ b/src/auto-reply/reply/get-reply-run.ts @@ -350,7 +350,7 @@ export async function runPreparedReply( const authProfileIdSource = sessionEntry?.authProfileOverrideSource; const followupRun = { prompt: queuedBody, - messageId: sessionCtx.MessageSid, + messageId: sessionCtx.MessageSidFull ?? sessionCtx.MessageSid, summaryLine: baseBodyTrimmedRaw, enqueuedAt: Date.now(), // Originating channel for reply routing. diff --git a/src/auto-reply/templating.ts b/src/auto-reply/templating.ts index 375ba6759..605a0fa69 100644 --- a/src/auto-reply/templating.ts +++ b/src/auto-reply/templating.ts @@ -38,10 +38,14 @@ export type MsgContext = { AccountId?: string; ParentSessionKey?: string; MessageSid?: string; + /** Provider-specific full message id when MessageSid is a shortened alias. */ + MessageSidFull?: string; MessageSids?: string[]; MessageSidFirst?: string; MessageSidLast?: string; ReplyToId?: string; + /** Provider-specific full reply-to id when ReplyToId is a shortened alias. */ + ReplyToIdFull?: string; ReplyToBody?: string; ReplyToSender?: string; ForwardedFrom?: string; diff --git a/src/channels/plugins/types.core.ts b/src/channels/plugins/types.core.ts index 3f6600620..dced4e14c 100644 --- a/src/channels/plugins/types.core.ts +++ b/src/channels/plugins/types.core.ts @@ -212,6 +212,7 @@ export type ChannelThreadingContext = { Channel?: string; To?: string; ReplyToId?: string; + ReplyToIdFull?: string; ThreadLabel?: string; MessageThreadId?: string | number; };