Skip to content

fix(routing): prevent duplicate delivery in dmScope=main sessions#33629

Closed
Yuandiaodiaodiao wants to merge 1 commit intoopenclaw:mainfrom
Yuandiaodiaodiao:fix/dmscope-main-duplicate-delivery
Closed

fix(routing): prevent duplicate delivery in dmScope=main sessions#33629
Yuandiaodiaodiao wants to merge 1 commit intoopenclaw:mainfrom
Yuandiaodiaodiao:fix/dmscope-main-duplicate-delivery

Conversation

@Yuandiaodiaodiao
Copy link
Copy Markdown
Contributor

Summary

Fixes #33619

Commit 050e928 (#29328) changed chat.send to inherit OriginatingChannel from the session's delivery context. This correctly enables cross-channel routing for per-channel sessions (e.g., Feishu) but causes duplicate message delivery for dmScope=main users who share a single session across all channels.

When a user chats on Discord then switches to WebChat, the shared main session's lastChannel ("discord") is inherited as OriginatingChannel, triggering cross-channel routing and delivering responses to both Discord and WebChat.

Fix

Check if the session key is channel-agnostic (shared main session or per-peer session) before inheriting the delivery context:

  • Channel-specific sessions (per-channel-peer, groups): inherit delivery context (correct behavior for Feishu routing)
  • Shared sessions (dmScope=main, per-peer): use INTERNAL_MESSAGE_CHANNEL to prevent cross-channel delivery

The check is based on session key structure — main sessions have <=3 colon segments (agent:main:main), per-peer has 4 with direct at index 2, while per-channel sessions have 5+ segments with an embedded channel name.

Test plan

  • pnpm tsgo passes
  • pnpm check passes
  • Updated existing Telegram/Feishu routing tests to use per-channel session keys
  • Added new test: shared main session does NOT inherit delivery context
  • All 9 chat directive-tags tests pass
  • All 55 dispatch-from-config + session-key tests pass

🤖 Generated with Claude Code

@openclaw-barnacle openclaw-barnacle bot added app: web-ui App: web-ui gateway Gateway runtime size: S labels Mar 3, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 495b3ea188

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +856 to +857
const isChannelAgnosticSession =
keyParts.length <= 3 || (keyParts.length === 4 && keyParts[2] === "direct");
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat threaded shared sessions as channel-agnostic

The new isChannelAgnosticSession guard only recognizes keys with <=3 parts or exactly agent:*:direct:*, so shared sessions that gain a thread suffix (for example agent:main:main:thread:...) are misclassified as channel-specific and still inherit deliveryContext. Telegram DMs explicitly append thread suffixes regardless of dmScope (src/telegram/bot-message-context.ts:224-229), so this path can still replay stale lastChannel/lastTo and duplicate delivery to an external channel when the user sends from WebChat. Classifying against the parent session key (before :thread:/:topic:) would avoid this hole.

Useful? React with 👍 / 👎.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 3, 2026

Greptile Summary

This PR fixes a duplicate message delivery regression introduced by commit 050e928, where chat.send was incorrectly inheriting the session's deliveryContext for dmScope=main users, causing cross-channel routing when switching between platforms (e.g., Discord → WebChat). The fix gates delivery-context inheritance on whether the session key embeds a specific channel, which is the right architectural signal.

Key changes:

  • chat.ts: Adds isChannelAgnosticSession check by parsing the session key's colon segments before deciding whether to inherit OriginatingChannel / routing metadata.
  • chat.directive-tags.test.ts: Updates two existing routing tests to pass per-channel session keys (so the guard doesn't suppress those tests), and adds a new test confirming that a shared "main" session key does not inherit delivery context.

Issue found:

  • The per-peer branch of the guard (keyParts.length === 4 && keyParts[2] === "direct") is too narrow: per-peer session keys extended with a thread suffix via resolveThreadSessionKeys (e.g. agent:main:direct:peerId:thread:tid, 6 parts) will not satisfy length === 4 and will be misclassified as channel-specific, reintroducing the duplicate-delivery bug for that combination. Similarly, main sessions with thread suffixes (agent:main:main:thread:tid, 5 parts) would also be misclassified. Dropping the length constraint and using just keyParts[2] === "direct" would partially address this, though a more complete fix would also handle main thread sessions.

Confidence Score: 3/5

  • The main fix for dmScope=main sessions is correct and well-tested, but thread-extended session keys (both per-peer and main) have a narrow path for the original duplicate-delivery bug to reoccur.
  • The main fix (guarding delivery-context inheritance based on session key structure) is correct and addresses the regression in [Bug]: chat.send inherits OriginatingChannel from session delivery context, causing duplicate delivery in dmScope=main sessions #33619. The new test suite confirms that dmScope=main users don't inherit cross-channel routing. However, per-peer thread sessions (e.g., agent:main:direct:peerId:thread:tid) and main thread sessions are not yet protected by the isChannelAgnosticSession guard—they would still inherit deliveryContext from previous channels, reintroducing duplicate delivery. The proposed one-line fix (dropping the length === 4 constraint) would improve safety for per-peer thread cases but leave main thread sessions vulnerable. A complete fix would also ensure main thread sessions are recognized as channel-agnostic.
  • src/gateway/server-methods/chat.ts — the isChannelAgnosticSession condition on line 857 should be enhanced to robustly handle all thread-extended session variants.

Last reviewed commit: 495b3ea

// to avoid duplicate cross-channel delivery (#33619).
const keyParts = sessionKey.split(":");
const isChannelAgnosticSession =
keyParts.length <= 3 || (keyParts.length === 4 && keyParts[2] === "direct");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per-peer thread sessions misclassified as channel-specific

The condition keyParts.length === 4 && keyParts[2] === "direct" only matches bare per-peer session keys (agent:{agentId}:direct:{peerId} – 4 parts). It will not match per-peer session keys that have been extended with a thread suffix via resolveThreadSessionKeys, e.g. agent:main:direct:peerId:thread:tid (6 parts). For those keys:

  • keyParts.length <= 3 → false
  • keyParts.length === 4 && keyParts[2] === "direct" → false (length is 6)
  • isChannelAgnosticSession = falsehasDeliverableRoute can become true → the delivery context (e.g. a previous "discord" channel) is inherited, reintroducing the duplicate-delivery bug for per-peer thread sessions.

Similarly, a main session with a thread suffix (agent:main:main:thread:tid, 5 parts) would also be misclassified as channel-specific.

The length === 4 guard is redundant because keyParts[2] === "direct" is never true for channel-specific session keys (which always have a real channel name at index 2). Dropping the length constraint makes the check robust against thread-extended variants:

Suggested change
keyParts.length <= 3 || (keyParts.length === 4 && keyParts[2] === "direct");
const isChannelAgnosticSession =
keyParts.length <= 3 || keyParts[2] === "direct";
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gateway/server-methods/chat.ts
Line: 857

Comment:
Per-peer thread sessions misclassified as channel-specific

The condition `keyParts.length === 4 && keyParts[2] === "direct"` only matches bare per-peer session keys (`agent:{agentId}:direct:{peerId}` – 4 parts). It will not match per-peer session keys that have been extended with a thread suffix via `resolveThreadSessionKeys`, e.g. `agent:main:direct:peerId:thread:tid` (6 parts). For those keys:

- `keyParts.length <= 3` → false
- `keyParts.length === 4 && keyParts[2] === "direct"` → false (length is 6)
-`isChannelAgnosticSession = false``hasDeliverableRoute` can become `true` → the delivery context (e.g. a previous "discord" channel) is inherited, reintroducing the duplicate-delivery bug for per-peer thread sessions.

Similarly, a main session with a thread suffix (`agent:main:main:thread:tid`, 5 parts) would also be misclassified as channel-specific.

The `length === 4` guard is redundant because `keyParts[2] === "direct"` is never true for channel-specific session keys (which always have a real channel name at index 2). Dropping the length constraint makes the check robust against thread-extended variants:

```suggestion
      const isChannelAgnosticSession =
        keyParts.length <= 3 || keyParts[2] === "direct";
```

How can I resolve this? If you propose a fix, please make it concise.

@Takhoffman
Copy link
Copy Markdown
Contributor

Superseded by #33786.

This routing/session synthesis path includes the shared duplicate-delivery guard for dmScope=main and keeps regression coverage consolidated in one merge path.

@Takhoffman
Copy link
Copy Markdown
Contributor

Thanks again for this work.

This PR was triaged and merged using an AI-assisted review/synthesis workflow.

This landed indirectly via the synthesized PR #33786, and your contribution is credited in the changelog and as a co-author on the merge commit.

Closing this PR as superseded by #33786 (reason: superseded). If anything here looks incorrect or incomplete, reply to reopen and we can reassess.

@Takhoffman Takhoffman closed this Mar 4, 2026
@Takhoffman
Copy link
Copy Markdown
Contributor

Correction for accuracy: the synthesized work is credited in the changelog, but the squash merge commit for #33786 does not include explicit Co-authored-by trailers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app: web-ui App: web-ui gateway Gateway runtime size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants