import fs from "fs" import path from "path" interface Session { matrixRoomId: string threadRootId: string opencodeSessionId: string eventId: string isActive: boolean messageCount: number inputChars: number outputChars: number lastActivity: number lastEventIds: Map } const SESSION_MAP_FILE = path.join(process.env.HOME || "/tmp", ".opencode-matrix-sessions.json") console.log(`[SessionManager] SESSION_MAP_FILE=${SESSION_MAP_FILE}`) interface SessionMapEntry { matrixRoomId: string threadRootId: string opencodeSessionId: string messageCount: number inputChars: number outputChars: number lastActivity: number } function loadSessionMap(): Map { 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: Map): void { try { const entries: SessionMapEntry[] = [] for (const [, entry] of map) { entries.push(entry) } fs.writeFileSync(SESSION_MAP_FILE, JSON.stringify(entries, null, 2)) } catch (e) { // ignore } } export class SessionManager { public sessions = new Map() private persistentMap: Map private activeQueries = new Map void>() private rateLimitMs: number private lastRequestByUser = new Map() private processedEvents = new Set() constructor(rateLimitSeconds: number) { 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: string, threadRootId: string): string { return `${matrixRoomId}::${threadRootId}` } getOrCreateSession(matrixRoomId: string, threadRootId: string, eventId: string): Session { 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: string, threadRootId: string): Session | undefined { const key = this.getSessionKey(matrixRoomId, threadRootId) return this.sessions.get(key) } removeSession(matrixRoomId: string, threadRootId: string): void { const key = this.getSessionKey(matrixRoomId, threadRootId) this.sessions.delete(key) this.persistentMap.delete(key) saveSessionMap(this.persistentMap) this.activeQueries.delete(key) } has(matrixRoomId: string, threadRootId: string): boolean { const key = this.getSessionKey(matrixRoomId, threadRootId) return this.sessions.has(key) } hasActiveQuery(matrixRoomId: string, threadRootId: string): boolean { const key = this.getSessionKey(matrixRoomId, threadRootId) return this.activeQueries.has(key) } markQueryActive(matrixRoomId: string, threadRootId: string, abortFn: () => void): () => void { const key = this.getSessionKey(matrixRoomId, threadRootId) this.activeQueries.set(key, abortFn) return () => this.activeQueries.delete(key) } isRateLimited(sender: string): boolean { 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: string | undefined): boolean { 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: string, threadRootId: string, opencodeSessionId: string): void { const key = this.getSessionKey(matrixRoomId, threadRootId) const session = this.sessions.get(key) if (session) { const entry: SessionMapEntry = { 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: Set): void { const toDelete: string[] = [] 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) } } }