From 9fed9f13023091b7d0d9d883113bc35a75a6f082 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Fri, 6 Mar 2026 08:53:16 -0500 Subject: [PATCH] fix(session): tighten direct-session webchat routing matching (#37867) * fix(session): require strict direct key routing shapes * test(session): cover direct route poisoning cases --- src/auto-reply/reply/session-delivery.test.ts | 56 +++++++++++++++++++ src/auto-reply/reply/session-delivery.ts | 40 ++++++++++++- 2 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 src/auto-reply/reply/session-delivery.test.ts diff --git a/src/auto-reply/reply/session-delivery.test.ts b/src/auto-reply/reply/session-delivery.test.ts new file mode 100644 index 00000000000..2bfb4812f64 --- /dev/null +++ b/src/auto-reply/reply/session-delivery.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from "vitest"; +import { resolveLastChannelRaw, resolveLastToRaw } from "./session-delivery.js"; + +describe("session delivery direct-session routing overrides", () => { + it.each([ + "agent:main:direct:user-1", + "agent:main:telegram:direct:123456", + "agent:main:telegram:account-a:direct:123456", + "agent:main:telegram:dm:123456", + "agent:main:telegram:direct:123456:thread:99", + "agent:main:telegram:account-a:direct:123456:topic:ops", + ])("lets webchat override persisted routes for strict direct key %s", (sessionKey) => { + expect( + resolveLastChannelRaw({ + originatingChannelRaw: "webchat", + persistedLastChannel: "telegram", + sessionKey, + }), + ).toBe("webchat"); + expect( + resolveLastToRaw({ + originatingChannelRaw: "webchat", + originatingToRaw: "session:dashboard", + persistedLastChannel: "telegram", + persistedLastTo: "123456", + sessionKey, + }), + ).toBe("session:dashboard"); + }); + + it.each([ + "agent:main:main:direct", + "agent:main:cron:job-1:dm", + "agent:main:subagent:worker:direct:user-1", + "agent:main:telegram:channel:direct", + "agent:main:telegram:account-a:direct", + "agent:main:telegram:direct:123456:cron:job-1", + ])("keeps persisted external routes for malformed direct-like key %s", (sessionKey) => { + expect( + resolveLastChannelRaw({ + originatingChannelRaw: "webchat", + persistedLastChannel: "telegram", + sessionKey, + }), + ).toBe("telegram"); + expect( + resolveLastToRaw({ + originatingChannelRaw: "webchat", + originatingToRaw: "session:dashboard", + persistedLastChannel: "telegram", + persistedLastTo: "group:12345", + sessionKey, + }), + ).toBe("group:12345"); + }); +}); diff --git a/src/auto-reply/reply/session-delivery.ts b/src/auto-reply/reply/session-delivery.ts index 86370f544ef..ef2f0cde227 100644 --- a/src/auto-reply/reply/session-delivery.ts +++ b/src/auto-reply/reply/session-delivery.ts @@ -1,6 +1,6 @@ import type { SessionEntry } from "../../config/sessions.js"; import { buildAgentMainSessionKey } from "../../routing/session-key.js"; -import { deriveSessionChatType, parseAgentSessionKey } from "../../sessions/session-key-utils.js"; +import { parseAgentSessionKey } from "../../sessions/session-key-utils.js"; import { deliveryContextFromSession, deliveryContextKey, @@ -38,8 +38,44 @@ function isMainSessionKey(sessionKey?: string): boolean { return parsed.rest.trim().toLowerCase() === "main"; } +const DIRECT_SESSION_MARKERS = new Set(["direct", "dm"]); +const THREAD_SESSION_MARKERS = new Set(["thread", "topic"]); + +function hasStrictDirectSessionTail(parts: string[], markerIndex: number): boolean { + const peerId = parts[markerIndex + 1]?.trim(); + if (!peerId) { + return false; + } + const tail = parts.slice(markerIndex + 2); + if (tail.length === 0) { + return true; + } + return tail.length === 2 && THREAD_SESSION_MARKERS.has(tail[0] ?? "") && Boolean(tail[1]?.trim()); +} + function isDirectSessionKey(sessionKey?: string): boolean { - return deriveSessionChatType(sessionKey) === "direct"; + const raw = (sessionKey ?? "").trim().toLowerCase(); + if (!raw) { + return false; + } + const scoped = parseAgentSessionKey(raw)?.rest ?? raw; + const parts = scoped.split(":").filter(Boolean); + if (parts.length < 2) { + return false; + } + if (DIRECT_SESSION_MARKERS.has(parts[0] ?? "")) { + return hasStrictDirectSessionTail(parts, 0); + } + const channel = normalizeMessageChannel(parts[0]); + if (!channel || !isDeliverableMessageChannel(channel)) { + return false; + } + if (DIRECT_SESSION_MARKERS.has(parts[1] ?? "")) { + return hasStrictDirectSessionTail(parts, 1); + } + return Boolean(parts[1]?.trim()) && DIRECT_SESSION_MARKERS.has(parts[2] ?? "") + ? hasStrictDirectSessionTail(parts, 2) + : false; } function isExternalRoutingChannel(channel?: string): channel is string {