fix(discord): ignore system messages so thread renames don't reach the agent - #51
Conversation
…e agent A thread rename emits a messageCreate system message whose content is the new thread name. With no system-message guard it was enqueued to the agent as a bare message. Filter message.system early (also drops pins, joins, etc). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fix(discord): ignore system messages so thread renames don't reach the agent
📝 WalkthroughWalkthroughThe PR adds a system-message early return in Discord message creation handling, introduces interaction-based channel binding resolution for commands, updates gist result fields and rendering, and sorts agent autocomplete results by name. ChangesDiscord message and command flow updates
Estimated code review effort: 3 (Moderate) | ~25 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…e agent A thread rename emits a messageCreate system message whose content is the new thread name. With no system-message guard it was enqueued to the agent as a bare message. Filter message.system early (also drops pins, joins, etc). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ort agent autocomplete /gist and /auto-run only checked the channel registry with the raw interaction channelId, so they always failed inside session threads. Fall back to the thread's parent channel (same behavior /session has). Also sort the /agents autocomplete list alphabetically by name. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
maestro-cli emits { success, agentId, gistUrl } but GistResult expected
{ url, id }, so the embed rendered [Open gist](undefined).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ents Fix/thread lookup and sorted agents
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
src/providers/discord/commands/agents.ts (1)
338-350: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueInconsistent handling of
loggercall promises.Line 341 explicitly discards the promise with
void logger.error(...), but thelogger.infocalls at lines 338 and 347 are invoked withoutvoidorawait. Iflogger.infoalso returns a promise, this is an inconsistent pattern within the same function.♻️ Suggested consistency fix
- logger.info('discord/disconnect', `Cleaned up files for agent ${agentId}`); + void logger.info('discord/disconnect', `Cleaned up files for agent ${agentId}`);- logger.info( + void logger.info( 'discord/disconnect', `Skipping file cleanup for agent ${agentId} - ${otherChannels.length} other channel(s) and ${otherThreads.length} other thread(s) still active`, );🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/providers/discord/commands/agents.ts` around lines 338 - 350, The disconnect cleanup in agents.ts uses mixed promise handling for logger calls: `logger.error` is explicitly fire-and-forget with `void`, while nearby `logger.info` calls in the same cleanup flow are not. Make the `logger.info` calls in the disconnect cleanup path consistent with the `logger.error` usage by either explicitly discarding them with `void` or awaiting them, matching the async contract of `logger` throughout `agents.ts` and the disconnect logic.src/core/maestro.ts (1)
360-389: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
dispatch()doesn't validate thesuccessfield like its siblings.
sessionList()andsessionShow()both throw whenparsed.success === false, butdispatch()returns the parsed payload as-is in both the try and catch paths without checkingsuccess, unlikesend()which validatesparsed.agentId && parsed.usagebefore returning from the catch branch. Callers ofdispatch()must remember to check.success/.errorthemselves, which is easy to miss and inconsistent with the rest of this API surface.Consider aligning
dispatch()'s error handling withsessionList/sessionShow(throw onsuccess === false) or documenting explicitly whydispatch()intentionally returns error info instead of throwing.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/core/maestro.ts` around lines 360 - 389, dispatch() currently returns parsed CLI output without checking the success field, unlike sessionList() and sessionShow(). Update Maestro.dispatch to validate the parsed DispatchResult in both the normal and stdout-error recovery paths, and throw when success === false so its behavior matches the other helpers; if dispatch is meant to surface error payloads instead, document that explicitly in the method contract near dispatch()/send().
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/providers/discord/channelsDb.ts`:
- Around line 40-49: The thread fallback in getChannelInfoForInteraction depends
on interaction.channel, which can be null and cause a missed parent-channel
lookup. Update the lookup logic to resolve the channel from
interaction.channelId using interaction.client.channels.fetch or an equivalent
cache-based fetch before checking isThread() and parentId, while keeping the
direct channelDb.get(interaction.channelId) lookup unchanged.
---
Nitpick comments:
In `@src/core/maestro.ts`:
- Around line 360-389: dispatch() currently returns parsed CLI output without
checking the success field, unlike sessionList() and sessionShow(). Update
Maestro.dispatch to validate the parsed DispatchResult in both the normal and
stdout-error recovery paths, and throw when success === false so its behavior
matches the other helpers; if dispatch is meant to surface error payloads
instead, document that explicitly in the method contract near dispatch()/send().
In `@src/providers/discord/commands/agents.ts`:
- Around line 338-350: The disconnect cleanup in agents.ts uses mixed promise
handling for logger calls: `logger.error` is explicitly fire-and-forget with
`void`, while nearby `logger.info` calls in the same cleanup flow are not. Make
the `logger.info` calls in the disconnect cleanup path consistent with the
`logger.error` usage by either explicitly discarding them with `void` or
awaiting them, matching the async contract of `logger` throughout `agents.ts`
and the disconnect logic.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 93e81d67-dadc-4a2f-8fa7-892f7c85cbbd
📒 Files selected for processing (7)
src/__tests__/gist-command.test.tssrc/core/maestro.tssrc/providers/discord/channelsDb.tssrc/providers/discord/commands/agents.tssrc/providers/discord/commands/auto-run.tssrc/providers/discord/commands/gist.tssrc/providers/discord/messageCreate.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- src/providers/discord/messageCreate.ts
| export function getChannelInfoForInteraction( | ||
| interaction: ChatInputCommandInteraction | AutocompleteInteraction, | ||
| ): AgentChannel | undefined { | ||
| const direct = channelDb.get(interaction.channelId); | ||
| if (direct) return direct; | ||
| if (interaction.channel?.isThread() && interaction.channel.parentId) { | ||
| return channelDb.get(interaction.channel.parentId); | ||
| } | ||
| return undefined; | ||
| } |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
Is discord.js interaction.channel guaranteed non-null for ChatInputCommandInteraction/AutocompleteInteraction in a cached guild thread, or can it be null/uncached?
💡 Result:
In discord.js, interaction.channel is not guaranteed to be non-null for ChatInputCommandInteraction or AutocompleteInteraction, even if the interaction occurs within a guild thread [1][2][3]. The interaction.channel property is typed as GuildTextBasedChannel | null (or similar, depending on the CacheType reducer) because discord.js can only populate this property if the channel is currently present in the client's internal cache [1][3]. If the bot has not cached the channel—which can happen if the bot does not have permission to view the channel, if the channel was never fetched, or due to other cache-related constraints—interaction.channel will be null [3]. Even when an interaction occurs in a "cached guild" (meaning the guild object is in the cache), the specific channel object might still be missing from the cache, resulting in a null value [3][4]. Therefore, you should always perform a null check before accessing interaction.channel to prevent runtime errors [5]. For a reliable implementation, you should: 1. Always check if interaction.channel is truthy before use [5]. 2. If the channel is required and is null, you may need to fetch it using interaction.client.channels.fetch(interaction.channelId) if you have the necessary permissions [3]. Note that interaction.channelId is always available and guaranteed to be a valid Snowflake for interactions in guilds [1][4][6].
Citations:
- 1: https://discord.js.org/docs/packages/discord.js/14.26.4/ChatInputCommandInteraction:Class
- 2: https://discordjs.dev/docs/packages/discord.js/main/ChatInputCommandInteraction:Class
- 3: thread channel becomes null when view_channel is restricted in parent text channel discordjs/discord.js#7883
- 4: https://discord.js.org/docs/packages/discord.js/main/ChatInputCommandInteraction:Class
- 5: https://www.answeroverflow.com/m/1255317821391376496
- 6: https://discord.js.org/docs/packages/discord.js/main/BaseInteraction:Class
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the surrounding file and related Discord channel lookup logic.
sed -n '1,220p' src/providers/discord/channelsDb.ts
printf '\n---\n'
rg -n "getChannelInfoForInteraction|channelDb|get\\(interaction\\.channelId\\)|isThread\\(" src/providers/discord -g '*.ts'Repository: RunMaestro/Maestro-Relay
Length of output: 5217
Avoid depending on interaction.channel for the thread fallback. interaction.channel can be null here, so a thread interaction may skip the parent-channel lookup and incorrectly return “not connected” even when a binding exists. Resolve the thread channel from interaction.client.channels.fetch(interaction.channelId) (or an equivalent cached lookup) before checking parentId.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/providers/discord/channelsDb.ts` around lines 40 - 49, The thread
fallback in getChannelInfoForInteraction depends on interaction.channel, which
can be null and cause a missed parent-channel lookup. Update the lookup logic to
resolve the channel from interaction.channelId using
interaction.client.channels.fetch or an equivalent cache-based fetch before
checking isThread() and parentId, while keeping the direct
channelDb.get(interaction.channelId) lookup unchanged.
|
Merged — thanks a lot for this, @scriptease, and sorry for the long silence on our side. 🙏 Review came back clean: no blockers, full suite green (258 tests), nothing of concern on the security side. Squashed it in as Follow-up on us: adding explicit regression tests for the thread-binding and agent-sort fixes, which currently aren't directly covered. |
…agent sort (#64) Codex review of #51 flagged two fixes as merged without direct regression tests. Adds coverage for both (tests only, no source changes): - Fix 2 (parent-channel binding): new discord-channelsDb.test.ts unit tests for getChannelInfoForInteraction — direct binding, thread fallback to parent, thread binding taking precedence, and the unbound/no-parentId/no-channel edge cases. Plus an integration test covering the auto-run autocomplete path, which had no coverage (gist already had one). - Fix 4 (sorted agents): asserts /agents autocomplete orders by name, and that sorting happens before the 25-choice Discord cap so the alphabetically-first agents survive truncation. Verified by mutation: reverting either fix fails these tests. Suite: 268 passing (was 258). Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
) * fix(discord): system messages, thread binding, gist URL, sorted agents Bundles four independent Discord provider fixes from the PR (the title only covered the first): 1. Ignore Discord system messages (thread renames, pin notices, joins) so they no longer reach the agent as user input. src/providers/discord/messageCreate.ts 2. Resolve agent binding from the parent channel when a message arrives in a thread, so threads inherit their channel's agent instead of coming back unbound. src/providers/discord/channelsDb.ts, commands/auto-run.ts 3. Use the gistUrl returned by `maestro-cli gist create` instead of reconstructing the URL locally. src/core/maestro.ts, providers/discord/commands/gist.ts 4. Sort the /agents list output for stable, readable ordering. src/providers/discord/commands/agents.ts Thanks to @scriptease for the contribution. Co-authored-by: Florian Agsteiner <florian.agsteiner@gmail.com> * fix(telegram): read renamed gistUrl field after PR #51 port The cherry-pick of 77245d2 renames GistResult.url to gistUrl. rc has a Telegram provider that main does not, and its /gist handler still read result.url. This does NOT fail typecheck: GistResult carries an [key: string]: unknown index signature, so result.url resolves to unknown instead of erroring. The break is runtime-only — Telegram would have posted 'undefined' as the gist URL. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Florian Agsteiner <florian.agsteiner@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Problem
When a user renames a Discord thread the AI is working in, Discord emits a
messageCreateevent carrying a system message (MessageType.ChannelNameChange). Itsauthoris the renaming user (not a bot) and itscontentis the new thread name. The handler had no system-message guard, so it passed every check and enqueued a bare message containing just the new thread name to the agent.Fix
One-line guard in
src/providers/discord/messageCreate.ts:message.systemistruefor all Discord system messages (thread renames, pins, member joins, etc.), so this also filters other system-message noise that should never reach the agent.Tests
Added regression test
handleMessageCreate ignores system messages (e.g. thread rename). All 212 tests pass.🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
Bug Fixes
Improvements
Tests