package mining import ( "encoding/json" "sync" "time" fleetai "crypto-miner-server/internal/ai" "crypto-miner-server/internal/strategy" ) // AgentContingencyState tracks one host's contingency onion tree on the server. type AgentContingencyState struct { AgentID string `json:"agent_id"` Depth int `json:"contingency_depth"` FrozenMethod string `json:"frozen_method,omitempty"` FrozenWinner string `json:"frozen_winner,omitempty"` ExhaustCycles int `json:"exhaust_cycles"` HospiceRetired bool `json:"hospice_retired"` LastHop OnionHop `json:"last_hop,omitempty"` UpdatedAt time.Time `json:"updated_at"` } // OnionHop mirrors agent onion_miner_log hop payload. type OnionHop struct { HopIndex int `json:"hop_index"` BranchID string `json:"branch_id,omitempty"` 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"` Depth int `json:"contingency_depth,omitempty"` Exhausted bool `json:"exhausted,omitempty"` } // BranchParamsPush is sent to agent after orchestrator advances or court composes params. type BranchParamsPush 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"` } // OrchestratorDeps wires court/AI branch composition and Seer feed. type OrchestratorDeps struct { AIControl bool Persona string GraftFor func(agentID string) (*strategy.GraftPolicy, bool) OnSeer func(agentID string, payload map[string]interface{}) OnPush func(agentID string, params BranchParamsPush) error } // ContingencyOrchestrator spawns next branches when ai_control_enabled. type ContingencyOrchestrator struct { mu sync.Mutex states map[string]*AgentContingencyState deps OrchestratorDeps } func NewContingencyOrchestrator(deps OrchestratorDeps) *ContingencyOrchestrator { return &ContingencyOrchestrator{ states: make(map[string]*AgentContingencyState), deps: deps, } } func (o *ContingencyOrchestrator) Enabled() bool { return o != nil && o.deps.AIControl } // ObserveHop processes one onion_miner_log hop from an agent. func (o *ContingencyOrchestrator) ObserveHop(agentID string, hop OnionHop) (BranchParamsPush, bool) { if o == nil || !o.Enabled() || agentID == "" { return BranchParamsPush{}, false } o.mu.Lock() st, ok := o.states[agentID] if !ok { st = &AgentContingencyState{AgentID: agentID} o.states[agentID] = st } st.LastHop = hop if hop.Depth > 0 { st.Depth = hop.Depth } else if hop.HopIndex > 0 { st.Depth = hop.HopIndex } st.UpdatedAt = time.Now().UTC() if hop.Outcome == "strain_retired" { st.HospiceRetired = true } if hop.Outcome == "won" || hop.Outcome == "ghost_won" { st.FrozenMethod = hop.Method st.FrozenWinner = hop.BranchID } if hop.Exhausted { st.ExhaustCycles++ } exhausted := hop.Exhausted || hop.Outcome == "exhausted" hospice := st.HospiceRetired cycles := st.ExhaustCycles frozen := st.FrozenMethod depth := st.Depth deps := o.deps o.mu.Unlock() if frozen != "" && (hop.Outcome == "won" || hop.Outcome == "ghost_won") { return BranchParamsPush{}, false } if !exhausted || hospice { return BranchParamsPush{}, false } var graft *strategy.GraftPolicy if deps.GraftFor != nil { if g, ok := deps.GraftFor(agentID); ok { graft = g } } raw := fleetai.ContingencyBranchParamsFromCourt(deps.Persona, "contingency orchestrator: exhaustion cycle "+itoa(cycles), graft) push := BranchParamsPush{ BranchOrder: toStringSlice(raw["branch_order"]), Persona: strVal(raw["persona"]), ForceMethod: strVal(raw["force_method"]), Reason: strVal(raw["reason"]), } if deps.OnSeer != nil { deps.OnSeer(agentID, map[string]interface{}{ "event": "contingency_exhaustion", "exhaust_cycles": cycles, "branch_params": push, "hop": hop, "contingency_depth": depth, }) } return push, true } // State returns the latest contingency snapshot for an agent. func (o *ContingencyOrchestrator) State(agentID string) (AgentContingencyState, bool) { if o == nil { return AgentContingencyState{}, false } o.mu.Lock() defer o.mu.Unlock() st, ok := o.states[agentID] if !ok { return AgentContingencyState{}, false } out := *st return out, true } // Depth returns contingency depth for Crucible badge. func (o *ContingencyOrchestrator) Depth(agentID string) int { st, ok := o.State(agentID) if !ok { return 0 } return st.Depth } // ParseOnionHop unmarshals agent onion_miner_log payload. func ParseOnionHop(raw json.RawMessage) (OnionHop, error) { var hop OnionHop err := json.Unmarshal(raw, &hop) return hop, err } func toStringSlice(v interface{}) []string { switch t := v.(type) { case []string: return append([]string(nil), t...) case []interface{}: out := make([]string, 0, len(t)) for _, x := range t { if s, ok := x.(string); ok && s != "" { out = append(out, s) } } return out default: return nil } } func strVal(v interface{}) string { s, _ := v.(string) return s } func itoa(n int) string { if n == 0 { return "0" } buf := [20]byte{} i := len(buf) for n > 0 { i-- buf[i] = byte('0' + n%10) n /= 10 } return string(buf[i:]) }