返回 DeepSeek-Reasonix
freshness.go
根目录 / internal / memory / freshness.go
1 // Freshness classification for saved facts: how a fact ages (volatility or
2 // the legacy type default), when it hard-expires, and what renews its clock.
3 package memory
4
5 import (
6 "strings"
7 "time"
8 )
9
10 const (
11 FreshnessFresh = "fresh"
12 FreshnessCurrent = "current"
13 FreshnessStale = "stale"
14 FreshnessExpired = "expired" // past an explicit expires_at; excluded from automatic recall
15 )
16
17 // Volatility is how fast a fact ages, orthogonal to Type: a project fact may
18 // be a release branch that dies in days or a README location that holds for
19 // years. Unset falls back to the legacy type-based windows.
20 type Volatility string
21
22 const (
23 VolatilityEvergreen Volatility = "evergreen" // never ages
24 VolatilityStable Volatility = "stable" // 90 days fresh / 365 current
25 VolatilityVolatile Volatility = "volatile" // 7 days fresh / 30 current
26 )
27
28 // NormalizeVolatility validates a persisted or requested volatility. Empty and
29 // unknown values return "" (unset) so the type-based default applies.
30 func NormalizeVolatility(s string) Volatility {
31 switch Volatility(strings.ToLower(strings.TrimSpace(s))) {
32 case VolatilityEvergreen:
33 return VolatilityEvergreen
34 case VolatilityStable:
35 return VolatilityStable
36 case VolatilityVolatile:
37 return VolatilityVolatile
38 }
39 return ""
40 }
41
42 // freshnessWindows resolves the (fresh, current) aging windows: an explicit
43 // volatility is self-describing and wins; unset falls back to the type
44 // defaults that predate the field.
45 func freshnessWindows(m Memory) (fresh, current time.Duration, evergreen bool) {
46 switch NormalizeVolatility(string(m.Volatility)) {
47 case VolatilityEvergreen:
48 return 0, 0, true
49 case VolatilityStable:
50 return 90 * 24 * time.Hour, 365 * 24 * time.Hour, false
51 case VolatilityVolatile:
52 return 7 * 24 * time.Hour, 30 * 24 * time.Hour, false
53 }
54 switch NormalizeType(string(m.Type)) {
55 case TypeReference:
56 return 14 * 24 * time.Hour, 45 * 24 * time.Hour, false
57 case TypeUser, TypeFeedback:
58 return 90 * 24 * time.Hour, 365 * 24 * time.Hour, false
59 default:
60 return 30 * 24 * time.Hour, 180 * 24 * time.Hour, false
61 }
62 }
63
64 // FreshnessFor exposes the freshness classification shared by automatic
65 // recall, /memory, and diagnostic surfaces. An explicit expiry is a hard
66 // boundary; otherwise the clock runs from the last content change or the last
67 // explicit verification, whichever is newer.
68 func FreshnessFor(fact Memory, now time.Time) string {
69 return memoryFreshness(fact, now)
70 }
71
72 func memoryFreshness(m Memory, now time.Time) string {
73 if !m.ExpiresAt.IsZero() && now.After(m.ExpiresAt) {
74 return FreshnessExpired
75 }
76 updated := m.UpdatedAt
77 if updated.IsZero() {
78 updated = m.CreatedAt
79 }
80 if m.LastVerifiedAt.After(updated) {
81 updated = m.LastVerifiedAt
82 }
83 if updated.IsZero() || updated.After(now) {
84 return FreshnessCurrent
85 }
86 fresh, current, evergreen := freshnessWindows(m)
87 if evergreen {
88 return FreshnessFresh
89 }
90 age := now.Sub(updated)
91 if age <= fresh {
92 return FreshnessFresh
93 }
94 if age <= current {
95 return FreshnessCurrent
96 }
97 return FreshnessStale
98 }
99
99 lines GO