feat(slack): thread-aware reply routing and App Home DM fix#682
Open
rvanmelle wants to merge 5 commits intoqwibitai:mainfrom
Open
feat(slack): thread-aware reply routing and App Home DM fix#682rvanmelle wants to merge 5 commits intoqwibitai:mainfrom
rvanmelle wants to merge 5 commits intoqwibitai:mainfrom
Conversation
Two gaps discovered during real-world setup: 1. Slack silently blocks DMs unless "Allow users to send Slash commands and messages from the messages tab" is enabled in App Home settings. This setting is completely separate from event subscriptions and OAuth scopes — no API error is surfaced when it's missing, making it very difficult to diagnose. Added as Step 5 in SLACK_SETUP.md and item 5 in the SKILL.md quick summary. 2. The reinstall yellow banner after scope/event changes doesn't always appear. Added a note to manually use "Reinstall to Workspace" in OAuth & Permissions when the banner is absent, and a warning that the bot token may change on reinstall. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
By default, replies go to the channel where the message was sent. When a message arrives as a threaded reply (thread_ts != ts), the response is posted back into that same thread. An opt-in SLACK_ALWAYS_REPLY_IN_THREAD=true env var causes all replies — including those triggered from the channel root — to be posted as threads, keeping conversations organised per-message. Implementation is self-contained within SlackChannel: - pendingThreadTs map (keyed by JID) carries thread context from incoming message to outgoing sendMessage call - Outgoing queue extended with threadTs to handle the disconnected edge case correctly - No changes to Channel interface, DB schema, or other channels Also fixes a TypeScript error: resolveUserName() called with msg.user ?? '' to handle the undefined case on BotMessageEvent. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
7 tests covering the new pendingThreadTs behaviour: - threaded reply → postMessage includes thread_ts - root channel message → no thread_ts in postMessage - thread_ts === ts (thread parent) → treated as root, no thread reply - thread context cleared after first sendMessage - queued messages preserve thread context through disconnect/reconnect - SLACK_ALWAYS_REPLY_IN_THREAD: root messages reply in new thread - SLACK_ALWAYS_REPLY_IN_THREAD: threaded replies use existing thread_ts Also renames stale test description from "flattens threaded replies into channel messages" to accurately reflect the current behaviour (replies are always delivered to onMessage, routing is separate). All 53 slack tests pass. Verified on Pi5 (ARM64, Docker runtime). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…=true Slack puts threaded DM replies in a thread panel, invisible in the main DM conversation. alwaysReplyInThread now only applies to group channels (channel_type != 'im'). DMs always reply directly regardless of the setting. Threaded replies within DMs still correctly route back into their thread. Adds test: SLACK_ALWAYS_REPLY_IN_THREAD=true / DM root message → no thread_ts in postMessage. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Collaborator
|
Thread-aware reply routing for Slack is a nice addition. @gavrielc this touches the routing layer so would appreciate a sanity check. |
|
I would appreciate this as well. Its way too noisy in a slack channel, when the bot just replies in channel instead of thread. I have burned a lot of Opus tokens, trying to adapt the default architecture to support reply in thread. Maybe its working now. The main challenge was dealing with concurrent messages from two different threads. |
This was referenced Mar 23, 2026
vongohren
added a commit
to vongohren/nanoclaw
that referenced
this pull request
Apr 6, 2026
Slack replies now go into threads instead of the channel root. When SLACK_ALWAYS_REPLY_IN_THREAD=true, root messages start a new thread; threaded replies respond in the same thread; DMs reply directly. Also adds shimmer typing indicator via Slack's assistant.threads.setStatus API (requires assistant:write scope, silent no-op without it). Opts out of setup/update diagnostics telemetry. Based on upstream PRs qwibitai#682 and qwibitai#653. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Three improvements to the Slack channel skill, discovered during real-world setup on a Raspberry Pi 5 (ARM64, Docker runtime).
1. Missing App Home DM setup step (docs fix)
Slack silently blocks DMs to the bot unless "Allow users to send Slash commands and messages from the messages tab" is enabled in App Home settings. This setting is completely separate from event subscriptions and OAuth scopes — no API error is surfaced when it's missing, making it very difficult to diagnose.
SLACK_SETUP.md(renumbered subsequent steps)SKILL.mdquick summary2. Thread-aware reply routing
Default behaviour (no config change needed):
SLACK_ALWAYS_REPLY_IN_THREAD=true:Implementation: Self-contained in
SlackChannelwith apendingThreadTs: Map<string, string>that carries thread context from the incoming message event to the outgoingsendMessagecall. No changes to theChannelinterface, DB schema, or other channels.Also fixes a TypeScript error:
resolveUserName()called withmsg.user ?? ''to handle the undefined case onBotMessageEvent.3. Fix: DM replies broken when
SLACK_ALWAYS_REPLY_IN_THREAD=trueWhen
SLACK_ALWAYS_REPLY_IN_THREADwas enabled, DM replies were silently sent with athread_ts, causing Slack to route them into a thread panel rather than the main DM conversation — invisible to the user, with no error surfaced. This is the same class of silent failure as the App Home issue. The fix skips always-in-thread mode for DM channels (channel_type === 'im').Design trade-off / future direction
This implementation deliberately keeps thread context in-memory and local to
SlackChannelto avoid pipeline-wide changes. The consequence is thatthread_tsis never persisted: the DB stores the message content, sender, timestamp, and JID, but not which Slack thread the message belonged to. Threaded replies and root channel messages are indistinguishable in stored history, and if the process restarts between receiving a message and sending the reply, the thread context is lost.A "proper" pipeline-wide solution would store
thread_ts(or a genericthread_id) as an optional field on every message and propagate it through theNewMessagetype,Channel.sendMessage, and the router. This would give the agent awareness of thread structure and make history queryable by thread.Slack is unlikely to be the only channel where this matters — Telegram supports threaded replies, Discord has forum channels, and future channels may too. If the maintainer would prefer to land a consistent
thread_idfield across the pipeline first, this PR could be held or rebased on top of that work. The in-memory approach is offered as a pragmatic first step that is fully correct within its constraints and easy to replace later.Tests
8 new unit tests added to
slack.test.tscovering:postMessageincludesthread_tsthread_tsinpostMessagethread_ts === ts(thread parent/root) → treated as root, nothread_tssendMessageSLACK_ALWAYS_REPLY_IN_THREAD=true: root channel messages reply in new threadSLACK_ALWAYS_REPLY_IN_THREAD=true: threaded replies use existingthread_tsSLACK_ALWAYS_REPLY_IN_THREAD=true: DM root messages reply directly (nothread_ts)All 54 Slack unit tests pass.
Manual testing
Tested on Pi5 (ARM64, Docker runtime) with a real Slack workspace:
SLACK_ALWAYS_REPLY_IN_THREAD=true→ root channel message starts a new threadSLACK_ALWAYS_REPLY_IN_THREAD=true→ threaded reply continues in the same threadSLACK_ALWAYS_REPLY_IN_THREAD=true→ DM replies directly (not threaded)🤖 Generated with Claude Code