Skip to main content
Glama
decay-engine.test.ts42.5 kB
/** * Temporal Decay Engine Tests * * Tests for exponential decay calculations with sector-specific rates. * Validates decay formula, minimum strength floor, age calculations, and batch processing. * * Requirements: 3.1, 3.2, 3.3, 3.4, 3.5 */ import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { DatabaseConnectionManager } from "../../../database/connection-manager"; import { MemorySector } from "../../../embeddings/types"; import type { Memory } from "../../../memory/types"; import { TemporalDecayEngine } from "../../../temporal/decay-engine"; import { SectorConfigManager } from "../../../temporal/sector-config"; import type { DecayConfig } from "../../../temporal/types"; describe("TemporalDecayEngine - Decay Calculations", () => { let configManager: SectorConfigManager; let config: DecayConfig; let db: DatabaseConnectionManager; let decayEngine: TemporalDecayEngine; beforeEach(async () => { configManager = new SectorConfigManager(); config = configManager.getConfig(); // Create database connection db = new DatabaseConnectionManager({ host: process.env.DB_HOST || "localhost", port: parseInt(process.env.DB_PORT || "5433"), database: process.env.DB_NAME || "thoughtmcp_test", user: process.env.DB_USER || "postgres", password: process.env.DB_PASSWORD || "postgres", poolSize: 5, connectionTimeout: 5000, idleTimeout: 30000, }); await db.connect(); // Create decay engine decayEngine = new TemporalDecayEngine(configManager, db); }); afterEach(async () => { await db.disconnect(); }); describe("Exponential Decay Formula", () => { it("should calculate decay using formula: strength = initial × exp(-λ × time)", () => { // Test exponential decay formula // Formula: strength = initial × exp(-λ × time) // where λ = baseLambda × sectorMultiplier // and time is in days const initialStrength = 1.0; const timeDays = 10; const sector = MemorySector.Episodic; // Create a memory that was last accessed 10 days ago const currentTime = new Date("2024-11-12T12:00:00Z"); const lastAccessed = new Date("2024-11-02T12:00:00Z"); // 10 days ago const memory: Memory = { id: "test-mem-1", content: "Test memory", createdAt: lastAccessed, lastAccessed, accessCount: 1, salience: 0.5, decayRate: config.baseLambda * config.sectorMultipliers[sector], strength: initialStrength, userId: "user-1", sessionId: "session-1", primarySector: "episodic", metadata: { keywords: [], tags: [], importance: 0.5, isAtomic: true, }, }; // Calculate decay using the engine const actualStrength = decayEngine.calculateDecayedStrength(memory, currentTime); // Calculate expected decay const lambda = config.baseLambda * config.sectorMultipliers[sector]; const expectedStrength = initialStrength * Math.exp(-lambda * timeDays); // Expected: ~0.74 for episodic with default config (0.02 * 1.5 * 10 days) expect(actualStrength).toBeCloseTo(expectedStrength, 2); expect(actualStrength).toBeCloseTo(0.74, 2); }); it("should apply exponential decay correctly for different time periods", () => { // Test decay over multiple time periods const initialStrength = 1.0; const sector = MemorySector.Semantic; const currentTime = new Date("2024-11-12T12:00:00Z"); // Test 1 day const memory1Day: Memory = { id: "test-mem-1day", content: "Test memory", createdAt: new Date("2024-11-11T12:00:00Z"), lastAccessed: new Date("2024-11-11T12:00:00Z"), accessCount: 1, salience: 0.5, decayRate: config.baseLambda * config.sectorMultipliers[sector], strength: initialStrength, userId: "user-1", sessionId: "session-1", primarySector: "semantic", metadata: { keywords: [], tags: [], importance: 0.5, isAtomic: true, }, }; const strength1Day = decayEngine.calculateDecayedStrength(memory1Day, currentTime); expect(strength1Day).toBeGreaterThan(0.99); // Minimal decay after 1 day // Test 30 days const memory30Days: Memory = { ...memory1Day, id: "test-mem-30days", lastAccessed: new Date("2024-10-13T12:00:00Z"), }; const strength30Days = decayEngine.calculateDecayedStrength(memory30Days, currentTime); expect(strength30Days).toBeGreaterThan(0.7); // Moderate decay after 30 days // Test 90 days const memory90Days: Memory = { ...memory1Day, id: "test-mem-90days", lastAccessed: new Date("2024-08-14T12:00:00Z"), }; const strength90Days = decayEngine.calculateDecayedStrength(memory90Days, currentTime); expect(strength90Days).toBeGreaterThan(0.4); // Significant decay after 90 days // Verify decay is monotonically decreasing expect(strength1Day).toBeGreaterThan(strength30Days); expect(strength30Days).toBeGreaterThan(strength90Days); }); it("should handle zero time (no decay)", () => { const initialStrength = 0.8; const sector = MemorySector.Procedural; const currentTime = new Date("2024-11-12T12:00:00Z"); const memory: Memory = { id: "test-mem-zero", content: "Test memory", createdAt: currentTime, lastAccessed: currentTime, // Same as current time = 0 days accessCount: 1, salience: 0.5, decayRate: config.baseLambda * config.sectorMultipliers[sector], strength: initialStrength, userId: "user-1", sessionId: "session-1", primarySector: "procedural", metadata: { keywords: [], tags: [], importance: 0.5, isAtomic: true, }, }; const strength = decayEngine.calculateDecayedStrength(memory, currentTime); // No time passed = no decay expect(strength).toBe(initialStrength); }); it("should handle fractional days correctly", () => { const initialStrength = 1.0; const sector = MemorySector.Emotional; const currentTime = new Date("2024-11-12T12:00:00Z"); const lastAccessed = new Date("2024-11-12T00:00:00Z"); // 12 hours ago const memory: Memory = { id: "test-mem-fractional", content: "Test memory", createdAt: lastAccessed, lastAccessed, accessCount: 1, salience: 0.5, decayRate: config.baseLambda * config.sectorMultipliers[sector], strength: initialStrength, userId: "user-1", sessionId: "session-1", primarySector: "emotional", metadata: { keywords: [], tags: [], importance: 0.5, isAtomic: true, }, }; const strength = decayEngine.calculateDecayedStrength(memory, currentTime); // Should have minimal decay after 12 hours expect(strength).toBeGreaterThan(0.98); expect(strength).toBeLessThan(1.0); }); }); describe("Sector-Specific Decay Rate Application", () => { it("should apply different decay rates for different sectors", () => { // Test that different sectors decay at different rates const initialStrength = 1.0; const timeDays = 30; // Calculate decay for each sector const episodicLambda = config.baseLambda * config.sectorMultipliers[MemorySector.Episodic]; const semanticLambda = config.baseLambda * config.sectorMultipliers[MemorySector.Semantic]; const proceduralLambda = config.baseLambda * config.sectorMultipliers[MemorySector.Procedural]; const emotionalLambda = config.baseLambda * config.sectorMultipliers[MemorySector.Emotional]; const reflectiveLambda = config.baseLambda * config.sectorMultipliers[MemorySector.Reflective]; const episodicStrength = initialStrength * Math.exp(-episodicLambda * timeDays); const semanticStrength = initialStrength * Math.exp(-semanticLambda * timeDays); const proceduralStrength = initialStrength * Math.exp(-proceduralLambda * timeDays); const emotionalStrength = initialStrength * Math.exp(-emotionalLambda * timeDays); const reflectiveStrength = initialStrength * Math.exp(-reflectiveLambda * timeDays); // Verify decay order: Episodic > Emotional > Reflective > Procedural > Semantic expect(episodicStrength).toBeLessThan(emotionalStrength); expect(emotionalStrength).toBeLessThan(reflectiveStrength); expect(reflectiveStrength).toBeLessThan(proceduralStrength); expect(proceduralStrength).toBeLessThan(semanticStrength); // Semantic should have highest strength (slowest decay) expect(semanticStrength).toBeGreaterThan(0.7); // Episodic should have lowest strength (fastest decay) expect(episodicStrength).toBeLessThan(0.7); }); it("should use correct multipliers from configuration", () => { // Verify multipliers are applied correctly const sector = MemorySector.Episodic; const effectiveRate = configManager.getEffectiveDecayRate(sector); const expectedRate = config.baseLambda * config.sectorMultipliers[sector]; expect(effectiveRate).toBe(expectedRate); }); it("should handle custom sector multipliers", () => { // Create custom config with different multipliers const customConfig: DecayConfig = { ...config, sectorMultipliers: { [MemorySector.Episodic]: 2.0, // Very fast decay [MemorySector.Semantic]: 0.3, // Very slow decay [MemorySector.Procedural]: 1.0, [MemorySector.Emotional]: 1.5, [MemorySector.Reflective]: 0.8, }, }; const customManager = new SectorConfigManager(customConfig); const episodicRate = customManager.getEffectiveDecayRate(MemorySector.Episodic); const semanticRate = customManager.getEffectiveDecayRate(MemorySector.Semantic); expect(episodicRate).toBe(customConfig.baseLambda * 2.0); expect(semanticRate).toBe(customConfig.baseLambda * 0.3); expect(episodicRate).toBeGreaterThan(semanticRate); }); }); describe("Minimum Strength Floor Enforcement", () => { it("should enforce minimum strength floor of 0.1", () => { // Test that strength never goes below minimum floor const initialStrength = 1.0; const timeDays = 1000; // Very long time const sector = MemorySector.Episodic; const lambda = config.baseLambda * config.sectorMultipliers[sector]; // Calculate raw decay (would be very low) const rawStrength = initialStrength * Math.exp(-lambda * timeDays); expect(rawStrength).toBeLessThan(config.minimumStrength); // After applying floor, should be exactly minimum const finalStrength = Math.max(rawStrength, config.minimumStrength); expect(finalStrength).toBe(config.minimumStrength); expect(finalStrength).toBe(0.1); }); it("should not modify strength above minimum floor", () => { const initialStrength = 1.0; const timeDays = 10; const sector = MemorySector.Semantic; const lambda = config.baseLambda * config.sectorMultipliers[sector]; const strength = initialStrength * Math.exp(-lambda * timeDays); // Strength should be above floor expect(strength).toBeGreaterThan(config.minimumStrength); // Applying floor should not change it const finalStrength = Math.max(strength, config.minimumStrength); expect(finalStrength).toBe(strength); }); it("should handle strength exactly at minimum floor", () => { const strength = config.minimumStrength; const finalStrength = Math.max(strength, config.minimumStrength); expect(finalStrength).toBe(config.minimumStrength); expect(finalStrength).toBe(0.1); }); it("should enforce floor for all sectors", () => { const initialStrength = 1.0; const timeDays = 500; // Long enough to hit floor for all sectors const sectors = [ MemorySector.Episodic, MemorySector.Semantic, MemorySector.Procedural, MemorySector.Emotional, MemorySector.Reflective, ]; for (const sector of sectors) { const lambda = config.baseLambda * config.sectorMultipliers[sector]; const rawStrength = initialStrength * Math.exp(-lambda * timeDays); const finalStrength = Math.max(rawStrength, config.minimumStrength); expect(finalStrength).toBeGreaterThanOrEqual(config.minimumStrength); } }); }); describe("Age Calculation in Days", () => { it("should calculate age in days from timestamps", () => { const now = new Date("2024-11-12T12:00:00Z"); const created = new Date("2024-11-02T12:00:00Z"); // 10 days ago const ageMs = now.getTime() - created.getTime(); const ageDays = ageMs / (1000 * 60 * 60 * 24); expect(ageDays).toBe(10); }); it("should handle fractional days correctly", () => { const now = new Date("2024-11-12T18:00:00Z"); const created = new Date("2024-11-12T06:00:00Z"); // 12 hours ago const ageMs = now.getTime() - created.getTime(); const ageDays = ageMs / (1000 * 60 * 60 * 24); expect(ageDays).toBe(0.5); }); it("should handle zero age (just created)", () => { const now = new Date("2024-11-12T12:00:00Z"); const created = new Date("2024-11-12T12:00:00Z"); const ageMs = now.getTime() - created.getTime(); const ageDays = ageMs / (1000 * 60 * 60 * 24); expect(ageDays).toBe(0); }); it("should calculate age for various time periods", () => { const now = new Date("2024-11-12T12:00:00Z"); // 1 day const created1Day = new Date("2024-11-11T12:00:00Z"); const age1Day = (now.getTime() - created1Day.getTime()) / (1000 * 60 * 60 * 24); expect(age1Day).toBe(1); // 7 days const created7Days = new Date("2024-11-05T12:00:00Z"); const age7Days = (now.getTime() - created7Days.getTime()) / (1000 * 60 * 60 * 24); expect(age7Days).toBe(7); // 30 days const created30Days = new Date("2024-10-13T12:00:00Z"); const age30Days = (now.getTime() - created30Days.getTime()) / (1000 * 60 * 60 * 24); expect(age30Days).toBe(30); // 90 days const created90Days = new Date("2024-08-14T12:00:00Z"); const age90Days = (now.getTime() - created90Days.getTime()) / (1000 * 60 * 60 * 24); expect(age90Days).toBe(90); }); it("should use lastAccessed for decay calculation, not createdAt", () => { // Memory should decay from last access, not creation const now = new Date("2024-11-12T12:00:00Z"); const created = new Date("2024-10-01T12:00:00Z"); // 42 days ago const lastAccessed = new Date("2024-11-10T12:00:00Z"); // 2 days ago // Age should be calculated from lastAccessed const ageFromAccess = (now.getTime() - lastAccessed.getTime()) / (1000 * 60 * 60 * 24); const ageFromCreation = (now.getTime() - created.getTime()) / (1000 * 60 * 60 * 24); expect(ageFromAccess).toBe(2); expect(ageFromCreation).toBe(42); // Decay should use lastAccessed (2 days), not created (42 days) const sector = MemorySector.Episodic; const lambda = config.baseLambda * config.sectorMultipliers[sector]; const strengthFromAccess = 1.0 * Math.exp(-lambda * ageFromAccess); const strengthFromCreation = 1.0 * Math.exp(-lambda * ageFromCreation); // Strength from access should be much higher (less decay) expect(strengthFromAccess).toBeGreaterThan(strengthFromCreation); expect(strengthFromAccess).toBeGreaterThan(0.9); }); }); describe("Batch Decay Processing", () => { it("should process multiple memories in batch", () => { // Test batch processing of decay calculations const now = new Date("2024-11-12T12:00:00Z"); const memories = [ { id: "mem1", lastAccessed: new Date("2024-11-02T12:00:00Z"), // 10 days ago strength: 1.0, sector: MemorySector.Episodic, }, { id: "mem2", lastAccessed: new Date("2024-11-07T12:00:00Z"), // 5 days ago strength: 0.8, sector: MemorySector.Semantic, }, { id: "mem3", lastAccessed: new Date("2024-11-11T12:00:00Z"), // 1 day ago strength: 0.9, sector: MemorySector.Procedural, }, ]; const results = memories.map((mem) => { const ageDays = (now.getTime() - mem.lastAccessed.getTime()) / (1000 * 60 * 60 * 24); const lambda = config.baseLambda * config.sectorMultipliers[mem.sector]; const newStrength = Math.max( mem.strength * Math.exp(-lambda * ageDays), config.minimumStrength ); return { id: mem.id, newStrength }; }); // Verify all memories processed expect(results).toHaveLength(3); // Verify decay applied to each expect(results[0].newStrength).toBeLessThan(1.0); // Episodic, 10 days expect(results[1].newStrength).toBeLessThan(0.8); // Semantic, 5 days expect(results[2].newStrength).toBeLessThan(0.9); // Procedural, 1 day // Verify all above minimum floor results.forEach((result) => { expect(result.newStrength).toBeGreaterThanOrEqual(config.minimumStrength); }); }); it("should handle large batches efficiently", () => { // Test processing 1000 memories (requirement: batch size 1000) const now = new Date("2024-11-12T12:00:00Z"); const batchSize = 1000; const memories = Array.from({ length: batchSize }, (_, i) => ({ id: `mem${i}`, lastAccessed: new Date(now.getTime() - i * 24 * 60 * 60 * 1000), // i days ago strength: 1.0, sector: MemorySector.Episodic, })); const startTime = Date.now(); const results = memories.map((mem) => { const ageDays = (now.getTime() - mem.lastAccessed.getTime()) / (1000 * 60 * 60 * 24); const lambda = config.baseLambda * config.sectorMultipliers[mem.sector]; const newStrength = Math.max( mem.strength * Math.exp(-lambda * ageDays), config.minimumStrength ); return { id: mem.id, newStrength }; }); const processingTime = Date.now() - startTime; // Verify all processed expect(results).toHaveLength(batchSize); // Verify processing is fast (should be < 100ms for 1000 calculations) expect(processingTime).toBeLessThan(100); // Verify decay gradient (older memories have lower strength) expect(results[0].newStrength).toBeGreaterThan(results[100].newStrength); // Note: results[500] will hit minimum floor (0.1) so we check it's at floor expect(results[500].newStrength).toBe(config.minimumStrength); }); it("should handle mixed sectors in batch", () => { const now = new Date("2024-11-12T12:00:00Z"); const sectors = [ MemorySector.Episodic, MemorySector.Semantic, MemorySector.Procedural, MemorySector.Emotional, MemorySector.Reflective, ]; const memories = sectors.map((sector, i) => ({ id: `mem${i}`, lastAccessed: new Date("2024-11-02T12:00:00Z"), // All 10 days old strength: 1.0, sector, })); const results = memories.map((mem) => { const ageDays = (now.getTime() - mem.lastAccessed.getTime()) / (1000 * 60 * 60 * 24); const lambda = config.baseLambda * config.sectorMultipliers[mem.sector]; const newStrength = Math.max( mem.strength * Math.exp(-lambda * ageDays), config.minimumStrength ); return { id: mem.id, sector: mem.sector, newStrength }; }); // Verify different decay rates applied const episodic = results.find((r) => r.sector === MemorySector.Episodic)!; const semantic = results.find((r) => r.sector === MemorySector.Semantic)!; expect(episodic.newStrength).toBeLessThan(semantic.newStrength); }); it("should handle empty batch gracefully", () => { const memories: Array<{ id: string; lastAccessed: Date; strength: number; sector: MemorySector; }> = []; const results = memories.map((mem) => { const ageDays = (Date.now() - mem.lastAccessed.getTime()) / (1000 * 60 * 60 * 24); const lambda = config.baseLambda * config.sectorMultipliers[mem.sector]; const newStrength = Math.max( mem.strength * Math.exp(-lambda * ageDays), config.minimumStrength ); return { id: mem.id, newStrength }; }); expect(results).toHaveLength(0); }); it("should preserve memory IDs in batch processing", () => { const now = new Date("2024-11-12T12:00:00Z"); const memories = [ { id: "abc123", lastAccessed: new Date("2024-11-02T12:00:00Z"), strength: 1.0, sector: MemorySector.Episodic, }, { id: "def456", lastAccessed: new Date("2024-11-07T12:00:00Z"), strength: 0.8, sector: MemorySector.Semantic, }, { id: "ghi789", lastAccessed: new Date("2024-11-11T12:00:00Z"), strength: 0.9, sector: MemorySector.Procedural, }, ]; const results = memories.map((mem) => { const ageDays = (now.getTime() - mem.lastAccessed.getTime()) / (1000 * 60 * 60 * 24); const lambda = config.baseLambda * config.sectorMultipliers[mem.sector]; const newStrength = Math.max( mem.strength * Math.exp(-lambda * ageDays), config.minimumStrength ); return { id: mem.id, newStrength }; }); expect(results[0].id).toBe("abc123"); expect(results[1].id).toBe("def456"); expect(results[2].id).toBe("ghi789"); }); }); describe("Integration with Configuration", () => { it("should use current configuration for decay calculations", () => { const initialStrength = 1.0; const timeDays = 10; const sector = MemorySector.Episodic; // Calculate with default config const defaultLambda = config.baseLambda * config.sectorMultipliers[sector]; const defaultStrength = initialStrength * Math.exp(-defaultLambda * timeDays); // Update configuration configManager.updateConfig({ baseLambda: 0.04, // Double the base rate }); const updatedConfig = configManager.getConfig(); const updatedLambda = updatedConfig.baseLambda * updatedConfig.sectorMultipliers[sector]; const updatedStrength = initialStrength * Math.exp(-updatedLambda * timeDays); // Updated config should produce more decay expect(updatedStrength).toBeLessThan(defaultStrength); }); it("should respect updated minimum strength floor", () => { const initialStrength = 1.0; const timeDays = 1000; // Very long time const sector = MemorySector.Episodic; // Update minimum strength configManager.updateConfig({ minimumStrength: 0.2, // Higher floor }); const updatedConfig = configManager.getConfig(); const lambda = updatedConfig.baseLambda * updatedConfig.sectorMultipliers[sector]; const rawStrength = initialStrength * Math.exp(-lambda * timeDays); const finalStrength = Math.max(rawStrength, updatedConfig.minimumStrength); expect(finalStrength).toBe(0.2); expect(finalStrength).toBeGreaterThan(0.1); // Higher than default floor }); }); describe("Database Operations - Apply Decay", () => { it("should apply decay to a single memory in database", async () => { // Create a test memory (2 days old for minimal decay) const twoDaysAgo = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000); const client = await db.getConnection(); try { await client.query( `INSERT INTO memories (id, content, created_at, last_accessed, access_count, salience, decay_rate, strength, user_id, session_id, primary_sector) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`, [ "test-decay-1", "Test memory for decay", twoDaysAgo, twoDaysAgo, 1, 0.5, 0.03, 1.0, "user-1", "session-1", "episodic", ] ); } finally { db.releaseConnection(client); } const memory: Memory = { id: "test-decay-1", content: "Test memory for decay", createdAt: twoDaysAgo, lastAccessed: twoDaysAgo, accessCount: 1, salience: 0.5, decayRate: 0.03, strength: 1.0, userId: "user-1", sessionId: "session-1", primarySector: "episodic", metadata: {}, }; // Apply decay await decayEngine.applyDecay(memory); // Verify memory was updated const client2 = await db.getConnection(); try { const result = await client2.query(`SELECT strength FROM memories WHERE id = $1`, [ "test-decay-1", ]); expect(result.rows).toHaveLength(1); expect(result.rows[0].strength).toBeLessThan(1.0); expect(result.rows[0].strength).toBeGreaterThan(0.9); // 2 days = minimal decay } finally { db.releaseConnection(client2); } // Cleanup const client3 = await db.getConnection(); try { await client3.query(`DELETE FROM memories WHERE id = $1`, ["test-decay-1"]); } finally { db.releaseConnection(client3); } }); it("should handle negative time (future lastAccessed)", async () => { const futureTime = new Date("2025-11-12T12:00:00Z"); const memory: Memory = { id: "test-future", content: "Test memory", createdAt: new Date(), lastAccessed: futureTime, accessCount: 1, salience: 0.5, decayRate: 0.03, strength: 0.8, userId: "user-1", sessionId: "session-1", primarySector: "episodic", metadata: {}, }; const currentTime = new Date("2024-11-12T12:00:00Z"); const strength = decayEngine.calculateDecayedStrength(memory, currentTime); // Should return original strength for future times expect(strength).toBe(0.8); }); }); describe("Database Operations - Batch Apply Decay", () => { it("should apply decay to multiple memories in batch", async () => { // Create test memories (2 days old for minimal decay) const twoDaysAgo = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000); const client = await db.getConnection(); try { for (let i = 1; i <= 3; i++) { await client.query( `INSERT INTO memories (id, content, created_at, last_accessed, access_count, salience, decay_rate, strength, user_id, session_id, primary_sector) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`, [ `test-batch-${i}`, `Test memory ${i}`, twoDaysAgo, twoDaysAgo, 1, 0.5, 0.03, 1.0, "user-1", "session-1", "episodic", ] ); } } finally { db.releaseConnection(client); } const memories: Memory[] = [1, 2, 3].map((i) => ({ id: `test-batch-${i}`, content: `Test memory ${i}`, createdAt: twoDaysAgo, lastAccessed: twoDaysAgo, accessCount: 1, salience: 0.5, decayRate: 0.03, strength: 1.0, userId: "user-1", sessionId: "session-1", primarySector: "episodic", metadata: {}, })); // Apply batch decay await decayEngine.batchApplyDecay(memories); // Verify all memories were updated const client2 = await db.getConnection(); try { for (let i = 1; i <= 3; i++) { const result = await client2.query(`SELECT strength FROM memories WHERE id = $1`, [ `test-batch-${i}`, ]); expect(result.rows).toHaveLength(1); expect(result.rows[0].strength).toBeLessThan(1.0); } } finally { db.releaseConnection(client2); } // Cleanup const client3 = await db.getConnection(); try { for (let i = 1; i <= 3; i++) { await client3.query(`DELETE FROM memories WHERE id = $1`, [`test-batch-${i}`]); } } finally { db.releaseConnection(client3); } }); it("should handle empty batch gracefully", async () => { await expect(decayEngine.batchApplyDecay([])).resolves.toBeUndefined(); }); it("should rollback on error", async () => { const memories: Memory[] = [ { id: "non-existent-memory", content: "Test", createdAt: new Date(), lastAccessed: new Date(), accessCount: 1, salience: 0.5, decayRate: 0.03, strength: 1.0, userId: "user-1", sessionId: "session-1", primarySector: "episodic", metadata: {}, }, ]; // Should not throw but transaction should rollback await expect(decayEngine.batchApplyDecay(memories)).resolves.toBeUndefined(); }); }); describe("Database Operations - Reinforce Memory", () => { it("should reinforce memory by boosting strength", async () => { // Create test memory const client = await db.getConnection(); try { await client.query( `INSERT INTO memories (id, content, created_at, last_accessed, access_count, salience, decay_rate, strength, user_id, session_id, primary_sector) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`, [ "test-reinforce-1", "Test memory", new Date(), new Date(), 1, 0.5, 0.03, 0.6, "user-1", "session-1", "episodic", ] ); } finally { db.releaseConnection(client); } // Reinforce with boost of 0.2 await decayEngine.reinforceMemory("test-reinforce-1", 0.2); // Verify strength increased const client2 = await db.getConnection(); try { const result = await client2.query(`SELECT strength FROM memories WHERE id = $1`, [ "test-reinforce-1", ]); expect(result.rows[0].strength).toBeCloseTo(0.8, 2); } finally { db.releaseConnection(client2); } // Cleanup const client3 = await db.getConnection(); try { await client3.query(`DELETE FROM memories WHERE id = $1`, ["test-reinforce-1"]); } finally { db.releaseConnection(client3); } }); it("should cap strength at 1.0", async () => { // Create test memory with high strength const client = await db.getConnection(); try { await client.query( `INSERT INTO memories (id, content, created_at, last_accessed, access_count, salience, decay_rate, strength, user_id, session_id, primary_sector) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`, [ "test-reinforce-cap", "Test memory", new Date(), new Date(), 1, 0.5, 0.03, 0.9, "user-1", "session-1", "episodic", ] ); } finally { db.releaseConnection(client); } // Reinforce with large boost await decayEngine.reinforceMemory("test-reinforce-cap", 0.5); // Verify strength capped at 1.0 const client2 = await db.getConnection(); try { const result = await client2.query(`SELECT strength FROM memories WHERE id = $1`, [ "test-reinforce-cap", ]); expect(result.rows[0].strength).toBe(1.0); } finally { db.releaseConnection(client2); } // Cleanup const client3 = await db.getConnection(); try { await client3.query(`DELETE FROM memories WHERE id = $1`, ["test-reinforce-cap"]); } finally { db.releaseConnection(client3); } }); it("should throw error for non-existent memory", async () => { await expect(decayEngine.reinforceMemory("non-existent", 0.1)).rejects.toThrow( "Memory not found" ); }); }); describe("Database Operations - Auto Reinforce on Access", () => { it("should auto-reinforce memory on access", async () => { // Create test memory const client = await db.getConnection(); try { await client.query( `INSERT INTO memories (id, content, created_at, last_accessed, access_count, salience, decay_rate, strength, user_id, session_id, primary_sector) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`, [ "test-auto-reinforce", "Test memory", new Date(), new Date(), 1, 0.5, 0.03, 0.7, "user-1", "session-1", "episodic", ] ); } finally { db.releaseConnection(client); } // Auto-reinforce await decayEngine.autoReinforceOnAccess("test-auto-reinforce"); // Verify strength increased and access count incremented const client2 = await db.getConnection(); try { const result = await client2.query( `SELECT strength, access_count FROM memories WHERE id = $1`, ["test-auto-reinforce"] ); expect(result.rows[0].strength).toBeGreaterThan(0.7); expect(result.rows[0].access_count).toBe(2); } finally { db.releaseConnection(client2); } // Cleanup const client3 = await db.getConnection(); try { await client3.query(`DELETE FROM memories WHERE id = $1`, ["test-auto-reinforce"]); } finally { db.releaseConnection(client3); } }); it("should throw error for non-existent memory", async () => { await expect(decayEngine.autoReinforceOnAccess("non-existent")).rejects.toThrow( "Memory not found" ); }); }); describe("Database Operations - Schedule Decay Job", () => { it("should validate cron expression", () => { expect(() => decayEngine.scheduleDecayJob("0 2 * * *")).not.toThrow(); }); it("should reject invalid cron expression", () => { expect(() => decayEngine.scheduleDecayJob("")).toThrow("Invalid cron expression"); }); it("should reject non-string cron expression", () => { expect(() => decayEngine.scheduleDecayJob(null as unknown as string)).toThrow( "Invalid cron expression" ); }); }); describe("Database Operations - Identify Pruning Candidates", () => { it("should identify memories below pruning threshold", async () => { // Create test memories with low strength const client = await db.getConnection(); try { await client.query( `INSERT INTO memories (id, content, created_at, last_accessed, access_count, salience, decay_rate, strength, user_id, session_id, primary_sector) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`, [ "test-prune-1", "Test memory", new Date(), new Date(), 1, 0.5, 0.03, 0.05, "user-1", "session-1", "episodic", ] ); await client.query( `INSERT INTO memory_metadata (memory_id, keywords, tags, importance, category, context, is_atomic, parent_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`, ["test-prune-1", [], [], 0.2, null, null, true, null] ); } finally { db.releaseConnection(client); } const candidates = await decayEngine.identifyPruningCandidates(0.1); expect(candidates).toContain("test-prune-1"); // Cleanup const client2 = await db.getConnection(); try { await client2.query(`DELETE FROM memory_metadata WHERE memory_id = $1`, ["test-prune-1"]); await client2.query(`DELETE FROM memories WHERE id = $1`, ["test-prune-1"]); } finally { db.releaseConnection(client2); } }); it("should not identify important memories", async () => { // Create test memory with low strength but high importance const client = await db.getConnection(); try { await client.query( `INSERT INTO memories (id, content, created_at, last_accessed, access_count, salience, decay_rate, strength, user_id, session_id, primary_sector) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`, [ "test-important", "Test memory", new Date(), new Date(), 1, 0.5, 0.03, 0.05, "user-1", "session-1", "episodic", ] ); await client.query( `INSERT INTO memory_metadata (memory_id, keywords, tags, importance, category, context, is_atomic, parent_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`, ["test-important", [], [], 0.8, null, null, true, null] ); } finally { db.releaseConnection(client); } const candidates = await decayEngine.identifyPruningCandidates(0.1); expect(candidates).not.toContain("test-important"); // Cleanup const client2 = await db.getConnection(); try { await client2.query(`DELETE FROM memory_metadata WHERE memory_id = $1`, ["test-important"]); await client2.query(`DELETE FROM memories WHERE id = $1`, ["test-important"]); } finally { db.releaseConnection(client2); } }); }); describe("Database Operations - Prune Memories", () => { it("should delete memories by IDs", async () => { // Create test memory const client = await db.getConnection(); try { await client.query( `INSERT INTO memories (id, content, created_at, last_accessed, access_count, salience, decay_rate, strength, user_id, session_id, primary_sector) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`, [ "test-delete-1", "Test memory", new Date(), new Date(), 1, 0.5, 0.03, 0.05, "user-1", "session-1", "episodic", ] ); } finally { db.releaseConnection(client); } const deletedCount = await decayEngine.pruneMemories(["test-delete-1"]); expect(deletedCount).toBe(1); // Verify memory was deleted const client2 = await db.getConnection(); try { const result = await client2.query(`SELECT * FROM memories WHERE id = $1`, [ "test-delete-1", ]); expect(result.rows).toHaveLength(0); } finally { db.releaseConnection(client2); } }); it("should handle empty array", async () => { const deletedCount = await decayEngine.pruneMemories([]); expect(deletedCount).toBe(0); }); it("should handle non-existent IDs gracefully", async () => { const deletedCount = await decayEngine.pruneMemories(["non-existent-1", "non-existent-2"]); expect(deletedCount).toBe(0); }); }); describe("Database Operations - Run Decay Maintenance", () => { it("should run full maintenance cycle", async () => { // Create test memories (2 days old) const twoDaysAgo = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000); const client = await db.getConnection(); try { for (let i = 1; i <= 5; i++) { await client.query( `INSERT INTO memories (id, content, created_at, last_accessed, access_count, salience, decay_rate, strength, user_id, session_id, primary_sector) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`, [ `test-maint-${i}`, `Test memory ${i}`, twoDaysAgo, twoDaysAgo, 1, 0.5, 0.03, i <= 2 ? 0.05 : 0.8, // First 2 have low strength "user-1", "session-1", "episodic", ] ); await client.query( `INSERT INTO memory_metadata (memory_id, keywords, tags, importance, category, context, is_atomic, parent_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`, [`test-maint-${i}`, [], [], 0.2, null, null, true, null] ); } } finally { db.releaseConnection(client); } const result = await decayEngine.runDecayMaintenance(); expect(result.processedCount).toBeGreaterThan(0); expect(result.processingTime).toBeGreaterThan(0); expect(result.errors).toHaveLength(0); // Cleanup remaining memories const client2 = await db.getConnection(); try { for (let i = 1; i <= 5; i++) { await client2.query(`DELETE FROM memory_metadata WHERE memory_id = $1`, [ `test-maint-${i}`, ]); await client2.query(`DELETE FROM memories WHERE id = $1`, [`test-maint-${i}`]); } } finally { db.releaseConnection(client2); } }); it("should handle errors gracefully", async () => { const result = await decayEngine.runDecayMaintenance(); expect(result).toHaveProperty("processedCount"); expect(result).toHaveProperty("prunedCount"); expect(result).toHaveProperty("processingTime"); expect(result).toHaveProperty("errors"); }); }); });

Latest Blog Posts

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/keyurgolani/ThoughtMcp'

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