package store import ( "strings" "time" ) func (m *Memory) prunableRevisionIDsLocked(tenantID string, cutoff time.Time) []string { var newest *Revision for _, rev := range m.revisions { if rev == nil || rev.TenantID != tenantID { continue } if newest == nil || rev.CreatedAt.After(newest.CreatedAt) { newest = rev } } if newest == nil { return nil } protected := map[string]struct{}{newest.ID: {}} for _, sp := range m.speakers { if sp == nil || sp.TenantID != tenantID { continue } if sp.LastAppliedRevisionID != nil && strings.TrimSpace(*sp.LastAppliedRevisionID) != "" { protected[strings.TrimSpace(*sp.LastAppliedRevisionID)] = struct{}{} } } for speakerID, info := range m.publishedRevision { sp, ok := m.speakers[speakerID] if !ok || sp == nil || sp.TenantID != tenantID { continue } if strings.TrimSpace(info.RevisionID) != "" { protected[strings.TrimSpace(info.RevisionID)] = struct{}{} } } var ids []string for id, rev := range m.revisions { if rev == nil || rev.TenantID != tenantID { continue } if !rev.CreatedAt.Before(cutoff) { continue } if _, keep := protected[id]; keep { continue } ids = append(ids, id) } return ids } func revisionBytesEstimate(rev *Revision, prefixes []PrefixRow) int64 { if rev == nil { return 0 } var n int64 n += int64(len(rev.ContentHash)) for _, v := range rev.PreviewFragments { n += int64(len(v)) } for _, pr := range prefixes { n += int64(len(pr.Prefix) + len(pr.Source)) } return n } func (m *Memory) EstimateRevisionPrune(tenantID string, cutoff time.Time, retentionMinutes int) (RevisionPruneEstimate, error) { m.mu.Lock() defer m.mu.Unlock() ids := m.prunableRevisionIDsLocked(tenantID, cutoff) var bytes int64 for _, id := range ids { bytes += revisionBytesEstimate(m.revisions[id], m.revPrefixes[id]) } prefixRows := 0 for _, id := range ids { prefixRows += len(m.revPrefixes[id]) } return RevisionPruneEstimate{ RetentionMinutes: retentionMinutes, CutoffAt: cutoff.UTC(), RevisionCount: len(ids), PrefixRowCount: prefixRows, OrphanSnapshotCount: 0, BytesEstimate: bytes, }, nil } func (m *Memory) PruneRevisionsWithStats(tenantID string, cutoff time.Time) (RevisionPruneResult, error) { est, err := m.EstimateRevisionPrune(tenantID, cutoff, 0) if err != nil { return RevisionPruneResult{}, err } m.mu.Lock() defer m.mu.Unlock() ids := m.prunableRevisionIDsLocked(tenantID, cutoff) for _, id := range ids { delete(m.revisions, id) delete(m.revPrefixes, id) } return RevisionPruneResult{ DeletedRevisions: len(ids), DeletedPrefixSnapshots: 0, DeletedPrefixRows: est.PrefixRowCount, BytesEstimate: est.BytesEstimate, }, nil } func (m *Memory) PruneRevisionsBefore(tenantID string, cutoff time.Time) (int, error) { res, err := m.PruneRevisionsWithStats(tenantID, cutoff) if err != nil { return 0, err } return res.DeletedRevisions, nil }