paperclipai/paperclip · error

CreateOS process stream ended without an exit status.

Error message

CreateOS process stream ended without an exit status.

What it means

execute() reconnects to the process event stream on network read failures (TypeError) to resume from the last sequence number. If it exhausts more than 3 reconnect attempts without ever seeing an exit/terminal event, it concludes the stream ended without a status and throws this error. The caller cannot know the process exit code.

Solutions

  1. Re-run the command; the processId-based reconnect already retried 3 times.
  2. Check sandbox lease health and recreate the lease if the sandbox is gone.
  3. Increase network reliability or the reconnect budget if runs are long-lived over unstable links.
  4. Query the process status out-of-band (GET /processes/:id) if the API offers it, to recover the exit code.
Defensive patterns

Strategy: retry

Try / catch

try {
  return await execute(lease, cmd, { signal });
} catch (e) {
  if (e.message === "CreateOS process stream ended without an exit status.") {
    // verify lease health, then retry once on a fresh lease
    await ensureLeaseHealthy(lease);
    return execute(freshLease(), cmd, { signal });
  }
  throw e;
}

Prevention

When it happens

Trigger: More than 3 reconnect cycles (each preceded by a 250ms delay) end without the server delivering a terminal exit event — e.g. persistent network failure, the sandbox dying before emitting exit, or the stream closing cleanly but prematurely.

Common situations: Flaky network between host and sandbox; sandbox VM terminated by infra while process ran; CreateOS server restarting and dropping stream history; long-running process outliving stream retention.

Related errors


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

Appendix: source

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

              exitCode: typeof exitCode === "number" ? exitCode : null,
              signal: typeof exitSignal === "string" ? exitSignal : null,
              timedOut: false, stdout: output.stdout, stderr: output.stderr,
              metadata: { processId, outputTruncated: output.truncated },
            };
          } 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.

View on GitHub (pinned to 3f1d897a7c)