202 lines
5.9 KiB
TypeScript
202 lines
5.9 KiB
TypeScript
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<string, string>
|
|
}
|
|
|
|
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<string, SessionMapEntry> {
|
|
const result = new Map<string, SessionMapEntry>()
|
|
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<string, SessionMapEntry>): 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<string, Session>()
|
|
private persistentMap: Map<string, SessionMapEntry>
|
|
private activeQueries = new Map<string, () => void>()
|
|
private rateLimitMs: number
|
|
private lastRequestByUser = new Map<string, number>()
|
|
private processedEvents = new Set<string>()
|
|
|
|
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<string>): 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)
|
|
}
|
|
}
|
|
}
|