From f62db7950a976cb66caef485737ba4d5ca6cbf16 Mon Sep 17 00:00:00 2001 From: Vignesh Natarajan Date: Fri, 3 Apr 2026 20:39:29 -0700 Subject: [PATCH] fix(control-ui): keep session key helpers browser-safe --- src/routing/session-key.test.ts | 2 +- src/sessions/send-policy.ts | 2 +- src/sessions/session-chat-type.ts | 62 +++++++++++++++++++++++++++++++ src/sessions/session-key-utils.ts | 39 ------------------- 4 files changed, 64 insertions(+), 41 deletions(-) create mode 100644 src/sessions/session-chat-type.ts diff --git a/src/routing/session-key.test.ts b/src/routing/session-key.test.ts index a5415e468df..bfe3ab50261 100644 --- a/src/routing/session-key.test.ts +++ b/src/routing/session-key.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; +import { deriveSessionChatType } from "../sessions/session-chat-type.js"; import { - deriveSessionChatType, getSubagentDepth, isCronSessionKey, parseThreadSessionSuffix, diff --git a/src/sessions/send-policy.ts b/src/sessions/send-policy.ts index e41a93f7cde..dbc7dedd3b5 100644 --- a/src/sessions/send-policy.ts +++ b/src/sessions/send-policy.ts @@ -1,7 +1,7 @@ import { normalizeChatType } from "../channels/chat-type.js"; import type { OpenClawConfig } from "../config/config.js"; import type { SessionChatType, SessionEntry } from "../config/sessions.js"; -import { deriveSessionChatType } from "./session-key-utils.js"; +import { deriveSessionChatType } from "./session-chat-type.js"; export type SessionSendPolicyDecision = "allow" | "deny"; diff --git a/src/sessions/session-chat-type.ts b/src/sessions/session-chat-type.ts new file mode 100644 index 00000000000..94994b82189 --- /dev/null +++ b/src/sessions/session-chat-type.ts @@ -0,0 +1,62 @@ +import { getBundledChannelContractSurfaces } from "../channels/plugins/contract-surfaces.js"; +import { parseAgentSessionKey } from "./session-key-utils.js"; + +export type SessionKeyChatType = "direct" | "group" | "channel" | "unknown"; + +type LegacySessionChatTypeSurface = { + deriveLegacySessionChatType?: (sessionKey: string) => "direct" | "group" | "channel" | undefined; +}; + +function listLegacySessionChatTypeSurfaces(): LegacySessionChatTypeSurface[] { + return getBundledChannelContractSurfaces() as LegacySessionChatTypeSurface[]; +} + +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 surface of listLegacySessionChatTypeSurfaces()) { + const derived = surface.deriveLegacySessionChatType?.(scoped); + if (derived) { + return derived; + } + } + return "unknown"; +} diff --git a/src/sessions/session-key-utils.ts b/src/sessions/session-key-utils.ts index ee6dd6e7ec4..00e4b173179 100644 --- a/src/sessions/session-key-utils.ts +++ b/src/sessions/session-key-utils.ts @@ -1,11 +1,8 @@ -import { getBundledChannelContractSurfaces } from "../channels/plugins/contract-surfaces.js"; - export type ParsedAgentSessionKey = { agentId: string; rest: string; }; -export type SessionKeyChatType = "direct" | "group" | "channel" | "unknown"; export type ParsedThreadSessionSuffix = { baseSessionKey: string | undefined; threadId: string | undefined; @@ -18,14 +15,6 @@ export type RawSessionConversationRef = { prefix: string; }; -type LegacySessionChatTypeSurface = { - deriveLegacySessionChatType?: (sessionKey: string) => "direct" | "group" | "channel" | undefined; -}; - -function listLegacySessionChatTypeSurfaces(): LegacySessionChatTypeSurface[] { - return getBundledChannelContractSurfaces() as LegacySessionChatTypeSurface[]; -} - /** * Parse agent-scoped session keys in a canonical, case-insensitive way. * Returned values are normalized to lowercase for stable comparisons/routing. @@ -52,34 +41,6 @@ export function parseAgentSessionKey( return { agentId, rest }; } -/** - * 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"; - } - for (const surface of listLegacySessionChatTypeSurfaces()) { - const derived = surface.deriveLegacySessionChatType?.(scoped); - if (derived) { - return derived; - } - } - return "unknown"; -} - export function isCronRunSessionKey(sessionKey: string | undefined | null): boolean { const parsed = parseAgentSessionKey(sessionKey); if (!parsed) {