ruvnet/ruflo · error

duplicate or empty task id

Error message

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

What it means

runBoundedPool requires every BoundedTask to carry a non-empty, unique id: results are collected into a Map keyed by id and completion order must not affect result order, so a duplicate id would silently overwrite one task's result and an empty id is unusable as a key. Validation runs over all tasks up front, before any work starts, and the error names the offending id.

Solutions

  1. Give each task a structurally unique id, e.g. `${source}:${name}` or crypto.randomUUID()
  2. If uniqueness across sources cannot be guaranteed, prefix by source before merging
  3. Assert on duplicates before calling: new Set(ids).size === tasks.length

Example fix

// before
const tasks = files.map(f => ({ id: path.basename(f), run: () => lint(f) }));
// two files named index.ts in different dirs -> duplicate id

// after
const tasks = files.map(f => ({ id: path.relative(root, f), run: () => lint(f) }));
Defensive patterns

Strategy: validation

Validate before calling

const ids = tasks.map(t => t.id);
if (ids.some(id => !id) || new Set(ids).size !== ids.length) {
  throw new Error('task ids must be non-empty and unique before runBoundedPool');
}

Type guard

const haveUniqueIds = (tasks: readonly { id: string }[]): boolean => {
  const seen = new Set<string>();
  return tasks.every(t => Boolean(t.id) && !seen.has(t.id) && seen.add(t.id));
};

Prevention

When it happens

Trigger: Building tasks with .map((x) => ({ id: x.name, ... })) where names repeat; reusing a constant id string; generating ids from array indices after filtering/deduping that leaves duplicates; forgetting to set id so it is undefined (falsy -> reported as 'empty').

Common situations: Fanout over work items keyed by non-unique fields (file basenames, issue titles), merging task lists from multiple sources without re-keying, refactors that renamed the id field and left it unset.

Related errors


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

Appendix: 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 fa13ee4ad6)