refactor: streamline thread reply planning
This commit is contained in:
@@ -366,6 +366,72 @@ export function sanitizeDiscordThreadName(
|
|||||||
return truncateUtf16Safe(base, 100) || `Thread ${fallbackId}`;
|
return truncateUtf16Safe(base, 100) || `Thread ${fallbackId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DiscordReplyDeliveryPlan = {
|
||||||
|
deliverTarget: string;
|
||||||
|
replyTarget: string;
|
||||||
|
replyReference: ReturnType<typeof createReplyReferencePlanner>;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function maybeCreateDiscordAutoThread(params: {
|
||||||
|
client: Client;
|
||||||
|
message: DiscordMessageEvent["message"];
|
||||||
|
isGuildMessage: boolean;
|
||||||
|
channelConfig?: DiscordChannelConfigResolved;
|
||||||
|
threadChannel?: DiscordThreadChannel | null;
|
||||||
|
baseText: string;
|
||||||
|
combinedBody: string;
|
||||||
|
}): Promise<string | undefined> {
|
||||||
|
if (!params.isGuildMessage) return undefined;
|
||||||
|
if (!params.channelConfig?.autoThread) return undefined;
|
||||||
|
if (params.threadChannel) return undefined;
|
||||||
|
try {
|
||||||
|
const threadName = sanitizeDiscordThreadName(
|
||||||
|
params.baseText || params.combinedBody || "Thread",
|
||||||
|
params.message.id,
|
||||||
|
);
|
||||||
|
const created = (await params.client.rest.post(
|
||||||
|
`${Routes.channelMessage(params.message.channelId, params.message.id)}/threads`,
|
||||||
|
{
|
||||||
|
body: {
|
||||||
|
name: threadName,
|
||||||
|
auto_archive_duration: 60,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)) as { id?: string };
|
||||||
|
const createdId = created?.id ? String(created.id) : "";
|
||||||
|
return createdId || undefined;
|
||||||
|
} catch (err) {
|
||||||
|
logVerbose(
|
||||||
|
`discord: autoThread failed for ${params.message.channelId}/${params.message.id}: ${String(err)}`,
|
||||||
|
);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveDiscordReplyDeliveryPlan(params: {
|
||||||
|
replyTarget: string;
|
||||||
|
replyToMode: ReplyToMode;
|
||||||
|
messageId: string;
|
||||||
|
threadChannel?: DiscordThreadChannel | null;
|
||||||
|
createdThreadId?: string | null;
|
||||||
|
}): DiscordReplyDeliveryPlan {
|
||||||
|
const originalReplyTarget = params.replyTarget;
|
||||||
|
let deliverTarget = originalReplyTarget;
|
||||||
|
let replyTarget = originalReplyTarget;
|
||||||
|
if (params.createdThreadId) {
|
||||||
|
deliverTarget = `channel:${params.createdThreadId}`;
|
||||||
|
replyTarget = deliverTarget;
|
||||||
|
}
|
||||||
|
const allowReference = deliverTarget === originalReplyTarget;
|
||||||
|
const replyReference = createReplyReferencePlanner({
|
||||||
|
replyToMode: allowReference ? params.replyToMode : "off",
|
||||||
|
existingId: params.threadChannel ? params.messageId : undefined,
|
||||||
|
startId: params.messageId,
|
||||||
|
allowReference,
|
||||||
|
});
|
||||||
|
return { deliverTarget, replyTarget, replyReference };
|
||||||
|
}
|
||||||
|
|
||||||
function summarizeAllowList(list?: Array<string | number>) {
|
function summarizeAllowList(list?: Array<string | number>) {
|
||||||
if (!list || list.length === 0) return "any";
|
if (!list || list.length === 0) return "any";
|
||||||
const sample = list.slice(0, 4).map((entry) => String(entry));
|
const sample = list.slice(0, 4).map((entry) => String(entry));
|
||||||
@@ -1205,45 +1271,25 @@ export function createDiscordMessageHandler(params: {
|
|||||||
runtime.error?.(danger("discord: missing reply target"));
|
runtime.error?.(danger("discord: missing reply target"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const originalReplyTarget = replyTarget;
|
const createdThreadId = await maybeCreateDiscordAutoThread({
|
||||||
|
client,
|
||||||
let deliverTarget = replyTarget;
|
message,
|
||||||
if (isGuildMessage && channelConfig?.autoThread && !threadChannel) {
|
isGuildMessage,
|
||||||
try {
|
channelConfig,
|
||||||
const threadName = sanitizeDiscordThreadName(
|
threadChannel,
|
||||||
baseText || combinedBody || "Thread",
|
baseText: baseText ?? "",
|
||||||
message.id,
|
combinedBody,
|
||||||
);
|
|
||||||
|
|
||||||
const created = (await client.rest.post(
|
|
||||||
`${Routes.channelMessage(message.channelId, message.id)}/threads`,
|
|
||||||
{
|
|
||||||
body: {
|
|
||||||
name: threadName,
|
|
||||||
auto_archive_duration: 60,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)) as { id?: string };
|
|
||||||
|
|
||||||
const createdId = created?.id ? String(created.id) : "";
|
|
||||||
if (createdId) {
|
|
||||||
deliverTarget = `channel:${createdId}`;
|
|
||||||
replyTarget = deliverTarget;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
logVerbose(
|
|
||||||
`discord: autoThread failed for ${message.channelId}/${message.id}: ${String(err)}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const replyReference = createReplyReferencePlanner({
|
|
||||||
replyToMode:
|
|
||||||
deliverTarget !== originalReplyTarget ? "off" : replyToMode,
|
|
||||||
existingId: threadChannel ? message.id : undefined,
|
|
||||||
startId: message.id,
|
|
||||||
allowReference: deliverTarget === originalReplyTarget,
|
|
||||||
});
|
});
|
||||||
|
const replyPlan = resolveDiscordReplyDeliveryPlan({
|
||||||
|
replyTarget,
|
||||||
|
replyToMode,
|
||||||
|
messageId: message.id,
|
||||||
|
threadChannel,
|
||||||
|
createdThreadId,
|
||||||
|
});
|
||||||
|
const deliverTarget = replyPlan.deliverTarget;
|
||||||
|
replyTarget = replyPlan.replyTarget;
|
||||||
|
const replyReference = replyPlan.replyReference;
|
||||||
|
|
||||||
if (isDirectMessage) {
|
if (isDirectMessage) {
|
||||||
const sessionCfg = cfg.session;
|
const sessionCfg = cfg.session;
|
||||||
|
|||||||
@@ -1136,11 +1136,11 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
|||||||
// Shared mutable ref for tracking if a reply was sent (used by both
|
// Shared mutable ref for tracking if a reply was sent (used by both
|
||||||
// auto-reply path and tool path for "first" threading mode).
|
// auto-reply path and tool path for "first" threading mode).
|
||||||
const hasRepliedRef = { value: false };
|
const hasRepliedRef = { value: false };
|
||||||
const replyReference = createReplyReferencePlanner({
|
const replyPlan = createSlackReplyDeliveryPlan({
|
||||||
replyToMode,
|
replyToMode,
|
||||||
existingId: incomingThreadTs,
|
incomingThreadTs,
|
||||||
startId: messageTs,
|
messageTs,
|
||||||
hasReplied: hasRepliedRef.value,
|
hasRepliedRef,
|
||||||
});
|
});
|
||||||
const onReplyStart = async () => {
|
const onReplyStart = async () => {
|
||||||
didSetStatus = true;
|
didSetStatus = true;
|
||||||
@@ -1153,11 +1153,11 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
|||||||
let didSendReply = false;
|
let didSendReply = false;
|
||||||
const { dispatcher, replyOptions, markDispatchIdle } =
|
const { dispatcher, replyOptions, markDispatchIdle } =
|
||||||
createReplyDispatcherWithTyping({
|
createReplyDispatcherWithTyping({
|
||||||
responsePrefix: resolveEffectiveMessagesConfig(cfg, route.agentId)
|
responsePrefix: resolveEffectiveMessagesConfig(cfg, route.agentId)
|
||||||
.responsePrefix,
|
.responsePrefix,
|
||||||
humanDelay: resolveHumanDelayConfig(cfg, route.agentId),
|
humanDelay: resolveHumanDelayConfig(cfg, route.agentId),
|
||||||
deliver: async (payload) => {
|
deliver: async (payload) => {
|
||||||
const replyThreadTs = replyReference.use();
|
const replyThreadTs = replyPlan.nextThreadTs();
|
||||||
await deliverReplies({
|
await deliverReplies({
|
||||||
replies: [payload],
|
replies: [payload],
|
||||||
target: replyTarget,
|
target: replyTarget,
|
||||||
@@ -1168,8 +1168,7 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
|||||||
replyThreadTs,
|
replyThreadTs,
|
||||||
});
|
});
|
||||||
didSendReply = true;
|
didSendReply = true;
|
||||||
replyReference.markSent();
|
replyPlan.markSent();
|
||||||
hasRepliedRef.value = replyReference.hasReplied();
|
|
||||||
},
|
},
|
||||||
onError: (err, info) => {
|
onError: (err, info) => {
|
||||||
runtime.error?.(
|
runtime.error?.(
|
||||||
@@ -2074,13 +2073,53 @@ export function resolveSlackThreadTs(params: {
|
|||||||
messageTs: string | undefined;
|
messageTs: string | undefined;
|
||||||
hasReplied: boolean;
|
hasReplied: boolean;
|
||||||
}): string | undefined {
|
}): string | undefined {
|
||||||
const planner = createReplyReferencePlanner({
|
const planner = createSlackReplyReferencePlanner({
|
||||||
|
replyToMode: params.replyToMode,
|
||||||
|
incomingThreadTs: params.incomingThreadTs,
|
||||||
|
messageTs: params.messageTs,
|
||||||
|
hasReplied: params.hasReplied,
|
||||||
|
});
|
||||||
|
return planner.use();
|
||||||
|
}
|
||||||
|
|
||||||
|
type SlackReplyDeliveryPlan = {
|
||||||
|
nextThreadTs: () => string | undefined;
|
||||||
|
markSent: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
function createSlackReplyReferencePlanner(params: {
|
||||||
|
replyToMode: "off" | "first" | "all";
|
||||||
|
incomingThreadTs: string | undefined;
|
||||||
|
messageTs: string | undefined;
|
||||||
|
hasReplied?: boolean;
|
||||||
|
}) {
|
||||||
|
return createReplyReferencePlanner({
|
||||||
replyToMode: params.replyToMode,
|
replyToMode: params.replyToMode,
|
||||||
existingId: params.incomingThreadTs,
|
existingId: params.incomingThreadTs,
|
||||||
startId: params.messageTs,
|
startId: params.messageTs,
|
||||||
hasReplied: params.hasReplied,
|
hasReplied: params.hasReplied,
|
||||||
});
|
});
|
||||||
return planner.use();
|
}
|
||||||
|
|
||||||
|
function createSlackReplyDeliveryPlan(params: {
|
||||||
|
replyToMode: "off" | "first" | "all";
|
||||||
|
incomingThreadTs: string | undefined;
|
||||||
|
messageTs: string | undefined;
|
||||||
|
hasRepliedRef: { value: boolean };
|
||||||
|
}): SlackReplyDeliveryPlan {
|
||||||
|
const replyReference = createSlackReplyReferencePlanner({
|
||||||
|
replyToMode: params.replyToMode,
|
||||||
|
incomingThreadTs: params.incomingThreadTs,
|
||||||
|
messageTs: params.messageTs,
|
||||||
|
hasReplied: params.hasRepliedRef.value,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
nextThreadTs: () => replyReference.use(),
|
||||||
|
markSent: () => {
|
||||||
|
replyReference.markSent();
|
||||||
|
params.hasRepliedRef.value = replyReference.hasReplied();
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deliverSlackSlashReplies(params: {
|
async function deliverSlackSlashReplies(params: {
|
||||||
|
|||||||
Reference in New Issue
Block a user