fix(agents): a soft-deleted agent could never be purged

Clearing the fleet's four leftover agents returned 404 on every one. They had
been soft-deleted back in June — correctly invisible in the UI ever since — and
`agents::get` filters `deleted_at IS NULL`, so `workspace_agent` could not find
them. Every route uses it, including `batch-delete`, the one that exists to
HARD-purge. So a soft-deleted agent was unreachable from the application
entirely and its row stayed forever.

`get_any` sees them, and only the purge path uses it: hiding soft-deleted rows
is right for every read, and wrong for the one operation whose whole job is
removing them. Written with `query_as` rather than the checked macro so it does
not force an offline-cache regeneration on every machine that builds this.

`fleet-reset.sh` now uses `batch-delete` for agents rather than
`DELETE /api/claws/{id}`. The latter is a SOFT delete, so pointing a reset
script at it would have quietly added to the pile it was meant to clear.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Omar Sobh
2026-08-09 18:15:56 -07:00
co-authored by Claude Opus 5
parent 16cfc29074
commit f27d2605eb
3 changed files with 131 additions and 8 deletions
+19 -1
View File
@@ -26,6 +26,19 @@ pub(crate) async fn workspace_agent(
Ok(agent)
}
/// As [`workspace_agent`], but sees soft-deleted agents too. PURGE ONLY.
pub(crate) async fn workspace_agent_any(
state: &AppState,
user: &cm_auth::AuthedUser,
agent_id: AgentId,
) -> Result<Agent, ApiError> {
let agent = cm_db::repo::agents::get_any(&state.pool, agent_id).await?;
if agent.workspace_id != user.workspace_id {
return Err(ApiError::NotFound);
}
Ok(agent)
}
/// `GET /api/claws/{id}/runtime-config` — the claw's model + §15 sandbox facts
/// (for the claw card / anatomy view's model badge).
#[derive(Serialize)]
@@ -1289,7 +1302,12 @@ pub async fn batch_delete(
let mut done = 0usize;
for id in agent_ids {
let base = 100 * done / total;
let agent = match workspace_agent(&state, &user, id).await {
// `workspace_agent_any`, not `workspace_agent`: a purge has to be
// able to see the rows it exists to remove. The soft-delete path
// correctly hides them from every read, which also hid them from
// the only route that could reap them — four soft-deleted agents
// from June were unreachable from the application entirely.
let agent = match workspace_agent_any(&state, &user, id).await {
Ok(a) => a,
Err(_) => { yield sse(json!({"stage":"skip","pct":base,"label":format!("{id}: not found or no access")})); done += 1; continue; }
};