paperclipai/paperclip · warning

CreateOS lease cleanup is already in progress.

Error message

CreateOS lease cleanup is already in progress.

What it means

This error is thrown by the CreateOS sandbox provider's release() function when a release/cleanup of the same lease scope is already running. The provider tracks in-flight cleanups in a 'closing' set keyed by environment+lease; a second concurrent release for the same lease is rejected instead of interleaving two destroys/pauses of the same sandbox. It is a concurrency guard, not a resource failure.

Solutions

  1. Wait for the first release to complete instead of retrying; the error is transient — once the in-flight cleanup finishes, 'closing' no longer contains the scope.
  2. Serialize release calls per lease: queue or memoize the in-flight promise and await it rather than calling release again.
  3. Check whether two code paths (environment shutdown and lease cleanup) can both release the same lease and ensure only one owns cleanup.
  4. If a release appears permanently stuck, inspect network/connectivity to the CreateOS API — a hung stopActive/destroySandbox keeps the scope in 'closing'.

Example fix

// before: retry immediately on failure
await provider.release(params).catch(() => provider.release(params));
// after: wait for in-flight cleanup to settle before retrying
try { await provider.release(params); }
catch (e) {
  if (String(e.message).includes('already in progress')) {
    await new Promise(r => setTimeout(r, 1000));
    await provider.release(params);
  } else throw e;
}
Defensive patterns

Strategy: retry

Validate before calling

// Track in-flight releases per lease in caller code
const inFlight = new Map<string, Promise<void>>();
function canRelease(leaseId: string): boolean { return !inFlight.has(leaseId); }

Type guard

function isCleanupInProgressError(e: unknown): boolean {
  return e instanceof Error && e.message.includes('lease cleanup is already in progress');
}

Try / catch

try {
  await plugin.release(params);
} catch (e) {
  if (isCleanupInProgressError(e)) {
    await inFlightRelease; // await the already-running cleanup
  } else throw e;
}

Prevention

When it happens

Trigger: Calling release (environment teardown or plugin release-lease) twice concurrently for the same providerLeaseId — e.g. the environment stop path and a manual destroy both firing, or retrying release while the first call is still awaiting client.destroySandbox/transition.

Common situations: Double-teardown on shutdown (host stop + lease reaper both releasing), UI-triggered destroy racing automatic cleanup, retry logic with too-aggressive timeouts calling release again while the first request is still in flight.

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/66d6881e7af1c461. Report an issue: GitHub.

Appendix: source

Thrown at packages/plugins/sandbox-providers/createos/src/plugin.ts:109

    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();
    }
  }

  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);
    }
  }

View on GitHub (pinned to 3f1d897a7c)