Fix: TUI messages route correctly instead of leaking WhatsApp route

This commit is contained in:
octane0411
2026-03-05 12:20:17 +08:00
committed by Tak Hoffman
parent 1805735c63
commit 80f37eb1a3
2 changed files with 61 additions and 8 deletions

View File

@@ -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";

View File

@@ -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:<peer>, 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:<peer>, 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 &&