From e09a08f1bbdfdcb80723fef57e199728388c00fe Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Mon, 12 Oct 2026 13:46:57 -0400 Subject: [PATCH] Point refused sandbox cleanup at the force option A run whose sandbox cleanup fails is kept for a retry, and only an explicit force deletes it without its sandboxes. Say so in the 409 so a caller with a run stuck behind an unavailable provider knows how to proceed. Co-Authored-By: Claude Opus 5.5 --- lib/apps/fabro-server/src/server.rs | 19 ++++++++++++++----- .../fabro-server/src/server/tests/deletion.rs | 9 +++------ 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/lib/apps/fabro-server/src/server.rs b/lib/apps/fabro-server/src/server.rs index e1dbdf4f5..27164f760 100644 --- a/lib/apps/fabro-server/src/server.rs +++ b/lib/apps/fabro-server/src/server.rs @@ -2991,10 +2991,9 @@ async fn delete_run_sandbox_resource( ); Ok(SandboxDeleteOutcome::Cleaned) } else { - Err(ApiError::new( - StatusCode::CONFLICT, - format!("Failed to delete the run's sandboxes: {problems}"), - )) + Err(sandbox_cleanup_conflict(&format!( + "Failed to delete the run's sandboxes: {problems}" + ))) } } // A live process still holds the run: only a forced delete leaves @@ -3021,12 +3020,22 @@ async fn delete_run_sandbox_resource( ); Ok(SandboxDeleteOutcome::Cleaned) } else { - Err(ApiError::new(StatusCode::CONFLICT, message)) + Err(sandbox_cleanup_conflict(&message)) } } } } +/// A refused cleanup keeps the run for a retry; say how to delete it anyway. +fn sandbox_cleanup_conflict(message: &str) -> ApiError { + ApiError::new( + StatusCode::CONFLICT, + format!( + "{message} (use force=true or --force to delete the run and leave its sandboxes behind)" + ), + ) +} + async fn reject_active_delete_without_force( state: &AppState, run_id: &RunId, diff --git a/lib/apps/fabro-server/src/server/tests/deletion.rs b/lib/apps/fabro-server/src/server/tests/deletion.rs index 66c53fb9e..f3634662a 100644 --- a/lib/apps/fabro-server/src/server/tests/deletion.rs +++ b/lib/apps/fabro-server/src/server/tests/deletion.rs @@ -234,12 +234,9 @@ async fn failed_cleanup_preserves_completed_results_across_retries_and_restart() for _ in 0..2 { let response = deletion_response(&run.app, run.id, false).await; let error = response_json!(response, StatusCode::CONFLICT).await; - assert!( - error["errors"][0]["detail"] - .as_str() - .unwrap() - .contains("Failed to delete the run's sandboxes") - ); + let detail = error["errors"][0]["detail"].as_str().unwrap(); + assert!(detail.contains("Failed to delete the run's sandboxes")); + assert!(detail.contains("use force=true or --force")); assert!(workspace.is_dir()); assert_completed_result(&run.app, run.id, &before).await; }