Skip to content

fix(gateway): stop shared-main chat.send from inheriting stale external routes#38418

Merged
vincentkoc merged 9 commits intoopenclaw:mainfrom
vincentkoc:vincentkoc-code/shared-main-chat-send-stale-route
Mar 7, 2026
Merged

fix(gateway): stop shared-main chat.send from inheriting stale external routes#38418
vincentkoc merged 9 commits intoopenclaw:mainfrom
vincentkoc:vincentkoc-code/shared-main-chat-send-stale-route

Conversation

@vincentkoc
Copy link
Copy Markdown
Member

Summary

Describe the problem and fix in 2–5 bullets:

  • Problem: shared-main and other channel-agnostic chat.send webchat sessions can inherit stale external delivery metadata from stored session context and route replies to the wrong channel.
  • Why it matters: Control UI replies should stay on webchat unless the session key itself encodes an explicit external target.
  • What changed: extracted the chat.send route decision into a focused helper and narrowed the fix so channel-scoped sessions still preserve explicit external delivery while shared-main/channel-agnostic webchat sessions refuse stale inherited routes.
  • What did NOT change (scope boundary): this does not alter direct-session stale-route handling already fixed in fix(session): keep direct WebChat replies on WebChat #37135, and it does not change Control UI visibility filtering already fixed in Fix: respect original delivery target for Control UI visibility #36030.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

User-visible / Behavior Changes

WebChat replies sent through shared-main or other channel-agnostic chat.send sessions no longer leak to stale external channels unless the session key explicitly targets that channel.

Security Impact (required)

  • New permissions/capabilities? (Yes/No) No
  • Secrets/tokens handling changed? (Yes/No) No
  • New/changed network calls? (Yes/No) No
  • Command/tool execution surface changed? (Yes/No) No
  • Data access scope changed? (Yes/No) No
  • If any Yes, explain risk + mitigation:

Repro + Verification

Environment

  • OS: macOS
  • Runtime/container: Node 22 + pnpm
  • Model/provider: n/a
  • Integration/channel (if any): Gateway / WebChat / iMessage routing
  • Relevant config (redacted): shared-main and channel-scoped session routing coverage via unit tests

Steps

  1. Reproduce a session where stored delivery context points at an external channel.
  2. Send a reply from a shared-main or channel-agnostic WebChat chat.send session.
  3. Confirm the reply stays on WebChat unless the session key explicitly encodes an external target.

Expected

  • Shared-main/channel-agnostic WebChat chat.send replies do not inherit stale external delivery metadata.
  • Channel-scoped sessions still honor explicit external delivery.

Actual

  • Covered by updated gateway unit tests and local verification commands below.

Evidence

Attach at least one:

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Human Verification (required)

What you personally verified (not just CI), and how:

  • Verified scenarios: focused route selection tests in src/gateway/server-methods/chat.directive-tags.test.ts, typecheck, lint/format, and production dependency audit.
  • Edge cases checked: explicit channel-scoped delivery remains intact; channel-agnostic/shared-main WebChat delivery no longer inherits stale route context.
  • What you did not verify: live end-to-end messaging against real iMessage/WebChat accounts.

Compatibility / Migration

  • Backward compatible? (Yes/No) Yes
  • Config/env changes? (Yes/No) No
  • Migration needed? (Yes/No) No
  • If yes, exact upgrade steps:

Failure Recovery (if this breaks)

  • How to disable/revert this change quickly: revert the PR commit or temporarily restore the previous route inheritance branch in src/gateway/server-methods/chat.ts.
  • Files/config to restore: src/gateway/server-methods/chat.ts, src/gateway/server-methods/chat.directive-tags.test.ts
  • Known bad symptoms reviewers should watch for: channel-scoped sessions unexpectedly losing intended external delivery, or shared-main WebChat replies escaping to external channels.

Risks and Mitigations

  • Risk: the narrowed helper could accidentally block intended delivery for explicitly channel-scoped sessions.
    • Mitigation: added targeted test coverage for the explicit-session-key boundary and kept the change scoped to chat.send route selection only.

