{"record":{"id":"fbc605056170bac4","repo":"Hmbown/CodeWhale","slug":"cloud-agent-create-succeeded-but-returned-no-usable-sandbox","errorCode":null,"errorMessage":"Cloud agent create succeeded but returned no usable sandbox id (raw: \"{raw}\"). A sandbox may need manual cleanup at the provider.","messagePattern":"Cloud agent create succeeded but returned no usable sandbox id \\(raw: \"(.+?)\"\\)\\. A sandbox may need manual cleanup at the provider\\.","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/cloud_dispatch.rs","lineNumber":1531,"sourceCode":"            // is impossible. Best-effort delete with the raw string when it\n            // is at least non-empty (the DELETE path itself validates and\n            // will refuse dangerous shapes), and always name it in the\n            // error so an operator can clean it up.\n            let raw = parsed\n                .get(\"id\")\n                .or_else(|| parsed.get(\"sandboxId\"))\n                .and_then(serde_json::Value::as_str)\n                .unwrap_or(\"\")\n                .trim();\n            if !raw.is_empty() && valid_sandbox_id(raw) {\n                let _ = Self::send_json(\n                    reqwest::Method::DELETE,\n                    &Self::control_plane_url(&format!(\"sandbox/{raw}\"))?,\n                    &api_key,\n                    serde_json::Value::Null,\n                );\n            }\n            bail!(\n                \"Cloud agent create succeeded but returned no usable sandbox id (raw: \\\"{raw}\\\"). \\\n                 A sandbox may need manual cleanup at the provider.\"\n            );\n        }\n        let toolbox_url = parsed\n            .get(\"toolboxProxyUrl\")\n            .and_then(serde_json::Value::as_str)\n            .map(str::trim)\n            .filter(|value| !value.is_empty() && value.len() <= MAX_REMOTE_BYTES)\n            .and_then(|value| validate_outbound_origin(value).ok())\n            .map(|url| url.to_string());\n        // Daytona applies labels via a dedicated PUT, not the create body\n        // (kept there for forward compatibility). Labels are load-bearing:\n        // the orphan reconciler joins them back to job records, so a\n        // sandbox without them is untraceable spend. If the PUT fails, the\n        // already-created sandbox is torn down immediately and create\n        // fails truthfully — no orphan, retryable — rather than returning\n        // a receipt the reconciler can never find again.","sourceCodeStart":1513,"sourceCodeEnd":1549,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/cloud_dispatch.rs#L1513-L1549","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Inspect the raw body shown in the error message to identify the actual response shape","Check for orphaned sandboxes in the Daytona dashboard and delete them manually","Update the id-extraction code (parsed.get(\"id\").or(get(\"sandboxId\"))) if the provider renamed the field","Pin/verify the Daytona API version the client targets"],"exampleFix":"// before\nlet sandbox_id = parsed.get(\"id\").or_else(|| parsed.get(\"sandboxId\"));\n// after\n// after updating extraction for the provider's new field name:\nlet sandbox_id = parsed.get(\"id\").or_else(|| parsed.get(\"sandboxId\")).or_else(|| parsed.get(\"sandbox_id\"));","handlingStrategy":"fallback","validationCode":"// after parsing, assert the id is usable before proceeding\nlet id = parsed.get(\"id\").and_then(|v| v.as_str()).unwrap_or(\"\");\nif !valid_sandbox_id(id) { warn!(\"unexpected create response: {parsed}\"); }","typeGuard":"fn extract_sandbox_id(v: &serde_json::Value) -> Option<&str> {\n    [\"id\", \"sandboxId\", \"sandbox_id\"].iter().find_map(|k| v.get(k)?.as_str()).filter(|s| valid_sandbox_id(s))\n}","tryCatchPattern":"let id = extract_sandbox_id(&parsed).ok_or_else(|| anyhow!(\"create returned no usable id\"))?;","preventionTips":["Pin and test against the Daytona API version in CI with a contract test","Alert on 2xx responses whose body lacks the expected id field","Keep the accepted id-field list in one place and update it with provider changes"],"tags":["cloud","daytona","schema","orphaned-resources"],"backgroundTag":"unexpected-api-response-shape","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-22T21:17:16.096Z"}