{"record":{"id":"b18b38b8a9fb68b3","repo":"paperclipai/paperclip","slug":"photon-attachment-belongs-to-another-line","errorCode":null,"errorMessage":"Photon attachment belongs to another line","messagePattern":"Photon attachment belongs to another line","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/src/services/photon/attachments.ts","lineNumber":78,"sourceCode":"  const raw = message.raw as PhotonMessage;\n  if (\n    !raw.chatGuids?.includes(chatGuid) ||\n    !raw.content?.attachments.some(\n      (candidate) => candidate.guid === parsed.data.attachmentGuid,\n    )\n  )\n    return null;\n  return parsed.data;\n}\nexport async function downloadPhotonAttachment(\n  client: GrpcAdvancedIMessage,\n  lineId: string,\n  locator: PhotonAttachmentLocator,\n  allocation: \"dedicated\" | \"shared\" = \"dedicated\",\n): Promise<Buffer> {\n  photonAttachmentLocatorSchema.parse(locator);\n  if (locator.lineId !== lineId)\n    throw new Error(\"Photon attachment belongs to another line\");\n  const source = await client.messages\n    .get(locator.messageGuid)\n    .catch((error) => {\n      throw photonFailure(error);\n    });\n  const attachment = source.content.attachments.find(\n    (candidate) => candidate.guid === locator.attachmentGuid,\n  );\n  if (\n    source.guid !== locator.messageGuid ||\n    !source.chatGuids.includes(locator.chatGuid) ||\n    !attachment ||\n    attachment.isSticker ||\n    attachment.isHidden\n  )\n    throw new Error(\n      \"Photon attachment does not belong to this message and chat\",\n    );","sourceCodeStart":60,"sourceCodeEnd":96,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/photon/attachments.ts#L60-L96","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nawait downloadPhotonAttachment(client, currentLineId, staleLocator);\n// after\nif (staleLocator.lineId !== currentLineId) {\n  locator = photonAttachmentLocator(attachment, currentLineId, chatGuid, message);\n}\nawait downloadPhotonAttachment(client, currentLineId, locator);","handlingStrategy":"validation","validationCode":"function canDownload(locator: PhotonAttachmentLocator, lineId: string): boolean {\n  return photonAttachmentLocatorSchema.safeParse(locator).success && locator.lineId === lineId;\n}","typeGuard":"function isForLine(locator: PhotonAttachmentLocator, lineId: string): locator is PhotonAttachmentLocator & { lineId: typeof lineId } {\n  return locator.lineId === lineId;\n}","tryCatchPattern":"try {\n  await downloadPhotonAttachment(client, lineId, locator);\n} catch (e) {\n  if (e instanceof Error && e.message === \"Photon attachment belongs to another line\") {\n    // rebuild locator for this line or fetch via the correct line's client\n  } else throw e;\n}","preventionTips":["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"],"tags":["ownership-check","attachments","line-scoping"],"backgroundTag":"invalid-argument-value","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}