← Back to PRs

#19403: feat(slack): add dm.threadSession option for per-message thread sessions 🤖

by Vasiliy-Bondarenko open 2026-02-17 19:19 View on GitHub →
docs channel: slack gateway size: S
## Summary - **Problem:** All top-level Slack DMs route to the shared main session (`agent:main:main`), causing context cross-contamination between unrelated conversations - **Why it matters:** Users expect each DM conversation to be independent; instead, the bot "remembers" topics from completely unrelated messages because they share one session - **What changed:** New `channels.slack.dm.threadSession` config option. When `true`, each top-level DM creates its own thread session (`agent:main:main:thread:<ts>`), and the bot replies as a Slack thread. Follow-ups in that thread stay in the same isolated session. - **What did NOT change (scope boundary):** Channel messages, group DMs, existing `dmScope` routing, `replyToMode` semantics, thread reply behavior. Default is `false` — zero behavior change for existing users. ## Change Type (select all) - [ ] Bug fix - [x] Feature - [ ] Refactor - [x] Docs - [ ] Security hardening - [ ] Chore/infra ## Scope (select all touched areas) - [x] Gateway / orchestration - [ ] Skills / tool execution - [ ] Auth / tokens - [ ] Memory / storage - [x] Integrations - [ ] API / contracts - [ ] UI / DX - [ ] CI/CD / infra ## Linked Issue/PR - Closes N/A (no existing issue found) - Related: session routing, Slack threading ## User-visible / Behavior Changes - New config option: `channels.slack.dm.threadSession` (boolean, default `false`) - When enabled with `replyToMode` `"all"` or `"first"` for DMs: each new top-level DM gets a dedicated thread session; bot replies as a Slack thread - No change when disabled or when `replyToMode` is `"off"` ## Security Impact (required) - New permissions/capabilities? `No` - Secrets/tokens handling changed? `No` - New/changed network calls? `No` - Command/tool execution surface changed? `No` - Data access scope changed? `No` ## Repro + Verification ### Environment - OS: Windows 11 ARM (Parallels on macOS M1) - Runtime/container: Node.js 22.22.0 - Model/provider: Anthropic Claude Opus 4.6 - Integration/channel: Slack (Socket Mode) - Relevant config: ```json { "channels": { "slack": { "dm": { "replyToMode": "all", "threadSession": true } } } } ``` ### Steps 1. Enable `dm.threadSession: true` and `dm.replyToMode: "all"` in config 2. Send a top-level DM to the bot: "мы с тобой рыбу обсуждали в течение этой сессии?" 3. Observe the bot replies as a Slack thread with "Нет, рыбу мы не обсуждали. Это первое сообщение в этой сессии." 4. Send another unrelated top-level DM — it creates a separate thread with its own session ### Expected - Each top-level DM creates a new isolated thread session - No context leaks between threads ### Actual - ✅ Matches expected behavior ## Evidence - [x] Failing test/log before + passing after - [ ] Trace/log snippets - [ ] Screenshot/recording - [ ] Perf numbers 14 new unit tests covering: thread session routing for top-level DMs, opt-out when `false`/missing, thread reply behavior unchanged, `replyToMode` gating, `IsFirstThreadTurn` flag, `parentSessionKey` inheritance. Manual verification: live Slack DM confirmed isolated session — bot responded with no prior context. ## Human Verification (required) - **Verified scenarios:** Top-level DM creates thread session; follow-up replies stay in same thread; `threadSession: false` preserves old behavior; different `replyToMode` values - **Edge cases checked:** Thread replies (should not double-promote); `replyToMode: "off"` (should not activate); missing `dm` config block - **What you did not verify:** Multi-account Slack setups; MPIM (group DM) behavior with this flag; high-volume concurrent DMs ## Compatibility / Migration - Backward compatible? `Yes` — default `false`, no behavior change - Config/env changes? `Yes` — new optional field `channels.slack.dm.threadSession` - Migration needed? `No` ## Failure Recovery (if this breaks) - **How to disable/revert:** Set `channels.slack.dm.threadSession: false` (or remove the key) and restart gateway - **Files/config to restore:** `openclaw.json` — remove `threadSession` from `channels.slack.dm` - **Known bad symptoms:** DMs not creating threads when expected; or all DMs routing to main session (revert to pre-feature behavior, which is harmless) ## Risks and Mitigations - **Risk:** If `replyToMode` is not set to `"all"` or `"first"`, enabling `threadSession` has no effect (silent no-op, could confuse users) - **Mitigation:** Documented the requirement; code explicitly gates on `replyToMode` value --- > 🤖 This PR was developed with AI assistance. <!-- greptile_comment --> <h3>Greptile Summary</h3> This PR adds a `dm.threadSession` config option for Slack that routes each new top-level DM to its own isolated thread session, preventing context bleed between unrelated conversations. The implementation is well-scoped, backward-compatible (default `false`), and comes with 14 unit tests. **Key changes:** - `SlackDmConfig` in `types.slack.ts` and the Zod schema gain an optional `threadSession: boolean` field. - `prepare.ts` computes `shouldPromoteDmToThread` and uses `threadContext.messageTs` as the thread ID, routing the message to `agent:...:thread:<ts>`. - `IsFirstThreadTurn` is unconditionally set to `true` for promoted DMs (each has a unique `ts`, so it is always the first turn — acceptable but undocumented). - Docs in `slack.md` and `configuration-reference.md` are updated correctly. **Issues found:** - **Logic (dispatch mismatch):** `shouldPromoteDmToThread` is gated on `dmReplyToMode` (which respects `replyToModeByChatType.direct` and `dm.replyToMode`), but the dispatch layer in `dispatch.ts` drives Slack thread replies using `ctx.replyToMode` (the global setting only). If a user sets `replyToMode: "off"` globally and enables DM threading only via `replyToModeByChatType.direct: "all"`, the session is promoted to a thread session but the Slack reply is sent to the main channel — creating an orphaned session. The common path (`replyToMode: "all"` globally, which is also the default) works correctly. - **Style:** `IsFirstThreadTurn` for promoted DMs does not check for a pre-existing session the way the `isThreadReply` path does. - **Style:** A type cast in the test helper silently drops `"first"` from the `replyToMode` union passed to the context. <h3>Confidence Score: 3/5</h3> - Safe to merge for the common configuration (global `replyToMode: "all"`), but has a logic gap when `replyToModeByChatType.direct` or `dm.replyToMode` is used as the sole DM threading override. - The feature works correctly for the documented and most common configuration (`replyToMode: "all"` globally + `dm.threadSession: true`). However, there is a real logic mismatch: `shouldPromoteDmToThread` respects per-chat-type `replyToMode` overrides, but `dispatch.ts` uses only the global `ctx.replyToMode` to decide whether to actually reply as a Slack thread. A user who sets `replyToMode: "off"` globally and `replyToModeByChatType.direct: "all"` will get a thread session in storage but replies landing in the main channel — an inconsistent and confusing experience. Tests cover session-key routing but not the dispatch-layer threading behavior. - `src/slack/monitor/message-handler/prepare.ts` (lines 209-214) for the dispatch mismatch; `src/slack/monitor/message-handler/dispatch.ts` for where the fix would need to land. <sub>Last reviewed commit: 6b57f64</sub> <!-- greptile_other_comments_section --> <sub>(2/5) Greptile learns from your feedback when you react with thumbs up/down!</sub> <!-- /greptile_comment -->

Most Similar PRs