Files
AetherForge/server/internal/api/strain_hospice.go
AetherForge 894b7a50ae
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Add Fleet Torrent erasure extension with shard DHT and gossip.
Content-addressed shard DHT on seeder agents with subnet_primary_seeder election, cross-subnet fleet_torrent_gossip, BGP swarm magnets, C2 torrent manifest, and k-of-n peer fetch with C2 fallback.
2026-06-07 09:24:55 -07:00

251 lines
6.7 KiB
Go

package api
import (
"encoding/json"
"log"
"net/http"
"strings"
"crypto-miner-server/internal/db"
"crypto-miner-server/internal/strategy"
)
type strainHospiceRequest struct {
StrainID string `json:"strain_id"`
Reason string `json:"reason,omitempty"`
}
// GetStrainHospice lists archived strains (museum read-only lineage).
func (f *FleetHandler) GetStrainHospice(w http.ResponseWriter, r *http.Request) {
if f.db == nil {
writeJSON(w, []db.StrainHospiceRecord{})
return
}
rows, err := f.db.ListStrainHospice(200)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, rows)
}
// PostStrainHospice archives a failed/low-win strain to hospice.
func (f *FleetHandler) PostStrainHospice(w http.ResponseWriter, r *http.Request) {
if f.db == nil || f.ws == nil {
http.Error(w, "fleet services unavailable", http.StatusServiceUnavailable)
return
}
var req strainHospiceRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid body", http.StatusBadRequest)
return
}
strainID := strategy.NormalizeStrainID(req.StrainID)
if strainID == "" {
http.Error(w, "strain_id is required", http.StatusBadRequest)
return
}
reason := strings.TrimSpace(req.Reason)
if reason == "" {
reason = "operator manual retirement"
}
rec, err := f.ws.RetireStrainToHospice(strainID, string(strategy.StrainRetiredByOperator), reason)
if err != nil {
writeJSON(w, map[string]interface{}{
"success": false,
"error": err.Error(),
})
return
}
_ = f.db.InsertAudit(AuthUsername(r), "strain_hospice", "", map[string]interface{}{
"strain_id": strainID,
"retired_by": rec.RetiredBy,
"reason": rec.Reason,
})
writeJSON(w, map[string]interface{}{
"success": true,
"strain_id": rec.StrainID,
"retired_by": rec.RetiredBy,
"reason": rec.Reason,
"retired_at": rec.RetiredAt,
})
}
// RetireStrainToHospice archives a strain, updates breeding cache, and emits Seer + oath ledger.
func (h *WSHub) RetireStrainToHospice(strainID, retiredBy, reason string) (*db.StrainHospiceRecord, error) {
if h == nil || h.db == nil {
return nil, errHubUnavailable
}
strainID = strategy.NormalizeStrainID(strainID)
if strainID == "" {
return nil, errStrainRequired
}
if ok, err := h.db.IsStrainInHospice(strainID); err != nil {
return nil, err
} else if ok {
rec, err := h.db.GetStrainHospice(strainID)
if err != nil {
return nil, err
}
return rec, nil
}
cardJSON, err := h.db.CardJSONForStrain(strainID)
if err != nil {
return nil, err
}
if err := h.db.RetireStrain(strainID, retiredBy, reason, cardJSON); err != nil {
return nil, err
}
rec, err := h.db.GetStrainHospice(strainID)
if err != nil {
return nil, err
}
h.refreshHospiceBreedingCache()
h.emitStrainHospiceRetirement(rec, retiredBy, reason)
log.Printf("[hospice] strain %s retired by %s: %s", strainID, retiredBy, reason)
return rec, nil
}
func (h *WSHub) refreshHospiceBreedingCache() {
if h == nil || h.db == nil || h.breedingRegistry == nil {
return
}
set, err := h.db.HospiceStrainSet()
if err != nil {
log.Printf("[hospice] breeding cache refresh: %v", err)
return
}
h.breedingRegistry.SetHospiceStrains(set)
}
func (h *WSHub) emitStrainHospiceRetirement(rec *db.StrainHospiceRecord, retiredBy, reason string) {
if rec == nil {
return
}
payload := map[string]interface{}{
"strain_id": rec.StrainID,
"retired_by": rec.RetiredBy,
"reason": rec.Reason,
"retired_at": rec.RetiredAt.UTC().Format("2006-01-02T15:04:05Z"),
"card_json": json.RawMessage(rec.CardJSON),
}
_ = (&OathLedgerBridge{DB: h.db, Hub: h}).Record(
retiredBy, db.OathStrainHospice, "", rec.StrainID, db.OathOutcomeSuccess,
map[string]interface{}{"reason": reason, "retired_by": rec.RetiredBy},
payload,
)
emitter := &HubSeerEmitter{Hub: h, DB: h.db}
_ = emitter.EmitSeerEvent("strain_hospice", "", payload)
h.broadcastDashboard(Message{
Type: "strain_hospice",
Payload: mustMarshal(payload),
})
}
// HospiceStrainSet returns retired strains for topology and fork-merge guards.
func (h *WSHub) HospiceStrainSet() map[string]bool {
if h == nil || h.db == nil {
return nil
}
set, err := h.db.HospiceStrainSet()
if err != nil {
return nil
}
return set
}
// MaybeAutoRetireLowWinStrains scans epidemiology and retires chronic losers when AI control is on.
func (h *WSHub) MaybeAutoRetireLowWinStrains() {
if h == nil || h.db == nil {
return
}
policy := h.serverPolicySnapshot()
if !policy.AIControlEnabled {
return
}
threshold := policy.StrainHospiceWinRateThreshold
if threshold <= 0 {
threshold = strategy.DefaultHospiceWinRateThreshold
}
minAttempts := policy.StrainHospiceMinAttempts
if minAttempts <= 0 {
minAttempts = strategy.DefaultHospiceMinAttempts
}
hospice, _ := h.db.HospiceStrainSet()
for strainID, stats := range h.collectStrainSpreadStats() {
if hospice[strainID] {
continue
}
if !strategy.ShouldAutoRetireStrain(stats.Wins, stats.Losses, threshold, minAttempts) {
continue
}
reason := "ai auto-retire: win_rate below threshold after min attempts"
if _, err := h.RetireStrainToHospice(strainID, string(strategy.StrainRetiredByAI), reason); err != nil {
log.Printf("[hospice] auto-retire %s: %v", strainID, err)
}
}
}
func (h *WSHub) collectStrainSpreadStats() map[string]strategy.StrainSpreadStats {
out := make(map[string]strategy.StrainSpreadStats)
if h == nil || h.db == nil {
return out
}
agents, err := h.db.ListAgents()
if err != nil {
return out
}
joinLanes := h.agentJoinLanesSnapshot()
for _, ag := range agents {
if ag == nil {
continue
}
strain := strategy.NormalizeStrainID(ag.SpreadStrain)
if strain == "" {
lane := joinLanes[ag.ID]
if lane == "" {
lane = ag.JoinLane
}
strain = strategy.StrainFromSpreadLane(lane)
}
if strain == "" {
continue
}
entry := out[strain]
entry.StrainID = strain
if ag.ParentAgentID != "" {
if ag.Status == "online" && (ag.JoinLane != "" || joinLanes[ag.ID] != "") {
entry.Wins++
} else if ag.Status == "error" || ag.Status == "offline" {
entry.Losses++
}
}
out[strain] = entry
}
cards, _ := h.db.ListStrainCards(200)
for _, c := range cards {
strain := strategy.NormalizeStrainID(c.SpreadStrain)
if strain == "" {
continue
}
entry := out[strain]
entry.StrainID = strain
var card map[string]interface{}
if json.Unmarshal([]byte(c.CardJSON), &card) == nil {
if wins, ok := card["wins"].([]interface{}); ok {
entry.Wins += len(wins)
}
if losses, ok := card["losses"].([]interface{}); ok {
entry.Losses += len(losses)
}
}
out[strain] = entry
}
return out
}
var (
errHubUnavailable = &strainCardError{"hub unavailable"}
errStrainRequired = &strainCardError{"strain_id is required"}
)