openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
124 lines (123 loc) • 6.08 kB
JavaScript
//#region src/logging/diagnostic-session-state.ts
/** Shared in-memory diagnostic session state map. */
const diagnosticSessionStates = /* @__PURE__ */ new Map();
const SESSION_STATE_TTL_MS = 1800 * 1e3;
const SESSION_STATE_PRUNE_INTERVAL_MS = 60 * 1e3;
const SESSION_STATE_MAX_ENTRIES = 2e3;
let lastSessionPruneAt = 0;
/** Prunes stale idle session states and caps the process-local state map. */
function pruneDiagnosticSessionStates(now = Date.now(), force = false) {
const shouldPruneForSize = diagnosticSessionStates.size > SESSION_STATE_MAX_ENTRIES;
if (!force && !shouldPruneForSize && now - lastSessionPruneAt < SESSION_STATE_PRUNE_INTERVAL_MS) return;
lastSessionPruneAt = now;
for (const [key, state] of diagnosticSessionStates.entries()) {
const ageMs = now - state.lastActivity;
if (state.state === "idle" && state.queueDepth <= 0 && ageMs > SESSION_STATE_TTL_MS) diagnosticSessionStates.delete(key);
}
if (diagnosticSessionStates.size <= SESSION_STATE_MAX_ENTRIES) return;
const excess = diagnosticSessionStates.size - SESSION_STATE_MAX_ENTRIES;
const ordered = Array.from(diagnosticSessionStates.entries()).toSorted((a, b) => a[1].lastActivity - b[1].lastActivity);
for (let i = 0; i < excess; i += 1) {
const key = ordered[i]?.[0];
if (!key) break;
diagnosticSessionStates.delete(key);
}
}
function resolveSessionKey({ sessionKey, sessionId }) {
return sessionKey ?? sessionId ?? "unknown";
}
function findStateEntryBySessionId(sessionId) {
for (const entry of diagnosticSessionStates.entries()) {
const [, state] = entry;
if (state.sessionId === sessionId) return entry;
}
}
function sessionStatePriority(state) {
return {
idle: 0,
waiting: 1,
processing: 2
}[state];
}
function mergeSessionState(target, source) {
const sourceIsNewer = source.lastActivity > target.lastActivity;
const sourceIsSameAgeAndMoreActive = source.lastActivity === target.lastActivity && sessionStatePriority(source.state) > sessionStatePriority(target.state);
target.sessionId ??= source.sessionId;
target.sessionKey ??= source.sessionKey;
if (source.sessionFile && (sourceIsNewer || !target.sessionFile)) target.sessionFile = source.sessionFile;
if (sourceIsNewer || sourceIsSameAgeAndMoreActive) target.state = source.state;
target.generation = Math.max(target.generation ?? 0, source.generation ?? 0);
target.lastActivity = Math.max(target.lastActivity, source.lastActivity);
target.queueDepth += source.queueDepth;
target.activeQueuedTurn ||= source.activeQueuedTurn;
target.lastStuckWarnAgeMs = target.lastStuckWarnAgeMs === void 0 || source.lastStuckWarnAgeMs === void 0 ? void 0 : Math.max(target.lastStuckWarnAgeMs, source.lastStuckWarnAgeMs);
target.lastLongRunningWarnAgeMs = target.lastLongRunningWarnAgeMs === void 0 || source.lastLongRunningWarnAgeMs === void 0 ? void 0 : Math.max(target.lastLongRunningWarnAgeMs, source.lastLongRunningWarnAgeMs);
if (source.toolCallHistory?.length) target.toolCallHistory = [...target.toolCallHistory ?? [], ...source.toolCallHistory];
if (source.toolLoopWarningBuckets?.size) {
const buckets = target.toolLoopWarningBuckets ??= /* @__PURE__ */ new Map();
for (const [bucket, count] of source.toolLoopWarningBuckets) buckets.set(bucket, Math.max(buckets.get(bucket) ?? 0, count));
}
if (source.commandPollCounts?.size) {
const counts = target.commandPollCounts ??= /* @__PURE__ */ new Map();
for (const [command, value] of source.commandPollCounts) {
const existing = counts.get(command);
if (!existing || value.lastPollAt > existing.lastPollAt) counts.set(command, value);
}
}
}
/** Gets or creates diagnostic state, merging aliases that share a session id. */
function getDiagnosticSessionState(ref) {
pruneDiagnosticSessionStates();
const key = resolveSessionKey(ref);
const direct = diagnosticSessionStates.get(key);
const sessionIdEntry = ref.sessionId ? findStateEntryBySessionId(ref.sessionId) : void 0;
const existing = direct ?? sessionIdEntry?.[1];
if (existing) {
if (direct && sessionIdEntry && sessionIdEntry[1] !== direct) {
mergeSessionState(direct, sessionIdEntry[1]);
diagnosticSessionStates.delete(sessionIdEntry[0]);
} else if (!direct && ref.sessionKey && sessionIdEntry) {
diagnosticSessionStates.delete(sessionIdEntry[0]);
diagnosticSessionStates.set(key, existing);
}
if (ref.sessionId) existing.sessionId = ref.sessionId;
if (ref.sessionKey) existing.sessionKey = ref.sessionKey;
if (ref.sessionFile) existing.sessionFile = ref.sessionFile;
return existing;
}
const created = {
sessionId: ref.sessionId,
sessionKey: ref.sessionKey,
sessionFile: ref.sessionFile,
lastActivity: Date.now(),
generation: 0,
state: "idle",
queueDepth: 0
};
diagnosticSessionStates.set(key, created);
pruneDiagnosticSessionStates(Date.now(), true);
return created;
}
/** Looks up diagnostic state without creating a new entry. */
function peekDiagnosticSessionState(ref) {
const key = resolveSessionKey(ref);
return diagnosticSessionStates.get(key) ?? (ref.sessionId ? findStateEntryBySessionId(ref.sessionId)?.[1] : void 0);
}
/** Returns the current state count for pruning tests. */
function getDiagnosticSessionStateCountForTest() {
return diagnosticSessionStates.size;
}
/** Clears all process-local diagnostic session state for tests. */
function resetDiagnosticSessionStateForTest() {
diagnosticSessionStates.clear();
lastSessionPruneAt = 0;
}
/** Checks whether a generation/state snapshot still matches current diagnostic state. */
function isDiagnosticSessionStateCurrent(params) {
if (params.generation === void 0) return true;
const state = peekDiagnosticSessionState(params);
if (!state) return false;
return (state.generation ?? 0) === params.generation && (params.state === void 0 || state.state === params.state);
}
//#endregion
export { peekDiagnosticSessionState as a, isDiagnosticSessionStateCurrent as i, getDiagnosticSessionState as n, pruneDiagnosticSessionStates as o, getDiagnosticSessionStateCountForTest as r, resetDiagnosticSessionStateForTest as s, diagnosticSessionStates as t };