Spaces:
Sleeping
Sleeping
| 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; | |