fix(signal): fall back to toolContext.currentMessageId for reactions

Signal reactions required an explicit messageId parameter, unlike
Telegram which already fell back to toolContext.currentMessageId.
This made agent-initiated reactions fail on Signal because the
inbound message ID was available in tool context but never used.

- Destructure toolContext in Signal action handler
- Fall back to toolContext.currentMessageId when messageId omitted
- Update reaction schema descriptions (not Telegram-specific)
- Add tests for fallback and missing-messageId rejection

Closes #17651
This commit is contained in:
dunamismax 2026-03-02 17:10:40 -05:00 committed by Peter Steinberger
parent f25be781c4
commit d9fdec12ab
3 changed files with 44 additions and 8 deletions

View File

@ -242,14 +242,14 @@ function buildReactionSchema() {
messageId: Type.Optional(
Type.String({
description:
"Target message id for reaction. For Telegram, if omitted, defaults to the current inbound message id when available.",
"Target message id for reaction. If omitted, defaults to the current inbound message id when available.",
}),
),
message_id: Type.Optional(
Type.String({
// Intentional duplicate alias for tool-schema discoverability in LLMs.
description:
"snake_case alias of messageId. For Telegram, if omitted, defaults to the current inbound message id when available.",
"snake_case alias of messageId. If omitted, defaults to the current inbound message id when available.",
}),
),
emoji: Type.Optional(Type.String()),

View File

@ -61,7 +61,11 @@ type SignalActionInput = Parameters<NonNullable<typeof signalMessageActions.hand
async function runSignalAction(
action: SignalActionInput["action"],
params: SignalActionInput["params"],
options?: { cfg?: OpenClawConfig; accountId?: string },
options?: {
cfg?: OpenClawConfig;
accountId?: string;
toolContext?: SignalActionInput["toolContext"];
},
) {
const cfg =
options?.cfg ?? ({ channels: { signal: { account: "+15550001111" } } } as OpenClawConfig);
@ -75,6 +79,7 @@ async function runSignalAction(
params,
cfg,
accountId: options?.accountId,
toolContext: options?.toolContext,
});
return { cfg };
}
@ -852,6 +857,33 @@ describe("signalMessageActions", () => {
}
});
it("falls back to toolContext.currentMessageId for reactions when messageId is omitted", async () => {
sendReactionSignal.mockClear();
await runSignalAction(
"react",
{ to: "+15559999999", emoji: "🔥" },
{ toolContext: { currentMessageId: "1737630212345" } },
);
expect(sendReactionSignal).toHaveBeenCalledTimes(1);
expect(sendReactionSignal).toHaveBeenCalledWith(
"+15559999999",
1737630212345,
"🔥",
expect.objectContaining({}),
);
});
it("rejects reaction when neither messageId nor toolContext.currentMessageId is provided", async () => {
const cfg = {
channels: { signal: { account: "+15550001111" } },
} as OpenClawConfig;
await expectSignalActionRejected(
{ to: "+15559999999", emoji: "✅" },
/messageId.*required/,
cfg,
);
});
it("requires targetAuthor for group reactions", async () => {
const cfg = {
channels: { signal: { account: "+15550001111" } },

View File

@ -90,7 +90,7 @@ export const signalMessageActions: ChannelMessageActionAdapter = {
},
supportsAction: ({ action }) => action !== "send",
handleAction: async ({ action, params, cfg, accountId }) => {
handleAction: async ({ action, params, cfg, accountId, toolContext }) => {
if (action === "send") {
throw new Error("Send should be handled by outbound, not actions handler.");
}
@ -126,10 +126,14 @@ export const signalMessageActions: ChannelMessageActionAdapter = {
throw new Error("recipient or group required");
}
const messageId = readStringParam(params, "messageId", {
required: true,
label: "messageId (timestamp)",
});
const messageId =
readStringParam(params, "messageId") ??
(toolContext?.currentMessageId != null ? String(toolContext.currentMessageId) : undefined);
if (!messageId) {
throw new Error(
"messageId (timestamp) required. Provide messageId explicitly or react to the current inbound message.",
);
}
const targetAuthor = readStringParam(params, "targetAuthor");
const targetAuthorUuid = readStringParam(params, "targetAuthorUuid");
if (target.groupId && !targetAuthor && !targetAuthorUuid) {