fix(agent): self-review fixes for archive feature

- Fix: workspace store now fetches agents with include_archived=true
  so archived agents are actually visible in the frontend (the archived
  UI was dead code before — ListAgents excludes archived by default)
- Fix: add error logging for CancelAgentTasksByAgent in ArchiveAgent
- Fix: add idempotency guards — return 409 Conflict when archiving
  an already-archived agent or restoring a non-archived agent
- Fix: revert unnecessary extra GetAgent query in ReconcileAgentStatus
  (archived agents won't have running tasks after CancelAgentTasksByAgent)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
yushen
2026-04-02 17:23:54 +08:00
parent 978765fe35
commit 2c7abefb6d
4 changed files with 15 additions and 9 deletions

View File

@@ -82,7 +82,7 @@ export const useWorkspaceStore = create<WorkspaceStore>((set, get) => ({
toast.error("Failed to load members");
return [] as MemberWithUser[];
}),
api.listAgents({ workspace_id: nextWorkspace.id }).catch((e) => {
api.listAgents({ workspace_id: nextWorkspace.id, include_archived: true }).catch((e) => {
logger.error("failed to load agents", e);
toast.error("Failed to load agents");
return [] as Agent[];
@@ -154,7 +154,7 @@ export const useWorkspaceStore = create<WorkspaceStore>((set, get) => ({
const { workspace } = get();
if (!workspace) return;
try {
const agents = await api.listAgents({ workspace_id: workspace.id });
const agents = await api.listAgents({ workspace_id: workspace.id, include_archived: true });
set({ agents });
} catch (e) {
logger.error("failed to refresh agents", e);

View File

@@ -291,10 +291,11 @@ export class ApiClient {
}
// Agents
async listAgents(params?: { workspace_id?: string }): Promise<Agent[]> {
async listAgents(params?: { workspace_id?: string; include_archived?: boolean }): Promise<Agent[]> {
const search = new URLSearchParams();
const wsId = params?.workspace_id ?? this.workspaceId;
if (wsId) search.set("workspace_id", wsId);
if (params?.include_archived) search.set("include_archived", "true");
return this.fetch(`/api/agents?${search}`);
}

View File

@@ -434,6 +434,10 @@ func (h *Handler) ArchiveAgent(w http.ResponseWriter, r *http.Request) {
if !h.canManageAgent(w, r, agent) {
return
}
if agent.ArchivedAt.Valid {
writeError(w, http.StatusConflict, "agent is already archived")
return
}
userID := requestUserID(r)
archived, err := h.Queries.ArchiveAgent(r.Context(), db.ArchiveAgentParams{
@@ -447,7 +451,9 @@ func (h *Handler) ArchiveAgent(w http.ResponseWriter, r *http.Request) {
}
// Cancel all pending/active tasks for this agent.
h.Queries.CancelAgentTasksByAgent(r.Context(), parseUUID(id))
if err := h.Queries.CancelAgentTasksByAgent(r.Context(), parseUUID(id)); err != nil {
slog.Warn("cancel agent tasks on archive failed", append(logger.RequestAttrs(r), "error", err, "agent_id", id)...)
}
wsID := uuidToString(archived.WorkspaceID)
slog.Info("agent archived", append(logger.RequestAttrs(r), "agent_id", id, "workspace_id", wsID)...)
@@ -466,6 +472,10 @@ func (h *Handler) RestoreAgent(w http.ResponseWriter, r *http.Request) {
if !h.canManageAgent(w, r, agent) {
return
}
if !agent.ArchivedAt.Valid {
writeError(w, http.StatusConflict, "agent is not archived")
return
}
restored, err := h.Queries.RestoreAgent(r.Context(), parseUUID(id))
if err != nil {

View File

@@ -314,12 +314,7 @@ func (s *TaskService) ReportProgress(ctx context.Context, taskID string, workspa
}
// ReconcileAgentStatus checks running task count and sets agent status accordingly.
// Skips archived agents — their status is irrelevant.
func (s *TaskService) ReconcileAgentStatus(ctx context.Context, agentID pgtype.UUID) {
agent, err := s.Queries.GetAgent(ctx, agentID)
if err != nil || agent.ArchivedAt.Valid {
return
}
running, err := s.Queries.CountRunningTasks(ctx, agentID)
if err != nil {
return