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 > 100View on GitHub (pinned to 01ad858492)
Solutions
- Deduplicate lanes so each enabled lane references a distinct roster file.
- Re-enable at least one lane (set executionClass to something other than 'disabled').
- 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
- Keep a one-lane-per-roster rule when authoring campaigns.
- Never commit a campaign with zero enabled lanes.
- Review lane diffs for accidental copy-paste duplicates.
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
- Roster selection must contain unique comma-separated IDs
- Unsupported live campaign schema in ${campaignPath}
- Invalid status '${String(rawStatus)}'. Must be one of: ${PLU
- "tool" is required and must be a string
- "runContext" is required and must be an object
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/3ec2f586c5d4bdf6.
Report an issue: GitHub.