Skip to main content
Glama
SESSION.md30.4 kB
# Session Management Documentation **Location:** `src/session/` **Purpose:** Persistent storage of team-pair session metadata with process state tracking **Technology:** SQLite with WAL mode for concurrent access --- ## Table of Contents 1. [Overview](#overview) 2. [Architecture](#architecture) 3. [Database Schema](#database-schema) 4. [Component Details](#component-details) 5. [Process State Management](#process-state-management) 6. [Session Lifecycle](#session-lifecycle) 7. [Integration Points](#integration-points) 8. [API Reference](#api-reference) --- ## Overview The Session subsystem provides **persistent storage** for team-pair conversation sessions using SQLite. It tracks: - **Session Identity:** UUID, fromTeam (required), toTeam (required) - **Process State:** stopped, spawning, idle, processing, terminating - **Usage Statistics:** message count, last used timestamp - **Cache References:** current cache session ID - **Response Tracking:** last response timestamp **Key Innovation:** Process state is stored in the database (managed by Iris), not in ClaudeProcess. This enables cache preservation across process recreation. --- ## Architecture ### Two-Layer Design ``` ┌────────────────────────────────────────────────────────────────┐ │ SessionManager (session-manager.ts) │ │ Business Logic Layer │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ store: SessionStore │ │ │ │ cache: Map<string, SessionInfo> (in-memory cache) │ │ │ │ │ │ │ │ Methods: │ │ │ │ • getOrCreateSession(fromTeam, toTeam) │ │ │ │ • updateProcessState(sessionId, state) │ │ │ │ • setCurrentCacheSessionId(sessionId, cacheSessionId) │ │ │ │ • updateLastResponse(sessionId) │ │ │ │ • recordUsage(sessionId) │ │ │ │ • listSessions(filters) │ │ │ └──────────────────────────────────────────────────────────┘ │ └────────────────────┬───────────────────────────────────────────┘ │ uses ▼ ┌────────────────────────────────────────────────────────────────┐ │ SessionStore (session-store.ts) │ │ Data Access Layer │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ db: Database (better-sqlite3) │ │ │ │ │ │ │ │ CRUD Methods: │ │ │ │ • create(fromTeam, toTeam, sessionId) │ │ │ │ • getByTeamPair(fromTeam, toTeam) │ │ │ │ • getBySessionId(sessionId) │ │ │ │ • list(filters) │ │ │ │ • updateProcessState(sessionId, state) │ │ │ │ • updateLastResponse(sessionId, timestamp) │ │ │ │ • incrementMessageCount(sessionId) │ │ │ └──────────────────────────────────────────────────────────┘ │ └────────────────────┬───────────────────────────────────────────┘ │ persists to ▼ ┌────────────────────────────────────────────────────────────────┐ │ SQLite Database │ │ team-sessions.db │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ Table: team_sessions │ │ │ │ │ │ │ │ Columns: │ │ │ │ • id (INTEGER PRIMARY KEY) │ │ │ │ • from_team (TEXT, NOT NULL) │ │ │ │ • to_team (TEXT, NOT NULL) │ │ │ │ • session_id (TEXT, UNIQUE) │ │ │ │ • created_at (INTEGER) │ │ │ │ • last_used_at (INTEGER) │ │ │ │ • message_count (INTEGER) │ │ │ │ • status (TEXT: active | archived) │ │ │ │ • process_state (TEXT: stopped | spawning | ...) │ │ │ │ • current_cache_session_id (TEXT, nullable) │ │ │ │ • last_response_at (INTEGER, nullable) │ │ │ └──────────────────────────────────────────────────────────┘ │ └────────────────────────────────────────────────────────────────┘ ``` ### File Structure ``` src/session/ ├── types.ts # TypeScript interfaces ├── session-store.ts # SQLite data access layer ├── session-manager.ts # Business logic + caching └── README.md # Future phase placeholder ``` --- ## Database Schema ### Table: team_sessions ```sql CREATE TABLE IF NOT EXISTS team_sessions ( id INTEGER PRIMARY KEY AUTOINCREMENT, -- Team pair identity from_team TEXT NOT NULL, -- Required: calling team name to_team TEXT NOT NULL, -- Required: target team name -- Session identifier (UUID) session_id TEXT NOT NULL UNIQUE, -- Timestamps created_at INTEGER NOT NULL, -- Unix timestamp (ms) last_used_at INTEGER NOT NULL, -- Unix timestamp (ms) -- Usage statistics message_count INTEGER DEFAULT 0, -- Session status status TEXT DEFAULT 'active', -- 'active' | 'archived' -- Process state (NEW - refactored architecture) process_state TEXT DEFAULT 'stopped', -- Cache reference (NEW - refactored architecture) current_cache_session_id TEXT, -- Response tracking (NEW - refactored architecture) last_response_at INTEGER, -- Debug info (NEW - for troubleshooting) ✅ launch_command TEXT, -- Full command used to spawn process team_config_snapshot TEXT, -- JSON snapshot of team config at spawn time -- Constraints UNIQUE(from_team, to_team) -- One session per team pair ); ``` ### Indexes ```sql -- Fast lookup by team pair CREATE INDEX IF NOT EXISTS idx_team_sessions_from_to ON team_sessions(from_team, to_team); -- Fast lookup by session ID CREATE INDEX IF NOT EXISTS idx_team_sessions_session_id ON team_sessions(session_id); -- Fast filtering by status CREATE INDEX IF NOT EXISTS idx_team_sessions_status ON team_sessions(status); ``` ### Schema Migration **For Existing Databases:** ```typescript private migrateSchema(): void { const columns = this.db.prepare("PRAGMA table_info(team_sessions)").all(); if (!columns.some(col => col.name === "process_state")) { this.db.exec( "ALTER TABLE team_sessions ADD COLUMN process_state TEXT DEFAULT 'stopped'" ); } if (!columns.some(col => col.name === "current_cache_session_id")) { this.db.exec( "ALTER TABLE team_sessions ADD COLUMN current_cache_session_id TEXT" ); } if (!columns.some(col => col.name === "last_response_at")) { this.db.exec( "ALTER TABLE team_sessions ADD COLUMN last_response_at INTEGER" ); } if (!columns.some(col => col.name === "launch_command")) { this.db.exec( "ALTER TABLE team_sessions ADD COLUMN launch_command TEXT" ); } if (!columns.some(col => col.name === "team_config_snapshot")) { this.db.exec( "ALTER TABLE team_sessions ADD COLUMN team_config_snapshot TEXT" ); } } ``` **Why Graceful Migration?** Existing Iris installations can upgrade without data loss. New columns added with safe defaults. --- ## Component Details ### SessionStore (session-store.ts) **Responsibility:** Pure data access layer - CRUD operations on SQLite **Configuration:** ```typescript constructor(dbPath?: string) { // Use provided path or default to $IRIS_HOME/data/team-sessions.db const absoluteDbPath = dbPath || getSessionDbPath(); // Ensure data directory exists mkdirSync(dirname(absoluteDbPath), { recursive: true }); // Open database with WAL mode for concurrent access this.db = new Database(absoluteDbPath); this.db.pragma("journal_mode = WAL"); // Initialize schema with migration this.initializeSchema(); } ``` **WAL Mode Benefits:** - Multiple readers can access database concurrently - Writers don't block readers - Better performance for read-heavy workloads ### Key CRUD Operations **Create Session:** ```typescript create( fromTeam: string, toTeam: string, sessionId: string ): SessionInfo { const now = Date.now(); const stmt = this.db.prepare(` INSERT INTO team_sessions ( from_team, to_team, session_id, created_at, last_used_at, message_count, status, process_state, current_cache_session_id, last_response_at ) VALUES (?, ?, ?, ?, ?, 0, 'active', 'stopped', NULL, NULL) `); const result = stmt.run(fromTeam, toTeam, sessionId, now, now); return this.rowToSessionInfo({ id: result.lastInsertRowid, from_team: fromTeam, to_team: toTeam, session_id: sessionId, created_at: now, last_used_at: now, message_count: 0, status: "active", process_state: "stopped", current_cache_session_id: null, last_response_at: null, }); } ``` **Get by Team Pair:** ```typescript getByTeamPair( fromTeam: string, toTeam: string ): SessionInfo | null { const stmt = this.db.prepare(` SELECT * FROM team_sessions WHERE from_team IS ? AND to_team = ? `); const row = stmt.get(fromTeam, toTeam); return row ? this.rowToSessionInfo(row) : null; } ``` **Critical Detail:** `from_team IS ?` handles NULL correctly (SQL NULL equality semantics). **Update Process State:** ```typescript updateProcessState(sessionId: string, processState: string): void { const stmt = this.db.prepare(` UPDATE team_sessions SET process_state = ? WHERE session_id = ? `); stmt.run(processState, sessionId); } ``` **Update Last Response:** ```typescript updateLastResponse(sessionId: string, timestamp: number): void { const stmt = this.db.prepare(` UPDATE team_sessions SET last_response_at = ? WHERE session_id = ? `); stmt.run(timestamp, sessionId); } ``` --- ### SessionManager (session-manager.ts) **Responsibility:** Business logic + caching layer **In-Memory Cache:** ```typescript class SessionManager { private cache = new Map<string, SessionInfo>(); private getCacheKey(fromTeam: string, toTeam: string): string { return `${fromTeam ?? 'null'}->${toTeam}`; } } ``` **Why Cache?** Avoid database hits for every message. Cache invalidated on updates. **Get or Create Session:** ```typescript async getOrCreateSession( fromTeam: string, toTeam: string ): Promise<SessionInfo> { // Check cache first const cacheKey = this.getCacheKey(fromTeam, toTeam); let session = this.cache.get(cacheKey); if (session) { // Update last used timestamp this.store.updateLastUsed(session.sessionId); session.lastUsedAt = new Date(); return session; } // Check database session = this.store.getByTeamPair(fromTeam, toTeam); if (session) { // Cache hit - update and return this.store.updateLastUsed(session.sessionId); session.lastUsedAt = new Date(); this.cache.set(cacheKey, session); return session; } // Create new session const sessionId = uuidv4(); // Initialize session file (if not in test mode) if (process.env.NODE_ENV !== "test") { const irisConfig = this.getIrisConfig(toTeam); await ClaudeProcess.initializeSessionFile( irisConfig, sessionId, this.config.settings.sessionInitTimeout ); } // Create database record session = this.store.create(fromTeam, toTeam, sessionId); // Cache it this.cache.set(cacheKey, session); return session; } ``` **Eager Initialization (Startup):** ```typescript async initialize(): Promise<void> { const teamNames = this.configManager.getTeamNames(); for (const teamName of teamNames) { await this.getOrCreateSession(fromTeam, teamName); } logger.info("SessionManager initialized", { teamsInitialized: teamNames.length, }); } ``` **Why Eager Init?** Pre-create session files at startup so first message doesn't pay 30s initialization cost. --- ## Process State Management ### State Machine **States:** ```typescript type ProcessState = | "stopped" // No process running | "spawning" // Process starting | "idle" // Ready, not processing | "processing" // Actively processing a tell | "terminating"; // Shutting down ``` **Transitions:** ``` stopped ──spawn──> spawning ──init──> idle ──executeTell──> processing ↑ │ │ │ └──────────────────────terminate────────────────────────result ``` ### State Updates (Managed by Iris) **Update Process State:** ```typescript updateProcessState(sessionId: string, state: string): void { this.store.updateProcessState(sessionId, state); // Invalidate cache const session = this.store.getBySessionId(sessionId); if (session) { const cacheKey = this.getCacheKey(session.fromTeam, session.toTeam); this.cache.delete(cacheKey); } } ``` **Get Process State:** ```typescript getProcessState(sessionId: string): string | null { const session = this.store.getBySessionId(sessionId); return session?.processState ?? null; } ``` **Set Current Cache Session:** ```typescript setCurrentCacheSessionId( sessionId: string, cacheSessionId: string | null ): void { this.store.setCurrentCacheSessionId(sessionId, cacheSessionId); // Invalidate cache const session = this.store.getBySessionId(sessionId); if (session) { const cacheKey = this.getCacheKey(session.fromTeam, session.toTeam); this.cache.delete(cacheKey); } } ``` **Update Last Response:** ```typescript updateLastResponse(sessionId: string): void { this.store.updateLastResponse(sessionId, Date.now()); // Invalidate cache const session = this.store.getBySessionId(sessionId); if (session) { const cacheKey = this.getCacheKey(session.fromTeam, session.toTeam); this.cache.delete(cacheKey); } } ``` **Why Invalidate Cache?** Process state changes frequently. Invalidation ensures fresh reads from database. --- ## Session Lifecycle ### Creation Flow ``` ┌────────────────────────────────────────────────────────────────┐ │ Iris.sendMessage(null, "alpha", "Hello") │ └────────────────────┬───────────────────────────────────────────┘ │ First message to alpha ▼ ┌────────────────────────────────────────────────────────────────┐ │ SessionManager.getOrCreateSession(null, "alpha") │ │ 1. Check cache: null │ │ 2. Check database: null │ │ 3. Create new session │ └────────────────────┬───────────────────────────────────────────┘ │ ▼ ┌────────────────────────────────────────────────────────────────┐ │ Generate UUID: "abc123-def4-5678-90ab-cdef12345678" │ └────────────────────┬───────────────────────────────────────────┘ │ ▼ ┌────────────────────────────────────────────────────────────────┐ │ ClaudeProcess.initializeSessionFile(config, sessionId) │ │ - Create ~/.claude/projects/{path}/{sessionId}.jsonl │ │ - Spawn temporary process with --session-id │ │ - Wait for pong response │ │ - Verify file exists │ └────────────────────┬───────────────────────────────────────────┘ │ ▼ ┌────────────────────────────────────────────────────────────────┐ │ SessionStore.create(null, "alpha", sessionId) │ │ INSERT INTO team_sessions ( │ │ from_team, to_team, session_id, │ │ created_at, last_used_at, │ │ process_state, current_cache_session_id, last_response_at │ │ ) VALUES (NULL, 'alpha', 'abc123...', now, now, │ │ 'stopped', NULL, NULL) │ └────────────────────┬───────────────────────────────────────────┘ │ ▼ ┌────────────────────────────────────────────────────────────────┐ │ Cache session in memory │ │ cache.set("null->alpha", sessionInfo) │ └────────────────────────────────────────────────────────────────┘ ``` ### Usage Flow ``` ┌────────────────────────────────────────────────────────────────┐ │ Iris.sendMessage(null, "alpha", "What is 2+2?") │ └────────────────────┬───────────────────────────────────────────┘ │ ▼ ┌────────────────────────────────────────────────────────────────┐ │ SessionManager.getOrCreateSession(null, "alpha") │ │ - Cache hit! Return cached session │ │ - Update last_used_at in database │ └────────────────────┬───────────────────────────────────────────┘ │ ▼ ┌────────────────────────────────────────────────────────────────┐ │ Iris updates process state │ │ sessionManager.updateProcessState(sessionId, "processing") │ │ - Write to database: process_state = "processing" │ │ - Invalidate cache │ └────────────────────┬───────────────────────────────────────────┘ │ ▼ ┌────────────────────────────────────────────────────────────────┐ │ Message processes... │ │ - Iris receives messages via cache.messages$ (RxJS) │ │ - Each message → updateLastResponse(sessionId) │ │ - Updates last_response_at in database │ └────────────────────┬───────────────────────────────────────────┘ │ ▼ ┌────────────────────────────────────────────────────────────────┐ │ Completion │ │ sessionManager.updateProcessState(sessionId, "idle") │ │ sessionManager.incrementMessageCount(sessionId) │ │ sessionManager.recordUsage(sessionId) │ └────────────────────────────────────────────────────────────────┘ ``` --- ## Integration Points ### With Iris Orchestrator **Iris uses SessionManager for ALL session operations:** ```typescript class IrisOrchestrator { constructor( private sessionManager: SessionManager, // ... ) {} async sendMessage(fromTeam, toTeam, message) { // Get session const session = await this.sessionManager.getOrCreateSession( fromTeam, toTeam ); // Check state const processState = this.sessionManager.getProcessState(session.sessionId); if (processState === "processing") { return { status: "busy" }; } // Update state this.sessionManager.updateProcessState(session.sessionId, "processing"); // ... execute tell // Update on each message cacheEntry.messages$.subscribe(msg => { this.sessionManager.updateLastResponse(session.sessionId); }); // Complete this.sessionManager.updateProcessState(session.sessionId, "idle"); this.sessionManager.incrementMessageCount(session.sessionId); } } ``` ### With Process Pool **Pool uses sessionId for --resume flag:** ```typescript const process = await pool.getOrCreateProcess( toTeam, session.sessionId, // Passed to ClaudeProcess constructor fromTeam ); // In ClaudeProcess.spawn(): if (this.sessionId) { args.push("--resume", this.sessionId); } ``` --- ## API Reference ### SessionManager ```typescript class SessionManager { constructor( store: SessionStore, configManager: TeamsConfigManager, config: TeamsConfig ); // Initialize (eager session file creation) async initialize(): Promise<void>; // Get or create session async getOrCreateSession( fromTeam: string, toTeam: string ): Promise<SessionInfo>; // Get existing session getSession(fromTeam: string, toTeam: string): SessionInfo | null; // Get by session ID getSessionById(sessionId: string): SessionInfo | null; // Process state management updateProcessState(sessionId: string, state: string): void; getProcessState(sessionId: string): string | null; // Cache references setCurrentCacheSessionId(sessionId: string, cacheSessionId: string | null): void; // Response tracking updateLastResponse(sessionId: string): void; // Usage tracking recordUsage(sessionId: string): void; incrementMessageCount(sessionId: string, count?: number): void; // Debug info ✅ updateDebugInfo( sessionId: string, launchCommand: string, teamConfigSnapshot: string ): void; // Queries listSessions(filters?: SessionFilters): SessionInfo[]; getStats(): { total: number; active: number; archived: number; totalMessages: number }; // Cleanup close(): void; } ``` ### SessionStore ```typescript class SessionStore { constructor(dbPath?: string); // CRUD operations create(fromTeam: string, toTeam: string, sessionId: string): SessionInfo; getByTeamPair(fromTeam: string, toTeam: string): SessionInfo | null; getBySessionId(sessionId: string): SessionInfo | null; list(filters?: SessionFilters): SessionInfo[]; delete(sessionId: string): void; // Updates updateLastUsed(sessionId: string): void; updateStatus(sessionId: string, status: SessionStatus): void; incrementMessageCount(sessionId: string, count?: number): void; resetMessageCount(sessionId: string): void; // Process state (NEW) updateProcessState(sessionId: string, processState: string): void; setCurrentCacheSessionId(sessionId: string, cacheSessionId: string | null): void; updateLastResponse(sessionId: string, timestamp: number): void; // Debug info (NEW) ✅ updateDebugInfo( sessionId: string, launchCommand: string, teamConfigSnapshot: string ): void; // Statistics getStats(): { total: number; active: number; archived: number; totalMessages: number }; // Transactions transaction<T>(fn: () => T): T; // Cleanup close(): void; } ``` ### SessionInfo ```typescript interface SessionInfo { id: number; fromTeam: string; toTeam: string; sessionId: string; createdAt: Date; lastUsedAt: Date; messageCount: number; status: SessionStatus; // NEW - Process state tracking processState: ProcessState; currentCacheSessionId: string | null; lastResponseAt: number | null; // NEW - Debug info for troubleshooting ✅ launchCommand: string | null; teamConfigSnapshot: string | null; } ``` --- ## Performance Characteristics **Database Operations:** - Session lookup: ~1ms (indexed query) - Session creation: ~2ms (INSERT + file creation) - State update: <1ms (indexed UPDATE) **Cache Performance:** - Cache hit: <0.1ms (Map lookup) - Cache miss: ~1ms (database query) **Typical Patterns:** - First message: 2ms (create session) - Subsequent messages: 0.1ms (cache hit) **Scalability:** - SQLite handles 100K+ sessions easily - WAL mode enables concurrent reads - In-memory cache reduces database load --- ## Tech Writer Notes **Coverage Areas:** - Session management architecture (two-layer design with SessionManager and SessionStore) - SQLite database schema for team_sessions table - In-memory caching strategy for performance - Process state tracking (stopped/spawning/idle/processing/terminating) - Debug info fields (launch_command, team_config_snapshot) for troubleshooting - Schema migration for graceful upgrades - Session lifecycle (creation, usage, state transitions) - Integration with Iris Orchestrator and Process Pool - API reference for SessionManager and SessionStore - Performance characteristics and scalability **Keywords:** session management, SQLite, SessionManager, SessionStore, team_sessions, process state, cache, WAL mode, session lifecycle, debug info, launch command, team config snapshot, getOrCreateSession, updateProcessState, updateDebugInfo, persistent storage **Last Updated:** 2025-10-17 **Change Context:** Added debug info fields (launch_command, team_config_snapshot) to database schema, migration code, API methods (updateDebugInfo), and SessionInfo interface. These fields enable troubleshooting of process spawn issues by capturing the exact command and team configuration used. **Related Files:** DASHBOARD.md (debug info display in UI), ARCHITECTURE.md (overall system design), PROCESS_POOL.md (process lifecycle), CONFIG.md (team configuration) --- **Document Version:** 1.1 **Last Updated:** October 2025

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jenova-marie/iris-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server