… channels

chat.send always originates from the webchat/control-UI surface.  Previously,
channel-scoped session keys (e.g. agent:main:slack:direct:U…) caused
OriginatingChannel to inherit the session's stored external route, so the
reply dispatcher would route responses to Slack/Telegram instead of back to
the gateway connection.  Remove the route-inheritance logic from chat.send and
always set OriginatingChannel to INTERNAL_MESSAGE_CHANNEL ("webchat").

Closes openclaw#34647

Made-with: Cursor
@vincentkoc vincentkoc marked this pull request as ready for review March 7, 2026 00:26
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 7, 2026

Greptile Summary

This PR extracts the chat.send route-selection logic from the inline body of the handler into a dedicated resolveChatSendOriginatingRoute helper, and narrows the fix so shared-main and other channel-agnostic webchat sessions no longer inherit stale external delivery metadata, while channel-scoped sessions continue to honour explicit external delivery.

  • New helper resolveChatSendOriginatingRoute: cleanly encapsulates all route-inheritance logic and early-returns webchat when deliver is not set. Equivalent to the old inline code for most paths.
  • ExplicitDeliverRoute assertions added: four existing channel-scoped tests now explicitly assert ExplicitDeliverRoute: true, and new tests cover shared-main, UI-on-main, CLI-on-configured-main, and custom non-channel sessions.
  • Undocumented behavioral narrowing (see inline comment on chat.ts:175): the guard for configured-main sessions was client?.connect !== undefined in the original code but is now params.client (client?.connect?.client). Any backend/service connection that populates connect but omits the nested client metadata object would previously have been eligible to inherit external delivery; after this change it falls back to webchat silently. This is not described in the PR, not explicitly tested, and could be a regression for such callers.
  • CHANGELOG entry: credits @Sid-Qin for a PR authored by @vincentkoc — likely a collaborator attribution, not a code issue.

Confidence Score: 3/5

  • Merge with caution — the core stale-route fix is correct but an undocumented narrowing of the configured-main session guard (client?.connectclient?.connect?.client) may silently break route inheritance for service callers that don't populate client metadata.
  • The primary bug fix (shared-main/channel-agnostic sessions refusing stale external routes) is well-reasoned, and the test coverage for the intended scenarios is solid. The score is reduced because the helper silently changes a secondary condition from checking client.connect to checking client.connect.client, a deeper property, without documentation or a covering test. If that connection shape occurs in production it would cause a regression where a legitimate configured-main CLI/backend caller loses intended external delivery.
  • src/gateway/server-methods/chat.ts — specifically the canInheritDeliverableRoute condition at line 175 regarding the params.client guard vs the original client?.connect !== undefined guard.

Last reviewed commit: 0489f17

@vincentkoc
Copy link
Copy Markdown
Member Author

Addressed the sessionKey parsing hardening pass.

Changes pushed:

  • chat.send now rejects sessionKey values longer than 512 chars at the RPC schema boundary.
  • resolveChatSendOriginatingRoute(...) now bails out before calling parseAgentSessionKey(...) when the raw key is oversized.
  • The helper now bounds downstream tokenization to the first 3 segments instead of splitting the entire string.
  • Added a regression test that verifies oversized chat.send session keys are rejected before dispatch.

