import logger from '../utils/logger.js'; import fs from 'fs/promises'; import path from 'path'; class ContextMemory { constructor(options = {}) { this._memory = new Map(); this._conversationHistory = []; this._decisions = []; this._maxHistory = options.maxHistorySize || 500; this._maxDecisions = options.maxDecisionsSize || 200; this._persistFile = options.persistFile || 'data/memory.json'; this._persistInterval = options.persistInterval || 15 * 60 * 1000; this._lastPersist = 0; this._initialized = false; } async initialize() { if (this._initialized) return; try { await this._load(); this._initialized = true; logger.info(`Memory initialized: ${this._memory.size} entries, ${this._conversationHistory.length} conversations`); } catch (error) { logger.warn(`Could not load memory: ${error.message}, starting fresh`); this._initialized = true; } } remember(key, value, metadata = {}) { this._memory.set(key, { value, metadata, timestamp: Date.now(), }); this._maybePersist(); } recall(key) { const entry = this._memory.get(key); if (!entry) return null; return entry.value; } recallWithMetadata(key) { return this._memory.get(key) || null; } forget(key) { this._memory.delete(key); this._maybePersist(); } has(key) { return this._memory.has(key); } getAllByTag(tag) { const results = []; for (const [key, entry] of this._memory.entries()) { if (entry.metadata.tags?.includes(tag)) { results.push({ key, ...entry }); } } return results; } addConversation(entry) { this._conversationHistory.push({ ...entry, timestamp: Date.now(), id: crypto.randomUUID(), }); if (this._conversationHistory.length > this._maxHistory) { this._conversationHistory = this._conversationHistory.slice(-this._maxHistory); } this._maybePersist(); } getConversationHistory(limit = 50) { return this._conversationHistory.slice(-limit); } getConversationByIssue(issueNumber) { return this._conversationHistory.filter( c => c.issueNumber === issueNumber || c.prNumber === issueNumber ); } addDecision(decision) { this._decisions.push({ ...decision, timestamp: Date.now(), id: crypto.randomUUID(), }); if (this._decisions.length > this._maxDecisions) { this._decisions = this._decisions.slice(-this._maxDecisions); } this._maybePersist(); } getDecisions(limit = 50) { return this._decisions.slice(-limit); } getRecentDecisions(hours = 24) { const cutoff = Date.now() - hours * 60 * 60 * 1000; return this._decisions.filter(d => d.timestamp > cutoff); } getContextSummary() { const recentDecisions = this.getRecentDecisions(24); const recentConversations = this._conversationHistory.slice(-20); return { totalMemories: this._memory.size, recentDecisions: recentDecisions.length, recentConversations: recentConversations.length, lastActivity: this._conversationHistory.length > 0 ? this._conversationHistory[this._conversationHistory.length - 1].timestamp : null, }; } getPersonaContext() { return { currentRole: this.recall('currentRole') || 'full-stack', workingOn: this.recall('currentTask') || null, recentFocus: this.recall('recentFocus') || null, preferences: this.recall('developerPreferences') || {}, ongoingDiscussions: this.getAllByTag('discussion').length, openPRs: this.getAllByTag('open-pr').length, activeIssues: this.getAllByTag('active-issue').length, }; } cleanup(maxAge = 7 * 24 * 60 * 60 * 1000) { const cutoff = Date.now() - maxAge; let cleaned = 0; for (const [key, entry] of this._memory.entries()) { if (entry.timestamp < cutoff) { this._memory.delete(key); cleaned++; } } this._conversationHistory = this._conversationHistory.filter( c => c.timestamp > cutoff ); this._decisions = this._decisions.filter(d => d.timestamp > cutoff); if (cleaned > 0) { logger.info(`Memory cleanup: removed ${cleaned} old entries`); this._persist(); } return cleaned; } clear() { this._memory.clear(); this._conversationHistory = []; this._decisions = []; this._persist(); } export() { return { memory: Array.from(this._memory.entries()), conversations: this._conversationHistory, decisions: this._decisions, version: '1.0', exportedAt: new Date().toISOString(), }; } import(data) { if (data.memory) { this._memory = new Map(data.memory); } if (data.conversations) { this._conversationHistory = data.conversations; } if (data.decisions) { this._decisions = data.decisions; } this._initialized = true; } async forcePersist() { await this._persist(); } _maybePersist() { const now = Date.now(); if (now - this._lastPersist > this._persistInterval) { this._persist(); } } async _persist() { try { const data = this.export(); const dir = path.dirname(this._persistFile); await fs.mkdir(dir, { recursive: true }); await fs.writeFile(this._persistFile, JSON.stringify(data, null, 2), 'utf8'); this._lastPersist = Date.now(); } catch (error) { logger.error(`Failed to persist memory: ${error.message}`); } } async _load() { try { const content = await fs.readFile(this._persistFile, 'utf8'); const data = JSON.parse(content); this.import(data); } catch (error) { if (error.code === 'ENOENT') { return; } throw error; } } } export default ContextMemory;