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
- 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.
- If the attachment is stale/orphaned, soft-delete the attachment/comment binding so the handoff path proceeds to create a fresh attachment + work product.
- Ensure createAttachment (issueService) creates the artifactWorkProductId in the same transaction; check for custom forks that skip work-product creation.
- 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
- Never delete issueWorkProducts rows directly; use the service soft-delete paths.
- Include work-product creation in the same transaction as attachment creation.
- Add a data-integrity check that alerts on attachments from runner runs lacking artifact work products.
- After schema/migrations, run a backfill verifying externalId = attachment id links.
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
- paperclip_runner_file_handoff_existing_binding_invalid
- paperclip_runner_file_handoff_origin_not_persisted
- A different semantic result was already committed
- Cannot build API path with an empty path segment.
- Challenge secret is required. Pass --token or --token-env.
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)