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

  1. Publish the corrected report under a new campaignId (new run/attempt or -report-<rev> suffix)
  2. Reproduce the exact original inputs/toolchain so the bundle digest matches byte-for-byte
  3. Delete the S3 campaign prefix (campaigns/<id>/) intentionally if you truly must replace it, understanding this breaks public immutability
  4. 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

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


AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10). Data as JSON: /api/errors/d916cc0ea1cd465a. Report an issue: GitHub.