From f88df59163ad287a093bf26dc600c13be5343daa Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Thu, 20 Aug 2026 17:39:19 -0400 Subject: [PATCH] Classify sandbox state-change rejections as transient infra MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A Daytona "Sandbox state change in progress" rejection surfacing through the pipeline lifecycle path ("Pipeline lifecycle operation failed") matched no transient-infra hint, so the run failure was categorized deterministic. The condition is a provider lifecycle transition that finishes on its own — the definition of transient infrastructure — and the deterministic label misinforms retry machinery and anyone reading the failure. Add two transient-infra hints: the provider rejection ("state change in progress") and the bounded-wait timeout an activation reports when a stop transition outlives its budget ("sandbox stop still in progress"). Co-Authored-By: Claude Fable 5 --- lib/components/fabro-workflow/src/error.rs | 35 +++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/lib/components/fabro-workflow/src/error.rs b/lib/components/fabro-workflow/src/error.rs index f8c08ca15..21833a8df 100644 --- a/lib/components/fabro-workflow/src/error.rs +++ b/lib/components/fabro-workflow/src/error.rs @@ -84,6 +84,8 @@ const TRANSIENT_INFRA_HINTS: &[&str] = &[ "cross-device link", "invalid cross-device link", "os error 18", + "state change in progress", + "sandbox stop still in progress", ]; const BUDGET_EXHAUSTED_HINTS: &[&str] = &[ @@ -807,6 +809,18 @@ mod tests { assert_eq!(err.failure_category(), FailureCategory::TransientInfra); } + #[test] + fn engine_error_with_sandbox_state_change_cause_classifies_transient() { + let source = TestOuterError { + message: "Failed to start Daytona sandbox", + source: TestCause("Sandbox state change in progress"), + }; + let err = Error::engine_with_source("Pipeline lifecycle operation failed", source); + + assert_eq!(err.failure_category(), FailureCategory::TransientInfra); + assert!(err.is_retryable()); + } + #[test] fn handler_error_display() { let err = Error::handler("LLM call failed"); @@ -1281,7 +1295,7 @@ mod tests { #[test] fn transient_infra_hints_count() { - assert_eq!(TRANSIENT_INFRA_HINTS.len(), 38); + assert_eq!(TRANSIENT_INFRA_HINTS.len(), 40); } #[test] @@ -1450,6 +1464,25 @@ mod tests { ); } + #[test] + fn classify_reason_sandbox_state_change_in_progress() { + assert_eq!( + classify_failure_reason( + "Pipeline lifecycle operation failed: failed to activate sandbox after node \ + attempt survey: Failed to start Daytona sandbox: Sandbox state change in progress" + ), + FailureCategory::TransientInfra + ); + } + + #[test] + fn classify_reason_sandbox_stop_still_in_progress() { + assert_eq!( + classify_failure_reason("Daytona sandbox stop still in progress after 120s"), + FailureCategory::TransientInfra + ); + } + #[test] fn classify_reason_500() { assert_eq!(