fix: stop main-session UI replies inheriting channel routes

This commit is contained in:
Ayaan Zaidi
2026-03-12 15:26:20 +05:30
parent 8ea79b64d0
commit 5acf6cae8e
2 changed files with 56 additions and 6 deletions

View File

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

View File

@@ -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 &&