{"record":{"id":"b3e9bcd3880030e9","repo":"paperclipai/paperclip","slug":"paperclip-runner-chat-attachment-source-size-mismatch","errorCode":"paperclip_runner_chat_attachment_source_size_mismatch","errorMessage":"paperclip_runner_chat_attachment_source_size_mismatch","messagePattern":"paperclip_runner_chat_attachment_source_size_mismatch","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/src/services/native-runtime/chat-attachment-reuse.ts","lineNumber":1302,"sourceCode":"    object = await Promise.race([objectPromise, acquisitionTimeout]);\n  } finally {\n    clearTimeout(acquisitionTimer);\n  }\n  const timeout = setTimeout(() => {\n    object.stream.destroy(\n      new Error(\"paperclip_runner_chat_attachment_source_read_timed_out\"),\n    );\n  }, timeoutMs);\n  timeout.unref?.();\n  const chunks: Buffer[] = [];\n  let total = 0;\n  try {\n    for await (const chunk of object.stream) {\n      const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);\n      total += buffer.length;\n      if (total > source.byteSize || total > MAX_ATTACHMENT_BYTES) {\n        object.stream.destroy();\n        throw new Error(\n          \"paperclip_runner_chat_attachment_source_size_mismatch\",\n        );\n      }\n      chunks.push(buffer);\n    }\n  } finally {\n    clearTimeout(timeout);\n  }\n  const body = Buffer.concat(chunks);\n  if (\n    body.length !== source.byteSize ||\n    createHash(\"sha256\").update(body).digest(\"hex\") !==\n      source.sha256.toLowerCase()\n  ) {\n    if (!object.stream.destroyed) {\n      object.stream.destroy();\n    }\n    throw new Error(","sourceCodeStart":1284,"sourceCodeEnd":1320,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/native-runtime/chat-attachment-reuse.ts#L1284-L1320","documentation":"This error is thrown by readSourceBytes in chat-attachment-reuse.ts while streaming a source attachment object from storage before re-binding it to a chat/native run. As bytes are read, a running total is compared against the declared source.byteSize and the hard cap MAX_ATTACHMENT_BYTES; the moment the streamed total exceeds either limit, the stream is destroyed and this error is thrown. It exists to prevent trusting a storage object whose actual size diverges from its declared metadata (or an oversized attachment) from being silently reused.","triggerScenarios":"Calling prepareReusedChatAttachment where storage.getObject(companyId, source.objectKey) returns a stream longer than source.byteSize (stale/mutated object under the same key), or where the object exceeds MAX_ATTACHMENT_BYTES (e.g. a metadata record with a wrong/undersized byteSize field, or an object overwritten with larger content after the source record was created).","commonSituations":"A storage backend (S3/local) object was re-uploaded or appended after the attachment source record was snapshotted; the source record's byteSize was computed at a different point than the object write (partial write followed by full write); a caller hand-crafted a ChatAttachmentReuseSource with an incorrect byteSize; or an oversized file slipped into storage bypassing upload validation.","solutions":["Verify source.byteSize matches the actual stored object (head the object / stat the file) and fix or regenerate the source record","Re-upload the source attachment so byteSize and content are consistent, then retry prepareReusedChatAttachment","Check whether anything wrote to the same objectKey after the source record was created (key collisions across namespaces); use a unique key","Confirm the object is within MAX_ATTACHMENT_BYTES; if larger, reject it upstream before attempting reuse"],"exampleFix":"// before (caller passes stale metadata)\nawait prepareReusedChatAttachment({ db, binding, source: { ...src, byteSize: 1024 }, title });\n// after (re-read actual size before reuse)\nconst head = await storage.headObject(binding.companyId, src.objectKey);\nawait prepareReusedChatAttachment({ db, binding, source: { ...src, byteSize: head.byteSize }, title });","handlingStrategy":"validation","validationCode":"const head = await storage.headObject(companyId, source.objectKey);\nif (head.byteSize !== source.byteSize) throw new Error(\"source byteSize stale; refresh source record\");\nif (head.byteSize > MAX_ATTACHMENT_BYTES) throw new Error(\"source object exceeds attachment cap\");","typeGuard":"function isReusableSource(s) {\n  return typeof s.byteSize === \"number\" && Number.isFinite(s.byteSize) && s.byteSize > 0 && s.byteSize <= MAX_ATTACHMENT_BYTES && typeof s.sha256 === \"string\" && /^[0-9a-f]{64}$/.test(s.sha256);\n}","tryCatchPattern":null,"preventionTips":["Always derive byteSize from the same write that stored the object, never from a stale snapshot","Use write-once object keys so an object cannot grow after its metadata is recorded","Validate attachment size against MAX_ATTACHMENT_BYTES at upload time","Treat any source-size mismatch as evidence of a metadata/storage divergence and regenerate the source record"],"tags":["storage","integrity","attachments","size-limit"],"backgroundTag":"file-size-limit-exceeded","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}