From 13d1712850e0e31d8f6b605de082c3d9e0b63fc2 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 23 Jan 2026 08:42:48 +0000 Subject: [PATCH] fix: honor accountId in message actions --- src/agents/tools/cron-tool.test.ts | 22 ++ src/agents/tools/cron-tool.ts | 10 + src/agents/tools/discord-actions-guild.ts | 260 +++++++++++++----- src/agents/tools/discord-actions-messaging.ts | 132 ++++++--- .../tools/discord-actions-moderation.ts | 56 +++- src/agents/tools/discord-actions.test.ts | 55 ++++ src/agents/tools/message-tool.ts | 3 + src/channels/plugins/actions/discord.test.ts | 103 ++++++- src/channels/plugins/actions/discord.ts | 4 +- .../discord/handle-action.guild-admin.ts | 78 +++++- .../plugins/actions/discord/handle-action.ts | 35 ++- src/cron/isolated-agent/run.ts | 1 + .../outbound/message-action-runner.test.ts | 62 +++++ src/infra/outbound/message-action-runner.ts | 3 + 14 files changed, 688 insertions(+), 136 deletions(-) diff --git a/src/agents/tools/cron-tool.test.ts b/src/agents/tools/cron-tool.test.ts index 4b7cd6615..640520239 100644 --- a/src/agents/tools/cron-tool.test.ts +++ b/src/agents/tools/cron-tool.test.ts @@ -188,4 +188,26 @@ describe("cron tool", () => { const text = cronCall.params?.payload?.text ?? ""; expect(text).not.toContain("Recent context:"); }); + + it("preserves explicit agentId null on add", async () => { + callGatewayMock.mockResolvedValueOnce({ ok: true }); + + const tool = createCronTool({ agentSessionKey: "main" }); + await tool.execute("call6", { + action: "add", + job: { + name: "reminder", + schedule: { atMs: 123 }, + agentId: null, + payload: { kind: "systemEvent", text: "Reminder: the thing." }, + }, + }); + + const call = callGatewayMock.mock.calls[0]?.[0] as { + method?: string; + params?: { agentId?: string | null }; + }; + expect(call.method).toBe("cron.add"); + expect(call.params?.agentId).toBeNull(); + }); }); diff --git a/src/agents/tools/cron-tool.ts b/src/agents/tools/cron-tool.ts index e8995a0b9..a1d218dd7 100644 --- a/src/agents/tools/cron-tool.ts +++ b/src/agents/tools/cron-tool.ts @@ -3,6 +3,7 @@ import { normalizeCronJobCreate, normalizeCronJobPatch } from "../../cron/normal import { loadConfig } from "../../config/config.js"; import { truncateUtf16Safe } from "../../utils.js"; import { optionalStringEnum, stringEnum } from "../schema/typebox.js"; +import { resolveSessionAgentId } from "../agent-scope.js"; import { type AnyAgentTool, jsonResult, readStringParam } from "./common.js"; import { callGatewayTool, type GatewayCallOptions } from "./gateway.js"; import { resolveInternalSessionKey, resolveMainSessionAlias } from "./sessions-helpers.js"; @@ -158,6 +159,15 @@ export function createCronTool(opts?: CronToolOptions): AnyAgentTool { throw new Error("job required"); } const job = normalizeCronJobCreate(params.job) ?? params.job; + if (job && typeof job === "object" && !("agentId" in job)) { + const cfg = loadConfig(); + const agentId = opts?.agentSessionKey + ? resolveSessionAgentId({ sessionKey: opts.agentSessionKey, config: cfg }) + : undefined; + if (agentId) { + (job as { agentId?: string }).agentId = agentId; + } + } const contextMessages = typeof params.contextMessages === "number" && Number.isFinite(params.contextMessages) ? params.contextMessages diff --git a/src/agents/tools/discord-actions-guild.ts b/src/agents/tools/discord-actions-guild.ts index cf43f90af..0994829bd 100644 --- a/src/agents/tools/discord-actions-guild.ts +++ b/src/agents/tools/discord-actions-guild.ts @@ -39,6 +39,7 @@ export async function handleDiscordGuildAction( params: Record, isActionEnabled: ActionGate, ): Promise> { + const accountId = readStringParam(params, "accountId"); switch (action) { case "memberInfo": { if (!isActionEnabled("memberInfo")) { @@ -50,7 +51,9 @@ export async function handleDiscordGuildAction( const userId = readStringParam(params, "userId", { required: true, }); - const member = await fetchMemberInfoDiscord(guildId, userId); + const member = accountId + ? await fetchMemberInfoDiscord(guildId, userId, { accountId }) + : await fetchMemberInfoDiscord(guildId, userId); return jsonResult({ ok: true, member }); } case "roleInfo": { @@ -60,7 +63,9 @@ export async function handleDiscordGuildAction( const guildId = readStringParam(params, "guildId", { required: true, }); - const roles = await fetchRoleInfoDiscord(guildId); + const roles = accountId + ? await fetchRoleInfoDiscord(guildId, { accountId }) + : await fetchRoleInfoDiscord(guildId); return jsonResult({ ok: true, roles }); } case "emojiList": { @@ -70,7 +75,9 @@ export async function handleDiscordGuildAction( const guildId = readStringParam(params, "guildId", { required: true, }); - const emojis = await listGuildEmojisDiscord(guildId); + const emojis = accountId + ? await listGuildEmojisDiscord(guildId, { accountId }) + : await listGuildEmojisDiscord(guildId); return jsonResult({ ok: true, emojis }); } case "emojiUpload": { @@ -85,12 +92,22 @@ export async function handleDiscordGuildAction( required: true, }); const roleIds = readStringArrayParam(params, "roleIds"); - const emoji = await uploadEmojiDiscord({ - guildId, - name, - mediaUrl, - roleIds: roleIds?.length ? roleIds : undefined, - }); + const emoji = accountId + ? await uploadEmojiDiscord( + { + guildId, + name, + mediaUrl, + roleIds: roleIds?.length ? roleIds : undefined, + }, + { accountId }, + ) + : await uploadEmojiDiscord({ + guildId, + name, + mediaUrl, + roleIds: roleIds?.length ? roleIds : undefined, + }); return jsonResult({ ok: true, emoji }); } case "stickerUpload": { @@ -108,13 +125,24 @@ export async function handleDiscordGuildAction( const mediaUrl = readStringParam(params, "mediaUrl", { required: true, }); - const sticker = await uploadStickerDiscord({ - guildId, - name, - description, - tags, - mediaUrl, - }); + const sticker = accountId + ? await uploadStickerDiscord( + { + guildId, + name, + description, + tags, + mediaUrl, + }, + { accountId }, + ) + : await uploadStickerDiscord({ + guildId, + name, + description, + tags, + mediaUrl, + }); return jsonResult({ ok: true, sticker }); } case "roleAdd": { @@ -128,7 +156,11 @@ export async function handleDiscordGuildAction( required: true, }); const roleId = readStringParam(params, "roleId", { required: true }); - await addRoleDiscord({ guildId, userId, roleId }); + if (accountId) { + await addRoleDiscord({ guildId, userId, roleId }, { accountId }); + } else { + await addRoleDiscord({ guildId, userId, roleId }); + } return jsonResult({ ok: true }); } case "roleRemove": { @@ -142,7 +174,11 @@ export async function handleDiscordGuildAction( required: true, }); const roleId = readStringParam(params, "roleId", { required: true }); - await removeRoleDiscord({ guildId, userId, roleId }); + if (accountId) { + await removeRoleDiscord({ guildId, userId, roleId }, { accountId }); + } else { + await removeRoleDiscord({ guildId, userId, roleId }); + } return jsonResult({ ok: true }); } case "channelInfo": { @@ -152,7 +188,9 @@ export async function handleDiscordGuildAction( const channelId = readStringParam(params, "channelId", { required: true, }); - const channel = await fetchChannelInfoDiscord(channelId); + const channel = accountId + ? await fetchChannelInfoDiscord(channelId, { accountId }) + : await fetchChannelInfoDiscord(channelId); return jsonResult({ ok: true, channel }); } case "channelList": { @@ -162,7 +200,9 @@ export async function handleDiscordGuildAction( const guildId = readStringParam(params, "guildId", { required: true, }); - const channels = await listGuildChannelsDiscord(guildId); + const channels = accountId + ? await listGuildChannelsDiscord(guildId, { accountId }) + : await listGuildChannelsDiscord(guildId); return jsonResult({ ok: true, channels }); } case "voiceStatus": { @@ -175,7 +215,9 @@ export async function handleDiscordGuildAction( const userId = readStringParam(params, "userId", { required: true, }); - const voice = await fetchVoiceStatusDiscord(guildId, userId); + const voice = accountId + ? await fetchVoiceStatusDiscord(guildId, userId, { accountId }) + : await fetchVoiceStatusDiscord(guildId, userId); return jsonResult({ ok: true, voice }); } case "eventList": { @@ -185,7 +227,9 @@ export async function handleDiscordGuildAction( const guildId = readStringParam(params, "guildId", { required: true, }); - const events = await listScheduledEventsDiscord(guildId); + const events = accountId + ? await listScheduledEventsDiscord(guildId, { accountId }) + : await listScheduledEventsDiscord(guildId); return jsonResult({ ok: true, events }); } case "eventCreate": { @@ -215,7 +259,9 @@ export async function handleDiscordGuildAction( entity_metadata: entityType === 3 && location ? { location } : undefined, privacy_level: 2, }; - const event = await createScheduledEventDiscord(guildId, payload); + const event = accountId + ? await createScheduledEventDiscord(guildId, payload, { accountId }) + : await createScheduledEventDiscord(guildId, payload); return jsonResult({ ok: true, event }); } case "channelCreate": { @@ -229,15 +275,28 @@ export async function handleDiscordGuildAction( const topic = readStringParam(params, "topic"); const position = readNumberParam(params, "position", { integer: true }); const nsfw = params.nsfw as boolean | undefined; - const channel = await createChannelDiscord({ - guildId, - name, - type: type ?? undefined, - parentId: parentId ?? undefined, - topic: topic ?? undefined, - position: position ?? undefined, - nsfw, - }); + const channel = accountId + ? await createChannelDiscord( + { + guildId, + name, + type: type ?? undefined, + parentId: parentId ?? undefined, + topic: topic ?? undefined, + position: position ?? undefined, + nsfw, + }, + { accountId }, + ) + : await createChannelDiscord({ + guildId, + name, + type: type ?? undefined, + parentId: parentId ?? undefined, + topic: topic ?? undefined, + position: position ?? undefined, + nsfw, + }); return jsonResult({ ok: true, channel }); } case "channelEdit": { @@ -255,15 +314,28 @@ export async function handleDiscordGuildAction( const rateLimitPerUser = readNumberParam(params, "rateLimitPerUser", { integer: true, }); - const channel = await editChannelDiscord({ - channelId, - name: name ?? undefined, - topic: topic ?? undefined, - position: position ?? undefined, - parentId, - nsfw, - rateLimitPerUser: rateLimitPerUser ?? undefined, - }); + const channel = accountId + ? await editChannelDiscord( + { + channelId, + name: name ?? undefined, + topic: topic ?? undefined, + position: position ?? undefined, + parentId, + nsfw, + rateLimitPerUser: rateLimitPerUser ?? undefined, + }, + { accountId }, + ) + : await editChannelDiscord({ + channelId, + name: name ?? undefined, + topic: topic ?? undefined, + position: position ?? undefined, + parentId, + nsfw, + rateLimitPerUser: rateLimitPerUser ?? undefined, + }); return jsonResult({ ok: true, channel }); } case "channelDelete": { @@ -273,7 +345,9 @@ export async function handleDiscordGuildAction( const channelId = readStringParam(params, "channelId", { required: true, }); - const result = await deleteChannelDiscord(channelId); + const result = accountId + ? await deleteChannelDiscord(channelId, { accountId }) + : await deleteChannelDiscord(channelId); return jsonResult(result); } case "channelMove": { @@ -286,12 +360,24 @@ export async function handleDiscordGuildAction( }); const parentId = readParentIdParam(params); const position = readNumberParam(params, "position", { integer: true }); - await moveChannelDiscord({ - guildId, - channelId, - parentId, - position: position ?? undefined, - }); + if (accountId) { + await moveChannelDiscord( + { + guildId, + channelId, + parentId, + position: position ?? undefined, + }, + { accountId }, + ); + } else { + await moveChannelDiscord({ + guildId, + channelId, + parentId, + position: position ?? undefined, + }); + } return jsonResult({ ok: true }); } case "categoryCreate": { @@ -301,12 +387,22 @@ export async function handleDiscordGuildAction( const guildId = readStringParam(params, "guildId", { required: true }); const name = readStringParam(params, "name", { required: true }); const position = readNumberParam(params, "position", { integer: true }); - const channel = await createChannelDiscord({ - guildId, - name, - type: 4, - position: position ?? undefined, - }); + const channel = accountId + ? await createChannelDiscord( + { + guildId, + name, + type: 4, + position: position ?? undefined, + }, + { accountId }, + ) + : await createChannelDiscord({ + guildId, + name, + type: 4, + position: position ?? undefined, + }); return jsonResult({ ok: true, category: channel }); } case "categoryEdit": { @@ -318,11 +414,20 @@ export async function handleDiscordGuildAction( }); const name = readStringParam(params, "name"); const position = readNumberParam(params, "position", { integer: true }); - const channel = await editChannelDiscord({ - channelId: categoryId, - name: name ?? undefined, - position: position ?? undefined, - }); + const channel = accountId + ? await editChannelDiscord( + { + channelId: categoryId, + name: name ?? undefined, + position: position ?? undefined, + }, + { accountId }, + ) + : await editChannelDiscord({ + channelId: categoryId, + name: name ?? undefined, + position: position ?? undefined, + }); return jsonResult({ ok: true, category: channel }); } case "categoryDelete": { @@ -332,7 +437,9 @@ export async function handleDiscordGuildAction( const categoryId = readStringParam(params, "categoryId", { required: true, }); - const result = await deleteChannelDiscord(categoryId); + const result = accountId + ? await deleteChannelDiscord(categoryId, { accountId }) + : await deleteChannelDiscord(categoryId); return jsonResult(result); } case "channelPermissionSet": { @@ -349,13 +456,26 @@ export async function handleDiscordGuildAction( const targetType = targetTypeRaw === "member" ? 1 : 0; const allow = readStringParam(params, "allow"); const deny = readStringParam(params, "deny"); - await setChannelPermissionDiscord({ - channelId, - targetId, - targetType, - allow: allow ?? undefined, - deny: deny ?? undefined, - }); + if (accountId) { + await setChannelPermissionDiscord( + { + channelId, + targetId, + targetType, + allow: allow ?? undefined, + deny: deny ?? undefined, + }, + { accountId }, + ); + } else { + await setChannelPermissionDiscord({ + channelId, + targetId, + targetType, + allow: allow ?? undefined, + deny: deny ?? undefined, + }); + } return jsonResult({ ok: true }); } case "channelPermissionRemove": { @@ -366,7 +486,11 @@ export async function handleDiscordGuildAction( required: true, }); const targetId = readStringParam(params, "targetId", { required: true }); - await removeChannelPermissionDiscord(channelId, targetId); + if (accountId) { + await removeChannelPermissionDiscord(channelId, targetId, { accountId }); + } else { + await removeChannelPermissionDiscord(channelId, targetId); + } return jsonResult({ ok: true }); } default: diff --git a/src/agents/tools/discord-actions-messaging.ts b/src/agents/tools/discord-actions-messaging.ts index f552f17fd..f90fb60de 100644 --- a/src/agents/tools/discord-actions-messaging.ts +++ b/src/agents/tools/discord-actions-messaging.ts @@ -58,6 +58,7 @@ export async function handleDiscordMessagingAction( required: true, }), ); + const accountId = readStringParam(params, "accountId"); const normalizeMessage = (message: unknown) => { if (!message || typeof message !== "object") return message; return withNormalizedTimestamp( @@ -78,14 +79,24 @@ export async function handleDiscordMessagingAction( removeErrorMessage: "Emoji is required to remove a Discord reaction.", }); if (remove) { - await removeReactionDiscord(channelId, messageId, emoji); + if (accountId) { + await removeReactionDiscord(channelId, messageId, emoji, { accountId }); + } else { + await removeReactionDiscord(channelId, messageId, emoji); + } return jsonResult({ ok: true, removed: emoji }); } if (isEmpty) { - const removed = await removeOwnReactionsDiscord(channelId, messageId); + const removed = accountId + ? await removeOwnReactionsDiscord(channelId, messageId, { accountId }) + : await removeOwnReactionsDiscord(channelId, messageId); return jsonResult({ ok: true, removed: removed.removed }); } - await reactMessageDiscord(channelId, messageId, emoji); + if (accountId) { + await reactMessageDiscord(channelId, messageId, emoji, { accountId }); + } else { + await reactMessageDiscord(channelId, messageId, emoji); + } return jsonResult({ ok: true, added: emoji }); } case "reactions": { @@ -100,6 +111,7 @@ export async function handleDiscordMessagingAction( const limit = typeof limitRaw === "number" && Number.isFinite(limitRaw) ? limitRaw : undefined; const reactions = await fetchReactionsDiscord(channelId, messageId, { + ...(accountId ? { accountId } : {}), limit, }); return jsonResult({ ok: true, reactions }); @@ -114,7 +126,10 @@ export async function handleDiscordMessagingAction( required: true, label: "stickerIds", }); - await sendStickerDiscord(to, stickerIds, { content }); + await sendStickerDiscord(to, stickerIds, { + ...(accountId ? { accountId } : {}), + content, + }); return jsonResult({ ok: true }); } case "poll": { @@ -140,7 +155,7 @@ export async function handleDiscordMessagingAction( await sendPollDiscord( to, { question, options: answers, maxSelections, durationHours }, - { content }, + { ...(accountId ? { accountId } : {}), content }, ); return jsonResult({ ok: true }); } @@ -149,7 +164,9 @@ export async function handleDiscordMessagingAction( throw new Error("Discord permissions are disabled."); } const channelId = resolveChannelId(); - const permissions = await fetchChannelPermissionsDiscord(channelId); + const permissions = accountId + ? await fetchChannelPermissionsDiscord(channelId, { accountId }) + : await fetchChannelPermissionsDiscord(channelId); return jsonResult({ ok: true, permissions }); } case "fetchMessage": { @@ -171,7 +188,9 @@ export async function handleDiscordMessagingAction( "Discord message fetch requires guildId, channelId, and messageId (or a valid messageLink).", ); } - const message = await fetchMessageDiscord(channelId, messageId); + const message = accountId + ? await fetchMessageDiscord(channelId, messageId, { accountId }) + : await fetchMessageDiscord(channelId, messageId); return jsonResult({ ok: true, message: normalizeMessage(message), @@ -185,7 +204,7 @@ export async function handleDiscordMessagingAction( throw new Error("Discord message reads are disabled."); } const channelId = resolveChannelId(); - const messages = await readMessagesDiscord(channelId, { + const query = { limit: typeof params.limit === "number" && Number.isFinite(params.limit) ? params.limit @@ -193,7 +212,10 @@ export async function handleDiscordMessagingAction( before: readStringParam(params, "before"), after: readStringParam(params, "after"), around: readStringParam(params, "around"), - }); + }; + const messages = accountId + ? await readMessagesDiscord(channelId, query, { accountId }) + : await readMessagesDiscord(channelId, query); return jsonResult({ ok: true, messages: messages.map((message) => normalizeMessage(message)), @@ -212,6 +234,7 @@ export async function handleDiscordMessagingAction( const embeds = Array.isArray(params.embeds) && params.embeds.length > 0 ? params.embeds : undefined; const result = await sendMessageDiscord(to, content, { + ...(accountId ? { accountId } : {}), mediaUrl, replyTo, embeds, @@ -229,9 +252,9 @@ export async function handleDiscordMessagingAction( const content = readStringParam(params, "content", { required: true, }); - const message = await editMessageDiscord(channelId, messageId, { - content, - }); + const message = accountId + ? await editMessageDiscord(channelId, messageId, { content }, { accountId }) + : await editMessageDiscord(channelId, messageId, { content }); return jsonResult({ ok: true, message }); } case "deleteMessage": { @@ -242,7 +265,11 @@ export async function handleDiscordMessagingAction( const messageId = readStringParam(params, "messageId", { required: true, }); - await deleteMessageDiscord(channelId, messageId); + if (accountId) { + await deleteMessageDiscord(channelId, messageId, { accountId }); + } else { + await deleteMessageDiscord(channelId, messageId); + } return jsonResult({ ok: true }); } case "threadCreate": { @@ -257,11 +284,13 @@ export async function handleDiscordMessagingAction( typeof autoArchiveMinutesRaw === "number" && Number.isFinite(autoArchiveMinutesRaw) ? autoArchiveMinutesRaw : undefined; - const thread = await createThreadDiscord(channelId, { - name, - messageId, - autoArchiveMinutes, - }); + const thread = accountId + ? await createThreadDiscord( + channelId, + { name, messageId, autoArchiveMinutes }, + { accountId }, + ) + : await createThreadDiscord(channelId, { name, messageId, autoArchiveMinutes }); return jsonResult({ ok: true, thread }); } case "threadList": { @@ -279,13 +308,24 @@ export async function handleDiscordMessagingAction( typeof params.limit === "number" && Number.isFinite(params.limit) ? params.limit : undefined; - const threads = await listThreadsDiscord({ - guildId, - channelId, - includeArchived, - before, - limit, - }); + const threads = accountId + ? await listThreadsDiscord( + { + guildId, + channelId, + includeArchived, + before, + limit, + }, + { accountId }, + ) + : await listThreadsDiscord({ + guildId, + channelId, + includeArchived, + before, + limit, + }); return jsonResult({ ok: true, threads }); } case "threadReply": { @@ -299,6 +339,7 @@ export async function handleDiscordMessagingAction( const mediaUrl = readStringParam(params, "mediaUrl"); const replyTo = readStringParam(params, "replyTo"); const result = await sendMessageDiscord(`channel:${channelId}`, content, { + ...(accountId ? { accountId } : {}), mediaUrl, replyTo, }); @@ -312,7 +353,11 @@ export async function handleDiscordMessagingAction( const messageId = readStringParam(params, "messageId", { required: true, }); - await pinMessageDiscord(channelId, messageId); + if (accountId) { + await pinMessageDiscord(channelId, messageId, { accountId }); + } else { + await pinMessageDiscord(channelId, messageId); + } return jsonResult({ ok: true }); } case "unpinMessage": { @@ -323,7 +368,11 @@ export async function handleDiscordMessagingAction( const messageId = readStringParam(params, "messageId", { required: true, }); - await unpinMessageDiscord(channelId, messageId); + if (accountId) { + await unpinMessageDiscord(channelId, messageId, { accountId }); + } else { + await unpinMessageDiscord(channelId, messageId); + } return jsonResult({ ok: true }); } case "listPins": { @@ -331,7 +380,9 @@ export async function handleDiscordMessagingAction( throw new Error("Discord pins are disabled."); } const channelId = resolveChannelId(); - const pins = await listPinsDiscord(channelId); + const pins = accountId + ? await listPinsDiscord(channelId, { accountId }) + : await listPinsDiscord(channelId); return jsonResult({ ok: true, pins: pins.map((pin) => normalizeMessage(pin)) }); } case "searchMessages": { @@ -354,13 +405,24 @@ export async function handleDiscordMessagingAction( : undefined; const channelIdList = [...(channelIds ?? []), ...(channelId ? [channelId] : [])]; const authorIdList = [...(authorIds ?? []), ...(authorId ? [authorId] : [])]; - const results = await searchMessagesDiscord({ - guildId, - content, - channelIds: channelIdList.length ? channelIdList : undefined, - authorIds: authorIdList.length ? authorIdList : undefined, - limit, - }); + const results = accountId + ? await searchMessagesDiscord( + { + guildId, + content, + channelIds: channelIdList.length ? channelIdList : undefined, + authorIds: authorIdList.length ? authorIdList : undefined, + limit, + }, + { accountId }, + ) + : await searchMessagesDiscord({ + guildId, + content, + channelIds: channelIdList.length ? channelIdList : undefined, + authorIds: authorIdList.length ? authorIdList : undefined, + limit, + }); if (!results || typeof results !== "object") { return jsonResult({ ok: true, results }); } diff --git a/src/agents/tools/discord-actions-moderation.ts b/src/agents/tools/discord-actions-moderation.ts index 260ce85ea..bd3a1e4b3 100644 --- a/src/agents/tools/discord-actions-moderation.ts +++ b/src/agents/tools/discord-actions-moderation.ts @@ -8,6 +8,7 @@ export async function handleDiscordModerationAction( params: Record, isActionEnabled: ActionGate, ): Promise> { + const accountId = readStringParam(params, "accountId"); switch (action) { case "timeout": { if (!isActionEnabled("moderation", false)) { @@ -25,13 +26,24 @@ export async function handleDiscordModerationAction( : undefined; const until = readStringParam(params, "until"); const reason = readStringParam(params, "reason"); - const member = await timeoutMemberDiscord({ - guildId, - userId, - durationMinutes, - until, - reason, - }); + const member = accountId + ? await timeoutMemberDiscord( + { + guildId, + userId, + durationMinutes, + until, + reason, + }, + { accountId }, + ) + : await timeoutMemberDiscord({ + guildId, + userId, + durationMinutes, + until, + reason, + }); return jsonResult({ ok: true, member }); } case "kick": { @@ -45,7 +57,11 @@ export async function handleDiscordModerationAction( required: true, }); const reason = readStringParam(params, "reason"); - await kickMemberDiscord({ guildId, userId, reason }); + if (accountId) { + await kickMemberDiscord({ guildId, userId, reason }, { accountId }); + } else { + await kickMemberDiscord({ guildId, userId, reason }); + } return jsonResult({ ok: true }); } case "ban": { @@ -63,12 +79,24 @@ export async function handleDiscordModerationAction( typeof params.deleteMessageDays === "number" && Number.isFinite(params.deleteMessageDays) ? params.deleteMessageDays : undefined; - await banMemberDiscord({ - guildId, - userId, - reason, - deleteMessageDays, - }); + if (accountId) { + await banMemberDiscord( + { + guildId, + userId, + reason, + deleteMessageDays, + }, + { accountId }, + ); + } else { + await banMemberDiscord({ + guildId, + userId, + reason, + deleteMessageDays, + }); + } return jsonResult({ ok: true }); } default: diff --git a/src/agents/tools/discord-actions.test.ts b/src/agents/tools/discord-actions.test.ts index 3eead3f40..0a04fcd6e 100644 --- a/src/agents/tools/discord-actions.test.ts +++ b/src/agents/tools/discord-actions.test.ts @@ -3,6 +3,7 @@ import { describe, expect, it, vi } from "vitest"; import type { DiscordActionConfig } from "../../config/config.js"; import { handleDiscordGuildAction } from "./discord-actions-guild.js"; import { handleDiscordMessagingAction } from "./discord-actions-messaging.js"; +import { handleDiscordModerationAction } from "./discord-actions-moderation.js"; const createChannelDiscord = vi.fn(async () => ({ id: "new-channel", @@ -35,8 +36,12 @@ const sendPollDiscord = vi.fn(async () => ({})); const sendStickerDiscord = vi.fn(async () => ({})); const setChannelPermissionDiscord = vi.fn(async () => ({ ok: true })); const unpinMessageDiscord = vi.fn(async () => ({})); +const timeoutMemberDiscord = vi.fn(async () => ({})); +const kickMemberDiscord = vi.fn(async () => ({})); +const banMemberDiscord = vi.fn(async () => ({})); vi.mock("../../discord/send.js", () => ({ + banMemberDiscord: (...args: unknown[]) => banMemberDiscord(...args), createChannelDiscord: (...args: unknown[]) => createChannelDiscord(...args), createThreadDiscord: (...args: unknown[]) => createThreadDiscord(...args), deleteChannelDiscord: (...args: unknown[]) => deleteChannelDiscord(...args), @@ -46,6 +51,7 @@ vi.mock("../../discord/send.js", () => ({ fetchMessageDiscord: (...args: unknown[]) => fetchMessageDiscord(...args), fetchChannelPermissionsDiscord: (...args: unknown[]) => fetchChannelPermissionsDiscord(...args), fetchReactionsDiscord: (...args: unknown[]) => fetchReactionsDiscord(...args), + kickMemberDiscord: (...args: unknown[]) => kickMemberDiscord(...args), listPinsDiscord: (...args: unknown[]) => listPinsDiscord(...args), listThreadsDiscord: (...args: unknown[]) => listThreadsDiscord(...args), moveChannelDiscord: (...args: unknown[]) => moveChannelDiscord(...args), @@ -60,12 +66,15 @@ vi.mock("../../discord/send.js", () => ({ sendPollDiscord: (...args: unknown[]) => sendPollDiscord(...args), sendStickerDiscord: (...args: unknown[]) => sendStickerDiscord(...args), setChannelPermissionDiscord: (...args: unknown[]) => setChannelPermissionDiscord(...args), + timeoutMemberDiscord: (...args: unknown[]) => timeoutMemberDiscord(...args), unpinMessageDiscord: (...args: unknown[]) => unpinMessageDiscord(...args), })); const enableAllActions = () => true; const disabledActions = (key: keyof DiscordActionConfig) => key !== "reactions"; +const channelInfoEnabled = (key: keyof DiscordActionConfig) => key === "channelInfo"; +const moderationEnabled = (key: keyof DiscordActionConfig) => key === "moderation"; describe("handleDiscordMessagingAction", () => { it("adds reactions", async () => { @@ -81,6 +90,20 @@ describe("handleDiscordMessagingAction", () => { expect(reactMessageDiscord).toHaveBeenCalledWith("C1", "M1", "✅"); }); + it("forwards accountId for reactions", async () => { + await handleDiscordMessagingAction( + "react", + { + channelId: "C1", + messageId: "M1", + emoji: "✅", + accountId: "ops", + }, + enableAllActions, + ); + expect(reactMessageDiscord).toHaveBeenCalledWith("C1", "M1", "✅", { accountId: "ops" }); + }); + it("removes reactions on empty emoji", async () => { await handleDiscordMessagingAction( "react", @@ -245,6 +268,15 @@ describe("handleDiscordGuildAction - channel management", () => { ).rejects.toThrow(/Discord channel management is disabled/); }); + it("forwards accountId for channelList", async () => { + await handleDiscordGuildAction( + "channelList", + { guildId: "G1", accountId: "ops" }, + channelInfoEnabled, + ); + expect(listGuildChannelsDiscord).toHaveBeenCalledWith("G1", { accountId: "ops" }); + }); + it("edits a channel", async () => { await handleDiscordGuildAction( "channelEdit", @@ -448,3 +480,26 @@ describe("handleDiscordGuildAction - channel management", () => { expect(removeChannelPermissionDiscord).toHaveBeenCalledWith("C1", "R1"); }); }); + +describe("handleDiscordModerationAction", () => { + it("forwards accountId for timeout", async () => { + await handleDiscordModerationAction( + "timeout", + { + guildId: "G1", + userId: "U1", + durationMinutes: 5, + accountId: "ops", + }, + moderationEnabled, + ); + expect(timeoutMemberDiscord).toHaveBeenCalledWith( + expect.objectContaining({ + guildId: "G1", + userId: "U1", + durationMinutes: 5, + }), + { accountId: "ops" }, + ); + }); +}); diff --git a/src/agents/tools/message-tool.ts b/src/agents/tools/message-tool.ts index 4ab3c7e18..21974f074 100644 --- a/src/agents/tools/message-tool.ts +++ b/src/agents/tools/message-tool.ts @@ -342,6 +342,9 @@ export function createMessageTool(options?: MessageToolOptions): AnyAgentTool { }) as ChannelMessageActionName; const accountId = readStringParam(params, "accountId") ?? agentAccountId; + if (accountId) { + params.accountId = accountId; + } const gateway = { url: readStringParam(params, "gatewayUrl", { trim: false }), diff --git a/src/channels/plugins/actions/discord.test.ts b/src/channels/plugins/actions/discord.test.ts index 46d8cd177..d69b6e74f 100644 --- a/src/channels/plugins/actions/discord.test.ts +++ b/src/channels/plugins/actions/discord.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import type { ClawdbotConfig } from "../../../config/config.js"; type SendMessageDiscord = typeof import("../../../discord/send.js").sendMessageDiscord; @@ -32,6 +32,32 @@ const loadDiscordMessageActions = async () => { return mod.discordMessageActions; }; +type SendMessageDiscord = typeof import("../../../discord/send.js").sendMessageDiscord; +type SendPollDiscord = typeof import("../../../discord/send.js").sendPollDiscord; + +const sendMessageDiscord = vi.fn, ReturnType>( + async () => ({ ok: true }) as Awaited>, +); +const sendPollDiscord = vi.fn, ReturnType>( + async () => ({ ok: true }) as Awaited>, +); + +vi.mock("../../../discord/send.js", async () => { + const actual = await vi.importActual( + "../../../discord/send.js", + ); + return { + ...actual, + sendMessageDiscord: (...args: Parameters) => sendMessageDiscord(...args), + sendPollDiscord: (...args: Parameters) => sendPollDiscord(...args), + }; +}); + +const loadHandleDiscordMessageAction = async () => { + const mod = await import("./discord/handle-action.js"); + return mod.handleDiscordMessageAction; +}; + describe("discord message actions", () => { it("lists channel and upload actions by default", async () => { const cfg = { channels: { discord: { token: "d0" } } } as ClawdbotConfig; @@ -53,3 +79,78 @@ describe("discord message actions", () => { expect(actions).not.toContain("channel-create"); }); }); + +describe("handleDiscordMessageAction", () => { + it("forwards context accountId for send", async () => { + sendMessageDiscord.mockClear(); + const handleDiscordMessageAction = await loadHandleDiscordMessageAction(); + + await handleDiscordMessageAction({ + action: "send", + params: { + to: "channel:123", + message: "hi", + }, + cfg: {} as ClawdbotConfig, + accountId: "ops", + }); + + expect(sendMessageDiscord).toHaveBeenCalledWith( + "channel:123", + "hi", + expect.objectContaining({ + accountId: "ops", + }), + ); + }); + + it("falls back to params accountId when context missing", async () => { + sendPollDiscord.mockClear(); + const handleDiscordMessageAction = await loadHandleDiscordMessageAction(); + + await handleDiscordMessageAction({ + action: "poll", + params: { + to: "channel:123", + pollQuestion: "Ready?", + pollOption: ["Yes", "No"], + accountId: "marve", + }, + cfg: {} as ClawdbotConfig, + }); + + expect(sendPollDiscord).toHaveBeenCalledWith( + "channel:123", + expect.objectContaining({ + question: "Ready?", + options: ["Yes", "No"], + }), + expect.objectContaining({ + accountId: "marve", + }), + ); + }); + + it("forwards accountId for thread replies", async () => { + sendMessageDiscord.mockClear(); + const handleDiscordMessageAction = await loadHandleDiscordMessageAction(); + + await handleDiscordMessageAction({ + action: "thread-reply", + params: { + channelId: "123", + message: "hi", + }, + cfg: {} as ClawdbotConfig, + accountId: "ops", + }); + + expect(sendMessageDiscord).toHaveBeenCalledWith( + "channel:123", + "hi", + expect.objectContaining({ + accountId: "ops", + }), + ); + }); +}); diff --git a/src/channels/plugins/actions/discord.ts b/src/channels/plugins/actions/discord.ts index fcae08633..ebed5eb0d 100644 --- a/src/channels/plugins/actions/discord.ts +++ b/src/channels/plugins/actions/discord.ts @@ -80,7 +80,7 @@ export const discordMessageActions: ChannelMessageActionAdapter = { } return null; }, - handleAction: async ({ action, params, cfg }) => { - return await handleDiscordMessageAction({ action, params, cfg }); + handleAction: async ({ action, params, cfg, accountId }) => { + return await handleDiscordMessageAction({ action, params, cfg, accountId }); }, }; diff --git a/src/channels/plugins/actions/discord/handle-action.guild-admin.ts b/src/channels/plugins/actions/discord/handle-action.guild-admin.ts index c2470e1dd..d65d044e2 100644 --- a/src/channels/plugins/actions/discord/handle-action.guild-admin.ts +++ b/src/channels/plugins/actions/discord/handle-action.guild-admin.ts @@ -7,7 +7,7 @@ import { import { handleDiscordAction } from "../../../../agents/tools/discord-actions.js"; import type { ChannelMessageActionContext } from "../../types.js"; -type Ctx = Pick; +type Ctx = Pick; export async function tryHandleDiscordMessageActionGuildAdmin(params: { ctx: Ctx; @@ -16,27 +16,37 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: { }): Promise | undefined> { const { ctx, resolveChannelId, readParentIdParam } = params; const { action, params: actionParams, cfg } = ctx; + const accountId = ctx.accountId ?? readStringParam(actionParams, "accountId"); if (action === "member-info") { const userId = readStringParam(actionParams, "userId", { required: true }); const guildId = readStringParam(actionParams, "guildId", { required: true, }); - return await handleDiscordAction({ action: "memberInfo", guildId, userId }, cfg); + return await handleDiscordAction( + { action: "memberInfo", accountId: accountId ?? undefined, guildId, userId }, + cfg, + ); } if (action === "role-info") { const guildId = readStringParam(actionParams, "guildId", { required: true, }); - return await handleDiscordAction({ action: "roleInfo", guildId }, cfg); + return await handleDiscordAction( + { action: "roleInfo", accountId: accountId ?? undefined, guildId }, + cfg, + ); } if (action === "emoji-list") { const guildId = readStringParam(actionParams, "guildId", { required: true, }); - return await handleDiscordAction({ action: "emojiList", guildId }, cfg); + return await handleDiscordAction( + { action: "emojiList", accountId: accountId ?? undefined, guildId }, + cfg, + ); } if (action === "emoji-upload") { @@ -50,7 +60,14 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: { }); const roleIds = readStringArrayParam(actionParams, "roleIds"); return await handleDiscordAction( - { action: "emojiUpload", guildId, name, mediaUrl, roleIds }, + { + action: "emojiUpload", + accountId: accountId ?? undefined, + guildId, + name, + mediaUrl, + roleIds, + }, cfg, ); } @@ -73,7 +90,15 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: { trim: false, }); return await handleDiscordAction( - { action: "stickerUpload", guildId, name, description, tags, mediaUrl }, + { + action: "stickerUpload", + accountId: accountId ?? undefined, + guildId, + name, + description, + tags, + mediaUrl, + }, cfg, ); } @@ -87,6 +112,7 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: { return await handleDiscordAction( { action: action === "role-add" ? "roleAdd" : "roleRemove", + accountId: accountId ?? undefined, guildId, userId, roleId, @@ -99,14 +125,20 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: { const channelId = readStringParam(actionParams, "channelId", { required: true, }); - return await handleDiscordAction({ action: "channelInfo", channelId }, cfg); + return await handleDiscordAction( + { action: "channelInfo", accountId: accountId ?? undefined, channelId }, + cfg, + ); } if (action === "channel-list") { const guildId = readStringParam(actionParams, "guildId", { required: true, }); - return await handleDiscordAction({ action: "channelList", guildId }, cfg); + return await handleDiscordAction( + { action: "channelList", accountId: accountId ?? undefined, guildId }, + cfg, + ); } if (action === "channel-create") { @@ -124,6 +156,7 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: { return await handleDiscordAction( { action: "channelCreate", + accountId: accountId ?? undefined, guildId, name, type: type ?? undefined, @@ -153,6 +186,7 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: { return await handleDiscordAction( { action: "channelEdit", + accountId: accountId ?? undefined, channelId, name: name ?? undefined, topic: topic ?? undefined, @@ -169,7 +203,10 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: { const channelId = readStringParam(actionParams, "channelId", { required: true, }); - return await handleDiscordAction({ action: "channelDelete", channelId }, cfg); + return await handleDiscordAction( + { action: "channelDelete", accountId: accountId ?? undefined, channelId }, + cfg, + ); } if (action === "channel-move") { @@ -186,6 +223,7 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: { return await handleDiscordAction( { action: "channelMove", + accountId: accountId ?? undefined, guildId, channelId, parentId: parentId === undefined ? undefined : parentId, @@ -206,6 +244,7 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: { return await handleDiscordAction( { action: "categoryCreate", + accountId: accountId ?? undefined, guildId, name, position: position ?? undefined, @@ -225,6 +264,7 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: { return await handleDiscordAction( { action: "categoryEdit", + accountId: accountId ?? undefined, categoryId, name: name ?? undefined, position: position ?? undefined, @@ -237,7 +277,10 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: { const categoryId = readStringParam(actionParams, "categoryId", { required: true, }); - return await handleDiscordAction({ action: "categoryDelete", categoryId }, cfg); + return await handleDiscordAction( + { action: "categoryDelete", accountId: accountId ?? undefined, categoryId }, + cfg, + ); } if (action === "voice-status") { @@ -245,14 +288,20 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: { required: true, }); const userId = readStringParam(actionParams, "userId", { required: true }); - return await handleDiscordAction({ action: "voiceStatus", guildId, userId }, cfg); + return await handleDiscordAction( + { action: "voiceStatus", accountId: accountId ?? undefined, guildId, userId }, + cfg, + ); } if (action === "event-list") { const guildId = readStringParam(actionParams, "guildId", { required: true, }); - return await handleDiscordAction({ action: "eventList", guildId }, cfg); + return await handleDiscordAction( + { action: "eventList", accountId: accountId ?? undefined, guildId }, + cfg, + ); } if (action === "event-create") { @@ -271,6 +320,7 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: { return await handleDiscordAction( { action: "eventCreate", + accountId: accountId ?? undefined, guildId, name, startTime, @@ -301,6 +351,7 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: { return await handleDiscordAction( { action: discordAction, + accountId: accountId ?? undefined, guildId, userId, durationMinutes, @@ -325,6 +376,7 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: { return await handleDiscordAction( { action: "threadList", + accountId: accountId ?? undefined, guildId, channelId, includeArchived, @@ -344,6 +396,7 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: { return await handleDiscordAction( { action: "threadReply", + accountId: accountId ?? undefined, channelId: resolveChannelId(), content, mediaUrl: mediaUrl ?? undefined, @@ -361,6 +414,7 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: { return await handleDiscordAction( { action: "searchMessages", + accountId: accountId ?? undefined, guildId, content: query, channelId: readStringParam(actionParams, "channelId"), diff --git a/src/channels/plugins/actions/discord/handle-action.ts b/src/channels/plugins/actions/discord/handle-action.ts index 82f08e686..90e95d14d 100644 --- a/src/channels/plugins/actions/discord/handle-action.ts +++ b/src/channels/plugins/actions/discord/handle-action.ts @@ -18,9 +18,10 @@ function readParentIdParam(params: Record): string | null | und } export async function handleDiscordMessageAction( - ctx: Pick, + ctx: Pick, ): Promise> { const { action, params, cfg } = ctx; + const accountId = ctx.accountId ?? readStringParam(params, "accountId"); const resolveChannelId = () => resolveDiscordChannelId( @@ -39,6 +40,7 @@ export async function handleDiscordMessageAction( return await handleDiscordAction( { action: "sendMessage", + accountId: accountId ?? undefined, to, content, mediaUrl: mediaUrl ?? undefined, @@ -62,6 +64,7 @@ export async function handleDiscordMessageAction( return await handleDiscordAction( { action: "poll", + accountId: accountId ?? undefined, to, question, answers, @@ -80,6 +83,7 @@ export async function handleDiscordMessageAction( return await handleDiscordAction( { action: "react", + accountId: accountId ?? undefined, channelId: resolveChannelId(), messageId, emoji, @@ -93,7 +97,13 @@ export async function handleDiscordMessageAction( const messageId = readStringParam(params, "messageId", { required: true }); const limit = readNumberParam(params, "limit", { integer: true }); return await handleDiscordAction( - { action: "reactions", channelId: resolveChannelId(), messageId, limit }, + { + action: "reactions", + accountId: accountId ?? undefined, + channelId: resolveChannelId(), + messageId, + limit, + }, cfg, ); } @@ -103,6 +113,7 @@ export async function handleDiscordMessageAction( return await handleDiscordAction( { action: "readMessages", + accountId: accountId ?? undefined, channelId: resolveChannelId(), limit, before: readStringParam(params, "before"), @@ -119,6 +130,7 @@ export async function handleDiscordMessageAction( return await handleDiscordAction( { action: "editMessage", + accountId: accountId ?? undefined, channelId: resolveChannelId(), messageId, content, @@ -130,7 +142,12 @@ export async function handleDiscordMessageAction( if (action === "delete") { const messageId = readStringParam(params, "messageId", { required: true }); return await handleDiscordAction( - { action: "deleteMessage", channelId: resolveChannelId(), messageId }, + { + action: "deleteMessage", + accountId: accountId ?? undefined, + channelId: resolveChannelId(), + messageId, + }, cfg, ); } @@ -141,6 +158,7 @@ export async function handleDiscordMessageAction( return await handleDiscordAction( { action: action === "pin" ? "pinMessage" : action === "unpin" ? "unpinMessage" : "listPins", + accountId: accountId ?? undefined, channelId: resolveChannelId(), messageId, }, @@ -149,7 +167,14 @@ export async function handleDiscordMessageAction( } if (action === "permissions") { - return await handleDiscordAction({ action: "permissions", channelId: resolveChannelId() }, cfg); + return await handleDiscordAction( + { + action: "permissions", + accountId: accountId ?? undefined, + channelId: resolveChannelId(), + }, + cfg, + ); } if (action === "thread-create") { @@ -161,6 +186,7 @@ export async function handleDiscordMessageAction( return await handleDiscordAction( { action: "threadCreate", + accountId: accountId ?? undefined, channelId: resolveChannelId(), name, messageId, @@ -179,6 +205,7 @@ export async function handleDiscordMessageAction( return await handleDiscordAction( { action: "sticker", + accountId: accountId ?? undefined, to: readStringParam(params, "to", { required: true }), stickerIds, content: readStringParam(params, "message"), diff --git a/src/cron/isolated-agent/run.ts b/src/cron/isolated-agent/run.ts index 296cf6aad..4f8f4deb3 100644 --- a/src/cron/isolated-agent/run.ts +++ b/src/cron/isolated-agent/run.ts @@ -299,6 +299,7 @@ export async function runCronIsolatedAgentTurn(params: { sessionId: cronSession.sessionEntry.sessionId, sessionKey: agentSessionKey, messageChannel, + agentAccountId: resolvedDelivery.accountId, sessionFile, workspaceDir, config: cfgWithAgentDefaults, diff --git a/src/infra/outbound/message-action-runner.test.ts b/src/infra/outbound/message-action-runner.test.ts index 0fd10eb3b..9b592d9d2 100644 --- a/src/infra/outbound/message-action-runner.test.ts +++ b/src/infra/outbound/message-action-runner.test.ts @@ -410,3 +410,65 @@ describe("runMessageAction sendAttachment hydration", () => { ); }); }); + +describe("runMessageAction accountId defaults", () => { + const handleAction = vi.fn(async () => jsonResult({ ok: true })); + const accountPlugin: ChannelPlugin = { + id: "discord", + meta: { + id: "discord", + label: "Discord", + selectionLabel: "Discord", + docsPath: "/channels/discord", + blurb: "Discord test plugin.", + }, + capabilities: { chatTypes: ["direct"] }, + config: { + listAccountIds: () => ["default"], + resolveAccount: () => ({}), + }, + actions: { + listActions: () => ["send"], + handleAction, + }, + }; + + beforeEach(() => { + setActivePluginRegistry( + createTestRegistry([ + { + pluginId: "discord", + source: "test", + plugin: accountPlugin, + }, + ]), + ); + handleAction.mockClear(); + }); + + afterEach(() => { + setActivePluginRegistry(createTestRegistry([])); + vi.clearAllMocks(); + }); + + it("propagates defaultAccountId into params", async () => { + await runMessageAction({ + cfg: {} as ClawdbotConfig, + action: "send", + params: { + channel: "discord", + target: "channel:123", + message: "hi", + }, + defaultAccountId: "ops", + }); + + expect(handleAction).toHaveBeenCalled(); + const ctx = handleAction.mock.calls[0]?.[0] as { + accountId?: string | null; + params: Record; + }; + expect(ctx.accountId).toBe("ops"); + expect(ctx.params.accountId).toBe("ops"); + }); +}); diff --git a/src/infra/outbound/message-action-runner.ts b/src/infra/outbound/message-action-runner.ts index dc8aeddf3..051098f34 100644 --- a/src/infra/outbound/message-action-runner.ts +++ b/src/infra/outbound/message-action-runner.ts @@ -803,6 +803,9 @@ export async function runMessageAction( const channel = await resolveChannel(cfg, params); const accountId = readStringParam(params, "accountId") ?? input.defaultAccountId; + if (accountId) { + params.accountId = accountId; + } const dryRun = Boolean(input.dryRun ?? readBooleanParam(params, "dryRun")); await hydrateSendAttachmentParams({