From 5acf6cae8e8ce5a4a9e24ee4b165490559e450e9 Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Thu, 12 Mar 2026 15:26:20 +0530 Subject: [PATCH] fix: stop main-session UI replies inheriting channel routes --- .../chat.directive-tags.test.ts | 43 +++++++++++++++++++ src/gateway/server-methods/chat.ts | 19 +++++--- 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/src/gateway/server-methods/chat.directive-tags.test.ts b/src/gateway/server-methods/chat.directive-tags.test.ts index 1415ef6d6..06b642b28 100644 --- a/src/gateway/server-methods/chat.directive-tags.test.ts +++ b/src/gateway/server-methods/chat.directive-tags.test.ts @@ -656,6 +656,49 @@ describe("chat directive tag stripping for non-streaming final payloads", () => ); }); + it("chat.send does not inherit external delivery context for UI clients on main sessions when deliver is enabled", async () => { + createTranscriptFixture("openclaw-chat-send-main-ui-deliver-no-route-"); + mockState.finalText = "ok"; + mockState.sessionEntry = { + deliveryContext: { + channel: "telegram", + to: "telegram:200482621", + accountId: "default", + }, + lastChannel: "telegram", + lastTo: "telegram:200482621", + lastAccountId: "default", + }; + const respond = vi.fn(); + const context = createChatContext(); + + await runNonStreamingChatSend({ + context, + respond, + idempotencyKey: "idem-main-ui-deliver-no-route", + client: { + connect: { + client: { + mode: GATEWAY_CLIENT_MODES.UI, + id: "openclaw-tui", + }, + }, + } as unknown, + sessionKey: "agent:main:main", + deliver: true, + expectBroadcast: false, + }); + + expect(mockState.lastDispatchCtx).toEqual( + expect.objectContaining({ + OriginatingChannel: "webchat", + OriginatingTo: undefined, + ExplicitDeliverRoute: false, + AccountId: undefined, + }), + ); + }); + it("chat.send inherits external delivery context for CLI clients on configured main sessions", async () => { createTranscriptFixture("openclaw-chat-send-config-main-cli-routes-"); mockState.mainSessionKey = "work"; diff --git a/src/gateway/server-methods/chat.ts b/src/gateway/server-methods/chat.ts index 13f3b9978..857868c59 100644 --- a/src/gateway/server-methods/chat.ts +++ b/src/gateway/server-methods/chat.ts @@ -20,6 +20,7 @@ import { } from "../../utils/directive-tags.js"; import { INTERNAL_MESSAGE_CHANNEL, + isGatewayCliClient, isWebchatClient, normalizeMessageChannel, } from "../../utils/message-channel.js"; @@ -181,21 +182,27 @@ function resolveChatSendOriginatingRoute(params: { typeof sessionScopeParts[1] === "string" && sessionChannelHint === routeChannelCandidate; const isFromWebchatClient = isWebchatClient(params.client); + const isFromGatewayCliClient = isGatewayCliClient(params.client); + const hasClientMetadata = + (typeof params.client?.mode === "string" && params.client.mode.trim().length > 0) || + (typeof params.client?.id === "string" && params.client.id.trim().length > 0); const configuredMainKey = (params.mainKey ?? "main").trim().toLowerCase(); const isConfiguredMainSessionScope = normalizedSessionScopeHead.length > 0 && normalizedSessionScopeHead === configuredMainKey; + const canInheritConfiguredMainRoute = + isConfiguredMainSessionScope && + params.hasConnectedClient && + (isFromGatewayCliClient || !hasClientMetadata); - // Webchat/Control UI clients never inherit external delivery routes, even when - // accessing channel-scoped sessions. External routes are only for non-webchat - // clients where the session key explicitly encodes an external target. - // Preserve the old configured-main contract: any connected non-webchat client - // may inherit the last external route even when client metadata is absent. + // Webchat clients never inherit external delivery routes. Configured-main + // sessions are stricter than channel-scoped sessions: only CLI callers, or + // legacy callers with no client metadata, may inherit the last external route. const canInheritDeliverableRoute = Boolean( !isFromWebchatClient && sessionChannelHint && sessionChannelHint !== INTERNAL_MESSAGE_CHANNEL && ((!isChannelAgnosticSessionScope && (isChannelScopedSession || hasLegacyChannelPeerShape)) || - (isConfiguredMainSessionScope && params.hasConnectedClient)), + canInheritConfiguredMainRoute), ); const hasDeliverableRoute = canInheritDeliverableRoute &&