From 40f594e7b3552786c3f424d826db4e8a9d3ac36f Mon Sep 17 00:00:00 2001 From: Jiang Bohan Date: Thu, 9 Apr 2026 19:15:50 +0800 Subject: [PATCH] fix(server): allow deleting runtimes when all bound agents are archived Previously, runtimes could never be deleted once an agent was created because agents can only be archived (not deleted) and the count check included archived agents. Now the check only counts active agents, and archived agents are cleaned up before runtime deletion. --- server/internal/handler/runtime.go | 14 ++++++++++---- server/pkg/db/generated/runtime.sql.go | 17 +++++++++++++---- server/pkg/db/queries/runtime.sql | 7 +++++-- 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/server/internal/handler/runtime.go b/server/internal/handler/runtime.go index 2cfc44cd3..90b9d8429 100644 --- a/server/internal/handler/runtime.go +++ b/server/internal/handler/runtime.go @@ -343,14 +343,20 @@ func (h *Handler) DeleteAgentRuntime(w http.ResponseWriter, r *http.Request) { return } - // Check if any agents are bound to this runtime (ON DELETE RESTRICT). - agentCount, err := h.Queries.CountAgentsByRuntime(r.Context(), rt.ID) + // Check if any active (non-archived) agents are bound to this runtime. + activeCount, err := h.Queries.CountActiveAgentsByRuntime(r.Context(), rt.ID) if err != nil { writeError(w, http.StatusInternalServerError, "failed to check runtime dependencies") return } - if agentCount > 0 { - writeError(w, http.StatusConflict, "cannot delete runtime: it has agents bound to it. Reassign or remove the agents first.") + if activeCount > 0 { + writeError(w, http.StatusConflict, "cannot delete runtime: it has active agents bound to it. Archive or reassign the agents first.") + return + } + + // Remove archived agents so the FK constraint (ON DELETE RESTRICT) won't block deletion. + if err := h.Queries.DeleteArchivedAgentsByRuntime(r.Context(), rt.ID); err != nil { + writeError(w, http.StatusInternalServerError, "failed to clean up archived agents") return } diff --git a/server/pkg/db/generated/runtime.sql.go b/server/pkg/db/generated/runtime.sql.go index e605fc1ec..683d97b8d 100644 --- a/server/pkg/db/generated/runtime.sql.go +++ b/server/pkg/db/generated/runtime.sql.go @@ -11,12 +11,12 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) -const countAgentsByRuntime = `-- name: CountAgentsByRuntime :one -SELECT count(*) FROM agent WHERE runtime_id = $1 +const countActiveAgentsByRuntime = `-- name: CountActiveAgentsByRuntime :one +SELECT count(*) FROM agent WHERE runtime_id = $1 AND archived_at IS NULL ` -func (q *Queries) CountAgentsByRuntime(ctx context.Context, runtimeID pgtype.UUID) (int64, error) { - row := q.db.QueryRow(ctx, countAgentsByRuntime, runtimeID) +func (q *Queries) CountActiveAgentsByRuntime(ctx context.Context, runtimeID pgtype.UUID) (int64, error) { + row := q.db.QueryRow(ctx, countActiveAgentsByRuntime, runtimeID) var count int64 err := row.Scan(&count) return count, err @@ -31,6 +31,15 @@ func (q *Queries) DeleteAgentRuntime(ctx context.Context, id pgtype.UUID) error return err } +const deleteArchivedAgentsByRuntime = `-- name: DeleteArchivedAgentsByRuntime :exec +DELETE FROM agent WHERE runtime_id = $1 AND archived_at IS NOT NULL +` + +func (q *Queries) DeleteArchivedAgentsByRuntime(ctx context.Context, runtimeID pgtype.UUID) error { + _, err := q.db.Exec(ctx, deleteArchivedAgentsByRuntime, runtimeID) + return err +} + const failTasksForOfflineRuntimes = `-- name: FailTasksForOfflineRuntimes :many UPDATE agent_task_queue SET status = 'failed', completed_at = now(), error = 'runtime went offline' diff --git a/server/pkg/db/queries/runtime.sql b/server/pkg/db/queries/runtime.sql index 1b80eb354..1e70a5372 100644 --- a/server/pkg/db/queries/runtime.sql +++ b/server/pkg/db/queries/runtime.sql @@ -73,5 +73,8 @@ ORDER BY created_at ASC; -- name: DeleteAgentRuntime :exec DELETE FROM agent_runtime WHERE id = $1; --- name: CountAgentsByRuntime :one -SELECT count(*) FROM agent WHERE runtime_id = $1; +-- name: CountActiveAgentsByRuntime :one +SELECT count(*) FROM agent WHERE runtime_id = $1 AND archived_at IS NULL; + +-- name: DeleteArchivedAgentsByRuntime :exec +DELETE FROM agent WHERE runtime_id = $1 AND archived_at IS NOT NULL;