paperclipai/paperclip · error · CreateosCleanupError

CreateOS process creation could not be confirmed; destroy…

Error message

CreateOS process creation could not be confirmed; destroy the lease before reusing it.

What it means

CreateosCleanupError thrown when a failure occurs after the creation request was sent but before the server confirmed the processId. Because the process may actually have started, the provider cannot safely let the caller reuse the lease — a subsequent process could collide with an orphan. The lease must be destroyed before reuse.

Solutions

  1. Destroy the sandbox lease before using it again — do not blindly retry commands on the same lease.
  2. Check the lease/sandbox via the API for orphaned processes before re-provisioning.
  3. Add retry logic at a higher level that creates a fresh lease instead of reusing this one.
  4. Reduce timeoutMs churn: ensure client timeout is long enough for create round-trips.

Example fix

// before
await execute(lease, cmd);
// after
try {
  await execute(lease, cmd);
} catch (e) {
  if (e instanceof CreateosCleanupError) await destroyLease(lease);
  throw e;
}
Defensive patterns

Strategy: try-catch

Type guard

function isCreateosCleanupError(e) {
  return e instanceof CreateosCleanupError || e?.constructor?.name === "CreateosCleanupError";
}

Try / catch

try {
  await execute(lease, cmd, { signal });
} catch (e) {
  if (isCreateosCleanupError(e)) {
    await destroyLease(lease); // mandatory: process state unconfirmed
    throw e;
  }
  throw e;
}

Prevention

When it happens

Trigger: Any error (network failure, timeout, stream error) raised in execute() while creationMayHaveSucceeded is true and processId is still unset — i.e. the POST to create the process was sent, but the response with processId was never received.

Common situations: Connection dropped right after the create request; request timeout firing between send and response; server accepted the process but the response was lost.

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

Appendix: source

Thrown at packages/plugins/sandbox-providers/createos/src/execute.ts:197

          } else if (event.type === "error") {
            throw new Error(event.error === "output_offset_expired"
              ? "CreateOS process output was evicted before it could be read."
              : "CreateOS process stream reported an error.");
          } else {
            throw new Error("CreateOS returned an unknown process event.");
          }
        }
      } catch (error) {
        // Network read failures can resume from the last accepted sequence.
        // Protocol errors must fail closed rather than reconnect past bad data.
        if (!(error instanceof TypeError) || signal.aborted) throw error;
      }
      if (++reconnects > 3) throw new Error("CreateOS process stream ended without an exit status.");
      await delay(250, undefined, { signal });
    }
  } catch (error) {
    if (creationMayHaveSucceeded && !processId) {
      throw new CreateosCleanupError("CreateOS process creation could not be confirmed; destroy the lease before reusing it.");
    }
    if (signal.aborted && signal.reason?.name === "TimeoutError") {
      output.finish();
      return {
        exitCode: null, timedOut: true, stdout: output.stdout, stderr: output.stderr,
        metadata: { processId, outputTruncated: output.truncated },
      };
    }
    if (signal.aborted) throw new Error("CreateOS command was cancelled.");
    throw error;
  } finally {
    const cleanupSignal = AbortSignal.timeout(client.config.timeoutMs);
    // Do not hide a cleanup failure: the host must know containment is unproven.
    try {
      if (processId && !completed) {
        try {
          const termination = await client.json(`${base}/${processId}?grace_ms=1000`, "DELETE", undefined, cleanupSignal);
          if (termination.tree_exited !== true) throw new Error("Process tree has not exited.");

View on GitHub (pinned to 3f1d897a7c)