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
- 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
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
- 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
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
- ACPX verified runtime descriptor is misplaced
- : capture has no states
- Attachment exceeds the configured size limit
- attachment_not_ready
- Cloud runtime identity is already claimed by another…
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.dbView on GitHub (pinned to 3f1d897a7c)