Skip to content

Conversation

@odilitime
Copy link
Member

@odilitime odilitime commented Dec 23, 2025

Note

Introduces scalable voice and messaging architecture with clear contracts, multi-bot support, and user-visible status improvements.

  • Multi-bot voice architecture: New DiscordClientRegistry and VoiceConnectionManager with per-bot clients and VoiceManager integration; exports CHANNEL_TTS/MUSIC/SFX/AMBIENT and defaults in audioChannels.ts with priority/ducking
  • Audio sink contract: Adds IAudioSink (src/contracts.ts) and DiscordAudioSink export for broadcast-based music integration; docs detail auto-wiring and reconnection
  • Progressive message updates: New ProgressiveMessage, MessageManager adjustments, and README/guide (PROGRESSIVE_UPDATES.md) for in-place edits with throttling/TTL
  • New actions: SET_VOICE_CHANNEL_STATUS and SET_LISTENING_ACTIVITY with robust parsing/validation; existing actions updated to resolve guild via channelId with snowflake-safe fallbacks
  • Attachments & memories: Smarter audio/video transcription + selective summarization, safer logging, and startup scrub to remove base64 data-URL images from saved attachments
  • Environment & validation: Clear token sourcing (prefers DISCORD_API_TOKEN / DISCORD_BOT_TOKENS), early validation with actionable errors; additional voice env defaults
  • Providers/exports: Adds audioState, agentRole, and plugin info providers; exports sink/contracts, channels, multi-bot types
  • Docs & tests: New architecture docs (MUSIC_ARCHITECTURE.md, COORDINATION_PATTERNS.md), README expansions; tests for token validation, progressive updates, message manager, voice pipeline, and env changes

Written by Cursor Bugbot for commit efb42f9. This will update automatically on new commits. Configure here.

Summary by CodeRabbit

  • New Features

    • Multi‑bot Discord support, per‑guild voice targets/sinks, multi‑channel prioritized audio (TTS, Music, SFX, Ambient), bridging, progressive in‑place message updates, and actions to set voice channel status and listening activity.
  • Documentation

    • New architecture/how‑to docs (broadcast audio, multi‑channel audio, progressive updates, voice/status) and expanded README with usage examples.
  • Providers

    • New runtime providers for audio state, agent role, and plugin info (dynamic).
  • Utilities

    • Improved attachment/data‑URL handling, message edit helpers, memory scrubbing, and environment/token parsing.
  • Tests

    • Added/expanded tests for messaging, progressive updates, voice pipeline, and token validation.

✏️ Tip: You can customize this high-level summary in your review settings.

…ents, and event-based slash command registration
Copilot AI review requested due to automatic review settings December 23, 2025 04:36
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 23, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds multi-bot Discord support, a broadcast-style multi-channel audio pipeline with sinks and connection/management, progressive in-place message updates, voice-status/listening actions, data-URL attachment utilities, many dynamic providers, expanded types/exports, and accompanying tests and helpers.

Changes

Cohort / File(s) Summary
Documentation
COORDINATION_PATTERNS.md, MUSIC_ARCHITECTURE.md, PROGRESSIVE_UPDATES.md, VOICE_STATUS_FEATURES.md, README.md
New and expanded docs describing coordination patterns, broadcast music architecture, progressive message updates, voice status/listening APIs, multi-channel audio, and README integrations.
Client Registry & Init
src/clientRegistry.ts, src/index.ts, src/service.ts, src/tests.ts
New DiscordClientRegistry, multi-bot startup/login flows, token parsing/validation, env init wiring, additional exports, and scrubbing step. Review token validation, login timeouts, and lifecycle wiring.
Voice & Connection Management
src/voice.ts, src/voiceConnectionManager.ts, src/voiceConnectionManager.ts
Per-channel playback, priority/ducking/mixing, bridging APIs, expanded VoiceManager public surface, and VoiceConnectionManager for cross-bot connection tracking. Audit concurrency, cleanup, and public API surface.
Audio Contracts & Sink
src/contracts.ts, src/sinks/discordAudioSink.ts, src/sinks/index.ts
New IAudioSink/AudioSinkStatus contract and DiscordAudioSink implementation with status events, polling/attachment, and re-export. Verify event typings and lifecycle/cleanup behavior.
Audio Channels & Environment
src/audioChannels.ts, src/environment.ts
New channel constants/configs (TTS, MUSIC, SFX, AMBIENT), helpers (getChannelName, canInterrupt), and new voice-related env settings with parsing/defaults. Validate config parsing and defaults.
Progressive Messaging
src/progressiveMessage.ts, src/messages.ts, __tests__/progressiveMessage.test.ts
New ProgressiveMessage class and MessageManager integration: correlationId edits, TTL, typing management, attachment preservation, edit fallback, and tests. Review TTL semantics, edit fallback, and memory creation.
Voice Status & Listening Actions
src/actions/setVoiceChannelStatus.ts, src/actions/setListeningActivity.ts
New LLM templates/actions to set/clear voice channel status and listening activity with channel resolution, permission checks, and memory logging. Review permission/error flows and channel resolution.
Attachments & Utilities
src/utils.ts, src/attachments.ts
Data URL support, createAttachmentFromMedia, editMessageContent, filterAttachmentsForMemory, and agentIdentifier logging helper. Audit binary handling, data-url parsing, and memory filtering.
Providers & State
src/providers/audioState.ts, src/providers/plugin-info.ts, src/providers/channelState.ts, src/providers/voiceState.ts, src/providers/agentRole.ts
New dynamic providers for audio state, plugin instructions/settings, agent role info; some providers marked dynamic and using DISCORD_SERVICE_NAME. Check sensitive-data exposure and gating.
Tests & Test Adjustments
__tests__/messageManager.test.ts, __tests__/token-validation.test.ts, __tests__/voiceManager.test.ts, __tests__/progressiveMessage.test.ts, __tests__/environment.test.ts
Tests added/updated: MessageManager constructor now expects IDiscordService + runtime; token validation tests; voice and progressive tests; ensure mocks fit new interfaces.
Emoji, Search & Guild Resolution
src/actions/reactToMessage.ts, src/actions/searchMessages.ts, many src/actions/*
Emoji selection enriched with character prefs and sentiment; many actions updated to resolve guilds/channels via channelId or messageServerId with snowflake checks. Audit fallback correctness and permission checks.
Compatibility & Types
src/compat.ts, src/types.ts
Proxy binding fix and expanded types (Readable import, IDiscordService extension, new settings/types). Review public type changes for backward compatibility.
Public API Surface
src/index.ts, src/voice.ts, src/contracts.ts, src/service.ts
Many new exports (audio channel types/helpers, ProgressiveMessage, DiscordClientRegistry, sinks, voice APIs, providers, actions). Verify export consistency and docs alignment.
Miscellaneous
src/actions/*, src/tests.ts, src/compat.ts, src/attachments.ts
Logging identity improvements (agentIdentifier), compat/runtime proxy binding, and various small refactors. Review logging changes for privacy and correctness.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Dev as Developer/User
  participant Registry as DiscordClientRegistry
  participant Service as DiscordService
  participant ConnMgr as VoiceConnectionManager
  participant VM as VoiceManager
  participant Sink as DiscordAudioSink

  Dev->>Registry: registerBot(config)
  Registry->>Service: create client + attach VoiceManager
  Service->>ConnMgr: registerConnection(botId,guildId,channelId,channel,voiceManager)
  Dev->>Service: request playAudio(stream, channel)
  Service->>VM: playAudio(stream, channel)
  VM->>Sink: feed(stream)
  Sink-->>VM: emit statusChange / error
  ConnMgr->>Service: expose VoiceTarget status
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐇
I hopped through queues and audio streams,
Wired sinks and multi-bot dreams,
Edited messages, held the line,
Channels duck, reconnect, and shine,
A little rabbit hums — bravo, fine!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately summarizes the main changes: multi-bot voice architecture, audio channels, and progressive updates are all central to the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 87.18% which is sufficient. The required threshold is 80.00%.

✏️ 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 odi-voice


📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between be12cf2 and efb42f9.

📒 Files selected for processing (1)
  • src/clientRegistry.ts
🧰 Additional context used
🧬 Code graph analysis (1)
src/clientRegistry.ts (2)
src/service.ts (1)
  • DiscordService (117-4872)
src/index.ts (1)
  • DiscordEventTypes (259-259)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cursor Bugbot
🔇 Additional comments (12)
src/clientRegistry.ts (12)

1-17: LGTM!

The imports are appropriate for the multi-bot registry functionality, and the BotClientInfo interface is well-designed with clear separation between construction-time fields (client, voiceManager, config) and post-login fields (botId, username).


24-59: LGTM!

The token validation is thorough, covering common misconfiguration scenarios (literal "undefined" strings, empty tokens, incorrect format). The detailed error messages will help developers troubleshoot authentication issues quickly.


64-73: LGTM!

Class fields are appropriately encapsulated, and the loginPromises map enables proper lifecycle management for in-flight login operations.


78-134: LGTM - Past concern addressed.

The previous review flagged incorrect use of DISCORD_APPLICATION_ID as a token fallback. This version correctly documents that DISCORD_APPLICATION_ID is NOT a valid token (lines 84-85, 94, 101) and only uses DISCORD_API_TOKEN for backward compatibility. The sequential registration in the loop preserves alias ordering, which is appropriate.


139-203: LGTM - Past concern addressed.

The previous review flagged that client.destroy() was not called on failed registration. This has been fixed at line 196, ensuring proper cleanup of Discord.js Client resources (REST client, caches, WebSocket connections) before removing from internal maps.


216-288: LGTM - Past concern addressed.

The previous review flagged that the ready listener persisted after timeout rejection, causing potential re-keying of cleaned-up clients. This has been properly addressed with:

  1. A settled flag preventing double-resolution
  2. A cleanup() function that removes all listeners and clears the timeout
  3. Cleanup called on all exit paths: ready, timeout, and login error

The implementation correctly handles the race condition between timeout and successful login.


293-325: LGTM!

Clean accessor methods with appropriate fallback logic in getClient() for alias lookup. The getClientForGuild() method returns the first matching bot, which is reasonable for the multi-bot use case.


330-347: LGTM!

The cleanup is thorough - destroying the client and removing entries by both botId and alias. The defensive delete by alias (line 345) is harmless when the alias isn't a key, and ensures no stale references remain.


352-360: LGTM!

Good backward compatibility pattern - tries the 'default' alias first (single-bot mode), then falls back to the first available client.


365-385: LGTM - Past concern addressed.

The previous review flagged a race condition where in-flight logins could add clients back after destroyAll completed. This is now fixed by waiting for all pending login promises with Promise.allSettled before proceeding with destruction.


390-399: LGTM!

Simple utility methods for checking registry state.


405-476: LGTM!

Well-structured event emission with thoughtful optimizations:

  1. Fetches full guild data only when needed (line 417)
  2. Skips user pre-fetch for large guilds (>1000 members) to avoid rate limiting and memory pressure
  3. Background user sync for small guilds is properly fire-and-forget with error handling
  4. Individual guild failures don't stop the iteration (inner try-catch)
  5. Both Discord-specific and platform-agnostic events are emitted for flexibility

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


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 and usage tips.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds comprehensive multi-bot voice token support to the Discord plugin, enabling multiple Discord bots to manage voice connections across different channels. Key enhancements include progressive message updates for real-time user feedback, a priority-based multi-channel audio system, and voice connection health monitoring.

Key Changes:

  • Multi-bot infrastructure with token validation and client registry
  • Progressive message updates that edit Discord messages in real-time
  • Priority-based audio channel system (TTS, Music, SFX, Ambient)
  • Voice connection health monitoring and auto-reconnection
  • Audio sink abstraction for plugin interoperability

Reviewed changes

Copilot reviewed 31 out of 31 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/voiceConnectionManager.ts New voice connection tracker for multiple bots across guilds
src/voice.ts Major refactor with multi-channel audio, health monitoring, ducking
src/clientRegistry.ts Multi-bot client management with token validation
src/service.ts Updated initialization for multi-bot support and auto-join
src/messages.ts Progressive message tracking with TTL and correlation IDs
src/progressiveMessage.ts Helper for throttled, debounced message updates
src/audioChannels.ts Priority-based audio channel configuration
src/contracts.ts IAudioSink interface for audio playback abstraction
src/sinks/discordAudioSink.ts Discord implementation of IAudioSink
src/types.ts New interfaces for multi-bot, VoiceTarget, audio settings
src/utils.ts Added editMessageContent for message editing
src/environment.ts New voice ducking and threshold settings
src/actions/* New actions for voice channel status and listening activity
src/providers/audioState.ts Provider for bot audio state (mute/deafen)
tests/* Test coverage for new features

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 19

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
README.md (1)

300-322: Remove duplicate "Attachment Handler" section.

There are two "Attachment Handler" sections:

  • Lines 306-309 (under new components)
  • Lines 318-322 (duplicate)

Remove the duplicate at lines 318-322.

🔎 Proposed fix
 ### Attachment Handler
 - Downloads and processes Discord attachments
 - Supports various media types
 - Integrates with media transcription

 ### VoiceManager

 - Manages voice channel interactions
 - Handles joining and leaving voice channels
 - Processes voice events and audio streams
 - Integrates with transcription services

-### Attachment Handler
-
-- Downloads and processes Discord attachments
-- Supports various media types
-- Integrates with media transcription
-
 ## Developer Guide
🧹 Nitpick comments (28)
src/attachments.ts (1)

104-108: Leftover TODO comment should be resolved or removed.

The comment // 'audio/wav' default? appears to be an open question or debugging note. The current code correctly handles all audio/* content types generically (preserving the original buffer and mime type), which is appropriate behavior.

If there's a specific concern about WAV handling, please clarify or create an issue. Otherwise, consider removing this comment before merging.

PROGRESSIVE_UPDATES.md (1)

10-14: Consider adding language hints to fenced code blocks.

Static analysis flagged these blocks as missing language specifiers. While intentionally plain text for user/bot interaction examples, adding a language hint (e.g., ```text) would satisfy linters and improve syntax highlighting consistency.

Also applies to: 21-30, 125-130, 143-146

src/actions/setListeningActivity.ts (2)

160-165: Use runtime.logger instead of console.error for consistency.

The codebase uses runtime.logger for structured logging. Replace console.error calls with runtime.logger.error for consistency and to enable proper log aggregation.

🔎 Proposed fix
     } catch (error) {
-      console.error('Error setting listening activity:', error);
+      runtime.logger.error({
+        src: 'plugin:discord',
+        action: 'SET_LISTENING_ACTIVITY',
+        error: error instanceof Error ? error.message : String(error),
+      }, 'Error setting listening activity');
       await callback({
         text: 'I encountered an error while trying to update my listening activity.',
         source: 'discord',
       });
     }

102-108: Same logging consistency issue here.

     if (!activityInfo) {
-      console.error("Couldn't parse listening activity information from message");
+      runtime.logger.error("Couldn't parse listening activity information from message");
src/providers/channelState.ts (1)

62-63: Remove the uncertain comment or resolve the question.

The comment // is ServiceType.DISCORD better? suggests unresolved design uncertainty. Since this PR is standardizing on DISCORD_SERVICE_NAME for service lookups (as seen consistently in other files like src/tests.ts), this comment should either be removed or the question should be resolved before merging.

Proposed fix
-      // is ServiceType.DISCORD better?
       const discordService = runtime.getService(DISCORD_SERVICE_NAME) as DiscordService;
__tests__/voiceManager.test.ts (2)

117-148: Address potential test flakiness and unhandled promise.

Two concerns with this test:

  1. Unhandled promise: playPromise is never awaited or handled, which could lead to unhandled rejection warnings if the promise rejects.

  2. Arbitrary delay: The 50ms setTimeout (line 140) is a race condition waiting to happen in CI environments. Consider using a deterministic approach.

Proposed improvement
         // Start playback but don't await fully - we just want to check setup
         const playPromise = voiceManager.playAudio(originalStream, { guildId });
 
-        // Wait a bit for async operations
-        await new Promise(resolve => setTimeout(resolve, 50));
+        // Wait for demuxProbe to be called
+        await vi.waitFor(() => {
+            expect(demuxProbeMock).toHaveBeenCalled();
+        }, { timeout: 100 });
 
         expect(demuxProbeMock).toHaveBeenCalledWith(originalStream);
         expect(createAudioResourceMock).toHaveBeenCalledWith(demuxedStream, { inputType: 'opus', inlineVolume: true });
 
         // Clean up
         originalStream.destroy();
         demuxedStream.destroy();
+        
+        // Ensure promise is handled to avoid unhandled rejection
+        playPromise.catch(() => { /* expected during cleanup */ });

