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
- Retry the read; a stable attachment will pass the second authorization unchanged.
- Identify what rewrote the attachment (re-upload, migration) and serialize it against run reads.
- If attachments are immutable by design, fix the writer that mutated objectKey/sha256 in place.
- 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
- Treat stored attachments as immutable; never overwrite objectKey/sha256 in place.
- Serialize attachment replacement against run reads.
- Re-run list_chat_attachments after a change error before retrying.
- Alert on any pipeline that mutates stored objects during runs.
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
- paperclip_runner_chat_attachment_storage_mismatch
- ACPX runtime executable could not be opened as a no-follow…
- ACPX runtime executable digest mismatch
- ACPX package directory changed during snapshot
- ACPX package directory changed while snapshotting
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)