Bug type
Regression (worked before, now fails)
Summary
Commit 050e928 (#29328, fix for #31573) changed chat.send to inherit OriginatingChannel from the session's deliveryContext/lastChannel instead of always using INTERNAL_MESSAGE_CHANNEL. This fixes Feishu per-channel session routing but causes duplicate message delivery for users with dmScope: "main" who share a single session across multiple channels.
When a user chats on Discord (or Telegram, Signal, etc.) and then switches to the WebChat UI, responses are now sent to both the external channel and the WebUI, because the shared main session's lastChannel is inherited as OriginatingChannel, triggering shouldRouteToOriginating = true in dispatch-from-config.ts.
Steps to reproduce
- Configure
dmScope: "main" (shared main session across all DM channels)
- Send a message via Discord (or any external channel)
- Open the WebChat UI and send a message on the same main session
- Observe that the bot's response is delivered to both Discord and WebChat
- WebChat shows 2 messages (one from the webchat dispatcher, one mirrored from the Discord delivery)
Expected behavior
When sending from WebChat, responses should only appear on WebChat. The external channel's lastChannel should not cause cross-channel delivery from the web UI.
Actual behavior
chat.ts reads session.deliveryContext.channel (e.g. "discord") and sets OriginatingChannel: "discord" on the dispatch context. dispatch-from-config.ts sees OriginatingChannel = "discord" with Provider = "webchat", so shouldRouteToOriginating = true, and the response is routed to Discord via routeReply in addition to the webchat dispatcher.
Root cause
In src/gateway/server-methods/chat.ts (lines added by 050e928):
const routeChannelCandidate = normalizeMessageChannel(
entry?.deliveryContext?.channel ?? entry?.lastChannel,
);
// ...
const originatingChannel = hasDeliverableRoute
? routeChannelCandidate
: INTERNAL_MESSAGE_CHANNEL;
This unconditionally inherits the session's delivery route. For per-channel sessions (Feishu, Telegram), this is correct — the user explicitly selected that session in the sidebar. But for dmScope=main shared sessions, the lastChannel reflects whichever external channel was used most recently, not the user's current intent.
Possible fix
The hasDeliverableRoute check should distinguish between per-channel sessions (where inheriting makes sense) and shared main sessions (where it doesn't). For example, only inherit when the session key contains a channel-specific segment (e.g. agent:main:feishu:direct:...) rather than the bare agent:main:main.
OpenClaw version
2026.3.3 (includes 050e928)
Operating system
Linux
Install method
Source (npm link)
Bug type
Regression (worked before, now fails)
Summary
Commit 050e928 (#29328, fix for #31573) changed
chat.sendto inheritOriginatingChannelfrom the session'sdeliveryContext/lastChannelinstead of always usingINTERNAL_MESSAGE_CHANNEL. This fixes Feishu per-channel session routing but causes duplicate message delivery for users withdmScope: "main"who share a single session across multiple channels.When a user chats on Discord (or Telegram, Signal, etc.) and then switches to the WebChat UI, responses are now sent to both the external channel and the WebUI, because the shared main session's
lastChannelis inherited asOriginatingChannel, triggeringshouldRouteToOriginating = trueindispatch-from-config.ts.Steps to reproduce
dmScope: "main"(shared main session across all DM channels)Expected behavior
When sending from WebChat, responses should only appear on WebChat. The external channel's
lastChannelshould not cause cross-channel delivery from the web UI.Actual behavior
chat.tsreadssession.deliveryContext.channel(e.g."discord") and setsOriginatingChannel: "discord"on the dispatch context.dispatch-from-config.tsseesOriginatingChannel = "discord"withProvider = "webchat", soshouldRouteToOriginating = true, and the response is routed to Discord viarouteReplyin addition to the webchat dispatcher.Root cause
In
src/gateway/server-methods/chat.ts(lines added by 050e928):This unconditionally inherits the session's delivery route. For per-channel sessions (Feishu, Telegram), this is correct — the user explicitly selected that session in the sidebar. But for
dmScope=mainshared sessions, thelastChannelreflects whichever external channel was used most recently, not the user's current intent.Possible fix
The
hasDeliverableRoutecheck should distinguish between per-channel sessions (where inheriting makes sense) and shared main sessions (where it doesn't). For example, only inherit when the session key contains a channel-specific segment (e.g.agent:main:feishu:direct:...) rather than the bareagent:main:main.OpenClaw version
2026.3.3 (includes 050e928)
Operating system
Linux
Install method
Source (npm link)