{"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../../src/gateway/sessions.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Session Manager - Manages OpenClaw agent sessions\n *\n * Each session is identified by a SessionKey and maintains:\n * - Conversation history (transcript)\n * - Token count\n * - State (idle, active, paused, error)\n */\n\nimport * as fs from \"node:fs/promises\";\nimport * as path from \"node:path\";\nimport type { ChannelScope, InboundMessage, Session, SessionKey, SessionState } from \"./types/index.js\";\n\n/** Session entry in sessions.json */\ninterface SessionEntry {\n\tid: string;\n\tkey: SessionKey;\n\tagentId: string;\n\tchannel: string;\n\tscope: ChannelScope;\n\tidentifier: string;\n\tcreatedAt: string;\n\tlastActivity: string;\n\tmessageCount: number;\n\ttokenCount: number;\n\tstate: SessionState;\n\ttranscriptPath: string;\n}\n\n/** Sessions index file structure */\ninterface SessionsIndex {\n\tversion: string;\n\tsessions: Record<SessionKey, SessionEntry>;\n}\n\n/** Session manager options */\nexport interface SessionManagerOptions {\n\t/** Session storage directory */\n\tsessionDir: string;\n\t/** Daily reset hour (0-23) */\n\tdailyResetHour?: number;\n\t/** Idle timeout in minutes */\n\tidleMinutes?: number;\n}\n\n/** Session manager */\nexport class SessionManager {\n\tprivate sessionDir: string;\n\tprivate dailyResetHour: number;\n\tprivate idleMinutes: number;\n\tprivate sessions = new Map<SessionKey, Session>();\n\tprivate index: SessionsIndex;\n\tprivate indexPath: string;\n\tprivate saveTimeout?: NodeJS.Timeout;\n\n\tconstructor(options: SessionManagerOptions) {\n\t\tthis.sessionDir = options.sessionDir;\n\t\tthis.dailyResetHour = options.dailyResetHour ?? 4;\n\t\tthis.idleMinutes = options.idleMinutes ?? 60;\n\t\tthis.indexPath = path.join(this.sessionDir, \"sessions.json\");\n\t\tthis.index = { version: \"1.0\", sessions: {} };\n\t}\n\n\t/** Initialize session manager */\n\tasync initialize(): Promise<void> {\n\t\tawait fs.mkdir(this.sessionDir, { recursive: true });\n\t\tawait this.loadIndex();\n\t}\n\n\t/** Load sessions index */\n\tprivate async loadIndex(): Promise<void> {\n\t\ttry {\n\t\t\tconst content = await fs.readFile(this.indexPath, \"utf-8\");\n\t\t\tthis.index = JSON.parse(content);\n\t\t} catch {\n\t\t\t// No index file yet, start fresh\n\t\t\tthis.index = { version: \"1.0\", sessions: {} };\n\t\t}\n\t}\n\n\t/** Save sessions index (debounced) */\n\tprivate scheduleSave(): void {\n\t\tif (this.saveTimeout) {\n\t\t\tclearTimeout(this.saveTimeout);\n\t\t}\n\t\tthis.saveTimeout = setTimeout(() => this.saveIndex(), 1000);\n\t}\n\n\t/** Save sessions index immediately */\n\tprivate async saveIndex(): Promise<void> {\n\t\tconst content = JSON.stringify(this.index, null, 2);\n\t\tawait fs.writeFile(this.indexPath, content, \"utf-8\");\n\t}\n\n\t/** Build session key */\n\tbuildSessionKey(agentId: string, channel: string, scope: ChannelScope, identifier: string): SessionKey {\n\t\treturn `agent:${agentId}:${channel}:${scope}:${identifier}`;\n\t}\n\n\t/** Parse session key */\n\tparseSessionKey(key: SessionKey): {\n\t\tagentId: string;\n\t\tchannel: string;\n\t\tscope: ChannelScope;\n\t\tidentifier: string;\n\t} {\n\t\tconst parts = key.split(\":\");\n\t\treturn {\n\t\t\tagentId: parts[1],\n\t\t\tchannel: parts[2],\n\t\t\tscope: parts[3] as ChannelScope,\n\t\t\tidentifier: parts[4],\n\t\t};\n\t}\n\n\t/** Get or create session */\n\tasync getOrCreateSession(\n\t\tagentId: string,\n\t\tchannel: string,\n\t\tscope: ChannelScope,\n\t\tidentifier: string,\n\t): Promise<Session> {\n\t\tconst key = this.buildSessionKey(agentId, channel, scope, identifier);\n\n\t\t// Check if already loaded\n\t\tif (this.sessions.has(key)) {\n\t\t\treturn this.sessions.get(key)!;\n\t\t}\n\n\t\t// Check if exists in index\n\t\tconst entry = this.index.sessions[key];\n\t\tif (entry) {\n\t\t\tconst session: Session = {\n\t\t\t\tkey,\n\t\t\t\tid: entry.id,\n\t\t\t\tagentId: entry.agentId,\n\t\t\t\tchannel: entry.channel,\n\t\t\t\tscope: entry.scope,\n\t\t\t\tidentifier: entry.identifier,\n\t\t\t\tcreatedAt: new Date(entry.createdAt),\n\t\t\t\tlastActivity: new Date(entry.lastActivity),\n\t\t\t\tmessageCount: entry.messageCount,\n\t\t\t\ttokenCount: entry.tokenCount,\n\t\t\t\tstate: entry.state,\n\t\t\t\ttranscriptPath: entry.transcriptPath,\n\t\t\t};\n\t\t\tthis.sessions.set(key, session);\n\t\t\treturn session;\n\t\t}\n\n\t\t// Create new session\n\t\treturn this.createSession(agentId, channel, scope, identifier);\n\t}\n\n\t/** Create new session */\n\tprivate async createSession(\n\t\tagentId: string,\n\t\tchannel: string,\n\t\tscope: ChannelScope,\n\t\tidentifier: string,\n\t): Promise<Session> {\n\t\tconst key = this.buildSessionKey(agentId, channel, scope, identifier);\n\t\tconst id = this.generateSessionId();\n\t\tconst transcriptPath = path.join(this.sessionDir, `${id}.jsonl`);\n\n\t\tconst session: Session = {\n\t\t\tkey,\n\t\t\tid,\n\t\t\tagentId,\n\t\t\tchannel,\n\t\t\tscope,\n\t\t\tidentifier,\n\t\t\tcreatedAt: new Date(),\n\t\t\tlastActivity: new Date(),\n\t\t\tmessageCount: 0,\n\t\t\ttokenCount: 0,\n\t\t\tstate: \"idle\",\n\t\t\ttranscriptPath,\n\t\t};\n\n\t\t// Add to index\n\t\tthis.index.sessions[key] = {\n\t\t\tid,\n\t\t\tkey,\n\t\t\tagentId,\n\t\t\tchannel,\n\t\t\tscope,\n\t\t\tidentifier,\n\t\t\tcreatedAt: session.createdAt.toISOString(),\n\t\t\tlastActivity: session.lastActivity.toISOString(),\n\t\t\tmessageCount: 0,\n\t\t\ttokenCount: 0,\n\t\t\tstate: \"idle\",\n\t\t\ttranscriptPath,\n\t\t};\n\n\t\tthis.sessions.set(key, session);\n\t\tawait this.saveIndex();\n\n\t\treturn session;\n\t}\n\n\t/** Generate unique session ID */\n\tprivate generateSessionId(): string {\n\t\tconst timestamp = Date.now().toString(36);\n\t\tconst random = Math.random().toString(36).substring(2, 8);\n\t\treturn `${timestamp}-${random}`;\n\t}\n\n\t/** Get session by key */\n\tgetSession(key: SessionKey): Session | undefined {\n\t\treturn this.sessions.get(key);\n\t}\n\n\t/** Get all active sessions */\n\tgetActiveSessions(): Session[] {\n\t\treturn Array.from(this.sessions.values()).filter((s) => s.state === \"active\");\n\t}\n\n\t/** Get sessions for agent */\n\tgetSessionsForAgent(agentId: string): Session[] {\n\t\treturn Array.from(this.sessions.values()).filter((s) => s.agentId === agentId);\n\t}\n\n\t/** Update session activity */\n\ttouchSession(key: SessionKey): void {\n\t\tconst session = this.sessions.get(key);\n\t\tif (session) {\n\t\t\tsession.lastActivity = new Date();\n\t\t\tconst entry = this.index.sessions[key];\n\t\t\tif (entry) {\n\t\t\t\tentry.lastActivity = session.lastActivity.toISOString();\n\t\t\t}\n\t\t\tthis.scheduleSave();\n\t\t}\n\t}\n\n\t/** Update session state */\n\tsetSessionState(key: SessionKey, state: SessionState): void {\n\t\tconst session = this.sessions.get(key);\n\t\tif (session) {\n\t\t\tsession.state = state;\n\t\t\tconst entry = this.index.sessions[key];\n\t\t\tif (entry) {\n\t\t\t\tentry.state = state;\n\t\t\t}\n\t\t\tthis.scheduleSave();\n\t\t}\n\t}\n\n\t/** Increment message count */\n\tincrementMessageCount(key: SessionKey): void {\n\t\tconst session = this.sessions.get(key);\n\t\tif (session) {\n\t\t\tsession.messageCount++;\n\t\t\tconst entry = this.index.sessions[key];\n\t\t\tif (entry) {\n\t\t\t\tentry.messageCount = session.messageCount;\n\t\t\t}\n\t\t\tthis.scheduleSave();\n\t\t}\n\t}\n\n\t/** Update token count */\n\tupdateTokenCount(key: SessionKey, tokens: number): void {\n\t\tconst session = this.sessions.get(key);\n\t\tif (session) {\n\t\t\tsession.tokenCount = tokens;\n\t\t\tconst entry = this.index.sessions[key];\n\t\t\tif (entry) {\n\t\t\t\tentry.tokenCount = tokens;\n\t\t\t}\n\t\t\tthis.scheduleSave();\n\t\t}\n\t}\n\n\t/** Append message to transcript */\n\tasync appendToTranscript(key: SessionKey, message: TranscriptEntry): Promise<void> {\n\t\tconst session = this.sessions.get(key);\n\t\tif (!session) return;\n\n\t\tconst line = JSON.stringify(message) + \"\\n\";\n\t\tawait fs.appendFile(session.transcriptPath, line, \"utf-8\");\n\n\t\tthis.incrementMessageCount(key);\n\t\tthis.touchSession(key);\n\t}\n\n\t/** Load transcript for session */\n\tasync loadTranscript(key: SessionKey): Promise<TranscriptEntry[]> {\n\t\tconst session = this.sessions.get(key);\n\t\tif (!session) return [];\n\n\t\ttry {\n\t\t\tconst content = await fs.readFile(session.transcriptPath, \"utf-8\");\n\t\t\treturn content\n\t\t\t\t.split(\"\\n\")\n\t\t\t\t.filter((line) => line.trim())\n\t\t\t\t.map((line) => JSON.parse(line));\n\t\t} catch {\n\t\t\treturn [];\n\t\t}\n\t}\n\n\t/** Reset session (clear transcript, keep metadata) */\n\tasync resetSession(key: SessionKey): Promise<void> {\n\t\tconst session = this.sessions.get(key);\n\t\tif (!session) return;\n\n\t\t// Clear transcript file\n\t\ttry {\n\t\t\tawait fs.writeFile(session.transcriptPath, \"\", \"utf-8\");\n\t\t} catch {\n\t\t\t// Ignore errors\n\t\t}\n\n\t\t// Reset counters\n\t\tsession.messageCount = 0;\n\t\tsession.tokenCount = 0;\n\t\tsession.state = \"idle\";\n\t\tsession.lastActivity = new Date();\n\n\t\tconst entry = this.index.sessions[key];\n\t\tif (entry) {\n\t\t\tentry.messageCount = 0;\n\t\t\tentry.tokenCount = 0;\n\t\t\tentry.state = \"idle\";\n\t\t\tentry.lastActivity = session.lastActivity.toISOString();\n\t\t}\n\n\t\tawait this.saveIndex();\n\t}\n\n\t/** Close and remove session */\n\tasync closeSession(key: SessionKey): Promise<void> {\n\t\tconst session = this.sessions.get(key);\n\t\tif (session) {\n\t\t\t// Delete transcript file\n\t\t\ttry {\n\t\t\t\tawait fs.unlink(session.transcriptPath);\n\t\t\t} catch {\n\t\t\t\t// Ignore errors\n\t\t\t}\n\t\t}\n\n\t\tthis.sessions.delete(key);\n\t\tdelete this.index.sessions[key];\n\t\tawait this.saveIndex();\n\t}\n\n\t/** Check if session should be reset (daily reset) */\n\tshouldDailyReset(session: Session): boolean {\n\t\tconst now = new Date();\n\t\tconst lastActivity = new Date(session.lastActivity);\n\n\t\t// Check if we're past the reset hour and last activity was before it\n\t\tif (now.getHours() >= this.dailyResetHour) {\n\t\t\tconst resetTime = new Date(now);\n\t\t\tresetTime.setHours(this.dailyResetHour, 0, 0, 0);\n\n\t\t\tif (lastActivity < resetTime) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/** Check if session is idle (timeout) */\n\tisIdle(session: Session): boolean {\n\t\tconst now = Date.now();\n\t\tconst lastActivity = new Date(session.lastActivity).getTime();\n\t\tconst idleMs = this.idleMinutes * 60 * 1000;\n\n\t\treturn now - lastActivity > idleMs;\n\t}\n\n\t/** Cleanup old/idle sessions */\n\tasync cleanup(): Promise<number> {\n\t\tconst toClose: SessionKey[] = [];\n\n\t\tfor (const [key, session] of this.sessions) {\n\t\t\tif (this.shouldDailyReset(session) || this.isIdle(session)) {\n\t\t\t\ttoClose.push(key);\n\t\t\t}\n\t\t}\n\n\t\tfor (const key of toClose) {\n\t\t\tawait this.closeSession(key);\n\t\t}\n\n\t\treturn toClose.length;\n\t}\n\n\t/** Get session statistics */\n\tgetStats(): {\n\t\ttotal: number;\n\t\tactive: number;\n\t\tidle: number;\n\t\ttotalMessages: number;\n\t\ttotalTokens: number;\n\t} {\n\t\tconst sessions = Array.from(this.sessions.values());\n\t\treturn {\n\t\t\ttotal: sessions.length,\n\t\t\tactive: sessions.filter((s) => s.state === \"active\").length,\n\t\t\tidle: sessions.filter((s) => s.state === \"idle\").length,\n\t\t\ttotalMessages: sessions.reduce((sum, s) => sum + s.messageCount, 0),\n\t\t\ttotalTokens: sessions.reduce((sum, s) => sum + s.tokenCount, 0),\n\t\t};\n\t}\n}\n\n/** Transcript entry types */\nexport interface TranscriptEntry {\n\tid: string;\n\tparentId?: string;\n\ttimestamp: string;\n\ttype: \"user\" | \"assistant\" | \"tool_call\" | \"tool_result\" | \"system\";\n\tcontent: unknown;\n\tmetadata?: Record<string, unknown>;\n}\n\n/** Build session key helper */\nexport function buildSessionKey(agentId: string, channel: string, scope: ChannelScope, identifier: string): SessionKey {\n\treturn `agent:${agentId}:${channel}:${scope}:${identifier}`;\n}\n"]}