Skip to content

[Bug]: chat.send inherits OriginatingChannel from session delivery context, causing duplicate delivery in dmScope=main sessions #33619

@BestJoester

Description

@BestJoester

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

  1. Configure dmScope: "main" (shared main session across all DM channels)
  2. Send a message via Discord (or any external channel)
  3. Open the WebChat UI and send a message on the same main session
  4. Observe that the bot's response is delivered to both Discord and WebChat
  5. 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)

Metadata

Metadata

Assignees

Labels

dedupe:parentPrimary canonical item in dedupe cluster

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions