paperclipai/paperclip · error · Error

Immutable campaign history changed for ${record.campaignId}

Error message

Immutable campaign history changed for ${record.campaignId}

What it means

History records are immutable: once a campaignId exists in history.json, re-merging must produce an identical record. If mergeProtocolEvalHistory finds an existing entry for the same campaignId whose JSON differs from the new record, it throws rather than silently rewriting published history. This preserves the integrity of the public eval history.

Source

Thrown at packages/paperclip-runner/scripts/publish-runner-protocol-eval-history.mjs:313

    allPassed: campaign.allPassed === true,
    totals: campaign.totals,
    rosters: campaign.rosters,
    source: campaign.source,
    ...(campaign.reportRevision
      ? { reportRevision: campaign.reportRevision }
      : {}),
  };
}

export function mergeProtocolEvalHistory(history, record) {
  if (history.schema !== "paperclip.runner-protocol-eval.history/v1") {
    throw new Error("Unsupported protocol eval history schema");
  }
  const existing = history.campaigns.find(
    (item) => item.campaignId === record.campaignId,
  );
  if (existing && JSON.stringify(existing) !== JSON.stringify(record)) {
    throw new Error(
      `Immutable campaign history changed for ${record.campaignId}`,
    );
  }
  const campaigns = existing
    ? [...history.campaigns]
    : [...history.campaigns, record];
  const activityAt = (campaign) =>
    campaign.reportRevision?.renderedAt ?? campaign.generatedAt;
  const activityOrder = (left, right) =>
    activityAt(right).localeCompare(activityAt(left));
  campaigns.sort(activityOrder);
  // Report revisions are discoverable history entries, never qualification runs.
  const qualifications = campaigns
    .filter((campaign) => !campaign.reportRevision)
    .sort((left, right) => right.generatedAt.localeCompare(left.generatedAt));
  const latest = qualifications[0] ?? null;
  const latestGreen =
    qualifications.find(

View on GitHub (pinned to 01ad858492)

Solutions

  1. Use a new campaignId (new GHA run/attempt or a -report-<rev> suffix) for the changed report
  2. Restore the original report contents so the derived record matches the stored one exactly
  3. If the change is an intentional report revision, publish under a distinct report-suffixed ID since the same campaignId must still match exactly
  4. As a last resort, deliberately replace the remote history.json entry (breaking immutability) with full awareness of the consequences

Example fix

// before: republish same id with edited totals
await publishProtocolEvalHistory({ reportRoot: "reports/gha-7-1-edited", ... }); // campaignId gha-7-1
// after: suffix a new report revision id
// campaign.json: "campaignId": "gha-7-1-report-r2"
Defensive patterns

Strategy: validation

Validate before calling

const record = protocolEvalHistoryRecord(campaign, publicRoot);
const existing = storedHistory.campaigns.find((c) => c.campaignId === record.campaignId);
if (existing && JSON.stringify(existing) !== JSON.stringify(record))
  throw new Error(`record for ${record.campaignId} would change; use a new campaignId`);

Try / catch

try {
  merged = mergeProtocolEvalHistory(history, record);
} catch (e) {
  if (String(e.message).startsWith("Immutable campaign history changed")) {
    const id = String(e.message).split("for ")[1];
    console.error(`campaign ${id} already published with different data; publish under a new id`);
  }
  throw e;
}

Prevention

When it happens

Trigger: Republishing the same campaignId after the report contents changed (different totals, generatedAt, source shas, or reportRevision); a non-deterministic report generation producing different totals for the same run; calling protocolEvalHistoryRecord with a different publicRoot/publicBaseUrl than the original publication.

Common situations: Re-running a workflow for the same GHA run/attempt after fixing eval code (totals differ); changing RUNNER_PROTOCOL_EVAL_HISTORY_PUBLIC_BASE_URL, which alters each record's publicUrl; editing campaign fields like complete/allPassed between publications.

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/876f647f69bd0fc0. Report an issue: GitHub.