Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
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.
251 lines
6.7 KiB
Go
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"}
|
|
)
|