paperclipai/paperclip · warning

CreateOS lease cleanup must finish before resume.

Error message

CreateOS lease cleanup must finish before resume.

What it means

Thrown by onEnvironmentResumeLease when the lease is currently being cleaned up (scope present in 'closing') or had an unconfirmed cleanup (scope in 'unconfirmedCleanup'). Resuming a lease mid-destroy or with unverifiable prior cleanup would attach work to a sandbox that may disappear or still hold stale processes, so the provider refuses. Wait for cleanup to finish or destroy and re-acquire.

Solutions

  1. Wait for the concurrent release to complete, then retry resume; the 'closing' guard is transient.
  2. If the lease has unconfirmed cleanup, destroy it (release with destroy=true) and acquire a fresh lease instead of resuming.
  3. Serialize lease lifecycle operations per lease so resume never overlaps release.
  4. Add a small backoff around resume retries to avoid racing the teardown path.

Example fix

// before: immediate resume after teardown request
stopEnvironment(env);
await plugin.onEnvironmentResumeLease(resumeParams); // throws
// after: await teardown, then resume or re-acquire
await stopEnvironment(env);
try { await plugin.onEnvironmentResumeLease(resumeParams); }
catch { await plugin.onEnvironmentAcquireLease(acquireParams); }
Defensive patterns

Strategy: retry

Validate before calling

// Track lifecycle state per lease in caller code
const releasing = new Set<string>();
function canResume(leaseId: string): boolean {
  return !releasing.has(leaseId);
}

Type guard

function isCleanupPendingError(e: unknown): boolean {
  return e instanceof Error && e.message.includes('cleanup must finish before resume');
}

Try / catch

try {
  await plugin.onEnvironmentResumeLease(params);
} catch (e) {
  if (isCleanupPendingError(e)) {
    await sleep(1000);
    await plugin.onEnvironmentResumeLease(params); // or acquire fresh lease
  } else throw e;
}

Prevention

When it happens

Trigger: Calling onEnvironmentResumeLease concurrently with release()/stopActive for the same lease (cleanup in progress), or resuming a lease previously released without destroy whose process cleanup was never confirmed (see error 116).

Common situations: Agent scheduler resuming work while an environment stop/teardown is still awaiting the CreateOS API; a retry loop resuming immediately after a failed release; reuse-mode pools where a prior pause was unconfirmed.

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

Appendix: source

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

      try {
        lease = await acquire({ ...params, runId: "probe" });
        const result = await execute(new CreateosClient(parseConfig(params.config)), {
          ...params, lease, command: "/bin/echo", args: ["paperclip-createos-ready"], cwd: CWD,
        }, AbortSignal.timeout(parseConfig(params.config).timeoutMs));
        if (result.timedOut || result.exitCode !== 0 || !result.stdout.includes("paperclip-createos-ready")) throw new Error("CreateOS command probe failed.");
        return { ok: true, summary: "CreateOS sandbox creation and command execution succeeded." };
      } catch (error) {
        return { ok: false, summary: error instanceof Error ? error.message : "CreateOS probe failed." };
      } finally {
        // Never leave a reusable probe sandbox behind or hide deletion failure.
        if (lease?.providerLeaseId) await new CreateosClient(parseConfig(params.config)).destroySandbox(lease.providerLeaseId);
      }
    },
    onEnvironmentAcquireLease: acquire,
    async onEnvironmentResumeLease(params) {
      if (!metadataMatches(params, params.leaseMetadata)) throw new Error("CreateOS lease does not belong to this environment.");
      const scope = key(params, params.providerLeaseId);
      if (closing.has(scope) || unconfirmedCleanup.has(scope)) throw new Error("CreateOS lease cleanup must finish before resume.");
      const marker = params.leaseMetadata?.marker;
      if (typeof marker !== "string" || !/^[0-9a-f-]{36}$/.test(marker)) return { providerLeaseId: null, metadata: { expired: true } };
      const config = parseConfig(params.config);
      if (params.leaseMetadata?.shape !== config.shape || params.leaseMetadata.rootfs !== config.rootfs || params.leaseMetadata.region !== config.region) {
        return { providerLeaseId: null, metadata: { expired: true } };
      }
      const client = new CreateosClient(config);
      const signal = AbortSignal.timeout(config.timeoutMs);
      try {
        const sandbox = await client.getSandbox(params.providerLeaseId, signal);
        if (["destroyed", "failed"].includes(sandbox.status!)) return { providerLeaseId: null, metadata: { expired: true } };
        await client.transition(params.providerLeaseId, "running", signal);
        const data = await client.json(`/sandboxes/${params.providerLeaseId}/exec`, "POST", {
          cmd: "/bin/cat", args: [MARKER],
        }, signal);
        const result = object(data.result);
        if (result.exit_code !== 0 || result.stdout !== marker) return { providerLeaseId: null, metadata: { expired: true } };
        return { providerLeaseId: params.providerLeaseId, metadata: { ...params.leaseMetadata, resumedLease: true } };

View on GitHub (pinned to 3f1d897a7c)