Skip to content

feat(slack): thread-aware reply routing and App Home DM fix#682

Open
rvanmelle wants to merge 5 commits intoqwibitai:mainfrom
rvanmelle:fix/slack-dm-setup-instructions
Open

feat(slack): thread-aware reply routing and App Home DM fix#682
rvanmelle wants to merge 5 commits intoqwibitai:mainfrom
rvanmelle:fix/slack-dm-setup-instructions

Conversation

@rvanmelle
Copy link
Copy Markdown

@rvanmelle rvanmelle commented Mar 4, 2026

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.

  • Added as Step 5 in SLACK_SETUP.md (renumbered subsequent steps)
  • Added as item 5 in SKILL.md quick summary
  • Added "Bot not responding to DMs" troubleshooting entry
  • Added note that the reinstall yellow banner doesn't always appear after scope/event changes

2. Thread-aware reply routing

Default behaviour (no config change needed):

  • Message in a thread → bot replies in that same thread
  • Message in the channel root → bot replies in the channel

SLACK_ALWAYS_REPLY_IN_THREAD=true:

  • Message in a channel thread → bot replies in that thread
  • Message in a channel root → bot starts a new thread anchored to that message
  • DM → bot always replies directly, regardless of this setting (see below)

Implementation: Self-contained in SlackChannel with a pendingThreadTs: Map<string, string> that carries thread context from the incoming message event to the outgoing sendMessage call. No changes to the Channel interface, DB schema, or other channels.

Also fixes a TypeScript error: resolveUserName() called with msg.user ?? '' to handle the undefined case on BotMessageEvent.

3. Fix: DM replies broken when SLACK_ALWAYS_REPLY_IN_THREAD=true

When SLACK_ALWAYS_REPLY_IN_THREAD was enabled, DM replies were silently sent with a thread_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 SlackChannel to avoid pipeline-wide changes. The consequence is that thread_ts is 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 generic thread_id) as an optional field on every message and propagate it through the NewMessage type, 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_id field 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.ts covering:

  • Threaded reply → postMessage includes thread_ts
  • Root channel message → no thread_ts in postMessage
  • thread_ts === ts (thread parent/root) → treated as root, no thread_ts
  • Thread context cleared after first sendMessage
  • Queued messages preserve thread context through disconnect/reconnect
  • SLACK_ALWAYS_REPLY_IN_THREAD=true: root channel messages reply in new thread
  • SLACK_ALWAYS_REPLY_IN_THREAD=true: threaded replies use existing thread_ts
  • SLACK_ALWAYS_REPLY_IN_THREAD=true: DM root messages reply directly (no thread_ts)

All 54 Slack unit tests pass.

Manual testing

Tested on Pi5 (ARM64, Docker runtime) with a real Slack workspace:

  • ✅ DMs to the bot work after enabling the App Home checkbox
  • ✅ Message in channel root → bot replies in channel (default)
  • ✅ Reply in a thread → bot replies in the same thread
  • SLACK_ALWAYS_REPLY_IN_THREAD=true → root channel message starts a new thread
  • SLACK_ALWAYS_REPLY_IN_THREAD=true → threaded reply continues in the same thread
  • SLACK_ALWAYS_REPLY_IN_THREAD=true → DM replies directly (not threaded)

🤖 Generated with Claude Code

rvanmelle and others added 3 commits March 3, 2026 23:44
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>
@rvanmelle rvanmelle changed the title docs(add-slack): add missing App Home DM step and reinstall note feat(slack): thread-aware reply routing and App Home DM fix Mar 4, 2026
rvanmelle and others added 2 commits March 4, 2026 00:42
…=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>
@TomGranot
Copy link
Copy Markdown
Collaborator

Thread-aware reply routing for Slack is a nice addition. @gavrielc this touches the routing layer so would appreciate a sanity check.

@Andy-NanoClaw-AI Andy-NanoClaw-AI added PR: Feature New feature or enhancement Status: Needs Review Ready for maintainer review labels Mar 5, 2026
@Andy-NanoClaw-AI Andy-NanoClaw-AI added Status: Blocked Blocked by merge conflicts or dependencies Status: Needs Review Ready for maintainer review and removed Status: Needs Review Ready for maintainer review Status: Blocked Blocked by merge conflicts or dependencies labels Mar 14, 2026
@blozano-tt
Copy link
Copy Markdown

blozano-tt commented Mar 22, 2026

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.

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

Labels

PR: Feature New feature or enhancement Status: Blocked Blocked by merge conflicts or dependencies

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants