/**
* @copyright 2025 Chris Bunting <cbuntingde@gmail.com>
* @license MIT
*
* Memory storage implementation for the Memory MCP Server
*/
import * as fs from 'fs';
import * as path from 'path';
import {
ShortTermMemory,
LongTermMemory,
EpisodicMemory,
MemoryStoreConfig
} from '../types/index.js';
import { Logger } from '../utils/Logger.js';
export class MemoryStore {
private shortTermMemory: Map<string, ShortTermMemory> = new Map();
private longTermMemory: Map<string, LongTermMemory> = new Map();
private episodicMemory: Map<string, EpisodicMemory> = new Map();
private config: MemoryStoreConfig;
private logger = Logger.getInstance();
constructor(config: MemoryStoreConfig) {
this.config = config;
this.ensureDataDirectory();
this.loadPersistedData();
}
private ensureDataDirectory(): void {
if (!fs.existsSync(this.config.dataDir)) {
fs.mkdirSync(this.config.dataDir, { recursive: true });
}
}
private loadPersistedData(): void {
try {
// Load long-term memory
const longTermPath = path.join(this.config.dataDir, 'long-term.json');
if (fs.existsSync(longTermPath)) {
const data = JSON.parse(fs.readFileSync(longTermPath, 'utf8'));
Object.entries(data).forEach(([userId, memory]: [string, any]) => {
this.longTermMemory.set(userId, memory);
});
}
// Load episodic memory
const episodicPath = path.join(this.config.dataDir, 'episodic.json');
if (fs.existsSync(episodicPath)) {
const data = JSON.parse(fs.readFileSync(episodicPath, 'utf8'));
Object.entries(data).forEach(([id, memory]: [string, any]) => {
this.episodicMemory.set(id, memory);
});
}
} catch (error) {
this.logger.error('Error loading persisted data:', error);
}
}
private persistLongTermMemory(): void {
try {
const data = Object.fromEntries(this.longTermMemory);
fs.writeFileSync(
path.join(this.config.dataDir, 'long-term.json'),
JSON.stringify(data, null, 2)
);
} catch (error) {
this.logger.error('Error persisting long-term memory:', error);
}
}
private persistEpisodicMemory(): void {
try {
const data = Object.fromEntries(this.episodicMemory);
fs.writeFileSync(
path.join(this.config.dataDir, 'episodic.json'),
JSON.stringify(data, null, 2)
);
} catch (error) {
this.logger.error('Error persisting episodic memory:', error);
}
}
// Short-term memory operations
setShortTermMemory(sessionId: string, key: string, value: any, ttlMinutes?: number): void {
const ttl = ttlMinutes || this.config.defaultTTLMinutes || 30;
const expiresAt = Date.now() + (ttl * 60 * 1000);
const existing = this.shortTermMemory.get(sessionId);
if (existing && existing.expiresAt > Date.now()) {
existing.data[key] = value;
existing.expiresAt = expiresAt;
} else {
this.shortTermMemory.set(sessionId, {
sessionId,
data: { [key]: value },
timestamp: Date.now(),
expiresAt
});
}
}
getShortTermMemory(sessionId: string, key?: string): any {
const memory = this.shortTermMemory.get(sessionId);
if (!memory || memory.expiresAt <= Date.now()) {
this.shortTermMemory.delete(sessionId);
return null;
}
return key ? memory.data[key] : memory.data;
}
clearExpiredShortTermMemory(): void {
const now = Date.now();
for (const [sessionId, memory] of this.shortTermMemory.entries()) {
if (memory.expiresAt <= now) {
this.shortTermMemory.delete(sessionId);
}
}
}
// Long-term memory operations
setLongTermMemory(userId: string, data: Partial<LongTermMemory>): void {
const existing = this.longTermMemory.get(userId) || {
userId,
lastUpdated: Date.now()
};
const updated = {
...existing,
...data,
lastUpdated: Date.now()
};
this.longTermMemory.set(userId, updated);
this.persistLongTermMemory();
}
getLongTermMemory(userId: string): LongTermMemory | null {
return this.longTermMemory.get(userId) || null;
}
getAllLongTermMemory(): Map<string, LongTermMemory> {
return new Map(this.longTermMemory);
}
// Episodic memory operations
addEpisodicMemory(memory: Omit<EpisodicMemory, 'id' | 'timestamp'>): string {
const id = `episodic_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const episodicMemory: EpisodicMemory = {
...memory,
id,
timestamp: Date.now()
};
this.episodicMemory.set(id, episodicMemory);
this.persistEpisodicMemory();
return id;
}
getEpisodicMemory(userId: string, limit?: number): EpisodicMemory[] {
const memories = Array.from(this.episodicMemory.values())
.filter(memory => memory.userId === userId)
.sort((a, b) => b.timestamp - a.timestamp);
return limit ? memories.slice(0, limit) : memories;
}
searchEpisodicMemory(userId: string, query: string, limit?: number): EpisodicMemory[] {
const searchTerm = query.toLowerCase();
const memories = Array.from(this.episodicMemory.values())
.filter(memory =>
memory.userId === userId &&
(memory.event.toLowerCase().includes(searchTerm) ||
memory.context.toLowerCase().includes(searchTerm) ||
memory.outcome?.toLowerCase().includes(searchTerm) ||
memory.tags?.some(tag => tag.toLowerCase().includes(searchTerm)))
)
.sort((a, b) => b.timestamp - a.timestamp);
return limit ? memories.slice(0, limit) : memories;
}
getAllEpisodicMemory(): Map<string, EpisodicMemory> {
return new Map(this.episodicMemory);
}
}