paperclipai/paperclip · error
CreateOS lease is closing or requires cleanup.
Error message
CreateOS lease is closing or requires cleanup.
What it means
track() refuses to start new work on a lease whose scope is shutting down, already closing, or flagged as having unconfirmed cleanup. The plugin tracks per-lease lifecycle state (closing set, unconfirmedCleanup set, shutdown flag) so operations are never issued against a sandbox being destroyed or one whose earlier cleanup failed. This error means the lease is in (or heading into) a terminal state.
Solutions
- Acquire a fresh lease — the old one is closing or flagged; it cannot accept new work.
- If unconfirmed cleanup was flagged, call onEnvironmentDestroyLease (destroy=true) to force sandbox destruction and clear the flag, then re-acquire.
- Wait for the in-flight release/destroy to finish before retrying, or serialize release and execute calls per lease in the host.
- Avoid issuing execute calls after shutdown has been initiated; check the plugin lifecycle state first.
Example fix
// before: blind retry against a closing lease
await driver.execute({ ...params, lease });
// after: destroy the flagged lease and re-acquire
try {
return await driver.execute({ ...params, lease });
} catch (e) {
if (e.message.includes("closing or requires cleanup")) {
await driver.destroyLease({ ...params, providerLeaseId: lease.providerLeaseId, leaseMetadata: lease.metadata });
lease = await driver.acquireLease(params);
return await driver.execute({ ...params, lease });
}
throw e;
} Defensive patterns
Strategy: retry
Validate before calling
// track a local set of lease ids you have released or that failed cleanup
const unusable = new Set<string>();
if (unusable.has(lease.providerLeaseId)) {
lease = await acquireLease(params); // skip known-closing leases
} Try / catch
try {
return await driver.execute({ ...params, lease });
} catch (e) {
if (e.message.includes("closing or requires cleanup")) {
await driver.destroyLease({ ...params, providerLeaseId: lease.providerLeaseId, leaseMetadata: lease.metadata });
const fresh = await driver.acquireLease(params);
return await driver.execute({ ...params, lease: fresh });
}
throw e;
} Prevention
- Serialize release/destroy and execute calls per lease in the host.
- After a cleanup-unconfirmed failure, destroy (not reuse) the lease.
- Do not queue new commands after initiating shutdown.
- Treat this error as terminal for the lease: always re-acquire rather than retry in place.
When it happens
Trigger: Executing or syncing while: the plugin is shutting down (onShutdown ran); release() marked the scope closing and is still stopping active calls; a prior operation threw CreateosCleanupError, adding the scope to unconfirmedCleanup; concurrent release and execute race on the same lease.
Common situations: Host timeout/auto-pause triggering lease release while a command is still being queued; an earlier execute failed with unconfirmed cleanup and the host retries on the same lease; process shutdown overlapping in-flight agent commands; a reuseLease=false config causing immediate destroy paths to close scopes aggressively.
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
- ACPX provider ownership admission is already active
- Bridge response envelope changed while reading.
- capability_live_attempt_not_running
- Capability live turn admission was abandoned during teardown
- Chat SDK endpoint runtime was retired
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18).
Data as JSON: /api/errors/0c3a5fad750535a2.
Report an issue: GitHub.
Appendix: source
Thrown at packages/plugins/sandbox-providers/createos/src/plugin.ts:82
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]);
}
async function stopActive(scope: string) {
const calls = [...(active.get(scope) ?? [])];
for (const call of calls) call.controller.abort();
await Promise.all(calls.map((call) => call.done));
}
async function track<T>(
params: PluginEnvironmentDriverBaseParams & { lease: PluginEnvironmentLease },
work: (client: CreateosClient, signal: AbortSignal) => Promise<T>,
timeoutOverride?: number,
): Promise<T> {
if (!params.lease.providerLeaseId || !metadataMatches(params, params.lease.metadata)) throw new Error("CreateOS execution requires a lease from this environment.");
const scope = key(params, params.lease.providerLeaseId);
if (shuttingDown || closing.has(scope) || unconfirmedCleanup.has(scope)) throw new Error("CreateOS lease is closing or requires cleanup.");
const config = parseConfig(params.config);
const timeoutMs = timeoutOverride ?? config.timeoutMs;
if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 86_400_000) throw new Error("Invalid CreateOS command timeout.");
const controller = new AbortController();
let finish!: () => void;
const entry: Active = { controller, done: new Promise<void>((resolve) => { finish = resolve; }) };
const calls = active.get(scope) ?? new Set<Active>();
calls.add(entry);
active.set(scope, calls);
try {
return await work(new CreateosClient(config), AbortSignal.any([controller.signal, AbortSignal.timeout(timeoutMs)]));
} catch (error) {
if (error instanceof CreateosCleanupError) unconfirmedCleanup.add(scope);
throw error;
} finally {
calls.delete(entry);
if (calls.size === 0) active.delete(scope);
finish();View on GitHub (pinned to 3f1d897a7c)