import { randomUUID } from 'node:crypto'; import type { SemanticAppState, SemanticElement } from '@/modules/computer-use/semantics/semantic-types.js'; const DEFAULT_STATE_TTL_MS = Number.parseInt(process.env.CLOUDCLI_COMPUTER_SEMANTIC_STATE_TTL_MS || String(10 * 60 * 1000), 10); type StoredState = { sessionId: string; appKey: string; state: SemanticAppState; updatedAt: number; }; function normalizeAppKey(app: string): string { return app.trim().toLowerCase(); } export class SemanticSessionStore { private states = new Map(); private latestBySessionApp = new Map(); createStateId(): string { return `state_${randomUUID()}`; } save(sessionId: string, state: SemanticAppState): SemanticAppState { const appKey = normalizeAppKey(state.app); const nextState = { ...state, stateId: state.stateId || this.createStateId(), }; this.states.set(nextState.stateId, { sessionId, appKey, state: nextState, updatedAt: Date.now(), }); this.latestBySessionApp.set(this.latestKey(sessionId, appKey), nextState.stateId); return nextState; } getState(sessionId: string, app: string, stateId?: string): SemanticAppState | null { this.expire(); if (stateId) { const entry = this.states.get(stateId); const appKey = normalizeAppKey(app); return entry && entry.sessionId === sessionId && entry.appKey === appKey ? entry.state : null; } const latestStateId = this.latestBySessionApp.get(this.latestKey(sessionId, normalizeAppKey(app))); return latestStateId ? this.states.get(latestStateId)?.state || null : null; } getElement(sessionId: string, app: string, elementIndex: string, stateId?: string): SemanticElement | null { const state = this.getState(sessionId, app, stateId); return state?.elements.find((element) => element.index === elementIndex) || null; } clearSession(sessionId: string): void { for (const [stateId, entry] of this.states.entries()) { if (entry.sessionId === sessionId) { this.states.delete(stateId); this.latestBySessionApp.delete(this.latestKey(entry.sessionId, entry.appKey)); } } } expire(now = Date.now()): void { const ttl = Number.isFinite(DEFAULT_STATE_TTL_MS) && DEFAULT_STATE_TTL_MS > 0 ? DEFAULT_STATE_TTL_MS : 10 * 60 * 1000; for (const [stateId, entry] of this.states.entries()) { if (now - entry.updatedAt > ttl) { this.states.delete(stateId); const key = this.latestKey(entry.sessionId, entry.appKey); if (this.latestBySessionApp.get(key) === stateId) { this.latestBySessionApp.delete(key); } } } } private latestKey(sessionId: string, appKey: string): string { return `${sessionId}:${appKey}`; } } export const semanticSessionStore = new SemanticSessionStore();