paperclipai/paperclip · error · Error

paperclip_runner_file_handoff_existing_binding_invalid

paperclip_runner_file_handoff_existing_binding_invalid

Error message

paperclip_runner_file_handoff_existing_binding_invalid

What it means

When a duplicate handoff attachment is found bound to a comment, the code re-validates that the bound comment still exists with the exact expected author, run, and body (`Prepared <title> for this response.`) and is not soft-deleted. If the comment row is missing or no longer matches, the pre-existing binding is considered corrupt and this error is thrown instead of returning the duplicate disposition.

Solutions

  1. Inspect the issueComments row for existing.commentId; if it was soft-deleted or its body changed, restore the canonical body (`Prepared <title> for this response.`) and clear deletedAt, or null out the attachment's issueCommentId so a fresh comment is created.
  2. If the binding is unrecoverable, detach the attachment (set issueCommentId null) and re-run the handoff so it takes the create path.
  3. Check for code paths or admin tools that edit/delete agent comments and exclude comments created by the runner protocol (authorType 'agent', authorizationReason 'paperclip_runner_protocol').
  4. Verify the duplicate-match query filters (authorAgentId, createdByRunId, exact body) against your data; align the stored comment with what the current code generates.
Defensive patterns

Strategy: validation

Validate before calling

// pre-check the duplicate binding's comment before calling the handoff
const [c] = await db.select().from(issueComments).where(and(
  eq(issueComments.id, existingCommentId),
  isNull(issueComments.deletedAt),
  eq(issueComments.authorType, 'agent'),
  eq(issueComments.createdByRunId, runId),
));
if (!c) throw new Error('comment binding invalid; detach attachment before retry');

Try / catch

try {
  return await prepareNativeRunnerFileHandoff(input);
} catch (e) {
  if (e.message === 'paperclip_runner_file_handoff_existing_binding_invalid') {
    await db.update(issueAttachments).set({ issueCommentId: null }).where(eq(issueAttachments.id, attachmentId));
    return prepareNativeRunnerFileHandoff(input); // now takes the create path
  }
  throw e;
}

Prevention

When it happens

Trigger: The attachment from the same run/filename/hash has a non-null issueCommentId, but the referenced issueComments row is deleted (deletedAt set), was authored by a different agent/run, or its body was edited so it no longer equals the canonical prepared-deliverable message.

Common situations: A user or moderation flow edited or soft-deleted the agent's 'Prepared ...' comment; a run retry after partial cleanup where the comment was removed but the attachment kept its commentId; data migrations that rewrote comment bodies; comment author recorded with a different runId due to retry re-binding.

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/1a78f007e2082997. Report an issue: GitHub.

Appendix: source

Thrown at server/src/services/native-runtime/native-runner-file-handoff.ts:1208

      .from(issueComments)
      .where(
        and(
          eq(issueComments.id, existing.commentId),
          eq(issueComments.companyId, input.binding.companyId),
          eq(issueComments.issueId, input.binding.issueId),
          eq(issueComments.authorType, "agent"),
          eq(issueComments.authorAgentId, input.binding.agentId),
          eq(issueComments.createdByRunId, input.binding.runId),
          eq(
            issueComments.body,
            `Prepared ${verified.title} for this response.`,
          ),
          isNull(issueComments.deletedAt),
        ),
      )
      .limit(1);
    if (!comment) {
      throw new Error("paperclip_runner_file_handoff_existing_binding_invalid");
    }
    return {
      result: {
        commandId: `deliverable-prepared:${existing.attachmentId}`,
        disposition: "duplicate",
        attachmentId: existing.attachmentId,
        contentPath: `/api/attachments/${existing.attachmentId}/content`,
        downloadPath: `/api/attachments/${existing.attachmentId}/content?download=1`,
        stateRevision: statusVersion,
        entityRefs: [
          existing.attachmentId,
          ...(existing.workProductId ? [existing.workProductId] : []),
          comment.id,
        ],
        scheduledWakeIds: [],
      },
      rollbackDefinitePreCommitFailure: null,
    };

View on GitHub (pinned to 3f1d897a7c)