Conversation
Add Slack bot integration following the same pattern as the Telegram bot. Uses Slack Socket Mode (WebSocket-based, no HTTP endpoint) with support for message forwarding, interactive prompts via Block Kit buttons, session management, and DAG run completion notifications.
…indicator - Clear sessionID when subscription loop ends so follow-up messages create a fresh session instead of hitting "session not found" - Remove startSubscription from monitor's notifyChat/notifyChannel to prevent duplicate notification messages from the snapshot - Add hourglass reaction to user messages while the agent is processing, removed on first response (Slack bot) Fixes applied to both Slack and Telegram bots.
Wire Slack bot config into loader env bindings, add provider switch in server/startall commands, and apply session-end cleanup from Telegram bot to Slack subscribe loop. Update notification monitor to skip subscription on already-completed sessions.
Post a "Thinking..." message and hourglass reaction when processing, both removed when the first response arrives.
Include conversation key in user identity so DM and channel threads get independent agent sessions instead of conflicting with each other.
- Document message.groups event and groups:history scope for private channels - Add troubleshooting note for missing event subscriptions - Change diagnostic logging to DEBUG level
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThis pull request adds comprehensive Slack bot support alongside existing Telegram integration. It includes Slack dependencies, provider-driven bot selection during startup, Slack configuration structures with environment loading, a complete Slack bot service with Socket Mode support and session management, and DAG run monitoring with AI-assisted notifications. Minor Telegram bot cleanup is also included. Changes
Sequence DiagramssequenceDiagram
participant User as Slack User
participant SlackAPI as Slack API
participant Bot as Slack Bot
participant AgentAPI as Agent API
participant Session as Session Store
User->>SlackAPI: Send message / reaction
SlackAPI->>Bot: Socket Mode event
Bot->>Bot: Route event (message, interactive, slash cmd)
Bot->>AgentAPI: CreateSession or SendMessage
AgentAPI->>Session: Create/update session
AgentAPI-->>Bot: Session ID / response
Bot->>Bot: Process agent response
Bot->>SlackAPI: Post message to channel/thread/DM
SlackAPI-->>User: Display message
Bot->>AgentAPI: SubscribeSession
loop Poll for updates
Bot->>Session: Check for new messages
Session-->>Bot: Assistant message
Bot->>SlackAPI: Update Slack message
end
sequenceDiagram
participant Monitor as DAG Monitor
participant DAGStore as DAG Run Store
participant AgentAPI as Agent API
participant Bot as Slack Bot
participant SlackAPI as Slack API
Monitor->>DAGStore: Poll for recent runs
DAGStore-->>Monitor: List of DAG runs
Monitor->>Monitor: Filter completed, unseen runs
Monitor->>AgentAPI: CreateSession (notification)
AgentAPI-->>Monitor: Session ID
Monitor->>AgentAPI: SendMessage (notification prompt)
AgentAPI->>AgentAPI: Generate AI response
Monitor->>AgentAPI: GetSessionDetail / SubscribeSession
AgentAPI-->>Monitor: AI-generated notification text
Monitor->>Bot: Post to allowed channels
Bot->>SlackAPI: Send message
SlackAPI-->>Monitor: Confirmation
Monitor->>Monitor: Mark run as seen
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly Related PRs
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
📝 Coding Plan
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
internal/cmn/config/loader.go (1)
1102-1147:⚠️ Potential issue | 🟠 MajorSlack env/default handling is unreachable when
def.Bots == nilBecause of the early return on Line 1102, the Slack default + env override logic on Line 1123-Line 1147 is skipped for env-only setups. This causes Slack bot config to stay empty even when
DAGU_BOTS_SLACK_*env vars are set.💡 Proposed fix
func (l *ConfigLoader) loadBotsConfig(cfg *Config, def Definition) { // Default safe mode to true cfg.Bots.SafeMode = true + // Default Slack respond_to_all to true + cfg.Bots.Slack.RespondToAll = true // Check env var override for provider if provider := l.v.GetString("bots.provider"); provider != "" { cfg.Bots.Provider = BotProvider(provider) } @@ if token := l.v.GetString("bots.telegram.token"); token != "" { cfg.Bots.Telegram.Token = token } + // Slack env overrides should work even without bots section in YAML + if botToken := l.v.GetString("bots.slack.bot_token"); botToken != "" { + cfg.Bots.Slack.BotToken = botToken + } + if appToken := l.v.GetString("bots.slack.app_token"); appToken != "" { + cfg.Bots.Slack.AppToken = appToken + } + if channels := parseStringList(l.v.Get("bots.slack.allowed_channel_ids")); len(channels) > 0 { + cfg.Bots.Slack.AllowedChannelIDs = channels + } + if l.v.IsSet("bots.slack.respond_to_all") { + cfg.Bots.Slack.RespondToAll = l.v.GetBool("bots.slack.respond_to_all") + } if def.Bots == nil { return } @@ - // Default Slack respond_to_all to true - cfg.Bots.Slack.RespondToAll = true - - // Check env var override for Slack tokens - if botToken := l.v.GetString("bots.slack.bot_token"); botToken != "" { - cfg.Bots.Slack.BotToken = botToken - } - if appToken := l.v.GetString("bots.slack.app_token"); appToken != "" { - cfg.Bots.Slack.AppToken = appToken - } - if def.Bots.Slack != nil { if cfg.Bots.Slack.BotToken == "" { cfg.Bots.Slack.BotToken = def.Bots.Slack.BotToken } if cfg.Bots.Slack.AppToken == "" { cfg.Bots.Slack.AppToken = def.Bots.Slack.AppToken } - if len(def.Bots.Slack.AllowedChannelIDs) > 0 { + if len(cfg.Bots.Slack.AllowedChannelIDs) == 0 && len(def.Bots.Slack.AllowedChannelIDs) > 0 { cfg.Bots.Slack.AllowedChannelIDs = def.Bots.Slack.AllowedChannelIDs } if def.Bots.Slack.RespondToAll != nil { cfg.Bots.Slack.RespondToAll = *def.Bots.Slack.RespondToAll } } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/cmn/config/loader.go` around lines 1102 - 1147, The early return when def.Bots == nil prevents applying Slack defaults and environment overrides; stop returning early and instead only skip copying defaults from def.Bots while still executing the Slack env/default logic: remove or replace the "if def.Bots == nil { return }" with per-section nil checks (e.g., check def.Bots.Telegram, def.Bots.Slack individually) and ensure cfg.Bots.Slack.RespondToAll = true plus the l.v.GetString("bots.slack.bot_token") / "bots.slack.app_token" overrides and subsequent conditional copies from def.Bots.Slack still run even when def.Bots is nil.internal/service/telegram/monitor.go (1)
229-243:⚠️ Potential issue | 🟠 MajorNotification session is still persisted, so “fresh session on follow-up” won’t happen
notifyChatstill storessessionIDon Line 234/Line 235, but the subscription that used to terminate/cleanup that session is no longer started. That leaves a completed notification session as active chat state and can reintroduce stale-session behavior.💡 Proposed fix
- // Adopt this session as the chat's active session so the user can - // send follow-up messages (e.g., "show me the logs", "retry it"). + // Notification sessions are one-shot; do not adopt as active chat session. cs := m.bot.getOrCreateChat(chatID) m.bot.resetChat(cs) - cs.mu.Lock() - cs.sessionID = sessionID - cs.ownerUserID = user.UserID - cs.mu.Unlock() m.bot.sendLongText(chatID, response) - // Don't start a subscription here — the notification session is already - // done, and subscribing would re-send the same response from the snapshot. - // If the user sends a follow-up, handleMessage will create a fresh session. + // Don't start a subscription here — this notification is complete. + // A follow-up message will create a fresh session. return true🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/service/telegram/monitor.go` around lines 229 - 243, notifyChat is persisting a completed notification session by setting cs.sessionID and cs.ownerUserID even though no subscription will later clear it; update notifyChat (around m.bot.getOrCreateChat / m.bot.resetChat / m.bot.sendLongText) to avoid persisting the notification session: either do not assign cs.sessionID and cs.ownerUserID for notification-only flows, or explicitly clear them (under cs.mu) immediately after sending the long text so the chat has no active session; keep the mutex usage (cs.mu.Lock/Unlock) when modifying cs.sessionID and cs.ownerUserID.
🧹 Nitpick comments (4)
internal/cmn/config/config.go (1)
37-63: Consider validatingbots.providerinConfig.Validate()Now that Slack is a first-class provider, an unknown provider value can silently no-op at runtime. Adding explicit validation for
none|telegram|slackwould fail fast on typoed config.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/cmn/config/config.go` around lines 37 - 63, Add explicit validation in Config.Validate() to ensure BotsConfig.Provider is one of the allowed constants (BotProviderNone, BotProviderTelegram, BotProviderSlack); if not, return an error stating the unsupported provider value. Locate the BotsConfig struct and BotProvider constants (BotProviderNone, BotProviderTelegram, BotProviderSlack) and add a guard in Config.Validate() that checks cfg.Bots.Provider and fails fast on any value outside those three options (include the invalid value in the error message for easier debugging).internal/service/slack/monitor.go (1)
119-122: Consider usingWarnlevel for status listing failures.Using
Debuglevel for theListStatuseserror may hide operational issues in production. This is an I/O operation that could fail due to database connectivity issues.Proposed fix
if err != nil { - m.logger.Debug("Failed to list DAG run statuses", slog.String("error", err.Error())) + m.logger.Warn("Failed to list DAG run statuses", slog.String("error", err.Error())) return }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/service/slack/monitor.go` around lines 119 - 122, Change the log level for failures when listing DAG run statuses from Debug to Warn so operational I/O failures are visible in production; locate the logger call that reads m.logger.Debug("Failed to list DAG run statuses", slog.String("error", err.Error())) (in monitor.go where ListStatuses is handled) and replace it with m.logger.Warn (preserving the same message and error field) so the warning shows up at the warn level without altering the error handling flow.internal/service/slack/bot.go (2)
354-390: Consider extracting shared logic withhandleTextCommand.The slash command handlers duplicate the "new" and "cancel" logic from
handleTextCommand. This is a minor maintainability concern.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/service/slack/bot.go` around lines 354 - 390, handleSlashCommand duplicates the "/dagu-new" and "/dagu-cancel" logic found in handleTextCommand; refactor by extracting the shared actions into helper methods (e.g., performNewSession and performCancelSession) that accept a chatState (cs) and context/command metadata, then have both handleSlashCommand and handleTextCommand call those helpers; use existing symbols like getOrCreateChat, resetChat, sendReply, and agentAPI.CancelSession to implement the helpers so cancel uses cs.sessionID and cs.ownerUserID and new calls resetChat and sends the same reply messages.
137-142: Socket client errors are logged but don't stop the bot.If
RunContextreturns an error (e.g., WebSocket connection failure), the error is logged but the main event loop continues running on a closed channel. Consider propagating fatal errors or implementing reconnection logic.Consider signaling fatal errors to the main loop
+ errCh := make(chan error, 1) go func() { if err := b.socketClient.RunContext(ctx); err != nil { b.logger.Error("Socket mode client error", slog.String("error", err.Error())) + errCh <- err } }() for { select { case <-ctx.Done(): b.logger.Info("Slack bot stopped") return nil + case err := <-errCh: + return fmt.Errorf("socket mode client failed: %w", err) + case evt, ok := <-b.socketClient.Events:🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/service/slack/bot.go` around lines 137 - 142, The goroutine calling b.socketClient.RunContext currently only logs errors via b.logger.Error, which allows the main event loop to continue on a closed socket; modify the goroutine to propagate fatal errors to the main routine by sending the error on a dedicated error channel or invoking the shared cancellation (e.g., cancel the context passed as ctx) so the main loop can shutdown or attempt reconnection; specifically update the anonymous goroutine that calls b.socketClient.RunContext(ctx) to forward returned errors to a new or existing error handling path (instead of only calling b.logger.Error) and update the main event loop to listen for that signal and either perform reconnection logic or exit gracefully.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@internal/service/slack/bot.go`:
- Around line 290-294: The code assumes strings.Fields(text) has at least one
element and will panic on whitespace-only input; before accessing [0], ensure
the fields slice is non-empty (e.g., call strings.Fields(text) into a variable,
check len(fields) > 0 or use strings.TrimSpace(text) to early-return), then
extract cmd and only call b.handleTextCommand(ctx, cs, cmd) when a command
exists; update the block around the text handling (referencing the variables
text, cmd and the method b.handleTextCommand) to perform this guard and avoid an
index out of range panic.
In `@internal/service/slack/monitor.go`:
- Around line 177-184: The current loop over m.bot.allowedChannels uses
allDelivered to decide whether to mark the run as seen, which causes retries to
re-notify channels that already succeeded; change the behavior to record
seen/delivery per-channel instead of a single allDelivered flag by updating the
run's seen state (or calling the existing markSeen-like function) each time
m.notifyChannel(ctx, channelID, s, prompt) returns true, or alternatively treat
any success as partial acceptance and do not retry channels that returned true
on subsequent polls; modify the code paths around m.notifyChannel,
m.bot.allowedChannels iteration, and where the run is marked seen so delivery
state is maintained per channel (e.g., store a map keyed by channelID or persist
per-channel markers) to prevent duplicate notifications on retry.
- Around line 214-219: resetChat is called without holding cs.mu, then cs.mu is
locked to set cs.sessionID and cs.ownerUserID, which allows another goroutine to
interleave and cause a race; fix by performing the reset and session assignment
atomically: either modify resetChat (or add a new method like
resetAndSetSession) to accept sessionID and ownerUserID and perform both the
reset and the assignments while holding cs.mu, or acquire cs.mu before calling
resetChat and ensure resetChat does not re-lock internally; update calls to use
the new API so cs.sessionID and cs.ownerUserID are set under the same lock as
the reset to eliminate the race.
---
Outside diff comments:
In `@internal/cmn/config/loader.go`:
- Around line 1102-1147: The early return when def.Bots == nil prevents applying
Slack defaults and environment overrides; stop returning early and instead only
skip copying defaults from def.Bots while still executing the Slack env/default
logic: remove or replace the "if def.Bots == nil { return }" with per-section
nil checks (e.g., check def.Bots.Telegram, def.Bots.Slack individually) and
ensure cfg.Bots.Slack.RespondToAll = true plus the
l.v.GetString("bots.slack.bot_token") / "bots.slack.app_token" overrides and
subsequent conditional copies from def.Bots.Slack still run even when def.Bots
is nil.
In `@internal/service/telegram/monitor.go`:
- Around line 229-243: notifyChat is persisting a completed notification session
by setting cs.sessionID and cs.ownerUserID even though no subscription will
later clear it; update notifyChat (around m.bot.getOrCreateChat /
m.bot.resetChat / m.bot.sendLongText) to avoid persisting the notification
session: either do not assign cs.sessionID and cs.ownerUserID for
notification-only flows, or explicitly clear them (under cs.mu) immediately
after sending the long text so the chat has no active session; keep the mutex
usage (cs.mu.Lock/Unlock) when modifying cs.sessionID and cs.ownerUserID.
---
Nitpick comments:
In `@internal/cmn/config/config.go`:
- Around line 37-63: Add explicit validation in Config.Validate() to ensure
BotsConfig.Provider is one of the allowed constants (BotProviderNone,
BotProviderTelegram, BotProviderSlack); if not, return an error stating the
unsupported provider value. Locate the BotsConfig struct and BotProvider
constants (BotProviderNone, BotProviderTelegram, BotProviderSlack) and add a
guard in Config.Validate() that checks cfg.Bots.Provider and fails fast on any
value outside those three options (include the invalid value in the error
message for easier debugging).
In `@internal/service/slack/bot.go`:
- Around line 354-390: handleSlashCommand duplicates the "/dagu-new" and
"/dagu-cancel" logic found in handleTextCommand; refactor by extracting the
shared actions into helper methods (e.g., performNewSession and
performCancelSession) that accept a chatState (cs) and context/command metadata,
then have both handleSlashCommand and handleTextCommand call those helpers; use
existing symbols like getOrCreateChat, resetChat, sendReply, and
agentAPI.CancelSession to implement the helpers so cancel uses cs.sessionID and
cs.ownerUserID and new calls resetChat and sends the same reply messages.
- Around line 137-142: The goroutine calling b.socketClient.RunContext currently
only logs errors via b.logger.Error, which allows the main event loop to
continue on a closed socket; modify the goroutine to propagate fatal errors to
the main routine by sending the error on a dedicated error channel or invoking
the shared cancellation (e.g., cancel the context passed as ctx) so the main
loop can shutdown or attempt reconnection; specifically update the anonymous
goroutine that calls b.socketClient.RunContext(ctx) to forward returned errors
to a new or existing error handling path (instead of only calling
b.logger.Error) and update the main event loop to listen for that signal and
either perform reconnection logic or exit gracefully.
In `@internal/service/slack/monitor.go`:
- Around line 119-122: Change the log level for failures when listing DAG run
statuses from Debug to Warn so operational I/O failures are visible in
production; locate the logger call that reads m.logger.Debug("Failed to list DAG
run statuses", slog.String("error", err.Error())) (in monitor.go where
ListStatuses is handled) and replace it with m.logger.Warn (preserving the same
message and error field) so the warning shows up at the warn level without
altering the error handling flow.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 3703dece-1629-4bf4-898e-04ff52aa6e17
⛔ Files ignored due to path filters (1)
go.sumis excluded by!**/*.sum
📒 Files selected for processing (10)
go.modinternal/cmd/server.gointernal/cmd/startall.gointernal/cmn/config/config.gointernal/cmn/config/definition.gointernal/cmn/config/loader.gointernal/service/slack/bot.gointernal/service/slack/monitor.gointernal/service/telegram/bot.gointernal/service/telegram/monitor.go
- Guard strings.Fields(text)[0] against whitespace-only panic - Accept partial delivery in monitor to avoid duplicate notifications - Make resetChat + session assignment atomic to prevent race condition Applied to both Slack and Telegram bots.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #1785 +/- ##
==========================================
- Coverage 68.82% 67.69% -1.13%
==========================================
Files 416 418 +2
Lines 48602 49385 +783
==========================================
- Hits 33448 33429 -19
- Misses 12359 13158 +799
- Partials 2795 2798 +3
... and 10 files with indirect coverage changes Continue to review full report in Codecov by Sentry.
🚀 New features to boost your workflow:
|
Summary
respond_to_allmodeConfiguration
Slack App Requirements
connections:writeapp-level tokenapp_mentions:read,chat:write,channels:history,groups:history,im:historyapp_mention,message.channels(public),message.groups(private),message.im(DMs)Test plan
respond_to_all: trueresponds to all channel messagesrespond_to_all: falserequires @mentionmessage.groupssubscriptionSummary by CodeRabbit
Release Notes
New Features
Bug Fixes