paperclipai/paperclip · error
Photon attachment metadata changed
Error message
Photon attachment metadata changed
What it means
When the download stream emits its header part, its metadata must match expectations exactly: it must be the first header, part.info.guid must equal locator.attachmentGuid (or match a validated shared-gateway alias), the part must not be hidden/sticker, and totalBytes must be a safe in-range integer. Any mismatch throws this error (server/src/services/photon/attachments.ts:139), because the streamed bytes would not correspond to the attachment requested.
Solutions
- Re-fetch the message metadata and rebuild the locator so the attachmentGuid matches what the stream header reports.
- If using allocation "shared", confirm the spc-att-* alias and that fileName/mimeType/totalBytes still match the message attachment metadata; otherwise use allocation "dedicated".
- Upgrade/align the Photon gateway version so header GUIDs are native UUIDs consistent with message metadata.
- Do not reuse a partially consumed download stream; open a fresh client.attachments.downloadStream per attempt.
Defensive patterns
Strategy: retry
Validate before calling
const att = (message.raw as PhotonMessage).content.attachments.find(a => a.guid === locator.attachmentGuid);
if (!att) throw new Error("no such attachment; header match will fail"); Try / catch
try {
await downloadPhotonAttachment(client, lineId, locator);
} catch (e) {
if (e instanceof Error && e.message === "Photon attachment metadata changed") {
// re-fetch message metadata, rebuild locator, retry once; else mark unavailable
} else throw e;
} Prevention
- Refresh message metadata immediately before downloading so header info matches
- Use dedicated allocation unless the shared alias fields are known to be stable
- Never reuse a partially consumed download stream
When it happens
Trigger: A second header part arrives (header already true); stream header guid differs from locator.attachmentGuid without a valid shared alias match (spc-att-* alias plus matching native UUID, totalBytes, mimeType, fileName); header part is hidden/sticker; header totalBytes is invalid or over the cap.
Common situations: Using a shared-gateway alias attachmentGuid where the gateway rewrote IDs and the alias-validation fields (mimeType/fileName/totalBytes) changed after the message lookup; gateway version mismatch emitting rewritten GUIDs in headers; retrying a stream that already delivered a header on a stale stream object.
Related errors
- Photon attachment header is missing
- Attachment exceeds the configured size limit
- attachment_not_ready
- Completion cites no registered attachment on this task. Use…
- CreateOS execution requires a lease from this environment.
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18).
Data as JSON: /api/errors/0e9331ef4fce3fa0.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/services/photon/attachments.ts:139
// The shared gateway rewrites message/metadata attachment IDs to opaque
// project aliases, but streams the native UUID in download headers.
// Ownership comes from the authenticated source-message lookup above
// and this exact alias-addressed RPC, never from matching filenames.
const sharedAlias = allocation === "shared" &&
/^spc-att-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(locator.attachmentGuid);
const matchingSharedHeader = sharedAlias &&
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(part.info.guid) &&
part.info.totalBytes === attachment.totalBytes &&
part.info.mimeType === attachment.mimeType &&
part.info.fileName === attachment.fileName;
if (
header ||
(part.info.guid !== locator.attachmentGuid && !matchingSharedHeader) ||
part.info.isHidden || part.info.isSticker ||
!Number.isSafeInteger(part.info.totalBytes) || part.info.totalBytes < 0 ||
part.info.totalBytes > MAX_ATTACHMENT_BYTES
)
throw new Error("Photon attachment metadata changed");
header = true;
companionInfo = part.companionInfo;
companionUnavailable = Boolean(
companionInfo &&
(companionInfo.kind !== "live-photo-video" ||
!["video/quicktime", "video/mp4"].includes(
companionInfo.mimeType,
) ||
!Number.isSafeInteger(companionInfo.totalBytes) ||
companionInfo.totalBytes <= 0 ||
companionInfo.totalBytes > MAX_ATTACHMENT_BYTES),
);
} else if (part.type === "primaryChunk") {
if (companionStarted)
throw new Error("Photon attachment chunks arrived out of order");
if (!header) throw new Error("Photon attachment header is missing");
length += part.data.length;
if (length > MAX_ATTACHMENT_BYTES)View on GitHub (pinned to 3f1d897a7c)