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
- Call `getSandbox(id)` to inspect the sandbox `status` — an error/failed state usually explains the rejection.
- Confirm the requested operation is legal for the sandbox's current state (only running sandboxes can pause; only paused/error can resume).
- Check the CreateOS dashboard/provider logs for the sandbox to see the underlying failure reason.
- Verify the provider API version hasn't changed the envelope `status` semantics or field name.
- 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
- Check sandbox status via getSandbox() before issuing transitions.
- Model the provider's state machine (paused/running/error/creating/pausing/resuming) in your own code and gate operations on it.
- Track provider release notes for envelope `{status,data}` changes.
- Remember HTTP 200 does not imply success for this API — always rely on the envelope status, which json() already enforces.
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
- CreateOS returned an invalid resource ID.
- CreateOS returned an invalid response.
- CreateOS returned invalid JSON.
- queued_comment_revision_conflict
- ACPX session is not at a safe suspension point
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)