150-203: Consider simplifying the assertion for userStream event.

The test correctly validates that no empty-stream warning is logged and that the userStream event is emitted. However, the JSON.stringify check on line 194 for detecting warnings could be brittle if the log format changes.

Alternative assertion approach
         // Check that no warning about empty stream was logged
         const warningCalls = warnSpy.mock.calls;
         const hasEmptyStreamWarning = warningCalls.some((call) => {
             const msg = call[0];
-            return typeof msg === 'object' && JSON.stringify(msg).includes('No receiveStream');
+            if (typeof msg === 'object' && msg !== null) {
+                return Object.values(msg).some(v => 
+                    typeof v === 'string' && v.includes('No receiveStream')
+                );
+            }
+            return typeof msg === 'string' && msg.includes('No receiveStream');
         });
__tests__/token-validation.test.ts (3)

1-1: Inconsistent test framework usage.

This file uses bun:test while other test files in this PR (e.g., voiceManager.test.ts, multibot-simulcast.test.ts, progressiveMessage.test.ts) use vitest. Consider standardizing on one test framework to maintain consistency and avoid potential CI configuration issues.

Convert to vitest for consistency
-import { describe, it, expect } from 'bun:test';
+import { describe, it, expect } from 'vitest';

85-91: Token format validation may be too lenient.

The mock token 'AAAAAAAAAAAAAAAAAAAAAA.AAAAAA.AAAAAAAAAAAAAAAAAAAAAAAAA_a' has 55 characters, just above the 50-character threshold. Real Discord tokens are typically 70+ characters. Consider using a more realistic mock token length to ensure the validation properly handles real-world token sizes.


3-41: Export validateDiscordToken for direct testing.

The test function duplicates the actual validation logic from src/clientRegistry.ts rather than testing the real implementation. If the actual implementation changes, these tests won't catch regressions. Export validateDiscordToken so tests can directly invoke the source function instead of maintaining parallel logic.

__tests__/multibot-simulcast.test.ts (1)

63-78: Timing-based assertions may be flaky in CI.

The assertions on lines 70 and 77 rely on timing thresholds (100ms and 50ms) which can fail in resource-constrained CI environments or under load. Consider:

  1. Increasing the thresholds with a buffer
  2. Making the assertions relative rather than absolute
  3. Skipping timing assertions in CI if necessary
Proposed adjustment for reliability
-    // Assert: Route should complete quickly (under 100ms for simulcast)
-    expect(routeTime).toBeLessThan(100);
+    // Assert: Route should complete quickly (under 500ms for simulcast, allowing CI variance)
+    expect(routeTime).toBeLessThan(500);

     // Verify all targets started within acceptable window
     const playbackTimes = mockTargets.map(t => (t as any).playbackStartTime);
     const maxDelta = Math.max(...playbackTimes) - Math.min(...playbackTimes);

-    // All targets should start within 50ms of each other
-    expect(maxDelta).toBeLessThan(50);
+    // All targets should start within 200ms of each other (allowing CI variance)
+    expect(maxDelta).toBeLessThan(200);
__tests__/progressiveMessage.test.ts (1)

99-111: Correlation ID test provides limited value.

