Skip to main content
Glama

DollhouseMCP

by DollhouseMCP
MEMORY_FOLDER_IMPLEMENTATION_PLAN.md13 kB
# Memory Folder Structure Implementation Plan ## Executive Summary Implement date-based folder organization for memory elements to prevent flat directory issues when scaling to 10K+ files/year. This is a clean implementation with NO backward compatibility needed (memories haven't been released yet). ## Current State Verification ✅ - **MemoryManager.ts exists**: VERIFIED - Lines 1-350+ implementing IElementManager - **Memory.test.ts exists**: VERIFIED - Unit tests in place - **constants.ts exists**: VERIFIED - Constants and security events defined - **No existing memory files**: VERIFIED - Clean slate for new structure ## Implementation Requirements ### 1. Core Folder Structure ``` ~/.dollhouse/portfolio/memories/ ├── 2025-09-18/ # Date folders (YYYY-MM-DD) │ ├── session-morning.yaml # Single file (<100KB) │ └── dnd-players-handbook/ # Multi-part (>100KB) [FUTURE] │ ├── index.yaml # Manifest │ └── part001.yaml # Sharded content ``` ### 2. Key Features to Implement 1. **Date-based folder organization** - YYYY-MM-DD folders 2. **Content hash generation** - SHA-256 for deduplication (Issue #994) 3. **Collision handling** - Version suffixes (-v2, -v3) for same names 4. **Platform source tagging** - Metadata for generation platform 5. **Enhanced MemoryManager** - Support new folder structure ## Files to Modify (EXACT IMPLEMENTATION) ### A. MemoryManager.ts Changes #### Add to class properties (line ~31): ```typescript private contentHashIndex: Map<string, string> = new Map(); ``` #### Add new method - generateMemoryPath (after line ~107): ```typescript /** * Generate date-based path for memory storage * Creates YYYY-MM-DD folder structure to prevent flat directory issues * @param element Memory element to save * @param fileName Optional custom filename * @returns Full path to memory file */ private async generateMemoryPath(element: Memory, fileName?: string): Promise<string> { const date = new Date(); const dateFolder = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; const datePath = path.join(this.memoriesDir, dateFolder); // Ensure date folder exists await fs.mkdir(datePath, { recursive: true }); // Generate filename const baseName = fileName || `${element.metadata.name?.toLowerCase().replace(/\s+/g, '-') || 'memory'}.yaml`; let finalName = baseName; let version = 1; // Handle collisions with version suffix while (await fs.access(path.join(datePath, finalName)).then(() => true).catch(() => false)) { version++; finalName = baseName.replace('.yaml', `-v${version}.yaml`); } return path.join(datePath, finalName); } ``` #### Add new method - calculateContentHash (after generateMemoryPath): ```typescript /** * Calculate SHA-256 hash of memory content for deduplication * Implements Issue #994 - Content-based deduplication */ private calculateContentHash(element: Memory): string { const crypto = require('crypto'); const content = JSON.stringify({ metadata: element.metadata, entries: JSON.parse(element.serialize()).entries }); return crypto.createHash('sha256').update(content).digest('hex'); } ``` #### Add new method - getDateFolders (after calculateContentHash): ```typescript /** * Get all date folders in memories directory * @returns Array of date folder names */ private async getDateFolders(): Promise<string[]> { try { const entries = await fs.readdir(this.memoriesDir, { withFileTypes: true }); return entries .filter(entry => entry.isDirectory() && /^\d{4}-\d{2}-\d{2}$/.test(entry.name)) .map(entry => entry.name) .sort() .reverse(); // Most recent first } catch (error) { if ((error as any).code === 'ENOENT') { return []; } throw error; } } ``` #### Update save method (line ~113): ```typescript async save(element: Memory, filePath?: string): Promise<void> { try { // Validate element const validation = element.validate(); if (!validation.valid) { throw new Error(`Invalid memory: ${validation.errors?.map(e => e.message).join(', ')}`); } // Calculate content hash for deduplication const contentHash = this.calculateContentHash(element); const existingPath = this.contentHashIndex.get(contentHash); if (existingPath) { // Log duplicate detection SecurityMonitor.logSecurityEvent({ type: 'MEMORY_DUPLICATE_DETECTED', severity: 'LOW', source: 'MemoryManager.save', details: `Duplicate content detected. Existing: ${existingPath}` }); } // Generate date-based path if not provided const fullPath = filePath ? await this.validateAndResolvePath(filePath) : await this.generateMemoryPath(element); // Ensure parent directory exists (for date folders) await fs.mkdir(path.dirname(fullPath), { recursive: true }); // [Rest of save implementation remains the same...] // Update content hash index this.contentHashIndex.set(contentHash, fullPath); ``` #### Update load method to check date folders (line ~43): ```typescript async load(filePath: string): Promise<Memory> { try { let fullPath: string; // Check if it's a relative path (no date folder) if (!filePath.includes(path.sep) || !filePath.match(/^\d{4}-\d{2}-\d{2}/)) { // Search in date folders const dateFolders = await this.getDateFolders(); let found = false; for (const dateFolder of dateFolders) { const testPath = path.join(this.memoriesDir, dateFolder, filePath); if (await fs.access(testPath).then(() => true).catch(() => false)) { fullPath = testPath; found = true; break; } } if (!found) { // Fall back to root directory for backward compatibility during transition fullPath = await this.validateAndResolvePath(filePath); } } else { fullPath = await this.validateAndResolvePath(filePath); } // [Rest of load implementation remains the same...] ``` #### Update list method to search date folders (line ~200): ```typescript async list(): Promise<Memory[]> { const memories: Memory[] = []; try { // Get all date folders const dateFolders = await this.getDateFolders(); // Also check root directory for any legacy files const rootFiles = await fs.readdir(this.memoriesDir) .then(files => files.filter(f => f.endsWith('.yaml'))) .catch(() => []); // Process root files first (legacy) for (const file of rootFiles) { try { const memory = await this.load(file); memories.push(memory); } catch (error) { SecurityMonitor.logSecurityEvent({ type: MEMORY_SECURITY_EVENTS.MEMORY_LIST_ITEM_FAILED, severity: 'LOW', source: 'MemoryManager.list', details: `Failed to load ${file}: ${error}` }); } } // Process date folders for (const dateFolder of dateFolders) { const folderPath = path.join(this.memoriesDir, dateFolder); const files = await fs.readdir(folderPath) .then(files => files.filter(f => f.endsWith('.yaml'))) .catch(() => []); for (const file of files) { try { const memory = await this.load(path.join(dateFolder, file)); memories.push(memory); } catch (error) { SecurityMonitor.logSecurityEvent({ type: MEMORY_SECURITY_EVENTS.MEMORY_LIST_ITEM_FAILED, severity: 'LOW', source: 'MemoryManager.list', details: `Failed to load ${dateFolder}/${file}: ${error}` }); } } } return memories; } catch (error) { // [Rest remains the same...] ``` ### B. constants.ts Changes #### Add new event type (line ~112): ```typescript MEMORY_DUPLICATE_DETECTED: 'MEMORY_DUPLICATE_DETECTED', ``` ### C. SecurityMonitor Updates #### Add to securityMonitor.ts event types: ```typescript | 'MEMORY_DUPLICATE_DETECTED' ``` ## Test Updates Required ### MemoryManager.test.ts Updates #### Add test for date folder creation: ```typescript describe('date folder structure', () => { it('should create date-based folders when saving', async () => { const memory = new Memory({ name: 'Test Memory', description: 'Testing date folders' }); await manager.save(memory); // Check that a date folder was created const entries = await fs.readdir(memoriesDir, { withFileTypes: true }); const dateFolders = entries.filter(e => e.isDirectory() && /^\d{4}-\d{2}-\d{2}$/.test(e.name)); expect(dateFolders).toHaveLength(1); // Check file exists in date folder const dateFolder = dateFolders[0].name; const files = await fs.readdir(path.join(memoriesDir, dateFolder)); expect(files.some(f => f.includes('test-memory'))).toBe(true); }); it('should handle collisions with version suffix', async () => { const memory1 = new Memory({ name: 'Same Name' }); const memory2 = new Memory({ name: 'Same Name' }); await manager.save(memory1); await manager.save(memory2); const dateFolders = await fs.readdir(memoriesDir, { withFileTypes: true }); const dateFolder = dateFolders.find(e => e.isDirectory())?.name; const files = await fs.readdir(path.join(memoriesDir, dateFolder!)); expect(files).toContain('same-name.yaml'); expect(files).toContain('same-name-v2.yaml'); }); it('should find memories across date folders', async () => { // Create memories in different date folders const date1 = '2025-09-17'; const date2 = '2025-09-18'; await fs.mkdir(path.join(memoriesDir, date1), { recursive: true }); await fs.mkdir(path.join(memoriesDir, date2), { recursive: true }); const memory1 = new Memory({ name: 'Memory 1' }); const memory2 = new Memory({ name: 'Memory 2' }); // Manually save to specific date folders for testing await manager.save(memory1, `${date1}/memory1.yaml`); await manager.save(memory2, `${date2}/memory2.yaml`); const memories = await manager.list(); expect(memories).toHaveLength(2); expect(memories.map(m => m.metadata.name)).toContain('Memory 1'); expect(memories.map(m => m.metadata.name)).toContain('Memory 2'); }); it('should detect duplicate content', async () => { const memory1 = new Memory({ name: 'Original' }); await memory1.addEntry('Same content', ['test']); const memory2 = new Memory({ name: 'Duplicate' }); await memory2.addEntry('Same content', ['test']); // Mock SecurityMonitor to capture events const logSpy = jest.spyOn(SecurityMonitor, 'logSecurityEvent'); await manager.save(memory1); await manager.save(memory2); expect(logSpy).toHaveBeenCalledWith( expect.objectContaining({ type: 'MEMORY_DUPLICATE_DETECTED' }) ); }); }); ``` ## Success Criteria 1. ✅ All memories saved to date-based folders 2. ✅ No flat directory with thousands of files 3. ✅ Collision handling with -v2, -v3 suffixes 4. ✅ Content hash detection for duplicates 5. ✅ All existing tests pass 6. ✅ New tests for folder structure pass 7. ✅ Clean implementation (<30 files changed) 8. ✅ Security events logged properly 9. ✅ No test/benchmark files in commit ## Implementation Order 1. Create this plan document 2. Update constants.ts with new event 3. Update MemoryManager.ts with all changes 4. Update securityMonitor.ts 5. Update all tests 6. Run tests to verify 7. Create clean commit 8. Create PR ## Verification Checklist - [ ] Date folders created correctly - [ ] Files saved with correct names - [ ] Collisions handled with versions - [ ] Duplicates detected via hash - [ ] Load finds files in date folders - [ ] List returns all memories - [ ] All tests passing - [ ] No extra files in commit - [ ] Security events logged ## Related Issues - #994 - Content-based deduplication (partial implementation) - #993 - Git-managed portfolio structure (foundation) - #981 - Memory sharding (foundation for future) ## Git Commands ```bash # After implementation git add src/elements/memories/ git add src/security/securityMonitor.ts git add test/__tests__/unit/elements/memories/ git add docs/development/MEMORY_FOLDER_IMPLEMENTATION_PLAN.md # Verify clean commit git status --short # Commit git commit -m "feat: Implement memory folder structure with date-based organization - Date folders (YYYY-MM-DD) prevent flat directory issues - Content hash detection for duplicates (Issue #994) - Collision handling with version suffixes - Updated all tests for new structure - No backward compatibility needed (memories not released)" # Push and create PR git push -u origin feature/memory-folder-structure-v2 gh pr create --title "feat: Memory folder structure with date-based organization" \ --body "Implementation of date-based folder structure for memory storage..." ```

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/DollhouseMCP/DollhouseMCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server