paperclipai/paperclip · error
paperclip_runner_chat_attachment_source_integrity_mismatch
paperclip_runner_chat_attachment_source_integrity_mismatch
Error message
paperclip_runner_chat_attachment_source_integrity_mismatch
What it means
After fully reading the source object in readSourceBytes, the buffer length and its SHA-256 digest are compared to source.byteSize and source.sha256. If either does not match, the stream is destroyed and this integrity error is thrown. It guards against reusing a storage object whose content no longer matches the snapshot metadata the reuse decision was based on.
Solutions
- Recompute the object's sha256 with `sha256sum` / crypto and update or regenerate the source record so its sha256/byteSize match storage
- Re-upload the attachment to a fresh objectKey and reference the new key in the source
- Normalize source.sha256 to lowercase hex before building the ChatAttachmentReuseSource (the comparison lowercases the stored value but the digest is lowercase hex)
- Audit for code paths that overwrite objects at existing keys and enforce write-once keys
Example fix
// before
source.sha256 = hash.toUpperCase();
// after
source.sha256 = createHash("sha256").update(body).digest("hex").toLowerCase(); Defensive patterns
Strategy: validation
Validate before calling
const hash = createHash("sha256").update(body).digest("hex").toLowerCase();
if (hash !== source.sha256.toLowerCase() || body.length !== source.byteSize) throw new Error("source object content diverges from recorded sha256/byteSize"); Type guard
function hasValidDigest(s) {
return typeof s.sha256 === "string" && /^[0-9a-f]{64}$/.test(s.sha256.toLowerCase()) && s.sha256 === s.sha256.toLowerCase();
} Try / catch
try {
await prepareReusedChatAttachment({ db, binding, source, title });
} catch (err) {
if (err.message === "paperclip_runner_chat_attachment_source_integrity_mismatch") {
// re-upload the source to a fresh key and rebuild the source record before retrying
} else throw err;
} Prevention
- Store sha256 as lowercase hex everywhere; normalize on ingest
- Re-verify object hash after any backup/restore or key migration
- Never overwrite objects in place; upload to a new key on content change
- Compute hashes from the exact bytes persisted, not pre-transformation input
When it happens
Trigger: storage.getObject returns content whose SHA-256 differs from source.sha256 (object mutated/replaced under the same key), whose length differs from source.byteSize (truncated write), or whose hash was stored with different casing than the computed lowercase hex (source.sha256 not lowercase), producing digest comparison failure.
Common situations: S3 eventual consistency or a failed prior overwrite left stale content at the key; a caller supplied a sha256 with uppercase characters since the code lowercases only the right-hand side; object key reuse across versions after a re-upload; database backup/restore mismatch between attachment metadata and object store contents.
Understand the failure class
Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.
Related errors
- paperclip_runner_chat_attachment_read_integrity_mismatch
- paperclip_runner_file_handoff_storage_mismatch
- ACPX runtime executable digest mismatch
- ACPX private snapshot digest mismatch
- ACPX snapshot manifest digest mismatch
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18).
Data as JSON: /api/errors/c8bf31b11226e358.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/services/native-runtime/chat-attachment-reuse.ts:1320
throw new Error(
"paperclip_runner_chat_attachment_source_size_mismatch",
);
}
chunks.push(buffer);
}
} finally {
clearTimeout(timeout);
}
const body = Buffer.concat(chunks);
if (
body.length !== source.byteSize ||
createHash("sha256").update(body).digest("hex") !==
source.sha256.toLowerCase()
) {
if (!object.stream.destroyed) {
object.stream.destroy();
}
throw new Error(
"paperclip_runner_chat_attachment_source_integrity_mismatch",
);
}
return body;
}
const DEFAULT_STORAGE_TIMEOUT_MS = 10_000;
async function deleteStorageObjectWithin(
storage: StorageService,
companyId: string,
objectKey: string,
timeoutMs: number,
): Promise<void> {
const deletion = storage
.deleteObject(companyId, objectKey)
.catch(() => undefined);
let timer: ReturnType<typeof setTimeout> | null = null;View on GitHub (pinned to 3f1d897a7c)