From f66f6ef9128edac11f855d0829138be218e93992 Mon Sep 17 00:00:00 2001 From: Hitesh Sisara Date: Wed, 24 Jun 2026 15:30:47 +0530 Subject: [PATCH] feat(discord): follow threads after first mention Once the bot is @mentioned in a Discord thread, it responds to all subsequent messages in that thread without requiring further mentions. This matches the behavior of Hermes and other Discord bots where you mention once to activate, then the bot follows the entire thread. Implementation: - Track mentioned thread IDs in a map (capped at 5000 entries) - On each group message, check if channelID is a tracked thread - If yes, skip mention requirement and process the message - If mentioned in a new thread, add it to the tracked set --- internal/channels/discord/discord.go | 18 ++++++++++++------ internal/channels/discord/handler.go | 28 +++++++++++++++++++++++++++- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/internal/channels/discord/discord.go b/internal/channels/discord/discord.go index 887b12fffc..652d3e2818 100644 --- a/internal/channels/discord/discord.go +++ b/internal/channels/discord/discord.go @@ -34,6 +34,11 @@ type Channel struct { audioMgr *audio.Manager // unified STT via audio.Manager (nil = no STT) // pairingService, pairingDebounce, approvedGroups, groupHistory, historyLimit, requireMention // are inherited from channels.BaseChannel. + + // mentionedThreads tracks thread/channel IDs where the bot was @mentioned. + // Once mentioned in a thread, respond to ALL subsequent messages without requiring another mention. + mentionedThreads map[string]bool + mentionedThreadsMu sync.Mutex } // New creates a new Discord channel from config. @@ -66,12 +71,13 @@ func New(cfg config.DiscordConfig, msgBus *bus.MessageBus, pairingSvc store.Pair } ch := &Channel{ - BaseChannel: base, - session: session, - config: cfg, - agentStore: agentStore, - configPermStore: configPermStore, - audioMgr: audioMgr, + BaseChannel: base, + session: session, + config: cfg, + agentStore: agentStore, + configPermStore: configPermStore, + audioMgr: audioMgr, + mentionedThreads: make(map[string]bool), } ch.SetRequireMention(requireMention) ch.SetPairingService(pairingSvc) diff --git a/internal/channels/discord/handler.go b/internal/channels/discord/handler.go index fc7df22979..5a23fcd51f 100644 --- a/internal/channels/discord/handler.go +++ b/internal/channels/discord/handler.go @@ -187,7 +187,33 @@ func (c *Channel) handleMessage(_ *discordgo.Session, m *discordgo.MessageCreate // When not mentioned, record message to pending history for later context. // `mentioned` was pre-computed above for policy gating. if peerKind == "group" && c.RequireMention() { - if !mentioned { + // Check if this message is in a thread where bot was previously mentioned. + inMentionedThread := false + if c.isThreadChannel(ctx, channelID) { + c.mentionedThreadsMu.Lock() + inMentionedThread = c.mentionedThreads[channelID] + c.mentionedThreadsMu.Unlock() + } + + if mentioned { + // Track this thread so future messages don't need a mention. + if c.isThreadChannel(ctx, channelID) { + c.mentionedThreadsMu.Lock() + c.mentionedThreads[channelID] = true + // Cap to prevent unbounded growth + if len(c.mentionedThreads) > 5000 { + i := 0 + for k := range c.mentionedThreads { + if i >= 2500 { + break + } + delete(c.mentionedThreads, k) + i++ + } + } + c.mentionedThreadsMu.Unlock() + } + } else if !inMentionedThread { // Collect media file paths for group history context. var mediaPaths []string for _, mf := range mediaFiles {