paperclipai/paperclip · error

paperclip_runner_chat_attachment_source_denied

paperclip_runner_chat_attachment_source_denied

Error message

paperclip_runner_chat_attachment_source_denied

What it means

Thrown by loadSource in chat-attachment-reuse.ts:936 when the source comment row cannot be found under FOR UPDATE with the binding's companyId, issueId, and a null deletedAt. The runner asked to reuse a chat attachment from a comment that either does not exist, belongs to a different company/issue, or was soft-deleted. It is a deliberate authorization-shaped rejection: the service does not distinguish 'not found' from 'not allowed', so runners cannot probe for comment IDs.

Solutions

  1. Verify the sourceCommentId comes from the current run's wake comment ids (contextSnapshot paperclipWake.commentIds) and matches the binding's issue; correct the id in the tool call.
  2. Check the comment still exists and is not soft-deleted (issue_comments.deleted_at is null, same company_id and issue_id as the binding); if deleted, ask the user to re-send or re-attach the file.
  3. Re-run or re-bind the issue so the binding references a live comment; stale bindings after comment cleanup must be refreshed.
  4. Use the chat attachment list tool (listAuthorizedChatAttachments) to discover valid (sourceCommentId, attachmentId) pairs instead of guessing ids.

Example fix

// before
await reuseChatAttachment({ binding, sourceCommentId: "cmt-from-old-run", attachmentId });
// -> paperclip_runner_chat_attachment_source_denied (comment deleted / wrong issue)

// after: resolve a live source comment from the binding's wake context
const page = await listAuthorizedChatAttachments({ db, binding, limit: 20 });
await reuseChatAttachment({ binding, sourceCommentId: page.items[0].sourceCommentId, attachmentId: page.items[0].attachmentId });
Defensive patterns

Strategy: validation

Validate before calling

// resolve valid sources from the server instead of guessing ids
const page = await listAuthorizedChatAttachments({ db, binding, limit: 50 });
const valid = page.items.some(i => i.sourceCommentId === sourceCommentId && i.attachmentId === attachmentId);
if (!valid) throw new Error("skip reuse: source not in authorized list");

Type guard

function isSourceDenied(err: unknown): err is Error {
  return err instanceof Error && err.message === "paperclip_runner_chat_attachment_source_denied";
}

Try / catch

try {
  await reuseChatAttachment({ binding, sourceCommentId, attachmentId });
} catch (err) {
  if (err instanceof Error && err.message === "paperclip_runner_chat_attachment_source_denied") {
    // fall back to enumerating authorized sources rather than retrying the same id
    return listAuthorizedChatAttachments({ db, binding, limit: 20 });
  }
  throw err;
}

Prevention

When it happens

Trigger: authorizeChatAttachmentReuse (via loadSource) is called with a sourceCommentId that: does not exist in issue_comments; exists under a different companyId or issueId than the ChatReuseBinding; or has a non-null deletedAt (soft-deleted before the call).

Common situations: The agent passes a stale or hallucinated comment id to the chat attachment reuse tool; the source comment was deleted by a user between the wake and the tool call; the agent references a comment from another issue or company; an old run binding is replayed after the comment was cleaned up.

Understand the failure class

Background: Record Not Found Errors: "not found", RecordNotFound, and "was not found" — what they mean and how to fix them — this error's family across 28 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18). Data as JSON: /api/errors/2e4e8e16e0043413. Report an issue: GitHub.

Appendix: source

Thrown at server/src/services/native-runtime/chat-attachment-reuse.ts:936

  sourceCommentId: string,
  attachmentId: string,
  allowEmpty = false,
): Promise<ChatAttachmentReuseSource> {
  const [sourceComment] = await tx
    .select({ id: issueComments.id })
    .from(issueComments)
    .where(
      and(
        eq(issueComments.id, sourceCommentId),
        eq(issueComments.companyId, binding.companyId),
        eq(issueComments.issueId, binding.issueId),
        isNull(issueComments.deletedAt),
      ),
    )
    .for("update")
    .limit(1);
  if (!sourceComment) {
    throw new Error("paperclip_runner_chat_attachment_source_denied");
  }
  const [row] = await tx
    .select({
      attachmentId: issueAttachments.id,
      parentCommentId: issueComments.id,
      filename: assets.originalFilename,
      contentType: assets.contentType,
      byteSize: assets.byteSize,
      sha256: assets.sha256,
      objectKey: assets.objectKey,
      createdAt: issueAttachments.createdAt,
    })
    .from(issueAttachments)
    .innerJoin(
      assets,
      and(
        eq(assets.id, issueAttachments.assetId),
        eq(assets.companyId, binding.companyId),

View on GitHub (pinned to 3f1d897a7c)