Skip to main content
Glama
orneryd

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

by orneryd
cooldown.go11.5 kB
// Package inference provides edge materialization with cooldown protection. // // Cooldown logic prevents echo chambers from rapid co-access bursts by // enforcing minimum time between materializations of the same edge pair. // // Feature flag: NORNICDB_COOLDOWN_ENABLED=true (enabled by default) // // Usage Example 1: Basic cooldown check // // table := NewCooldownTable() // if table.CanMaterialize("nodeA", "nodeB", "relates_to") { // db.CreateEdge("nodeA", "nodeB", "relates_to") // table.RecordMaterialization("nodeA", "nodeB", "relates_to") // } // // Usage Example 2: With custom cooldown durations // // customCooldowns := map[string]time.Duration{ // "important_link": 30 * time.Minute, // Longer cooldown for important edges // "casual_link": 1 * time.Minute, // Shorter cooldown for casual edges // } // table := NewCooldownTableWithConfig(customCooldowns) // // Usage Example 3: Check with reason (for debugging) // // canMat, reason := table.CanMaterializeWithReason("nodeA", "nodeB", "relates_to") // if !canMat { // log.Printf("Cannot materialize: %s", reason) // "cooldown active, 3m2s remaining" // } // // ELI12 (Explain Like I'm 12): // // Imagine you're playing tag at recess. After you tag someone: // - Cooldown prevents you from immediately tagging them again (no "tag-backs") // - You must wait 30 seconds before you can tag that same person // - This prevents annoying rapid-fire tagging (echo chamber) // // In NornicDB: // - You suggest edge A→B based on co-access // - Edge gets created // - 5 seconds later, they're accessed together again → same suggestion! // - Cooldown says "no, wait 5 minutes before suggesting A→B again" // - This prevents creating duplicate edges or flip-flopping // // Without cooldown, rapid co-access could create hundreds of duplicate suggestions! package inference import ( "fmt" "sync" "time" "github.com/orneryd/nornicdb/pkg/config" ) // DefaultCooldowns defines standard cooldown durations per edge label. // These can be overridden per-table or globally. var DefaultCooldowns = map[string]time.Duration{ "relates_to": 5 * time.Minute, "similar_to": 10 * time.Minute, "coaccess": 1 * time.Minute, "topology": 15 * time.Minute, // For TLP-generated edges "depends_on": 30 * time.Minute, "references": 5 * time.Minute, "semantic_link": 10 * time.Minute, } // DefaultCooldown is used when no label-specific cooldown is configured. const DefaultCooldown = 5 * time.Minute // CooldownEntry tracks the last materialization time for an edge pair. type CooldownEntry struct { LastMaterialized time.Time Count int64 // Total materializations for this pair } // CooldownTable tracks when edges were last materialized to prevent spam. // Thread-safe for concurrent access. type CooldownTable struct { mu sync.RWMutex entries map[string]*CooldownEntry // key: "src:dst:label" defaults map[string]time.Duration // per-label defaults // Stats totalChecks int64 totalBlocked int64 totalAllowed int64 } // CooldownStats provides observability into cooldown behavior. type CooldownStats struct { TotalEntries int64 TotalChecks int64 TotalBlocked int64 TotalAllowed int64 BlockRate float64 // Blocked / Checks (0.0 - 1.0) } // NewCooldownTable creates a new cooldown table with default settings. func NewCooldownTable() *CooldownTable { return &CooldownTable{ entries: make(map[string]*CooldownEntry), defaults: copyDefaults(DefaultCooldowns), } } // NewCooldownTableWithConfig creates a cooldown table with custom defaults. func NewCooldownTableWithConfig(labelCooldowns map[string]time.Duration) *CooldownTable { ct := NewCooldownTable() for label, duration := range labelCooldowns { ct.defaults[label] = duration } return ct } // cooldownKey creates a consistent key for an edge pair. func cooldownKey(src, dst, label string) string { return fmt.Sprintf("%s:%s:%s", src, dst, label) } // CanMaterialize checks if an edge can be materialized without violating cooldown. // Returns true if cooldown period has passed or if cooldown feature is disabled. func (ct *CooldownTable) CanMaterialize(src, dst, label string) bool { // Feature flag check - if disabled, always allow if !config.IsCooldownEnabled() { return true } ct.mu.RLock() defer ct.mu.RUnlock() ct.totalChecks++ key := cooldownKey(src, dst, label) entry, exists := ct.entries[key] if !exists { ct.totalAllowed++ return true } cooldown := ct.getCooldownDuration(label) elapsed := time.Since(entry.LastMaterialized) if elapsed >= cooldown { ct.totalAllowed++ return true } ct.totalBlocked++ return false } // CanMaterializeWithReason returns whether materialization is allowed and the reason. // Useful for debugging and audit logs. func (ct *CooldownTable) CanMaterializeWithReason(src, dst, label string) (bool, string) { if !config.IsCooldownEnabled() { return true, "cooldown feature disabled" } ct.mu.RLock() defer ct.mu.RUnlock() key := cooldownKey(src, dst, label) entry, exists := ct.entries[key] if !exists { return true, "first materialization for this pair" } cooldown := ct.getCooldownDuration(label) elapsed := time.Since(entry.LastMaterialized) if elapsed >= cooldown { return true, fmt.Sprintf("cooldown expired (elapsed: %s, required: %s)", elapsed, cooldown) } remaining := cooldown - elapsed return false, fmt.Sprintf("cooldown active (remaining: %s, last: %s ago)", remaining.Round(time.Second), elapsed.Round(time.Second)) } // RecordMaterialization records that an edge was materialized. // Should be called after successfully creating an edge. func (ct *CooldownTable) RecordMaterialization(src, dst, label string) { ct.mu.Lock() defer ct.mu.Unlock() key := cooldownKey(src, dst, label) entry, exists := ct.entries[key] if !exists { entry = &CooldownEntry{} ct.entries[key] = entry } entry.LastMaterialized = time.Now() entry.Count++ } // RecordMaterializationAt records a materialization at a specific time. // Useful for replaying events or testing. func (ct *CooldownTable) RecordMaterializationAt(src, dst, label string, t time.Time) { ct.mu.Lock() defer ct.mu.Unlock() key := cooldownKey(src, dst, label) entry, exists := ct.entries[key] if !exists { entry = &CooldownEntry{} ct.entries[key] = entry } entry.LastMaterialized = t entry.Count++ } // getCooldownDuration returns the cooldown duration for a label. // Must be called with at least a read lock held. func (ct *CooldownTable) getCooldownDuration(label string) time.Duration { if duration, ok := ct.defaults[label]; ok { return duration } return DefaultCooldown } // SetLabelCooldown sets the cooldown duration for a specific label. func (ct *CooldownTable) SetLabelCooldown(label string, duration time.Duration) { ct.mu.Lock() defer ct.mu.Unlock() ct.defaults[label] = duration } // GetLabelCooldown returns the cooldown duration for a label. func (ct *CooldownTable) GetLabelCooldown(label string) time.Duration { ct.mu.RLock() defer ct.mu.RUnlock() return ct.getCooldownDuration(label) } // TimeUntilAllowed returns how long until a materialization will be allowed. // Returns 0 if already allowed. func (ct *CooldownTable) TimeUntilAllowed(src, dst, label string) time.Duration { if !config.IsCooldownEnabled() { return 0 } ct.mu.RLock() defer ct.mu.RUnlock() key := cooldownKey(src, dst, label) entry, exists := ct.entries[key] if !exists { return 0 } cooldown := ct.getCooldownDuration(label) elapsed := time.Since(entry.LastMaterialized) if elapsed >= cooldown { return 0 } return cooldown - elapsed } // GetEntry returns the cooldown entry for an edge pair. // Returns nil if no entry exists. func (ct *CooldownTable) GetEntry(src, dst, label string) *CooldownEntry { ct.mu.RLock() defer ct.mu.RUnlock() key := cooldownKey(src, dst, label) entry, exists := ct.entries[key] if !exists { return nil } // Return a copy to prevent mutation return &CooldownEntry{ LastMaterialized: entry.LastMaterialized, Count: entry.Count, } } // Clear removes all cooldown entries. // Useful for testing or resetting state. func (ct *CooldownTable) Clear() { ct.mu.Lock() defer ct.mu.Unlock() ct.entries = make(map[string]*CooldownEntry) ct.totalChecks = 0 ct.totalBlocked = 0 ct.totalAllowed = 0 } // Cleanup removes expired entries to prevent memory growth. // Should be called periodically (e.g., every 10 minutes). func (ct *CooldownTable) Cleanup() int { ct.mu.Lock() defer ct.mu.Unlock() removed := 0 now := time.Now() for key, entry := range ct.entries { // Extract label from key (format: "src:dst:label") // Find the last colon to get the label lastColon := len(key) - 1 for lastColon >= 0 && key[lastColon] != ':' { lastColon-- } label := "" if lastColon >= 0 && lastColon < len(key)-1 { label = key[lastColon+1:] } cooldown := ct.getCooldownDuration(label) // Remove entries that have been expired for longer than the cooldown period // This gives a 2x grace period before cleanup if now.Sub(entry.LastMaterialized) > cooldown*2 { delete(ct.entries, key) removed++ } } return removed } // Stats returns current cooldown table statistics. func (ct *CooldownTable) Stats() CooldownStats { ct.mu.RLock() defer ct.mu.RUnlock() stats := CooldownStats{ TotalEntries: int64(len(ct.entries)), TotalChecks: ct.totalChecks, TotalBlocked: ct.totalBlocked, TotalAllowed: ct.totalAllowed, } if stats.TotalChecks > 0 { stats.BlockRate = float64(stats.TotalBlocked) / float64(stats.TotalChecks) } return stats } // Size returns the number of tracked edge pairs. func (ct *CooldownTable) Size() int { ct.mu.RLock() defer ct.mu.RUnlock() return len(ct.entries) } // copyDefaults creates a copy of the default cooldowns map. func copyDefaults(defaults map[string]time.Duration) map[string]time.Duration { copy := make(map[string]time.Duration, len(defaults)) for k, v := range defaults { copy[k] = v } return copy } // CooldownTableOption configures a CooldownTable. type CooldownTableOption func(*CooldownTable) // WithLabelCooldown sets a specific label cooldown during initialization. func WithLabelCooldown(label string, duration time.Duration) CooldownTableOption { return func(ct *CooldownTable) { ct.defaults[label] = duration } } // WithDefaultCooldown sets the fallback cooldown for unknown labels. func WithDefaultCooldown(duration time.Duration) CooldownTableOption { return func(ct *CooldownTable) { // Store as empty string key for default lookup ct.defaults[""] = duration } } // NewCooldownTableWithOptions creates a table with functional options. func NewCooldownTableWithOptions(opts ...CooldownTableOption) *CooldownTable { ct := NewCooldownTable() for _, opt := range opts { opt(ct) } return ct } // Global singleton for convenience var globalCooldownTable *CooldownTable var globalCooldownOnce sync.Once // GlobalCooldownTable returns the global cooldown table singleton. // Lazily initialized on first call. func GlobalCooldownTable() *CooldownTable { globalCooldownOnce.Do(func() { globalCooldownTable = NewCooldownTable() }) return globalCooldownTable } // ResetGlobalCooldownTable resets the global cooldown table. // Primarily for testing. func ResetGlobalCooldownTable() { globalCooldownOnce = sync.Once{} globalCooldownTable = nil }

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