Discord: fix forum thread starters
This commit is contained in:
committed by
Peter Steinberger
parent
ee70a1d1fb
commit
6ef9fc64d7
@@ -1,6 +1,7 @@
|
||||
import type { Client } from "@buape/carbon";
|
||||
import { ChannelType, MessageType } from "@buape/carbon";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { Routes } from "discord-api-types/v10";
|
||||
|
||||
const sendMock = vi.fn();
|
||||
const reactMock = vi.fn();
|
||||
@@ -381,6 +382,112 @@ describe("discord tool result dispatch", () => {
|
||||
expect(capturedCtx?.ThreadLabel).toContain("Discord thread #general");
|
||||
});
|
||||
|
||||
it("treats forum threads as distinct sessions without channel payloads", async () => {
|
||||
const { createDiscordMessageHandler } = await import("./monitor.js");
|
||||
let capturedCtx:
|
||||
| {
|
||||
SessionKey?: string;
|
||||
ParentSessionKey?: string;
|
||||
ThreadStarterBody?: string;
|
||||
ThreadLabel?: string;
|
||||
}
|
||||
| undefined;
|
||||
dispatchMock.mockImplementationOnce(async ({ ctx, dispatcher }) => {
|
||||
capturedCtx = ctx;
|
||||
dispatcher.sendFinalReply({ text: "hi" });
|
||||
return { queuedFinal: true, counts: { final: 1 } };
|
||||
});
|
||||
|
||||
const cfg = {
|
||||
agent: { model: "anthropic/claude-opus-4-5", workspace: "/tmp/clawd" },
|
||||
session: { store: "/tmp/clawdbot-sessions.json" },
|
||||
discord: {
|
||||
dm: { enabled: true, policy: "open" },
|
||||
guilds: { "*": { requireMention: false } },
|
||||
},
|
||||
routing: { allowFrom: [] },
|
||||
} as ReturnType<typeof import("../config/config.js").loadConfig>;
|
||||
|
||||
const handler = createDiscordMessageHandler({
|
||||
cfg,
|
||||
discordConfig: cfg.discord,
|
||||
accountId: "default",
|
||||
token: "token",
|
||||
runtime: {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: (code: number): never => {
|
||||
throw new Error(`exit ${code}`);
|
||||
},
|
||||
},
|
||||
botUserId: "bot-id",
|
||||
guildHistories: new Map(),
|
||||
historyLimit: 0,
|
||||
mediaMaxBytes: 10_000,
|
||||
textLimit: 2000,
|
||||
replyToMode: "off",
|
||||
dmEnabled: true,
|
||||
groupDmEnabled: false,
|
||||
guildEntries: { "*": { requireMention: false } },
|
||||
});
|
||||
|
||||
const fetchChannel = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
type: ChannelType.PublicThread,
|
||||
name: "topic-1",
|
||||
parentId: "forum-1",
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
type: ChannelType.GuildForum,
|
||||
name: "support",
|
||||
});
|
||||
const restGet = vi.fn().mockResolvedValue({
|
||||
content: "starter message",
|
||||
author: { id: "u1", username: "Alice", discriminator: "0001" },
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
const client = {
|
||||
fetchChannel,
|
||||
rest: {
|
||||
get: restGet,
|
||||
},
|
||||
} as unknown as Client;
|
||||
|
||||
await handler(
|
||||
{
|
||||
message: {
|
||||
id: "m6",
|
||||
content: "thread reply",
|
||||
channelId: "t1",
|
||||
timestamp: new Date().toISOString(),
|
||||
type: MessageType.Default,
|
||||
attachments: [],
|
||||
embeds: [],
|
||||
mentionedEveryone: false,
|
||||
mentionedUsers: [],
|
||||
mentionedRoles: [],
|
||||
author: { id: "u2", bot: false, username: "Bob", tag: "Bob#2" },
|
||||
},
|
||||
author: { id: "u2", bot: false, username: "Bob", tag: "Bob#2" },
|
||||
member: { displayName: "Bob" },
|
||||
guild: { id: "g1", name: "Guild" },
|
||||
guild_id: "g1",
|
||||
},
|
||||
client,
|
||||
);
|
||||
|
||||
expect(capturedCtx?.SessionKey).toBe("agent:main:discord:channel:t1");
|
||||
expect(capturedCtx?.ParentSessionKey).toBe(
|
||||
"agent:main:discord:channel:forum-1",
|
||||
);
|
||||
expect(capturedCtx?.ThreadStarterBody).toContain("starter message");
|
||||
expect(capturedCtx?.ThreadLabel).toContain("Discord thread #support");
|
||||
expect(restGet).toHaveBeenCalledWith(
|
||||
Routes.channelMessage("t1", "t1"),
|
||||
);
|
||||
});
|
||||
|
||||
it("scopes thread sessions to the routed agent", async () => {
|
||||
const { createDiscordMessageHandler } = await import("./monitor.js");
|
||||
|
||||
|
||||
@@ -139,14 +139,22 @@ async function resolveDiscordThreadStarter(params: {
|
||||
channel: DiscordThreadChannel;
|
||||
client: Client;
|
||||
parentId?: string;
|
||||
parentType?: ChannelType;
|
||||
}): Promise<DiscordThreadStarter | null> {
|
||||
const cacheKey = params.channel.id;
|
||||
const cached = DISCORD_THREAD_STARTER_CACHE.get(cacheKey);
|
||||
if (cached) return cached;
|
||||
try {
|
||||
if (!params.parentId) return null;
|
||||
const parentType = params.parentType;
|
||||
const isForumParent =
|
||||
parentType === ChannelType.GuildForum ||
|
||||
parentType === ChannelType.GuildMedia;
|
||||
const messageChannelId = isForumParent
|
||||
? params.channel.id
|
||||
: params.parentId;
|
||||
if (!messageChannelId) return null;
|
||||
const starter = (await params.client.rest.get(
|
||||
Routes.channelMessage(params.parentId, params.channel.id),
|
||||
Routes.channelMessage(messageChannelId, params.channel.id),
|
||||
)) as {
|
||||
content?: string | null;
|
||||
embeds?: Array<{ description?: string | null }>;
|
||||
@@ -226,6 +234,14 @@ export type DiscordMessageHandler = (
|
||||
client: Client,
|
||||
) => Promise<void>;
|
||||
|
||||
function isDiscordThreadType(type: ChannelType | undefined): boolean {
|
||||
return (
|
||||
type === ChannelType.PublicThread ||
|
||||
type === ChannelType.PrivateThread ||
|
||||
type === ChannelType.AnnouncementThread
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveDiscordReplyTarget(opts: {
|
||||
replyToMode: ReplyToMode;
|
||||
replyToId?: string;
|
||||
@@ -666,12 +682,32 @@ export function createDiscordMessageHandler(params: {
|
||||
message.channel &&
|
||||
"isThread" in message.channel &&
|
||||
message.channel.isThread();
|
||||
const isThreadByType =
|
||||
isGuildMessage && isDiscordThreadType(channelInfo?.type);
|
||||
const threadChannel = isThreadChannel
|
||||
? (message.channel as DiscordThreadChannel)
|
||||
: null;
|
||||
: isThreadByType
|
||||
? {
|
||||
id: message.channelId,
|
||||
name: channelInfo?.name ?? undefined,
|
||||
parentId: channelInfo?.parentId ?? undefined,
|
||||
}
|
||||
: null;
|
||||
const threadParentId =
|
||||
threadChannel?.parentId ?? threadChannel?.parent?.id ?? undefined;
|
||||
const threadParentName = threadChannel?.parent?.name;
|
||||
threadChannel?.parentId ??
|
||||
threadChannel?.parent?.id ??
|
||||
channelInfo?.parentId ??
|
||||
undefined;
|
||||
let threadParentName = threadChannel?.parent?.name;
|
||||
let threadParentType: ChannelType | undefined;
|
||||
if (threadChannel && threadParentId) {
|
||||
const parentInfo = await resolveDiscordChannelInfo(
|
||||
client,
|
||||
threadParentId,
|
||||
);
|
||||
threadParentName = threadParentName ?? parentInfo?.name;
|
||||
threadParentType = parentInfo?.type;
|
||||
}
|
||||
const threadName = threadChannel?.name;
|
||||
const configChannelName = threadParentName ?? channelName;
|
||||
const configChannelSlug = configChannelName
|
||||
@@ -935,6 +971,7 @@ export function createDiscordMessageHandler(params: {
|
||||
channel: threadChannel,
|
||||
client,
|
||||
parentId: threadParentId,
|
||||
parentType: threadParentType,
|
||||
});
|
||||
if (starter?.text) {
|
||||
const starterEnvelope = formatThreadStarterEnvelope({
|
||||
@@ -1684,13 +1721,17 @@ async function deliverDiscordReply(params: {
|
||||
async function resolveDiscordChannelInfo(
|
||||
client: Client,
|
||||
channelId: string,
|
||||
): Promise<{ type: ChannelType; name?: string; topic?: string } | null> {
|
||||
): Promise<
|
||||
{ type: ChannelType; name?: string; topic?: string; parentId?: string } | null
|
||||
> {
|
||||
try {
|
||||
const channel = await client.fetchChannel(channelId);
|
||||
if (!channel) return null;
|
||||
const name = "name" in channel ? (channel.name ?? undefined) : undefined;
|
||||
const topic = "topic" in channel ? (channel.topic ?? undefined) : undefined;
|
||||
return { type: channel.type, name, topic };
|
||||
const parentId =
|
||||
"parentId" in channel ? (channel.parentId ?? undefined) : undefined;
|
||||
return { type: channel.type, name, topic, parentId };
|
||||
} catch (err) {
|
||||
logVerbose(`discord: failed to fetch channel ${channelId}: ${String(err)}`);
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user