From 2e654e8d639758f51da87da76648362eae607496 Mon Sep 17 00:00:00 2001 From: David Guttman Date: Mon, 12 Jan 2026 13:11:48 -1000 Subject: [PATCH] Fix Discord autoThread thread-only replies (#807) Co-authored-by: Shadow --- CHANGELOG.md | 1 + src/discord/monitor.ts | 30 +++++++++++++++++++----------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f99fd1571..74b74a958 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,7 @@ - Auto-reply: elevated/reasoning toggles now enqueue system events so the model sees the mode change immediately. - Tools: keep `image` available in sandbox and fail over when image models return empty output (fixes “(no text returned)”). - Discord: add per-channel `autoThread` to auto-create threads for top-level messages. (#800) — thanks @davidguttman. +- Discord: fix autoThread routing so replies stay in the created thread and avoid reply references. (#807) — thanks @davidguttman. - Onboarding: TUI defaults to `deliver: false` to avoid cross-provider auto-delivery leaks; onboarding spawns the TUI with explicit `deliver: false`. (#791 — thanks @roshanasingh4) ## 2026.1.11 diff --git a/src/discord/monitor.ts b/src/discord/monitor.ts index 2d856ad4b..4ff3dc07d 100644 --- a/src/discord/monitor.ts +++ b/src/discord/monitor.ts @@ -1179,23 +1179,26 @@ export function createDiscordMessageHandler(params: { OriginatingChannel: "discord" as const, OriginatingTo: discordTo, }; - const replyTarget = ctxPayload.To ?? undefined; + let replyTarget = ctxPayload.To ?? undefined; if (!replyTarget) { runtime.error?.(danger("discord: missing reply target")); return; } + const originalReplyTarget = replyTarget; + let deliverTarget = replyTarget; if (isGuildMessage && channelConfig?.autoThread && !threadChannel) { try { - const base = truncateUtf16Safe( - (baseText || combinedBody || "Thread").replace(/\s+/g, " ").trim(), - 80, - ); - const authorLabel = author.username ?? author.id; - const threadName = - truncateUtf16Safe(`${authorLabel}: ${base}`.trim(), 100) || - `Thread ${message.id}`; + const rawName = baseText || combinedBody || "Thread"; + const cleanedName = rawName + .replace(/<@!?\d+>/g, "") // user mentions + .replace(/<@&\d+>/g, "") // role mentions + .replace(/<#\d+>/g, "") // channel mentions + .replace(/\s+/g, " ") + .trim(); + const base = truncateUtf16Safe(cleanedName || "Thread", 80); + const threadName = truncateUtf16Safe(base, 100) || `Thread ${message.id}`; const created = (await client.rest.post( `${Routes.channelMessage(message.channelId, message.id)}/threads`, @@ -1210,6 +1213,8 @@ export function createDiscordMessageHandler(params: { const createdId = created?.id ? String(created.id) : ""; if (createdId) { deliverTarget = `channel:${createdId}`; + // When autoThread is enabled, *always* reply in the created thread. + replyTarget = deliverTarget; } } catch (err) { logVerbose( @@ -1251,12 +1256,15 @@ export function createDiscordMessageHandler(params: { deliver: async (payload) => { await deliverDiscordReply({ replies: [payload], - target: deliverTarget, + target: replyTarget, token, accountId, rest: client.rest, runtime, - replyToMode: deliverTarget !== replyTarget ? "off" : replyToMode, + // The original message is in the parent channel; never try to reply-reference it + // when posting inside the newly-created thread. + replyToMode: + deliverTarget !== originalReplyTarget ? "off" : replyToMode, textLimit, maxLinesPerMessage: discordConfig?.maxLinesPerMessage, });