package decision import "sync" // ringSize is how many turns are kept. Turns arrive at human rate, not machine // rate, so the whole store is memory: no migration, no insert on the answer // path, and nothing of his words survives a restart. That is what makes this // cheap enough to leave on always (V-564). Ecosystem traces went to SQLite // because one act writes several hops and they must outlive the turn; an // arbitration record is read minutes later or never. const ringSize = 25 // Ring holds the newest records, newest first on read. type Ring struct { mu sync.Mutex recs []*Record } func NewRing() *Ring { return &Ring{} } // Push adds one finished record and drops the oldest past the bound. // // The dropped pointers are cleared before the reslice. Resliceing alone moves // the window forward and leaves the evicted records addressable from the // backing array, so up to ringSize turns he had already aged out stayed in // memory until the next append reallocated. That is a leak anywhere and it is // the wrong one here, because the reason this store is memory-only is that his // words should not outlive the diagnosis. func (r *Ring) Push(rec *Record) { if r == nil || rec == nil { return } r.mu.Lock() defer r.mu.Unlock() r.recs = append(r.recs, rec) if drop := len(r.recs) - ringSize; drop > 0 { for i := 0; i < drop; i++ { r.recs[i] = nil } r.recs = r.recs[drop:] } } // Recent returns up to n records, newest first. func (r *Ring) Recent(n int) []*Record { if r == nil || n <= 0 { return nil } r.mu.Lock() defer r.mu.Unlock() if n > len(r.recs) { n = len(r.recs) } out := make([]*Record, 0, n) for i := 0; i < n; i++ { out = append(out, r.recs[len(r.recs)-1-i]) } return out }