@sethdouglasford/claude-flow
Version:
Claude Code Flow - Advanced AI-powered development workflows with SPARC methodology
426 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;
}
}
createBank(agentId) {
if (!this.initialized) {
throw new MemoryError("Memory manager not initialized");
}
const bank = {
id: `bank-${Date.now()}-${Math.random().toString(36).slice(2)}`,
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 Promise.resolve(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 });
}
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)
void 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 });
return Promise.resolve();
}
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;
}
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) {
const searchTerm = query.search.toLowerCase();
results = results.filter(entry => entry.content.toLowerCase().includes(searchTerm) ||
entry.tags.some(tag => tag.toLowerCase().includes(searchTerm)));
}
// 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 Promise.resolve(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: ${String(this.config.backend)}`);
}
}
startSyncInterval() {
this.syncInterval = setInterval(() => {
void this.syncCache();
}, 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