paperclipai/paperclip · error · Error
Immutable campaign ${campaign.campaignId} already exists wit
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.
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)
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 ${record.campaignId}
- RUNNER_PROTOCOL_EVAL_HISTORY_S3_BUCKET is not a valid bucket
- Chat Evalbook must not mix in legacy plain attempt pages
- Report campaign ID does not match publication target
- Unable to inspect protocol eval history object: ${detail.sli
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/d916cc0ea1cd465a.
Report an issue: GitHub.