{"record":{"id":"876f647f69bd0fc0","repo":"paperclipai/paperclip","slug":"immutable-campaign-history-changed-for-record-ca","errorCode":null,"errorMessage":"Immutable campaign history changed for ${record.campaignId}","messagePattern":"Immutable campaign history changed for (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/paperclip-runner/scripts/publish-runner-protocol-eval-history.mjs","lineNumber":313,"sourceCode":"    allPassed: campaign.allPassed === true,\n    totals: campaign.totals,\n    rosters: campaign.rosters,\n    source: campaign.source,\n    ...(campaign.reportRevision\n      ? { reportRevision: campaign.reportRevision }\n      : {}),\n  };\n}\n\nexport function mergeProtocolEvalHistory(history, record) {\n  if (history.schema !== \"paperclip.runner-protocol-eval.history/v1\") {\n    throw new Error(\"Unsupported protocol eval history schema\");\n  }\n  const existing = history.campaigns.find(\n    (item) => item.campaignId === record.campaignId,\n  );\n  if (existing && JSON.stringify(existing) !== JSON.stringify(record)) {\n    throw new Error(\n      `Immutable campaign history changed for ${record.campaignId}`,\n    );\n  }\n  const campaigns = existing\n    ? [...history.campaigns]\n    : [...history.campaigns, record];\n  const activityAt = (campaign) =>\n    campaign.reportRevision?.renderedAt ?? campaign.generatedAt;\n  const activityOrder = (left, right) =>\n    activityAt(right).localeCompare(activityAt(left));\n  campaigns.sort(activityOrder);\n  // Report revisions are discoverable history entries, never qualification runs.\n  const qualifications = campaigns\n    .filter((campaign) => !campaign.reportRevision)\n    .sort((left, right) => right.generatedAt.localeCompare(left.generatedAt));\n  const latest = qualifications[0] ?? null;\n  const latestGreen =\n    qualifications.find(","sourceCodeStart":295,"sourceCodeEnd":331,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/packages/paperclip-runner/scripts/publish-runner-protocol-eval-history.mjs#L295-L331","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before: republish same id with edited totals\nawait publishProtocolEvalHistory({ reportRoot: \"reports/gha-7-1-edited\", ... }); // campaignId gha-7-1\n// after: suffix a new report revision id\n// campaign.json: \"campaignId\": \"gha-7-1-report-r2\"","handlingStrategy":"validation","validationCode":"const record = protocolEvalHistoryRecord(campaign, publicRoot);\nconst existing = storedHistory.campaigns.find((c) => c.campaignId === record.campaignId);\nif (existing && JSON.stringify(existing) !== JSON.stringify(record))\n  throw new Error(`record for ${record.campaignId} would change; use a new campaignId`);","typeGuard":null,"tryCatchPattern":"try {\n  merged = mergeProtocolEvalHistory(history, record);\n} catch (e) {\n  if (String(e.message).startsWith(\"Immutable campaign history changed\")) {\n    const id = String(e.message).split(\"for \")[1];\n    console.error(`campaign ${id} already published with different data; publish under a new id`);\n  }\n  throw e;\n}","preventionTips":["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"],"tags":["immutability","consistency","publishing"],"backgroundTag":"checksum-mismatch","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}