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

  1. Acquire a fresh lease — the old one is closing or flagged; it cannot accept new work.
  2. If unconfirmed cleanup was flagged, call onEnvironmentDestroyLease (destroy=true) to force sandbox destruction and clear the flag, then re-acquire.
  3. Wait for the in-flight release/destroy to finish before retrying, or serialize release and execute calls per lease in the host.
  4. 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

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


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)