mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
191 lines
5.6 KiB
TypeScript
191 lines
5.6 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
import type { AcpSession } from "./types.js";
|
|
|
|
export type AcpSessionStore = {
|
|
createSession: (params: { sessionKey: string; cwd: string; sessionId?: string }) => AcpSession;
|
|
hasSession: (sessionId: string) => boolean;
|
|
getSession: (sessionId: string) => AcpSession | undefined;
|
|
getSessionByRunId: (runId: string) => AcpSession | undefined;
|
|
setActiveRun: (sessionId: string, runId: string, abortController: AbortController) => void;
|
|
clearActiveRun: (sessionId: string) => void;
|
|
cancelActiveRun: (sessionId: string) => boolean;
|
|
clearAllSessionsForTest: () => void;
|
|
};
|
|
|
|
type AcpSessionStoreOptions = {
|
|
maxSessions?: number;
|
|
idleTtlMs?: number;
|
|
now?: () => number;
|
|
};
|
|
|
|
const DEFAULT_MAX_SESSIONS = 5_000;
|
|
const DEFAULT_IDLE_TTL_MS = 24 * 60 * 60 * 1_000;
|
|
|
|
export function createInMemorySessionStore(options: AcpSessionStoreOptions = {}): AcpSessionStore {
|
|
const maxSessions = Math.max(1, options.maxSessions ?? DEFAULT_MAX_SESSIONS);
|
|
const idleTtlMs = Math.max(1_000, options.idleTtlMs ?? DEFAULT_IDLE_TTL_MS);
|
|
const now = options.now ?? Date.now;
|
|
const sessions = new Map<string, AcpSession>();
|
|
const runIdToSessionId = new Map<string, string>();
|
|
|
|
const touchSession = (session: AcpSession, nowMs: number) => {
|
|
session.lastTouchedAt = nowMs;
|
|
};
|
|
|
|
const removeSession = (sessionId: string) => {
|
|
const session = sessions.get(sessionId);
|
|
if (!session) {
|
|
return false;
|
|
}
|
|
if (session.activeRunId) {
|
|
runIdToSessionId.delete(session.activeRunId);
|
|
}
|
|
session.abortController?.abort();
|
|
sessions.delete(sessionId);
|
|
return true;
|
|
};
|
|
|
|
const reapIdleSessions = (nowMs: number) => {
|
|
const idleBefore = nowMs - idleTtlMs;
|
|
for (const [sessionId, session] of sessions.entries()) {
|
|
if (session.activeRunId || session.abortController) {
|
|
continue;
|
|
}
|
|
if (session.lastTouchedAt > idleBefore) {
|
|
continue;
|
|
}
|
|
removeSession(sessionId);
|
|
}
|
|
};
|
|
|
|
const evictOldestIdleSession = () => {
|
|
let oldestSessionId: string | null = null;
|
|
let oldestLastTouchedAt = Number.POSITIVE_INFINITY;
|
|
for (const [sessionId, session] of sessions.entries()) {
|
|
if (session.activeRunId || session.abortController) {
|
|
continue;
|
|
}
|
|
if (session.lastTouchedAt >= oldestLastTouchedAt) {
|
|
continue;
|
|
}
|
|
oldestLastTouchedAt = session.lastTouchedAt;
|
|
oldestSessionId = sessionId;
|
|
}
|
|
if (!oldestSessionId) {
|
|
return false;
|
|
}
|
|
return removeSession(oldestSessionId);
|
|
};
|
|
|
|
const createSession: AcpSessionStore["createSession"] = (params) => {
|
|
const nowMs = now();
|
|
const sessionId = params.sessionId ?? randomUUID();
|
|
const existingSession = sessions.get(sessionId);
|
|
if (existingSession) {
|
|
existingSession.sessionKey = params.sessionKey;
|
|
existingSession.cwd = params.cwd;
|
|
touchSession(existingSession, nowMs);
|
|
return existingSession;
|
|
}
|
|
reapIdleSessions(nowMs);
|
|
if (sessions.size >= maxSessions && !evictOldestIdleSession()) {
|
|
throw new Error(
|
|
`ACP session limit reached (max ${maxSessions}). Close idle ACP clients and retry.`,
|
|
);
|
|
}
|
|
const session: AcpSession = {
|
|
sessionId,
|
|
sessionKey: params.sessionKey,
|
|
cwd: params.cwd,
|
|
createdAt: nowMs,
|
|
lastTouchedAt: nowMs,
|
|
abortController: null,
|
|
activeRunId: null,
|
|
};
|
|
sessions.set(sessionId, session);
|
|
return session;
|
|
};
|
|
|
|
const hasSession: AcpSessionStore["hasSession"] = (sessionId) => sessions.has(sessionId);
|
|
|
|
const getSession: AcpSessionStore["getSession"] = (sessionId) => {
|
|
const session = sessions.get(sessionId);
|
|
if (session) {
|
|
touchSession(session, now());
|
|
}
|
|
return session;
|
|
};
|
|
|
|
const getSessionByRunId: AcpSessionStore["getSessionByRunId"] = (runId) => {
|
|
const sessionId = runIdToSessionId.get(runId);
|
|
if (!sessionId) {
|
|
return undefined;
|
|
}
|
|
const session = sessions.get(sessionId);
|
|
if (session) {
|
|
touchSession(session, now());
|
|
}
|
|
return session;
|
|
};
|
|
|
|
const setActiveRun: AcpSessionStore["setActiveRun"] = (sessionId, runId, abortController) => {
|
|
const session = sessions.get(sessionId);
|
|
if (!session) {
|
|
return;
|
|
}
|
|
session.activeRunId = runId;
|
|
session.abortController = abortController;
|
|
runIdToSessionId.set(runId, sessionId);
|
|
touchSession(session, now());
|
|
};
|
|
|
|
const clearActiveRun: AcpSessionStore["clearActiveRun"] = (sessionId) => {
|
|
const session = sessions.get(sessionId);
|
|
if (!session) {
|
|
return;
|
|
}
|
|
if (session.activeRunId) {
|
|
runIdToSessionId.delete(session.activeRunId);
|
|
}
|
|
session.activeRunId = null;
|
|
session.abortController = null;
|
|
touchSession(session, now());
|
|
};
|
|
|
|
const cancelActiveRun: AcpSessionStore["cancelActiveRun"] = (sessionId) => {
|
|
const session = sessions.get(sessionId);
|
|
if (!session?.abortController) {
|
|
return false;
|
|
}
|
|
session.abortController.abort();
|
|
if (session.activeRunId) {
|
|
runIdToSessionId.delete(session.activeRunId);
|
|
}
|
|
session.abortController = null;
|
|
session.activeRunId = null;
|
|
touchSession(session, now());
|
|
return true;
|
|
};
|
|
|
|
const clearAllSessionsForTest: AcpSessionStore["clearAllSessionsForTest"] = () => {
|
|
for (const session of sessions.values()) {
|
|
session.abortController?.abort();
|
|
}
|
|
sessions.clear();
|
|
runIdToSessionId.clear();
|
|
};
|
|
|
|
return {
|
|
createSession,
|
|
hasSession,
|
|
getSession,
|
|
getSessionByRunId,
|
|
setActiveRun,
|
|
clearActiveRun,
|
|
cancelActiveRun,
|
|
clearAllSessionsForTest,
|
|
};
|
|
}
|
|
|
|
export const defaultAcpSessionStore = createInMemorySessionStore();
|