paperclipai/paperclip · error

CreateOS returned an unsuccessful response.

Error message

CreateOS returned an unsuccessful response.

What it means

Thrown by `json()` when the response parsed successfully as an object but its `status` field is not exactly the string `"success"`. CreateOS wraps every payload in a `{status, data}` envelope; any other `status` (e.g. `"error"`, `"failed"`, or a missing field) means the provider acknowledged the request but reported it as not successful. The error is intentionally opaque — provider failure details could leak private data, so check the provider's own dashboard/logs.

Solutions

  1. Call `getSandbox(id)` to inspect the sandbox `status` — an error/failed state usually explains the rejection.
  2. Confirm the requested operation is legal for the sandbox's current state (only running sandboxes can pause; only paused/error can resume).
  3. Check the CreateOS dashboard/provider logs for the sandbox to see the underlying failure reason.
  4. Verify the provider API version hasn't changed the envelope `status` semantics or field name.
  5. For destroySandbox, note 404s are already tolerated; other failures during cleanup can be safely retried as destroy is idempotent.

Example fix

// before: treating the failure as transient
await client.transition(id, "running", signal); // may loop throwing
// after: check sandbox state and only transition when eligible
const sandbox = await client.getSandbox(id);
if (sandbox.status === "error") {
  throw new Error(`Sandbox ${id} is in error state; recreate it instead of resuming.`);
}
await client.transition(id, "running", signal);
Defensive patterns

Strategy: try-catch

Validate before calling

const sandbox = await client.getSandbox(id);
const canResume = sandbox.status === "paused" || sandbox.status === "error";
if (!canResume) throw new Error(`Sandbox ${id} (${sandbox.status}) cannot be resumed.`);

Try / catch

try {
  await client.json(`/sandboxes/${id}/resume`, "POST", undefined, signal);
} catch (e) {
  if (e instanceof Error && e.message === "CreateOS returned an unsuccessful response.") {
    // check getSandbox status; recreate the sandbox if it is in error state
  } else throw e;
}

Prevention

When it happens

Trigger: Any `json()` call (getSandbox, createSandbox, destroySandbox, transition's resume/pause POST) where the provider responds 200 with `envelope.status` other than "success" — e.g. the sandbox entered an error state, an operation was rejected at the application level, or the envelope schema changed and `status` was renamed or removed.

Common situations: Requesting a transition on a sandbox that cannot perform it (e.g. pause on an already-paused sandbox), provider-side quota/limit rejection that still returns HTTP 200, provider API version change altering the envelope, hitting a compatible-but-different sandbox service with a different envelope convention.

Related errors


AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18). Data as JSON: /api/errors/94aeecdcf3bc2a98. Report an issue: GitHub.

Appendix: source

Thrown at packages/plugins/sandbox-providers/createos/src/client.ts:80

        : path.includes("/connect?") ? "output connection"
        : path.endsWith("/processes") ? "process creation"
        : path.includes("/processes/") ? "process cleanup"
        : path.endsWith("/exec") ? "workspace command"
        : "sandbox lifecycle";
      throw new CreateosApiError(response.status, operation);
    }
    return response;
  }

  async json(path: string, method = "GET", body?: unknown, signal?: AbortSignal): Promise<Record<string, unknown>> {
    const response = await this.request(path, {
      method,
      ...(body !== undefined ? { body: JSON.stringify(body), headers: { "Content-Type": "application/json" } } : {}),
      signal,
    });
    let envelope: Record<string, unknown>;
    try { envelope = object(await response.json()); } catch { throw new Error("CreateOS returned invalid JSON."); }
    if (envelope.status !== "success") throw new Error("CreateOS returned an unsuccessful response.");
    return object(envelope.data);
  }

  async getSandbox(id: string, signal?: AbortSignal): Promise<Sandbox> {
    const data = await this.json(`/sandboxes/${identifier(id)}`, "GET", undefined, signal);
    if (data.id !== id || typeof data.status !== "string") throw new Error("CreateOS sandbox identity or state is invalid.");
    return { id, status: data.status };
  }

  async createSandbox(signal: AbortSignal): Promise<Sandbox> {
    const { shape, rootfs, region } = this.config;
    const data = await this.json("/sandboxes", "POST", {
      shape,
      ...(rootfs ? { rootfs } : {}),
      ...(region ? { region } : {}),
      ingress_enabled: false,
      // The host owns lease release. Idle pause is not a command timeout or a
      // guaranteed expiry, and could suspend a quiet active agent.

View on GitHub (pinned to 3f1d897a7c)