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
- Use a new campaignId (new GHA run/attempt or a -report-<rev> suffix) for the changed report
- Restore the original report contents so the derived record matches the stored one exactly
- If the change is an intentional report revision, publish under a distinct report-suffixed ID since the same campaignId must still match exactly
- 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
- Treat a published campaignId as frozen; use a new run/attempt or -report-<rev> suffix for changes
- Keep RUNNER_PROTOCOL_EVAL_HISTORY_PUBLIC_BASE_URL stable, since it is baked into every record's publicUrl
- Make report generation deterministic for the same inputs
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
- Report campaign ID does not match publication target
- Immutable campaign ${campaign.campaignId} already exists wit
- request.session.provider must match request.provider
- request.session.requestedModel must match request.model
- Chat Evalbook must not mix in legacy plain attempt pages
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/876f647f69bd0fc0.
Report an issue: GitHub.