You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
OpenClaw Bug Report: Dual Session Processing on Slack Channels with replyToMode: "all"
Product: OpenClaw Version tested: 2026.4.5 (source code analysis), 2026.4.2 (live reproduction) Severity: High (causes 2x–4x token spend, context pollution across senders) Date: 2026-04-09
1. Summary
When replyToMode: "all" is configured on a Slack channel (not a DM), every top-level channel message is processed by the LLM twice — once in a per-message thread-scoped session (used for threaded reply delivery) and once in the shared parent channel session. The parent channel session accumulates ALL messages from ALL senders into a single growing context, causing:
Massive token waste — every message triggers two independent LLM calls with completely different response IDs
Cross-sender context pollution — all senders' conversations bleed into the parent session
Unbounded cost growth — the parent session context grows with every message across all threads
Configure a Slack channel with allow: true, requireMention: false, and global replyToMode: "all"
Have User A post a top-level message in the channel
The agent replies in a thread under User A's message (correct behavior)
Have User B post a different top-level message in the same channel
The agent replies in a thread under User B's message (correct behavior)
Observe: Both User A's and User B's messages are in the parent channel session, and the LLM was called independently for both the thread session AND the parent session for each message
Observed behavior on April 8, 2026
10 top-level messages from 4 different senders (Janice, Renea, Kevin, Edward) in #fox-email over ~5 hours:
#
Sender
message_ts
Thread Session ID
Thread Cost
Parent Session Cost
1
Janice
1775660310.007009
3da7b8e2
$1.11
Included in $18.46
2
Renea
1775679025.746549
805dc0f3
$0.29
Included in $18.46
3
Renea
1775679068.095299
e1fddd17
$0.68
Included in $18.46
4
Janice
1775686608.909049
668e5247
$0.23
Included in $18.46
5
Kevin
1775689714.554029
df14ba11
$1.77
Included in $18.46
6
Kevin
1775691340.591129
25502d5b
$0.33
Included in $18.46
7
Renea
1775694933.768589
e328f4e8
$0.34
Included in $18.46
8
Renea
1775695293.271419
1988fe0b
$0.18
Included in $18.46
9
Edward
1775699200.009569
221e12bd
$0.00
Included in $18.46
10
Edward
1775710479.753369
9e0f9089
$0.00
Included in $18.46
Thread sessions total: $4.93 (correct, isolated per-message) Parent session total: $18.46 (82 LLM turns, all messages accumulated) Response ID overlap: 0 out of 82+94 — every response is a unique, independent LLM call Effective cost multiplier: ~4.7x ($23.39 actual vs ~$4.93 expected)
3. Session Store Evidence
The sessions.json file confirms the dual routing. Each message creates BOTH:
The thread context function: resolveSlackThreadContext() (line ~540)
functionresolveSlackThreadContext(params){constincomingThreadTs=params.message.thread_ts;// undefined (top-level)consteventTs=params.message.event_ts;constmessageTs=params.message.ts??eventTs;// "1775660310.007009"constisThreadReply=typeofincomingThreadTs==="string"&&incomingThreadTs.length>0&&(incomingThreadTs!==messageTs||Boolean(params.message.parent_user_id));// → false (no thread_ts)return{
incomingThreadTs,// undefined
messageTs,// "1775660310.007009"
isThreadReply,// falsereplyToId: incomingThreadTs??messageTs,// "1775660310.007009"// messageThreadId IS set correctly for reply delivery:messageThreadId: isThreadReply
? incomingThreadTs
: params.replyToMode==="all" ? messageTs : void0// → "1775660310.007009"};}
The session key function: resolveThreadSessionKeys() (session-key-BR3Z-ljs.js, line ~255)
functionresolveThreadSessionKeys(params){constthreadId=(params.threadId??"").trim();// When threadId is empty (void 0 from canonicalThreadId):if(!threadId)return{sessionKey: params.baseSessionKey,// → parent channel session keyparentSessionKey: void0};// When threadId is provided (non-room path):constnormalizedThreadId=(params.normalizeThreadId??((value)=>value.toLowerCase()))(threadId);return{sessionKey: `${params.baseSessionKey}:thread:${normalizedThreadId}`,parentSessionKey: params.parentSessionKey};}
The SessionKey field routes the inbound message processing to the parent channel session. The MessageThreadId field is used later in the delivery/reply layer to create a separate thread-scoped session for reply targeting.
Both sessions process the message independently with the LLM.
5. The Asymmetry Between Rooms and DMs
The code has an explicit asymmetry in the canonicalThreadId ternary:
constcanonicalThreadId=isRoomish
? (isThreadReply&&threadTs ? threadTs : void0)// ROOMS: only uses threadTs for actual thread replies
: (isThreadReply ? threadTs : autoThreadId);// DMs: uses autoThreadId for top-level messages
For DMs with replyToMode: "all": Top-level messages correctly get canonicalThreadId = autoThreadId = messageTs, so sessionKey = baseKey:thread:<messageTs> — each message gets its own session.
For rooms/channels with replyToMode: "all": Top-level messages get canonicalThreadId = void 0, so sessionKey = baseKey — ALL messages share the parent channel session.
This asymmetry is the bug. The DM path was fixed in version 2026.2.25 (PR #26849: "when replyToMode="all" auto-threads top-level Slack DMs, seed the thread session key from the message ts"). The equivalent fix was never applied to the room/channel path.
6. Relevant Changelog Entries
Version 2026.2.25 (DM fix — applied, but only for DMs):
Slack/Threading: when replyToMode="all" auto-threads top-level Slack DMs, seed the thread session key from the message ts so the initial message and later replies share the same isolated :thread: session instead of falling back to base DM context. (#26849)
Version 2026.2.27 (Thread isolation — applied, but doesn't cover this case):
Slack/Thread session isolation: route channel/group top-level messages into thread-scoped sessions (:thread:<ts>) and read inbound previousTimestamp from the resolved thread session key, preventing cross-thread context bleed and stale timestamp lookups. (#10686)
Note: Despite the description saying "route channel/group top-level messages into thread-scoped sessions," the actual code still has void 0 for rooms in the ternary. Either this fix was incomplete, regressed, or the description doesn't accurately reflect the code's behavior.
Version 2026.3.2 (replyToMode=off fix — different case):
Slack/session routing: keep top-level channel messages in one shared session when replyToMode=off, while preserving thread-scoped keys for true thread replies and non-off modes. (#32193)
This fix is specifically for replyToMode=off. The phrase "preserving thread-scoped keys for true thread replies and non-off modes" suggests thread-scoped keys should work for replyToMode=all on channels — but the code doesn't implement this for top-level (non-thread-reply) room messages.
7. Proposed Fix
In resolveSlackRoutingContext(), change the canonicalThreadId computation to use autoThreadId for rooms:
// Current (buggy):constcanonicalThreadId=isRoomish
? (isThreadReply&&threadTs ? threadTs : void0)
: (isThreadReply ? threadTs : autoThreadId);// Proposed fix:constcanonicalThreadId=isRoomish
? (isThreadReply&&threadTs ? threadTs : autoThreadId)// ← use autoThreadId
: (isThreadReply ? threadTs : autoThreadId);// Or simplified (since both branches are now identical):constcanonicalThreadId=isThreadReply&&threadTs ? threadTs : autoThreadId;
This ensures that when replyToMode: "all" is active on a channel, each top-level message gets its own thread-scoped session key (baseKey:thread:<messageTs>) instead of falling back to the parent channel session.
Impact of fix: The inbound processing session key will match the thread-scoped session that's already being created for reply delivery, eliminating the duplicate processing.
8. Workaround
No clean workaround exists. Options:
Set replyToMode: "off" on the channel — but this means the agent won't reply in threads, which defeats the purpose for multi-sender channels
Set requireMention: true — reduces message volume but doesn't fix the dual-session routing
Accept the cost — the parent session will keep growing until reset
Showing the parent session (7f7593bb) accumulating messages from multiple senders:
1. [user] Slack message from Janice Pena — asking about Travel Nursing salary data
2. [assistant] cost=$0.88 — processes Janice's salary question (with thinking)
3-15. [tool calls] — reads spreadsheet, processes salary data, sends Slack reply
16. [assistant] cost=$0.08 — NO_REPLY
17. [user] Slack message from Renea Nielsen — asking for compliance review of email copy
18. [assistant] cost=$0.76 — processes Renea's compliance request (with thinking)
19-32. [tool calls] — searches memory, reads compliance KB, sends compliance review
33. [user] QUEUED messages: more from Renea (different message)
34-36. [assistant+tools] — processes queued Renea message
37. [user] Slack edit notification
38. [assistant] cost=$0.79 — processes edit
39. [user] Slack delete + Janice's new message
40. [assistant] cost=$0.80 — Janice asking about what Fox has learned from her
... continues with Kevin's messages, Edward's messages, etc.
All 10 senders' conversations are interleaved in this single session. Each successive message pays for the full accumulated context of all prior messages.
14. Cost Breakdown
Thread sessions (correct, isolated):
Session ID
Sender(s)
Lines
Cost
3da7b8e2
Janice
60
$1.11
805dc0f3
Renea
16
$0.29
e1fddd17
Renea
43
$0.68
668e5247
Janice
10
$0.23
df14ba11
Kevin
105
$1.77
25502d5b
Kevin
--
$0.33
e328f4e8
Renea
--
$0.34
1988fe0b
Renea
--
$0.18
221e12bd
Edward
--
$0.00
9e0f9089
Edward
--
$0.00
Total
$4.93
Parent session (buggy accumulator):
Session ID
All senders combined
Lines
Cost
7f7593bb
Janice+Renea+Kevin+Edward
179
$18.46
Totals:
Actual spend: $23.39
Expected spend (thread sessions only): $4.93
Waste: $18.46 (79% of total)
Multiplier: 4.7x
15. Questions for the OpenClaw Team
Was the isRoomish branch in the canonicalThreadId ternary intentionally different from the DM branch? If so, what's the expected behavior for replyToMode: "all" on channels?
Is there a second code path that creates the thread-scoped session from MessageThreadId that we haven't identified? Understanding both paths would help validate the fix.
Would the proposed fix (using autoThreadId for rooms) break any intentional behavior where channels with replyToMode: "all" are expected to maintain a shared parent context?
OpenClaw Bug Report: Dual Session Processing on Slack Channels with
replyToMode: "all"Product: OpenClaw
Version tested: 2026.4.5 (source code analysis), 2026.4.2 (live reproduction)
Severity: High (causes 2x–4x token spend, context pollution across senders)
Date: 2026-04-09
1. Summary
When
replyToMode: "all"is configured on a Slack channel (not a DM), every top-level channel message is processed by the LLM twice — once in a per-message thread-scoped session (used for threaded reply delivery) and once in the shared parent channel session. The parent channel session accumulates ALL messages from ALL senders into a single growing context, causing:2. Reproduction
Config (Fox agent, OpenClaw 4.2)
{ "channels": { "slack": { "replyToMode": "all", "channels": { "C0ANBM0SJKF": { "allow": true, "requireMention": false } } } } }Steps
allow: true,requireMention: false, and globalreplyToMode: "all"Observed behavior on April 8, 2026
10 top-level messages from 4 different senders (Janice, Renea, Kevin, Edward) in
#fox-emailover ~5 hours:Thread sessions total: $4.93 (correct, isolated per-message)
Parent session total: $18.46 (82 LLM turns, all messages accumulated)
Response ID overlap: 0 out of 82+94 — every response is a unique, independent LLM call
Effective cost multiplier: ~4.7x ($23.39 actual vs ~$4.93 expected)
3. Session Store Evidence
The
sessions.jsonfile confirms the dual routing. Each message creates BOTH:A. A thread-scoped session key (correct):
B. The parent channel session key (all messages route here too):
Session file
7f7593bb-...-topic-1775660310.007009.jsonlcontains all 10 senders' messages (179 lines, 82 LLM turns, $18.46).4. Root Cause (Source Code Analysis)
File:
dist/prepare-D5Swazfl.js(maps toextensions/slack/src/routing.tsor similar)The routing function:
resolveSlackRoutingContext()(line ~920)The thread context function:
resolveSlackThreadContext()(line ~540)The session key function:
resolveThreadSessionKeys()(session-key-BR3Z-ljs.js, line ~255)The message construction (line ~1232):
What happens next
The
SessionKeyfield routes the inbound message processing to the parent channel session. TheMessageThreadIdfield is used later in the delivery/reply layer to create a separate thread-scoped session for reply targeting.Both sessions process the message independently with the LLM.
5. The Asymmetry Between Rooms and DMs
The code has an explicit asymmetry in the
canonicalThreadIdternary:For DMs with
replyToMode: "all": Top-level messages correctly getcanonicalThreadId = autoThreadId = messageTs, sosessionKey=baseKey:thread:<messageTs>— each message gets its own session.For rooms/channels with
replyToMode: "all": Top-level messages getcanonicalThreadId = void 0, sosessionKey=baseKey— ALL messages share the parent channel session.This asymmetry is the bug. The DM path was fixed in version 2026.2.25 (PR #26849: "when
replyToMode="all"auto-threads top-level Slack DMs, seed the thread session key from the messagets"). The equivalent fix was never applied to the room/channel path.6. Relevant Changelog Entries
Version 2026.2.25 (DM fix — applied, but only for DMs):
Version 2026.2.27 (Thread isolation — applied, but doesn't cover this case):
Note: Despite the description saying "route channel/group top-level messages into thread-scoped sessions," the actual code still has
void 0for rooms in the ternary. Either this fix was incomplete, regressed, or the description doesn't accurately reflect the code's behavior.Version 2026.3.2 (replyToMode=off fix — different case):
This fix is specifically for
replyToMode=off. The phrase "preserving thread-scoped keys for true thread replies and non-off modes" suggests thread-scoped keys should work forreplyToMode=allon channels — but the code doesn't implement this for top-level (non-thread-reply) room messages.7. Proposed Fix
In
resolveSlackRoutingContext(), change thecanonicalThreadIdcomputation to useautoThreadIdfor rooms:This ensures that when
replyToMode: "all"is active on a channel, each top-level message gets its own thread-scoped session key (baseKey:thread:<messageTs>) instead of falling back to the parent channel session.Impact of fix: The inbound processing session key will match the thread-scoped session that's already being created for reply delivery, eliminating the duplicate processing.
8. Workaround
No clean workaround exists. Options:
replyToMode: "off"on the channel — but this means the agent won't reply in threads, which defeats the purpose for multi-sender channelsrequireMention: true— reduces message volume but doesn't fix the dual-session routing9. Files Referenced
dist/prepare-D5Swazfl.jsdist/session-key-BR3Z-ljs.jsCHANGELOG.mdsessions/sessions.json(Fox)sessions/7f7593bb-...-topic-1775660310.007009.jsonl(Fox)sessions/3da7b8e2-...jsonlthrough9e0f9089-...jsonl(Fox)10. Full Source Code Excerpts
resolveSlackThreadContext(prepare-D5Swazfl.js, line 538)resolveSlackThreadTargets(prepare-D5Swazfl.js, line ~555)resolveSlackRoutingContext(prepare-D5Swazfl.js, line 908)resolveThreadSessionKeys(session-key-BR3Z-ljs.js, line 255)Message construction (prepare-D5Swazfl.js, line ~1215)
11. Fox Slack Config (Sanitized)
{ "mode": "socket", "enabled": true, "replyToMode": "all", "groupPolicy": "allowlist", "dmPolicy": "allowlist", "streaming": "block", "nativeStreaming": true, "allowBots": true, "channels": { "C0ANBM0SJKF": { "allow": true, "requireMention": false } } }Note:
threadInheritParentis not set (defaults to false/undefined).threadHistoryScopeis not set.12. Session Store Snapshot (Fox,
sessions.jsonexcerpt)All entries for channel
c0anbm0sjkf(#fox-email):{ "agent:main:slack:channel:c0anbm0sjkf": { "sessionId": "7f7593bb-c78d-4677-83a2-afb7853d7ed7", "updatedAt": 1775710558469, "displayName": "slack:#fox-email" }, "agent:main:slack:channel:c0anbm0sjkf:thread:1775660310.007009": { "sessionId": "3da7b8e2-3956-4ef4-b603-039704dcd35b", "updatedAt": 1775696077967, "displayName": "slack:#fox-email" }, "agent:main:slack:channel:c0anbm0sjkf:thread:1775679025.746549": { "sessionId": "805dc0f3-4825-42ff-b2a4-3b1de1ea5923", "updatedAt": 1775695384636, "displayName": "slack:#fox-email" }, "agent:main:slack:channel:c0anbm0sjkf:thread:1775679068.095299": { "sessionId": "e1fddd17-6187-494e-b556-8f9e17b77e6a", "updatedAt": 1775695715853, "displayName": "slack:#fox-email" }, "agent:main:slack:channel:c0anbm0sjkf:thread:1775686608.909049": { "sessionId": "668e5247-5c24-4356-ad88-b1f0992739aa", "updatedAt": 1775687127587, "displayName": "slack:#fox-email" }, "agent:main:slack:channel:c0anbm0sjkf:thread:1775689714.554029": { "sessionId": "df14ba11-6a7d-4993-bcea-34db89f1e998", "updatedAt": 1775695753084, "displayName": "slack:#fox-email" }, "agent:main:slack:channel:c0anbm0sjkf:thread:1775691340.591129": { "sessionId": "25502d5b-a259-434b-995a-bba4bcc06071", "updatedAt": 1775719657511, "displayName": "slack:#fox-email" }, "agent:main:slack:channel:c0anbm0sjkf:thread:1775694933.768589": { "sessionId": "e328f4e8-773d-4e62-9d72-ae2d7d0e7327", "updatedAt": 1775695230953, "displayName": "slack:#fox-email" }, "agent:main:slack:channel:c0anbm0sjkf:thread:1775695293.271419": { "sessionId": "1988fe0b-d82b-40bf-b905-d4c7b8cf418f", "updatedAt": 1775696477892, "displayName": "Slack thread #fox-email: <@U0AEHL4S6TC> Tell me what you've learned..." }, "agent:main:slack:channel:c0anbm0sjkf:thread:1775699200.009569": { "sessionId": "221e12bd-2aee-4134-8a25-7e3986b0783c", "updatedAt": 1775699332990, "displayName": "slack:#fox-email" }, "agent:main:slack:channel:c0anbm0sjkf:thread:1775710479.753369": { "sessionId": "9e0f9089-26a6-4592-8d12-21badb6a6821", "updatedAt": 1775710517133, "displayName": "slack:g-c0anbm0sjkf" } }Key observation: Each message has its own
:thread:session key AND all messages also route to the baseagent:main:slack:channel:c0anbm0sjkfsession.13. Parent Session Message Flow (First 20 entries)
Showing the parent session (
7f7593bb) accumulating messages from multiple senders:All 10 senders' conversations are interleaved in this single session. Each successive message pays for the full accumulated context of all prior messages.
14. Cost Breakdown
Thread sessions (correct, isolated):
Parent session (buggy accumulator):
Totals:
15. Questions for the OpenClaw Team
Was the
isRoomishbranch in thecanonicalThreadIdternary intentionally different from the DM branch? If so, what's the expected behavior forreplyToMode: "all"on channels?The changelog for version 2026.2.27 (fix(slack): use thread-level sessions for channels to prevent context mixing #10686) says "route channel/group top-level messages into thread-scoped sessions." Was this fix intended to cover this case? If so, it appears to have regressed or was incomplete.
Is there a second code path that creates the thread-scoped session from
MessageThreadIdthat we haven't identified? Understanding both paths would help validate the fix.Would the proposed fix (using
autoThreadIdfor rooms) break any intentional behavior where channels withreplyToMode: "all"are expected to maintain a shared parent context?