activity-simulator / src /core /contextMemory.js
abedelbahnasy55's picture
feat: cloud simulator - Docker, dashboard, auto-start
ccb6b75
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;