paperclipai/paperclip · error
CreateOS process cleanup is unconfirmed; destroy this lease…
Error message
CreateOS process cleanup is unconfirmed; destroy this lease before reusing it.
What it means
Thrown in release() when a lease is being released without destroy while config.reuseLease is enabled, but a previous cleanup of this lease's process was never confirmed (the scope is in the 'unconfirmedCleanup' set). The provider refuses to pause/reuse a sandbox whose prior process termination could not be verified, because reusing it could attach to a stale or still-running process. Destroying the lease clears the flag.
Solutions
- Destroy the lease instead of reusing it: call release with destroy=true (or client.destroySandbox) — this clears the unconfirmedCleanup flag.
- If reuse is not needed, set config.reuseLease to false so releases always destroy and never hit this path.
- Investigate why the earlier cleanup was unconfirmed (network errors, unexpected CreateOS API responses) before relying on reuse for these leases.
- If the flag is stale and the sandbox is verifiably clean, destroy-and-recreate is the only supported reset; there is no API to clear the flag without destroy.
Example fix
// before: reuse release fails
await release({ ...params, leaseMetadata }, false);
// after: destroy to clear unconfirmed cleanup state
await release({ ...params, leaseMetadata }, true); Defensive patterns
Strategy: fallback
Validate before calling
// Destroy leases with unconfirmed cleanup instead of reusing
if (leaseHadUnconfirmedCleanup(scope)) {
await release({ ...params }, true); // destroy=true path
} else {
await release({ ...params }, false);
} Type guard
function isUnconfirmedCleanupError(e: unknown): boolean {
return e instanceof Error && e.message.includes('process cleanup is unconfirmed');
} Try / catch
try {
await release(params, false); // try reuse
} catch (e) {
if (isUnconfirmedCleanupError(e)) {
await release(params, true); // fall back to destroy
} else throw e;
} Prevention
- Investigate the root cause of unconfirmed cleanups (CreateOS API errors) rather than just retrying
- Disable reuseLease if your workloads cannot tolerate destroy-and-recreate
- Treat unconfirmed-cleanup leases as poisoned and always destroy them
- Log and alert on this error to catch host-level force-stops of sandboxes
When it happens
Trigger: Releasing a reusable lease after a prior release ended without confirmed process cleanup (e.g. an earlier pause returned 404-or-unknown state, or a transition failed mid-flight), then attempting the default non-destroy release path with reuseLease: true.
Common situations: Reuse-mode pools where a sandbox was force-stopped at the host level so the later pause couldn't confirm in-sandbox process termination; a crashed earlier release that marked cleanup unconfirmed; operator toggled reuseLease on for leases that already had unconfirmed cleanup.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
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 creation could not be confirmed; destroy…
- CreateOS setup failed and cleanup is unconfirmed for sandbox
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18).
Data as JSON: /api/errors/6aa90ab4aee42b02.
Report an issue: GitHub.
Appendix: source
Thrown at packages/plugins/sandbox-providers/createos/src/plugin.ts:119
}
}
async function release(params: PluginEnvironmentReleaseLeaseParams, destroy: boolean) {
const id = params.providerLeaseId;
if (!id) return;
if (!metadataMatches(params, params.leaseMetadata)) throw new Error("CreateOS lease does not belong to this environment.");
const scope = key(params, id);
if (closing.has(scope)) throw new Error("CreateOS lease cleanup is already in progress.");
closing.add(scope);
try {
await stopActive(scope);
const config = parseConfig(params.config);
const client = new CreateosClient(config);
if (destroy || !config.reuseLease) {
await client.destroySandbox(id);
unconfirmedCleanup.delete(scope);
} else {
if (unconfirmedCleanup.has(scope)) throw new Error("CreateOS process cleanup is unconfirmed; destroy this lease before reusing it.");
try { await client.transition(id, "paused", AbortSignal.timeout(config.timeoutMs)); }
catch (error) { if (!(error instanceof CreateosApiError && error.status === 404)) throw error; }
}
} finally {
closing.delete(scope);
}
}
return definePlugin({
async setup(context) { ctx = context; ctx.logger.info("CreateOS sandbox provider ready"); },
async onHealth() { return { status: "ok", message: "CreateOS provider loaded; probe an environment to check connectivity." }; },
async onEnvironmentValidateConfig(params) {
try { return { ok: true, normalizedConfig: { ...parseConfig(params.config) } }; }
catch (error) { return { ok: false, errors: [error instanceof Error ? error.message : "Invalid CreateOS configuration."] }; }
},
async onEnvironmentProbe(params) {
let lease: PluginEnvironmentLease | null = null;
try {View on GitHub (pinned to 3f1d897a7c)