{"record":{"id":"5044b277e34c2112","repo":"paperclipai/paperclip","slug":"paperclip-runner-chat-attachment-origin-not-persisted","errorCode":"paperclip_runner_chat_attachment_origin_not_persisted","errorMessage":"paperclip_runner_chat_attachment_origin_not_persisted","messagePattern":"paperclip_runner_chat_attachment_origin_not_persisted","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/src/services/native-runtime/chat-attachment-reuse.ts","lineNumber":1467,"sourceCode":"    ) {\n      throw new Error(\"paperclip_runner_chat_attachment_storage_mismatch\");\n    }\n    const attachment = await issueService(input.db).createAttachment({\n      issueId: input.binding.issueId,\n      provider: stored.provider,\n      objectKey: stored.objectKey,\n      contentType: stored.contentType,\n      byteSize: stored.byteSize,\n      sha256: stored.sha256,\n      originalFilename: stored.originalFilename,\n      createdByAgentId: input.binding.agentId,\n      createdByRunId: input.binding.runId,\n    });\n    if (\n      !attachment.artifactWorkProductId ||\n      attachment.originatingRunId !== input.binding.runId\n    ) {\n      throw new Error(\"paperclip_runner_chat_attachment_origin_not_persisted\");\n    }\n    const [workProduct] = await input.db\n      .select({ metadata: issueWorkProducts.metadata })\n      .from(issueWorkProducts)\n      .where(\n        and(\n          eq(issueWorkProducts.id, attachment.artifactWorkProductId),\n          eq(issueWorkProducts.companyId, input.binding.companyId),\n          eq(issueWorkProducts.issueId, input.binding.issueId),\n          eq(issueWorkProducts.createdByRunId, input.binding.runId),\n        ),\n      )\n      .for(\"update\")\n      .limit(1);\n    if (!workProduct) {\n      throw new Error(\"paperclip_runner_chat_attachment_origin_not_persisted\");\n    }\n    await input.db","sourceCodeStart":1449,"sourceCodeEnd":1485,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/native-runtime/chat-attachment-reuse.ts#L1449-L1485","documentation":"After createAttachment persists the reused attachment, the code verifies that the resulting attachment row has an artifactWorkProductId and that its originatingRunId equals the binding runId; it then re-reads the issue_work_products row FOR UPDATE scoped to company/issue/run and throws if absent. This error means the attachment-to-work-product origin linkage was not durably persisted, i.e. createAttachment did not produce the expected audit/origin artifacts for this run.","triggerScenarios":"issueService.createAttachment returned a row without artifactWorkProductId (work-product creation skipped or failed inside the service), or wrote originatingRunId different from binding.runId, or the follow-up SELECT on issueWorkProducts found no row matching the work-product id + companyId + issueId + createdByRunId (deleted, wrong scope, or not yet committed).","commonSituations":"A partially migrated createAttachment implementation that no longer creates artifact work products; transaction visibility issues where the work product was written in a different, not-yet-committed transaction; a storage provider path that bypasses work-product creation for certain attachment kinds; tests using a stubbed issueService whose createAttachment omits work-product linkage.","solutions":["Update the issueService.createAttachment implementation to always create the artifact work product with originatingRunId set to the creating run and link artifactWorkProductId on the attachment","Use a real (non-stubbed) issueService in tests or make the stub replicate work-product creation","Check for concurrent deletion or scope mismatch of the work-product row (companyId/issueId/createdByRunId must match the binding exactly)","Inspect DB transactions: ensure createAttachment and work-product insert commit before this verification SELECT runs"],"exampleFix":"// before (stubbed service in test)\ncreateAttachment: async (a) => ({ ...a, id: \"att1\", originatingRunId: \"other-run\", artifactWorkProductId: null }),\n// after\ncreateAttachment: async (a) => {\n  const wp = await db.insert(issueWorkProducts).values({ companyId, issueId, createdByRunId: a.createdByRunId, kind: \"artifact\" }).returning();\n  return { ...a, id: \"att1\", originatingRunId: a.createdByRunId, artifactWorkProductId: wp[0].id };\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"function hasPersistedOrigin(a, runId) {\n  return typeof a.artifactWorkProductId === \"string\" && a.artifactWorkProductId.length > 0 && a.originatingRunId === runId;\n}","tryCatchPattern":"try {\n  await prepareReusedChatAttachment({ db, binding, source, title });\n} catch (err) {\n  if (err.message === \"paperclip_runner_chat_attachment_origin_not_persisted\") {\n    // investigate createAttachment transaction/commit and work-product linkage; attachment may exist without origin\n  } else throw err;\n}","preventionTips":["Keep attachment insert and its artifact work-product creation in one transaction in createAttachment","Ensure originatingRunId is always set to the creating run id in createAttachment","Use the real issueService in tests; stubs must replicate work-product creation","Never delete issueWorkProducts rows without handling attachments referencing them"],"tags":["database","invariant","attachments","work-products"],"backgroundTag":"internal-invariant-violation","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"}