diff --git a/CHANGELOG.md b/CHANGELOG.md index 95ba337cc44..950befeaa73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Discord: send initial content when creating non-forum threads so `thread-create` content is delivered. (#18117) Thanks @zerone0x. - Security: replace deprecated SHA-1 sandbox configuration hashing with SHA-256 for deterministic sandbox cache identity and recreation checks. Thanks @kexinoh. - Security/Logging: redact Telegram bot tokens from error messages and uncaught stack traces to prevent accidental secret leakage into logs. Thanks @aether-ai-agent. - Sandbox/Security: block dangerous sandbox Docker config (bind mounts, host networking, unconfined seccomp/apparmor) to prevent container escape via config injection. Thanks @aether-ai-agent. diff --git a/src/discord/send.creates-thread.test.ts b/src/discord/send.creates-thread.test.ts index 5e2f5b2d731..957b709937b 100644 --- a/src/discord/send.creates-thread.test.ts +++ b/src/discord/send.creates-thread.test.ts @@ -107,6 +107,61 @@ describe("sendMessageDiscord", () => { ); }); + it("sends initial message for non-forum threads with content", async () => { + const { rest, getMock, postMock } = makeDiscordRest(); + getMock.mockResolvedValue({ type: ChannelType.GuildText }); + postMock.mockResolvedValue({ id: "t1" }); + await createThreadDiscord( + "chan1", + { name: "thread", content: "Hello thread!" }, + { rest, token: "t" }, + ); + expect(postMock).toHaveBeenCalledTimes(2); + // First call: create thread + expect(postMock).toHaveBeenNthCalledWith( + 1, + Routes.threads("chan1"), + expect.objectContaining({ + body: expect.objectContaining({ name: "thread", type: ChannelType.PublicThread }), + }), + ); + // Second call: send message to thread + expect(postMock).toHaveBeenNthCalledWith( + 2, + Routes.channelMessages("t1"), + expect.objectContaining({ + body: { content: "Hello thread!" }, + }), + ); + }); + + it("sends initial message for message-attached threads with content", async () => { + const { rest, getMock, postMock } = makeDiscordRest(); + postMock.mockResolvedValue({ id: "t1" }); + await createThreadDiscord( + "chan1", + { name: "thread", messageId: "m1", content: "Discussion here" }, + { rest, token: "t" }, + ); + // Should not detect channel type for message-attached threads + expect(getMock).not.toHaveBeenCalled(); + expect(postMock).toHaveBeenCalledTimes(2); + // First call: create thread from message + expect(postMock).toHaveBeenNthCalledWith( + 1, + Routes.threads("chan1", "m1"), + expect.objectContaining({ body: { name: "thread" } }), + ); + // Second call: send message to thread + expect(postMock).toHaveBeenNthCalledWith( + 2, + Routes.channelMessages("t1"), + expect.objectContaining({ + body: { content: "Discussion here" }, + }), + ); + }); + it("lists active threads by guild", async () => { const { rest, getMock } = makeDiscordRest(); getMock.mockResolvedValue({ threads: [] }); diff --git a/src/discord/send.messages.ts b/src/discord/send.messages.ts index 92ff6bb8ebb..1c8c67499a1 100644 --- a/src/discord/send.messages.ts +++ b/src/discord/send.messages.ts @@ -134,7 +134,17 @@ export async function createThreadDiscord( const route = payload.messageId ? Routes.threads(channelId, payload.messageId) : Routes.threads(channelId); - return await rest.post(route, { body }); + const thread = (await rest.post(route, { body })) as { id: string }; + + // For non-forum channels, send the initial message separately after thread creation. + // Forum channels handle this via the `message` field in the request body. + if (!isForumLike && payload.content?.trim()) { + await rest.post(Routes.channelMessages(thread.id), { + body: { content: payload.content }, + }); + } + + return thread; } export async function listThreadsDiscord(payload: DiscordThreadList, opts: DiscordReactOpts = {}) {