Hmbown/CodeWhale · error

Cloud agent create succeeded but returned no usable sandbox…

Error message

Cloud agent create succeeded but returned no usable sandbox id (raw: "{raw}"). A sandbox may need manual cleanup at the provider.

What it means

The create call returned HTTP success but the JSON body contained no id field usable as a path token (`id`/`sandboxId` missing or failing `valid_sandbox_id`). The library attempts a best-effort DELETE of the raw id and then fails, warning that an orphaned sandbox may need manual cleanup at the provider.

Solutions

  1. Inspect the raw body shown in the error message to identify the actual response shape
  2. Check for orphaned sandboxes in the Daytona dashboard and delete them manually
  3. Update the id-extraction code (parsed.get("id").or(get("sandboxId"))) if the provider renamed the field
  4. Pin/verify the Daytona API version the client targets

Example fix

// before
let sandbox_id = parsed.get("id").or_else(|| parsed.get("sandboxId"));
// after
// after updating extraction for the provider's new field name:
let sandbox_id = parsed.get("id").or_else(|| parsed.get("sandboxId")).or_else(|| parsed.get("sandbox_id"));
Defensive patterns

Strategy: fallback

Validate before calling

// after parsing, assert the id is usable before proceeding
let id = parsed.get("id").and_then(|v| v.as_str()).unwrap_or("");
if !valid_sandbox_id(id) { warn!("unexpected create response: {parsed}"); }

Type guard

fn extract_sandbox_id(v: &serde_json::Value) -> Option<&str> {
    ["id", "sandboxId", "sandbox_id"].iter().find_map(|k| v.get(k)?.as_str()).filter(|s| valid_sandbox_id(s))
}

Try / catch

let id = extract_sandbox_id(&parsed).ok_or_else(|| anyhow!("create returned no usable id"))?;

Prevention

When it happens

Trigger: Daytona returns 2xx with an unexpected body: an error envelope with 200, a changed field name, an empty id, or an id containing slashes/invalid characters.

Common situations: Daytona API version drift (response schema changed), an auth proxy swallowing the real response, or a partially failed create that returned 200 with a stub body.

Understand the failure class

Background: "invalid response format", "malformed payload", "missing data field": when an API returns 200 but the response shape is wrong — this error's family across 23 libraries.

Related errors


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

Appendix: source

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

            // is impossible. Best-effort delete with the raw string when it
            // is at least non-empty (the DELETE path itself validates and
            // will refuse dangerous shapes), and always name it in the
            // error so an operator can clean it up.
            let raw = parsed
                .get("id")
                .or_else(|| parsed.get("sandboxId"))
                .and_then(serde_json::Value::as_str)
                .unwrap_or("")
                .trim();
            if !raw.is_empty() && valid_sandbox_id(raw) {
                let _ = Self::send_json(
                    reqwest::Method::DELETE,
                    &Self::control_plane_url(&format!("sandbox/{raw}"))?,
                    &api_key,
                    serde_json::Value::Null,
                );
            }
            bail!(
                "Cloud agent create succeeded but returned no usable sandbox id (raw: \"{raw}\"). \
                 A sandbox may need manual cleanup at the provider."
            );
        }
        let toolbox_url = parsed
            .get("toolboxProxyUrl")
            .and_then(serde_json::Value::as_str)
            .map(str::trim)
            .filter(|value| !value.is_empty() && value.len() <= MAX_REMOTE_BYTES)
            .and_then(|value| validate_outbound_origin(value).ok())
            .map(|url| url.to_string());
        // Daytona applies labels via a dedicated PUT, not the create body
        // (kept there for forward compatibility). Labels are load-bearing:
        // the orphan reconciler joins them back to job records, so a
        // sandbox without them is untraceable spend. If the PUT fails, the
        // already-created sandbox is torn down immediately and create
        // fails truthfully — no orphan, retryable — rather than returning
        // a receipt the reconciler can never find again.

View on GitHub (pinned to 73e0f67d83)