paperclipai/paperclip · error · GitHubAttachmentUnavailableError
github_attachment_canonical_source_mismatch
github_attachment_canonical_source_mismatch
Error message
github_attachment_canonical_source_mismatch
What it means
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.
Source
Thrown at server/src/services/chat-github-attachments.ts:559
!request ||
!value ||
typeof value !== "object" ||
Array.isArray(value)
)
throw new GitHubAttachmentUnavailableError(
"github_attachment_canonical_response_unavailable",
);
const row = value as Record<string, unknown>;
const thread =
/^github:([^:]+):(?:(issue):)?([1-9][0-9]*)(?::rc:([1-9][0-9]*))?$/i.exec(
locator.sourceThreadId,
)!;
if (
String(row.id) !== locator.sourceMessageId ||
typeof row.url !== "string" ||
row.url.toLowerCase() !== request.url.toLowerCase()
)
throw new GitHubAttachmentUnavailableError(
"github_attachment_canonical_source_mismatch",
);
if (
typeof row.body !== "string" ||
row.body.length > 200_000 ||
createHash("sha256").update(row.body).digest("hex") !==
locator.sourceBodySha256
)
throw new GitHubAttachmentUnavailableError(
"github_attachment_canonical_body_mismatch",
);
if (typeof row.body_html !== "string" || row.body_html.length > 600_000)
throw new GitHubAttachmentUnavailableError(
"github_attachment_canonical_html_unavailable",
);
if (thread[4]) {
if (
row.pull_request_url !==View on GitHub (pinned to 01ad858492)
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.
Example fix
// before: resolving an attachment against a row fetched from a different comment id
const res = await fetch(`${base}/issues/comments/${wrongId}`);
// after: fetch the exact comment id pinned in the locator
const res = await fetch(`${base}/issues/comments/${locator.sourceMessageId}`); Defensive patterns
Strategy: validation
Validate before calling
// Validate identity fields before handing the row to the resolver
const ok =
String(row.id) === locator.sourceMessageId &&
typeof row.url === 'string' &&
row.url.toLowerCase() === canonicalUrl.toLowerCase();
if (!ok) throw new Error('fetched comment does not match the attachment source'); Type guard
function matchesSource(row: Record<string, unknown>, locator: AttachmentLocator, requestUrl: string): boolean {
return String(row.id) === locator.sourceMessageId &&
typeof row.url === 'string' &&
row.url.toLowerCase() === requestUrl.toLowerCase();
} Try / catch
const target = resolveGitHubCommentAttachmentTarget(attachment, value);
if (target === null) {
// source mismatch (or any other validation failure) mapped to null:
// re-capture the locator from the live comment instead of retrying blindly
await recaptureAttachmentLocator(attachment);
return null;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
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
- github_attachment_canonical_body_mismatch
- Anthropic pinned Agent version identity does not match the s
- runnerd digest mismatch: expected ${request.runnerd.sha256},
- Could not prepare managed GitHub launchers
- Materialized OpenCode executable digest mismatch
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/b4df10084499afbc.
Report an issue: GitHub.