openclaw - ✅(Solved) Fix [Bug]: Slack Assistant API: message_changed events dropped due to bot self-filter in Bolt middleware [2 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#55445Fetched 2026-04-08 01:39:27
View on GitHub
Comments
0
Participants
1
Timeline
7
Reactions
0
Participants
Timeline (top)
cross-referenced ×3labeled ×2referenced ×2

Summary When using Slack's Assistant API (DM threads with a bot), user messages are frequently delivered as message_changed events with the bot's own user ID as the sender. These events are silently dropped by the Bolt self-message filter at line 575 of runtime-api-D-RDIrc9.js, causing approximately 50% of user messages to never reach the agent. Environment

OpenClaw version: 2026.3.24 (also reproduced on 2026.3.23-2) Channel: Slack (socket mode) OS: macOS (Mac Studio M3 Ultra) Model provider: vLLM (local, via proxy on localhost:8080)

Reproduction

Configure OpenClaw with Slack in socket mode Send a DM to the bot in Slack Observe that some messages work and some silently time out with "LLM request timed out" In verbose gateway logs, dropped messages appear as:

slack: drop message_changed sender U0ADE8G5DL5 channel=D0ADHLY5WJE reason=sender-not-allowlisted

Messages that DO work appear as normal message events routed to peer=direct:U0ABVDL7C9M (real user ID) Messages that FAIL arrive as message_changed events routed to peer=direct:U0ADE8G5DL5 (bot's own user ID)

Root Cause Analysis There are two layers of filtering that block these events: Layer 1: Bolt self-message filter (line 575 of runtime-api) javascriptif (botUserId !== void 0 && "user" in args.event && args.event.user === botUserId && !["member_joined_channel", "member_left_channel"].includes(args.event.type)) return; This filter silently drops ANY event where event.user === botUserId. When Slack's assistant API wraps a user message as a message_changed event, the top level event.user is set to the bot's user ID, not the actual human user. The filter kills the event before it ever reaches the message_changed handler. Layer 2: sender-not-allowlisted filter (line 17585 of runtime-api) Even after patching Layer 1 to allow message type events through, the resolveSenderId function for message_changed events (line 18425) resolves the sender incorrectly: javascriptresolveSenderId: (event) => { const changed = event; return changed.message?.user ?? changed.previous_message?.user ?? changed.message?.bot_id ?? changed.previous_message?.bot_id; } For assistant API events, changed.message.user is the bot's user ID, so the resolved sender is the bot itself. This causes the event to be routed as peer=direct:<botUserId>, which is then either dropped by the allowlist filter or silently ignored as a self-message. Confirmation via Slack API curl -s https://slack.com/api/auth.test -H "Authorization: Bearer xoxb-..."

{ "ok": true, "user": "alfred", "user_id": "U0ADE8G5DL5", <-- this is the sender ID on dropped messages "bot_id": "B0ADWJRU9BK" } The dropped message_changed events are attributed to U0ADE8G5DL5 which is the bot itself, confirmed by auth.test. Diagnostic Evidence Verbose gateway log showing a WORKING message: 2026-03-26T10:14:43.915-07:00 [routing] resolveAgentRoute: channel=slack accountId=default peer=direct:U0ABVDL7C9M guildId=none teamId=T0AC8R91SMC bindings=0 2026-03-26T10:14:44.056-07:00 slack inbound: channel=D0ADHLY5WJE from=slack:U0ABVDL7C9M preview="What model are you running" 2026-03-26T10:14:44.062-07:00 slack stream preview ready (maxChars=8000, throttleMs=1000) ... 2026-03-26T10:15:22.606-07:00 slack: delivered 1 reply to user:U0ABVDL7C9M Verbose gateway log showing a DROPPED message: 2026-03-26T10:20:07.476-07:00 slack: drop message_changed sender U0ADE8G5DL5 channel=D0ADHLY5WJE reason=sender-not-allowlisted No routing, no inbound, no agent run. The message is killed at the filter stage. After patching Layer 1 only (allowing message type through Bolt filter): 2026-03-26T17:40:40.912-07:00 [routing] resolveAgentRoute: channel=slack accountId=default peer=direct:U0ADE8G5DL5 guildId=none teamId=T0AC8R91SMC bindings=0 The event passes the Bolt filter but is now routed as the bot talking to itself (peer=direct:U0ADE8G5DL5), and no agent run is triggered. Patches Attempted I attempted two patches, neither fully resolves the issue: Patch 1: Allow message_changed through Bolt self-filter javascript// Line 575: Added "message" to the exclusion list if (botUserId !== void 0 && "user" in args.event && args.event.user === botUserId && !["member_joined_channel", "member_left_channel", "message"].includes(args.event.type)) return; Result: Event passes Layer 1 but is routed with bot's ID as the peer, so no agent run triggers. Patch 2: Allow message_changed through allowlist filter javascript// Line 18356: Skip drop for message_changed events if (!auth.allowed && eventKind !== "message_changed") { Result: Combined with Patch 1, the event reaches routing but resolveSenderId still returns the bot's user ID, so it is routed as peer=direct:<botUserId>. Suggested Fix The resolveSenderId for message_changed events needs to extract the real human user from deeper in the event payload. In Slack's assistant API, the actual user ID may be in:

event.message.blocks[].elements[].user_id event.message.metadata.event_payload.user The channel context (it is a DM, so the non-bot participant is the user)

Additionally, the Bolt self-filter at line 575 should not apply to message_changed events where the subtype indicates it is an assistant API interaction, or alternatively, it should look at the resolved sender from the subtype handler rather than the top level event.user. A raw event dump of the message_changed payload from the Slack assistant API would help determine exactly where the real user ID lives. My debug logging patch at the resolveSenderId function never fired because the event was killed at the Bolt layer (line 575) before reaching it. Impact Approximately 50% of user messages in Slack DMs are silently dropped. The behavior is intermittent because Slack alternates between delivering messages as normal message events (which work) and message_changed events (which are dropped). There is no error message or indication in the standard gateway logs that messages are being dropped. Only verbose mode (--verbose) reveals the drop message_changed entries. Workaround None found. CLI and web interfaces work correctly as they do not go through the Slack socket mode path.

Error Message

Approximately 50% of user messages in Slack DMs are silently dropped. The behavior is intermittent because Slack alternates between delivering messages as normal message events (which work) and message_changed events (which are dropped). There is no error message or indication in the standard gateway logs that messages are being dropped. Only verbose mode (--verbose) reveals the drop message_changed entries. Approximately 50% of user messages are silently dropped. Slack's Assistant API intermittently delivers user messages as message_changed events with the bot's own user ID as the top level sender. The Bolt self-message filter at line 575 of runtime-api-D-RDIrc9.js kills these events before they reach any handler. Messages that happen to arrive as normal message events work correctly. There is no error shown to the user in Slack beyond a generic "LLM request timed out" after the gateway fails to respond. The standard gateway logs show no indication that messages are being dropped. Only --verbose mode reveals the drop message_changed lines. It affects anyone using OpenClaw with Slack's Assistant API in DM threads, regardless of which model or provider they are running. Any user whose Slack bot has the assistant:write scope and receives messages through the assistant thread UI will hit this. The severity is high because there is no error feedback to the user, no indication in standard logs, and the failure is intermittent which makes it extremely difficult to diagnose without knowing to run --verbose. Users will assume their model provider is flaky or their network is dropping requests when the gateway is silently discarding valid messages.

Root Cause

Root Cause Analysis There are two layers of filtering that block these events: Layer 1: Bolt self-message filter (line 575 of runtime-api) javascriptif (botUserId !== void 0 && "user" in args.event && args.event.user === botUserId && !["member_joined_channel", "member_left_channel"].includes(args.event.type)) return; This filter silently drops ANY event where event.user === botUserId. When Slack's assistant API wraps a user message as a message_changed event, the top level event.user is set to the bot's user ID, not the actual human user. The filter kills the event before it ever reaches the message_changed handler. Layer 2: sender-not-allowlisted filter (line 17585 of runtime-api) Even after patching Layer 1 to allow message type events through, the resolveSenderId function for message_changed events (line 18425) resolves the sender incorrectly: javascriptresolveSenderId: (event) => { const changed = event; return changed.message?.user ?? changed.previous_message?.user ?? changed.message?.bot_id ?? changed.previous_message?.bot_id; } For assistant API events, changed.message.user is the bot's user ID, so the resolved sender is the bot itself. This causes the event to be routed as peer=direct:<botUserId>, which is then either dropped by the allowlist filter or silently ignored as a self-message. Confirmation via Slack API curl -s https://slack.com/api/auth.test -H "Authorization: Bearer xoxb-..."

Fix Action

Fix / Workaround

Root Cause Analysis There are two layers of filtering that block these events: Layer 1: Bolt self-message filter (line 575 of runtime-api) javascriptif (botUserId !== void 0 && "user" in args.event && args.event.user === botUserId && !["member_joined_channel", "member_left_channel"].includes(args.event.type)) return; This filter silently drops ANY event where event.user === botUserId. When Slack's assistant API wraps a user message as a message_changed event, the top level event.user is set to the bot's user ID, not the actual human user. The filter kills the event before it ever reaches the message_changed handler. Layer 2: sender-not-allowlisted filter (line 17585 of runtime-api) Even after patching Layer 1 to allow message type events through, the resolveSenderId function for message_changed events (line 18425) resolves the sender incorrectly: javascriptresolveSenderId: (event) => { const changed = event; return changed.message?.user ?? changed.previous_message?.user ?? changed.message?.bot_id ?? changed.previous_message?.bot_id; } For assistant API events, changed.message.user is the bot's user ID, so the resolved sender is the bot itself. This causes the event to be routed as peer=direct:<botUserId>, which is then either dropped by the allowlist filter or silently ignored as a self-message. Confirmation via Slack API curl -s https://slack.com/api/auth.test -H "Authorization: Bearer xoxb-..."

{ "ok": true, "user": "alfred", "user_id": "U0ADE8G5DL5", <-- this is the sender ID on dropped messages "bot_id": "B0ADWJRU9BK" } The dropped message_changed events are attributed to U0ADE8G5DL5 which is the bot itself, confirmed by auth.test. Diagnostic Evidence Verbose gateway log showing a WORKING message: 2026-03-26T10:14:43.915-07:00 [routing] resolveAgentRoute: channel=slack accountId=default peer=direct:U0ABVDL7C9M guildId=none teamId=T0AC8R91SMC bindings=0 2026-03-26T10:14:44.056-07:00 slack inbound: channel=D0ADHLY5WJE from=slack:U0ABVDL7C9M preview="What model are you running" 2026-03-26T10:14:44.062-07:00 slack stream preview ready (maxChars=8000, throttleMs=1000) ... 2026-03-26T10:15:22.606-07:00 slack: delivered 1 reply to user:U0ABVDL7C9M Verbose gateway log showing a DROPPED message: 2026-03-26T10:20:07.476-07:00 slack: drop message_changed sender U0ADE8G5DL5 channel=D0ADHLY5WJE reason=sender-not-allowlisted No routing, no inbound, no agent run. The message is killed at the filter stage. After patching Layer 1 only (allowing message type through Bolt filter): 2026-03-26T17:40:40.912-07:00 [routing] resolveAgentRoute: channel=slack accountId=default peer=direct:U0ADE8G5DL5 guildId=none teamId=T0AC8R91SMC bindings=0 The event passes the Bolt filter but is now routed as the bot talking to itself (peer=direct:U0ADE8G5DL5), and no agent run is triggered. Patches Attempted I attempted two patches, neither fully resolves the issue: Patch 1: Allow message_changed through Bolt self-filter javascript// Line 575: Added "message" to the exclusion list if (botUserId !== void 0 && "user" in args.event && args.event.user === botUserId && !["member_joined_channel", "member_left_channel", "message"].includes(args.event.type)) return; Result: Event passes Layer 1 but is routed with bot's ID as the peer, so no agent run triggers. Patch 2: Allow message_changed through allowlist filter javascript// Line 18356: Skip drop for message_changed events if (!auth.allowed && eventKind !== "message_changed") { Result: Combined with Patch 1, the event reaches routing but resolveSenderId still returns the bot's user ID, so it is routed as peer=direct:<botUserId>. Suggested Fix The resolveSenderId for message_changed events needs to extract the real human user from deeper in the event payload. In Slack's assistant API, the actual user ID may be in:

Additionally, the Bolt self-filter at line 575 should not apply to message_changed events where the subtype indicates it is an assistant API interaction, or alternatively, it should look at the resolved sender from the subtype handler rather than the top level event.user. A raw event dump of the message_changed payload from the Slack assistant API would help determine exactly where the real user ID lives. My debug logging patch at the resolveSenderId function never fired because the event was killed at the Bolt layer (line 575) before reaching it. Impact Approximately 50% of user messages in Slack DMs are silently dropped. The behavior is intermittent because Slack alternates between delivering messages as normal message events (which work) and message_changed events (which are dropped). There is no error message or indication in the standard gateway logs that messages are being dropped. Only verbose mode (--verbose) reveals the drop message_changed entries. Workaround None found. CLI and web interfaces work correctly as they do not go through the Slack socket mode path.

PR fix notes

PR #55471: fix(slack): allow message_changed events through self-filter for Assistant API (#55445)

Description (problem / solution / changelog)

Summary

Slack Assistant API DM threads deliver ~50% of user messages as message_changed events with the bot's own user ID as sender. Both the Bolt self-message filter and OpenClaw's sender filter dropped these, causing intermittent message loss ("LLM request timed out").

Root Cause

Two layers of filtering blocked legitimate user messages:

  1. Bolt's blanket self-event filter checked event.user === botUserId and dropped message_changed events
  2. OpenClaw's sender filter classified the message as bot-authored

For Slack Assistant API, message_changed events with bot user ID are actually user edits/rewrites, not bot self-messages.

Changes

  • extensions/slack/src/monitor/provider.ts: Disabled Bolt's blanket self-event drop for message_changed events
  • extensions/slack/src/monitor/events/messages.ts: Added subtype-aware self-message filter using message.edited.user to correctly identify the actual author
  • extensions/slack/src/monitor/events/message-subtype-handlers.ts: Updated sender resolution for message_changed to prioritize edited.user before message.user/bot_id

Test

  • LSP diagnostics clean on all 3 changed files
  • pnpm test -- extensions/slack/src/monitor/ passed (244 tests across 23 files)

Closes #55445

Changed files

  • extensions/slack/src/monitor/context.ts (modified, +3/-0)
  • extensions/slack/src/monitor/events/message-subtype-handlers.ts (modified, +6/-1)
  • extensions/slack/src/monitor/events/messages.test.ts (modified, +98/-1)
  • extensions/slack/src/monitor/events/messages.ts (modified, +62/-0)
  • extensions/slack/src/monitor/events/system-event-test-harness.ts (modified, +4/-0)
  • extensions/slack/src/monitor/provider.ts (modified, +5/-0)

PR #56677: fix(slack): enforce streaming mode mutual exclusion + guard stream teardown

Description (problem / solution / changelog)

Fixes #56675 Related: #54469, #23791

Problem

Slack has three independent streaming mechanisms (native streaming, draft preview, block streaming) that can be active simultaneously, causing duplicate message delivery. Only one mutual exclusion rule was enforced:

Native streaming ON → block streaming OFF  ✅ (already existed)
Draft preview ON → block streaming OFF     ❌ (not enforced)

When draft preview and block streaming run together:

  1. Block replies arrive as separate messages
  2. Draft preview gets finalized via chat.update into a permanent message
  3. User sees the same content twice

Fix

Two changes in dispatch.ts:

1. Enforce mutual exclusion: draft preview → disable block streaming

disableBlockStreaming: useStreaming
  ? true
  : previewStreamingEnabled
    ? true  // NEW: draft preview ON → no block streaming
    : typeof account.config.blockStreaming === "boolean"
      ? !account.config.blockStreaming
      : undefined,

2. Guard stopSlackStream against unflushed state

When the stream session exists but the streamer was never initialized (e.g., startStream failed), stopSlackStream would attempt to start a new stream to flush the buffer. Now checks finalStream.streamer before calling stop.

What this does NOT fix (separate PRs needed)

  • #52536 — Thread target for streaming (needs thread_ts plumbing change)
  • #50976 — Multi-agent streaming routing (needs session scoping)
  • #55445 — Bolt middleware self-filter (needs middleware change)

Testing

  • Zero type errors
  • Backward compatible: configs that only use one streaming mode are unaffected
  • Configs that accidentally enabled both modes now get correct single-mode behavior

AI Disclosure

🤖 AI-assisted (Kiro CLI).

Changed files

  • extensions/slack/src/monitor/message-handler/dispatch.ts (modified, +12/-4)

Code Example

Working message (verbose gateway log):
2026-03-26T10:14:43.915-07:00 [routing] resolveAgentRoute: channel=slack accountId=default peer=direct:U0ABVDL7C9M guildId=none teamId=T0AC8R91SMC bindings=0
2026-03-26T10:14:44.056-07:00 slack inbound: channel=D0ADHLY5WJE from=slack:U0ABVDL7C9M preview="What model are you running"
2026-03-26T10:15:22.606-07:00 slack: delivered 1 reply to user:U0ABVDL7C9M
Dropped message (verbose gateway log):
2026-03-26T10:20:07.476-07:00 slack: drop message_changed sender U0ADE8G5DL5 channel=D0ADHLY5WJE reason=sender-not-allowlisted
2026-03-26T10:20:30.612-07:00 slack: drop message_changed sender U0ADE8G5DL5 channel=D0ADHLY5WJE reason=sender-not-allowlisted
After patching Bolt self-filter (event passes but routes to bot):
2026-03-26T17:40:40.912-07:00 [routing] resolveAgentRoute: channel=slack accountId=default peer=direct:U0ADE8G5DL5 guildId=none teamId=T0AC8R91SMC bindings=0
Bot identity confirmation:
curl -s https://slack.com/api/auth.test -H "Authorization: Bearer xoxb-..."

{
    "ok": true,
    "user": "alfred",
    "user_id": "U0ADE8G5DL5",
    "bot_id": "B0ADWJRU9BK"
}
Filter location in source:
Line 575 of dist/runtime-api-D-RDIrc9.js (Bolt self-filter):
javascriptif (botUserId !== void 0 && "user" in args.event && args.event.user === botUserId && !["member_joined_channel", "member_left_channel"].includes(args.event.type)) return;
Line 18425 of dist/runtime-api-D-RDIrc9.js (resolveSenderId for message_changed):
javascriptresolveSenderId: (event) => {
    const changed = event;
    return changed.message?.user ?? changed.previous_message?.user ?? changed.message?.bot_id ?? changed.previous_message?.bot_id;
}
RAW_BUFFERClick to expand / collapse

Bug type

Crash (process/app exits or hangs)

Beta release blocker

No

Summary

Summary When using Slack's Assistant API (DM threads with a bot), user messages are frequently delivered as message_changed events with the bot's own user ID as the sender. These events are silently dropped by the Bolt self-message filter at line 575 of runtime-api-D-RDIrc9.js, causing approximately 50% of user messages to never reach the agent. Environment

OpenClaw version: 2026.3.24 (also reproduced on 2026.3.23-2) Channel: Slack (socket mode) OS: macOS (Mac Studio M3 Ultra) Model provider: vLLM (local, via proxy on localhost:8080)

Reproduction

Configure OpenClaw with Slack in socket mode Send a DM to the bot in Slack Observe that some messages work and some silently time out with "LLM request timed out" In verbose gateway logs, dropped messages appear as:

slack: drop message_changed sender U0ADE8G5DL5 channel=D0ADHLY5WJE reason=sender-not-allowlisted

Messages that DO work appear as normal message events routed to peer=direct:U0ABVDL7C9M (real user ID) Messages that FAIL arrive as message_changed events routed to peer=direct:U0ADE8G5DL5 (bot's own user ID)

Root Cause Analysis There are two layers of filtering that block these events: Layer 1: Bolt self-message filter (line 575 of runtime-api) javascriptif (botUserId !== void 0 && "user" in args.event && args.event.user === botUserId && !["member_joined_channel", "member_left_channel"].includes(args.event.type)) return; This filter silently drops ANY event where event.user === botUserId. When Slack's assistant API wraps a user message as a message_changed event, the top level event.user is set to the bot's user ID, not the actual human user. The filter kills the event before it ever reaches the message_changed handler. Layer 2: sender-not-allowlisted filter (line 17585 of runtime-api) Even after patching Layer 1 to allow message type events through, the resolveSenderId function for message_changed events (line 18425) resolves the sender incorrectly: javascriptresolveSenderId: (event) => { const changed = event; return changed.message?.user ?? changed.previous_message?.user ?? changed.message?.bot_id ?? changed.previous_message?.bot_id; } For assistant API events, changed.message.user is the bot's user ID, so the resolved sender is the bot itself. This causes the event to be routed as peer=direct:<botUserId>, which is then either dropped by the allowlist filter or silently ignored as a self-message. Confirmation via Slack API curl -s https://slack.com/api/auth.test -H "Authorization: Bearer xoxb-..."

{ "ok": true, "user": "alfred", "user_id": "U0ADE8G5DL5", <-- this is the sender ID on dropped messages "bot_id": "B0ADWJRU9BK" } The dropped message_changed events are attributed to U0ADE8G5DL5 which is the bot itself, confirmed by auth.test. Diagnostic Evidence Verbose gateway log showing a WORKING message: 2026-03-26T10:14:43.915-07:00 [routing] resolveAgentRoute: channel=slack accountId=default peer=direct:U0ABVDL7C9M guildId=none teamId=T0AC8R91SMC bindings=0 2026-03-26T10:14:44.056-07:00 slack inbound: channel=D0ADHLY5WJE from=slack:U0ABVDL7C9M preview="What model are you running" 2026-03-26T10:14:44.062-07:00 slack stream preview ready (maxChars=8000, throttleMs=1000) ... 2026-03-26T10:15:22.606-07:00 slack: delivered 1 reply to user:U0ABVDL7C9M Verbose gateway log showing a DROPPED message: 2026-03-26T10:20:07.476-07:00 slack: drop message_changed sender U0ADE8G5DL5 channel=D0ADHLY5WJE reason=sender-not-allowlisted No routing, no inbound, no agent run. The message is killed at the filter stage. After patching Layer 1 only (allowing message type through Bolt filter): 2026-03-26T17:40:40.912-07:00 [routing] resolveAgentRoute: channel=slack accountId=default peer=direct:U0ADE8G5DL5 guildId=none teamId=T0AC8R91SMC bindings=0 The event passes the Bolt filter but is now routed as the bot talking to itself (peer=direct:U0ADE8G5DL5), and no agent run is triggered. Patches Attempted I attempted two patches, neither fully resolves the issue: Patch 1: Allow message_changed through Bolt self-filter javascript// Line 575: Added "message" to the exclusion list if (botUserId !== void 0 && "user" in args.event && args.event.user === botUserId && !["member_joined_channel", "member_left_channel", "message"].includes(args.event.type)) return; Result: Event passes Layer 1 but is routed with bot's ID as the peer, so no agent run triggers. Patch 2: Allow message_changed through allowlist filter javascript// Line 18356: Skip drop for message_changed events if (!auth.allowed && eventKind !== "message_changed") { Result: Combined with Patch 1, the event reaches routing but resolveSenderId still returns the bot's user ID, so it is routed as peer=direct:<botUserId>. Suggested Fix The resolveSenderId for message_changed events needs to extract the real human user from deeper in the event payload. In Slack's assistant API, the actual user ID may be in:

event.message.blocks[].elements[].user_id event.message.metadata.event_payload.user The channel context (it is a DM, so the non-bot participant is the user)

Additionally, the Bolt self-filter at line 575 should not apply to message_changed events where the subtype indicates it is an assistant API interaction, or alternatively, it should look at the resolved sender from the subtype handler rather than the top level event.user. A raw event dump of the message_changed payload from the Slack assistant API would help determine exactly where the real user ID lives. My debug logging patch at the resolveSenderId function never fired because the event was killed at the Bolt layer (line 575) before reaching it. Impact Approximately 50% of user messages in Slack DMs are silently dropped. The behavior is intermittent because Slack alternates between delivering messages as normal message events (which work) and message_changed events (which are dropped). There is no error message or indication in the standard gateway logs that messages are being dropped. Only verbose mode (--verbose) reveals the drop message_changed entries. Workaround None found. CLI and web interfaces work correctly as they do not go through the Slack socket mode path.

Steps to reproduce

Install OpenClaw 2026.3.24 on macOS Configure Slack in socket mode with a bot that has assistant:write scope and uses Slack's Assistant API (DM threads) Configure any model provider (local or cloud) Start the gateway: openclaw gateway Open Slack and send a DM to the bot Send 5 to 10 messages in the same thread Observe that some messages get responses and others silently time out with "LLM request timed out" Start the gateway in verbose mode: openclaw gateway --verbose Repeat step 6 In the verbose output, observe that dropped messages appear as slack: drop message_changed sender <bot_user_id> channel=<channel_id> reason=sender-not-allowlisted Confirm the dropped sender ID matches the bot's own user ID via curl -s https://slack.com/api/auth.test -H "Authorization: Bearer <bot_token>"

Expected behavior

All user messages sent in a Slack DM thread with the bot should be received and processed by the agent, regardless of whether Slack delivers them as message events or message_changed events via the Assistant API. The gateway should resolve the real human user as the sender for message_changed events and not drop them as bot self-messages.

Actual behavior

Approximately 50% of user messages are silently dropped. Slack's Assistant API intermittently delivers user messages as message_changed events with the bot's own user ID as the top level sender. The Bolt self-message filter at line 575 of runtime-api-D-RDIrc9.js kills these events before they reach any handler. Messages that happen to arrive as normal message events work correctly. There is no error shown to the user in Slack beyond a generic "LLM request timed out" after the gateway fails to respond. The standard gateway logs show no indication that messages are being dropped. Only --verbose mode reveals the drop message_changed lines.

OpenClaw version

2026.3.24 (also reproduced on 2026.3.23-2)

Operating system

macOS 15 (Mac Studio M3 Ultra, Apple Silicon)

Install method

npm

Model

qwen 3.5 397b 6bit

Provider / routing chain

Slack (socket mode) → OpenClaw gateway (localhost:18789) → blockops-proxy (localhost:8080) → mlx-vlm (localhost:8082) serving Qwen3.5-397B-6bit

Additional provider/model setup details

Model served locally via mlx-vlm on Mac Studio M3 Ultra 512GB. Custom async Python proxy on port 8080 handles tool call parsing, thinking token stripping, and SSE streaming. OpenClaw configured with vLLM provider pointing at the proxy (baseUrl: http://127.0.0.1:8080/v1). Model ID is the local path (/Users/alfredpennyworth/models/qwen3.5-397b-6bit). Slack config has nativeStreaming: true, streaming: partial, groupPolicy: open, socket mode enabled. The proxy and mlx-vlm are confirmed working via direct curl. The issue is entirely in the Slack event handling path between the gateway receiving the socket mode event and dispatching it to the agent.

Logs, screenshots, and evidence

Working message (verbose gateway log):
2026-03-26T10:14:43.915-07:00 [routing] resolveAgentRoute: channel=slack accountId=default peer=direct:U0ABVDL7C9M guildId=none teamId=T0AC8R91SMC bindings=0
2026-03-26T10:14:44.056-07:00 slack inbound: channel=D0ADHLY5WJE from=slack:U0ABVDL7C9M preview="What model are you running"
2026-03-26T10:15:22.606-07:00 slack: delivered 1 reply to user:U0ABVDL7C9M
Dropped message (verbose gateway log):
2026-03-26T10:20:07.476-07:00 slack: drop message_changed sender U0ADE8G5DL5 channel=D0ADHLY5WJE reason=sender-not-allowlisted
2026-03-26T10:20:30.612-07:00 slack: drop message_changed sender U0ADE8G5DL5 channel=D0ADHLY5WJE reason=sender-not-allowlisted
After patching Bolt self-filter (event passes but routes to bot):
2026-03-26T17:40:40.912-07:00 [routing] resolveAgentRoute: channel=slack accountId=default peer=direct:U0ADE8G5DL5 guildId=none teamId=T0AC8R91SMC bindings=0
Bot identity confirmation:
curl -s https://slack.com/api/auth.test -H "Authorization: Bearer xoxb-..."

{
    "ok": true,
    "user": "alfred",
    "user_id": "U0ADE8G5DL5",
    "bot_id": "B0ADWJRU9BK"
}
Filter location in source:
Line 575 of dist/runtime-api-D-RDIrc9.js (Bolt self-filter):
javascriptif (botUserId !== void 0 && "user" in args.event && args.event.user === botUserId && !["member_joined_channel", "member_left_channel"].includes(args.event.type)) return;
Line 18425 of dist/runtime-api-D-RDIrc9.js (resolveSenderId for message_changed):
javascriptresolveSenderId: (event) => {
    const changed = event;
    return changed.message?.user ?? changed.previous_message?.user ?? changed.message?.bot_id ?? changed.previous_message?.bot_id;
}

Impact and severity

It affects anyone using OpenClaw with Slack's Assistant API in DM threads, regardless of which model or provider they are running. Any user whose Slack bot has the assistant:write scope and receives messages through the assistant thread UI will hit this. The severity is high because there is no error feedback to the user, no indication in standard logs, and the failure is intermittent which makes it extremely difficult to diagnose without knowing to run --verbose. Users will assume their model provider is flaky or their network is dropping requests when the gateway is silently discarding valid messages.

Additional information

Workaround attempted: Setting channels.slack.nativeStreaming to false in openclaw.json did not change the behavior. Updating from 2026.3.23-2 to 2026.3.24 also did not resolve it. CLI and web UI interfaces work correctly since they bypass the Slack socket mode path entirely. Happy to test: I can test a patched build immediately. I have verbose logging set up and a reproducible environment. The issue reproduces within 5 to 10 messages in any DM thread.

extent analysis

Fix Plan

To resolve the issue, we need to modify the resolveSenderId function for message_changed events to extract the real human user ID from the event payload. We also need to update the Bolt self-filter to allow message_changed events through.

Step 1: Update resolveSenderId function

resolveSenderId: (event) => {
  const changed = event;
  // Check if the event is an assistant API interaction
  if (changed.subtype === 'assistant') {
    // Extract the real human user ID from the event payload
    const userId = changed.message.blocks[0].elements[0].user_id || changed.message.metadata.event_payload.user;
    return userId;
  }
  // Fallback to the original implementation
  return changed.message?.user ?? changed.previous_message?.user ?? changed.message?.bot_id ?? changed.previous_message?.bot_id;
}

Step 2: Update Bolt self-filter

if (botUserId !== void 0 && "user" in args.event && args.event.user === botUserId && !["member_joined_channel", "member_left_channel", "message", "message_changed"].includes(args.event.type)) return;

Verification

To verify the fix, follow these steps:

  1. Update the resolveSenderId function and Bolt self-filter as described above.
  2. Restart the OpenClaw gateway.
  3. Send a DM to the bot in Slack and observe the verbose gateway logs.
  4. Verify that the message_changed events are no longer dropped and are routed to the correct user ID.

Extra Tips

  • Make sure to test the fix in a reproducible environment to ensure that the issue is resolved.
  • Consider adding additional logging to help diagnose any future issues with the resolveSenderId function or Bolt self-filter.
  • If you encounter any issues with the updated code, try to isolate the problem by testing individual components or functions.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

All user messages sent in a Slack DM thread with the bot should be received and processed by the agent, regardless of whether Slack delivers them as message events or message_changed events via the Assistant API. The gateway should resolve the real human user as the sender for message_changed events and not drop them as bot self-messages.

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING