fix(agents): stop listing soft-deleted agents
deploy / test (push) Successful in 3m54s
deploy / build (push) Successful in 5m20s

Deleting an agent looked like a no-op: it disappeared from the workforce but
stayed on the Team board, and deleting it again did nothing because the row was
already marked. Two queries selected from `agents` without `deleted_at IS NULL`:

  routes/team.rs   the leaderboard — the surface still showing them
  routes/world.rs  the "working" set — a deleted agent holding a stale
                   agent_containers row rendered as live

Observed on this deployment: /api/workforce correctly returned nothing while
/api/team/leaderboard returned two agents soft-deleted back in June.

NOT changed: those rows still exist. Making delete permanent means hard_purge,
which also deletes usage_events — billing history, 6 credits on one of these
two. Discarding that as a side effect of tidying a roster is an explicit
decision, not something a display fix should smuggle in.

.sqlx regenerated: team.rs uses the compile-time-checked query! macro, so the
cached entry no longer matched.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Omar Sobh
2026-08-14 12:20:00 -07:00
co-authored by Claude Opus 5
parent a494634f81
commit ccbc387f4b
6 changed files with 8 additions and 90 deletions
@@ -1,16 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE topology_runs\n SET checkpoint = $2, last_event_id = $3, updated_at = now()\n WHERE id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Jsonb",
"Int8"
]
},
"nullable": []
},
"hash": "5fcbd4d6adbf02489051e2fa63d1df670863bf90e55c1c0ac0ab011759cbd272"
}
@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE topology_runs\n SET status = 'queued', updated_at = now()\n WHERE status = 'running' AND updated_at < now() - make_interval(secs => $1)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Float8"
]
},
"nullable": []
},
"hash": "7298995b5b58aed46888bb9e5c8d331483aee162fc6bcf1e53232d2afc7c3e62"
}
@@ -1,56 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE topology_runs\n SET status = 'running', started_at = COALESCE(started_at, now()), updated_at = now()\n WHERE id = (\n SELECT id FROM topology_runs\n WHERE status = 'queued'\n ORDER BY created_at\n FOR UPDATE SKIP LOCKED\n LIMIT 1\n )\n RETURNING id, workspace_id, task, graph, checkpoint, last_event_id, tier",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "workspace_id",
"type_info": "Uuid"
},
{
"ordinal": 2,
"name": "task",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "graph",
"type_info": "Jsonb"
},
{
"ordinal": 4,
"name": "checkpoint",
"type_info": "Jsonb"
},
{
"ordinal": 5,
"name": "last_event_id",
"type_info": "Int8"
},
{
"ordinal": 6,
"name": "tier",
"type_info": "Text"
}
],
"parameters": {
"Left": []
},
"nullable": [
false,
false,
false,
true,
true,
false,
false
]
},
"hash": "9eae6ca16ffc9346456128ce676ef04f3478f873d6ac5f95154b797f454f44c0"
}
@@ -1,6 +1,6 @@
{ {
"db_name": "PostgreSQL", "db_name": "PostgreSQL",
"query": "SELECT a.id, a.name, a.accent,\n COALESCE(SUM(u.credits), 0)::BIGINT AS \"credits!\",\n COALESCE(SUM(u.tokens_in + u.tokens_out), 0)::BIGINT AS \"tokens!\",\n COUNT(u.id)::BIGINT AS \"runs!\"\n FROM agents a\n LEFT JOIN usage_events u ON u.agent_id = a.id\n WHERE a.workspace_id = $1\n GROUP BY a.id, a.name, a.accent\n ORDER BY \"credits!\" DESC, \"tokens!\" DESC, a.name", "query": "SELECT a.id, a.name, a.accent,\n COALESCE(SUM(u.credits), 0)::BIGINT AS \"credits!\",\n COALESCE(SUM(u.tokens_in + u.tokens_out), 0)::BIGINT AS \"tokens!\",\n COUNT(u.id)::BIGINT AS \"runs!\"\n FROM agents a\n LEFT JOIN usage_events u ON u.agent_id = a.id\n -- deleted_at: a soft-deleted agent is gone everywhere else, so\n -- listing it here made deletion look like a no-op — the operator\n -- deletes it, the board still shows it, and deleting again does\n -- nothing because the row is already marked.\n WHERE a.workspace_id = $1 AND a.deleted_at IS NULL\n GROUP BY a.id, a.name, a.accent\n ORDER BY \"credits!\" DESC, \"tokens!\" DESC, a.name",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@@ -48,5 +48,5 @@
null null
] ]
}, },
"hash": "d4ef449c48b15519b7195be637dca3d456140ce477993d25a87e209174f79aba" "hash": "d5bc028ca030daed4e6111990945d8d7011414d830f7d6d0a04980efb79af2a6"
} }
+5 -1
View File
@@ -100,7 +100,11 @@ pub async fn leaderboard(
COUNT(u.id)::BIGINT AS "runs!" COUNT(u.id)::BIGINT AS "runs!"
FROM agents a FROM agents a
LEFT JOIN usage_events u ON u.agent_id = a.id LEFT JOIN usage_events u ON u.agent_id = a.id
WHERE a.workspace_id = $1 -- deleted_at: a soft-deleted agent is gone everywhere else, so
-- listing it here made deletion look like a no-op — the operator
-- deletes it, the board still shows it, and deleting again does
-- nothing because the row is already marked.
WHERE a.workspace_id = $1 AND a.deleted_at IS NULL
GROUP BY a.id, a.name, a.accent GROUP BY a.id, a.name, a.accent
ORDER BY "credits!" DESC, "tokens!" DESC, a.name"#, ORDER BY "credits!" DESC, "tokens!" DESC, a.name"#,
user.workspace_id.as_uuid(), user.workspace_id.as_uuid(),
+1 -1
View File
@@ -36,7 +36,7 @@ async fn working_agents(pool: &PgPool, ws: WorkspaceId) -> HashSet<String> {
let rows = sqlx::query( let rows = sqlx::query(
"SELECT DISTINCT a.id::text AS id "SELECT DISTINCT a.id::text AS id
FROM agents a JOIN agent_containers ac ON ac.agent_id = a.id FROM agents a JOIN agent_containers ac ON ac.agent_id = a.id
WHERE a.workspace_id = $1", WHERE a.workspace_id = $1 AND a.deleted_at IS NULL",
) )
.bind(ws.as_uuid()) .bind(ws.as_uuid())
.fetch_all(pool) .fetch_all(pool)