feat: implement short ID mapping for BlueBubbles messages and enhance reply context caching

- Added functionality to resolve short message IDs to full UUIDs and vice versa, optimizing token usage.
- Introduced a reply cache to store message context for replies when metadata is omitted in webhook payloads.
- Updated message handling to utilize short IDs for outbound messages and replies, improving efficiency.
- Enhanced error messages to clarify required parameters for actions like react, edit, and unsend.
- Added tests to ensure correct behavior of new features and maintain existing functionality.
This commit is contained in:
Tyler Yust
2026-01-21 00:14:55 -08:00
parent 89c5035aa2
commit b073deee20
10 changed files with 720 additions and 67 deletions

View File

@@ -1,6 +1,6 @@
import type { NormalizedUsage } from "../../agents/usage.js";
import { getChannelDock } from "../../channels/dock.js";
import type { ChannelThreadingToolContext } from "../../channels/plugins/types.js";
import type { ChannelId, ChannelThreadingToolContext } from "../../channels/plugins/types.js";
import { normalizeChannelId } from "../../channels/registry.js";
import type { ClawdbotConfig } from "../../config/config.js";
import { isReasoningTagProvider } from "../../utils/provider-utils.js";
@@ -21,17 +21,25 @@ export function buildThreadingToolContext(params: {
}): ChannelThreadingToolContext {
const { sessionCtx, config, hasRepliedRef } = params;
if (!config) return {};
const provider = normalizeChannelId(sessionCtx.Provider);
if (!provider) return {};
const dock = getChannelDock(provider);
if (!dock?.threading?.buildToolContext) return {};
const rawProvider = sessionCtx.Provider?.trim().toLowerCase();
if (!rawProvider) return {};
const provider = normalizeChannelId(rawProvider);
// WhatsApp context isolation keys off conversation id, not the bot's own number.
const threadingTo =
provider === "whatsapp"
rawProvider === "whatsapp"
? (sessionCtx.From ?? sessionCtx.To)
: provider === "imessage" && sessionCtx.ChatType === "direct"
: rawProvider === "imessage" && sessionCtx.ChatType === "direct"
? (sessionCtx.From ?? sessionCtx.To)
: sessionCtx.To;
// Fallback for unrecognized/plugin channels (e.g., BlueBubbles before plugin registry init)
const dock = provider ? getChannelDock(provider) : undefined;
if (!dock?.threading?.buildToolContext) {
return {
currentChannelId: threadingTo?.trim() || undefined,
currentChannelProvider: provider ?? (rawProvider as ChannelId),
hasRepliedRef,
};
}
const context =
dock.threading.buildToolContext({
cfg: config,
@@ -47,7 +55,7 @@ export function buildThreadingToolContext(params: {
}) ?? {};
return {
...context,
currentChannelProvider: provider,
currentChannelProvider: provider!, // guaranteed non-null since dock exists
};
}

View File

@@ -140,7 +140,7 @@ describe("createTypingSignaler", () => {
expect(typing.startTypingOnText).not.toHaveBeenCalled();
});
it("does not start typing on tool start before text", async () => {
it("starts typing on tool start before text", async () => {
const typing = createMockTypingController();
const signaler = createTypingSignaler({
typing,
@@ -150,8 +150,9 @@ describe("createTypingSignaler", () => {
await signaler.signalToolStart();
expect(typing.startTypingLoop).not.toHaveBeenCalled();
expect(typing.refreshTypingTtl).not.toHaveBeenCalled();
expect(typing.startTypingLoop).toHaveBeenCalled();
expect(typing.refreshTypingTtl).toHaveBeenCalled();
expect(typing.startTypingOnText).not.toHaveBeenCalled();
});
it("refreshes ttl on tool start when active after text", async () => {

View File

@@ -95,13 +95,13 @@ export function createTypingSignaler(params: {
const signalToolStart = async () => {
if (disabled) return;
if (!hasRenderableText) return;
// Start typing as soon as tools begin executing, even before the first text delta.
if (!typing.isActive()) {
await typing.startTypingLoop();
typing.refreshTypingTtl();
return;
}
// Keep typing indicator alive during tool execution without changing mode semantics.
// Keep typing indicator alive during tool execution.
typing.refreshTypingTtl();
};

View File

@@ -57,6 +57,7 @@ export const MESSAGE_ACTION_TARGET_MODE: Record<ChannelMessageActionName, Messag
const ACTION_TARGET_ALIASES: Partial<Record<ChannelMessageActionName, string[]>> = {
unsend: ["messageId"],
edit: ["messageId"],
react: ["chatGuid", "chatIdentifier", "chatId"],
renameGroup: ["chatGuid", "chatIdentifier", "chatId"],
setGroupIcon: ["chatGuid", "chatIdentifier", "chatId"],
addParticipant: ["chatGuid", "chatIdentifier", "chatId"],