ruvnet/ruflo · error · Error
duplicate agent id: ${agent.id}
Error message
duplicate agent id: ${agent.id} What it means
CodexWorktreeCoordinator.prepare() gives every agent in a run its own git worktree and branch named ruflo/<runId>/<agent.id>, so agent ids must be unique within one prepare() call. Ids are collected into a Set and the call throws as soon as a repeat is seen, before any worktree or registry file is created. A duplicate would otherwise collide branch names and registry assignments for the run.
Source
Thrown at v3/@claude-flow/codex/src/worktrees/coordinator.ts:57
constructor(repoRoot: string) {
this.repoRoot = resolve(repoRoot);
const top = git(this.repoRoot, ['rev-parse', '--show-toplevel']);
if (resolve(top) !== this.repoRoot) throw new Error(`repoRoot must be the git top-level: ${top}`);
this.registryDir = join(this.repoRoot, '.claude-flow', 'swarm', 'worktrees');
this.worktreeBase = join(dirname(this.repoRoot), '.ruflo-worktrees', basename(this.repoRoot));
}
prepare(
runId: string,
agents: Array<{ id: string; readOnly?: boolean }>,
options: { allowDirty?: boolean; baseRef?: string } = {},
): WorktreeRunRecord {
assertId(runId, 'run id');
if (!agents.length) throw new Error('at least one agent is required');
const unique = new Set<string>();
for (const agent of agents) {
assertId(agent.id, 'agent id');
if (unique.has(agent.id)) throw new Error(`duplicate agent id: ${agent.id}`);
unique.add(agent.id);
}
const registryPath = this.registryPath(runId);
if (existsSync(registryPath)) return this.status(runId);
if (!options.allowDirty && git(this.repoRoot, ['status', '--porcelain']).length > 0) {
throw new Error('refusing to prepare writing worktrees from a dirty repository');
}
mkdirSync(this.registryDir, { recursive: true });
mkdirSync(join(this.worktreeBase, runId), { recursive: true });
const assignments: WorktreeAssignment[] = [];
try {
for (const agent of agents) {
const readOnly = agent.readOnly === true;
const branch = `ruflo/${runId}/${agent.id}`;
const worktreePath = join(this.worktreeBase, runId, agent.id);
if (readOnly) {
git(this.repoRoot, ['worktree', 'add', '--detach', worktreePath, options.baseRef ?? 'HEAD']);View on GitHub (pinned to fa13ee4ad6)
Solutions
- Deduplicate or rename agents before calling prepare(): suffix ids with an index (coder-1, coder-2) derived from the loop counter
- If two workers were meant to share one checkout, keep a single entry (extra readers can be marked readOnly) instead of duplicating the id
- Assert uniqueness early with new Set(agents.map(a => a.id)).size === agents.length so the failure points at your caller code, not deep inside the library
Example fix
// before
const agents = roles.flatMap(r => [{ id: r.name }, { id: r.name }]);
coordinator.prepare('run-1', agents); // throws: duplicate agent id
// after
const agents = roles.map((r, i) => ({ id: `${r.name}-${i + 1}` }));
coordinator.prepare('run-1', agents); Defensive patterns
Strategy: validation
Validate before calling
const SAFE_ID = /^[a-z0-9][a-z0-9._-]{0,63}$/;
function assertPreparable(runId: string, agents: Array<{ id: string }>): void {
const ids = agents.map(a => a.id);
if (new Set(ids).size !== ids.length) {
const dup = ids.find((id, i) => ids.indexOf(id) !== i);
throw new Error(`agents contain a duplicate id: ${dup}`);
}
for (const id of ids) if (!SAFE_ID.test(id)) throw new Error(`agent id not SAFE_ID: ${id}`);
}
assertPreparable(runId, agents);
coordinator.prepare(runId, agents); Prevention
- Derive agent ids from a guaranteed-unique source such as run id + loop index
- Generate ids from the SAFE_ID charset ([a-z0-9][a-z0-9._-]{0,63}) so they also pass assertId
When it happens
Trigger: prepare('run-1', [{ id: 'coder-1' }, { id: 'coder-1' }]) — any agents array containing two entries with the same id string. Ids must also match SAFE_ID /^[a-z0-9][a-z0-9._-]{0,63}$/ (enforced separately by assertId).
Common situations: Generating agent rosters in a loop that resets its counter, spawning two role-named agents ('coder') without an index suffix, or copy-pasting an agent list entry when adding a reviewer.
Related errors
- repoRoot must be the git top-level: ${top}
- duplicate repository path: ${path}
- repoPath must be the Git top-level: ${top}
- invalid ${label}: ${value}
- at least one agent is required
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/8e6f85579abd748b.
Report an issue: GitHub.