Files
openclaw/src/sessions/session-key-utils.ts
2026-04-07 13:44:42 +01:00

164 lines
4.6 KiB
TypeScript

import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalLowercaseString,
normalizeOptionalString,
} from "../shared/string-coerce.js";
export type ParsedAgentSessionKey = {
agentId: string;
rest: string;
};
export type ParsedThreadSessionSuffix = {
baseSessionKey: string | undefined;
threadId: string | undefined;
};
export type RawSessionConversationRef = {
channel: string;
kind: "group" | "channel";
rawId: string;
prefix: string;
};
/**
* 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 = normalizeOptionalLowercaseString(sessionKey);
if (!raw) {
return null;
}
const parts = raw.split(":").filter(Boolean);
if (parts.length < 3) {
return null;
}
if (parts[0] !== "agent") {
return null;
}
const agentId = normalizeOptionalString(parts[1]);
const rest = parts.slice(2).join(":");
if (!agentId || !rest) {
return null;
}
return { agentId, rest };
}
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 normalizeOptionalLowercaseString(parsed.rest)?.startsWith("cron:") === true;
}
export function isSubagentSessionKey(sessionKey: string | undefined | null): boolean {
const raw = normalizeOptionalString(sessionKey);
if (!raw) {
return false;
}
if (normalizeOptionalLowercaseString(raw)?.startsWith("subagent:")) {
return true;
}
const parsed = parseAgentSessionKey(raw);
return normalizeOptionalLowercaseString(parsed?.rest)?.startsWith("subagent:") === true;
}
export function getSubagentDepth(sessionKey: string | undefined | null): number {
const raw = normalizeOptionalLowercaseString(sessionKey);
if (!raw) {
return 0;
}
return raw.split(":subagent:").length - 1;
}
export function isAcpSessionKey(sessionKey: string | undefined | null): boolean {
const raw = normalizeOptionalString(sessionKey);
if (!raw) {
return false;
}
const normalized = normalizeLowercaseStringOrEmpty(raw);
if (normalized.startsWith("acp:")) {
return true;
}
const parsed = parseAgentSessionKey(raw);
return normalizeOptionalLowercaseString(parsed?.rest)?.startsWith("acp:") === true;
}
export function parseThreadSessionSuffix(
sessionKey: string | undefined | null,
): ParsedThreadSessionSuffix {
const raw = normalizeOptionalString(sessionKey);
if (!raw) {
return { baseSessionKey: undefined, threadId: undefined };
}
const lowerRaw = normalizeLowercaseStringOrEmpty(raw);
const threadMarker = ":thread:";
const threadIndex = lowerRaw.lastIndexOf(threadMarker);
const markerIndex = threadIndex;
const marker = threadMarker;
const baseSessionKey = markerIndex === -1 ? raw : raw.slice(0, markerIndex);
const threadIdRaw = markerIndex === -1 ? undefined : raw.slice(markerIndex + marker.length);
const threadId = normalizeOptionalString(threadIdRaw);
return { baseSessionKey, threadId };
}
export function parseRawSessionConversationRef(
sessionKey: string | undefined | null,
): RawSessionConversationRef | null {
const raw = normalizeOptionalString(sessionKey);
if (!raw) {
return null;
}
const rawParts = raw.split(":").filter(Boolean);
const bodyStartIndex =
rawParts.length >= 3 && normalizeOptionalLowercaseString(rawParts[0]) === "agent" ? 2 : 0;
const parts = rawParts.slice(bodyStartIndex);
if (parts.length < 3) {
return null;
}
const channel = normalizeOptionalLowercaseString(parts[0]);
const kind = normalizeOptionalLowercaseString(parts[1]);
if (!channel || (kind !== "group" && kind !== "channel")) {
return null;
}
const rawId = normalizeOptionalString(parts.slice(2).join(":"));
const prefix = normalizeOptionalString(rawParts.slice(0, bodyStartIndex + 2).join(":"));
if (!rawId || !prefix) {
return null;
}
return { channel, kind, rawId, prefix };
}
export function resolveThreadParentSessionKey(
sessionKey: string | undefined | null,
): string | null {
const { baseSessionKey, threadId } = parseThreadSessionSuffix(sessionKey);
if (!threadId) {
return null;
}
const parent = normalizeOptionalString(baseSessionKey);
if (!parent) {
return null;
}
return parent;
}