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

  1. Deduplicate or rename agents before calling prepare(): suffix ids with an index (coder-1, coder-2) derived from the loop counter
  2. If two workers were meant to share one checkout, keep a single entry (extra readers can be marked readOnly) instead of duplicating the id
  3. 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

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


AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18). Data as JSON: /api/errors/8e6f85579abd748b. Report an issue: GitHub.