Skip to content

feat(slack): per-message thread for top-level posts in per-thread sessions#2472

Open
luisfontes wants to merge 1 commit into
nanocoai:channelsfrom
luxurypresence:upstream-pr/slack-dm-thread-per-message
Open

feat(slack): per-message thread for top-level posts in per-thread sessions#2472
luisfontes wants to merge 1 commit into
nanocoai:channelsfrom
luxurypresence:upstream-pr/slack-dm-thread-per-message

Conversation

@luisfontes
Copy link
Copy Markdown

@luisfontes luisfontes commented May 14, 2026

TL;DR

Before: a wiring with session_mode=per-thread on a Slack DM collapses every top-level DM into one ever-growing session, and the bot's replies post as flat top-level messages.
After: each top-level DM becomes its own session and the bot's reply posts in-thread.

(No change in session_mode=shared. Depends on #2471.)

Summary

Implements ChannelAdapter.rewriteThreadIdForSession on the Slack adapter. When a wiring is in session_mode=per-thread, top-level Slack posts (DMs and channel posts without a parent thread) each get their own thread/session instead of all collapsing under the same threadId. The reply posts in-thread.

Why

Slack's @chat-adapter/slack encodes top-level (un-threaded) messages with thread_ts="". For DMs that means every unsolicited message arrives with the same threadId — so a wiring with session_mode=per-thread on a DM degenerates to a single ever-growing session, and outbound replies post as flat channel messages (because postMessage reads thread_ts from the threadId and empty means "post at top level").

The rewriter re-encodes the threadId with the message's own ts as thread_ts. Each top-level post becomes its own thread/session AND Slack auto-creates the thread on first reply. In-thread events already carry a real thread_ts, so decoded.threadTs is non-empty and the rewrite no-ops.

Why no toggle

The router only consults this hook when the wiring's effective session_mode is per-thread (see #2471 against main). shared / agent-shared users see no change. Operators who explicitly opt into per-thread DMs get the correct behavior without any new config flag.

Depends on

Files

  • src/channels/slack.ts — attaches bridge.rewriteThreadIdForSession.

Test plan

  • Verify with session_mode=per-thread DM wiring: top-level DM sends create distinct sessions; bot replies post in-thread.
  • Verify with session_mode=shared DM wiring: no change from today's behavior (hook not consulted).
  • Verify in-thread DM events still resolve to the same thread (decoded.threadTs non-empty → no-op).

🤖 Generated with Claude Code

…sions

Implement ChannelAdapter.rewriteThreadIdForSession on the Slack
adapter. Slack's chat-sdk adapter encodes top-level (un-threaded)
messages with an empty thread_ts — for DMs this means every
unsolicited message arrives with the same threadId, so a wiring with
`session_mode=per-thread` degenerates to a single ever-growing
session and outbound replies post as flat channel messages instead of
in-thread (postMessage decodes thread_ts from the threadId, and an
empty thread_ts means "post at top level").

The rewriter mints a per-message threadId by re-encoding the channel
with the message's own ts as thread_ts. Each new top-level post
becomes its own thread/session AND the reply posts in-thread because
Slack auto-creates the thread on the first reply. In-thread events
already carry a real thread_ts (decoded.threadTs is non-empty) so the
rewrite no-ops.

The router only consults this hook when the wiring's effective
session_mode is per-thread (see ChannelAdapter.rewriteThreadIdForSession
docs upstream of this branch). In shared / agent-shared mode the
threadId doesn't gate the session, so the rewriter wouldn't help and
isn't called.

Note for review: depends on the host-side hook landing in main first
(adapter interface + router integration). Once main is merged forward
into channels, this PR rebases trivially.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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