feat(slack): add mute/unmute thread command#4
Conversation
When a user sends 'mute' in a thread, the bot stops responding in that thread unless explicitly @mentioned. Sending 'unmute' resumes normal behavior. The bot acknowledges both commands with a short reply. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
| const threadTs = message.thread_ts ?? message.ts; | ||
| const muteKey = threadTs ? `${message.channel}:${threadTs}` : undefined; |
There was a problem hiding this comment.
🟡 Mute check runs before thread_ts resolution, causing mute bypass for thread replies with missing thread_ts
The mute key is computed at line 131 using message.thread_ts ?? message.ts, but threadTsResolver.resolve() is called later at line 169. When Slack omits thread_ts on a thread reply (indicated by parent_user_id being set — a known edge case handled by the thread resolver at src/slack/monitor/thread-resolution.ts:81), the mute key will be based on message.ts (the reply's own timestamp) instead of the actual thread root timestamp. This means:
- If a thread was correctly muted, subsequent thread replies missing
thread_tswill use a differentmuteKeyand bypass the mute check at line 165. - If someone sends "mute" from such a message, the mute will be registered under the wrong key and won't apply to the actual thread.
The debouncer already accounts for this edge case with the maybe-thread key prefix at line 48-49, but the mute logic does not.
Prompt for agents
In src/slack/monitor/message-handler.ts, the mute/unmute key computation and mute-silence check (lines 131-166) should use the resolved thread_ts, not the raw message fields. Move the mute/unmute logic (lines 130-167) to AFTER the threadTsResolver.resolve() call at line 169, and compute threadTs/muteKey from resolvedMessage instead of message. Specifically:
1. Move line 169 (const resolvedMessage = await threadTsResolver.resolve(...)) to just after the aside check (after line 128)
2. Compute threadTs and muteKey from resolvedMessage.thread_ts ?? resolvedMessage.ts instead of message.thread_ts ?? message.ts
3. Use resolvedMessage.channel for the muteKey
4. Pass resolvedMessage through the mute/unmute checks
5. On line 170, enqueue resolvedMessage (already resolved) instead of re-resolving
This ensures the mute key matches the actual thread identity even when Slack omits thread_ts on thread replies.
Was this helpful? React with 👍 or 👎 to provide feedback.
Summary
Adds
muteandunmutecommands to the Slack message handler. When a user sends "mute" in a thread, the bot stops responding in that thread unless explicitly @mentioned. Sending "unmute" resumes normal behavior. Both commands are acknowledged with a short reply.The mute state is stored in-memory per
createSlackMessageHandlerinstance, keyed bychannel:threadTs. It is not persisted — a pod restart clears all mute state.Implementation sits in
message-handler.tsalongside the existingasidefilter, before the debouncer enqueue step.Review & Testing Checklist for Human
mutedThreadsis an unboundedSet<string>with no TTL or max size. Unlike the existingseenMessagesdedupe cache (which hasttlMs: 60_000, maxSize: 500), muted threads accumulate forever. For low-traffic deployments this is fine; for high-traffic ones, consider whether eviction or a TTL cache is needed.threadTsfalls back tomessage.ts, which mutes a "thread" of one message. Verify this is acceptable or if non-threaded mute should be rejected.Notes
strippedTextvariable introduced for theasidefilter, keeping the code DRY.chat.postMessagewith fire-and-forget.catch(), matching the session link pattern from PR feat(slack): session link auto-post + aside filter (merge PR #1 with CI fix) #3.Link to Devin session: https://app.devin.ai/sessions/460eec68d4c74aaa8c85af43c562f11f
Requested by: @jld-adriano