paperclipai/paperclip · error
Photon attachment belongs to another line
Error message
Photon attachment belongs to another line
What it means
downloadPhotonAttachment is called with a lineId argument and a locator object that itself carries a lineId. This guard (server/src/services/photon/attachments.ts:77) throws when the two disagree, i.e. the caller is trying to download an attachment attributed to a different chat line than the one it is operating on. It is a tenant/ownership cross-check that prevents cross-line attachment fetching.
Solutions
- Check the locator you pass: ensure locator.lineId equals the lineId argument in the same call.
- Re-derive the locator with photonAttachmentLocator(attachment, lineId, chatGuid, message) for the current line instead of reusing a stored locator.
- If the attachment truly belongs to another line, resolve that line's client and call with the correct lineId.
- Verify stored fetchMetadata was not migrated/copied across lines without updating lineId.
Example fix
// before
await downloadPhotonAttachment(client, currentLineId, staleLocator);
// after
if (staleLocator.lineId !== currentLineId) {
locator = photonAttachmentLocator(attachment, currentLineId, chatGuid, message);
}
await downloadPhotonAttachment(client, currentLineId, locator); Defensive patterns
Strategy: validation
Validate before calling
function canDownload(locator: PhotonAttachmentLocator, lineId: string): boolean {
return photonAttachmentLocatorSchema.safeParse(locator).success && locator.lineId === lineId;
} Type guard
function isForLine(locator: PhotonAttachmentLocator, lineId: string): locator is PhotonAttachmentLocator & { lineId: typeof lineId } {
return locator.lineId === lineId;
} Try / catch
try {
await downloadPhotonAttachment(client, lineId, locator);
} catch (e) {
if (e instanceof Error && e.message === "Photon attachment belongs to another line") {
// rebuild locator for this line or fetch via the correct line's client
} else throw e;
} Prevention
- Always derive locators via photonAttachmentLocator() with the exact lineId/chatGuid/message in scope
- Never store and reuse locators across lines without re-validating lineId
- Keep lineId on the locator and the call argument from the same source object
When it happens
Trigger: Calling downloadPhotonAttachment(client, lineIdA, locator) where locator.lineId === lineIdB (locator.lineId !== lineId). Any caller that reuses a cached locator with the wrong line context triggers it.
Common situations: A caller fetched locators for multiple lines and mixed them up; an attachment row was re-parented or copied to another line without regenerating fetchMetadata; recovery/rehydration code passes a stale lineId after a line was re-created.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- Photon attachment does not belong to this message and chat
- Attachment exceeds the configured size limit
- attachment_not_ready
- Completion cites no registered attachment on this task. Use…
- External chat attachment ids must be UUIDs
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18).
Data as JSON: /api/errors/b18b38b8a9fb68b3.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/services/photon/attachments.ts:78
const raw = message.raw as PhotonMessage;
if (
!raw.chatGuids?.includes(chatGuid) ||
!raw.content?.attachments.some(
(candidate) => candidate.guid === parsed.data.attachmentGuid,
)
)
return null;
return parsed.data;
}
export async function downloadPhotonAttachment(
client: GrpcAdvancedIMessage,
lineId: string,
locator: PhotonAttachmentLocator,
allocation: "dedicated" | "shared" = "dedicated",
): Promise<Buffer> {
photonAttachmentLocatorSchema.parse(locator);
if (locator.lineId !== lineId)
throw new Error("Photon attachment belongs to another line");
const source = await client.messages
.get(locator.messageGuid)
.catch((error) => {
throw photonFailure(error);
});
const attachment = source.content.attachments.find(
(candidate) => candidate.guid === locator.attachmentGuid,
);
if (
source.guid !== locator.messageGuid ||
!source.chatGuids.includes(locator.chatGuid) ||
!attachment ||
attachment.isSticker ||
attachment.isHidden
)
throw new Error(
"Photon attachment does not belong to this message and chat",
);View on GitHub (pinned to 3f1d897a7c)