diff --git a/src/gateway/server-methods/chat.directive-tags.test.ts b/src/gateway/server-methods/chat.directive-tags.test.ts index 0ea0e0181c2..1f72c5d15b5 100644 --- a/src/gateway/server-methods/chat.directive-tags.test.ts +++ b/src/gateway/server-methods/chat.directive-tags.test.ts @@ -4,7 +4,7 @@ import path from "node:path"; import { CURRENT_SESSION_VERSION } from "@mariozechner/pi-coding-agent"; import { afterEach, describe, expect, it, vi } from "vitest"; import type { MsgContext } from "../../auto-reply/templating.js"; -import { GATEWAY_CLIENT_CAPS } from "../protocol/client-info.js"; +import { GATEWAY_CLIENT_CAPS, GATEWAY_CLIENT_MODES } from "../protocol/client-info.js"; import type { GatewayRequestContext } from "./types.js"; const mockState = vi.hoisted(() => ({ @@ -550,6 +550,47 @@ 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", async () => { + createTranscriptFixture("openclaw-chat-send-main-ui-routes-"); + mockState.finalText = "ok"; + mockState.sessionEntry = { + deliveryContext: { + channel: "whatsapp", + to: "whatsapp:+8613800138000", + accountId: "default", + }, + lastChannel: "whatsapp", + lastTo: "whatsapp:+8613800138000", + lastAccountId: "default", + }; + const respond = vi.fn(); + const context = createChatContext(); + + await runNonStreamingChatSend({ + context, + respond, + idempotencyKey: "idem-main-ui-routes", + client: { + connect: { + client: { + mode: GATEWAY_CLIENT_MODES.UI, + id: "openclaw-tui", + }, + }, + } as unknown, + sessionKey: "agent:main:main", + expectBroadcast: false, + }); + + expect(mockState.lastDispatchCtx).toEqual( + expect.objectContaining({ + OriginatingChannel: "webchat", + OriginatingTo: undefined, + AccountId: undefined, + }), + ); + }); + it("chat.send does not inherit external delivery context for non-channel custom sessions", async () => { createTranscriptFixture("openclaw-chat-send-custom-no-cross-route-"); mockState.finalText = "ok"; diff --git a/src/gateway/server-methods/chat.ts b/src/gateway/server-methods/chat.ts index 13feee2d131..e90fcf15611 100644 --- a/src/gateway/server-methods/chat.ts +++ b/src/gateway/server-methods/chat.ts @@ -17,7 +17,11 @@ import { stripInlineDirectiveTagsForDisplay, stripInlineDirectiveTagsFromMessageForDisplay, } from "../../utils/directive-tags.js"; -import { INTERNAL_MESSAGE_CHANNEL, normalizeMessageChannel } from "../../utils/message-channel.js"; +import { + INTERNAL_MESSAGE_CHANNEL, + isWebchatClient, + normalizeMessageChannel, +} from "../../utils/message-channel.js"; import { abortChatRunById, abortChatRunsForSessionKey, @@ -28,7 +32,11 @@ import { } from "../chat-abort.js"; import { type ChatImageContent, parseMessageWithAttachments } from "../chat-attachments.js"; import { stripEnvelopeFromMessage, stripEnvelopeFromMessages } from "../chat-sanitize.js"; -import { GATEWAY_CLIENT_CAPS, hasGatewayClientCap } from "../protocol/client-info.js"; +import { + GATEWAY_CLIENT_CAPS, + GATEWAY_CLIENT_MODES, + hasGatewayClientCap, +} from "../protocol/client-info.js"; import { ErrorCodes, errorShape, @@ -880,14 +888,18 @@ export const chatHandlers: GatewayRequestHandlers = { !isChannelScopedSession && typeof sessionScopeParts[1] === "string" && sessionChannelHint === routeChannelCandidate; - // Only inherit prior external route metadata for channel-scoped sessions. - // Channel-agnostic sessions (main, direct:, etc.) can otherwise - // leak stale routes across surfaces. + const clientMode = client?.connect?.client?.mode; + const isFromWebchatClient = + isWebchatClient(client?.connect?.client) || clientMode === GATEWAY_CLIENT_MODES.UI; + // Channel-agnostic session scopes (main, direct:, etc.) can leak + // stale routes across surfaces. Allow main sessions only from non-Webchat + // clients so CLI replies can keep the last WA/Telegram route. const canInheritDeliverableRoute = Boolean( sessionChannelHint && sessionChannelHint !== INTERNAL_MESSAGE_CHANNEL && - !isChannelAgnosticSessionScope && - (isChannelScopedSession || hasLegacyChannelPeerShape), + ((!isChannelAgnosticSessionScope && + (isChannelScopedSession || hasLegacyChannelPeerShape)) || + (sessionChannelHint === "main" && client?.connect !== undefined && !isFromWebchatClient)), ); const hasDeliverableRoute = canInheritDeliverableRoute &&