From dd224b2ae4af612f31982342dcdd6567cae9c0db Mon Sep 17 00:00:00 2001 From: rabbitblood Date: Mon, 20 Apr 2026 11:08:44 -0700 Subject: [PATCH] fix: add ?purge=true hard-delete to DELETE /workspaces/:id (#1087) Soft-delete (status='removed') leaves orphan DB rows and FK data forever. When ?purge=true is passed, after container cleanup the handler cascade- deletes all leaf FK tables and hard-removes the workspace row. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../internal/handlers/workspace.go | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/workspace-server/internal/handlers/workspace.go b/workspace-server/internal/handlers/workspace.go index a11f0a9a..2265f2ff 100644 --- a/workspace-server/internal/handlers/workspace.go +++ b/workspace-server/internal/handlers/workspace.go @@ -841,6 +841,36 @@ func (h *WorkspaceHandler) Delete(c *gin.Context) { "cascade_deleted": len(descendantIDs), }) + // Hard purge: cascade delete all FK data and remove the DB row entirely (#1087) + if c.Query("purge") == "true" { + purgeIDs := pq.Array(allIDs) + // Order matters: delete from leaf tables first, then workspace row + for _, table := range []string{ + "agent_memories", "activity_logs", "workspace_secrets", + "workspace_channels", "workspace_config", "workspace_memory", + "workspace_token_usage", "approval_requests", "audit_events", + "workflow_checkpoints", "workspace_artifacts", "agents", + "workspace_auth_tokens", "workspace_schedules", "canvas_layouts", + } { + if _, err := db.DB.ExecContext(ctx, + fmt.Sprintf("DELETE FROM %s WHERE workspace_id = ANY($1::uuid[])", table), + purgeIDs); err != nil { + log.Printf("Purge %s error for %v: %v", table, allIDs, err) + } + } + // Null out parent_id / forwarded_to references + db.DB.ExecContext(ctx, "UPDATE workspaces SET parent_id = NULL WHERE parent_id = ANY($1::uuid[])", purgeIDs) + db.DB.ExecContext(ctx, "UPDATE workspaces SET forwarded_to = NULL WHERE forwarded_to = ANY($1::uuid[])", purgeIDs) + // Hard delete the workspace row + if _, err := db.DB.ExecContext(ctx, "DELETE FROM workspaces WHERE id = ANY($1::uuid[])", purgeIDs); err != nil { + log.Printf("Purge workspace row error for %v: %v", allIDs, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "purge failed: " + err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"status": "purged", "cascade_deleted": len(descendantIDs)}) + return + } + c.JSON(http.StatusOK, gin.H{"status": "removed", "cascade_deleted": len(descendantIDs)}) }