paperclipai/paperclip · error · Error

paperclip_runner_file_handoff_work_product_missing

paperclip_runner_file_handoff_work_product_missing

Error message

paperclip_runner_file_handoff_work_product_missing

What it means

prepareNativeRunnerFileHandoff in native-runner-file-handoff.ts implements idempotent deliverable handoff: when an identical attachment from the same run already exists and is bound to a comment, it returns a 'duplicate' disposition instead of re-uploading. This error throws when such an existing attachment has a bound comment but no matching artifact work-product row (the leftJoin on issueWorkProducts with type='artifact', provider='paperclip', externalId=attachmentId returned null). It is an internal invariant check: an attachment bound to a comment from this run should always have its work product companion row.

Solutions

  1. Recreate the missing issueWorkProducts artifact row with provider='paperclip', type='artifact', externalId set to the existing attachment id, and createdByRunId matching originatingRunId on the attachment.
  2. If the attachment is stale/orphaned, soft-delete the attachment/comment binding so the handoff path proceeds to create a fresh attachment + work product.
  3. Ensure createAttachment (issueService) creates the artifactWorkProductId in the same transaction; check for custom forks that skip work-product creation.
  4. Verify no migration backfilled attachments without work products; write a backfill script joining issueAttachments to issueWorkProducts via externalId = attachment id.

Example fix

// backfill missing work product before re-running handoff
// before
-- issue_work_products row missing for attachment
-- after
INSERT INTO issue_work_products (company_id, issue_id, type, provider, external_id, created_by_run_id, title)
SELECT a.company_id, a.issue_id, 'artifact', 'paperclip', a.id::text, a.originating_run_id, a.original_filename
FROM issue_attachments a
LEFT JOIN issue_work_products wp ON wp.external_id = a.id::text AND wp.deleted_at IS NULL
WHERE a.id = '<attachmentId>' AND wp.id IS NULL;
Defensive patterns

Strategy: validation

Validate before calling

// before calling the handoff, confirm the work product exists for a prior attachment
const [wp] = await db.select().from(issueWorkProducts).where(and(
  eq(issueWorkProducts.companyId, companyId),
  eq(issueWorkProducts.issueId, issueId),
  eq(issueWorkProducts.type, 'artifact'),
  eq(issueWorkProducts.provider, 'paperclip'),
  eq(issueWorkProducts.externalId, existingAttachmentId),
  eq(issueWorkProducts.createdByRunId, runId),
));
if (!wp) throw new Error('work product missing; backfill before retrying handoff');

Try / catch

try {
  await prepareNativeRunnerFileHandoff(input);
} catch (e) {
  if (e.message === 'paperclip_runner_file_handoff_work_product_missing') {
    await backfillArtifactWorkProduct(db, { attachmentId, runId, issueId, companyId });
    return retryHandoff(input);
  }
  throw e;
}

Prevention

When it happens

Trigger: Re-running prepareNativeRunnerFileHandoff for a deliverable whose attachment was previously created and attached to a comment, but the corresponding issueWorkProducts row (externalId = attachment id, createdByRunId = run id) is missing or was created with a different runId/type/provider/externalId.

Common situations: Manual database cleanup or migration that deleted/renamed work-product rows; older data created before the work-product-on-attachment invariant was introduced; partial writes from an older buggy version; running the handoff against an attachment created by a different code path that does not create artifact work products.

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

Appendix: source

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

    )
    .where(
      and(
        eq(issueAttachments.companyId, input.binding.companyId),
        eq(issueAttachments.issueId, input.binding.issueId),
        eq(issueAttachments.originatingRunId, input.binding.runId),
        eq(assets.createdByAgentId, input.binding.agentId),
        eq(assets.originalFilename, verified.filename),
        eq(assets.contentType, verified.contentType),
        eq(assets.byteSize, verified.body.length),
        eq(assets.sha256, verified.sha256),
      ),
    )
    .orderBy(issueAttachments.createdAt, issueAttachments.id)
    .limit(1);

  if (existing?.commentId) {
    if (!existing.workProductId) {
      throw new Error("paperclip_runner_file_handoff_work_product_missing");
    }
    const [comment] = await input.db
      .select({ id: issueComments.id })
      .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),
        ),

View on GitHub (pinned to 3f1d897a7c)