From 7f4d1b7531aca935f1af828842bc82ad124b3b65 Mon Sep 17 00:00:00 2001 From: Pushkar Kathayat Date: Sun, 1 Mar 2026 11:17:28 +0530 Subject: [PATCH] fix(discord): support applied_tags parameter for forum thread creation Forum channels that require tags fail with "A tag is required" when creating threads because there was no way to pass tag IDs. Add appliedTags parameter to the thread-create action so forum posts can include required tags from the channel's available_tags list. --- src/agents/tools/discord-actions-messaging.ts | 16 +++++--- src/agents/tools/message-tool.ts | 1 + .../plugins/actions/discord/handle-action.ts | 2 + src/discord/send.creates-thread.test.ts | 38 +++++++++++++++++++ src/discord/send.messages.ts | 3 ++ src/discord/send.types.ts | 2 + 6 files changed, 56 insertions(+), 6 deletions(-) diff --git a/src/agents/tools/discord-actions-messaging.ts b/src/agents/tools/discord-actions-messaging.ts index 3235ed2fba2..9d0b3818334 100644 --- a/src/agents/tools/discord-actions-messaging.ts +++ b/src/agents/tools/discord-actions-messaging.ts @@ -363,13 +363,17 @@ export async function handleDiscordMessagingAction( typeof autoArchiveMinutesRaw === "number" && Number.isFinite(autoArchiveMinutesRaw) ? autoArchiveMinutesRaw : undefined; + const appliedTags = readStringArrayParam(params, "appliedTags"); + const payload = { + name, + messageId, + autoArchiveMinutes, + content, + appliedTags: appliedTags ?? undefined, + }; const thread = accountId - ? await createThreadDiscord( - channelId, - { name, messageId, autoArchiveMinutes, content }, - { accountId }, - ) - : await createThreadDiscord(channelId, { name, messageId, autoArchiveMinutes, content }); + ? await createThreadDiscord(channelId, payload, { accountId }) + : await createThreadDiscord(channelId, payload); return jsonResult({ ok: true, thread }); } case "threadList": { diff --git a/src/agents/tools/message-tool.ts b/src/agents/tools/message-tool.ts index 6b7ddf2b77f..4e8d4a2efe3 100644 --- a/src/agents/tools/message-tool.ts +++ b/src/agents/tools/message-tool.ts @@ -312,6 +312,7 @@ function buildThreadSchema() { return { threadName: Type.Optional(Type.String()), autoArchiveMin: Type.Optional(Type.Number()), + appliedTags: Type.Optional(Type.Array(Type.String())), }; } diff --git a/src/channels/plugins/actions/discord/handle-action.ts b/src/channels/plugins/actions/discord/handle-action.ts index 97fd23a0de8..4c868c71efb 100644 --- a/src/channels/plugins/actions/discord/handle-action.ts +++ b/src/channels/plugins/actions/discord/handle-action.ts @@ -230,6 +230,7 @@ export async function handleDiscordMessageAction( const autoArchiveMinutes = readNumberParam(params, "autoArchiveMin", { integer: true, }); + const appliedTags = readStringArrayParam(params, "appliedTags"); return await handleDiscordAction( { action: "threadCreate", @@ -239,6 +240,7 @@ export async function handleDiscordMessageAction( messageId, content, autoArchiveMinutes, + appliedTags: appliedTags ?? undefined, }, cfg, actionOptions, diff --git a/src/discord/send.creates-thread.test.ts b/src/discord/send.creates-thread.test.ts index 957b709937b..3fd70b99882 100644 --- a/src/discord/send.creates-thread.test.ts +++ b/src/discord/send.creates-thread.test.ts @@ -76,6 +76,44 @@ describe("sendMessageDiscord", () => { ); }); + it("passes applied_tags for forum threads", async () => { + const { rest, getMock, postMock } = makeDiscordRest(); + getMock.mockResolvedValue({ type: ChannelType.GuildForum }); + postMock.mockResolvedValue({ id: "t1" }); + await createThreadDiscord( + "chan1", + { name: "tagged post", appliedTags: ["tag1", "tag2"] }, + { rest, token: "t" }, + ); + expect(postMock).toHaveBeenCalledWith( + Routes.threads("chan1"), + expect.objectContaining({ + body: { + name: "tagged post", + message: { content: "tagged post" }, + applied_tags: ["tag1", "tag2"], + }, + }), + ); + }); + + it("omits applied_tags for non-forum threads", async () => { + const { rest, getMock, postMock } = makeDiscordRest(); + getMock.mockResolvedValue({ type: ChannelType.GuildText }); + postMock.mockResolvedValue({ id: "t1" }); + await createThreadDiscord( + "chan1", + { name: "thread", appliedTags: ["tag1"] }, + { rest, token: "t" }, + ); + expect(postMock).toHaveBeenCalledWith( + Routes.threads("chan1"), + expect.objectContaining({ + body: expect.not.objectContaining({ applied_tags: expect.anything() }), + }), + ); + }); + it("falls back when channel lookup is unavailable", async () => { const { rest, getMock, postMock } = makeDiscordRest(); getMock.mockRejectedValue(new Error("lookup failed")); diff --git a/src/discord/send.messages.ts b/src/discord/send.messages.ts index ae661c027a7..54484def68f 100644 --- a/src/discord/send.messages.ts +++ b/src/discord/send.messages.ts @@ -124,6 +124,9 @@ export async function createThreadDiscord( if (isForumLike) { const starterContent = payload.content?.trim() ? payload.content : payload.name; body.message = { content: starterContent }; + if (payload.appliedTags?.length) { + body.applied_tags = payload.appliedTags; + } } // When creating a standalone thread (no messageId) in a non-forum channel, // default to public thread (type 11). Discord defaults to private (type 12) diff --git a/src/discord/send.types.ts b/src/discord/send.types.ts index a13f90b1e17..c69058f8687 100644 --- a/src/discord/send.types.ts +++ b/src/discord/send.types.ts @@ -74,6 +74,8 @@ export type DiscordThreadCreate = { content?: string; /** Discord thread type (default: PublicThread for standalone threads). */ type?: number; + /** Tag IDs to apply when creating a forum/media thread (Discord `applied_tags`). */ + appliedTags?: string[]; }; export type DiscordThreadList = {