Validation:

  • pnpm vitest run src/gateway/server-methods/chat.directive-tags.test.ts
  • pnpm tsgo
  • pnpm check

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: 0f284f21b2

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +144 to +148
if (params.sessionKey.length > CHAT_SEND_SESSION_KEY_MAX_LENGTH) {
return {
originatingChannel: INTERNAL_MESSAGE_CHANNEL,
explicitDeliverRoute: false,
};
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 Remove canonical-length guard from route resolution

The new guard checks params.sessionKey.length inside resolveChatSendOriginatingRoute, but this function is called with canonicalKey from loadSessionEntry (see chat.send), not the already-validated raw request key. For legacy/non-agent keys near the 512-char limit, canonicalization prepends agent:<id>: and can push the key over CHAT_SEND_SESSION_KEY_MAX_LENGTH, causing ExplicitDeliverRoute to be forced false even when deliver: true and explicit channel scope are valid. This silently reroutes replies to webchat for long-but-valid session keys instead of preserving external delivery.

Useful? React with 👍 / 👎.

mcaxtr pushed a commit to mcaxtr/openclaw that referenced this pull request Mar 7, 2026
…al routes (openclaw#38418)

* fix(gateway): prevent webchat messages from cross-routing to external channels

chat.send always originates from the webchat/control-UI surface.  Previously,
channel-scoped session keys (e.g. agent:main:slack:direct:U…) caused
OriginatingChannel to inherit the session's stored external route, so the
reply dispatcher would route responses to Slack/Telegram instead of back to
the gateway connection.  Remove the route-inheritance logic from chat.send and
always set OriginatingChannel to INTERNAL_MESSAGE_CHANNEL ("webchat").

Closes openclaw#34647

Made-with: Cursor

* Gateway: preserve configured-main connect gating

* Gateway: cover connect-without-client routing

* Gateway: add chat.send session key length limit

* Gateway: cap chat.send session key schema

* Gateway: bound chat.send session key parsing

* Gateway: cover oversized chat.send session keys

* Update CHANGELOG.md

---------

Co-authored-by: SidQin-cyber <[email protected]>
joshavant pushed a commit that referenced this pull request Mar 7, 2026
…al routes (#38418)

* fix(gateway): prevent webchat messages from cross-routing to external channels

chat.send always originates from the webchat/control-UI surface.  Previously,
channel-scoped session keys (e.g. agent:main:slack:direct:U…) caused
OriginatingChannel to inherit the session's stored external route, so the
reply dispatcher would route responses to Slack/Telegram instead of back to
the gateway connection.  Remove the route-inheritance logic from chat.send and
always set OriginatingChannel to INTERNAL_MESSAGE_CHANNEL ("webchat").

Closes #34647

Made-with: Cursor

* Gateway: preserve configured-main connect gating

* Gateway: cover connect-without-client routing

* Gateway: add chat.send session key length limit

* Gateway: cap chat.send session key schema

* Gateway: bound chat.send session key parsing

* Gateway: cover oversized chat.send session keys

* Update CHANGELOG.md

---------

Co-authored-by: SidQin-cyber <[email protected]>
widingmarcus-cyber added a commit to widingmarcus-cyber/openclaw that referenced this pull request Mar 7, 2026
…nel-scoped sessions (openclaw#38957)

When webchat/Control UI clients access channel-scoped sessions (e.g.
agent:main:imessage:direct:+123), they were incorrectly inheriting
the external delivery route from the session's stored context. This
caused webchat messages to be tagged with channel: 'imessage' instead
of channel: 'webchat', leading to cross-channel message synchronization
when dmScope is set to 'per-channel-peer'.

The fix adds isFromWebchatClient as a top-level condition in
canInheritDeliverableRoute, ensuring webchat clients never inherit
external delivery routes regardless of session type.

This extends openclaw#38418 which only fixed shared-main sessions.
vincentkoc added a commit to BryanTegomoh/openclaw-fork that referenced this pull request Mar 8, 2026
…al routes (openclaw#38418)

* fix(gateway): prevent webchat messages from cross-routing to external channels

chat.send always originates from the webchat/control-UI surface.  Previously,
channel-scoped session keys (e.g. agent:main:slack:direct:U…) caused
OriginatingChannel to inherit the session's stored external route, so the
reply dispatcher would route responses to Slack/Telegram instead of back to
the gateway connection.  Remove the route-inheritance logic from chat.send and
always set OriginatingChannel to INTERNAL_MESSAGE_CHANNEL ("webchat").

Closes openclaw#34647

Made-with: Cursor

* Gateway: preserve configured-main connect gating

* Gateway: cover connect-without-client routing

* Gateway: add chat.send session key length limit

* Gateway: cap chat.send session key schema

* Gateway: bound chat.send session key parsing

* Gateway: cover oversized chat.send session keys

* Update CHANGELOG.md

---------

Co-authored-by: SidQin-cyber <[email protected]>
Saitop pushed a commit to NomiciAI/openclaw that referenced this pull request Mar 8, 2026
…al routes (openclaw#38418)

* fix(gateway): prevent webchat messages from cross-routing to external channels

chat.send always originates from the webchat/control-UI surface.  Previously,
channel-scoped session keys (e.g. agent:main:slack:direct:U…) caused
OriginatingChannel to inherit the session's stored external route, so the
reply dispatcher would route responses to Slack/Telegram instead of back to
the gateway connection.  Remove the route-inheritance logic from chat.send and
always set OriginatingChannel to INTERNAL_MESSAGE_CHANNEL ("webchat").

Closes openclaw#34647

Made-with: Cursor

* Gateway: preserve configured-main connect gating

* Gateway: cover connect-without-client routing

* Gateway: add chat.send session key length limit

* Gateway: cap chat.send session key schema

* Gateway: bound chat.send session key parsing

* Gateway: cover oversized chat.send session keys

* Update CHANGELOG.md

---------

Co-authored-by: SidQin-cyber <[email protected]>
jenawant pushed a commit to jenawant/openclaw that referenced this pull request Mar 10, 2026
…al routes (openclaw#38418)

* fix(gateway): prevent webchat messages from cross-routing to external channels

chat.send always originates from the webchat/control-UI surface.  Previously,
channel-scoped session keys (e.g. agent:main:slack:direct:U…) caused
OriginatingChannel to inherit the session's stored external route, so the
reply dispatcher would route responses to Slack/Telegram instead of back to
the gateway connection.  Remove the route-inheritance logic from chat.send and
always set OriginatingChannel to INTERNAL_MESSAGE_CHANNEL ("webchat").

Closes openclaw#34647

Made-with: Cursor

* Gateway: preserve configured-main connect gating

* Gateway: cover connect-without-client routing

* Gateway: add chat.send session key length limit

* Gateway: cap chat.send session key schema

* Gateway: bound chat.send session key parsing

* Gateway: cover oversized chat.send session keys

* Update CHANGELOG.md

---------

Co-authored-by: SidQin-cyber <[email protected]>
dhoman pushed a commit to dhoman/chrono-claw that referenced this pull request Mar 11, 2026
…al routes (openclaw#38418)

* fix(gateway): prevent webchat messages from cross-routing to external channels

chat.send always originates from the webchat/control-UI surface.  Previously,
channel-scoped session keys (e.g. agent:main:slack:direct:U…) caused
OriginatingChannel to inherit the session's stored external route, so the
reply dispatcher would route responses to Slack/Telegram instead of back to
the gateway connection.  Remove the route-inheritance logic from chat.send and
always set OriginatingChannel to INTERNAL_MESSAGE_CHANNEL ("webchat").

Closes openclaw#34647

Made-with: Cursor

* Gateway: preserve configured-main connect gating

* Gateway: cover connect-without-client routing

* Gateway: add chat.send session key length limit

* Gateway: cap chat.send session key schema

* Gateway: bound chat.send session key parsing

* Gateway: cover oversized chat.send session keys

* Update CHANGELOG.md

---------

Co-authored-by: SidQin-cyber <[email protected]>
senw-developers pushed a commit to senw-developers/va-openclaw that referenced this pull request Mar 17, 2026
…al routes (openclaw#38418)

* fix(gateway): prevent webchat messages from cross-routing to external channels

chat.send always originates from the webchat/control-UI surface.  Previously,
channel-scoped session keys (e.g. agent:main:slack:direct:U…) caused
OriginatingChannel to inherit the session's stored external route, so the
reply dispatcher would route responses to Slack/Telegram instead of back to
the gateway connection.  Remove the route-inheritance logic from chat.send and
always set OriginatingChannel to INTERNAL_MESSAGE_CHANNEL ("webchat").

Closes openclaw#34647

Made-with: Cursor

* Gateway: preserve configured-main connect gating

* Gateway: cover connect-without-client routing

* Gateway: add chat.send session key length limit

* Gateway: cap chat.send session key schema

* Gateway: bound chat.send session key parsing

* Gateway: cover oversized chat.send session keys

* Update CHANGELOG.md

---------

Co-authored-by: SidQin-cyber <[email protected]>
V-Gutierrez pushed a commit to V-Gutierrez/openclaw-vendor that referenced this pull request Mar 17, 2026
…al routes (openclaw#38418)

* fix(gateway): prevent webchat messages from cross-routing to external channels

chat.send always originates from the webchat/control-UI surface.  Previously,
channel-scoped session keys (e.g. agent:main:slack:direct:U…) caused
OriginatingChannel to inherit the session's stored external route, so the
reply dispatcher would route responses to Slack/Telegram instead of back to
the gateway connection.  Remove the route-inheritance logic from chat.send and
always set OriginatingChannel to INTERNAL_MESSAGE_CHANNEL ("webchat").

Closes openclaw#34647

Made-with: Cursor

* Gateway: preserve configured-main connect gating

* Gateway: cover connect-without-client routing

* Gateway: add chat.send session key length limit

* Gateway: cap chat.send session key schema

* Gateway: bound chat.send session key parsing

* Gateway: cover oversized chat.send session keys

* Update CHANGELOG.md

---------

Co-authored-by: SidQin-cyber <[email protected]>
alexey-pelykh pushed a commit to remoteclaw/remoteclaw that referenced this pull request Mar 20, 2026
…al routes (openclaw#38418)

* fix(gateway): prevent webchat messages from cross-routing to external channels

chat.send always originates from the webchat/control-UI surface.  Previously,
channel-scoped session keys (e.g. agent:main:slack:direct:U…) caused
OriginatingChannel to inherit the session's stored external route, so the
reply dispatcher would route responses to Slack/Telegram instead of back to
the gateway connection.  Remove the route-inheritance logic from chat.send and
always set OriginatingChannel to INTERNAL_MESSAGE_CHANNEL ("webchat").

Closes openclaw#34647

Made-with: Cursor

* Gateway: preserve configured-main connect gating

* Gateway: cover connect-without-client routing

* Gateway: add chat.send session key length limit

* Gateway: cap chat.send session key schema

* Gateway: bound chat.send session key parsing

* Gateway: cover oversized chat.send session keys

* Update CHANGELOG.md

---------

Co-authored-by: SidQin-cyber <[email protected]>
(cherry picked from commit 563a125)
alexey-pelykh pushed a commit to remoteclaw/remoteclaw that referenced this pull request Mar 20, 2026
…al routes (openclaw#38418)

* fix(gateway): prevent webchat messages from cross-routing to external channels

chat.send always originates from the webchat/control-UI surface.  Previously,
channel-scoped session keys (e.g. agent:main:slack:direct:U…) caused
OriginatingChannel to inherit the session's stored external route, so the
reply dispatcher would route responses to Slack/Telegram instead of back to
the gateway connection.  Remove the route-inheritance logic from chat.send and
always set OriginatingChannel to INTERNAL_MESSAGE_CHANNEL ("webchat").

Closes openclaw#34647

Made-with: Cursor

* Gateway: preserve configured-main connect gating

* Gateway: cover connect-without-client routing

* Gateway: add chat.send session key length limit

* Gateway: cap chat.send session key schema

* Gateway: bound chat.send session key parsing

* Gateway: cover oversized chat.send session keys

* Update CHANGELOG.md

---------

Co-authored-by: SidQin-cyber <[email protected]>
(cherry picked from commit 563a125)
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 maintainer Maintainer-authored PR size: M

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