fix(slack): seed thread routing for implicit-conversation channels (#78505)#78522
Conversation
|
Codex review: needs changes before merge. Summary Reproducibility: yes. at source level. Current main leaves implicit Real behavior proof Next step before merge Security Review findings
Review detailsBest possible solution: Add the active-release changelog entry, then merge the focused Slack routing fix after exact-head validation passes. Do we have a high-confidence way to reproduce the issue? Yes, at source level. Current main leaves implicit Is this the best way to solve the issue? Yes, with one process fix. Extending the existing prepare-level seed gate is the narrowest maintainable fix for this Slack session split, but the PR still needs the required changelog entry. Full review comments:
Overall correctness: patch is correct Acceptance criteria:
What I checked:
Likely related people:
Remaining risk / open question:
Codex review notes: model gpt-5.5, reasoning high; reviewed against 4721ca8e459f. Re-review progress:
|
When a Slack channel has `requireMention: false` and a non-`off` reply mode, every top-level bot reply creates a Slack thread (because `replyToMode` does). Without seeding the inbound root, the root turn landed on the channel session while later thread replies landed on a fresh `:thread:<root_ts>` session, breaking conversational continuity. Extend `seedTopLevelRoomThreadBySource` to also fire for those channels, mirroring how `app_mention` / `explicitlyMentioned` roots already get seeded. The thread session key is now consistent on both sides of the turn, so follow-up thread messages route back to the originating session. Fixes openclaw#78505
179a0d8 to
7ed9ed5
Compare
|
@zeroth-blip thanks for catching this, reviewed and this is the right fix. Il push a separate docs + changelog fix as the docs need to catch up. The current docs say channel sessions are agent: |
…penclaw#78522) When a Slack channel has `requireMention: false` and a non-`off` reply mode, every top-level bot reply creates a Slack thread (because `replyToMode` does). Without seeding the inbound root, the root turn landed on the channel session while later thread replies landed on a fresh `:thread:<root_ts>` session, breaking conversational continuity. Extend `seedTopLevelRoomThreadBySource` to also fire for those channels, mirroring how `app_mention` / `explicitlyMentioned` roots already get seeded. The thread session key is now consistent on both sides of the turn, so follow-up thread messages route back to the originating session. Fixes openclaw#78505
…penclaw#78522) When a Slack channel has `requireMention: false` and a non-`off` reply mode, every top-level bot reply creates a Slack thread (because `replyToMode` does). Without seeding the inbound root, the root turn landed on the channel session while later thread replies landed on a fresh `:thread:<root_ts>` session, breaking conversational continuity. Extend `seedTopLevelRoomThreadBySource` to also fire for those channels, mirroring how `app_mention` / `explicitlyMentioned` roots already get seeded. The thread session key is now consistent on both sides of the turn, so follow-up thread messages route back to the originating session. Fixes openclaw#78505
Summary
requireMention: false, the inbound root turn landed on the channel session (agent:main:slack:channel:<id>) while later replies inside the bot-created thread landed on a fresh:thread:<root_ts>session, splitting one logical conversation across two OpenClaw sessions.seedTopLevelRoomThreadBySourceinprepareSlackMessageto also fire when the channel will implicitly produce a Slack thread anyway (isRoom+requireMention: false+replyToMode != "off"). The root and its thread replies now share the same:thread:<root_ts>session key, the same wayapp_mention/ explicit-mention roots already do.requireMention: truechannels,replyToMode: "off"channels, app_mention / explicit-mention paths, regex mention paths, runtime conversation bindings. No new config field, no migration. Outbound routing precedence (replyToIdvsthreadId) is also intentionally untouched and tracked separately.Change Type (select all)
Scope (select all touched areas)
Linked Issue/PR
Real behavior proof (required for external PRs)
Behavior or issue addressed: Slack thread replies in
requireMention: falsechannels split off into a separate OpenClaw session.Real environment tested: OpenClaw
2026.5.5(production install on macOS 26.3.1 / arm64), Slack Socket Mode, channelC0AGG76CP1SwithrequireMention: false,channels.slack.replyToMode = "first",messages.groupChat.visibleReplies = "automatic". Local hotfix (functionally identical to this patch) applied directly to the bundleddist/prepare-CD7Ym2Zf.jsand the gateway restarted viaopenclaw gateway restart.Exact steps or command run after this patch:
#genai(no@mention).openclaw statusand the sessions list snapshot.Evidence after fix (live terminal output, copied verbatim from the running gateway):
Gateway health after applying the source change and restarting:
Live Slack sessions for channel
C0AGG76CP1S(#genai) after the fix, captured via the OpenClaw runtime sessions API and copied verbatim:Notably, no bare
agent:main:slack:channel:c0agg76cp1ssession is created for new top-level turns anymore — both root and thread reply land on the same:thread:<root_ts>key.Redacted runtime log excerpt from the same machine after the patch (gateway pid 5029, Slack Socket Mode connected):
#genaichannel routes to a:thread:<root_ts>-scoped OpenClaw session from the very first turn. Follow-up replies inside the same Slack thread resolve to the same session key as the root (no more channel-vs-thread split). Verified live viaopenclaw status(gateway healthy, pid 5029) and the sessions list snapshot (terminal output above), captured immediately after restarting the gateway with the patched build. No new bareagent:main:slack:channel:c0agg76cp1ssession was created for any new top-level turn during the verification window.replyToMode = "batched"channels (logic identical, just no live channel handy); ACP-bound conversations (covered by existing tests).agent:main:slack:channel:c0agg76cp1ssession for the root + a freshagent:main:slack:channel:c0agg76cp1s:thread:<root_ts>session for every later in-thread message (live observation captured in Slack: outbound thread reply does not promote the session key to the thread, so the next inbound thread message lands in a fresh session #78505).Root Cause (if applicable)
prepareSlackMessagecomputedseedTopLevelRoomThreadBySourcefromapp_mention | wasMentioned | explicitlyMentioned. Channels configured withrequireMention: falseaccept implicit prompts (no@mentionrequired), but the seeding rule did not include that case. The root turn therefore did not seed a thread session, while the user's later in-thread replies — being actualthread_tsreplies — did get a thread-scoped session, producing the asymmetry reported in Slack: outbound thread reply does not promote the session key to the thread, so the next inbound thread message lands in a fresh session #78505.requireMention: false+ non-offreplyToModepath throughprepareSlackMessageend-to-end. Existing thread-session-key tests at the routing level only fedseedTopLevelRoomThreaddirectly, bypassing the gate where the regression actually lives.Regression Test Plan (if applicable)
extensions/slack/src/monitor/message-handler/prepare.test.tsrequireMention: falsechannel turn withreplyToMode: "first"and no@mentionmust produce a:thread:<root_ts>session key. A subsequentthread_ts-bearing reply must resolve to the same session key.prepareSlackMessageseeding gate, so only an end-to-endprepareSlackMessageassertion (not a pure routing-layer one) actually pins it. Reverting the source change locally makes the new test fail with the exact mismatch from Slack: outbound thread reply does not promote the session key to the thread, so the next inbound thread message lands in a fresh session #78505 (agent:main:slack:channel:c0agg76cp1svs…:thread:1778073105.769279), confirming the test catches the regression.app_mentionpath.User-visible / Behavior Changes
For Slack channels with
requireMention: falseand a non-offreplyToMode, top-level inbound messages now route to a thread-scoped session (agent:<agent>:slack:channel:<id>:thread:<root_ts>) from the first turn, instead of the bare channel session. This matches what those channels already do forapp_mention/ explicit-mention roots, and matches the visible Slack behavior (every top-level bot reply already lives inside a Slack thread on those channels).Channels with
requireMention: true, channels withreplyToMode: "off", DMs, and group DMs are unchanged.Diagram (if applicable)
Security Impact (required)
NoNoNoNoNoRepro + Verification
Environment
2026.5.5, Node 25.8.2messages.groupChat.visibleReplies: "automatic"channels.slack.replyToMode: "first"channels.slack.replyToModeByChatType: { direct: "off", group: "first", channel: "first" }channels.slack.thread: { historyScope: "thread", inheritParent: false }channels.slack.channels.<channelId>: { enabled: true, requireMention: false }Steps
@bot).openclaw status/ sessions list for the channel.Expected
Actual (with fix)
agent:main:slack:channel:<id>:thread:<root_ts>.Evidence
Human Verification (required)
requireMention: falsechannel — both turns now share one:thread:<root_ts>session.app_mentionand explicit-mention routes — still consolidate as before (existing tests).replyToMode: "off"channels — gate stays disabled (existing testkeeps top-level channel turns in one session when replyToMode=offstill passes).requireMention: truechannels — gate stays disabled.replyToMode: "batched"(logic-equivalent, covered by unit tests).Review Conversations
Compatibility / Migration
YesNoNoRisks and Mitigations
agent:<id>:slack:channel:<id>session for cross-thread continuity in arequireMention: falsechannel will now route per-thread.app_mentionchannels post-Fix Slack inbound thread session routing #72498, and the existing pre-Fix Slack inbound thread session routing #72498 behavior was already inconsistent with how the bot visibly replies (inside a thread). No persisted state is migrated; existing sessions are not renamed or merged. Users wanting channel-scoped sessions can setreplyToMode: "off".