Skip to main content
Glama
systempromptio

SystemPrompt Coding Agent

Official
state-persistence.ts13 kB
/** * @fileoverview State persistence service for managing application state storage * @module services/state-persistence * * @remarks * This service handles all state persistence operations including saving and loading * tasks, sessions, logs, and reports. It implements filesystem-based storage with * automatic backups and cleanup mechanisms. * * @example * ```typescript * import { StatePersistence } from './services/state-persistence'; * * const persistence = StatePersistence.getInstance(); * * // Save a task * await persistence.saveTask({ * id: 'task-123', * description: 'Build feature', * status: 'in_progress', * // ... other fields * }); * * // Load all tasks * const tasks = await persistence.loadTasks(); * ``` */ import * as fs from 'fs/promises'; import * as path from 'path'; import { EventEmitter } from 'events'; import type { Task } from '../types/task.js'; import { validateTaskId } from '../utils/id-validation.js'; import { ServiceError } from '../types/shared.js'; import { logger } from '../utils/logger.js'; /** * Configuration options for state persistence * * @interface PersistenceConfig */ export interface PersistenceConfig { /** * Storage type (currently only filesystem is supported) */ type: 'filesystem'; /** * Base directory path for storing state files */ basePath?: string; } /** * Structure of persisted application state * * @interface PersistedState */ export interface PersistedState { /** * Array of all tasks */ tasks: Task[]; /** * Array of active sessions */ sessions: Array<{ id: string; type: string; status: string; created_at: string; task_id?: string; }>; /** * Aggregated metrics */ metrics: { total_tasks: number; completed_tasks: number; failed_tasks: number; average_completion_time: number; }; /** * ISO timestamp of last save */ last_saved: string; } /** * Node.js error with code property * @internal */ interface NodeError extends Error { code?: string; } /** * Report structure for persistence * * @interface Report */ export interface Report { /** * Unique report identifier */ id: string; /** * Report type/category */ type: string; /** * ISO timestamp of report creation */ timestamp: string; /** * Report data payload */ data: Record<string, unknown>; } /** * Manages state persistence for the application * * @class StatePersistence * @extends EventEmitter * * @remarks * This class implements a singleton pattern and provides methods for: * - Saving and loading application state * - Managing task persistence * - Storing session logs * - Creating backups with automatic cleanup * - Generating reports * * Events emitted: * - 'state:saved' - When state is successfully saved * - 'state:loaded' - When state is successfully loaded * - 'state:save-error' - When state save fails * - 'autosave:triggered' - When auto-save runs * - 'shutdown:save' - When service is shutting down */ export class StatePersistence extends EventEmitter { private static instance: StatePersistence; private config: PersistenceConfig; private saveInterval: NodeJS.Timeout | null = null; private statePath: string; private constructor(config?: PersistenceConfig) { super(); this.config = config || { type: 'filesystem', basePath: process.env.STATE_PATH || './coding-agent-state' }; this.statePath = this.config.basePath || process.env.STATE_PATH || './coding-agent-state'; this.initializeStorage(); } /** * Gets singleton instance of StatePersistence * * @param config - Optional configuration for persistence * @returns The singleton StatePersistence instance * * @example * ```typescript * const persistence = StatePersistence.getInstance({ * type: 'filesystem', * basePath: './my-state-dir' * }); * ``` */ static getInstance(config?: PersistenceConfig): StatePersistence { if (!StatePersistence.instance) { StatePersistence.instance = new StatePersistence(config); } return StatePersistence.instance; } /** * Initializes storage directories and auto-save * * @private */ private async initializeStorage(): Promise<void> { try { await fs.mkdir(this.statePath, { recursive: true }); await Promise.all([ fs.mkdir(path.join(this.statePath, 'tasks'), { recursive: true }), fs.mkdir(path.join(this.statePath, 'sessions'), { recursive: true }), fs.mkdir(path.join(this.statePath, 'logs'), { recursive: true }), fs.mkdir(path.join(this.statePath, 'reports'), { recursive: true }) ]); logger.info(`State persistence initialized at: ${this.statePath}`); } catch (error) { logger.error('Failed to initialize state storage:', error); } this.saveInterval = setInterval(() => { this.autoSave().catch((error) => logger.error('Auto-save failed:', error) ); }, 30000); } /** * Saves application state to storage * * @param state - The application state to save * * @remarks * Creates a backup of existing state before saving. * Maintains up to 10 backups with automatic cleanup. * * @example * ```typescript * await persistence.saveState({ * tasks: allTasks, * sessions: activeSessions, * metrics: calculatedMetrics, * last_saved: new Date().toISOString() * }); * ``` */ async saveState(state: PersistedState): Promise<void> { const stateFile = path.join(this.statePath, 'state.json'); const backupFile = path.join(this.statePath, `state.backup.${Date.now()}.json`); try { try { const existing = await fs.readFile(stateFile, 'utf-8'); await fs.writeFile(backupFile, existing); await this.cleanupBackups(); } catch (error) { // No existing state file, continue } await fs.writeFile( stateFile, JSON.stringify(state, null, 2), 'utf-8' ); this.emit('state:saved', { timestamp: new Date().toISOString() }); } catch (error) { logger.error('Failed to save state:', error); this.emit('state:save-error', error); } } /** * Loads application state from storage * * @returns The loaded state or null if no state exists * * @example * ```typescript * const state = await persistence.loadState(); * if (state) { * console.log(`Loaded ${state.tasks.length} tasks`); * } * ``` */ async loadState(): Promise<PersistedState | null> { const stateFile = path.join(this.statePath, 'state.json'); try { const data = await fs.readFile(stateFile, 'utf-8'); const state = JSON.parse(data) as PersistedState; this.emit('state:loaded', { timestamp: new Date().toISOString() }); return state; } catch (error) { const nodeError = error as NodeError; if (nodeError.code !== 'ENOENT') { logger.error('Failed to load state:', error); } return null; } } /** * Saves a single task to storage * * @param task - The task to save * @throws {ServiceError} If task save fails * * @example * ```typescript * await persistence.saveTask({ * id: 'task-123', * description: 'Implement login', * status: 'in_progress', * // ... other fields * }); * ``` */ async saveTask(task: Task): Promise<void> { const safeId = validateTaskId(task.id); const taskFile = path.join(this.statePath, 'tasks', `${safeId}.json`); try { await fs.writeFile( taskFile, JSON.stringify(task, null, 2), 'utf-8' ); logger.debug(`[StatePersistence] Saved task as ${safeId}.json`); } catch (error) { logger.error(`Failed to save task ${task.id}:`, error); throw new ServiceError( `Failed to save task ${task.id}`, 'TASK_SAVE_ERROR', 500, error ); } } /** * Loads all tasks from storage * * @returns Array of loaded tasks * * @example * ```typescript * const tasks = await persistence.loadTasks(); * console.log(`Loaded ${tasks.length} tasks from disk`); * ``` */ async loadTasks(): Promise<Task[]> { const tasksDir = path.join(this.statePath, 'tasks'); try { const files = await fs.readdir(tasksDir); const tasks: Task[] = []; for (const file of files) { if (file.endsWith('.json')) { try { const data = await fs.readFile( path.join(tasksDir, file), 'utf-8' ); tasks.push(JSON.parse(data)); } catch (error) { logger.error(`Failed to load task ${file}:`, error); } } } return tasks; } catch (error) { logger.error('Failed to load tasks:', error); return []; } } /** * Saves session log to storage * * @param sessionId - The session ID * @param log - The log content to append * * @example * ```typescript * await persistence.saveSessionLog('session-123', 'Task started at 10:00 AM'); * ``` */ async saveSessionLog(sessionId: string, log: string): Promise<void> { const logFile = path.join( this.statePath, 'logs', `session_${sessionId}_${Date.now()}.log` ); try { await fs.appendFile(logFile, log + '\n', 'utf-8'); } catch (error) { logger.error(`Failed to save session log:`, error); } } /** * Saves report to storage * * @param reportId - Unique identifier for the report * @param report - The report data to save * @returns The path to the saved report file * @throws {ServiceError} If report save fails * * @example * ```typescript * const reportPath = await persistence.saveReport('report-123', { * id: 'report-123', * type: 'task-summary', * timestamp: new Date().toISOString(), * data: { taskCount: 10, completionRate: 0.8 } * }); * ``` */ async saveReport(reportId: string, report: Report): Promise<string> { const safeId = validateTaskId(reportId); const reportFile = path.join( this.statePath, 'reports', `${safeId}.json` ); try { await fs.writeFile( reportFile, JSON.stringify(report, null, 2), 'utf-8' ); logger.debug(`[StatePersistence] Saved report as ${safeId}.json`); return reportFile; } catch (error) { logger.error(`Failed to save report:`, error); throw new ServiceError( 'Failed to save report', 'REPORT_SAVE_ERROR', 500, error ); } } /** * Cleans up old backup files, keeping only the last 10 * * @private */ private async cleanupBackups(): Promise<void> { try { const files = await fs.readdir(this.statePath); const backups = files .filter(f => f.startsWith('state.backup.')) .sort() .reverse(); for (let i = 10; i < backups.length; i++) { await fs.unlink(path.join(this.statePath, backups[i])); } } catch (error) { logger.error('Failed to cleanup backups:', error); } } /** * Triggers auto-save event * * @private */ private async autoSave(): Promise<void> { this.emit('autosave:triggered'); } /** * Deletes a task from storage * * @param taskId - The ID of the task to delete * * @example * ```typescript * await persistence.deleteTask('task-123'); * ``` */ async deleteTask(taskId: string): Promise<void> { const safeId = validateTaskId(taskId); const taskFile = path.join(this.statePath, 'tasks', `${safeId}.json`); logger.debug(`[StatePersistence] Deleting task file: ${taskFile}`); try { await fs.unlink(taskFile); logger.debug(`[StatePersistence] Successfully deleted task file: ${taskId}.json`); } catch (error) { const nodeError = error as NodeError; if (nodeError.code === 'ENOENT') { logger.debug(`[StatePersistence] Task file not found: ${taskFile}`); } else { logger.error(`[StatePersistence] Failed to delete task ${taskId}:`, error); } } } /** * Shuts down the persistence service * * * @remarks * Clears the auto-save interval and emits a shutdown event. * Should be called during application shutdown. * * @example * ```typescript * process.on('SIGTERM', async () => { * await persistence.shutdown(); * process.exit(0); * }); * ``` */ async shutdown(): Promise<void> { if (this.saveInterval) { clearInterval(this.saveInterval); } this.emit('shutdown:save'); } }

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/systempromptio/systemprompt-code-orchestrator'

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