ruvnet/ruflo · error · Error

duplicate or empty task id: ${task.id}

Error message

duplicate or empty task id: ${task.id}

What it means

runBoundedPool walks all tasks up front and requires every task.id to be non-empty and unique within the batch. A duplicate or empty id throws before scheduling, preserving the invariant that result order maps to task ids deterministically. The check uses a Set, so the first repeat (or empty string) aborts the run.

Source

Thrown at v3/@claude-flow/cli/src/services/bounded-worker-pool.ts:36

/**
 * Deterministic bounded worker pool for Codex/MetaHarness fanout.
 *
 * Completion order never affects result order. The caller-supplied AbortSignal
 * and timeout cancel both queued and cooperative running work. No unbounded
 * Promise.all is used.
 */
export async function runBoundedPool<T>(
  tasks: readonly BoundedTask<T>[],
  options: { maxConcurrency: number; timeoutMs?: number; signal?: AbortSignal },
): Promise<BoundedPoolResult<T>> {
  const started = Date.now();
  if (!Number.isInteger(options.maxConcurrency) || options.maxConcurrency < 1) {
    throw new Error('maxConcurrency must be a positive integer');
  }
  const ids = new Set<string>();
  for (const task of tasks) {
    if (!task.id || ids.has(task.id)) throw new Error(`duplicate or empty task id: ${task.id}`);
    ids.add(task.id);
  }
  const maxConcurrency = Math.min(options.maxConcurrency, tasks.length || 1);
  const controller = new AbortController();
  const onAbort = () => controller.abort(options.signal?.reason ?? new Error('cancelled'));
  options.signal?.addEventListener('abort', onAbort, { once: true });
  const timer = options.timeoutMs && options.timeoutMs > 0
    ? setTimeout(() => controller.abort(new Error('worker-pool-timeout')), options.timeoutMs)
    : undefined;

  const results = new Map<string, BoundedTaskResult<T>>();
  let cursor = 0;
  let active = 0;
  let peakConcurrency = 0;

  const worker = async (): Promise<void> => {
    while (cursor < tasks.length) {
      const taskIndex = cursor++;

View on GitHub (pinned to 6b01dc5a68)

Solutions

  1. Generate ids from a guaranteed-unique source (crypto.randomUUID, or composite key like `${namespace}:${key}`).
  2. Deduplicate tasks by id before calling runBoundedPool.
  3. Validate non-empty and unique ids at the producer side.

Example fix

// before: index-based ids collide after a refactor
tasks.push({ id: item.name, run }); // two items share name -> throws

// after: composite unique id
import { randomUUID } from 'node:crypto';
tasks.push({ id: `${item.type}:${item.name}:${randomUUID()}`, run });
Defensive patterns

Strategy: validation

Validate before calling

function dedupeTaskIds(tasks) {
  const seen = new Set();
  for (const t of tasks) {
    if (!t.id || seen.has(t.id)) throw new Error(`duplicate or empty task id: ${t.id}`);
    seen.add(t.id);
  }
  return tasks;
}

Type guard

function tasksHaveUniqueIds(tasks): boolean {
  const ids = tasks.map(t => t.id).filter(Boolean);
  return ids.length === tasks.length && new Set(ids).size === ids.length;
}

Prevention

When it happens

Trigger: Two tasks sharing the same id (e.g., generated from an index that reset, or a non-unique key); a task with id '' (default when a field is missing); tasks built from a map without guaranteeing key uniqueness.

Common situations: Generating ids from filenames that collide; using array index as id but two tasks reference the same logical unit; refactoring that dropped the id assignment; concurrent producers appending to the same task list with overlapping keys.

Related errors


AI-assisted analysis of ruvnet/ruflo@6b01dc5a68 (2026-08-12). Data as JSON: /api/errors/048b240da14f463f. Report an issue: GitHub.