paperclipai/paperclip · error

paperclip_runner_chat_attachment_read_source_changed

paperclip_runner_chat_attachment_read_source_changed

Error message

paperclip_runner_chat_attachment_read_source_changed

What it means

The scope reads bytes, then re-runs authorization and compares the freshly authorized source (objectKey, sha256, byteSize, contentType) against the source used for the read. If any field differs — i.e. the stored attachment changed between the first authorization and the committed revalidation — the staged content is considered untrustworthy and this error is thrown instead of publishing a path.

Solutions

  1. Retry the read; a stable attachment will pass the second authorization unchanged.
  2. Identify what rewrote the attachment (re-upload, migration) and serialize it against run reads.
  3. If attachments are immutable by design, fix the writer that mutated objectKey/sha256 in place.
  4. Surface this as 'attachment changed during read; please re-select' in agent-facing UX.

Example fix

// before
const file = await scope.read(input); // throws if source mutated mid-read
// after
let file;
for (let i = 0; i < 3; i++) {
  try { file = await scope.read(input); break; }
  catch (e) {
    if (e.message !== "paperclip_runner_chat_attachment_read_source_changed") throw e;
  }
}
Defensive patterns

Strategy: retry

Type guard

function isSourceChangedError(e: unknown): boolean {
  return e instanceof Error && e.message === "paperclip_runner_chat_attachment_read_source_changed";
}

Try / catch

try {
  return await scope.read(input);
} catch (e) {
  if (isSourceChangedError(e)) {
    return { status: "attachment_changed" }; // re-list and re-read, don't trust old bytes
  }
  throw e;
}

Prevention

When it happens

Trigger: The same attachmentId was re-uploaded/overwritten in storage (new objectKey or hash) while the read was in flight; byteSize or contentType metadata updated between the two authorizations; a migration or re-ingest rewrote the object mid-read.

Common situations: Users editing/replacing attachments while an agent run is reading them; background jobs re-encrypting or re-hashing stored objects; TOCTOU races under active chat mutation.

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/0233b810846d5b5c. Report an issue: GitHub.

Appendix: source

Thrown at server/src/services/native-runtime/chat-attachment-read.ts:167

        allowEmpty: true,
        ...input,
      });
      this.#assertOpen();
      return source;
    });
  }

  async #read(input: { sourceCommentId: string; attachmentId: string }) {
    const source = await this.#authorized(input);
    const body = await this.#bytes(source);
    const current = await this.#authorized(input);
    if (
      current.objectKey !== source.objectKey ||
      current.sha256 !== source.sha256 ||
      current.byteSize !== source.byteSize ||
      current.contentType !== source.contentType
    ) {
      throw new Error("paperclip_runner_chat_attachment_read_source_changed");
    }
    // The committed revalidation admits these exact verified bytes. Never
    // hold issue/endpoint/principal locks over filesystem work (including
    // descriptor inspection and fsync), which can delay inbound admission.
    this.#assertOpen();
    const staged = await stageNativeRunnerAttachmentBytes({
      workspaceRoot: this.options.workspaceRoot,
      body,
    });
    this.#cleanups.push(staged.cleanup);
    try {
      // Cancellation during asynchronous staging must never publish a path.
      this.#assertOpen();
      return {
        sourceCommentId: source.sourceCommentId,
        attachmentId: source.attachmentId,
        filename: current.filename,
        contentType: current.contentType,

View on GitHub (pinned to 3f1d897a7c)