paperclipai/paperclip · error · Error
Immutable campaign already exists with a different digest
Error message
Immutable campaign ${campaign.campaignId} already exists with a different digest What it means
Published campaign bundles are immutable: once bundle-manifest.json exists in S3 for a campaign, republishing must produce the identical bundleDigest (a SHA-256 over the sorted file path/sha256/bytes entries). If the existing manifest's digest differs, the publisher throws instead of overwriting immutable public content. This catches any change to the report for the same campaignId.
Solutions
- Publish the corrected report under a new campaignId (new run/attempt or -report-<rev> suffix)
- Reproduce the exact original inputs/toolchain so the bundle digest matches byte-for-byte
- Delete the S3 campaign prefix (campaigns/<id>/) intentionally if you truly must replace it, understanding this breaks public immutability
- Diff the existing manifest's files array against the new manifest to see which file changed before deciding
Example fix
// before: same id, edited HTML // campaign.json: "campaignId": "gha-5-1" (re-published after edit) // after: distinct revision id // campaign.json: "campaignId": "gha-5-1-report-fix1"
Defensive patterns
Strategy: validation
Validate before calling
const existing = JSON.parse(await downloadManifest(campaignId)); // null if absent
const fresh = buildManifest(reportRoot, campaignId);
if (existing && existing.bundleDigest !== fresh.bundleDigest)
throw new Error(`bundle for ${campaignId} changed: pick a new campaignId`); Try / catch
try {
await publishProtocolEvalHistory({ reportRoot, destination, viewerRoot });
} catch (e) {
if (String(e.message).includes("already exists with a different digest")) {
console.error("compare existing bundle-manifest.json files[] to your new manifest to find the changed file; republish under a new id");
}
throw e;
} Prevention
- Never republish a campaignId with modified report content; suffix a new -report-<rev> ID
- Compare old vs new bundle manifests (path/sha256) to identify what changed before republishing
- Build reports reproducibly; pin the viewer build used for publication
When it happens
Trigger: Regenerating the report for the same GHA run/attempt with any changed file (different totals, viewer assets, timestamps embedded in HTML); publishing from a different machine/toolchain that produces byte-different assets for the same campaign; a partially failing upload leaving a manifest for a different bundle version.
Common situations: Re-running the publish step on the same workflow run after code changed between attempts with the same attempt number; fixing a typo in generated HTML and re-publishing the same campaign ID; using a different viewer build than the original publication.
Understand the failure class
Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.
Related errors
- Immutable campaign history changed for
- Chat Evalbook must not mix in legacy plain attempt pages
- Immutable Evalbook record changed
- native_execution_input_persisted_binding_mismatch
- npm accepted the preview but it is not yet visible
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/d916cc0ea1cd465a.
Report an issue: GitHub.
Appendix: source
Thrown at packages/paperclip-runner/scripts/publish-runner-protocol-eval-history.mjs:500
);
const history = await enrichProtocolEvalHistory(mergedHistory, {
currentCampaign: campaign,
loadCampaign: async (id) => {
if (!SAFE_CAMPAIGN.test(id)) throw new Error("Unsafe historical campaign ID");
return downloadJson(validatedDestination.bucket,
`${validatedDestination.prefix}/campaigns/${id}/campaign.json`,
join(temporary, `${id}.json`));
},
});
const campaignPrefix = `${validatedDestination.prefix}/campaigns/${campaign.campaignId}`;
const manifestKey = `${campaignPrefix}/bundle-manifest.json`;
const existing = await downloadJson(
validatedDestination.bucket,
manifestKey,
join(temporary, "existing-manifest.json"),
);
if (existing && existing.bundleDigest !== manifest.bundleDigest) {
throw new Error(
`Immutable campaign ${campaign.campaignId} already exists with a different digest`,
);
}
if (!existing) {
await uploadImmutableReport(
validatedDestination.bucket,
campaignPrefix,
reportRoot,
);
const manifestFile = join(temporary, "bundle-manifest.json");
await writeFile(manifestFile, json(manifest));
await uploadFile(
validatedDestination.bucket,
manifestKey,
manifestFile,
"public,max-age=31536000,immutable",
);
}View on GitHub (pinned to 01ad858492)