Hmbown/CodeWhale · error

Cloud agent sandbox teardown failed

Error message

Cloud agent sandbox teardown failed (HTTP {status}).

What it means

Thrown by `teardown` when the control-plane `DELETE sandbox/{id}` returns neither a success status nor 404. 2xx and 404 are treated as idempotent success (the comment notes Daytona returns 204 on delete and 404 means already gone); any other status — typically 401/403/429/5xx — means the sandbox was not confirmed deleted. The sandbox may therefore still be running and incurring provider cost.

Solutions

  1. Retry the teardown — DELETE is idempotent here, so re-invoking later is safe and will 404 once gone.
  2. Check the HTTP status: 401/403 means fix CODEWHALE cloud credentials (`Self::api_key`) before retrying; 429 means back off and retry.
  3. If it keeps failing, delete the sandbox by id in the provider dashboard to stop billing, then retry teardown to reconcile local state.
  4. Audit for orphaned sandboxes from prior failed teardowns via `list_job_sandboxes` and clean them up.

Example fix

// caller: teardown is idempotent, so retry with backoff
for attempt in 0..3 {
    match provider.teardown(&receipt) {
        Ok(()) => break,
        Err(e) if attempt < 2 => std::thread::sleep(Duration::from_secs(2 * (attempt + 1))),
        Err(e) => return Err(e),
    }
}
Defensive patterns

Strategy: retry

Try / catch

// DELETE is idempotent (2xx or 404 both succeed), so retry with backoff
for attempt in 0..3 {
    match provider.teardown(&receipt) {
        Ok(()) => break,
        Err(e) if attempt == 2 => {
            log_orphan_sandbox(&receipt.sandbox_id); // clean up manually
        }
        Err(_) => std::thread::sleep(Duration::from_secs(2u64.pow(attempt))),
    }
}

Prevention

When it happens

Trigger: Canceling or completing a cloud job when the DELETE call fails: expired/invalid API key (401), rate limiting (429), control-plane outage (5xx), or a conflict status if the provider disallows deleting a sandbox in its current state.

Common situations: API key rotated or revoked between job start and teardown; provider rate limits hit during mass job cancellation; transient Daytona outage leaving many orphaned sandboxes.

Understand the failure class

Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/98d5e3ece5319c16. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/cloud_dispatch.rs:1780

    fn teardown(&self, receipt: &SandboxReceipt) -> Result<()> {
        let api_key = Self::api_key()?;
        if !valid_sandbox_id(&receipt.sandbox_id) {
            bail!("the sandbox id is not a usable path token");
        }
        let url = Self::control_plane_url(&format!("sandbox/{}", receipt.sandbox_id))?;
        let response = Self::send_json(
            reqwest::Method::DELETE,
            &url,
            &api_key,
            serde_json::Value::Null,
        )?;
        // Daytona returns 204 on delete; treat 404 as already-gone success so
        // cancel/complete teardown is idempotent.
        let status = response.status();
        if status.is_success() || status.as_u16() == 404 {
            Ok(())
        } else {
            bail!("Cloud agent sandbox teardown failed (HTTP {status}).");
        }
    }

    fn list_job_sandboxes(&self) -> Result<Vec<LabeledSandbox>> {
        let api_key = Self::api_key()?;
        // The provider's list call takes a JSON-encoded exact-match labels
        // filter (same OpenAPI family as create/get/delete above); filtering
        // on the product tag keeps the response to Codewhale dispatch
        // sandboxes only — never the user's own sandboxes on a shared key.
        let mut url = Self::control_plane_url("sandbox")?;
        url.query_pairs_mut().append_pair(
            "labels",
            &format!("{{\"{SANDBOX_PRODUCT_LABEL}\":\"{SANDBOX_PRODUCT_VALUE}\"}}"),
        );
        let response = Self::send_json(
            reqwest::Method::GET,
            &url,
            &api_key,

View on GitHub (pinned to 73e0f67d83)