Add Strain Hospice for graceful low-win strain retirement.
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled

Archive failed epidemiology strains to museum hospice with SQLite persistence, operator/AI/court triggers, breeding and graft guards, topology museum nodes, and Seer plus oath ledger accountability.
This commit is contained in:
AetherForge
2026-06-07 09:26:39 -07:00
parent 894b7a50ae
commit d605ef4adb
18 changed files with 701 additions and 18 deletions

View File

@@ -44,9 +44,10 @@ type LaneWinnerInput struct {
// BreedingRegistry tracks lane-specific winners and crossbred siblings per fingerprint.
type BreedingRegistry struct {
mu sync.RWMutex
lanes map[string]map[string]LaneWinner
bred map[string]BredPhenotype
mu sync.RWMutex
lanes map[string]map[string]LaneWinner
bred map[string]BredPhenotype
hospice map[string]bool
}
func NewBreedingRegistry() *BreedingRegistry {
@@ -56,6 +57,25 @@ func NewBreedingRegistry() *BreedingRegistry {
}
}
// SetHospiceStrains updates the retired-strain set used to skip breeding parents.
func (r *BreedingRegistry) SetHospiceStrains(strains map[string]bool) {
if r == nil {
return
}
r.mu.Lock()
defer r.mu.Unlock()
if len(strains) == 0 {
r.hospice = nil
return
}
r.hospice = make(map[string]bool, len(strains))
for k, v := range strains {
if v {
r.hospice[NormalizeStrainID(k)] = true
}
}
}
// RecordLaneWinner stores a lane winner and crossbreeds when two distinct lanes exist.
func (r *BreedingRegistry) RecordLaneWinner(in LaneWinnerInput) (BredPhenotype, bool) {
fp := strings.TrimSpace(in.Fingerprint)
@@ -63,6 +83,9 @@ func (r *BreedingRegistry) RecordLaneWinner(in LaneWinnerInput) (BredPhenotype,
if r == nil || fp == "" || lane == "" || len(in.TierOrder) == 0 {
return BredPhenotype{}, false
}
if LaneInHospice(lane, r.hospiceSnapshot()) {
return BredPhenotype{}, false
}
winner := LaneWinner{
SpreadLane: lane,
TierOrder: append([]string(nil), in.TierOrder...),
@@ -81,7 +104,7 @@ func (r *BreedingRegistry) RecordLaneWinner(in LaneWinnerInput) (BredPhenotype,
if len(r.lanes[fp]) < 2 {
return BredPhenotype{}, false
}
bred := breedLaneWinners(fp, r.lanes[fp])
bred := breedLaneWinners(fp, r.lanes[fp], r.hospice)
if len(bred.TierOrder) == 0 {
return BredPhenotype{}, false
}
@@ -115,9 +138,28 @@ func (r *BreedingRegistry) LaneCount(fingerprint string) int {
return len(r.lanes[fp])
}
func breedLaneWinners(fingerprint string, lanes map[string]LaneWinner) BredPhenotype {
func (r *BreedingRegistry) hospiceSnapshot() map[string]bool {
if r == nil {
return nil
}
r.mu.RLock()
defer r.mu.RUnlock()
if len(r.hospice) == 0 {
return nil
}
out := make(map[string]bool, len(r.hospice))
for k, v := range r.hospice {
out[k] = v
}
return out
}
func breedLaneWinners(fingerprint string, lanes map[string]LaneWinner, hospice map[string]bool) BredPhenotype {
parents := make([]LaneWinner, 0, len(lanes))
for _, w := range lanes {
if LaneInHospice(w.SpreadLane, hospice) {
continue
}
parents = append(parents, w)
}
sort.Slice(parents, func(i, j int) bool {

View File

@@ -0,0 +1,94 @@
package strategy
import (
"strings"
)
const (
// DefaultHospiceWinRateThreshold retires strains below 15% win rate.
DefaultHospiceWinRateThreshold = 0.15
// DefaultHospiceMinAttempts requires enough spread outcomes before auto-retire.
DefaultHospiceMinAttempts = 5
)
// StrainRetiredBy identifies who sent a strain to hospice.
type StrainRetiredBy string
const (
StrainRetiredByOperator StrainRetiredBy = "operator"
StrainRetiredByAI StrainRetiredBy = "ai"
StrainRetiredByCourt StrainRetiredBy = "court"
)
// StrainSpreadStats aggregates epidemiology wins/losses for one spread strain.
type StrainSpreadStats struct {
StrainID string
Wins int
Losses int
}
// NormalizeStrainID lowercases #RRGGBB strain identifiers.
func NormalizeStrainID(strain string) string {
s := strings.TrimSpace(strings.ToLower(strain))
if s == "" {
return ""
}
if !strings.HasPrefix(s, "#") && len(s) == 6 {
s = "#" + s
}
return s
}
// StrainFromSpreadLane maps a join/spread lane to its stable strain color id.
func StrainFromSpreadLane(lane string) string {
if s := NormalizeStrainID(SpreadStrainFromJoinLane(lane)); s != "" {
return s
}
return NormalizeStrainID(lane)
}
// LaneInHospice reports whether a spread/join lane's strain is retired.
func LaneInHospice(lane string, hospice map[string]bool) bool {
if len(hospice) == 0 {
return false
}
return hospice[StrainFromSpreadLane(lane)]
}
// StrainInHospice reports whether a strain id is in the hospice set.
func StrainInHospice(strain string, hospice map[string]bool) bool {
if len(hospice) == 0 {
return false
}
return hospice[NormalizeStrainID(strain)]
}
// StrainWinRate returns wins / (wins + losses); 1.0 when no attempts recorded.
func StrainWinRate(wins, losses int) float64 {
total := wins + losses
if total == 0 {
return 1.0
}
return float64(wins) / float64(total)
}
// ShouldAutoRetireStrain is true when AI hospice policy applies to low performers.
func ShouldAutoRetireStrain(wins, losses int, threshold float64, minAttempts int) bool {
total := wins + losses
if total < minAttempts || minAttempts <= 0 {
return false
}
if threshold <= 0 {
threshold = DefaultHospiceWinRateThreshold
}
return StrainWinRate(wins, losses) < threshold
}
// PersonaPrimaryLane returns the first spread lane for a persona preset.
func PersonaPrimaryLane(persona string) string {
order := PersonaSpreadTierOrder(persona)
if len(order) == 0 {
return ""
}
return strings.TrimSpace(order[0])
}

View File

@@ -0,0 +1,26 @@
package strategy
import "testing"
func TestShouldAutoRetireStrain(t *testing.T) {
if !ShouldAutoRetireStrain(1, 9, 0.15, 5) {
t.Fatal("10% win rate with 10 attempts should retire at 15% threshold")
}
if ShouldAutoRetireStrain(1, 3, 0.15, 5) {
t.Fatal("min attempts not met")
}
if StrainWinRate(0, 0) != 1.0 {
t.Fatalf("empty stats should be 1.0, got %v", StrainWinRate(0, 0))
}
}
func TestLaneInHospice(t *testing.T) {
strain := StrainFromSpreadLane("winrm")
hospice := map[string]bool{strain: true}
if !LaneInHospice("winrm", hospice) {
t.Fatalf("winrm strain %s should be in hospice", strain)
}
if LaneInHospice("docker", hospice) {
t.Fatal("docker lane should not match winrm hospice entry")
}
}