mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 03:52:42 +00:00
fix(acp): harden session lifecycle against flooding
This commit is contained in:
@@ -11,17 +11,93 @@ export type AcpSessionStore = {
|
||||
clearAllSessionsForTest: () => void;
|
||||
};
|
||||
|
||||
export function createInMemorySessionStore(): AcpSessionStore {
|
||||
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: Date.now(),
|
||||
createdAt: nowMs,
|
||||
lastTouchedAt: nowMs,
|
||||
abortController: null,
|
||||
activeRunId: null,
|
||||
};
|
||||
@@ -29,11 +105,24 @@ export function createInMemorySessionStore(): AcpSessionStore {
|
||||
return session;
|
||||
};
|
||||
|
||||
const getSession: AcpSessionStore["getSession"] = (sessionId) => sessions.get(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);
|
||||
return sessionId ? sessions.get(sessionId) : undefined;
|
||||
if (!sessionId) {
|
||||
return undefined;
|
||||
}
|
||||
const session = sessions.get(sessionId);
|
||||
if (session) {
|
||||
touchSession(session, now());
|
||||
}
|
||||
return session;
|
||||
};
|
||||
|
||||
const setActiveRun: AcpSessionStore["setActiveRun"] = (sessionId, runId, abortController) => {
|
||||
@@ -44,6 +133,7 @@ export function createInMemorySessionStore(): AcpSessionStore {
|
||||
session.activeRunId = runId;
|
||||
session.abortController = abortController;
|
||||
runIdToSessionId.set(runId, sessionId);
|
||||
touchSession(session, now());
|
||||
};
|
||||
|
||||
const clearActiveRun: AcpSessionStore["clearActiveRun"] = (sessionId) => {
|
||||
@@ -56,6 +146,7 @@ export function createInMemorySessionStore(): AcpSessionStore {
|
||||
}
|
||||
session.activeRunId = null;
|
||||
session.abortController = null;
|
||||
touchSession(session, now());
|
||||
};
|
||||
|
||||
const cancelActiveRun: AcpSessionStore["cancelActiveRun"] = (sessionId) => {
|
||||
@@ -69,6 +160,7 @@ export function createInMemorySessionStore(): AcpSessionStore {
|
||||
}
|
||||
session.abortController = null;
|
||||
session.activeRunId = null;
|
||||
touchSession(session, now());
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user