{"record":{"id":"c6cd9b6e09a4aee1","repo":"paperclipai/paperclip","slug":"bridge-envelope-changed-while-reading","errorCode":null,"errorMessage":"Bridge envelope changed while reading.","messagePattern":"Bridge envelope changed while reading\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/adapter-utils/src/sandbox-callback-bridge.ts","lineNumber":541,"sourceCode":"        .filter((entry) => entry.isFile() && entry.name.endsWith(\".json\"))\n        .map((entry) => entry.name)\n        .sort((left, right) => left.localeCompare(right));\n    },\n    fileSize: async (remotePath) => (await fs.stat(remotePath)).size,\n    readTextFile: async (remotePath, maxBytes) => {\n      if (maxBytes === undefined) return fs.readFile(remotePath, \"utf8\");\n      const file = await fs.open(remotePath, \"r\");\n      try {\n        const stat = await file.stat();\n        if (stat.size > maxBytes) throw new Error(\"Bridge envelope exceeded the configured size limit.\");\n        const bytes = Buffer.alloc(Math.min(stat.size, maxBytes) + 1);\n        let length = 0;\n        while (length < bytes.length) {\n          const read = await file.read(bytes, length, bytes.length - length, length);\n          if (!read.bytesRead) break;\n          length += read.bytesRead;\n        }\n        if (length > stat.size) throw new Error(\"Bridge envelope changed while reading.\");\n        return bytes.subarray(0, length).toString(\"utf8\");\n      } finally { await file.close(); }\n    },\n    writeTextFile: async (remotePath, body) => {\n      await fs.mkdir(path.posix.dirname(remotePath), { recursive: true });\n      // Write to a temporary path that does NOT end in `.json`, then rename it\n      // onto the final `.json` path. A direct `writeFile` truncates the final\n      // path first, so a `.json`-only reader (the stdin poller) can see an\n      // empty or partial file. The atomic rename never exposes partial content.\n      const tempPath = `${remotePath}.paperclip-upload.decoded`;\n      await fs.writeFile(tempPath, body, \"utf8\");\n      await fs.rename(tempPath, remotePath);\n    },\n    writeResponseFile: async (responsePath, body, options = {}) => {\n      const responseDir = path.posix.dirname(responsePath);\n      const tempPath = `${responsePath}.tmp`;\n      const lockDir = `${responsePath}.paperclip-write.lock`;\n      const lockPidFile = `${lockDir}/pid`;","sourceCodeStart":523,"sourceCodeEnd":559,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/packages/adapter-utils/src/sandbox-callback-bridge.ts#L523-L559","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before (non-atomic, races with readers)\nawait fs.writeFile(requestPath, envelope, \"utf8\");\n// after (atomic rename, as the bridge expects)\nconst tempPath = `${requestPath}.paperclip-upload.decoded`;\nawait fs.writeFile(tempPath, envelope, \"utf8\");\nawait fs.rename(tempPath, requestPath);","handlingStrategy":"retry","validationCode":"const sizeBefore = await client.fileSize(requestPath);\nconst raw = await client.readTextFile(requestPath, sizeBefore);\nif ((await client.fileSize(requestPath)) !== sizeBefore) throw new Error(\"envelope changed, retry\");","typeGuard":null,"tryCatchPattern":"try {\n  raw = await client.readTextFile(requestPath, limit);\n} catch (error) {\n  if ((error as Error).message === \"Bridge envelope changed while reading.\") {\n    await sleep(50);\n    return readEnvelope(requestPath, limit); // bounded retry; writer likely mid-rename\n  }\n  throw error;\n}","preventionTips":["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."],"tags":["concurrency","race-condition","file-io","ipc"],"backgroundTag":"internal-invariant-violation","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-22T11:17:16.035Z"}