paperclipai/paperclip · error

paperclip_runner_chat_attachment_storage_mismatch

paperclip_runner_chat_attachment_storage_mismatch

Error message

paperclip_runner_chat_attachment_storage_mismatch

What it means

After re-uploading the source bytes to the issue namespace via putStorageObjectWithin, the code verifies the storage write result (stored.byteSize, stored.sha256, stored.contentType) against the verified body and the source metadata. If any field disagrees, this error is thrown and the new storage object is not linked as an attachment. It ensures the storage layer actually persisted exactly the bytes it was given with the intended content type.

Solutions

  1. Fix the StorageService.putFile implementation to report byteSize/sha256/contentType computed from the exact bytes it persisted
  2. Pass contentType exactly as stored (avoid backends silently rewriting it); if the backend normalizes, propagate its returned value consistently
  3. In tests, make mock putFile echo the real input body's length/hash/contentType instead of hardcoded values
  4. Log the stored vs expected values on mismatch to identify which field diverges before retrying

Example fix

// before (mock storage returning canned metadata)
async putFile(input) { return { provider: "local", objectKey: key, byteSize: 0, sha256: "", contentType: "application/octet-stream", ... }; }
// after
async putFile(input) {
  await write(key, input.body);
  return { provider: "local", objectKey: key, byteSize: input.body.length, sha256: createHash("sha256").update(input.body).digest("hex").toLowerCase(), contentType: input.contentType, originalFilename: input.originalFilename };
}
Defensive patterns

Strategy: validation

Validate before calling

const expectedSha = createHash("sha256").update(body).digest("hex").toLowerCase();
// after putFile returns `stored`:
if (stored.byteSize !== body.length || stored.sha256.toLowerCase() !== expectedSha || stored.contentType !== source.contentType)
  throw new Error("storage write metadata diverges from written bytes");

Try / catch

try {
  await prepareReusedChatAttachment({ db, binding, source, title });
} catch (err) {
  if (err.message === "paperclip_runner_chat_attachment_storage_mismatch") {
    // delete the orphaned stored object and retry with a corrected StorageService
    await storage.deleteObject(binding.companyId, candidateObjectKey).catch(() => undefined);
  } else throw err;
}

Prevention

When it happens

Trigger: storage.putFile returns metadata that disagrees with the write: byteSize != body.length, sha256 differs from input.source.sha256, or contentType differs from input.source.contentType — e.g. the storage service normalized/overrode the content type, a custom StorageService in tests returns canned metadata, or the putFile implementation reports pre-compression or multipart metrics.

Common situations: A custom or mock StorageService returning incorrect sha256/byteSize in its putFile result; a storage backend that rewrites content-type (e.g. defaults to application/octet-stream or appends charset); an adapter computing sha256 before transforming the body (compression/encryption) while metadata was compared against the raw source; case differences in contentType strings.

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


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

Appendix: source

Thrown at server/src/services/native-runtime/chat-attachment-reuse.ts:1450

  );
  const stored = await putStorageObjectWithin(
    storage,
    {
      companyId: input.binding.companyId,
      namespace: `issues/${input.binding.issueId}`,
      originalFilename: input.source.filename,
      contentType: input.source.contentType,
      body,
    },
    storageTimeoutMs,
  );
  try {
    if (
      stored.byteSize !== body.length ||
      stored.sha256.toLowerCase() !== input.source.sha256.toLowerCase() ||
      stored.contentType !== input.source.contentType
    ) {
      throw new Error("paperclip_runner_chat_attachment_storage_mismatch");
    }
    const attachment = await issueService(input.db).createAttachment({
      issueId: input.binding.issueId,
      provider: stored.provider,
      objectKey: stored.objectKey,
      contentType: stored.contentType,
      byteSize: stored.byteSize,
      sha256: stored.sha256,
      originalFilename: stored.originalFilename,
      createdByAgentId: input.binding.agentId,
      createdByRunId: input.binding.runId,
    });
    if (
      !attachment.artifactWorkProductId ||
      attachment.originatingRunId !== input.binding.runId
    ) {
      throw new Error("paperclip_runner_chat_attachment_origin_not_persisted");
    }

View on GitHub (pinned to 3f1d897a7c)