openclaw/src/sessions/session-key-utils.ts

133 lines
3.6 KiB
TypeScript

export type ParsedAgentSessionKey = {
agentId: string;
rest: string;
};
export type SessionKeyChatType = "direct" | "group" | "channel" | "unknown";
/**
* Parse agent-scoped session keys in a canonical, case-insensitive way.
* Returned values are normalized to lowercase for stable comparisons/routing.
*/
export function parseAgentSessionKey(
sessionKey: string | undefined | null,
): ParsedAgentSessionKey | null {
const raw = (sessionKey ?? "").trim().toLowerCase();
if (!raw) {
return null;
}
const parts = raw.split(":").filter(Boolean);
if (parts.length < 3) {
return null;
}
if (parts[0] !== "agent") {
return null;
}
const agentId = parts[1]?.trim();
const rest = parts.slice(2).join(":");
if (!agentId || !rest) {
return null;
}
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";
}
// Legacy Discord keys can be shaped like:
// discord:<accountId>:guild-<guildId>:channel-<channelId>
if (/^discord:(?:[^:]+:)?guild-[^:]+:channel-[^:]+$/.test(scoped)) {
return "channel";
}
return "unknown";
}
export function isCronRunSessionKey(sessionKey: string | undefined | null): boolean {
const parsed = parseAgentSessionKey(sessionKey);
if (!parsed) {
return false;
}
return /^cron:[^:]+:run:[^:]+$/.test(parsed.rest);
}
export function isCronSessionKey(sessionKey: string | undefined | null): boolean {
const parsed = parseAgentSessionKey(sessionKey);
if (!parsed) {
return false;
}
return parsed.rest.toLowerCase().startsWith("cron:");
}
export function isSubagentSessionKey(sessionKey: string | undefined | null): boolean {
const raw = (sessionKey ?? "").trim();
if (!raw) {
return false;
}
if (raw.toLowerCase().startsWith("subagent:")) {
return true;
}
const parsed = parseAgentSessionKey(raw);
return Boolean((parsed?.rest ?? "").toLowerCase().startsWith("subagent:"));
}
export function getSubagentDepth(sessionKey: string | undefined | null): number {
const raw = (sessionKey ?? "").trim().toLowerCase();
if (!raw) {
return 0;
}
return raw.split(":subagent:").length - 1;
}
export function isAcpSessionKey(sessionKey: string | undefined | null): boolean {
const raw = (sessionKey ?? "").trim();
if (!raw) {
return false;
}
const normalized = raw.toLowerCase();
if (normalized.startsWith("acp:")) {
return true;
}
const parsed = parseAgentSessionKey(raw);
return Boolean((parsed?.rest ?? "").toLowerCase().startsWith("acp:"));
}
const THREAD_SESSION_MARKERS = [":thread:", ":topic:"];
export function resolveThreadParentSessionKey(
sessionKey: string | undefined | null,
): string | null {
const raw = (sessionKey ?? "").trim();
if (!raw) {
return null;
}
const normalized = raw.toLowerCase();
let idx = -1;
for (const marker of THREAD_SESSION_MARKERS) {
const candidate = normalized.lastIndexOf(marker);
if (candidate > idx) {
idx = candidate;
}
}
if (idx <= 0) {
return null;
}
const parent = raw.slice(0, idx).trim();
return parent ? parent : null;
}