paperclipai/paperclip · error

Bridge response envelope changed while reading.

Error message

Bridge response envelope changed while reading.

What it means

The response file can change between the stat and the read loop (the host is still writing, or another process replaced it). After reading, if the bytes read do not equal the originally statted size, the bridge assumes the envelope is inconsistent and refuses to parse it, since a partial or rewritten JSON envelope would be corrupt.

Solutions

  1. Retry the bridge request; the stale file is removed and a fresh response is written.
  2. Ensure only one host process serves the shared bridge directory.
  3. Fix the host to write responses atomically (write to temp file, rename into place).
  4. Verify no crash/interruption truncated the host's write; check host logs.
Defensive patterns

Strategy: retry

Try / catch

try { return await waitForResponse(id); } catch (e) {
  if (e.message.includes("changed while reading")) { await sleep(pollIntervalMs); return waitForResponse(id); }
  throw e;
}

Prevention

When it happens

Trigger: In waitForResponse, the read loop accumulates length !== stat.size for the response file at <responsesDir>/<requestId>.json.

Common situations: Host still mid-write when the sandbox opens the file (write-then-rename not used); two hosts sharing one responses directory; filesystem where append partially completed (crash during write).

Understand the failure class

Background: "failed to read file", EACCES, ENOENT and "could not read <path>" errors: when a program can't read a file from disk — this error's family across 49 libraries.

Related errors


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

Appendix: source

Thrown at packages/adapter-utils/src/sandbox-callback-bridge.ts:2428

    const deadline = Date.now() + responseTimeoutMs;
    while (Date.now() < deadline) {
      const handle = await fs.open(responsePath, "r").catch(error => {
        if (error.code === "ENOENT") return null;
        throw error;
      });
      if (handle) {
        try {
          const stat = await handle.stat();
          if (stat.size > sandboxBridgeEnvelopeLimit(maxBodyBytes)) throw new Error("Bridge response envelope exceeded the configured size limit.");
          reserveResponse(6 * stat.size + 1);
          const bytes = Buffer.alloc(stat.size + 1);
          let length = 0;
          while (length < bytes.length) {
            const read = await handle.read(bytes, length, bytes.length - length, length);
            if (!read.bytesRead) break;
            length += read.bytesRead;
          }
          if (length !== stat.size) throw new Error("Bridge response envelope changed while reading.");
          return JSON.parse(bytes.subarray(0, length).toString("utf8"));
        } finally {
          await handle.close();
          await fs.rm(responsePath, { force: true }).catch(() => undefined);
        }
      }
      await sleep(pollIntervalMs);
    }
    throw new Error("Timed out waiting for host bridge response.");
  }

  const server = createServer(async (req, res) => {
    // readBodyBytes reserves the body's bytes against the process ledger and
    // hands back a release function; this holds it so the finally below
    // releases those bytes exactly once no matter how this handler ends —
    // its normal completion, a thrown error, a client abort, or a deadline
    // timeout all reach the same finally.
    let releaseBodyReservation = null;

View on GitHub (pinned to 3f1d897a7c)