fix(routing): prevent duplicate delivery in dmScope=main sessions#33629
fix(routing): prevent duplicate delivery in dmScope=main sessions#33629Yuandiaodiaodiao wants to merge 1 commit intoopenclaw:mainfrom
Conversation
…red main sessions (openclaw#33619)
There was a problem hiding this comment.
💡 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".
| const isChannelAgnosticSession = | ||
| keyParts.length <= 3 || (keyParts.length === 4 && keyParts[2] === "direct"); |
There was a problem hiding this comment.
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 SummaryThis PR fixes a duplicate message delivery regression introduced by commit 050e928, where Key changes:
Issue found:
Confidence Score: 3/5
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"); |
There was a problem hiding this 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→ falsekeyParts.length === 4 && keyParts[2] === "direct"→ false (length is 6)- →
isChannelAgnosticSession = false→hasDeliverableRoutecan becometrue→ 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:
| 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.|
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. |
|
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. |
|
Correction for accuracy: the synthesized work is credited in the changelog, but the squash merge commit for #33786 does not include explicit |
Summary
Fixes #33619
Commit 050e928 (#29328) changed
chat.sendto inheritOriginatingChannelfrom the session's delivery context. This correctly enables cross-channel routing for per-channel sessions (e.g., Feishu) but causes duplicate message delivery fordmScope=mainusers 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 asOriginatingChannel, 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:
INTERNAL_MESSAGE_CHANNELto prevent cross-channel deliveryThe check is based on session key structure — main sessions have
<=3colon segments (agent:main:main), per-peer has 4 withdirectat index 2, while per-channel sessions have 5+ segments with an embedded channel name.Test plan
pnpm tsgopassespnpm checkpasses🤖 Generated with Claude Code