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
- Generate ids from a guaranteed-unique source (crypto.randomUUID, or composite key like `${namespace}:${key}`).
- Deduplicate tasks by id before calling runBoundedPool.
- 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
- Generate task ids from a guaranteed-unique source (crypto.randomUUID or composite key).
- Dedupe tasks by id before submitting to the pool.
- Validate ids at the producer so empty/duplicate ids never reach runBoundedPool.
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
- Must provide name and embeddings array
- maxConcurrency must be a positive integer
- duplicate anchor task id: ${task.id}
- unknown game "${key}". Known: ${Object.keys(GAMES).join(', '
- unknown strategy "${name}". Available: ${roster.map((r) => r
AI-assisted analysis of ruvnet/ruflo@6b01dc5a68 (2026-08-12).
Data as JSON: /api/errors/048b240da14f463f.
Report an issue: GitHub.