From c569e5faba0509ead67a24373b6cb77690491b2e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 7 Apr 2026 12:50:59 +0800 Subject: [PATCH] Sessions: split chat type derivation seam --- src/routing/session-key.test.ts | 6 +-- src/sessions/session-chat-type-shared.ts | 67 +++++++++++++++++++++++ src/sessions/session-chat-type.ts | 68 +++++++----------------- 3 files changed, 89 insertions(+), 52 deletions(-) create mode 100644 src/sessions/session-chat-type-shared.ts diff --git a/src/routing/session-key.test.ts b/src/routing/session-key.test.ts index bfe3ab50261..56cbdd234a9 100644 --- a/src/routing/session-key.test.ts +++ b/src/routing/session-key.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { deriveSessionChatType } from "../sessions/session-chat-type.js"; +import { deriveSessionChatTypeFromKey } from "../sessions/session-chat-type-shared.js"; import { getSubagentDepth, isCronSessionKey, @@ -70,7 +70,7 @@ describe("isCronSessionKey", () => { }); }); -describe("deriveSessionChatType", () => { +describe("deriveSessionChatTypeFromKey", () => { it.each([ { key: "agent:main:discord:direct:user1", expected: "direct" }, { key: "agent:main:telegram:group:g1", expected: "group" }, @@ -83,7 +83,7 @@ describe("deriveSessionChatType", () => { { key: "agent:main", expected: "unknown" }, { key: "", expected: "unknown" }, ] as const)("derives chat type for %j => $expected", ({ key, expected }) => { - expect(deriveSessionChatType(key)).toBe(expected); + expect(deriveSessionChatTypeFromKey(key)).toBe(expected); }); }); diff --git a/src/sessions/session-chat-type-shared.ts b/src/sessions/session-chat-type-shared.ts new file mode 100644 index 00000000000..92c6566683d --- /dev/null +++ b/src/sessions/session-chat-type-shared.ts @@ -0,0 +1,67 @@ +import { parseAgentSessionKey } from "./session-key-utils.js"; + +export type SessionKeyChatType = "direct" | "group" | "channel" | "unknown"; + +function deriveBuiltInLegacySessionChatType( + scopedSessionKey: string, +): SessionKeyChatType | undefined { + if (/^group:[^:]+$/.test(scopedSessionKey)) { + return "group"; + } + if (/^[0-9]+(?:-[0-9]+)*@g\.us$/.test(scopedSessionKey)) { + return "group"; + } + if (/^whatsapp:(?!.*:group:).+@g\.us$/.test(scopedSessionKey)) { + return "group"; + } + if (/^discord:(?:[^:]+:)?guild-[^:]+:channel-[^:]+$/.test(scopedSessionKey)) { + return "channel"; + } + return undefined; +} + +export function deriveSessionChatTypeFromScopedKey( + scopedSessionKey: string, + deriveLegacySessionChatTypes: Array< + (scopedSessionKey: string) => SessionKeyChatType | undefined + > = [], +): SessionKeyChatType { + const tokens = new Set(scopedSessionKey.split(":").filter(Boolean)); + if (tokens.has("group")) { + return "group"; + } + if (tokens.has("channel")) { + return "channel"; + } + if (tokens.has("direct") || tokens.has("dm")) { + return "direct"; + } + const builtInLegacy = deriveBuiltInLegacySessionChatType(scopedSessionKey); + if (builtInLegacy) { + return builtInLegacy; + } + for (const deriveLegacySessionChatType of deriveLegacySessionChatTypes) { + const derived = deriveLegacySessionChatType(scopedSessionKey); + if (derived) { + return derived; + } + } + return "unknown"; +} + +/** + * Best-effort chat-type extraction from session keys across canonical and legacy formats. + */ +export function deriveSessionChatTypeFromKey( + sessionKey: string | undefined | null, + deriveLegacySessionChatTypes: Array< + (scopedSessionKey: string) => SessionKeyChatType | undefined + > = [], +): SessionKeyChatType { + const raw = (sessionKey ?? "").trim().toLowerCase(); + if (!raw) { + return "unknown"; + } + const scoped = parseAgentSessionKey(raw)?.rest ?? raw; + return deriveSessionChatTypeFromScopedKey(scoped, deriveLegacySessionChatTypes); +} diff --git a/src/sessions/session-chat-type.ts b/src/sessions/session-chat-type.ts index 01f533ba38e..febdfb9579c 100644 --- a/src/sessions/session-chat-type.ts +++ b/src/sessions/session-chat-type.ts @@ -1,54 +1,24 @@ import { iterateBootstrapChannelPlugins } from "../channels/plugins/bootstrap-registry.js"; -import { parseAgentSessionKey } from "./session-key-utils.js"; +import { + deriveSessionChatTypeFromKey, + type SessionKeyChatType, +} from "./session-chat-type-shared.js"; -export type SessionKeyChatType = "direct" | "group" | "channel" | "unknown"; +export { + deriveSessionChatTypeFromKey, + type SessionKeyChatType, +} from "./session-chat-type-shared.js"; -function deriveBuiltInLegacySessionChatType( - scopedSessionKey: string, -): SessionKeyChatType | undefined { - if (/^group:[^:]+$/.test(scopedSessionKey)) { - return "group"; - } - if (/^[0-9]+(?:-[0-9]+)*@g\.us$/.test(scopedSessionKey)) { - return "group"; - } - if (/^whatsapp:(?!.*:group:).+@g\.us$/.test(scopedSessionKey)) { - return "group"; - } - if (/^discord:(?:[^:]+:)?guild-[^:]+:channel-[^:]+$/.test(scopedSessionKey)) { - return "channel"; - } - return undefined; -} - -/** - * Best-effort chat-type extraction from session keys across canonical and legacy formats. - */ export function deriveSessionChatType(sessionKey: string | undefined | null): SessionKeyChatType { - const raw = (sessionKey ?? "").trim().toLowerCase(); - if (!raw) { - return "unknown"; - } - const scoped = parseAgentSessionKey(raw)?.rest ?? raw; - const tokens = new Set(scoped.split(":").filter(Boolean)); - if (tokens.has("group")) { - return "group"; - } - if (tokens.has("channel")) { - return "channel"; - } - if (tokens.has("direct") || tokens.has("dm")) { - return "direct"; - } - const builtInLegacy = deriveBuiltInLegacySessionChatType(scoped); - if (builtInLegacy) { - return builtInLegacy; - } - for (const plugin of iterateBootstrapChannelPlugins()) { - const derived = plugin.messaging?.deriveLegacySessionChatType?.(scoped); - if (derived) { - return derived; - } - } - return "unknown"; + return deriveSessionChatTypeFromKey( + sessionKey, + iterateBootstrapChannelPlugins() + .map((plugin) => plugin.messaging?.deriveLegacySessionChatType) + .filter( + ( + deriveLegacySessionChatType, + ): deriveLegacySessionChatType is NonNullable => + Boolean(deriveLegacySessionChatType), + ), + ); }