paperclipai/paperclip · critical
CreateOS setup failed and cleanup is unconfirmed for sandbox
Error message
CreateOS setup failed and cleanup is unconfirmed for sandbox ${sandbox.id}. What it means
During lease acquisition, CreateOS plugin provisions a sandbox, transitions it to running, prepares the workspace, and uploads a lease marker. If any of those steps fail, acquire tries to destroy the partially-created sandbox so no orphan is left behind. This error is thrown when that compensating destroySandbox call itself fails, meaning the setup failed AND the provider may still be billing for a live sandbox the host no longer references.
Solutions
- Check the CreateOS dashboard/API for a sandbox with the ID in the message and destroy it manually to avoid orphaned billing.
- Retry acquire once connectivity is confirmed — the failed sandbox was never returned as a lease, so a fresh acquire creates a new sandbox.
- Verify the API key has both sandbox-create and sandbox-delete permissions for the configured apiUrl.
- If destroys consistently fail, probe the environment (onEnvironmentProbe) to check API health before acquiring leases.
Example fix
// before: destroy failure hides the original error entirely
catch { throw new Error(`CreateOS setup failed and cleanup is unconfirmed for sandbox ${sandbox.id}.`); }
// after: run a probe first and surface the original cause when destroying
const probe = await fetch(`${config.apiUrl}/sandboxes/${sandbox.id}`, { headers: { authorization: `Bearer ${resolveApiKey(config)}` } });
if (probe.ok) console.warn(`sandbox ${sandbox.id} may still be live; destroy it manually`);
throw error; // rethrow the original setup failure for better diagnosis Defensive patterns
Strategy: try-catch
Validate before calling
// check API reachability and credentials before acquiring
const res = await fetch(`${config.apiUrl}/health`, { headers: { authorization: `Bearer ${apiKey}` } });
if (!res.ok) throw new Error(`CreateOS API unhealthy (${res.status}); skipping acquire`); Try / catch
try {
lease = await acquireLease(params);
} catch (e) {
if (e.message.includes("cleanup is unconfirmed")) {
const sandboxId = e.message.match(/sandbox ([^\s.]+)/)?.[1];
logger.error(`orphaned sandbox possible: ${sandboxId}; reconcile via CreateOS API`);
}
throw e;
} Prevention
- Probe the environment before batch-acquiring leases to confirm API health.
- Use an API key with both create and delete scopes.
- Periodically reconcile live sandboxes against active leases to catch orphans.
- Alert on any error containing 'cleanup is unconfirmed' — it implies possible billing for unused sandboxes.
When it happens
Trigger: acquire() fails partway (transition to 'running' fails, workspace mkdir exec fails, marker upload fails) and then the compensating client.destroySandbox(sandbox.id) also throws — e.g. the CreateOS API went down between the two calls, the API key lacks delete permission, or a transient network partition interrupts the destroy request.
Common situations: CreateOS API outage or flaky network during environment probe/acquire; sandbox stuck in a transitional state that cannot be deleted; API key created with create but not delete scope; rate limiting that lets create succeed but rejects the immediate destroy call.
Related errors
- CreateOS command cleanup failed; process termination is…
- CreateOS lease cleanup is already in progress.
- CreateOS lease cleanup must finish before resume.
- CreateOS process cleanup is unconfirmed; destroy this lease…
- CreateOS process creation could not be confirmed; destroy…
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18).
Data as JSON: /api/errors/aac92d31524e287c.
Report an issue: GitHub.
Appendix: source
Thrown at packages/plugins/sandbox-providers/createos/src/plugin.ts:49
const data = await client.json(`/sandboxes/${sandbox.id}/exec`, "POST", {
cmd: "/bin/bash", args: ["-lc", `mkdir -p -- ${shellQuote(CWD)}`],
}, signal);
if (object(data.result).exit_code !== 0) throw new Error("CreateOS workspace preparation failed; the image must provide Bash.");
const marker = randomUUID();
await client.upload(sandbox.id, MARKER, marker, signal);
return {
providerLeaseId: sandbox.id,
metadata: {
provider: "createos", apiUrl: config.apiUrl,
companyId: params.companyId, environmentId: params.environmentId,
remoteCwd: CWD, shellCommand: "bash", marker,
shape: config.shape, rootfs: config.rootfs, region: config.region,
reuseLease: config.reuseLease,
},
};
} catch (error) {
try { await client.destroySandbox(sandbox.id); }
catch { throw new Error(`CreateOS setup failed and cleanup is unconfirmed for sandbox ${sandbox.id}.`); }
throw error;
}
}
// Each worker owns its own transient lifecycle state. The host owns durable leases.
export function createPlugin() {
let ctx: PluginContext | null = null;
let shuttingDown = false;
type Active = { controller: AbortController; done: Promise<void> };
const active = new Map<string, Set<Active>>();
const closing = new Set<string>();
const unconfirmedCleanup = new Set<string>();
function key(params: PluginEnvironmentDriverBaseParams, id: string): string {
const config = parseConfig(params.config);
const account = createHash("sha256").update(resolveApiKey(config)).digest("hex");
return JSON.stringify([params.companyId, params.environmentId, config.apiUrl, account, id]);
}View on GitHub (pinned to 3f1d897a7c)