{"record":{"id":"c92ae38073fa4a78","repo":"paperclipai/paperclip","slug":"paperclip-runner-file-handoff-work-product-missing","errorCode":"paperclip_runner_file_handoff_work_product_missing","errorMessage":"paperclip_runner_file_handoff_work_product_missing","messagePattern":"paperclip_runner_file_handoff_work_product_missing","errorType":"error_code","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"server/src/services/native-runtime/native-runner-file-handoff.ts","lineNumber":1186,"sourceCode":"    )\n    .where(\n      and(\n        eq(issueAttachments.companyId, input.binding.companyId),\n        eq(issueAttachments.issueId, input.binding.issueId),\n        eq(issueAttachments.originatingRunId, input.binding.runId),\n        eq(assets.createdByAgentId, input.binding.agentId),\n        eq(assets.originalFilename, verified.filename),\n        eq(assets.contentType, verified.contentType),\n        eq(assets.byteSize, verified.body.length),\n        eq(assets.sha256, verified.sha256),\n      ),\n    )\n    .orderBy(issueAttachments.createdAt, issueAttachments.id)\n    .limit(1);\n\n  if (existing?.commentId) {\n    if (!existing.workProductId) {\n      throw new Error(\"paperclip_runner_file_handoff_work_product_missing\");\n    }\n    const [comment] = await input.db\n      .select({ id: issueComments.id })\n      .from(issueComments)\n      .where(\n        and(\n          eq(issueComments.id, existing.commentId),\n          eq(issueComments.companyId, input.binding.companyId),\n          eq(issueComments.issueId, input.binding.issueId),\n          eq(issueComments.authorType, \"agent\"),\n          eq(issueComments.authorAgentId, input.binding.agentId),\n          eq(issueComments.createdByRunId, input.binding.runId),\n          eq(\n            issueComments.body,\n            `Prepared ${verified.title} for this response.`,\n          ),\n          isNull(issueComments.deletedAt),\n        ),","sourceCodeStart":1168,"sourceCodeEnd":1204,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/native-runtime/native-runner-file-handoff.ts#L1168-L1204","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// backfill missing work product before re-running handoff\n// before\n-- issue_work_products row missing for attachment\n-- after\nINSERT INTO issue_work_products (company_id, issue_id, type, provider, external_id, created_by_run_id, title)\nSELECT a.company_id, a.issue_id, 'artifact', 'paperclip', a.id::text, a.originating_run_id, a.original_filename\nFROM issue_attachments a\nLEFT JOIN issue_work_products wp ON wp.external_id = a.id::text AND wp.deleted_at IS NULL\nWHERE a.id = '<attachmentId>' AND wp.id IS NULL;","handlingStrategy":"validation","validationCode":"// before calling the handoff, confirm the work product exists for a prior attachment\nconst [wp] = await db.select().from(issueWorkProducts).where(and(\n  eq(issueWorkProducts.companyId, companyId),\n  eq(issueWorkProducts.issueId, issueId),\n  eq(issueWorkProducts.type, 'artifact'),\n  eq(issueWorkProducts.provider, 'paperclip'),\n  eq(issueWorkProducts.externalId, existingAttachmentId),\n  eq(issueWorkProducts.createdByRunId, runId),\n));\nif (!wp) throw new Error('work product missing; backfill before retrying handoff');","typeGuard":null,"tryCatchPattern":"try {\n  await prepareNativeRunnerFileHandoff(input);\n} catch (e) {\n  if (e.message === 'paperclip_runner_file_handoff_work_product_missing') {\n    await backfillArtifactWorkProduct(db, { attachmentId, runId, issueId, companyId });\n    return retryHandoff(input);\n  }\n  throw e;\n}","preventionTips":["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."],"tags":["database","idempotency","internal-invariant","native-runner"],"backgroundTag":"record-not-found","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}