paperclipai/paperclip · error · Error

Maintained live campaign must contain unique enabled rosters

Error message

Maintained live campaign must contain unique enabled rosters

What it means

After mapping the campaign's enabled (non-'disabled' executionClass) lanes to their roster basenames, maintainedRosterSelection requires the resulting selection to be non-empty and contain no duplicate rosters. Two lanes pointing at the same roster file, or zero enabled lanes, trigger this error.

Source

Thrown at packages/paperclip-runner/scripts/runner-protocol-eval-campaign.mjs:106

  const campaign = await loadObject(campaignPath);
  if (
    campaign.schema !== "paperclip-runner/live-campaign/v1" ||
    !Array.isArray(campaign.lanes)
  ) {
    throw new Error(`Unsupported live campaign schema in ${campaignPath}`);
  }
  const selected = campaign.lanes
    .filter((lane) => lane.executionClass !== "disabled")
    .map((lane) => {
      const rosterPath = inside(
        programRoot,
        resolve(dirname(campaignPath), String(lane.roster ?? "")),
        "Campaign roster",
      );
      return basename(rosterPath);
    });
  if (selected.length === 0 || new Set(selected).size !== selected.length) {
    throw new Error(
      "Maintained live campaign must contain unique enabled rosters",
    );
  }
  return new Set(selected);
}

export async function buildProtocolEvalCatalog({
  evalsRoot,
  rosterSelection = "all",
  campaignId,
  source = {},
  maxParallel = 100,
}) {
  safeId(campaignId, "campaign ID");
  if (
    !Number.isSafeInteger(maxParallel) ||
    maxParallel < 2 ||
    maxParallel > 100

View on GitHub (pinned to 01ad858492)

Solutions

  1. Deduplicate lanes so each enabled lane references a distinct roster file.
  2. Re-enable at least one lane (set executionClass to something other than 'disabled').
  3. Use the explicit roster selection flag instead of the maintained campaign if you intentionally want a subset.

Example fix

// before
{ "lanes": [ { "roster": "rosters/a.json" }, { "roster": "rosters/a.json" } ] }
// after
{ "lanes": [ { "roster": "rosters/a.json" }, { "roster": "rosters/b.json" } ] }
Defensive patterns

Strategy: validation

Validate before calling

const enabled = campaign.lanes.filter((l) => l.executionClass !== "disabled").map((l) => basename(resolve(dirname(campaignPath), l.roster)));
if (enabled.length === 0 || new Set(enabled).size !== enabled.length) throw new Error("Enabled lanes must reference unique rosters");

Type guard

const hasUniqueEnabledRosters = (campaign) => {
  const rosters = (campaign.lanes ?? []).filter((l) => l.executionClass !== "disabled").map((l) => l.roster);
  return rosters.length > 0 && new Set(rosters).size === rosters.length;
};

Try / catch

try {
  const selection = await maintainedRosterSelection(programRoot);
} catch (err) {
  if (err.message.includes("unique enabled rosters")) console.error("Check campaigns/live-direct-full.json for duplicated lanes or all-disabled lanes.");
  throw err;
}

Prevention

When it happens

Trigger: campaigns/live-direct-full.json has multiple enabled lanes whose lane.roster resolve to the same roster file, or every lane has executionClass 'disabled' (or there are no lanes).

Common situations: Copy-pasting a lane entry to add a variant but forgetting to point it at a different roster; accidentally duplicating a lane; disabling all lanes while iterating and then running the campaign.

Related errors


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