paperclipai/paperclip · error

paperclip_runner_chat_attachment_origin_not_persisted

paperclip_runner_chat_attachment_origin_not_persisted

Error message

paperclip_runner_chat_attachment_origin_not_persisted

What it means

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.

Solutions

  1. 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
  2. Use a real (non-stubbed) issueService in tests or make the stub replicate work-product creation
  3. Check for concurrent deletion or scope mismatch of the work-product row (companyId/issueId/createdByRunId must match the binding exactly)
  4. Inspect DB transactions: ensure createAttachment and work-product insert commit before this verification SELECT runs

Example fix

// before (stubbed service in test)
createAttachment: async (a) => ({ ...a, id: "att1", originatingRunId: "other-run", artifactWorkProductId: null }),
// after
createAttachment: async (a) => {
  const wp = await db.insert(issueWorkProducts).values({ companyId, issueId, createdByRunId: a.createdByRunId, kind: "artifact" }).returning();
  return { ...a, id: "att1", originatingRunId: a.createdByRunId, artifactWorkProductId: wp[0].id };
}
Defensive patterns

Strategy: try-catch

Type guard

function hasPersistedOrigin(a, runId) {
  return typeof a.artifactWorkProductId === "string" && a.artifactWorkProductId.length > 0 && a.originatingRunId === runId;
}

Try / catch

try {
  await prepareReusedChatAttachment({ db, binding, source, title });
} catch (err) {
  if (err.message === "paperclip_runner_chat_attachment_origin_not_persisted") {
    // investigate createAttachment transaction/commit and work-product linkage; attachment may exist without origin
  } else throw err;
}

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18). Data as JSON: /api/errors/5044b277e34c2112. Report an issue: GitHub.

Appendix: source

Thrown at server/src/services/native-runtime/chat-attachment-reuse.ts:1467

    ) {
      throw new Error("paperclip_runner_chat_attachment_storage_mismatch");
    }
    const attachment = await issueService(input.db).createAttachment({
      issueId: input.binding.issueId,
      provider: stored.provider,
      objectKey: stored.objectKey,
      contentType: stored.contentType,
      byteSize: stored.byteSize,
      sha256: stored.sha256,
      originalFilename: stored.originalFilename,
      createdByAgentId: input.binding.agentId,
      createdByRunId: input.binding.runId,
    });
    if (
      !attachment.artifactWorkProductId ||
      attachment.originatingRunId !== input.binding.runId
    ) {
      throw new Error("paperclip_runner_chat_attachment_origin_not_persisted");
    }
    const [workProduct] = await input.db
      .select({ metadata: issueWorkProducts.metadata })
      .from(issueWorkProducts)
      .where(
        and(
          eq(issueWorkProducts.id, attachment.artifactWorkProductId),
          eq(issueWorkProducts.companyId, input.binding.companyId),
          eq(issueWorkProducts.issueId, input.binding.issueId),
          eq(issueWorkProducts.createdByRunId, input.binding.runId),
        ),
      )
      .for("update")
      .limit(1);
    if (!workProduct) {
      throw new Error("paperclip_runner_chat_attachment_origin_not_persisted");
    }
    await input.db

View on GitHub (pinned to 3f1d897a7c)