{"record":{"id":"b4df10084499afbc","repo":"paperclipai/paperclip","slug":"github-attachment-canonical-source-mismatch","errorCode":"github_attachment_canonical_source_mismatch","errorMessage":"github_attachment_canonical_source_mismatch","messagePattern":"github_attachment_canonical_source_mismatch","errorType":"error_code","errorClass":"GitHubAttachmentUnavailableError","httpStatus":null,"severity":"error","filePath":"server/src/services/chat-github-attachments.ts","lineNumber":559,"sourceCode":"    !request ||\n    !value ||\n    typeof value !== \"object\" ||\n    Array.isArray(value)\n  )\n    throw new GitHubAttachmentUnavailableError(\n      \"github_attachment_canonical_response_unavailable\",\n    );\n  const row = value as Record<string, unknown>;\n  const thread =\n    /^github:([^:]+):(?:(issue):)?([1-9][0-9]*)(?::rc:([1-9][0-9]*))?$/i.exec(\n      locator.sourceThreadId,\n    )!;\n  if (\n    String(row.id) !== locator.sourceMessageId ||\n    typeof row.url !== \"string\" ||\n    row.url.toLowerCase() !== request.url.toLowerCase()\n  )\n    throw new GitHubAttachmentUnavailableError(\n      \"github_attachment_canonical_source_mismatch\",\n    );\n  if (\n    typeof row.body !== \"string\" ||\n    row.body.length > 200_000 ||\n    createHash(\"sha256\").update(row.body).digest(\"hex\") !==\n      locator.sourceBodySha256\n  )\n    throw new GitHubAttachmentUnavailableError(\n      \"github_attachment_canonical_body_mismatch\",\n    );\n  if (typeof row.body_html !== \"string\" || row.body_html.length > 600_000)\n    throw new GitHubAttachmentUnavailableError(\n      \"github_attachment_canonical_html_unavailable\",\n    );\n  if (thread[4]) {\n    if (\n      row.pull_request_url !==","sourceCodeStart":541,"sourceCodeEnd":577,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/server/src/services/chat-github-attachments.ts#L541-L577","documentation":"After parsing the canonical comment, resolveCanonicalAttachmentTargetOrThrow cross-checks that the API row is the exact message the attachment locator was pinned to: row.id must equal locator.sourceMessageId, row.url must be a string matching the canonical request URL case-insensitively, and for review-comment threads the pull_request_url and in_reply_to_id must match. Any mismatch throws github_attachment_canonical_source_mismatch because the fetched rendering would be evidence for a different source than the attachment claims.","triggerScenarios":"The API response's `id` differs from the stored sourceMessageId; `row.url` is missing, non-string, or differs (case-insensitively) from the canonical api.github.com comment URL; for review comments (locator with :rc:), row.pull_request_url is not the expected /pulls/{n} URL or in_reply_to_id (falling back to id) does not match the review-comment id; for issue comments row.issue_url does not match the expected /issues/{n} URL.","commonSituations":"The comment was edited/moved so its URL changed; the attachment descriptor was copied from another issue or thread (stale locator); GitHub redirects a review-comment lookup to a different endpoint; resolving an attachment against the wrong repository's comment with the same numeric id.","solutions":["Re-attach the file from the current, live comment so the stored locator (sourceMessageId, URL, body hash) is regenerated against the actual source.","Verify the attachment locator was created from the same repo/issue/comment you are fetching; fix stale or copied descriptors.","For review comments, confirm you fetched the /pulls/{n}/comments/{id} resource (whose issue_url/pull_request_url fields match) rather than a plain issue comment.","Clear caches of previously rendered attachment targets and re-run resolution after the comment's URL or thread changed."],"exampleFix":"// before: resolving an attachment against a row fetched from a different comment id\nconst res = await fetch(`${base}/issues/comments/${wrongId}`);\n// after: fetch the exact comment id pinned in the locator\nconst res = await fetch(`${base}/issues/comments/${locator.sourceMessageId}`);","handlingStrategy":"validation","validationCode":"// Validate identity fields before handing the row to the resolver\nconst ok =\n  String(row.id) === locator.sourceMessageId &&\n  typeof row.url === 'string' &&\n  row.url.toLowerCase() === canonicalUrl.toLowerCase();\nif (!ok) throw new Error('fetched comment does not match the attachment source');","typeGuard":"function matchesSource(row: Record<string, unknown>, locator: AttachmentLocator, requestUrl: string): boolean {\n  return String(row.id) === locator.sourceMessageId &&\n    typeof row.url === 'string' &&\n    row.url.toLowerCase() === requestUrl.toLowerCase();\n}","tryCatchPattern":"const target = resolveGitHubCommentAttachmentTarget(attachment, value);\nif (target === null) {\n  // source mismatch (or any other validation failure) mapped to null:\n  // re-capture the locator from the live comment instead of retrying blindly\n  await recaptureAttachmentLocator(attachment);\n  return null;\n}","preventionTips":["Store sourceMessageId and the canonical URL together at attach time and never copy locators between attachments/issues.","Fetch by the locator's exact comment id — never by a guessed or numerically similar id.","For review comments, use the pulls comments endpoint so pull_request_url/in_reply_to_id line up.","Re-capture the locator whenever a comment is edited or moved; treat stale locators as permanently unresolvable."],"tags":["github","integrity","identity-mismatch","stale-data"],"backgroundTag":"checksum-mismatch","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}