UNPKG

agentic-qe

Version:

Agentic Quality Engineering Fleet System - AI-driven quality management platform

460 lines 15.9 kB
"use strict"; /** * MemoryManager - Intelligent memory management for AQE Fleet * * Provides in-memory storage with TTL support, namespacing, and optional * SQLite persistence for cross-session memory and agent coordination. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.MemoryManager = void 0; const events_1 = require("events"); const Database_1 = require("../utils/Database"); const Logger_1 = require("../utils/Logger"); class MemoryManager extends events_1.EventEmitter { constructor(database) { super(); this.storage = new Map(); this.defaultTTL = 3600000; // 1 hour in milliseconds this.initialized = false; this.database = database || new Database_1.Database(); this.logger = Logger_1.Logger.getInstance(); // Setup automatic cleanup every 5 minutes this.cleanupInterval = setInterval(() => { this.cleanupExpired(); }, 5 * 60 * 1000); } /** * Initialize the memory manager */ async initialize() { if (this.initialized) { return; } try { await this.database.initialize(); // Load persistent memory from database if available await this.loadPersistentMemory(); this.initialized = true; this.logger.info('MemoryManager initialized successfully'); } catch (error) { this.logger.error('Failed to initialize MemoryManager:', error); throw error; } } /** * Store a value in memory */ async store(key, value, options = {}) { const namespace = options.namespace || 'default'; const fullKey = this.createFullKey(key, namespace); const ttl = options.ttl || this.defaultTTL; const expiresAt = ttl > 0 ? Date.now() + ttl : undefined; const record = { key, value, namespace, ttl, metadata: options.metadata, timestamp: Date.now() }; // Store in memory this.storage.set(fullKey, record); // Store in database if persistence is enabled if (options.persist && this.database) { try { // Use the database's run method to store the record const sql = `INSERT OR REPLACE INTO memory_store (key, value, namespace, ttl, metadata, created_at, expires_at) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP, ?)`; await this.database.run(sql, [ key, JSON.stringify(record), namespace, ttl, JSON.stringify(options.metadata || {}), expiresAt ? new Date(expiresAt).toISOString() : null ]); } catch (error) { this.logger.warn('Failed to persist memory to database:', error); } } this.emit('store', { key, namespace, value, ttl }); this.logger.debug(`Stored key ${fullKey}`, { namespace, ttl, persist: options.persist }); } /** * Retrieve a value from memory */ async retrieve(key, namespace = 'default') { const fullKey = this.createFullKey(key, namespace); const record = this.storage.get(fullKey); if (!record) { // Try loading from database if not in memory const persistentRecord = await this.loadFromDatabase(key, namespace); if (persistentRecord) { // Add back to memory this.storage.set(fullKey, persistentRecord); return persistentRecord.value; } return undefined; } // Check if expired if (this.isExpired(record)) { this.storage.delete(fullKey); this.emit('expired', { key, namespace }); return undefined; } this.emit('retrieve', { key, namespace, value: record.value }); return record.value; } /** * Delete a key from memory */ async delete(key, namespace = 'default') { const fullKey = this.createFullKey(key, namespace); const existed = this.storage.delete(fullKey); // Also delete from database if (this.database) { try { const sql = `DELETE FROM memory_store WHERE key = ? AND namespace = ?`; await this.database.run(sql, [key, namespace]); } catch (error) { this.logger.warn('Failed to delete from database:', error); } } if (existed) { this.emit('delete', { key, namespace }); } return existed; } /** * Check if a key exists */ async exists(key, namespace = 'default') { const fullKey = this.createFullKey(key, namespace); const record = this.storage.get(fullKey); if (!record) { // Check database const persistentRecord = await this.loadFromDatabase(key, namespace); return !!persistentRecord; } // Check if expired if (this.isExpired(record)) { this.storage.delete(fullKey); return false; } return true; } /** * List all keys in a namespace */ async list(namespace = 'default') { const prefix = `${namespace}:`; const keys = []; // Get from memory for (const [fullKey, record] of this.storage.entries()) { if (fullKey.startsWith(prefix) && !this.isExpired(record)) { keys.push(record.key); } } // Get from database if (this.database) { try { const sql = `SELECT key FROM memory_store WHERE namespace = ? AND (expires_at IS NULL OR expires_at > CURRENT_TIMESTAMP)`; const rows = await this.database.all(sql, [namespace]); const dbKeys = rows.map(row => row.key); // Merge and deduplicate const allKeys = new Set([...keys, ...dbKeys]); return Array.from(allKeys); } catch (error) { this.logger.warn('Failed to list keys from database:', error); } } return keys; } /** * Search for keys by pattern */ async search(options = {}) { const { namespace = 'default', pattern = '.*', limit = 100, includeExpired = false } = options; const regex = new RegExp(pattern, 'i'); const results = []; const prefix = `${namespace}:`; for (const [fullKey, record] of this.storage.entries()) { if (results.length >= limit) break; if (fullKey.startsWith(prefix)) { const isExpired = this.isExpired(record); if (!includeExpired && isExpired) { continue; } if (regex.test(record.key) || regex.test(JSON.stringify(record.value))) { results.push({ ...record }); } } } return results; } /** * Clear all keys in a namespace */ async clear(namespace = 'default') { const prefix = `${namespace}:`; const keysToDelete = []; // Find keys to delete for (const fullKey of this.storage.keys()) { if (fullKey.startsWith(prefix)) { keysToDelete.push(fullKey); } } // Delete from memory for (const fullKey of keysToDelete) { this.storage.delete(fullKey); } // Clear from database if (this.database) { try { const sql = `DELETE FROM memory_store WHERE namespace = ?`; await this.database.run(sql, [namespace]); } catch (error) { this.logger.warn('Failed to clear namespace from database:', error); } } this.emit('clear', { namespace, count: keysToDelete.length }); return keysToDelete.length; } /** * Get memory statistics */ getStats() { const namespaces = new Set(); let totalSize = 0; let expiredKeys = 0; let persistentKeys = 0; for (const [, record] of this.storage.entries()) { namespaces.add(record.namespace); totalSize += JSON.stringify(record).length; if (this.isExpired(record)) { expiredKeys++; } if (record.ttl === undefined || record.ttl <= 0) { persistentKeys++; } } return { totalKeys: this.storage.size, totalSize, namespaces: Array.from(namespaces), expiredKeys, persistentKeys }; } /** * Set TTL for an existing key */ async setTTL(key, ttl, namespace = 'default') { const fullKey = this.createFullKey(key, namespace); const record = this.storage.get(fullKey); if (!record) { return false; } record.ttl = ttl; record.timestamp = Date.now(); // Reset timestamp this.storage.set(fullKey, record); this.emit('ttl_updated', { key, namespace, ttl }); return true; } /** * Get TTL for a key */ async getTTL(key, namespace = 'default') { const fullKey = this.createFullKey(key, namespace); const record = this.storage.get(fullKey); if (!record) { return undefined; } if (record.ttl === undefined || record.ttl <= 0) { return -1; // Persistent key } const elapsed = Date.now() - record.timestamp; const remaining = record.ttl - elapsed; return remaining > 0 ? remaining : 0; } /** * Export memory data for backup */ async export(namespace) { const records = []; for (const [, record] of this.storage.entries()) { if (!namespace || record.namespace === namespace) { if (!this.isExpired(record)) { records.push({ ...record }); } } } return records; } /** * Import memory data from backup */ async import(records) { let imported = 0; for (const record of records) { try { await this.store(record.key, record.value, { namespace: record.namespace, ttl: record.ttl, metadata: record.metadata, persist: true }); imported++; } catch (error) { this.logger.warn(`Failed to import record ${record.key}:`, error); } } this.logger.info(`Imported ${imported} memory records`); return imported; } /** * Cleanup expired keys */ cleanupExpired() { const expiredKeys = []; for (const [fullKey, record] of this.storage.entries()) { if (this.isExpired(record)) { expiredKeys.push(fullKey); } } for (const fullKey of expiredKeys) { this.storage.delete(fullKey); } if (expiredKeys.length > 0) { this.emit('cleanup', { expiredCount: expiredKeys.length }); this.logger.debug(`Cleaned up ${expiredKeys.length} expired keys`); } } /** * Shutdown memory manager */ async shutdown() { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); } // Save all non-expired data to database await this.saveToPersistence(); await this.database.close(); this.storage.clear(); this.removeAllListeners(); this.initialized = false; this.logger.info('MemoryManager shutdown complete'); } /** * Create full key with namespace */ createFullKey(key, namespace) { return `${namespace}:${key}`; } /** * Check if a record is expired */ isExpired(record) { if (record.ttl === undefined || record.ttl <= 0) { return false; // Persistent key } const elapsed = Date.now() - record.timestamp; return elapsed > record.ttl; } /** * Load a record from database */ async loadFromDatabase(key, namespace) { if (!this.database) { return undefined; } try { const sql = `SELECT value FROM memory_store WHERE key = ? AND namespace = ? AND (expires_at IS NULL OR expires_at > CURRENT_TIMESTAMP)`; const row = await this.database.get(sql, [key, namespace]); if (row) { return JSON.parse(row.value); } } catch (error) { this.logger.warn('Failed to load from database:', error); } return undefined; } /** * Load persistent memory from database */ async loadPersistentMemory() { if (!this.database) { return; } try { // This is a simplified implementation // In a real implementation, you would iterate through all namespaces const namespaces = ['default', 'fleet', 'agents', 'tasks', 'coordination']; for (const namespace of namespaces) { try { const sql = `SELECT key FROM memory_store WHERE namespace = ? AND (expires_at IS NULL OR expires_at > CURRENT_TIMESTAMP)`; const rows = await this.database.all(sql, [namespace]); for (const row of rows) { const record = await this.loadFromDatabase(row.key, namespace); if (record && !this.isExpired(record)) { const fullKey = this.createFullKey(row.key, namespace); this.storage.set(fullKey, record); } } } catch (error) { this.logger.warn(`Failed to load keys for namespace ${namespace}:`, error); } } this.logger.info('Loaded persistent memory from database'); } catch (error) { this.logger.warn('Failed to load persistent memory:', error); } } /** * Save memory to persistence */ async saveToPersistence() { if (!this.database) { return; } try { let saved = 0; for (const [, record] of this.storage.entries()) { if (!this.isExpired(record)) { const sql = `INSERT OR REPLACE INTO memory_store (key, value, namespace, ttl, metadata, created_at, expires_at) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP, ?)`; const expiresAt = record.ttl && record.ttl > 0 ? new Date(record.timestamp + record.ttl).toISOString() : null; await this.database.run(sql, [ record.key, JSON.stringify(record), record.namespace, record.ttl || 0, JSON.stringify(record.metadata || {}), expiresAt ]); saved++; } } this.logger.info(`Saved ${saved} memory records to persistence`); } catch (error) { this.logger.warn('Failed to save to persistence:', error); } } } exports.MemoryManager = MemoryManager; //# sourceMappingURL=MemoryManager.js.map