The test comment acknowledges that internal correlation IDs can't be tested directly, and the test ends with expect(true).toBe(true) which is essentially a no-op. Consider either:

  1. Removing this test if correlation IDs truly can't be validated
  2. Testing observable behavior that depends on correlation IDs (e.g., that updates don't interfere between instances)
Potential improvement
   describe('Correlation ID', () => {
-    it('should generate unique correlation IDs for different instances', () => {
+    it('should allow concurrent instances without interference', async () => {
       const progress1 = new ProgressiveMessage(mockCallback, 'discord');
       const progress2 = new ProgressiveMessage(mockCallback, 'discord');

-      // Both should have different internal correlation IDs
-      // (We can't test this directly, but we test the behavior)
-      progress1.update('Test 1');
-      progress2.update('Test 2');
+      // Both instances should complete independently
+      await Promise.all([
+        progress1.complete('Done 1'),
+        progress2.complete('Done 2'),
+      ]);

-      expect(true).toBe(true); // Simple sanity check
+      // Both should have sent their respective messages
+      const texts = calledWith.map(c => c.text);
+      expect(texts).toContain('Done 1');
+      expect(texts).toContain('Done 2');
     });
   });
VOICE_STATUS_FEATURES.md (1)

82-93: Document potential API availability concerns.

The REST API call to /channels/{channel.id}/voice-status may not be available in all Discord API versions or may require specific bot permissions. Consider adding a note about API availability and fallback behavior when the endpoint is not accessible.

__tests__/multibot-independent.test.ts (3)

45-72: Consider using a more deterministic wait mechanism instead of fixed timeouts.

The 100ms timeout on line 57 for waiting for stream data to be processed could lead to flaky tests on slower CI environments.

🔎 Suggestion: Use event-based or polling wait
     // Wait for stream data to be processed
-    await new Promise(resolve => setTimeout(resolve, 100));
+    // Wait for all targets to receive their stream type
+    await Promise.all(mockTargets.map(target => 
+      new Promise<void>(resolve => {
+        const checkInterval = setInterval(() => {
+          if ((target as any).streamType) {
+            clearInterval(checkInterval);
+            resolve();
+          }
+        }, 10);
+        // Fallback timeout
+        setTimeout(() => { clearInterval(checkInterval); resolve(); }, 500);
+      })
+    ));

135-153: Incomplete test with commented-out assertions.

This test has no actual assertions - the volume control API is hypothetical. Consider either implementing the test when the API is ready, or removing/skipping the test entirely with a clear TODO issue reference.

🔎 Suggestion: Mark as skipped with explanation
-  it('should allow independent volume control per zone', async () => {
+  it.skip('should allow independent volume control per zone', async () => {
     // Note: This would require VoiceTarget to support volume control
-    // This test demonstrates the API design
+    // TODO: Implement when VoiceTarget gains setVolume() method
+    // Issue: <link-to-tracking-issue>

274-279: 50ms timeout fallback could mask test issues.

This timeout ensures the play promise resolves even without data, but it could mask actual bugs where streams don't emit data when they should.

Consider logging when the timeout fallback is used to aid debugging:

         // Timeout fallback - resolve after 50ms if no data
         setTimeout(() => {
           if (!resolved) {
             resolved = true;
+            console.warn(`[Test] Mock target ${target.id} play resolved via timeout fallback`);
             resolve();
           }
         }, 50);
src/sinks/discordAudioSink.ts (2)

75-78: Hardcoded channel value may limit flexibility.

The channel: 1 is hardcoded for "Music channel". If the multi-channel audio system supports different channel priorities, this should be configurable.

🔎 Consider making channel configurable
+import { CHANNEL_MUSIC } from '../audioChannels';
+
 export class DiscordAudioSink extends EventEmitter implements IAudioSink {
   readonly id: string;
   private guildId: string;
   private voiceManager: VoiceManager;
   private _status: AudioSinkStatus = 'disconnected';
+  private channel: number;

-  constructor(id: string, guildId: string, voiceManager: VoiceManager) {
+  constructor(id: string, guildId: string, voiceManager: VoiceManager, channel: number = CHANNEL_MUSIC) {
     super();
     this.id = id;
     this.guildId = guildId;
     this.voiceManager = voiceManager;
+    this.channel = channel;
     
     // ...
   }
   
   async feed(stream: Readable): Promise<void> {
     // ...
     await this.voiceManager.playAudio(stream, {
       guildId: this.guildId,
-      channel: 1, // Music channel
+      channel: this.channel,
     });
   }

147-148: Field declarations should be at the top of the class.

The connectionPollInterval and connectionAttached fields are declared mid-file after the method that uses them. Moving them to the top with other fields improves readability.

🔎 Move field declarations
 export class DiscordAudioSink extends EventEmitter implements IAudioSink {
   readonly id: string;
   private guildId: string;
   private voiceManager: VoiceManager;
   private _status: AudioSinkStatus = 'disconnected';
+  private connectionPollInterval: NodeJS.Timeout | null = null;
+  private connectionAttached = false;

   constructor(id: string, guildId: string, voiceManager: VoiceManager) {
     // ...
   }
   
   // ... methods ...
   
-  private connectionPollInterval: NodeJS.Timeout | null = null;
-  private connectionAttached = false;
src/types.ts (1)

566-575: Consider adding validation constraints to DiscordBotConfig.

The token field should ideally have a minimum length or format hint in the documentation, as Discord tokens have a specific format. Also, autoJoin could benefit from documenting that these should be voice channel IDs.

src/actions/setVoiceChannelStatus.ts (1)

89-91: Empty catch block silently swallows errors.

When channel fetch by ID fails, the error is silently ignored. Consider logging at debug level for troubleshooting.

🔎 Proposed fix
       } catch (e) {
-        // ID not found, continue to name search
+        // ID not found or access denied, continue to name search
+        // Could log at debug level if a logger were available
       }

Alternatively, pass the runtime logger to this function for better observability.

src/voiceConnectionManager.ts (1)

42-42: Consider using logger.info or logger.debug instead of logger.log.

The logger.log method is less common in structured logging. For connection lifecycle events, logger.info or logger.debug would be more appropriate for filtering and observability.

Also applies to: 51-51

src/service.ts (1)

2196-2379: handleAutoJoinVoiceChannel is well-documented but very long.

The method is ~180 lines with excellent inline documentation explaining the multi-bot auto-join logic. While the comments are valuable, consider extracting helper functions for testability and readability.

The core logic could be split into:

  • parseAutoJoinChannelIds() - Parse and validate channel IDs
  • findAvailableBotForChannel() - Find a bot that can join a specific channel
  • reportAutoJoinResults() - Log the summary

This would make the main method easier to follow while preserving the detailed documentation.

src/voice.ts (6)

252-263: Consider stronger typing for resource and volumeTransformer.

Using any for these fields reduces type safety. Consider importing and using proper types from @discordjs/voice:

Suggested type improvements
+import type { AudioResource, VolumeTransformer } from '@discordjs/voice';
+
 interface ChannelPlayerState {
   player: AudioPlayer;
   channel: number;
   guildId: string;
-  resource: any;
+  resource: AudioResource;
   finished: () => void;
   cancelled: () => void;
   abortController?: AbortController;
   originalVolume?: number;
   duckedVolume?: number;
-  volumeTransformer?: any;
+  volumeTransformer?: VolumeTransformer;
 }

749-754: Minor typo in comment.

Line 749 has a typo: "autopausesdue" should be "autopauses due".

-          // This handles network hiccups where the player autopausesdue to missing connection
+          // This handles network hiccups where the player autopauses due to missing connection

1435-1512: Consider extracting isValidTranscription to a module-level function.

This function is quite comprehensive (80+ lines) and could benefit from being extracted outside of processTranscription for:

  • Better testability
  • Potential reuse by other components
  • Improved readability of the parent method
Suggested refactor
// Move to module level (before VoiceManager class)
function isValidTranscription(text: string): boolean {
  if (!text || text.trim().length < 2) return false;
  // ... rest of implementation
}

1670-1686: Stream type handling can be simplified.

Lines 1670-1679 manually handle stream type conversion, but the comment at line 1676 notes that playAudio() handles Web ReadableStream conversion internally (lines 2087-2111). Consider passing the stream directly:

Simplified approach
               if (responseStream) {
-                let audioStream: Readable;
-                if (Buffer.isBuffer(responseStream)) {
-                  audioStream = Readable.from(responseStream, { objectMode: false });
-                } else if (responseStream instanceof Readable) {
-                  audioStream = responseStream;
-                } else {
-                  // playAudio() handles Web ReadableStream conversion internally
-                  // For other types, try to wrap with Readable.from()
-                  audioStream = responseStream as any;
-                }
+                // playAudio() handles Buffer, Readable, and Web ReadableStream internally
+                const audioStream = Buffer.isBuffer(responseStream)
+                  ? Readable.from(responseStream, { objectMode: false })
+                  : responseStream;
                 // Use mix: true so TTS ducks music instead of stopping it
                 await this.playAudio(audioStream, {

2817-2839: Consider adding error handling for connection destruction during bridge.

If the source connection is destroyed while the bridge is active, the speaking listener may continue firing and cause errors. Consider adding connection state validation:

Suggested improvement
     // Listen for speaking events
     sourceConn.receiver.speaking.on('start', speakingStartHandler);
+
+    // Handle source connection destruction
+    sourceConn.on('stateChange', (_, newState) => {
+      if (newState.status === VoiceConnectionStatus.Destroyed) {
+        bridgeCleanup();
+      }
+    });

1114-1120: Using clearTimeout on setInterval handle.

rampTimer is created with setInterval (line 2563) but cleared with clearTimeout here and at line 1943. While Node.js allows this, it's technically incorrect and could be confusing.

Suggested fix
-      if (duckState.rampTimer) {
-        clearTimeout(duckState.rampTimer as any);
-      }
+      if (duckState.rampTimer) {
+        clearInterval(duckState.rampTimer);
+      }

Apply similarly at line 1943 in stopChannelPlayer.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 64dda7c and 693f58a.

📒 Files selected for processing (31)
  • COORDINATION_PATTERNS.md
  • MUSIC_ARCHITECTURE.md
  • PROGRESSIVE_UPDATES.md
  • README.md
  • VOICE_STATUS_FEATURES.md
  • __tests__/messageManager.test.ts
  • __tests__/multibot-independent.test.ts
  • __tests__/multibot-simulcast.test.ts
  • __tests__/progressiveMessage.test.ts
  • __tests__/token-validation.test.ts
  • __tests__/voiceManager.test.ts
  • src/actions/setListeningActivity.ts
  • src/actions/setVoiceChannelStatus.ts
  • src/attachments.ts
  • src/audioChannels.ts
  • src/clientRegistry.ts
  • src/contracts.ts
  • src/environment.ts
  • src/index.ts
  • src/messages.ts
  • src/progressiveMessage.ts
  • src/providers/audioState.ts
  • src/providers/channelState.ts
  • src/service.ts
  • src/sinks/discordAudioSink.ts
  • src/sinks/index.ts
  • src/tests.ts
  • src/types.ts
  • src/utils.ts
  • src/voice.ts
  • src/voiceConnectionManager.ts
🧰 Additional context used
🧬 Code graph analysis (8)
src/providers/audioState.ts (1)
src/voice.ts (1)
  • getVoiceConnection (825-839)
src/messages.ts (1)
src/utils.ts (3)
  • editMessageContent (768-793)
  • getAttachmentFileName (187-233)
  • sendMessageInChunks (309-486)
src/contracts.ts (1)
src/sinks/discordAudioSink.ts (1)
  • status (60-62)
src/progressiveMessage.ts (1)
src/service.ts (1)
  • delay (2831-2833)
__tests__/messageManager.test.ts (2)
src/types.ts (1)
  • IDiscordService (478-492)
src/messages.ts (1)
  • MessageManager (43-820)
src/index.ts (3)
src/service.ts (2)
  • setVoiceChannelStatus (3799-3848)
  • setListeningActivity (3858-3884)
src/providers/audioState.ts (1)
  • audioStateProvider (16-164)
src/permissions.ts (1)
  • getPermissionValues (238-247)
src/clientRegistry.ts (2)
src/voice.ts (1)
  • VoiceManager (318-2910)
src/index.ts (1)
  • DiscordEventTypes (175-175)
src/voice.ts (4)
src/index.ts (4)
  • AudioChannelConfig (30-30)
  • PlaybackHandle (30-30)
  • DEFAULT_CHANNEL_CONFIGS (36-36)
  • DiscordEventTypes (175-175)
src/compat.ts (1)
  • ICompatRuntime (53-58)
src/environment.ts (1)
  • getDiscordSettings (93-194)
src/sinks/discordAudioSink.ts (1)
  • status (60-62)
🪛 LanguageTool
COORDINATION_PATTERNS.md

[style] ~95-~95: You have already used this phrasing in nearby sentences. Consider replacing it to add variety to your writing.
Context: ... Service Discovery ### Problem Plugins need to connect to each other at runtime. ### ...

(REP_NEED_TO_VB)

PROGRESSIVE_UPDATES.md

[grammar] ~102-~102: Ensure spelling is correct
Context: ...s don't expire. ### 3. Why Debouncing (300ms minDelay)? Problem: If an operatio...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~106-~106: Ensure spelling is correct
Context: ...Solution*: The first update() waits 300ms before sending. If complete() is call...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~114-~114: Ensure spelling is correct
Context: ...s like waiting. ### 4. Why Throttling (500ms between updates)? Problem: Discord...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~118-~118: Ensure spelling is correct
Context: ...ger rate limits. Solution: Enforce 500ms minimum between updates. If update() ...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~120-~120: Ensure spelling is correct
Context: ...ebounce and send the last value. Why 500ms?: Allows ~2 edits/second (10 per 5 sec...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~164-~164: Ensure spelling is correct
Context: ...er API (no flag to forget). ### 7. Why isInterim Flag Instead of isFinal? Reason: D...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🪛 markdownlint-cli2 (0.18.1)
PROGRESSIVE_UPDATES.md

10-10: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


21-21: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


125-125: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


143-143: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Upload results
  • GitHub Check: Cursor Bugbot
🔇 Additional comments (52)
src/utils.ts (2)

435-435: Good type narrowing improvement.

The explicit type guard (c): c is ButtonBuilder | StringSelectMenuBuilder => c !== null properly narrows the array type, eliminating the need for downstream type assertions.


750-793: Well-designed utility with appropriate error handling.

The editMessageContent function follows good practices:

  • Returns null on failure for graceful degradation
  • Validates empty content upfront
  • Truncates to Discord's limit with ellipsis indicator
  • Comprehensive JSDoc explaining design decisions

Minor note: The truncation math is correct (1997 + 3 = 2000 chars max).

PROGRESSIVE_UPDATES.md (1)

1-50: Excellent documentation with clear rationale.

The progressive updates documentation thoroughly explains the architecture, design decisions (correlation IDs, TTL, debouncing, throttling), and provides actionable usage guidance. The "why" explanations are particularly valuable for future maintainers.

README.md (1)

567-672: Comprehensive audio system documentation.

The Multi-Channel Audio System and Audio Sink Integration sections provide clear explanations of:

  • Predefined channels and priority-based behavior
  • Custom channel registration
  • IAudioSink interface usage
  • Auto-reconnection and plugin integration

This will help developers integrate with the audio system effectively.

src/audioChannels.ts (2)

1-94: Excellent documentation with clear design rationale.

The inline documentation thoroughly explains the "why" behind each channel's priority, pausability, and interruptibility settings. This will be valuable for maintainers and developers extending the audio system.


165-178: Correct priority-based interrupt logic.

The canInterrupt function correctly implements the priority system:

  • Returns false if configs are missing (safe default)
  • Requires both higher priority AND existing channel to be interruptible
  • CHANNEL_TTS (interruptible: false) cannot be interrupted as intended
src/clientRegistry.ts (4)

24-59: Good token validation with helpful error messages.

The validateDiscordToken function provides thorough format validation with actionable error messages. The checks for empty strings, literal "undefined"/"null", length, and dot-separated format are practical heuristics that catch common misconfiguration issues.


340-350: Verify iteration safety in destroyAll.

The code uses Array.from(this.clients.keys()) to create a snapshot before iterating, which correctly avoids modifying the map during iteration. This is safe.

One consideration: removeBot may delete both the botId and alias keys (lines 318-321), so if multiple keys point to the same client, subsequent deletions may warn about "not found." This is harmless but could be cleaner.


202-263: Login timeout and error handling look solid.

The 30-second timeout is appropriate for Discord login. The error handler on the client properly logs errors, and the Promise correctly rejects on login failure with cleanup.


424-433: Smart optimization for large guilds.

Skipping user pre-fetch for guilds with >1000 members and discovering users organically is a good performance optimization. The background sync for smaller guilds is appropriately non-blocking.

src/sinks/index.ts (1)

1-2: Clean barrel export.

Standard re-export pattern that exposes DiscordAudioSink from the sinks module.

src/providers/channelState.ts (1)

5-5: LGTM!

The import change from ServiceType to DISCORD_SERVICE_NAME aligns with the PR's goal of standardizing service discovery using public constants.

src/tests.ts (2)

13-13: LGTM!

Consistent import of DISCORD_SERVICE_NAME constant for standardized service discovery.


76-76: LGTM!

Service lookup using DISCORD_SERVICE_NAME is consistent with the PR's standardization approach and matches the pattern established across other files.

COORDINATION_PATTERNS.md (1)

1-9: Good documentation structure.

The document provides a clear overview of the broadcast architecture with well-organized sections. The status and last-updated metadata help track currency.

__tests__/voiceManager.test.ts (1)

16-32: LGTM!

Comprehensive mocking of @discordjs/voice module with all necessary exports for testing the audio pipeline.

__tests__/multibot-simulcast.test.ts (1)

204-232: Mock target's play() resolves immediately but has async side effects.

The play() method resolves immediately (line 230) while stream handlers continue to run asynchronously. This design choice is intentional (as per the comment on lines 228-229) but could lead to test flakiness if assertions run before stream handlers execute.

The tests mitigate this with setTimeout delays, but consider documenting this behavior more explicitly or using a more deterministic synchronization mechanism.

__tests__/progressiveMessage.test.ts (2)

1-23: LGTM!

Clean test setup with a well-structured mock callback that captures all invocations for assertion.


113-129: LGTM!

Good test coverage for source field validation, ensuring both Discord and non-Discord sources are handled correctly.

VOICE_STATUS_FEATURES.md (1)

1-9: LGTM!

Clear introduction documenting the new capabilities for voice channel status and listening activity.

__tests__/multibot-independent.test.ts (2)

1-12: LGTM! Clean test setup with appropriate imports.

The imports are well-organized, bringing in test utilities from Vitest, stream utilities from Node, and the necessary types/classes for testing multi-bot routing.


295-308: Well-designed test stream helper with proper synchronization.

Using process.nextTick to ensure listeners are set up before emitting data is the correct pattern for test streams.

src/providers/audioState.ts (1)

139-163: Well-structured provider response with comprehensive audio state.

The response structure is consistent across all return paths, with proper data/values/text separation. The issue detection logic is clear and the text summary is user-friendly.

src/contracts.ts (1)

1-69: Well-designed audio sink contract with excellent documentation.

The interface design properly separates concerns between Discord internals and the music-player consumer. The typed event methods and clear lifecycle documentation make this easy to implement correctly. The "WHY" comments explaining design decisions are particularly valuable for future maintainers.

__tests__/messageManager.test.ts (2)

59-80: Good refactoring to dependency injection pattern.

The mockDiscordService properly implements the IDiscordService interface with getChannelType and buildMemoryFromMessage methods. This aligns with the constructor signature change and improves testability.


338-359: Test correctly validates attachment processing through processMessage.

The test now directly calls processMessage and verifies the attachment is included in the result, which is a more focused unit test approach.

src/messages.ts (4)

50-53: Good design for progressive message tracking with proper TypeScript typing.

The Map structure with message reference and timeout handle is appropriate for managing progressive update state.


78-139: Excellent progressive message lifecycle management with thorough documentation.

The three helper methods (trackProgressiveMessage, resetProgressiveTTL, cleanupProgressiveMessage) properly handle the lifecycle with 60-second TTL for leak prevention. The detailed "WHY" comments explaining the design decisions are invaluable for maintainability.


195-196: Good improvement: prefer server-specific displayName (nickname).

Using message.member?.displayName as the primary source with fallback to message.author.displayName correctly prioritizes server nicknames.


337-410: Well-implemented progressive update flow with proper fallback handling.

The logic correctly handles:

  • Editing existing tracked messages
  • TTL refresh on successful edits
  • Skipping memory creation for interim updates
  • Graceful fallback when edits fail

The detailed inline comments explaining each decision point are excellent.

src/sinks/discordAudioSink.ts (1)

217-233: Good cleanup in destroy() with proper resource handling.

The destroy method properly clears the polling interval, stops playback, and removes listeners. Adding the connection listener cleanup (as noted above) would complete this.

MUSIC_ARCHITECTURE.md (3)

63-78: Documentation shows methods not present in the actual interface.

The documented IAudioSink interface includes connect(channelId) and disconnect() methods, but the implementation in src/contracts.ts doesn't include these. This creates confusion about the actual API contract.

🔎 Either update docs or add methods to interface

If connect/disconnect are not part of the current implementation:

 interface IAudioSink extends EventEmitter {
   readonly id: string;
   readonly status: AudioSinkStatus; // 'connected' | 'disconnected' | 'connecting' | 'error'
   
   feed(stream: Readable): Promise<void>;
   stop(): Promise<void>;
-  
-  connect(channelId: string): Promise<void>;
-  disconnect(): Promise<void>;
+  
+  getDescription(): string;
 }

1-39: Excellent architecture documentation with clear diagrams.

The ASCII diagram effectively illustrates the broadcast-centric model and the separation between plugin-music-player and plugin-discord. The explanatory comments make the design decisions accessible.


169-204: Good documentation of multi-channel audio system and priority behavior.

The channel priority constants and DJ integration example provide clear guidance for implementers. The related documentation links help with discoverability.

src/progressiveMessage.ts (4)

1-65: Exceptional documentation explaining the "why" behind design decisions.

The extensive JSDoc comments explaining debouncing, throttling, and platform-specific behavior make this code highly maintainable. The usage example is particularly helpful.


152-161: Fire-and-forget pattern for important updates on non-progressive platforms.

The callback is called without awaiting (line 153-158), which means errors are only logged, not propagated. This is intentional for interim updates, but for "important" updates, you might want to track failures differently.

Is fire-and-forget the intended behavior for important updates? If an important update fails silently, users on non-Discord platforms might not get feedback about long operations.


140-193: Well-implemented debounce and throttle logic.

The update method correctly handles:

  • First update waits for minDelay before sending
  • Subsequent updates are throttled to prevent rate limiting
  • Pending updates are properly managed with timer cleanup

This ensures responsive UX while respecting Discord's rate limits.


237-253: Smart completion logic handles fast operations gracefully.

The check on line 247 correctly bypasses progressive mode if the operation completed before any updates were sent and was faster than minDelay. This prevents unnecessary message edits for instant operations.

src/types.ts (1)

534-561: Well-designed VoiceTarget interface for multi-bot routing.

The VoiceTarget interface provides a clean abstraction for voice connection targets with appropriate methods for playback control. The composite ID format bot-uuid:guild-id:channel-id enables unique identification across multi-bot scenarios.

src/voiceConnectionManager.ts (1)

65-97: Overall clean implementation of multi-bot voice target management.

The getVoiceTargets() method elegantly creates VoiceTarget proxies that delegate to the underlying VoiceManager. The composite key pattern (botId:guildId:channelId) ensures unique identification across the multi-bot topology.

src/index.ts (1)

29-51: Clean export organization for the new multi-bot infrastructure.

The exports are well-organized with clear comments grouping related types and utilities. This makes it easy for external consumers to understand and use the new audio channel, progressive message, and multi-bot voice capabilities.

src/service.ts (2)

1084-1149: Well-implemented voice state update handler for agent tracking.

The voiceStateUpdate handler properly tracks agent connect/disconnect/move events and updates audio state. The conditional logic clearly handles all state transition cases (join, leave, move, mute/deafen changes).


3790-3848: Clean implementation of setVoiceChannelStatus with proper validation.

The method includes all necessary checks (client ready, channel exists, voice channel type), truncates status to Discord's 500-char limit with a warning, and uses the correct REST endpoint. Good error handling throughout.

src/actions/setVoiceChannelStatus.ts (1)

99-106: Loose channel name matching could match unintended channels.

The regex pattern [^a-z0-9 ] strips all special characters when matching channel names. This causes "voice-1" and "voice_1" to both normalize to "voice1", creating ambiguous matches. When the exact match fails and the fuzzy match is applied, .find() returns the first matching channel, potentially returning an unintended one if multiple channels normalize to the same pattern. Consider prioritizing exact matches or implementing stricter matching logic with disambiguation.

Also applies to: 118-125

src/voice.ts (8)

1-54: Imports look appropriate for the new functionality.

The new imports (demuxProbe, getDiscordSettings, DEFAULT_CHANNEL_CONFIGS, DiscordEventTypes) align with the channel-based audio architecture being introduced.


446-453: Timer lifecycle management looks correct.

The voiceStatFlushTimer and connectionWatchdog intervals are properly cleared in the cleanup() method (lines 553-562). Good practice for resource management.


489-542: Connection health monitoring implementation is solid.

The watchdog properly:

  • Skips connections with pending reconnects
  • Uses appropriate timeout (15s) for stuck connections
  • Handles both recovery and cleanup paths
  • Cleans up orphaned health records

854-864: Good duplicate monitoring prevention.

The monitoredUsers Set effectively prevents duplicate audio monitors for the same user, which was likely causing resource leaks or duplicate processing in the previous implementation.


2224-2244: Promise resolver pattern is correctly implemented.

The external resolver pattern used here is safe because the Promise constructor callback executes synchronously, guaranteeing finishedResolver and cancelledResolver are assigned before any async code that uses them.


2562-2586: Volume ramp implementation is well-designed.

The gradual volume restoration with configurable ramp duration provides a smooth user experience. The use of setInterval with step calculations is appropriate.

Minor: Line 2585 casts to any due to type mismatch. Consider using ReturnType<typeof setInterval> for consistency with other timer declarations.


1962-1971: Defensive check for malformed keys is reasonable.

The check at lines 1965-1967 for keys containing ':' provides safety against accidental misuse of the connections Map, even though the current implementation should only use guildId keys.


1823-1843: Legacy method uses wrong key type for connection lookup.

playAudioStream looks up connections using entityId, but the connections Map is keyed by guildId. This will cause the method to never find a connection.

Consider:

  1. Deprecate and remove: If this method is unused, remove it entirely.
  2. Fix the lookup: If still needed, change the parameter from entityId to guildId:
-  async playAudioStream(entityId: UUID, audioStream: Readable) {
-    const connection = this.connections.get(entityId);
+  async playAudioStream(guildId: string, audioStream: Readable) {
+    const connection = this.connections.get(guildId);

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/actions/reactToMessage.ts (1)

224-308: Excellent fix for the forbidden emoji fallback issue! 🎉

The previous concern about forbidden emojis being used when all alternatives are filtered has been comprehensively addressed. The new fallback cascade is robust:

  1. Lines 273-280: Tries neutral emojis filtered for forbidden items
  2. Lines 282-292: Scans ALL emoji categories to find any non-forbidden option
  3. Lines 294-299: Only falls back to neutral[0] as absolute last resort with a clear warning

This ensures the character's forbidden list is respected in almost all cases, with clear logging when it cannot be.

Optional: Extract common neutral filtering logic

The neutral emoji filtering logic appears in multiple places (lines 273, 441). Consider extracting to a helper:

function getNeutralAllowedEmojis(prefs: { forbidden: string[] }): string[] {
  return sentimentEmojis.neutral.filter((e) => !prefs.forbidden.includes(e));
}

This would reduce duplication and make the intent clearer.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 058453c and 87e358f.

📒 Files selected for processing (2)
  • __tests__/voiceManager.test.ts
  • src/actions/reactToMessage.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/voiceManager.test.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cursor Bugbot
🔇 Additional comments (5)
src/actions/reactToMessage.ts (5)

127-140: LGTM! Well-structured sentiment emoji mapping.

The sentiment categories are well-chosen and provide good coverage for common Discord interactions. The neutral fallback options are appropriate.


146-162: Simple but effective sentiment detection.

The keyword-based heuristic provides a good balance between simplicity and effectiveness for emoji selection. The neutral default ensures graceful handling of unmatched patterns.


168-218: LGTM! Robust preference handling.

The dual-format support for emoji preferences (simple array vs. categorized object) provides good flexibility for character configuration. The defensive type checking ensures graceful handling of missing or malformed settings.


427-453: Previous LLM path forbidden emoji issue is now fixed! ✅

The concern about the LLM path falling back to a forbidden emoji when no alternatives exist has been properly addressed. The current implementation:

  • Validates the LLM-selected emoji against the forbidden list (line 431)
  • Attempts to find allowed alternatives from sentiment-matched emojis (lines 435-438)
  • Falls back to neutral alternatives (lines 441-443)
  • Crucially, sets emoji = null when no safe alternative exists (line 450), which prevents the reaction entirely rather than using a forbidden emoji

This is the correct behavior and resolves the previous review concern.


392-409: LGTM! Character-aware emoji selection well integrated.

The character path integration properly falls back to character-based emoji selection when the fast path fails. The additional check on line 401 for characterForbidsEmojis is technically redundant (already checked in selectCharacterEmoji at line 229) but provides clearer logging context, which is acceptable.

- Add filterAttachmentsForMemory() to strip data URLs from stored memories
- Add createAttachmentFromMedia() to handle base64→Buffer conversion for Discord
- Add scrubBase64ImagesFromMemories() to clean existing memories on startup
- Update all memory creation points to filter attachments

This prevents the RECENT_MESSAGES provider from loading huge base64 images
into LLM context, which was causing 200k token limit errors.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

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 (5)
src/service.ts (2)

401-519: Align send-path memories with new base64/data-URL filtering

handleSendMessage correctly uses createAttachmentFromMedia for Discord uploads, but when persisting memories you still copy content.attachments verbatim. That means base64/data-URL images sent by the agent will continue to land in the messages table, bypassing filterAttachmentsForMemory and relying solely on the startup scrubber.

Consider applying filterAttachmentsForMemory(content.attachments) here too so new sent-message memories never store raw data URLs in the first place.

Suggested change (memory creation block)
-                  ...(hasAttachments && content.attachments
-                    ? { attachments: content.attachments }
-                    : {}),
+                  ...(hasAttachments && content.attachments
+                    ? { attachments: filterAttachmentsForMemory(content.attachments) }
+                    : {}),

4693-4757: Use ActivityType.Listening enum instead of magic number

In setListeningActivity, the code uses the hard-coded type: 2:

await this.client.user.setActivity(activity, {
  type: 2, // ActivityType.Listening
  url,
});

Import and use ActivityType.Listening from discord.js (v14.18.0) instead. This is the recommended approach and avoids silent breakage if the underlying enum changes.

-import { … } from 'discord.js';
+import { …, ActivityType } from 'discord.js';

-      await this.client.user.setActivity(activity, {
-        type: 2, // ActivityType.Listening
-        url: url,
-      });
+      await this.client.user.setActivity(activity, {
+        type: ActivityType.Listening,
+        url,
+      });
src/utils.ts (1)

190-241: Data‑URL parsing only supports simple ;base64, form

parseDataUrl currently matches only data:<mime>;base64,<data>:

const match = dataUrl.match(/^data:([^;]+);base64,(.+)$/);

This will return null for valid data URLs that include additional parameters (e.g. data:image/png;charset=utf-8;base64,...).

If you expect those forms from upstream tools, consider relaxing the regex to allow extra ;... segments before ;base64, or delegating to a small data-URL parsing helper.

src/messages.ts (1)

390-557: Avoid duplicating attachments on each chunk in progressive final send path

In the progressive branch where there is no existing tracked message (or edit failed), you send new messages (possibly chunked) and then create memories:

for (const m of messages) {
  const memory: Memory = {
    
    content: {
      ...content,
      attachments: filterAttachmentsForMemory(content.attachments),},};
}

Unlike the non-progressive path, this does not check whether each m actually carries attachments; all chunks get the same attachments array, so a multi-chunk progressive response will duplicate attachments across all associated memories.

To keep behavior consistent with the normal path (which uses hasAttachments), consider only attaching filtered attachments to the message(s) where m.attachments.size > 0.

Suggested change
-            const memories: Memory[] = [];
-            for (const m of messages) {
-              const memory: Memory = {
+            const memories: Memory[] = [];
+            for (const m of messages) {
+              const hasAttachments = m.attachments?.size > 0;
+              const memory: Memory = {
                 …
                 content: {
                   ...content,
-                  // Filter out base64 attachments to prevent context bloat
-                  attachments: filterAttachmentsForMemory(content.attachments),
+                  // Only include attachments for the chunk that actually has them
+                  attachments:
+                    hasAttachments && content.attachments
+                      ? filterAttachmentsForMemory(content.attachments)
+                      : undefined,
                   …
                 },
                 …
               };
src/index.ts (1)

54-132: Reuse shared isDataUrl/filtering helpers to avoid divergence

src/index.ts defines its own isDataUrl and manual filtering logic in scrubBase64ImagesFromMemories, even though src/utils.ts now exports isDataUrl and filterAttachmentsForMemory.

To keep behavior consistent and avoid future drift between the startup scrubber and per-message filtering, consider:

  • Importing isDataUrl (and possibly filterAttachmentsForMemory) from ./utils instead of re-implementing them here.
  • Or, at minimum, delegating the attachment filtering in the scrubber to filterAttachmentsForMemory so both paths use the same placeholder and criteria.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 87e358f and 3396087.

📒 Files selected for processing (4)
  • src/index.ts
  • src/messages.ts
  • src/service.ts
  • src/utils.ts
🧰 Additional context used
🧬 Code graph analysis (2)
src/index.ts (2)
src/utils.ts (1)
  • isDataUrl (186-188)
src/permissions.ts (1)
  • getPermissionValues (238-247)
src/messages.ts (1)
src/utils.ts (4)
  • editMessageContent (899-924)
  • filterAttachmentsForMemory (342-366)
  • createAttachmentFromMedia (313-332)
  • sendMessageInChunks (442-617)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cursor Bugbot
🔇 Additional comments (7)
src/utils.ts (2)

306-365: Attachment creation + memory filtering behavior looks good

The combination of:

  • createAttachmentFromMedia decoding data URLs into AttachmentBuilder buffers, and
  • filterAttachmentsForMemory dropping those data URLs (with a placeholder summary),

gives a nice separation between what’s sent to Discord and what’s persisted into memories.

No functional issues spotted; just noting that this aligns well with the new startup scrubber.


882-923: 2000 character limit is correct for Discord message edits

The 2000 character limit is confirmed as the correct hard limit for regular Discord accounts. The implementation sensibly truncates rather than splits, since message edits can only update one message.

Note: The codebase uses MAX_MESSAGE_LENGTH = 1900 elsewhere for new messages (a conservative buffer), while editMessageContent hardcodes 2000 to match Discord's actual API limit. Both approaches are valid for their use cases. If targeting Nitro accounts with their 4000 character limit, the function would need enhancement.

src/messages.ts (2)

50-143: Progressive message tracking and TTL behavior

The progressiveMessages map with trackProgressiveMessage, resetProgressiveTTL, and cleanupProgressiveMessage gives a clear, bounded way to support correlation‑ID based progressive updates without leaking entries indefinitely. The 60s TTL and reset-on-update logic make sense for long-running actions.

No functional issue here; just calling out that this is a solid structure, and cleanup is correctly invoked on final updates and edit failures.


575-645: Non‑progressive attachment handling and memory filtering look consistent

In the normal (non-progressive) path you:

  • Convert content.attachments via createAttachmentFromMedia before sending (DM or channel), and
  • Only attach filterAttachmentsForMemory(content.attachments) to memories where m.attachments.size > 0.

That both preserves outgoing behavior and keeps memory rows free of base64 data URLs. This looks correct and consistent with the new utils and scrubber behavior.

src/index.ts (1)

161-252: Token validation and startup scrubber behavior

The updated init now:

  • Treats only DISCORD_API_TOKEN and DISCORD_BOT_TOKENS as valid bot-token sources, and
  • Logs a clear banner when neither is set, explicitly clarifying that DISCORD_APPLICATION_ID is not a token.
  • Runs scrubBase64ImagesFromMemories once on startup to clean the last ~1000 memories.

This addresses earlier confusion around DISCORD_APPLICATION_ID and keeps base64 cleanup non-fatal. No issues spotted; just a note that the additional startup I/O is bounded and should be fine.

src/service.ts (2)

2513-2650: The time option is supported in discord.js v14 and is a documented parameter for GuildMemberManager.fetch()—it sets the max wait time in milliseconds for gateway responses (default ~60000). Your concern about it being unsupported can be disregarded.

That said, in the large-guild path (lines ~2540–2548), you do have redundant timeout layers: the time: 5000 option in fetch() combined with the Promise.race wrapper using a 6000ms timeout. The discord.js timeout will trigger first. Consider removing the time option there and relying solely on the Promise.race wrapper for clarity. In the small-guild path (line ~2625), the time: 10000 option is useful and has no competing timeout.

Likely an incorrect or invalid review comment.


4634-4691: The REST endpoint and payload are correct; the GuildVoice restriction is appropriate

The /channels/{id}/voice-status endpoint with payload { status: string | null } is correct. The discord.js REST client defaults to API version v10, so the call is properly formatted. However, note that this endpoint is undocumented in the official Discord API documentation and remains subject to change—consider adding a comment noting this limitation.

Regarding Stage channels: the voice-status endpoint is not supported for GuildStageVoice channels, only GuildVoice, so the current type restriction is correct and appropriate.

Prevents runtime throws for text-only clients or failed voiceManager
construction by checking voiceManager exists before calling
getVoiceConnection() or joinChannel(). Logs debug message when
skipping clients without voiceManager.
…stant

- Skip logging voice connection state changes when status hasn't actually changed
- Skip logging audio player state changes when status hasn't actually changed
- Add DISCORD_SERVICE_NAME constant for service identification
- Minor import cleanup in types.ts
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

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/service.ts (1)

2557-2627: Optional: Simplify redundant timeout logic.

The online member fetching has two timeout mechanisms: guild.members.fetch({ time: 5000 }) and Promise.race with a 6-second timeout. The inner timeout (5s) will always fire first, making the outer timeout (6s) redundant.

Consider simplifying:

-            const fetchPromise = guild.members.fetch({ limit: 100, time: 5000 });
-            const timeoutPromise = new Promise((_, reject) =>
-              setTimeout(() => reject(new Error('Member fetch timeout')), 6000)
-            );
-
-            const onlineMembers = (await Promise.race([fetchPromise, timeoutPromise])) as any;
+            const onlineMembers = await guild.members.fetch({ limit: 100, time: 5000 });

The fetch's built-in timeout is sufficient and clearer.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 3396087 and 9c5f613.

📒 Files selected for processing (3)
  • src/service.ts
  • src/types.ts
  • src/voice.ts
🧰 Additional context used
🧬 Code graph analysis (1)
src/types.ts (1)
src/index.ts (2)
  • VoiceTarget (46-46)
  • DiscordBotConfig (46-46)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cursor Bugbot
🔇 Additional comments (17)
src/types.ts (5)

11-11: LGTM: Import additions support new voice/stream functionality.

The new imports enable:

  • Service extension for IDiscordService (line 502)
  • Readable type for VoiceTarget.play() signature (line 580)
  • VOICE_TRANSCRIPTION event type for voice transcription events (line 64)

Also applies to: 24-24, 64-64


502-516: LGTM: Service extension enables proper integration with ElizaOS core.

Extending Service and adding getChannelType and buildMemoryFromMessage methods provides a cleaner API surface for Discord service implementations.


527-554: LGTM: Comprehensive voice settings with sensible defaults.

The DiscordSettings interface provides fine-grained control over voice behavior:

  • Voice ducking configuration for dynamic volume adjustment
  • Listen-only mode for transcription without TTS
  • Channel whitelisting and message filtering

All settings are optional with defaults documented, enabling zero-config usage.


560-601: LGTM: Multi-bot voice architecture types are well-designed.

VoiceTarget provides a clean abstraction for audio playback:

  • Composite ID format enables per-guild targeting across multiple bots
  • play(stream: Readable) signature aligns with Node.js streams
  • getStatus() return type uses string literals for type safety

DiscordBotConfig supports multi-bot orchestration with optional auto-join.


670-706: LGTM: Discord component types enable rich interactions.

The component types (DiscordSelectOption, DiscordComponentOptions, DiscordActionRow) mirror Discord's API structure for buttons, select menus, and other interactive components.

Numeric type codes match Discord's API conventions.

src/voice.ts (8)

260-346: LGTM: Audio channel architecture is well-documented.

The new interfaces (AudioChannelConfig, PlaybackHandle, ChannelPlayerState) support priority-based audio with ducking and interruption control.

The extensive class documentation clearly explains:

  • Audio playback pipeline
  • Critical stream handling requirements (no listeners before demuxProbe)
  • Channel-based architecture

430-486: LGTM: Constructor properly initializes audio system.

Key initialization:

  • Default audio channels registered immediately (ensures availability)
  • Ducking config loaded from settings with proper boolean parsing
  • Stats flushing (30s) and connection watchdog (5s) timers started

Timers are properly cleaned up in cleanup() method (lines 580-616).


580-616: LGTM: Cleanup method comprehensively handles all resources.

All previously flagged gaps from past review have been addressed:

  • Reconnect timeouts cleared (lines 596-600)
  • Ducking timers cleared (lines 602-607)
  • Active bridges cleaned up (lines 609-613)

The method now properly cleans up stats flush timer, connection watchdog, reconnect timeouts, ducking state, and audio bridges.

✅ Past review issue resolved.


1009-1069: LGTM: Race condition properly fixed with immediate tracking.

The race condition flagged in past review has been resolved:

  • User added to monitoredUsers immediately after check (line 1017)
  • Early exit paths clean up tracking (lines 1022-1024, 1039-1040, 1064-1066)
  • Pipeline callback ensures cleanup on completion (lines 1145-1159)

This prevents duplicate monitors when monitorMember is called concurrently for the same user.

✅ Past review issue resolved.


1816-1822: LGTM: Listen-only mode correctly uses parsed settings.

The truthy string bug flagged in past review has been resolved:

  • Uses getDiscordSettings() for proper boolean parsing (line 1819)
  • Checks settings.voiceListenOnly instead of raw setting string
  • Transcription event always emitted (lines 1796-1814)
  • Response generation skipped only when truly in listen-only mode

✅ Past review issue resolved.


2428-2462: LGTM: Web ReadableStream conversion uses streaming approach.

The memory buffering issue flagged in past review has been resolved:

  • Primary path uses Readable.fromWeb() for streaming conversion (lines 2438-2440)
  • No buffering into memory for modern runtimes (Node.js 18+, Bun)
  • Fallback to buffering only for older runtimes with warning (lines 2442-2460)
  • Proper cleanup with reader.releaseLock() in finally block

This avoids OOM for large audio files while maintaining compatibility.

✅ Past review issue resolved.


2364-2653: LGTM: Audio playback pipeline properly implements format detection.

The playAudio() method correctly:

  • Uses demuxProbe for format detection (lines 2483-2508)
  • Handles Web ReadableStream conversion (lines 2428-2462)
  • Validates stream before and after probe
  • Creates AudioResource with detected format (lines 2521-2524)
  • Handles priority-based interruption and ducking (line 2404)
  • Monitors resource stream for debugging (lines 2532-2549)
  • Properly cleans up on completion/error (lines 2614-2638)

The extensive documentation clearly explains stream handling requirements.


3105-3286: LGTM: Audio bridging provides clean telephone booth POC.

The bridging implementation correctly:

  • Captures audio from source guild's receiver (line 3162-3165)
  • Creates AudioResource from Opus stream (lines 3169-3171)
  • Plays to target guild via dedicated AudioPlayer (line 3174)
  • Tracks per-user streams to prevent duplicates (lines 3138-3150)
  • Provides proper cleanup for user streams and bridges (lines 3198-3216)
  • Supports bidirectional bridging (lines 3236-3265)
  • Cleanup functions are called in cleanup() method (lines 609-613)
src/service.ts (4)

199-201: Token validation correctly fixed.

The token fallback chain now only includes valid bot token settings (DISCORD_API_TOKEN and DISCORD_BOT_TOKENS), with DISCORD_APPLICATION_ID properly removed as flagged in previous reviews.


2793-2797: LGTM! Null guards added as requested in previous review.

The auto-join logic now properly guards against missing voiceManager in both the pre-population loop and the join loop, preventing runtime errors for text-only clients or failed voiceManager construction.

Also applies to: 2838-2842


3385-3395: LGTM! Reaction removal properly delegates to generic handler.

The implementation now correctly uses handleReaction(reaction, user, 'remove') instead of duplicating logic, addressing the previous review concern about code duplication and deprecated property usage.


4746-4769: LGTM! Activity cleared using proper Discord API.

The implementation correctly uses setPresence({ activities: [] }) instead of the problematic setActivity(null as any) that was flagged in previous reviews. This prevents invalid Gateway packets.

…nager for multi-bot

- Track auto-join setTimeout for proper cleanup in stop()
- Use voiceConnectionManager to find correct voice manager per guild
- Fixes audio sink creation in multi-bot deployments where primary bot may not be in target guild
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/service.ts (1)

2772-2970: Optional: Remove auto-join timeout from tracking array after execution for consistency.

The auto-join timeout (line 2772) is added to this.timeouts (line 2970) but never removed after execution. This differs from the button interaction timeouts (lines 2291-2294, 2340-2343) which clean up after themselves. While this doesn't cause functional issues (the timeout ID is harmless after firing and gets cleared on service stop), removing it after execution would be more consistent with the rest of the codebase and prevent accumulating stale timeout IDs.

🔎 Proposed fix
     const autoJoinTimeout = setTimeout(async () => {
       try {
         // ... auto-join logic ...
       } catch (error) {
         this.runtime.logger.error(`Error in auto-join process: ${error instanceof Error ? error.message : String(error)}`);
       }
+      // Remove from tracking array after execution
+      const index = this.timeouts.indexOf(autoJoinTimeout);
+      if (index > -1) {
+        this.timeouts.splice(index, 1);
+      }
     }, 5000);
     // Track timeout for cleanup on service stop
     this.timeouts.push(autoJoinTimeout);
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 9c5f613 and d1c15fb.

📒 Files selected for processing (1)
  • src/service.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cursor Bugbot
🔇 Additional comments (6)
src/service.ts (6)

167-282: LGTM! Multi-bot initialization is well-structured.

The multi-bot infrastructure initialization is comprehensive and robust:

  • Proper promise coordination via clientReadyPromise ensures all consumers wait for initialization
  • Error handling provides helpful context for token validation failures with actionable guidance
  • Cleanup of partial state on errors prevents resource leaks
  • Token validation correctly removes DISCORD_APPLICATION_ID from fallback chain (addressing past review comment)
  • Backward compatibility maintained by setting this.client and this.voiceManager to primary client

The error handling flow ensures readyResolver() is called in all paths (success, error, no token), preventing promise deadlocks.


297-327: LGTM! Audio sink selection correctly handles multi-bot deployments.

The enhanced getAudioSink logic now properly addresses the multi-bot scenario flagged in past reviews:

  1. First checks voiceConnectionManager for guilds where a bot is already connected
  2. Uses the voice manager from the active bot in that guild
  3. Falls back to primary voice manager if no connections exist
  4. Returns null with clear logging if no voice manager is available

This ensures audio sinks work correctly when the primary bot is not in the target guild.


2752-2971: LGTM! Auto-join implementation comprehensively handles multi-bot scenarios.

The rewritten auto-join logic addresses all concerns from past reviews and adds robust multi-bot support:

Past issues resolved:

  • Timeout is now tracked in this.timeouts array for proper cleanup
  • Pre-existing connections are checked before attempting joins
  • Guards against missing voiceManager prevent runtime errors

Multi-bot logic:

  • Parses comma-separated channel IDs for flexibility
  • Pre-populates activeConnections to prevent duplicate joins
  • Iterates channels first (outer loop) for balanced distribution
  • Comprehensive result tracking and summary logging

Error handling:

  • Try-catch around each join attempt
  • Non-fatal errors logged with context
  • Summary shows success/failure breakdown per channel

The 5-second delay provides adequate time for guild cache population across multiple bots.


1344-1405: LGTM! Voice state tracking properly handles agent state changes.

The new voiceStateUpdate handler provides comprehensive tracking:

  • Delegates non-agent voice state updates to voiceManager
  • Safely checks both oldState.member?.id and newState.member?.id to handle partial states
  • Covers all transition scenarios: connect, disconnect, move, mute/deaf changes
  • Updates audio state and delegates appropriate handler calls to voiceManager
  • Clear logging for each transition type

4668-4783: LGTM! Voice status methods correctly implemented.

All three new public methods are well-implemented:

setVoiceChannelStatus (lines 4668-4717):

  • Validates channel type before setting status
  • Truncates to Discord's 500-character limit with warning
  • Uses REST API directly (correct approach - discord.js has no helper for this endpoint)

setListeningActivity (lines 4727-4753):

  • Uses correct activity type (2 = Listening)
  • Handles optional URL parameter

clearActivity (lines 4760-4783):

  • Now uses setPresence({ activities: [] }) instead of null as any (addresses past review comment)
  • Correctly clears presence without invalid Gateway packets

All methods have proper error handling and logging.


2568-2641: LGTM! Member fetching timeout prevents hangs on large guilds.

The new conditional fetch with timeout protection (lines 2568-2641) is a smart optimization:

  • Only triggers when cache has fewer than 100 members for large guilds
  • Uses Promise.race with 6-second timeout as safety net beyond discord.js's 5-second request timeout
  • Error handling treats timeout as non-fatal with debug-level logging
  • Falls back to cached members on failure

The dual-timeout approach (5s in fetch + 6s in Promise.race) ensures the operation won't hang indefinitely.

- Add agentIdentifier getter to DiscordService, MessageManager, VoiceManager, AttachmentManager
- Returns character name with agentId fallback for human-readable logs
- Data objects (Entity, Memory, World, Room) still use UUID agentId as required by types
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

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 (2)
src/messages.ts (1)

89-139: Consider extracting the TTL constant.

The 60-second TTL value (60000ms) is duplicated in both trackProgressiveMessage (line 117) and resetProgressiveTTL (line 136). Extracting this to a named constant improves maintainability and makes the timeout duration self-documenting.

🔎 Suggested refactor
+  private static readonly PROGRESSIVE_MESSAGE_TTL_MS = 60000;
+
   private trackProgressiveMessage(key: string, message: DiscordMessage): void {
     // Clear any existing timeout for this key
     const existing = this.progressiveMessages.get(key);
     if (existing?.timeout) {
       clearTimeout(existing.timeout);
     }

-    // Set 60-second TTL
+    // Set TTL for auto-cleanup
     const timeout = setTimeout(() => {
       this.progressiveMessages.delete(key);
       this.runtime.logger.debug(`Progressive message ${key} TTL expired`);
-    }, 60000);
+    }, MessageManager.PROGRESSIVE_MESSAGE_TTL_MS);

Apply the same change in resetProgressiveTTL.

src/service.ts (1)

256-262: Log client destruction errors for better observability.

The empty catch block on line 258 silently swallows any errors from client.destroy(). While these errors during cleanup might be expected, logging them would improve debugging and observability.

🔎 Suggested improvement
       // Cleanup any partially initialized clients
       if (this.client) {
-        this.client.destroy().catch(() => { });
+        this.client.destroy().catch((err) => {
+          this.runtime.logger.debug(`Error destroying client during cleanup: ${err instanceof Error ? err.message : String(err)}`);
+        });
       }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between d1c15fb and 7a7ba8c.

📒 Files selected for processing (4)
  • src/attachments.ts
  • src/messages.ts
  • src/service.ts
  • src/voice.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/attachments.ts
🧰 Additional context used
🧬 Code graph analysis (2)
src/messages.ts (1)
src/utils.ts (4)
  • editMessageContent (899-924)
  • filterAttachmentsForMemory (342-366)
  • createAttachmentFromMedia (313-332)
  • sendMessageInChunks (442-617)
src/service.ts (2)
src/types.ts (1)
  • VoiceTarget (560-587)
src/utils.ts (1)
  • createAttachmentFromMedia (313-332)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cursor Bugbot
🔇 Additional comments (6)
src/messages.ts (6)

50-53: LGTM!

The progressiveMessages map with { message, timeout } structure is a clean design for tracking progressive updates with TTL cleanup.


221-222: LGTM!

Good improvement to prefer server-specific display name (nickname) over global display name for better context in multi-server scenarios.


500-505: DM attachment handling is now correct.

The DM send now properly includes both text content and file attachments, addressing the previous review concern about dropped attachments.


519-521: LGTM!

Proper guard ensures we only track messages that were successfully sent and have valid IDs.


649-653: LGTM!

Good use of filterAttachmentsForMemory to prevent base64 data URLs from bloating the memory/context. The conditional attachment inclusion based on hasAttachments ensures data consistency.


724-734: LGTM!

Good defensive programming with the 30-second failsafe timeout for the typing indicator. This prevents the typing indicator from showing indefinitely if something goes wrong in the message processing pipeline.

… timeouts

- Memory.agentId must be UUID, not character name (messages.ts)
- Remove invalid 'time' option from guild.members.fetch() calls
- Wrap member fetch in Promise.race for proper timeout handling
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
src/messages.ts (1)

491-491: Consider stronger typing for messages array.

The messages variable is typed as any[] (lines 491, 568), and the tracking validation at line 519-521 only checks messages[0].id without confirming it's a proper Discord message. While the current code works (both paths return Discord messages), explicit typing would prevent future regressions and improve IDE support.

💡 Suggested type improvement
-          let messages: any[] = [];
+          let messages: DiscordMessage[] = [];

Apply the same change at line 568 in the non-progressive flow.

Also applies to: 519-521, 568-568

src/service.ts (1)

153-157: Inconsistent optional chaining in agentIdentifier getter.

The getter uses optional chaining for this.runtime?.character?.name but then accesses this.runtime.agentId without optional chaining. If this.runtime is undefined, the second part of the expression will throw a TypeError.

🔎 Suggested fix for consistent optional chaining
  private get agentIdentifier(): string {
-    return this.runtime?.character?.name || this.runtime.agentId;
+    return this.runtime?.character?.name || this.runtime?.agentId || 'unknown';
  }

Alternatively, if runtime is guaranteed to be defined after construction (which it should be), remove the optional chaining entirely:

  private get agentIdentifier(): string {
-    return this.runtime?.character?.name || this.runtime.agentId;
+    return this.runtime.character?.name || this.runtime.agentId;
  }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 7a7ba8c and ae4d8e1.

📒 Files selected for processing (2)
  • src/messages.ts
  • src/service.ts
🧰 Additional context used
🧬 Code graph analysis (1)
src/messages.ts (1)
src/utils.ts (4)
  • editMessageContent (899-924)
  • filterAttachmentsForMemory (342-366)
  • createAttachmentFromMedia (313-332)
  • sendMessageInChunks (442-617)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cursor Bugbot
🔇 Additional comments (10)
src/messages.ts (5)

55-60: Previous critical issue resolved: Memory.agentId now correctly uses UUID.

The past review flagged that agentIdentifier (returning a character name) was incorrectly assigned to Memory.agentId, which requires a UUID. The current code correctly uses this.runtime.agentId at all three memory creation sites (lines 447-448, 545-546, 639-640), and reserves agentIdentifier exclusively for logging as documented in the getter comment.

Also applies to: 447-448, 545-546, 639-640


50-53: Well-designed progressive message tracking with proper resource management.

The Map-based tracking with 60-second TTL prevents memory leaks from stalled actions, and the timeout reset logic ensures long-running actions with frequent updates don't expire prematurely. The cleanup is thorough (clearing timeout before deleting entry), and the detailed inline comments clearly explain the design rationale.

Also applies to: 89-150


492-513: Previous issue resolved: DM path now includes file attachments.

The past review flagged that the DM send (line 435 in old code) only sent text, dropping any prepared attachments. The current implementation correctly includes the files parameter (lines 500-503) and captures the returned message for tracking (line 500-505), resolving both the attachment loss and message tracking issues.


481-489: Attachment handling is consistent and correct across all message flows.

All paths (progressive DM, progressive channel, non-progressive DM, non-progressive channel) correctly:

  • Convert Media to AttachmentBuilder using createAttachmentFromMedia
  • Filter out null results from failed conversions
  • Include attachments in both message sends and memory creation
  • Apply filterAttachmentsForMemory to prevent base64 data URLs from bloating context

Also applies to: 584-592, 610-619


445-462: Memory creation is consistent and correctly structured across all paths.

All memory creation sites (progressive edit, progressive fallback, non-progressive) correctly:

  • Use this.runtime.agentId for both entityId and agentId (UUID requirement)
  • Apply filterAttachmentsForMemory to prevent base64 data URLs from bloating conversation context
  • Populate all required Memory fields with appropriate values
  • Associate memories with the correct room and message references

Also applies to: 541-564, 631-659

src/service.ts (5)

206-217: Excellent fix: Token validation now correctly excludes DISCORD_APPLICATION_ID.

The token fallback chain has been properly corrected to only use valid bot token environment variables (DISCORD_API_TOKEN and DISCORD_BOT_TOKENS). The helpful warning messages clearly distinguish between a missing token and the application ID, which should prevent user confusion.

This addresses the previous critical issue where DISCORD_APPLICATION_ID was incorrectly included in the token fallback chain.


2575-2732: Well done: Member fetch calls now properly handle timeouts.

Both guild.members.fetch() calls have been corrected:

  1. Line 2588: Removed the invalid time option while keeping the existing Promise.race timeout protection.
  2. Lines 2668-2672: Added proper Promise.race timeout wrapper (10 seconds) to prevent indefinite hangs on slow networks.

The implementation correctly uses Discord.js's valid options (limit) and provides robust timeout handling for reliability in production environments.


2763-2982: Outstanding implementation: Auto-join feature comprehensively addresses all previous concerns.

This implementation has successfully resolved all flagged issues from the previous review:

  1. Timeout tracking (line 2981): The autoJoinTimeout is properly tracked in this.timeouts array for cleanup on service stop.
  2. Pre-existing connections (lines 2808-2831): The activeConnections Set is pre-populated by checking all clients' existing voice connections, preventing duplicate join attempts.
  3. VoiceManager guards (lines 2817-2820, 2862-2865): Null checks added in both the pre-population loop and the join loop to safely skip text-only clients.

Additional highlights:

  • Excellent documentation: The inline comments thoroughly explain the "why" behind design decisions (comma-separated IDs, Discord's one-connection-per-guild limitation, algorithm choice, etc.).
  • Multi-bot coordination: Properly handles complex scenarios like multiple bots across multiple guilds.
  • Comprehensive logging: Summary results clearly show which bots joined which channels, making debugging straightforward.
  • Robust error handling: Try-catch blocks and defensive checks throughout.

This is production-ready code with exceptional documentation quality.


3413-3421: Good refactoring: Eliminated code duplication in handleReactionRemove.

The method now properly delegates to the generic handleReaction method instead of duplicating ~80 lines of logic. This:

  • Reduces maintenance burden (single source of truth for reaction handling)
  • Eliminates the potential for divergence between add/remove handlers
  • Correctly uses messageServerId instead of the deprecated serverId property (via the generic handler)

4771-4794: Perfect fix: Activity clearing now uses proper Discord.js API.

The clearActivity method has been corrected to use setPresence({ activities: [] }) instead of the problematic setActivity(null as any). This prevents invalid Gateway packets that could invalidate the bot's session.

The implementation correctly handles edge cases:

  • Checks if client is ready
  • Checks if client.user is available
  • Proper error handling with logging

…dates

- Add flushInProgress flag to prevent concurrent flush operations
- Schedule retry timer when flush is already in progress
- Clear flag on callback completion using .finally()
- Prevents duplicate Discord messages when API response is slow
- Memory.agentId must be UUID, not character name
- Fixes build error: Type 'string' is not assignable to UUID type
…race condition fix

- Embed reply chain (up to 5 levels) directly in message text for agent context
- Add metadata: inReplyToAuthor, discordInReplyToMessageId, discordInReplyToUserId
- Add agentRoleProvider for agent's Discord role/permissions info
- Fix progressive message race condition with waitForFlushComplete()
- Add backwards compatibility for messageServerId (Discord snowflake) fallback
- Actions now use channelId with snowflake fallback for guild lookup
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/actions/leaveChannel.ts (1)

412-446: Bug: currentServerId is undefined.

The findChannel calls at lines 418, 426, 436, and 444 pass currentServerId as an argument, but this variable is never defined in the handler scope. This will always be undefined, breaking the guild lookup in findChannel.

Should this be using the resolved guild or passing messageServerId instead?

🔎 Proposed fix - use messageServerId
      // Find the channel (try voice first if it's a voice request)
      let targetChannel = isVoiceRequest
        ? await findChannel(
            discordService,
            channelInfo.channelIdentifier,
            currentChannelId,
-           currentServerId,
+           messageServerId,
            true,
          )
        : await findChannel(
            discordService,
            channelInfo.channelIdentifier,
            currentChannelId,
-           currentServerId,
+           messageServerId,
            false,
          );

      // If not found, try the opposite type
      if (!targetChannel) {
        targetChannel = isVoiceRequest
          ? await findChannel(
              discordService,
              channelInfo.channelIdentifier,
              currentChannelId,
-             currentServerId,
+             messageServerId,
              false,
            )
          : await findChannel(
              discordService,
              channelInfo.channelIdentifier,
              currentChannelId,
-             currentServerId,
+             messageServerId,
              true,
            );
      }
♻️ Duplicate comments (8)
src/actions/readChannel.ts (1)

17-24: Duplicate isDiscordSnowflake helper.

Same function duplicated here. See comment on searchMessages.ts for consolidation suggestion.

src/actions/getUserInfo.ts (1)

17-24: Duplicate isDiscordSnowflake helper.

Same function duplicated here. See comment on searchMessages.ts for consolidation suggestion.

src/actions/serverInfo.ts (1)

14-21: Duplicate isDiscordSnowflake helper.

Same function duplicated here. See comment on searchMessages.ts for consolidation suggestion.

src/actions/leaveChannel.ts (1)

21-28: Duplicate isDiscordSnowflake helper.

Same function duplicated here. See comment on searchMessages.ts for consolidation suggestion.

src/actions/sendDM.ts (2)

17-24: Duplicate isDiscordSnowflake helper.

Same function duplicated here. See comment on searchMessages.ts for consolidation suggestion.


26-62: Duplicate getGuildFromRoom helper.

Same helper duplicated here. See comment on leaveChannel.ts for consolidation suggestion.

src/actions/joinChannel.ts (2)

20-27: Duplicate isDiscordSnowflake helper.

Same function duplicated here. See comment on searchMessages.ts for consolidation suggestion.


94-130: Duplicate getGuildFromRoom helper.

Same helper duplicated here. See comment on leaveChannel.ts for consolidation suggestion.

🧹 Nitpick comments (7)
src/actions/searchMessages.ts (2)

17-24: Consider extracting isDiscordSnowflake to a shared utility.

This helper is duplicated across searchMessages.ts, readChannel.ts, getUserInfo.ts, serverInfo.ts, leaveChannel.ts, sendDM.ts, and joinChannel.ts. Extract it to a shared location (e.g., src/utils.ts) to reduce duplication and ensure consistent behavior.


243-277: Guild resolution logic is consistent with other actions.

The multi-path approach (channelId → messageServerId fallback) aligns with the pattern used in other action files. However, consider extracting this into a shared getGuildFromRoom helper like in leaveChannel.ts, sendDM.ts, and joinChannel.ts to reduce duplication.

The empty catch blocks at lines 254-256 and 266-268 silently swallow errors. While this is acceptable for fallback logic, consider logging at debug level for troubleshooting.

🔎 Optional: Add debug logging for failed fetches
          try {
            currentChannel = await discordService.client.channels.fetch(channelId) as GuildChannel | undefined;
          } catch {
-           // Channel fetch failed
+           // Channel fetch failed - continue to fallback
          }
src/actions/readChannel.ts (1)

186-220: Guild resolution logic is correct but inconsistent with other files.

This file uses inline guild resolution while leaveChannel.ts, sendDM.ts, and joinChannel.ts use a getGuildFromRoom helper. Consider using the same helper pattern for consistency.

src/actions/leaveChannel.ts (1)

30-66: Duplicate getGuildFromRoom helper.

This helper is also defined in sendDM.ts and joinChannel.ts. Extract to a shared utility to reduce duplication.

src/providers/agentRole.ts (1)

77-122: Consider adding debug logging in catch blocks.

The empty catch blocks at lines 82 and 107 silently swallow errors, which can make debugging difficult. While the graceful fallbacks are appropriate, adding debug-level logging would help diagnose issues in production.

🔎 Suggested improvement
     if (!channel) {
       try {
         channel = await discordService.client.channels.fetch(channelId) as GuildChannel | undefined;
-      } catch {
+      } catch (error) {
+        runtime.logger?.debug?.({ src: 'agentRoleProvider', channelId, error: error instanceof Error ? error.message : String(error) }, 'Failed to fetch channel');
         return {
           data: {},
           values: {},
           text: '',
         };
       }
     }
     try {
       botMember = guild.members.cache.get(discordService.client.user?.id ?? '');
       if (!botMember && discordService.client.user?.id) {
         botMember = await guild.members.fetch(discordService.client.user.id);
       }
-    } catch {
-      // Bot might not be in guild cache yet
+    } catch (error) {
+      // Bot might not be in guild cache yet - log for debugging
+      runtime.logger?.debug?.({ src: 'agentRoleProvider', guildId: guild.id, error: error instanceof Error ? error.message : String(error) }, 'Failed to fetch bot member');
     }
src/index.ts (2)

250-253: Awaiting memory scrub blocks plugin initialization.

The await scrubBase64ImagesFromMemories(runtime) call blocks the init function from completing until all memories are processed. Combined with the earlier performance concern, consider making this non-blocking:

🔎 Non-blocking alternative
     // Clean up base64 images from existing memories to prevent context bloat
     // This runs once on startup to fix any memories that were saved before this fix
-    await scrubBase64ImagesFromMemories(runtime);
+    // Run in background to not block plugin initialization
+    scrubBase64ImagesFromMemories(runtime).catch((error) => {
+      runtime.logger.warn(
+        { src: 'plugin:discord', error: error instanceof Error ? error.message : String(error) },
+        'Background memory scrubbing failed (non-fatal)'
+      );
+    });

68-133: Startup memory scrubbing blocks plugin initialization and may impact boot time on large deployments.

The function fetches up to 1000 memories and updates them sequentially with individual await calls at line 252, blocking initialization. While error handling prevents plugin failure, the synchronous wait still impacts startup time. The code comment indicates "once on startup," but there's no guard to prevent re-running on subsequent restarts, so it executes on every initialization.

Consider deferring this to a background task after initialization, or adding a timestamp check to skip unnecessary re-runs if already completed recently.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between ae4d8e1 and 93fb31f.

📒 Files selected for processing (12)
  • src/actions/getUserInfo.ts
  • src/actions/joinChannel.ts
  • src/actions/leaveChannel.ts
  • src/actions/readChannel.ts
  • src/actions/searchMessages.ts
  • src/actions/sendDM.ts
  • src/actions/serverInfo.ts
  • src/index.ts
  • src/progressiveMessage.ts
  • src/providers/agentRole.ts
  • src/service.ts
  • src/voice.ts
🧰 Additional context used
🧬 Code graph analysis (2)
src/progressiveMessage.ts (1)
src/index.ts (1)
  • ProgressiveMessage (44-44)
src/index.ts (2)
src/utils.ts (1)
  • isDataUrl (186-188)
src/permissions.ts (1)
  • getPermissionValues (238-247)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cursor Bugbot
🔇 Additional comments (16)
src/actions/searchMessages.ts (1)

96-107: Good defensive normalization for LLM output.

The normalizeNullish function handles common LLM hallucinations like "null", "undefined", and "none" strings. This is a solid approach to make the JSON parsing more robust.

src/progressiveMessage.ts (2)

324-338: Polling implementation is acceptable but consider event-driven alternative.

The 50ms polling with 2s timeout is a pragmatic solution. The warning at line 336 provides visibility when timeouts occur.

For a future improvement, you could refactor to use a Promise that resolves when the flush completes, avoiding the polling loop entirely:

🔎 Optional future improvement
private flushPromise: Promise<void> | null = null;

private flushUpdate(): void {
    // ... existing checks ...
    
    this.flushPromise = this.callback(content)
        .catch(error => {
            logger.warn(`Progressive update flush failed: ${error}`);
        })
        .finally(() => {
            this.flushInProgress = false;
            this.flushPromise = null;
        });
    // ...
}

private async waitForFlushComplete(): Promise<void> {
    if (this.flushPromise) {
        await Promise.race([
            this.flushPromise,
            new Promise(resolve => setTimeout(resolve, 2000))
        ]);
    }
}

260-285: Complete method handles the race condition correctly.

The waitForFlushComplete() call at line 274 ensures the final message waits for any in-progress flush. The fast-path optimization at lines 279-281 for operations under minDelay is a good UX touch.

src/actions/getUserInfo.ts (1)

168-202: Guild resolution implementation is correct.

The multi-path resolution (channelId → messageServerId) is implemented consistently. The backward compatibility fallback ensures existing room data structures continue to work.

src/actions/serverInfo.ts (1)

133-167: LGTM - Guild resolution is correct.

The implementation is consistent with other action files.

src/actions/sendDM.ts (1)

259-268: LGTM - Correct usage of channel/server ID resolution.

The handler correctly extracts currentChannelId and messageServerId from room data and passes them to findUser.

src/actions/joinChannel.ts (1)

289-349: LGTM - Guild resolution and channel finding are correctly wired.

The handler properly extracts IDs from room data and uses the multi-path resolution for both explicit channel finding and the fallback to the user's current voice channel.

src/providers/agentRole.ts (4)

1-6: LGTM!

Imports are well-organized, properly use the type keyword for type-only imports, and follow TypeScript best practices.


25-75: LGTM!

The guard clauses provide good defensive programming, handling non-Discord messages, missing rooms, DMs, and service availability checks with graceful fallbacks.


124-244: LGTM!

The role extraction, permission mapping, and text generation logic are well-implemented. The comprehensive permission list covers the most relevant Discord capabilities, and the behavioral guidelines provide useful context for the agent.


249-257: LGTM!

The formatPermissionName helper correctly converts camelCase to Title Case using a clean regex approach.

src/index.ts (5)

18-29: LGTM!

New imports for actions and providers are well-organized and align with the expanded plugin functionality.


31-53: LGTM!

The expanded exports are well-documented with clear grouping comments, making it easy for external consumers to understand what's available.


156-159: LGTM!

New actions and providers are properly wired into the plugin definition.


229-248: LGTM!

Token validation correctly checks only DISCORD_API_TOKEN and DISCORD_BOT_TOKENS. The warning message is clear, actionable, and properly clarifies that DISCORD_APPLICATION_ID is the OAuth2 client ID, not a bot token.


256-292: LGTM!

The additional exports are well-organized with clear grouping comments, providing a comprehensive public API for external consumers.

…, firstUpdateSent timing

- setVoiceChannelStatus: use channelId/messageServerId pattern like other actions
- index.ts: remove duplicate isDataUrl, import from utils
- progressiveMessage: set firstUpdateSent before async callback to prevent minDelay bypass
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/progressiveMessage.ts (1)

326-340: Consider storing the flush promise directly.

The polling approach works but could be simplified by storing the callback promise as a class field and awaiting it directly in waitForFlushComplete(), eliminating the need for polling loops and timeouts.

🔎 Optional refactor using promise-based approach

Add a field to store the flush promise:

+   private flushPromise: Promise<void> | null = null;

Update flushUpdate() to store the promise:

    private flushUpdate(): void {
        // ... existing guard logic ...
        
        this.flushInProgress = true;
        this.firstUpdateSent = true;
        
        // ... existing content setup ...
        
-       this.callback(content)
+       this.flushPromise = this.callback(content)
            .catch(error => {
                logger.warn(`Progressive update flush failed: ${error}`);
            })
            .finally(() => {
                this.flushInProgress = false;
+               this.flushPromise = null;
            });
    }

Simplify waitForFlushComplete():

    private async waitForFlushComplete(): Promise<void> {
-       if (!this.flushInProgress) return;
-       
-       const maxWait = 2000;
-       const pollInterval = 50;
-       const startTime = Date.now();
-       
-       while (this.flushInProgress && (Date.now() - startTime) < maxWait) {
-           await new Promise(resolve => setTimeout(resolve, pollInterval));
-       }
-       
-       if (this.flushInProgress) {
-           logger.warn('Progressive message flush timed out - proceeding with final message');
-       }
+       if (this.flushPromise) {
+           await this.flushPromise;
+       }
    }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 93fb31f and 4f96e02.

📒 Files selected for processing (3)
  • src/actions/setVoiceChannelStatus.ts
  • src/index.ts
  • src/progressiveMessage.ts
✅ Files skipped from review due to trivial changes (1)
  • src/index.ts
🧰 Additional context used
🧬 Code graph analysis (2)
src/progressiveMessage.ts (2)
src/index.ts (1)
  • ProgressiveMessage (45-45)
src/service.ts (1)
  • delay (3501-3503)
src/actions/setVoiceChannelStatus.ts (2)
src/index.ts (3)
  • DiscordService (255-255)
  • DiscordService (256-256)
  • DISCORD_SERVICE_NAME (254-254)
src/service.ts (2)
  • DiscordService (117-4854)
  • setVoiceChannelStatus (4738-4787)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cursor Bugbot
🔇 Additional comments (11)
src/actions/setVoiceChannelStatus.ts (4)

18-63: Well-structured utility functions with proper backwards compatibility.

The isDiscordSnowflake helper correctly distinguishes Discord IDs from UUIDs, and getGuildFromRoom implements the established pattern (channelId primary, messageServerId fallback with snowflake validation) mentioned in past review feedback. The type assertions are safely guarded by subsequent null and property checks.


90-144: Excellent defensive validation that addresses past review feedback.

The function properly validates both channelIdentifier and statusMessage as strings with explicit type guards (lines 116-123, 127-134), preventing undefined values from reaching discordService.setVoiceChannelStatus. The retry logic with debug logging and final warning after 3 attempts is robust. Allowing empty strings for statusMessage is correct for clearing voice channel status.


149-229: Null-safe channel resolution that addresses past review feedback.

The function correctly addresses the previous concern about unsafe optional chaining by:

  • Pre-computing normalized strings once (lines 174-177) to avoid repeated toLowerCase() calls
  • Explicitly checking ch and ch.name existence before accessing properties (lines 186, 207)
  • Using runtime.logger.error instead of console.error (line 226)

The search strategy (current guild first via getGuildFromRoom, then all guilds) is well-designed and handles both ID and name-based lookups with proper error recovery.


231-416: Comprehensive action implementation with proper error handling.

The handler follows best practices:

  • Validates Discord source and service availability upfront (lines 243-265)
  • Uses the established room property pattern with channelId and messageServerId (lines 278-280)
  • Provides user-friendly error messages at each failure point
  • Creates structured memory with metadata on success (lines 310-328)
  • Includes diverse examples covering set, update, clear, and emoji scenarios (lines 351-412)

The integration with findVoiceChannel and discordService.setVoiceChannelStatus is clean, and the comprehensive try-catch with detailed logging ensures operational visibility.

src/progressiveMessage.ts (7)

4-65: Excellent documentation that explains design rationale.

The comprehensive comments explain not just what the code does, but why each design decision was made (debouncing, throttling, important flag, etc.). This makes the code highly maintainable.


93-106: LGTM! Constructor with sensible defaults.

The default values (300ms minDelay to suppress fast operations, 500ms throttle to stay under Discord rate limits) align well with the documented rationale.


141-194: LGTM! Correct debouncing and throttling implementation.

The update method properly handles:

  • Platform-specific behavior (important flag for non-Discord sources)
  • First-update debouncing to suppress fast operations
  • Throttled subsequent updates with proper timer management

227-230: Timing fix correctly applied!

The previous review concern about firstUpdateSent timing has been properly addressed. Setting firstUpdateSent = true immediately (line 230) before the async callback ensures subsequent update() calls respect throttle timing and don't bypass minDelay while the flush is in-flight.


212-255: Race condition properly mitigated.

The flushInProgress flag and retry scheduling (lines 217-225) correctly prevent concurrent flushes from creating duplicate messages. The cleanup in the .finally() block ensures the flag is always reset.


262-287: LGTM! Proper synchronization before final message.

The complete() method correctly:

  • Clears pending timers
  • Waits for in-progress flushes to prevent race conditions
  • Implements fast-path optimization for quick operations

345-366: LGTM! Clean final message handling.

The sendFinal() method correctly:

  • Conditionally includes progressive metadata
  • Sets isInterim: false to create a memory for the final message
  • Handles errors gracefully by returning an empty array

Discord.js v14+ requires Partials.Channel to receive DM messages.
Without it, DM channels aren't cached and messageCreate events
don't fire for direct messages.

- Add Partials.Channel, Partials.Message, Partials.User to Client config
- Add debug logging for messageCreate events and event listener setup
- reactToMessage: Return null when all emojis are forbidden instead of
  using neutral[0], matching docstring and LLM path behavior
- clientRegistry: Call client.destroy() on failed bot registration to
  release Discord.js resources (REST client, caches, WebSocket state)
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

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/clientRegistry.ts (2)

208-269: Consider exposing loginPromises or removing unused tracking.

The loginPromises map stores pending login promises but is never read from. This could be useful for:

  • Awaiting all pending logins before operations
  • Cancellation support in destroyAll

If not needed, consider removing the tracking to reduce complexity.


324-327: Dead code: alias is never a map key after successful login.

After login succeeds, clients are re-keyed from tempId (alias) to botId at lines 242-244. The alias is stored in config.alias but is no longer a key in the clients map, making this delete a no-op.

Consider removing the redundant delete or adding a comment explaining when this branch would be relevant.

Suggested simplification
     // Cleanup
     clientInfo.client.destroy();
     this.clients.delete(botId);
-    if (clientInfo.config.alias) {
-      this.clients.delete(clientInfo.config.alias);
-    }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 760f5ee and a0773c9.

📒 Files selected for processing (2)
  • src/actions/reactToMessage.ts
  • src/clientRegistry.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cursor Bugbot
🔇 Additional comments (13)
src/actions/reactToMessage.ts (5)

127-162: LGTM!

The sentiment detection implementation is clean. The keyword-based approach with a neutral fallback is appropriate for this use case, and the emoji mappings provide good coverage for common sentiments.


164-218: LGTM!

The character preference utilities handle both configuration formats cleanly. The defensive programming with optional chaining and array/object type guards ensures robustness against malformed settings.


271-300: Previous forbidden emoji fallback issue is now properly addressed.

The multi-tier fallback chain (neutral → all categories → null) ensures a forbidden emoji is never returned. This correctly addresses the previous review comments about sentimentEmojis.neutral[0] potentially being forbidden.


389-409: LGTM!

The character path correctly distinguishes between style-forbidden (early return) and preference-forbidden (fall through to LLM) cases. The flow allows the LLM path a chance to find alternatives like custom Discord emojis not in the predefined sentiment lists.


427-461: Previous LLM path forbidden emoji issue is now properly addressed.

The refactored logic correctly handles forbidden emojis:

  • Tries sentiment-matched alternatives first
  • Falls back to filtered neutral emojis
  • Sets emoji = null when no safe option exists (line 450)
  • Only creates reactionInfo when emoji is valid (line 455)

This correctly addresses the previous comments about the || emoji fallback returning forbidden emojis.

src/clientRegistry.ts (8)

1-6: LGTM!

Imports are well-organized with clear separation between discord.js, core framework, and local module dependencies.


8-17: LGTM!

The interface cleanly separates construction-time properties from post-login properties with appropriate optionality.


19-59: LGTM!

Comprehensive token validation with clear, actionable error messages for each failure mode. The structured return type allows callers to provide specific guidance.


64-73: LGTM!

Clean class structure with appropriate private state management using Maps for efficient lookups.


78-104: LGTM!

The token handling is now correct. The code properly uses only DISCORD_API_TOKEN for authentication and the comments clearly document that DISCORD_APPLICATION_ID is not a valid token.


179-200: LGTM!

The error handling now correctly calls client.destroy() to release Discord.js resources before cleanup. The troubleshooting messages are helpful for debugging token issues.


274-287: LGTM!

The two-phase lookup (direct key then alias scan) handles both ID and alias queries efficiently for typical bot counts.


376-447: LGTM!

Well-structured event emission with appropriate handling for large guilds. The fire-and-forget background user sync pattern at lines 432-436 avoids blocking startup while still attempting to pre-populate user data for small guilds.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Fixes race condition where in-flight login callbacks could add clients
back to the map after destroyAll cleared it. Now uses Promise.allSettled
to wait for pending logins to complete or fail before destroying clients.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/clientRegistry.ts`:
- Around line 216-268: The timeout path can reject while the
client.once('ready') handler remains attached, allowing that handler to run
later and re-key this.clients; to fix, store the ready callback in a named
function (the handler passed to client.once('ready')) and on timeout (the
timeout variable's callback) remove that handler (e.g., client.removeListener or
client.off for 'ready') and also remove the 'error' listener you attached, then
reject; likewise ensure you clear the timeout and remove those listeners in the
client.login.catch path and any early rejects so no lingering listeners remain.
🧹 Nitpick comments (1)
src/clientRegistry.ts (1)

24-59: Token validation doesn't return the trimmed token.

The function validates trimmedToken but returns only { valid: true } without the trimmed value. Callers use the original (possibly whitespace-padded) token. If a token has leading/trailing whitespace, it will pass validation but may fail at Discord login.

Consider returning the sanitized token:

♻️ Suggested improvement
-function validateDiscordToken(token: string): { valid: boolean; error?: string } {
+function validateDiscordToken(token: string): { valid: boolean; error?: string; sanitizedToken?: string } {
   if (!token) {
     return { valid: false, error: 'Token is empty or undefined' };
   }

   const trimmedToken = token.trim();
   // ... existing validation ...

-  return { valid: true };
+  return { valid: true, sanitizedToken: trimmedToken };
 }

Then use validation.sanitizedToken instead of the original token when registering bots.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between a0773c9 and be12cf2.

📒 Files selected for processing (1)
  • src/clientRegistry.ts
🧰 Additional context used
🧬 Code graph analysis (1)
src/clientRegistry.ts (3)
src/voice.ts (1)
  • VoiceManager (347-3294)
src/service.ts (1)
  • DiscordService (117-4872)
src/index.ts (1)
  • DiscordEventTypes (259-259)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cursor Bugbot
🔇 Additional comments (7)
src/clientRegistry.ts (7)

78-104: LGTM - Token fallback clarification addressed.

The comments on lines 84-85, 94, and 101 now clearly document that DISCORD_APPLICATION_ID is the OAuth2 client/application ID used for invite URLs, not for bot authentication. This addresses the previous review concern about confusing error messages.


179-200: LGTM - Client cleanup on failed registration now implemented.

The error handler at lines 195-196 now properly calls client.destroy() before removing from maps, addressing the previous review concern about unreleased Discord.js Client resources.


274-287: LGTM!

The lookup logic correctly handles both direct ID lookup and alias-based search.


292-306: LGTM!

Both utility methods are implemented correctly with clear semantics.


311-328: LGTM!

The removal logic properly cleans up the client and handles both ID and alias keys defensively.


346-366: LGTM - Race condition fix properly implemented.

The destroyAll method now awaits pending logins via Promise.allSettled before destroying clients, addressing the previous review concern about in-flight logins surviving cleanup. The clients.clear() on line 364 is redundant since removeBot already deletes entries, but it's harmless as a defensive measure.


386-457: Well-structured world event emission with appropriate optimization.

The method properly emits both Discord-specific and platform-agnostic WORLD_CONNECTED events. The optimization to skip user pre-fetch for large guilds (>1000 members) and use background sync for smaller guilds is a good design choice that prevents blocking startup. Both buildStandardizedRooms and buildStandardizedUsers methods exist as public methods on the service and are correctly used here.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

The timeout, ready, and login error paths could leave lingering event
listeners attached to the client. If timeout fired first, the ready
handler could still execute later and re-key the clients map.

- Add settled flag to prevent multiple path execution
- Create cleanup function to remove all listeners and clear timeout
- Call cleanup in all exit paths (ready, timeout, login error)
- Use client.off() to remove named handlers properly
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.

1 participant