refactor(discord): centralize autoThread reply plan (#856)

This commit is contained in:
Peter Steinberger
2026-01-14 23:07:01 +00:00
parent 4ec2222fa9
commit a70937c926
3 changed files with 142 additions and 22 deletions

View File

@@ -25,9 +25,7 @@ import {
import { buildDirectLabel, buildGuildLabel, resolveReplyContext } from "./reply-context.js";
import { deliverDiscordReply } from "./reply-delivery.js";
import {
maybeCreateDiscordAutoThread,
resolveDiscordAutoThreadContext,
resolveDiscordReplyDeliveryPlan,
resolveDiscordAutoThreadReplyPlan,
resolveDiscordThreadStarter,
} from "./threading.js";
import { sendTyping } from "./typing.js";
@@ -201,35 +199,22 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
parentSessionKey,
useSuffix: false,
});
const inboundTarget = `channel:${message.channelId}`;
const createdThreadId = await maybeCreateDiscordAutoThread({
const replyPlan = await resolveDiscordAutoThreadReplyPlan({
client,
message,
isGuildMessage,
channelConfig,
threadChannel,
threadChannel,
baseText: baseText ?? "",
combinedBody,
});
const replyPlan = resolveDiscordReplyDeliveryPlan({
replyTarget: inboundTarget,
replyToMode,
messageId: message.id,
threadChannel,
createdThreadId,
agentId: route.agentId,
channel: route.channel,
});
const deliverTarget = replyPlan.deliverTarget;
const replyTarget = replyPlan.replyTarget;
const replyReference = replyPlan.replyReference;
const autoThreadContext = isGuildMessage
? resolveDiscordAutoThreadContext({
agentId: route.agentId,
channel: route.channel,
messageChannelId: message.channelId,
createdThreadId,
})
: null;
const autoThreadContext = replyPlan.autoThreadContext;
const effectiveFrom = isDirectMessage
? `discord:${author.id}`

View File

@@ -1,6 +1,11 @@
import { describe, expect, it } from "vitest";
import { buildAgentSessionKey } from "../../routing/resolve-route.js";
import { resolveDiscordAutoThreadContext } from "./threading.js";
import type { Client } from "@buape/carbon";
import {
resolveDiscordAutoThreadContext,
resolveDiscordAutoThreadReplyPlan,
resolveDiscordReplyDeliveryPlan,
} from "./threading.js";
describe("resolveDiscordAutoThreadContext", () => {
it("returns null when no createdThreadId", () => {
@@ -42,3 +47,88 @@ describe("resolveDiscordAutoThreadContext", () => {
});
});
describe("resolveDiscordReplyDeliveryPlan", () => {
it("uses reply references when posting to the original target", () => {
const plan = resolveDiscordReplyDeliveryPlan({
replyTarget: "channel:parent",
replyToMode: "all",
messageId: "m1",
threadChannel: null,
createdThreadId: null,
});
expect(plan.deliverTarget).toBe("channel:parent");
expect(plan.replyTarget).toBe("channel:parent");
expect(plan.replyReference.use()).toBe("m1");
});
it("disables reply references when autoThread creates a new thread", () => {
const plan = resolveDiscordReplyDeliveryPlan({
replyTarget: "channel:parent",
replyToMode: "all",
messageId: "m1",
threadChannel: null,
createdThreadId: "thread",
});
expect(plan.deliverTarget).toBe("channel:thread");
expect(plan.replyTarget).toBe("channel:thread");
expect(plan.replyReference.use()).toBeUndefined();
});
it("always uses existingId when inside a thread", () => {
const plan = resolveDiscordReplyDeliveryPlan({
replyTarget: "channel:thread",
replyToMode: "off",
messageId: "m1",
threadChannel: { id: "thread" },
createdThreadId: null,
});
expect(plan.replyReference.use()).toBe("m1");
});
});
describe("resolveDiscordAutoThreadReplyPlan", () => {
it("switches delivery + session context to the created thread", async () => {
const client = {
rest: { post: async () => ({ id: "thread" }) },
} as unknown as Client;
const plan = await resolveDiscordAutoThreadReplyPlan({
client,
message: { id: "m1", channelId: "parent" } as unknown as import("./listeners.js").DiscordMessageEvent["message"],
isGuildMessage: true,
channelConfig: { autoThread: true } as unknown as import("./allow-list.js").DiscordChannelConfigResolved,
threadChannel: null,
baseText: "hello",
combinedBody: "hello",
replyToMode: "all",
agentId: "agent",
channel: "discord",
});
expect(plan.deliverTarget).toBe("channel:thread");
expect(plan.replyReference.use()).toBeUndefined();
expect(plan.autoThreadContext?.SessionKey).toBe(
buildAgentSessionKey({
agentId: "agent",
channel: "discord",
peer: { kind: "channel", id: "thread" },
}),
);
});
it("does nothing when autoThread is disabled", async () => {
const client = { rest: { post: async () => ({ id: "thread" }) } } as unknown as Client;
const plan = await resolveDiscordAutoThreadReplyPlan({
client,
message: { id: "m1", channelId: "parent" } as unknown as import("./listeners.js").DiscordMessageEvent["message"],
isGuildMessage: true,
channelConfig: { autoThread: false } as unknown as import("./allow-list.js").DiscordChannelConfigResolved,
threadChannel: null,
baseText: "hello",
combinedBody: "hello",
replyToMode: "all",
agentId: "agent",
channel: "discord",
});
expect(plan.deliverTarget).toBe("channel:parent");
expect(plan.autoThreadContext).toBeNull();
});
});

View File

@@ -202,6 +202,51 @@ export function resolveDiscordAutoThreadContext(params: {
};
}
export type DiscordAutoThreadReplyPlan = DiscordReplyDeliveryPlan & {
createdThreadId?: string;
autoThreadContext: DiscordAutoThreadContext | null;
};
export async function resolveDiscordAutoThreadReplyPlan(params: {
client: Client;
message: DiscordMessageEvent["message"];
isGuildMessage: boolean;
channelConfig?: DiscordChannelConfigResolved | null;
threadChannel?: DiscordThreadChannel | null;
baseText: string;
combinedBody: string;
replyToMode: ReplyToMode;
agentId: string;
channel: string;
}): Promise<DiscordAutoThreadReplyPlan> {
const originalReplyTarget = `channel:${params.message.channelId}`;
const createdThreadId = await maybeCreateDiscordAutoThread({
client: params.client,
message: params.message,
isGuildMessage: params.isGuildMessage,
channelConfig: params.channelConfig,
threadChannel: params.threadChannel,
baseText: params.baseText,
combinedBody: params.combinedBody,
});
const deliveryPlan = resolveDiscordReplyDeliveryPlan({
replyTarget: originalReplyTarget,
replyToMode: params.replyToMode,
messageId: params.message.id,
threadChannel: params.threadChannel,
createdThreadId,
});
const autoThreadContext = params.isGuildMessage
? resolveDiscordAutoThreadContext({
agentId: params.agentId,
channel: params.channel,
messageChannelId: params.message.channelId,
createdThreadId,
})
: null;
return { ...deliveryPlan, createdThreadId, autoThreadContext };
}
export async function maybeCreateDiscordAutoThread(params: {
client: Client;
message: DiscordMessageEvent["message"];