claude-flow-tbowman01
Version:
Enterprise-grade AI agent orchestration with ruv-swarm integration (Alpha Release)
423 lines • 15.3 kB
JavaScript
/**
* Memory manager interface and implementation
*/
import { MemoryError } from '../utils/errors.js';
import { SQLiteBackend } from './backends/sqlite.js';
import { MarkdownBackend } from './backends/markdown.js';
import { MemoryCache } from './cache.js';
import { MemoryIndexer } from './indexer.js';
/**
* Memory manager implementation
*/
export class MemoryManager {
config;
eventBus;
logger;
backend;
cache;
indexer;
banks = new Map();
initialized = false;
syncInterval;
constructor(config, eventBus, logger) {
this.config = config;
this.eventBus = eventBus;
this.logger = logger;
// Initialize backend based on configuration
this.backend = this.createBackend();
// Initialize cache
this.cache = new MemoryCache(this.config.cacheSizeMB * 1024 * 1024, // Convert MB to bytes
this.logger);
// Initialize indexer
this.indexer = new MemoryIndexer(this.logger);
}
async initialize() {
if (this.initialized) {
return;
}
this.logger.info('Initializing memory manager...');
try {
// Initialize backend
await this.backend.initialize();
// Initialize indexer with existing entries
const allEntries = await this.backend.getAllEntries();
await this.indexer.buildIndex(allEntries);
// Start sync interval
this.startSyncInterval();
this.initialized = true;
this.logger.info('Memory manager initialized');
}
catch (error) {
this.logger.error('Failed to initialize memory manager', error);
throw new MemoryError('Memory manager initialization failed', { error });
}
}
async shutdown() {
if (!this.initialized) {
return;
}
this.logger.info('Shutting down memory manager...');
try {
// Stop sync interval
if (this.syncInterval) {
clearInterval(this.syncInterval);
}
// Flush cache
await this.flushCache();
// Close all banks
const bankIds = Array.from(this.banks.keys());
await Promise.all(bankIds.map((id) => this.closeBank(id)));
// Shutdown backend
await this.backend.shutdown();
this.initialized = false;
this.logger.info('Memory manager shutdown complete');
}
catch (error) {
this.logger.error('Error during memory manager shutdown', error);
throw error;
}
}
async createBank(agentId) {
if (!this.initialized) {
throw new MemoryError('Memory manager not initialized');
}
const bank = {
id: `bank_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
agentId,
createdAt: new Date(),
lastAccessed: new Date(),
entryCount: 0,
};
this.banks.set(bank.id, bank);
this.logger.info('Memory bank created', { bankId: bank.id, agentId });
return bank.id;
}
async closeBank(bankId) {
const bank = this.banks.get(bankId);
if (!bank) {
throw new MemoryError(`Memory bank not found: ${bankId}`);
}
// Flush any cached entries for this bank
const bankEntries = this.cache.getByPrefix(`${bank.agentId}:`);
for (const entry of bankEntries) {
await this.backend.store(entry);
}
this.banks.delete(bankId);
this.logger.info('Memory bank closed', { bankId });
}
async store(entry) {
if (!this.initialized) {
throw new MemoryError('Memory manager not initialized');
}
this.logger.debug('Storing memory entry', {
id: entry.id,
type: entry.type,
agentId: entry.agentId,
});
try {
// Add to cache
this.cache.set(entry.id, entry);
// Add to index
this.indexer.addEntry(entry);
// Store in backend (async, don't wait)
this.backend.store(entry).catch((error) => {
this.logger.error('Failed to store entry in backend', {
id: entry.id,
error,
});
});
// Update bank stats
const bank = Array.from(this.banks.values()).find((b) => b.agentId === entry.agentId);
if (bank) {
bank.entryCount++;
bank.lastAccessed = new Date();
}
// Emit event
this.eventBus.emit('memory:created', { entry });
}
catch (error) {
this.logger.error('Failed to store memory entry', error);
throw new MemoryError('Failed to store memory entry', { error });
}
}
async retrieve(id) {
if (!this.initialized) {
throw new MemoryError('Memory manager not initialized');
}
// Check cache first
const cached = this.cache.get(id);
if (cached) {
return cached;
}
// Retrieve from backend
const entry = await this.backend.retrieve(id);
if (entry) {
// Add to cache
this.cache.set(id, entry);
}
return entry;
}
async query(query) {
if (!this.initialized) {
throw new MemoryError('Memory manager not initialized');
}
this.logger.debug('Querying memory', query);
try {
// Use index for fast querying
let results = this.indexer.search(query);
// Apply additional filters if needed
if (query.search) {
results = results.filter((entry) => entry.content.toLowerCase().includes(query.search.toLowerCase()) ||
entry.tags.some((tag) => tag.toLowerCase().includes(query.search.toLowerCase())));
}
// Apply time range filter
if (query.startTime || query.endTime) {
results = results.filter((entry) => {
const timestamp = entry.timestamp.getTime();
if (query.startTime && timestamp < query.startTime.getTime()) {
return false;
}
if (query.endTime && timestamp > query.endTime.getTime()) {
return false;
}
return true;
});
}
// Apply pagination
const start = query.offset || 0;
const limit = query.limit || 100;
results = results.slice(start, start + limit);
return results;
}
catch (error) {
this.logger.error('Failed to query memory', error);
throw new MemoryError('Failed to query memory', { error });
}
}
async update(id, updates) {
if (!this.initialized) {
throw new MemoryError('Memory manager not initialized');
}
const existing = await this.retrieve(id);
if (!existing) {
throw new MemoryError(`Memory entry not found: ${id}`);
}
// Create updated entry
const updated = {
...existing,
...updates,
id: existing.id, // Ensure ID doesn't change
version: existing.version + 1,
timestamp: new Date(),
};
// Update in cache
this.cache.set(id, updated);
// Update in index
this.indexer.updateEntry(updated);
// Update in backend
await this.backend.update(id, updated);
// Emit event
this.eventBus.emit('memory:updated', {
entry: updated,
previousVersion: existing.version,
});
}
async delete(id) {
if (!this.initialized) {
throw new MemoryError('Memory manager not initialized');
}
// Remove from cache
this.cache.delete(id);
// Remove from index
this.indexer.removeEntry(id);
// Delete from backend
await this.backend.delete(id);
// Emit event
this.eventBus.emit('memory:deleted', { entryId: id });
}
async getHealthStatus() {
try {
const backendHealth = await this.backend.getHealthStatus();
const cacheMetrics = this.cache.getMetrics();
const indexMetrics = this.indexer.getMetrics();
const metrics = {
totalEntries: indexMetrics.totalEntries,
cacheSize: cacheMetrics.size,
cacheHitRate: cacheMetrics.hitRate,
activeBanks: this.banks.size,
...backendHealth.metrics,
};
return {
healthy: backendHealth.healthy,
metrics,
...(backendHealth.error && { error: backendHealth.error }),
};
}
catch (error) {
return {
healthy: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}
async performMaintenance() {
if (!this.initialized) {
return;
}
this.logger.debug('Performing memory manager maintenance');
try {
// Clean up old entries based on retention policy
if (this.config.retentionDays > 0) {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - this.config.retentionDays);
const oldEntries = await this.query({
endTime: cutoffDate,
});
for (const entry of oldEntries) {
await this.delete(entry.id);
}
this.logger.info(`Cleaned up ${oldEntries.length} old memory entries`);
}
// Perform cache maintenance
this.cache.performMaintenance();
// Perform backend maintenance
if (this.backend.performMaintenance) {
await this.backend.performMaintenance();
}
// Update bank statistics
for (const bank of this.banks.values()) {
const entries = await this.query({ agentId: bank.agentId });
bank.entryCount = entries.length;
bank.lastAccessed = new Date();
}
this.logger.debug('Memory manager maintenance completed');
}
catch (error) {
this.logger.error('Error during memory manager maintenance', error);
}
}
createBackend() {
switch (this.config.backend) {
case 'sqlite':
return new SQLiteBackend(this.config.sqlitePath || './claude-flow.db', this.logger);
case 'markdown':
return new MarkdownBackend(this.config.markdownDir || './memory', this.logger);
case 'hybrid':
// Use SQLite for structured data and Markdown for human-readable backup
return new HybridBackend(new SQLiteBackend(this.config.sqlitePath || './claude-flow.db', this.logger), new MarkdownBackend(this.config.markdownDir || './memory', this.logger), this.logger);
default:
throw new MemoryError(`Unknown memory backend: ${this.config.backend}`);
}
}
startSyncInterval() {
this.syncInterval = setInterval(async () => {
try {
await this.syncCache();
}
catch (error) {
this.logger.error('Cache sync error', error);
}
}, this.config.syncInterval);
}
async syncCache() {
const dirtyEntries = this.cache.getDirtyEntries();
if (dirtyEntries.length === 0) {
return;
}
this.logger.debug('Syncing cache to backend', { count: dirtyEntries.length });
const promises = dirtyEntries.map((entry) => this.backend.store(entry).catch((error) => {
this.logger.error('Failed to sync entry', { id: entry.id, error });
}));
await Promise.all(promises);
this.cache.markClean(dirtyEntries.map((e) => e.id));
// Emit sync event
this.eventBus.emit('memory:synced', { entries: dirtyEntries });
}
async flushCache() {
const allEntries = this.cache.getAllEntries();
if (allEntries.length === 0) {
return;
}
this.logger.info('Flushing cache to backend', { count: allEntries.length });
const promises = allEntries.map((entry) => this.backend.store(entry).catch((error) => {
this.logger.error('Failed to flush entry', { id: entry.id, error });
}));
await Promise.all(promises);
}
}
/**
* Hybrid backend that uses both SQLite and Markdown
*/
class HybridBackend {
primary;
secondary;
logger;
constructor(primary, secondary, logger) {
this.primary = primary;
this.secondary = secondary;
this.logger = logger;
}
async initialize() {
await Promise.all([this.primary.initialize(), this.secondary.initialize()]);
}
async shutdown() {
await Promise.all([this.primary.shutdown(), this.secondary.shutdown()]);
}
async store(entry) {
// Store in both backends
await Promise.all([
this.primary.store(entry),
this.secondary.store(entry).catch((error) => {
this.logger.warn('Failed to store in secondary backend', { error });
}),
]);
}
async retrieve(id) {
// Try primary first
const entry = await this.primary.retrieve(id);
if (entry) {
return entry;
}
// Fall back to secondary
return await this.secondary.retrieve(id);
}
async update(id, entry) {
await Promise.all([
this.primary.update(id, entry),
this.secondary.update(id, entry).catch((error) => {
this.logger.warn('Failed to update in secondary backend', { error });
}),
]);
}
async delete(id) {
await Promise.all([
this.primary.delete(id),
this.secondary.delete(id).catch((error) => {
this.logger.warn('Failed to delete from secondary backend', { error });
}),
]);
}
async query(query) {
// Use primary for querying (faster)
return await this.primary.query(query);
}
async getAllEntries() {
return await this.primary.getAllEntries();
}
async getHealthStatus() {
const [primaryHealth, secondaryHealth] = await Promise.all([
this.primary.getHealthStatus(),
this.secondary.getHealthStatus(),
]);
const error = primaryHealth.error || secondaryHealth.error;
return {
healthy: primaryHealth.healthy && secondaryHealth.healthy,
...(error && { error }),
metrics: {
...primaryHealth.metrics,
...secondaryHealth.metrics,
},
};
}
}
//# sourceMappingURL=manager.js.map