diff --git a/CHANGELOG.md b/CHANGELOG.md index 855dfcf4aaa..e285807e999 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai - Exec/child commands: mark child command environments with `OPENCLAW_CLI` so subprocesses can detect when they were launched from the OpenClaw CLI. (#41411) Thanks @vincentkoc. - iOS/Home canvas: add a bundled welcome screen with a live agent overview that refreshes on connect, reconnect, and foreground return, and move the compact connection pill off the top-left canvas overlay. (#42456) Thanks @ngutman. - iOS/Home canvas: replace floating controls with a docked toolbar, make the bundled home scaffold adapt to smaller phones, and open chat in the resolved main session instead of a synthetic `ios` session. (#42456) Thanks @ngutman. +- Discord/auto threads: add `autoArchiveDuration` channel config for auto-created threads so Discord thread archiving can stay at 1 hour, 1 day, 3 days, or 1 week instead of always using the 1-hour default. (#35065) Thanks @davidguttman. ### Breaking diff --git a/src/config/zod-schema.providers-core.ts b/src/config/zod-schema.providers-core.ts index 3ceefb480ff..0bb676fa5ad 100644 --- a/src/config/zod-schema.providers-core.ts +++ b/src/config/zod-schema.providers-core.ts @@ -384,6 +384,16 @@ export const DiscordGuildChannelSchema = z systemPrompt: z.string().optional(), includeThreadStarter: z.boolean().optional(), autoThread: z.boolean().optional(), + /** Archive duration for auto-created threads in minutes. Discord supports 60, 1440 (1 day), 4320 (3 days), 10080 (1 week). Default: 60. */ + autoArchiveDuration: z + .union([ + z.enum(["60", "1440", "4320", "10080"]), + z.literal(60), + z.literal(1440), + z.literal(4320), + z.literal(10080), + ]) + .optional(), }) .strict(); diff --git a/src/discord/monitor/allow-list.ts b/src/discord/monitor/allow-list.ts index 5432cb5d128..b736928e276 100644 --- a/src/discord/monitor/allow-list.ts +++ b/src/discord/monitor/allow-list.ts @@ -40,6 +40,7 @@ export type DiscordGuildEntryResolved = { systemPrompt?: string; includeThreadStarter?: boolean; autoThread?: boolean; + autoArchiveDuration?: "60" | "1440" | "4320" | "10080" | 60 | 1440 | 4320 | 10080; } >; }; @@ -55,6 +56,7 @@ export type DiscordChannelConfigResolved = { systemPrompt?: string; includeThreadStarter?: boolean; autoThread?: boolean; + autoArchiveDuration?: "60" | "1440" | "4320" | "10080" | 60 | 1440 | 4320 | 10080; matchKey?: string; matchSource?: ChannelMatchSource; }; @@ -401,6 +403,7 @@ function resolveDiscordChannelConfigEntry( systemPrompt: entry.systemPrompt, includeThreadStarter: entry.includeThreadStarter, autoThread: entry.autoThread, + autoArchiveDuration: entry.autoArchiveDuration, }; return resolved; } diff --git a/src/discord/monitor/threading.auto-thread.test.ts b/src/discord/monitor/threading.auto-thread.test.ts index 6228914bb39..2affabcae44 100644 --- a/src/discord/monitor/threading.auto-thread.test.ts +++ b/src/discord/monitor/threading.auto-thread.test.ts @@ -1,5 +1,5 @@ import { ChannelType } from "@buape/carbon"; -import { describe, it, expect, vi } from "vitest"; +import { describe, it, expect, vi, beforeEach } from "vitest"; import { maybeCreateDiscordAutoThread } from "./threading.js"; describe("maybeCreateDiscordAutoThread", () => { @@ -89,3 +89,74 @@ describe("maybeCreateDiscordAutoThread", () => { expect(postMock).toHaveBeenCalled(); }); }); + +describe("maybeCreateDiscordAutoThread autoArchiveDuration", () => { + const postMock = vi.fn(); + const getMock = vi.fn(); + const mockClient = { + rest: { post: postMock, get: getMock }, + } as unknown as Parameters[0]["client"]; + const mockMessage = { + id: "msg1", + timestamp: "123", + } as unknown as Parameters[0]["message"]; + + beforeEach(() => { + postMock.mockReset(); + getMock.mockReset(); + }); + + it("uses configured autoArchiveDuration", async () => { + postMock.mockResolvedValueOnce({ id: "thread1" }); + await maybeCreateDiscordAutoThread({ + client: mockClient, + message: mockMessage, + messageChannelId: "text1", + isGuildMessage: true, + channelConfig: { allowed: true, autoThread: true, autoArchiveDuration: "10080" }, + channelType: ChannelType.GuildText, + baseText: "test", + combinedBody: "test", + }); + expect(postMock).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ body: expect.objectContaining({ auto_archive_duration: 10080 }) }), + ); + }); + + it("accepts numeric autoArchiveDuration", async () => { + postMock.mockResolvedValueOnce({ id: "thread1" }); + await maybeCreateDiscordAutoThread({ + client: mockClient, + message: mockMessage, + messageChannelId: "text1", + isGuildMessage: true, + channelConfig: { allowed: true, autoThread: true, autoArchiveDuration: 4320 }, + channelType: ChannelType.GuildText, + baseText: "test", + combinedBody: "test", + }); + expect(postMock).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ body: expect.objectContaining({ auto_archive_duration: 4320 }) }), + ); + }); + + it("defaults to 60 when autoArchiveDuration not set", async () => { + postMock.mockResolvedValueOnce({ id: "thread1" }); + await maybeCreateDiscordAutoThread({ + client: mockClient, + message: mockMessage, + messageChannelId: "text1", + isGuildMessage: true, + channelConfig: { allowed: true, autoThread: true }, + channelType: ChannelType.GuildText, + baseText: "test", + combinedBody: "test", + }); + expect(postMock).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ body: expect.objectContaining({ auto_archive_duration: 60 }) }), + ); + }); +}); diff --git a/src/discord/monitor/threading.ts b/src/discord/monitor/threading.ts index 14377d8e644..28897e9b7aa 100644 --- a/src/discord/monitor/threading.ts +++ b/src/discord/monitor/threading.ts @@ -397,12 +397,18 @@ export async function maybeCreateDiscordAutoThread(params: { params.baseText || params.combinedBody || "Thread", params.message.id, ); + + // Parse archive duration from config, default to 60 minutes + const archiveDuration = params.channelConfig?.autoArchiveDuration + ? Number(params.channelConfig.autoArchiveDuration) + : 60; + const created = (await params.client.rest.post( `${Routes.channelMessage(messageChannelId, params.message.id)}/threads`, { body: { name: threadName, - auto_archive_duration: 60, + auto_archive_duration: archiveDuration, }, }, )) as { id?: string };