Skip to main content
Glama
orneryd

M.I.M.I.R - Multi-agent Intelligent Memory & Insight Repository

by orneryd
decay_integration.go16.9 kB
// Package temporal - Decay integration for adaptive memory retention. // // DecayIntegration modifies NornicDB's decay system based on temporal patterns: // - Frequently accessed nodes decay SLOWER (important memories persist) // - Rarely accessed nodes decay FASTER (forgotten memories fade) // - Nodes with daily patterns maintain longer (routine knowledge) // - Burst-accessed nodes get temporary boost (current focus) // // This creates a more human-like memory system where: // - Things you use often stay fresh // - Things you forget naturally fade // - Context matters (current session nodes are prioritized) // // Integration points: // - DecayManager: Call GetDecayModifier() to adjust decay rate // - ArchiveManager: Call ShouldArchive() to identify cold nodes // - SearchRanker: Call GetRelevanceBoost() to rank results // // # ELI12 (Explain Like I'm 12) // // Your brain forgets things! But it's SMART about what it forgets: // // 🧠 Your best friend's name? → NEVER forget (use it daily!) // 🧠 What you had for lunch today → Remember for a bit, then forget // 🧠 Random fact from 5 years ago → Probably already forgot it // // DecayIntegration makes the database work like your brain: // // The "decay" is like forgetting. Every memory slowly fades over time. // But the Kalman filter velocity tells us HOW to adjust the forgetting speed: // // 📈 Velocity positive (accessing MORE often): // "Hey, you're using this a lot lately - slow down the forgetting!" // Decay multiplier: 0.1 (10x slower decay) // // 📉 Velocity negative (accessing LESS often): // "You used to look at this every day, now it's been weeks..." // Decay multiplier: 2.0 (2x faster decay) // // 📊 Velocity stable: // "Normal usage pattern, normal forgetting speed" // Decay multiplier: 1.0 (normal decay) // // Special cases: // // 🔥 BURST: Looking at something 10 times RIGHT NOW? // → "Super important right now!" → Nearly zero decay // // 📅 DAILY PATTERN: Access every morning at 9am? // → "Part of your routine!" → Slower decay // // ❄️ COLD: Haven't touched in 2 weeks? // → "Probably not important anymore" → Faster decay, maybe archive // // The Kalman filter smooths out noise. If you access something once by accident, // it doesn't suddenly become "important". It waits to see a TREND. package temporal import ( "math" "sync" "time" "github.com/orneryd/nornicdb/pkg/filter" ) // DecayModifier represents how decay should be adjusted. type DecayModifier struct { // Multiplier for decay rate (0.5 = half decay speed, 2.0 = double decay speed) Multiplier float64 // Reason for the modification Reason string // Confidence in this modification (0-1) Confidence float64 // Components that contributed to this modifier Components []DecayComponent } // DecayComponent represents a single factor affecting decay. type DecayComponent struct { Name string Multiplier float64 Weight float64 } // DecayIntegrationConfig holds configuration for decay integration. type DecayIntegrationConfig struct { // BaseDecayRate - the default decay rate per hour (0-1) BaseDecayRate float64 // FrequentAccessBoost - how much to slow decay for frequent access (0.1 = 10x slower) FrequentAccessBoost float64 // RareAccessPenalty - how much to speed decay for rare access (2.0 = 2x faster) RareAccessPenalty float64 // DailyPatternBoost - boost for nodes with daily patterns DailyPatternBoost float64 // BurstBoostDuration - how long burst boost lasts (seconds) BurstBoostDuration float64 // BurstBoostMultiplier - decay multiplier during burst BurstBoostMultiplier float64 // SessionBoostMultiplier - decay multiplier for current session nodes SessionBoostMultiplier float64 // MinDecayMultiplier - minimum decay multiplier (prevent immortal nodes) MinDecayMultiplier float64 // MaxDecayMultiplier - maximum decay multiplier (prevent instant death) MaxDecayMultiplier float64 // VelocityWeight - how much velocity affects decay VelocityWeight float64 // PatternWeight - how much patterns affect decay PatternWeight float64 // RecencyWeight - how much recency affects decay RecencyWeight float64 } // DefaultDecayIntegrationConfig returns sensible defaults. func DefaultDecayIntegrationConfig() DecayIntegrationConfig { return DecayIntegrationConfig{ BaseDecayRate: 0.01, // 1% per hour base decay FrequentAccessBoost: 0.1, // 10x slower decay for frequent access RareAccessPenalty: 2.0, // 2x faster decay for rare access DailyPatternBoost: 0.5, // 2x slower for daily patterns BurstBoostDuration: 300, // 5 minute burst boost BurstBoostMultiplier: 0.1, // 10x slower during burst SessionBoostMultiplier: 0.2, // 5x slower for current session MinDecayMultiplier: 0.05, // Never slower than 20x base MaxDecayMultiplier: 5.0, // Never faster than 5x base VelocityWeight: 0.4, // 40% weight for velocity PatternWeight: 0.3, // 30% weight for patterns RecencyWeight: 0.3, // 30% weight for recency } } // ConservativeDecayConfig returns config that preserves more memories. func ConservativeDecayConfig() DecayIntegrationConfig { cfg := DefaultDecayIntegrationConfig() cfg.FrequentAccessBoost = 0.05 // 20x slower for frequent cfg.MinDecayMultiplier = 0.02 // Can be 50x slower cfg.MaxDecayMultiplier = 2.0 // Never faster than 2x return cfg } // AggressiveDecayConfig returns config that forgets faster. func AggressiveDecayConfig() DecayIntegrationConfig { cfg := DefaultDecayIntegrationConfig() cfg.RareAccessPenalty = 5.0 // 5x faster for rare cfg.MinDecayMultiplier = 0.2 // Can only be 5x slower cfg.MaxDecayMultiplier = 10.0 // Can be 10x faster return cfg } // DecayIntegration manages decay rate modifications based on temporal data. type DecayIntegration struct { mu sync.RWMutex config DecayIntegrationConfig // Components tracker *Tracker patternDetector *PatternDetector sessionDetector *SessionDetector // Per-node decay state nodeDecay map[string]*nodeDecayState // Kalman filter for smoothing decay adjustments decayFilter *filter.KalmanAdaptive } // nodeDecayState tracks decay-related state for a node. type nodeDecayState struct { // Last calculated modifier lastModifier DecayModifier // Last update time lastUpdate time.Time // Burst state burstStart time.Time inBurst bool // Cached scores velocityScore float64 patternScore float64 recencyScore float64 } // NewDecayIntegration creates a new decay integration system. func NewDecayIntegration(cfg DecayIntegrationConfig) *DecayIntegration { return &DecayIntegration{ config: cfg, tracker: NewTracker(DefaultConfig()), patternDetector: NewPatternDetector(DefaultPatternDetectorConfig()), sessionDetector: NewSessionDetector(DefaultSessionDetectorConfig()), nodeDecay: make(map[string]*nodeDecayState), decayFilter: filter.NewKalmanAdaptive(filter.DefaultAdaptiveConfig()), } } // NewDecayIntegrationWithComponents creates decay integration with existing components. func NewDecayIntegrationWithComponents( cfg DecayIntegrationConfig, tracker *Tracker, pattern *PatternDetector, session *SessionDetector, ) *DecayIntegration { return &DecayIntegration{ config: cfg, tracker: tracker, patternDetector: pattern, sessionDetector: session, nodeDecay: make(map[string]*nodeDecayState), decayFilter: filter.NewKalmanAdaptive(filter.DefaultAdaptiveConfig()), } } // RecordAccess records an access and updates all temporal components. func (di *DecayIntegration) RecordAccess(nodeID string) { di.RecordAccessAt(nodeID, time.Now()) } // RecordAccessAt records an access at a specific time. func (di *DecayIntegration) RecordAccessAt(nodeID string, timestamp time.Time) { // Update all components di.tracker.RecordAccessAt(nodeID, timestamp) di.patternDetector.RecordAccess(nodeID, timestamp) di.sessionDetector.RecordAccess(nodeID, timestamp) // Update decay state di.mu.Lock() defer di.mu.Unlock() state, exists := di.nodeDecay[nodeID] if !exists { state = &nodeDecayState{} di.nodeDecay[nodeID] = state } // Check for burst if exists && time.Since(state.lastUpdate).Seconds() < 10 { if !state.inBurst { state.inBurst = true state.burstStart = timestamp } } state.lastUpdate = timestamp } // GetDecayModifier returns the decay rate modifier for a node. func (di *DecayIntegration) GetDecayModifier(nodeID string) DecayModifier { di.mu.RLock() state := di.nodeDecay[nodeID] di.mu.RUnlock() components := make([]DecayComponent, 0, 5) totalWeight := 0.0 weightedMultiplier := 0.0 // 1. Velocity component (access rate trend) velocity, trend := di.tracker.GetAccessRateTrend(nodeID) velocityMult := di.calculateVelocityMultiplier(velocity, trend) components = append(components, DecayComponent{ Name: "velocity", Multiplier: velocityMult, Weight: di.config.VelocityWeight, }) weightedMultiplier += velocityMult * di.config.VelocityWeight totalWeight += di.config.VelocityWeight // 2. Pattern component (daily/weekly patterns) patterns := di.patternDetector.DetectPatterns(nodeID, velocity) patternMult := di.calculatePatternMultiplier(patterns) components = append(components, DecayComponent{ Name: "pattern", Multiplier: patternMult, Weight: di.config.PatternWeight, }) weightedMultiplier += patternMult * di.config.PatternWeight totalWeight += di.config.PatternWeight // 3. Recency component (time since last access) stats := di.tracker.GetStats(nodeID) recencyMult := di.calculateRecencyMultiplier(stats) components = append(components, DecayComponent{ Name: "recency", Multiplier: recencyMult, Weight: di.config.RecencyWeight, }) weightedMultiplier += recencyMult * di.config.RecencyWeight totalWeight += di.config.RecencyWeight // 4. Session component (current session boost) session := di.sessionDetector.GetCurrentSession(nodeID) if session != nil && session.IsCurrent { sessionMult := di.config.SessionBoostMultiplier components = append(components, DecayComponent{ Name: "session", Multiplier: sessionMult, Weight: 0.5, // High weight for current session }) weightedMultiplier += sessionMult * 0.5 totalWeight += 0.5 } // 5. Burst component (recent rapid access) if state != nil && state.inBurst { burstAge := time.Since(state.burstStart).Seconds() if burstAge < di.config.BurstBoostDuration { burstMult := di.config.BurstBoostMultiplier components = append(components, DecayComponent{ Name: "burst", Multiplier: burstMult, Weight: 0.3, }) weightedMultiplier += burstMult * 0.3 totalWeight += 0.3 } else { // Burst expired di.mu.Lock() if s := di.nodeDecay[nodeID]; s != nil { s.inBurst = false } di.mu.Unlock() } } // Calculate final multiplier var finalMultiplier float64 if totalWeight > 0 { finalMultiplier = weightedMultiplier / totalWeight } else { finalMultiplier = 1.0 } // Clamp to min/max if finalMultiplier < di.config.MinDecayMultiplier { finalMultiplier = di.config.MinDecayMultiplier } if finalMultiplier > di.config.MaxDecayMultiplier { finalMultiplier = di.config.MaxDecayMultiplier } // Smooth with Kalman filter smoothed := di.decayFilter.Process(finalMultiplier) if smoothed > 0 { finalMultiplier = smoothed } // Determine reason reason := di.determineReason(components, finalMultiplier) // Calculate confidence confidence := di.calculateConfidence(stats, components) return DecayModifier{ Multiplier: finalMultiplier, Reason: reason, Confidence: confidence, Components: components, } } // calculateVelocityMultiplier converts velocity to decay multiplier. func (di *DecayIntegration) calculateVelocityMultiplier(velocity float64, trend string) float64 { switch trend { case "increasing": // More access = slower decay return di.config.FrequentAccessBoost * (1.0 + velocity) case "decreasing": // Less access = faster decay return di.config.RareAccessPenalty * (1.0 - velocity) default: return 1.0 } } // calculatePatternMultiplier converts patterns to decay multiplier. func (di *DecayIntegration) calculatePatternMultiplier(patterns []DetectedPattern) float64 { if len(patterns) == 0 { return 1.0 } bestMult := 1.0 for _, p := range patterns { var mult float64 switch p.Type { case PatternDaily: mult = di.config.DailyPatternBoost * (1.0 - p.Confidence*0.5) case PatternWeekly: mult = di.config.DailyPatternBoost * (1.0 - p.Confidence*0.3) case PatternBurst: mult = di.config.BurstBoostMultiplier case PatternGrowing: mult = di.config.FrequentAccessBoost case PatternDecaying: mult = di.config.RareAccessPenalty default: mult = 1.0 } if mult < bestMult { bestMult = mult } } return bestMult } // calculateRecencyMultiplier converts time since last access to multiplier. func (di *DecayIntegration) calculateRecencyMultiplier(stats *NodeStats) float64 { if stats == nil { return di.config.RareAccessPenalty } hoursSinceAccess := time.Since(stats.LastAccess).Hours() if hoursSinceAccess < 1 { return di.config.FrequentAccessBoost // Very recent } else if hoursSinceAccess < 24 { return 0.5 // Accessed today } else if hoursSinceAccess < 168 { // 1 week return 1.0 // Normal decay } else { return di.config.RareAccessPenalty // Old } } // determineReason generates a human-readable reason for the modifier. func (di *DecayIntegration) determineReason(components []DecayComponent, multiplier float64) string { if multiplier < 0.3 { for _, c := range components { if c.Name == "session" { return "current_session" } if c.Name == "burst" { return "burst_activity" } } return "frequently_accessed" } else if multiplier > 1.5 { return "rarely_accessed" } return "normal_activity" } // calculateConfidence calculates confidence in the decay modifier. func (di *DecayIntegration) calculateConfidence(stats *NodeStats, components []DecayComponent) float64 { if stats == nil { return 0.1 // Low confidence without data } // More accesses = more confidence accessConfidence := math.Min(float64(stats.TotalAccesses)/100.0, 1.0) // More components = more confidence componentConfidence := math.Min(float64(len(components))/5.0, 1.0) return (accessConfidence + componentConfidence) / 2.0 } // GetEffectiveDecayRate returns the actual decay rate for a node. func (di *DecayIntegration) GetEffectiveDecayRate(nodeID string) float64 { modifier := di.GetDecayModifier(nodeID) return di.config.BaseDecayRate * modifier.Multiplier } // ShouldArchive checks if a node should be archived based on temporal data. func (di *DecayIntegration) ShouldArchive(nodeID string, currentScore float64, archiveThreshold float64) bool { modifier := di.GetDecayModifier(nodeID) // Adjust threshold based on temporal data // Hot nodes get a lower effective threshold (harder to archive) // Cold nodes get a higher effective threshold (easier to archive) effectiveThreshold := archiveThreshold * modifier.Multiplier return currentScore < effectiveThreshold } // GetRelevanceBoost returns a relevance boost for search ranking. func (di *DecayIntegration) GetRelevanceBoost(nodeID string) float64 { modifier := di.GetDecayModifier(nodeID) // Invert the decay modifier - low decay = high relevance // Clamp to reasonable range (0.5 - 2.0) boost := 1.0 / modifier.Multiplier if boost < 0.5 { boost = 0.5 } if boost > 2.0 { boost = 2.0 } return boost } // GetHotNodes returns nodes that should be prioritized (slow decay). func (di *DecayIntegration) GetHotNodes(limit int) []string { return di.tracker.GetHotNodes(limit) } // GetColdNodes returns nodes that are candidates for archival. func (di *DecayIntegration) GetColdNodes(limit int) []string { return di.tracker.GetColdNodes(limit) } // Reset clears all temporal data. func (di *DecayIntegration) Reset() { di.mu.Lock() defer di.mu.Unlock() di.tracker.Reset() di.patternDetector.Reset() di.sessionDetector.Reset() di.nodeDecay = make(map[string]*nodeDecayState) } // GetStats returns statistics for the decay integration. type DecayIntegrationStats struct { TrackedNodes int ActiveSessions int TotalAccesses int64 AverageMultiplier float64 } // GetStats returns current statistics. func (di *DecayIntegration) GetStats() DecayIntegrationStats { globalStats := di.tracker.GetGlobalStats() activeSessions := di.sessionDetector.GetActiveSessions() di.mu.RLock() nodeCount := len(di.nodeDecay) di.mu.RUnlock() return DecayIntegrationStats{ TrackedNodes: nodeCount, ActiveSessions: len(activeSessions), TotalAccesses: globalStats.TotalAccesses, } }

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/orneryd/Mimir'

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