paperclipai/paperclip · error
Bridge envelope changed while reading.
Error message
Bridge envelope changed while reading.
What it means
During readTextFile in the file-system queue client, the file is read in a loop into a buffer sized from an initial stat(). If the accumulated bytes read (length) exceed the size recorded at stat time, the file grew (or was rewritten) between stat and read, and the client throws "Bridge envelope changed while reading." This guarantees the reader never returns a torn mixture of two different envelopes.
Solutions
- Ensure all producers write envelopes via the provided writeTextFile/writeResponseFile (temp file + atomic rename), never direct writeFile/append to the final `.json` path.
- Upgrade any older queue peer that writes directly to the final path so it uses the atomic temp+rename protocol.
- Use unique request file names per call so concurrent jobs never target the same path.
- If the error recurs on one file, delete it and let the producer resend; the file is likely corrupt.
- Retry the read once — a one-off race typically clears once the writer finishes.
Example fix
// before (non-atomic, races with readers)
await fs.writeFile(requestPath, envelope, "utf8");
// after (atomic rename, as the bridge expects)
const tempPath = `${requestPath}.paperclip-upload.decoded`;
await fs.writeFile(tempPath, envelope, "utf8");
await fs.rename(tempPath, requestPath); Defensive patterns
Strategy: retry
Validate before calling
const sizeBefore = await client.fileSize(requestPath);
const raw = await client.readTextFile(requestPath, sizeBefore);
if ((await client.fileSize(requestPath)) !== sizeBefore) throw new Error("envelope changed, retry"); Try / catch
try {
raw = await client.readTextFile(requestPath, limit);
} catch (error) {
if ((error as Error).message === "Bridge envelope changed while reading.") {
await sleep(50);
return readEnvelope(requestPath, limit); // bounded retry; writer likely mid-rename
}
throw error;
} Prevention
- Only write envelopes through the bridge's temp-file + atomic-rename helpers, never direct writeFile to the final .json path.
- Use unique request/response file names per call to avoid cross-job write races.
- Upgrade any older queue peers that write non-atomically to the final path.
- Never append to or edit an envelope file that a reader may already be consuming.
- If one specific file repeatedly triggers this, delete it — it is likely corrupt from a prior crash.
When it happens
Trigger: A producer rewrites or appends to the same `.json` request/response path while the queue client is mid-read — i.e. non-atomic writes (direct writeFile/truncate-then-write or `> file` redirect) instead of temp-file + atomic rename, so stat.size shrinks/changes relative to what is actually read.
Common situations: Two sandbox jobs race to write the same request file name; an old peer version writes envelopes non-atomically (bypassing the .paperclip-upload temp+rename path); a crash left a partially written file that is then appended to; external tooling (editor, log shipper) touched the file during the read window.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- Bridge response envelope changed while reading.
- Capability live turn admission was abandoned during teardown
- Capability live turn start completed after admission…
- CreateOS lease cleanup is already in progress.
- native_replacement_stopped_session_changed
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18).
Data as JSON: /api/errors/c6cd9b6e09a4aee1.
Report an issue: GitHub.
Appendix: source
Thrown at packages/adapter-utils/src/sandbox-callback-bridge.ts:541
.filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
.map((entry) => entry.name)
.sort((left, right) => left.localeCompare(right));
},
fileSize: async (remotePath) => (await fs.stat(remotePath)).size,
readTextFile: async (remotePath, maxBytes) => {
if (maxBytes === undefined) return fs.readFile(remotePath, "utf8");
const file = await fs.open(remotePath, "r");
try {
const stat = await file.stat();
if (stat.size > maxBytes) throw new Error("Bridge envelope exceeded the configured size limit.");
const bytes = Buffer.alloc(Math.min(stat.size, maxBytes) + 1);
let length = 0;
while (length < bytes.length) {
const read = await file.read(bytes, length, bytes.length - length, length);
if (!read.bytesRead) break;
length += read.bytesRead;
}
if (length > stat.size) throw new Error("Bridge envelope changed while reading.");
return bytes.subarray(0, length).toString("utf8");
} finally { await file.close(); }
},
writeTextFile: async (remotePath, body) => {
await fs.mkdir(path.posix.dirname(remotePath), { recursive: true });
// Write to a temporary path that does NOT end in `.json`, then rename it
// onto the final `.json` path. A direct `writeFile` truncates the final
// path first, so a `.json`-only reader (the stdin poller) can see an
// empty or partial file. The atomic rename never exposes partial content.
const tempPath = `${remotePath}.paperclip-upload.decoded`;
await fs.writeFile(tempPath, body, "utf8");
await fs.rename(tempPath, remotePath);
},
writeResponseFile: async (responsePath, body, options = {}) => {
const responseDir = path.posix.dirname(responsePath);
const tempPath = `${responsePath}.tmp`;
const lockDir = `${responsePath}.paperclip-write.lock`;
const lockPidFile = `${lockDir}/pid`;View on GitHub (pinned to 3f1d897a7c)