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
- 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.
- 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.
- Re-run or re-bind the issue so the binding references a live comment; stale bindings after comment cleanup must be refreshed.
- 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
- Always take sourceCommentId from the run's wake comment ids, never from model-invented ids.
- Treat soft-deleted comments as permanently unusable sources; never cache comment ids across user deletions.
- Enumerate candidates with listAuthorizedChatAttachments before the first reuse call.
- Scope any local id bookkeeping to (companyId, issueId) to prevent cross-issue id leakage.
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
- paperclip_runner_chat_attachment_principal_denied
- paperclip_runner_chat_attachment_read_scope_unavailable
- paperclip_runner_file_handoff_not_authorized
- paperclip_runner_tool_binding_not_authorized
- paperclip_runner_tool_binding_not_authorized
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)