paperclipai/paperclip · error

CreateOS returned an empty process stream.

Error message

CreateOS returned an empty process stream.

What it means

events() consumes the NDJSON process stream from the CreateOS execution HTTP response. If response.body is null/undefined the plugin has no stream to read, so it throws instead of silently yielding nothing. This normally indicates the HTTP layer produced a response without a body, which the plugin never expects on a successful execution request.

Solutions

  1. Verify the CreateOS API URL points at the real streaming execution endpoint (correct origin and /v1 path).
  2. Run on a fetch implementation that supports streaming response bodies (native undici/Node 18+ fetch), not a stubbed polyfill.
  3. Check proxies/middleware between the plugin and CreateOS that might return an empty body for streamed endpoints.
  4. In tests, mock fetch with a Response containing a readable body, e.g. new Response('"{}"\n', { status: 200 }).

Example fix

// before (test mock)
global.fetch = async () => new Response(null, { status: 200 });
// after
global.fetch = async () => new Response(JSON.stringify({ type: "exit", code: 0 }) + "\n", { status: 200 });
Defensive patterns

Strategy: try-catch

Type guard

function hasBody(res) { return res != null && res.body != null; }

Try / catch

try {
  for await (const ev of plugin.execute(params)) { /* ... */ }
} catch (e) {
  if (e.message.includes('empty process stream')) {
    throw new Error('Execution response had no body; check the API URL/endpoint and your fetch runtime: ' + e.message);
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling execute when the fetch Response for the process stream has body === null — e.g. the runtime returned an opaque/buffered response, a service-worker interception stripped the body, or the client was constructed in an environment without streaming body support.

Common situations: Running the plugin in a non-Node fetch polyfill that doesn't expose response.body; a proxy returning 204/empty responses for the stream endpoint; fetch mocked in tests without a body.

Related errors


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

Appendix: source

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

}

function commandScript(params: PluginEnvironmentExecuteParams, stdinPath: string | null): string {
  if (!params.command) throw new Error("A sandbox command is required.");
  const env = Object.entries(params.env ?? {}).map(([key, value]) => {
    if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || typeof value !== "string") {
      throw new Error("Invalid sandbox environment variable.");
    }
    return `${key}=${shellQuote(value)}`;
  });
  const command = [params.command, ...(params.args ?? [])].map(shellQuote).join(" ");
  return [
    params.cwd ? `cd -- ${shellQuote(params.cwd)} || exit` : "",
    `exec env ${env.join(" ")} ${command}${stdinPath ? ` < ${shellQuote(stdinPath)}` : ""}`,
  ].filter(Boolean).join("\n");
}

async function* events(response: Response): AsyncGenerator<Record<string, unknown>> {
  if (!response.body) throw new Error("CreateOS returned an empty process stream.");
  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let pending = "";
  try {
    for (;;) {
      const { value, done } = await reader.read();
      pending += done ? decoder.decode() : decoder.decode(value, { stream: true });
      let newline: number;
      while ((newline = pending.indexOf("\n")) >= 0) {
        const line = pending.slice(0, newline);
        pending = pending.slice(newline + 1);
        if (line.length > MAX_LINE_BYTES) throw new Error("CreateOS process frame is too large.");
        if (line.trim()) yield parseEvent(line);
      }
      if (pending.length > MAX_LINE_BYTES) throw new Error("CreateOS process frame is too large.");
      if (done) {
        if (pending.trim()) yield parseEvent(pending);
        return;

View on GitHub (pinned to 3f1d897a7c)