From 6209e2f3aeea4fd1f097e5e5c8ed91a4b5b68e03 Mon Sep 17 00:00:00 2001 From: Bohan Jiang <52446949+Bohan-J@users.noreply.github.com> Date: Thu, 9 Apr 2026 19:17:54 +0800 Subject: [PATCH] fix(server): allow deleting runtimes when all bound agents are archived (#589) 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 2cfc44cd31..90b9d84293 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 e605fc1ecb..683d97b8dc 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 1b80eb354d..1e70a53721 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;