reap: cascade orgs → companies → teams → agents
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:
@@ -194,3 +194,15 @@ pub async fn teams_for_company(
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Distinct team ids bound to a company via `company_teams`. The
|
||||
/// cascade-reap path drills through here to collect agents.
|
||||
pub async fn teams_of_company(pool: &PgPool, company_id: Uuid) -> Result<Vec<Uuid>, DbError> {
|
||||
let rows = sqlx::query!(
|
||||
"SELECT DISTINCT team_id FROM company_teams WHERE company_id = $1",
|
||||
company_id,
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
Ok(rows.into_iter().map(|r| r.team_id).collect())
|
||||
}
|
||||
|
||||
@@ -164,3 +164,15 @@ pub async fn delete_org(pool: &PgPool, id: Uuid, workspace_id: WorkspaceId) -> R
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Distinct company ids bound to an org via `org_companies`. The
|
||||
/// cascade-reap path drills through here to collect teams → agents.
|
||||
pub async fn companies_of_org(pool: &PgPool, org_id: Uuid) -> Result<Vec<Uuid>, DbError> {
|
||||
let rows = sqlx::query!(
|
||||
"SELECT DISTINCT company_id FROM org_companies WHERE org_id = $1",
|
||||
org_id,
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
Ok(rows.into_iter().map(|r| r.company_id).collect())
|
||||
}
|
||||
|
||||
@@ -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!(
|
||||
|
||||
Reference in New Issue
Block a user