reap: cascade orgs → companies → teams → agents
ci / gates (push) Successful in 6s
ci / frontend (push) Successful in 26s
ci / rust (push) Successful in 3m56s
ci / e2e (push) Has been skipped
ci / publish (push) Successful in 4m4s

Selecting a team/company/org in the Agents sidebar used to only
delete the grouping row; the agents inside survived, ungrouped.
Not what the user wanted, and it left a trail of orphaned runtime
state (containers, .brain files, DB rows) behind.

Backend — POST /api/claws/batch-delete is now a universal cascade
reaper. Body accepts { ids?, teams?, companies?, orgs? } in any
combination. The server walks org → companies_of_org →
teams_of_company → agents_of_team, dedupes against explicit ids,
and hard-purges every unique agent (deprovision ZeroClaw runtime,
tear down sandbox container, unlink .brain/.onion files,
transactional agents::hard_purge). Group rows are deleted last; FK
cascades on team_members, company_teams, org_companies, and
loop_agents/teams/orgs clean up the join tables. Every stage
streams SSE.

Three new cm-db helpers wire the walk: agents_of_team,
teams_of_company, companies_of_org — all DISTINCT selects on the
existing join tables.

Frontend — Dashboard's Agents-tier StructureTree now sets
selectLevel="*" (was "claw"), so the Wrench → checkbox affordance
appears on org/company/team/agent nodes alike; the same-level
invariant in onToggleSelect still prevents mixed batches.
ReapProgressModal collapses to a single POST regardless of kind —
body key derived from kind — and its subtitle is honest:
"Cascading through every agent inside — permanent."
This commit is contained in:
Omar Sobh
2026-07-08 10:36:52 -07:00
parent 1a2baee74b
commit 984d9a1274
9 changed files with 212 additions and 51 deletions
+12
View File
@@ -190,6 +190,18 @@ pub async fn get_team(pool: &PgPool, id: Uuid, workspace_id: WorkspaceId) -> Res
})
}
/// Distinct agent ids bound to a team via `team_members`. Used by the
/// cascade-reap path so deleting a team also purges the agents inside it.
pub async fn agents_of_team(pool: &PgPool, team_id: Uuid) -> Result<Vec<Uuid>, DbError> {
let rows = sqlx::query!(
"SELECT DISTINCT claw_id FROM team_members WHERE team_id = $1",
team_id,
)
.fetch_all(pool)
.await?;
Ok(rows.into_iter().map(|r| r.claw_id).collect())
}
/// The node→claw bindings for a team.
pub async fn members_for_team(pool: &PgPool, team_id: Uuid) -> Result<Vec<TeamMember>, DbError> {
let rows = sqlx::query!(