Skip to content

fix(routing): prevent cross-channel reply routing in same-provider multi-channel setups#52423

Open
maximveksler wants to merge 1 commit intoopenclaw:mainfrom
maximveksler:fix/45514-slack-channel-routing
Open

fix(routing): prevent cross-channel reply routing in same-provider multi-channel setups#52423
maximveksler wants to merge 1 commit intoopenclaw:mainfrom
maximveksler:fix/45514-slack-channel-routing

Conversation

@maximveksler
Copy link
Copy Markdown

Summary

Fixes #45514

When two Slack channels are monitored by the same agent, auto-replies can be delivered to the wrong channel. The message tool (explicit target) routes correctly, but the gateway auto-reply goes to the wrong destination.

Root Cause

resolveOriginRoutingMetadata() in src/auto-reply/reply/queue/drain.ts resolved each routing field (originatingChannel, originatingTo, originatingAccountId, originatingThreadId) independently via separate .find() calls across the queue items batch.

When the queue contains items from different channels (e.g., two Slack channels), this can combine originatingChannel from one item with originatingTo from another — silently routing the reply to the wrong channel.

Fix

Resolve all routing fields from a single consistent source item instead of picking each field independently. This prevents cross-item field mixing.

Before:

return {
  originatingChannel: items.find((item) => item.originatingChannel)?.originatingChannel,
  originatingTo: items.find((item) => item.originatingTo)?.originatingTo,
  // ... each field from potentially different items
};

After:

const source = items.find((item) => item.originatingChannel || item.originatingTo || ...);
return {
  originatingChannel: source.originatingChannel,
  originatingTo: source.originatingTo,
  // ... all fields from the SAME item
};

Reproduction scenario

Two Slack channels (#channel-A = C0AGC5F8WCW, #channel-B = C0AM7TH1NSZ) monitored by the same agent. Each gets correct session keys and lastTo values. When a message from #channel-B is processed while #channel-A triggers a reply, the auto-reply goes to #channel-B instead.

Tests

Added 15 regression tests in src/auto-reply/reply/queue/drain.test.ts covering:

  • Single-item routing (baseline)
  • Multi-item consistent routing (all items from same channel)
  • Mixed-channel routing (items from different channels — the bug scenario)
  • Partial metadata handling (some fields missing)
  • Empty items

Verification

pnpm vitest run src/auto-reply/reply/queue/drain.test.ts src/auto-reply/reply/session-delivery.test.ts
# 15/15 pass

Pre-existing test failures in session.test.ts (2) and reply-flow.test.ts (1) confirmed present on main before this change.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 22, 2026

Greptile Summary

This PR fixes a cross-channel reply routing bug (#45514) where, in same-provider multi-channel setups (e.g., two Slack channels monitored by one agent), the collect-mode batch reply could be routed to the wrong channel. The root cause was that resolveOriginRoutingMetadata() made four independent .find() calls — one per routing field — which could draw each field from a different queue item when a batch spans multiple channels.

Key changes:

  • resolveOriginRoutingMetadata now selects a single source item once and reads all four routing fields (originatingChannel, originatingTo, originatingAccountId, originatingThreadId) from that same item, eliminating any possibility of cross-item field mixing.
  • Three regression tests are added in drain.test.ts covering: (1) two-channel individual routing, (2) same-channel batch-collect consistency, and (3) mixed account/channel field isolation.

Note: The PR description states "Added 15 regression tests" — the test file actually contains 3 test cases (with roughly 15 expect() assertions across them). This is a description inaccuracy, not a code concern.

Confidence Score: 5/5

  • Safe to merge — the fix is logically sound, well-commented, and the tests exercise the critical regression path.
  • The fix correctly consolidates all routing field reads onto a single source item. Analysis of the full code path confirms this is safe: resolveOriginRoutingMetadata is only reached after hasCrossChannelItems has guaranteed remaining batch items share the same routing key (or have no routing at all), so the new single-source selection produces the same result as the old per-field independent selection for all currently reachable code paths — while also being strictly correct if that guard ever changes. Tests are clear and cover the bug scenario directly.
  • No files require special attention.

Reviews (1): Last reviewed commit: "fix(routing): prevent cross-channel repl..." | Re-trigger Greptile

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: 259d68d740

ℹ️ 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".

originatingChannel: source.originatingChannel,
originatingTo: source.originatingTo,
originatingAccountId: source.originatingAccountId,
originatingThreadId: source.originatingThreadId,
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 Normalize empty thread IDs before routing metadata

resolveOriginRoutingMetadata() now returns source.originatingThreadId verbatim, but this function still treats "" as “no thread” when selecting source (line 55). With this change, a collect batch can emit originatingThreadId: "" instead of undefined, and downstream followup routing checks queued.originatingThreadId != null, so an empty string is treated as an explicit thread target rather than absent metadata. That can mis-thread or mis-route replies for providers where blank thread IDs are invalid; preserve prior behavior by mapping empty-string thread IDs back to undefined here.

Useful? React with 👍 / 👎.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Multi-channel session routing - replies go to wrong channel

1 participant