Skip to content

fix(discord): ignore system messages so thread renames don't reach the agent - #51

Merged
chr1syy merged 6 commits into
RunMaestro:mainfrom
scriptease:main
Jul 20, 2026
Merged

fix(discord): ignore system messages so thread renames don't reach the agent#51
chr1syy merged 6 commits into
RunMaestro:mainfrom
scriptease:main

Conversation

@scriptease

@scriptease scriptease commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Problem

When a user renames a Discord thread the AI is working in, Discord emits a messageCreate event carrying a system message (MessageType.ChannelNameChange). Its author is the renaming user (not a bot) and its content is 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:

if (message.author.bot) return;
if (message.system) return;      // ← added
if (!message.guild) return;

message.system is true for 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

    • Discord “system” messages (e.g., thread renames) are now ignored and no longer trigger message processing.
    • The gist command now correctly resolves channel/agent context when used inside a thread.
  • Improvements

    • Gist embeds now use the updated gist URL returned by the service.
    • Agent autocomplete results are now sorted by agent name.
    • Auto-run channel/agent lookup is now more reliable for both autocomplete and execution.
  • Tests

    • Added/updated coverage for system-message handling and gist command behavior in threads.

scriptease and others added 2 commits June 20, 2026 15:55
…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
@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The 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.

Changes

Discord message and command flow updates

Layer / File(s) Summary
System message guard
src/providers/discord/messageCreate.ts, src/__tests__/messageCreate.test.ts
handleMessageCreate returns immediately for Discord system messages, and a test verifies the enqueue callback is not invoked.
Interaction channel resolution
src/providers/discord/channelsDb.ts, src/providers/discord/commands/auto-run.ts
A helper resolves channel bindings from an interaction, including thread parent fallback, and auto-run uses it in autocomplete and execute paths.
Gist command and result shape
src/core/maestro.ts, src/providers/discord/commands/gist.ts, src/__tests__/gist-command.test.ts
GistResult now exposes success, agentId, and gistUrl; the gist command uses the interaction helper and renders links from gistUrl, with tests updated for thread lookup and the new payload shape.
Agent autocomplete sorting
src/providers/discord/commands/agents.ts
Agent autocomplete results are now sorted by agent name before responding.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

  • RunMaestro/Maestro-Relay#26: Introduced the /gist command and the gist result flow that this PR updates to use gistUrl and thread-aware channel resolution.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ 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 clearly matches the main fix: ignoring Discord system messages so thread renames do not reach the agent.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

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

scriptease and others added 4 commits June 28, 2026 07:51
…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

@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

🧹 Nitpick comments (2)
src/providers/discord/commands/agents.ts (1)

338-350: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Inconsistent handling of logger call promises.

Line 341 explicitly discards the promise with void logger.error(...), but the logger.info calls at lines 338 and 347 are invoked without void or await. If logger.info also 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 the success field like its siblings.

sessionList() and sessionShow() both throw when parsed.success === false, but dispatch() returns the parsed payload as-is in both the try and catch paths without checking success, unlike send() which validates parsed.agentId && parsed.usage before returning from the catch branch. Callers of dispatch() must remember to check .success/.error themselves, which is easy to miss and inconsistent with the rest of this API surface.

Consider aligning dispatch()'s error handling with sessionList/sessionShow (throw on success === false) or documenting explicitly why dispatch() 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

📥 Commits

Reviewing files that changed from the base of the PR and between e323180 and 4461fd5.

📒 Files selected for processing (7)
  • src/__tests__/gist-command.test.ts
  • src/core/maestro.ts
  • src/providers/discord/channelsDb.ts
  • src/providers/discord/commands/agents.ts
  • src/providers/discord/commands/auto-run.ts
  • src/providers/discord/commands/gist.ts
  • src/providers/discord/messageCreate.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/providers/discord/messageCreate.ts

Comment on lines +40 to +49
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;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 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:


🏁 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.

@chr1syy
chr1syy merged commit 77245d2 into RunMaestro:main Jul 20, 2026
3 checks passed
@chr1syy

chr1syy commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

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 77245d2 with a commit message spelling out all four fixes, since the PR title only really covered the system-messages one. The parent-channel thread binding in particular was a good catch.

Follow-up on us: adding explicit regression tests for the thread-binding and agent-sort fixes, which currently aren't directly covered.

chr1syy added a commit that referenced this pull request Jul 20, 2026
…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>
chr1syy added a commit that referenced this pull request Jul 20, 2026
)

* 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants