feat: auto-respond in bot-created Discord threads - #144
Conversation
📝 WalkthroughWalkthroughDetects Discord thread context and consistently uses the thread's parent channel ID for group lookups, trigger resolution, and routing. Adds auto-triggering for threads created by the bot and preserves thread context for replies, logging, and attachment routing. DM and non-thread behavior remain unchanged. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
src/channels/discord.ts (1)
432-438: Consider extracting JID computation to reduce duplication.The thread-aware JID computation logic (DM →
dc:dm:, thread →dc:<parentId>, channel →dc:<channelId>) is duplicated at lines 396-400 and 434-438. Extracting a small helper would improve maintainability.♻️ Proposed refactor
Add a helper function near the top of the class or as a module-level function:
function computeChatJid( isDM: boolean, authorId: string, channelId: string, threadParentId: string | null, ): string { if (isDM) return `dc:dm:${authorId}`; if (threadParentId) return `dc:${threadParentId}`; return `dc:${channelId}`; }Then replace both occurrences:
- const _chatJid = isDM - ? `dc:dm:${message.author.id}` - : isThread && threadParentId - ? `dc:${threadParentId}` - : `dc:${message.channelId}`; + const _chatJid = computeChatJid(isDM, message.author.id, message.channelId, threadParentId);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/channels/discord.ts` around lines 432 - 438, Extract the duplicated JID computation into a single helper (e.g., computeChatJid) that accepts isDM, authorId, channelId, and threadParentId and returns the correct string ("dc:dm:<authorId>" for DMs, "dc:<threadParentId>" for thread parents, otherwise "dc:<channelId>"); then replace both inline computations in this file (the block that builds chatJid in the message handling and the other occurrence noted in the review) with calls to computeChatJid(isDM, message.author.id, message.channelId, threadParentId) so the logic is centralized and maintainable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/channels/discord.ts`:
- Around line 529-536: The code prepends "[In thread...]" then tests
TRIGGER_PATTERN, which fails if the original message started with an `@-mention`;
capture the original content before modifying (e.g., const originalContent =
content), test TRIGGER_PATTERN.test(originalContent) to decide whether to add
`@${ASSISTANT_NAME}`, then set content = `[In thread: ${threadName}]
${originalContent}` (or attach the mention to the modified content only when the
original lacked the trigger). Update the block around
isThread/message.channel.ownerId/botId/threadName/content/TRIGGER_PATTERN to use
the originalContent check so you don’t double-prefix messages that already
mention the assistant.
- Around line 535-536: Change the hard-coded global ASSISTANT_NAME used when
prefixing content in bot-created threads to use the group's resolved trigger
variable (the same one you compute earlier for `@mention` handling) so multi-agent
setups use their per-group trigger; locate the assignment that sets content =
`@${ASSISTANT_NAME} ${content}` and replace the ASSISTANT_NAME reference with
the group's resolved trigger variable (the one computed in the `@mention` logic)
so auto-triggering in bot-created threads uses the group's trigger consistently.
---
Nitpick comments:
In `@src/channels/discord.ts`:
- Around line 432-438: Extract the duplicated JID computation into a single
helper (e.g., computeChatJid) that accepts isDM, authorId, channelId, and
threadParentId and returns the correct string ("dc:dm:<authorId>" for DMs,
"dc:<threadParentId>" for thread parents, otherwise "dc:<channelId>"); then
replace both inline computations in this file (the block that builds chatJid in
the message handling and the other occurrence noted in the review) with calls to
computeChatJid(isDM, message.author.id, message.channelId, threadParentId) so
the logic is centralized and maintainable.
Messages sent in Discord threads created by this bot now trigger the agent automatically — no @mention required, same as DMs. **Changes to handleMessage:** - Detect thread messages via `message.channel.isThread()` - For threads, resolve the parent channel JID for group lookup (threads aren't registered groups, but their parent channels are) - Bot filter block updated to also use parent channel JID for threads (preserves per-group trigger patterns for agent-to-agent comms) - Auto-trigger logic: if `message.channel.ownerId === botId`, prepend trigger and thread context (`[In thread: <name>]`) to the message - Responses currently go to the parent channel; full in-thread reply routing can be added in a follow-up PR **Not changed:** - Thread registration: no new DB entries needed - Message storage: thread messages stored under parent channel JID - Reactions and other Discord events: unaffected Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Use per-group trigger name/pattern for auto-trigger in bot threads (fixes multi-agent setups where group trigger differs from ASSISTANT_NAME) - Check original content before prepending thread context to avoid potential double trigger prefix edge case - Fix mentionsOtherUsersOnly TS2345 error: add botId != null guard so Map.has() receives string (not string | undefined) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
65303bf to
5f5ea4d
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/channels/discord.ts`:
- Around line 450-452: The thread parent resolution uses
message.channel.parent?.id which can be null if the parent channel isn't cached;
update the logic in the thread handling where threadParentId is computed
(currently referencing isThread and message.channel.parent) to prefer
message.channel.parentId (the raw API field) and fall back to
message.channel.parent?.id only if needed, ensuring threadParentId is set
reliably for routing through the registered parent channel.
| const threadParentId = isThread && message.channel.parent | ||
| ? message.channel.parent.id | ||
| : null; |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
discord.js v14 ThreadChannel parentId property documentation
💡 Result:
In discord.js v14, ThreadChannel#parentId is documented on the ThreadChannel class as:
parentId: Snowflake | null— “The id of the parent channel of this thread.” (discord.js.org)
Related property:
parent: ... | null— the cached parent channel object (may benullif it isn’t in cache). (discord.js.org)
Citations:
- 1: https://discord.js.org/docs/packages/discord.js/14.25.1/ThreadChannel%3AClass?utm_source=openai
- 2: https://discord.js.org/docs/packages/discord.js/14.18.0/PublicThreadChannel%3Ainterface?utm_source=openai
🌐 Web query:
discord.js v14 ThreadChannel parent vs parentId caching behavior
💡 Result:
In discord.js v14, ThreadChannel#parent and ThreadChannel#parentId differ mainly because one is cache-backed and the other is raw API data:
thread.parentis derived from cache and therefore can benullif the parent channel isn’t in your client’s channel cache (e.g., aggressive cache limits, unusual client options, or the thread object came from a payload that didn’t populate/cache the parent channel). The typings reflect this:parentis… | null. [1]thread.parentIdis (normally) the stable way to identify the parent and is intended to be usable for fetching the parent (client.channels.fetch(thread.parentId)), but it is typed asSnowflake | nullfor historical/edge payload reasons (Discord sometimes omittedparent_idin certain contexts in the past). Maintainers have describedparentas nullable due to cache access, andparentIdas the field you can use to fetch. [2]- Practical pattern you’ll see in the ecosystem: use
parentIdfor identification, andparentonly when you’re okay with cache dependence (often guarded with optional chaining). [3][1]
What to do if you need the parent reliably:
- Prefer
thread.parentIdoverthread.parent?.id. [3] - If
thread.parentisnull, fetch the parent by id:await client.channels.fetch(thread.parentId)(guard for the null type). [2] - If you ever observe
parentId === null, re-fetch the thread (await thread.fetch()) and re-check—this can correct “incomplete” thread objects depending on how you received them. (The nullable typing exists because such incomplete payloads have existed.) [2][1]
Rule of thumb: parent = cached object (may be missing), parentId = the identifier you should store/use to fetch. [1][2][3]
Sources: [1] discord.js ThreadChannel docs (v14.25.1) parent/parentId types and descriptions, [2] discord.js issue discussion captured by Lightrun about parent being nullable due to cache and parentId being the fetch key, [3] Stack Overflow example using message.channel.parentId for forum-thread parent identification.
🏁 Script executed:
# Check if the repository has discord.js types or package.json that reveals the version
find . -name "package.json" -o -name "package-lock.json" -o -name "yarn.lock" | head -5Repository: omniaura/nanoclaw
Length of output: 235
🏁 Script executed:
# Look for existing thread handling code in the file to understand the context
grep -n "ThreadChannel\|parentId\|parent\.id" src/channels/discord.ts | head -20Repository: omniaura/nanoclaw
Length of output: 243
🏁 Script executed:
sed -n '440,460p' src/channels/discord.tsRepository: omniaura/nanoclaw
Length of output: 981
🏁 Script executed:
# Check the type of message.channel at this point in the code
sed -n '430,480p' src/channels/discord.tsRepository: omniaura/nanoclaw
Length of output: 2328
🏁 Script executed:
# Check the type definitions and imports at the top of the file
head -50 src/channels/discord.tsRepository: omniaura/nanoclaw
Length of output: 1303
🏁 Script executed:
# Look for where message.channel is typed or used with type assertions
grep -B5 -A5 "message.channel.isThread" src/channels/discord.ts | head -30Repository: omniaura/nanoclaw
Length of output: 459
Prefer parentId over parent?.id for thread parent resolution.
ThreadChannel.parent can be null if the parent channel isn't cached, whereas parentId is always available as the raw API data field. If caching causes parent to be null, messages won't route through the registered parent channel.
♻️ Suggested fix
- const threadParentId = isThread && message.channel.parent
- ? message.channel.parent.id
+ const threadParentId = isThread && 'parentId' in message.channel
+ ? message.channel.parentId
: null;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const threadParentId = isThread && message.channel.parent | |
| ? message.channel.parent.id | |
| : null; | |
| const threadParentId = isThread && 'parentId' in message.channel | |
| ? message.channel.parentId | |
| : null; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/channels/discord.ts` around lines 450 - 452, The thread parent resolution
uses message.channel.parent?.id which can be null if the parent channel isn't
cached; update the logic in the thread handling where threadParentId is computed
(currently referencing isThread and message.channel.parent) to prefer
message.channel.parentId (the raw API field) and fall back to
message.channel.parent?.id only if needed, ensuring threadParentId is set
reliably for routing through the registered parent channel.
Summary
@mentionrequired[In thread: thread-name]so the agent knows where it's respondingHow it works
Known limitation
Responses go to the parent channel, not the thread. Full in-thread reply routing requires tracking
reply_jidin the message schema and plumbing it throughsendMessage— can be done in a follow-up PR.Test plan
@mentionstill works in both thread typesCloses #77
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes