Author SHA1 Message Date
chenchenandClaude Opus 4.8 6dbfc98552 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>
2026-06-08 15:47:10 +08:00
+36 -4
View File
@@ -1,9 +1,11 @@
package controller package controller
import ( import (
"context"
"crypto/subtle" "crypto/subtle"
"strconv" "strconv"
"strings" "strings"
"sync"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -256,6 +258,14 @@ func HeicodeListAgents(c *gin.Context) {
agentError(c, "DEPLOYMENT_CONFLICT", "failed to list agents") agentError(c, "DEPLOYMENT_CONFLICT", "failed to list agents")
return 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)) items := make([]gin.H, 0, len(rows))
for _, row := range rows { for _, row := range rows {
items = append(items, templateAgentResponse(row)) items = append(items, templateAgentResponse(row))
@@ -263,9 +273,31 @@ func HeicodeListAgents(c *gin.Context) {
common.ApiSuccess(c, gin.H{"items": items, "total": len(items)}) 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. // 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). // 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) == "" { if strings.TrimSpace(row.RuntimeDeploymentID) == "" {
return return
} }
@@ -275,7 +307,7 @@ func refreshAgentStatus(c *gin.Context, row *model.AgentDeployment) {
if strings.EqualFold(strings.TrimSpace(row.Status), "stopped") { if strings.EqualFold(strings.TrimSpace(row.Status), "stopped") {
return return
} }
status, err := amGetAgentStatus(c.Request.Context(), row.RuntimeDeploymentID) status, err := amGetAgentStatus(ctx, row.RuntimeDeploymentID)
if err != nil || strings.TrimSpace(status) == "" || status == row.Status { if err != nil || strings.TrimSpace(status) == "" || status == row.Status {
return return
} }
@@ -299,7 +331,7 @@ func HeicodeGetAgent(c *gin.Context) {
if !ok { if !ok {
return return
} }
refreshAgentStatus(c, &row) refreshAgentStatus(c.Request.Context(), &row)
common.ApiSuccess(c, templateAgentResponse(row)) common.ApiSuccess(c, templateAgentResponse(row))
} }
@@ -310,7 +342,7 @@ func HeicodeGetAgentStatus(c *gin.Context) {
if !ok { if !ok {
return return
} }
refreshAgentStatus(c, &row) refreshAgentStatus(c.Request.Context(), &row)
common.ApiSuccess(c, gin.H{ common.ApiSuccess(c, gin.H{
"agent_id": row.DeploymentID, "agent_id": row.DeploymentID,
"status": row.Status, "status": row.Status,