Skip to content
This repository was archived by the owner on Feb 21, 2026. It is now read-only.

feat: auto-respond in bot-created Discord threads - #144

Merged
Peyton-Spencer merged 2 commits into
mainfrom
feat/discord-thread-auto-respond-77
Feb 19, 2026
Merged

feat: auto-respond in bot-created Discord threads#144
Peyton-Spencer merged 2 commits into
mainfrom
feat/discord-thread-auto-respond-77

Conversation

@Peyton-Spencer

@Peyton-Spencer Peyton-Spencer commented Feb 19, 2026

Copy link
Copy Markdown

Summary

  • Users who reply in a Discord thread the bot created now get automatic responses — no @mention required
  • The parent channel's registered group config is used for routing (threads aren't registered, but their parent channels are)
  • Thread context is added to the message: [In thread: thread-name] so the agent knows where it's responding
  • Bot-to-bot trigger filtering also uses parent channel context in threads

How it works

Thread message arrives
  ↓
isThread = message.channel.isThread()  → true
threadParentId = message.channel.parent.id  → e.g. "1234567890"
chatJid = "dc:1234567890"  (parent channel, which IS registered)
  ↓
Group found → process normally
  ↓
hasTrigger = false, isReplyToBot = false
message.channel.ownerId === botId?  → YES (bot created this thread)
  ↓
content = "[In thread: my-task-thread] original message"
content = "@PeytonOmni [In thread: my-task-thread] original message"
  ↓
Stored in DB under parent channel JID → agent picks up → responds

Known limitation

Responses go to the parent channel, not the thread. Full in-thread reply routing requires tracking reply_jid in the message schema and plumbing it through sendMessage — can be done in a follow-up PR.

Test plan

  • Create a thread from a bot message in a registered Discord channel
  • Post a message in the thread (no @mention)
  • Verify agent responds in the parent channel with thread context
  • Post a message in a user-created thread (no @mention) — verify agent does NOT auto-respond
  • Verify @mention still works in both thread types

Closes #77

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Discord threads fully integrated: routing and lookups use parent channel context so thread messages map to correct groups.
    • Threads created by the bot auto-trigger processing and include thread-context labels.
    • Bot mentions and per-group triggers now work inside threads; replies preserve thread context and attachments associate with the right group.
  • Bug Fixes

    • @allagents expansion and trigger checks correctly honor thread parent context.

@coderabbitai

coderabbitai Bot commented Feb 19, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Detects 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

Cohort / File(s) Summary
Thread-aware Message Routing
src/channels/discord.ts
Adds thread detection (isThread), computes threadParentId/thread JID for group lookups and chat JID resolution; updates trigger and bot-mention logic to use parent thread IDs; implements auto-triggering in bot-created threads; preserves thread context for replies, attachments, and logging.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐇 I hopped into a thread with a curious cheer,
Found parent channels guiding messages clear.
Bot-made threads now wake on every note,
Replies stay nested — a snug little moat.
Hooray for threads where conversations float!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR implements auto-trigger and thread-context awareness for bot-created threads [#77], but does not implement in-thread reply routing or streaming configuration as specified in the issue requirements. Implement in-thread reply routing (currently responses go to parent channel) and address streaming behavior for thread replies per issue #77 requirements, or document these as planned follow-ups.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding auto-response functionality for bot-created Discord threads, which is the primary focus of the PR.
Out of Scope Changes check ✅ Passed All changes are focused on thread-aware message routing and bot-created thread auto-triggering in Discord; no out-of-scope modifications detected.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/discord-thread-auto-respond-77

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/channels/discord.ts
Comment thread src/channels/discord.ts Outdated
Peyton-Spencer and others added 2 commits February 19, 2026 15:17
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>
@Peyton-Spencer
Peyton-Spencer force-pushed the feat/discord-thread-auto-respond-77 branch from 65303bf to 5f5ea4d Compare February 19, 2026 15:17
@Peyton-Spencer
Peyton-Spencer merged commit 63c53c8 into main Feb 19, 2026
2 of 3 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/channels/discord.ts
Comment on lines +450 to +452
const threadParentId = isThread && message.channel.parent
? message.channel.parent.id
: null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 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 be null if it isn’t in cache). (discord.js.org)

Citations:


🌐 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.parent is derived from cache and therefore can be null if 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: parent is … | null. [1]
  • thread.parentId is (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 as Snowflake | null for historical/edge payload reasons (Discord sometimes omitted parent_id in certain contexts in the past). Maintainers have described parent as nullable due to cache access, and parentId as the field you can use to fetch. [2]
  • Practical pattern you’ll see in the ecosystem: use parentId for identification, and parent only when you’re okay with cache dependence (often guarded with optional chaining). [3][1]

What to do if you need the parent reliably:

  1. Prefer thread.parentId over thread.parent?.id. [3]
  2. If thread.parent is null, fetch the parent by id: await client.channels.fetch(thread.parentId) (guard for the null type). [2]
  3. 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 -5

Repository: 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 -20

Repository: omniaura/nanoclaw

Length of output: 243


🏁 Script executed:

sed -n '440,460p' src/channels/discord.ts

Repository: 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.ts

Repository: 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.ts

Repository: 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 -30

Repository: 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.

Suggested change
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.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Discord: Agent should respond to messages in threads it created

1 participant