fix(agent): refresh live status from AM in the agent LIST endpoint

GET /api/heicode/agents previously returned only the stored status, so a
freshly-deployed agent stayed "Pending"/"启动中" forever — the desktop client
polling the list never saw it go "running", and the web list only updated when
the user opened the detail panel (which polls /agents/{id}/status and was the
only path that refreshed from AM).

Now the list refreshes every non-terminal (non-"stopped") agent's live status
from AM before responding, concurrently under an 8s deadline. Per-user agents
are capped (≤8) so the fan-out is bounded and the list never hangs on a slow
AM. refreshAgentStatus now takes context.Context (callers pass the request ctx).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-08 15:47:10 +08:00
co-authored by Claude Opus 4.8
parent cbbdd66c2e
commit 6dbfc98552
+36 -4
View File
@@ -1,9 +1,11 @@
package controller
import (
"context"
"crypto/subtle"
"strconv"
"strings"
"sync"
"time"
"github.com/gin-gonic/gin"
@@ -256,6 +258,14 @@ func HeicodeListAgents(c *gin.Context) {
agentError(c, "DEPLOYMENT_CONFLICT", "failed to list agents")
return
}
// Refresh live status from AM for every non-terminal agent BEFORE returning,
// so the list (and the desktop client that polls it) reflects reality without
// needing the detail panel to be opened. Previously only the detail/status
// endpoints refreshed, so a freshly-deployed agent stayed "Pending" in the
// list forever. Bounded: per-user agent count is capped (≤8) and the refreshes
// run concurrently under a short deadline, so the list never hangs on a slow AM.
refreshAgentStatusBatch(c.Request.Context(), rows)
items := make([]gin.H, 0, len(rows))
for _, row := range rows {
items = append(items, templateAgentResponse(row))
@@ -263,9 +273,31 @@ func HeicodeListAgents(c *gin.Context) {
common.ApiSuccess(c, gin.H{"items": items, "total": len(items)})
}
// refreshAgentStatusBatch concurrently refreshes the live status of every
// non-terminal agent in rows (best-effort) under a single bounded deadline.
// Each row is updated in place + persisted by refreshAgentStatus.
func refreshAgentStatusBatch(ctx context.Context, rows []model.AgentDeployment) {
ctx, cancel := context.WithTimeout(ctx, 8*time.Second)
defer cancel()
var wg sync.WaitGroup
for i := range rows {
row := &rows[i]
if strings.TrimSpace(row.RuntimeDeploymentID) == "" ||
strings.EqualFold(strings.TrimSpace(row.Status), "stopped") {
continue
}
wg.Add(1)
go func(r *model.AgentDeployment) {
defer wg.Done()
refreshAgentStatus(ctx, r)
}(row)
}
wg.Wait()
}
// refreshAgentStatus best-effort pulls the live status from AM and persists it.
// On any AM error it keeps the last-known status (never blocks the read).
func refreshAgentStatus(c *gin.Context, row *model.AgentDeployment) {
func refreshAgentStatus(ctx context.Context, row *model.AgentDeployment) {
if strings.TrimSpace(row.RuntimeDeploymentID) == "" {
return
}
@@ -275,7 +307,7 @@ func refreshAgentStatus(c *gin.Context, row *model.AgentDeployment) {
if strings.EqualFold(strings.TrimSpace(row.Status), "stopped") {
return
}
status, err := amGetAgentStatus(c.Request.Context(), row.RuntimeDeploymentID)
status, err := amGetAgentStatus(ctx, row.RuntimeDeploymentID)
if err != nil || strings.TrimSpace(status) == "" || status == row.Status {
return
}
@@ -299,7 +331,7 @@ func HeicodeGetAgent(c *gin.Context) {
if !ok {
return
}
refreshAgentStatus(c, &row)
refreshAgentStatus(c.Request.Context(), &row)
common.ApiSuccess(c, templateAgentResponse(row))
}
@@ -310,7 +342,7 @@ func HeicodeGetAgentStatus(c *gin.Context) {
if !ok {
return
}
refreshAgentStatus(c, &row)
refreshAgentStatus(c.Request.Context(), &row)
common.ApiSuccess(c, gin.H{
"agent_id": row.DeploymentID,
"status": row.Status,