import fs from "fs"; import path from "path"; const SESSION_MAP_FILE = path.join(process.env.HOME || "/tmp", ".opencode-matrix-sessions.json"); console.log(`[SessionManager] SESSION_MAP_FILE=${SESSION_MAP_FILE}`); function loadSessionMap() { const result = new Map(); try { if (fs.existsSync(SESSION_MAP_FILE)) { const data = JSON.parse(fs.readFileSync(SESSION_MAP_FILE, "utf-8")); console.log(`[SessionManager] Loaded ${data.length} sessions from ${SESSION_MAP_FILE}`); if (Array.isArray(data)) { for (const entry of data) { const key = `${entry.matrixRoomId}::${entry.threadRootId}`; result.set(key, entry); } } } else { console.log(`[SessionManager] File not found: ${SESSION_MAP_FILE}`); } } catch (e) { console.log(`[SessionManager] Error loading: ${e}`); } return result; } function saveSessionMap(map) { try { const entries = []; for (const [, entry] of map) { entries.push(entry); } fs.writeFileSync(SESSION_MAP_FILE, JSON.stringify(entries, null, 2)); } catch (e) { // ignore } } export class SessionManager { sessions = new Map(); persistentMap; activeQueries = new Map(); rateLimitMs; lastRequestByUser = new Map(); processedEvents = new Set(); constructor(rateLimitSeconds) { this.rateLimitMs = (rateLimitSeconds || 5) * 1000; this.persistentMap = loadSessionMap(); // Restore sessions from persistent map for (const [key, entry] of this.persistentMap) { const [matrixRoomId, threadRootId] = key.split("::"); this.sessions.set(key, { matrixRoomId, threadRootId, opencodeSessionId: entry.opencodeSessionId, eventId: "", isActive: false, messageCount: entry.messageCount, inputChars: entry.inputChars, outputChars: entry.outputChars, lastActivity: entry.lastActivity, lastEventIds: new Map(), }); } } getSessionKey(matrixRoomId, threadRootId) { return `${matrixRoomId}::${threadRootId}`; } getOrCreateSession(matrixRoomId, threadRootId, eventId) { const key = this.getSessionKey(matrixRoomId, threadRootId); let session = this.sessions.get(key); if (!session) { session = { matrixRoomId, threadRootId, opencodeSessionId: "", eventId, isActive: false, messageCount: 0, inputChars: 0, outputChars: 0, lastActivity: Date.now(), lastEventIds: new Map(), }; this.sessions.set(key, session); } return session; } getSession(matrixRoomId, threadRootId) { const key = this.getSessionKey(matrixRoomId, threadRootId); return this.sessions.get(key); } removeSession(matrixRoomId, threadRootId) { const key = this.getSessionKey(matrixRoomId, threadRootId); this.sessions.delete(key); this.persistentMap.delete(key); saveSessionMap(this.persistentMap); this.activeQueries.delete(key); } has(matrixRoomId, threadRootId) { const key = this.getSessionKey(matrixRoomId, threadRootId); return this.sessions.has(key); } hasActiveQuery(matrixRoomId, threadRootId) { const key = this.getSessionKey(matrixRoomId, threadRootId); return this.activeQueries.has(key); } markQueryActive(matrixRoomId, threadRootId, abortFn) { const key = this.getSessionKey(matrixRoomId, threadRootId); this.activeQueries.set(key, abortFn); return () => this.activeQueries.delete(key); } isRateLimited(sender) { const now = Date.now(); const last = this.lastRequestByUser.get(sender); if (last && now - last < this.rateLimitMs) { return true; } this.lastRequestByUser.set(sender, now); return false; } isDuplicateEvent(eventId) { if (!eventId) return false; if (this.processedEvents.has(eventId)) return true; if (this.processedEvents.size > 10000) { this.processedEvents.clear(); } this.processedEvents.add(eventId); return false; } saveSession(matrixRoomId, threadRootId, opencodeSessionId) { const key = this.getSessionKey(matrixRoomId, threadRootId); const session = this.sessions.get(key); if (session) { const entry = { matrixRoomId, threadRootId, opencodeSessionId, messageCount: session.messageCount, inputChars: session.inputChars, outputChars: session.outputChars, lastActivity: session.lastActivity, }; this.persistentMap.set(key, entry); saveSessionMap(this.persistentMap); } } syncWithOpenCode(openCodeSessionIds) { const toDelete = []; for (const [, entry] of this.persistentMap) { if (!openCodeSessionIds.has(entry.opencodeSessionId)) { toDelete.push(`${entry.matrixRoomId}::${entry.threadRootId}`); } } for (const key of toDelete) { this.persistentMap.delete(key); this.sessions.delete(key); this.activeQueries.delete(key); } if (toDelete.length > 0) { saveSessionMap(this.persistentMap); } } }