ruvnet/ruflo · error
maxConcurrency must be a positive integer
Error message
maxConcurrency must be a positive integer
What it means
runBoundedPool validates its options before starting any work: options.maxConcurrency must be an integer >= 1 (Number.isInteger rejects 0, negatives, NaN, Infinity, and fractional values). The pool exists to make Codex/MetaHarness fanout deterministic and bounded, so an invalid concurrency limit is a caller bug and fails fast rather than degrading to unbounded execution.
Solutions
- Clamp and floor the value at the call site: const max = Math.max(1, Math.floor(Number(opts.concurrency) || DEFAULT))
- If deriving from tasks.length, guard the empty case explicitly before calling runBoundedPool
- Type the options object so maxConcurrency: number forces callers to resolve undefined/NaN earlier
Example fix
// before
await runBoundedPool(tasks, { maxConcurrency: tasks.length }); // throws when tasks is empty
// after
const maxConcurrency = Math.max(1, Math.floor(Number(options.concurrency ?? 4)));
const results = tasks.length
? await runBoundedPool(tasks, { maxConcurrency, timeoutMs: 30_000 })
: { results: [], cancelled: false, elapsedMs: 0 }; Defensive patterns
Strategy: validation
Validate before calling
const raw = Number(config.concurrency); // from env/config
const maxConcurrency = Number.isInteger(raw) && raw >= 1 ? raw : 4; // safe default
await runBoundedPool(tasks, { maxConcurrency: Math.min(maxConcurrency, tasks.length || 1), timeoutMs: 30_000 }); Type guard
const isValidConcurrency = (v: unknown): v is number => typeof v === 'number' && Number.isInteger(v) && v >= 1;
Prevention
- Never pass tasks.length unchecked — clamp with Math.max(1, ...)
- Parse env/config concurrency once at load time with a validated default, not at each call site
- Type options as { maxConcurrency: number } so undefined from optional config fails at compile time
When it happens
Trigger: Passing tasks.length as maxConcurrency when tasks is empty (0), reading concurrency from an env var or config without parsing ('4' is not an integer, undefined from a missing key), or computing it with arithmetic that yields NaN/fractional values (e.g. tasks.length / 2 without Math.floor).
Common situations: Dynamic fanout where the task list can be empty, config-driven concurrency with a typo'd key, division-based heuristics like Math.round(n/3) that can round to 0 for small n.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- duplicate or empty task id
- actualUsd must be a non-negative finite number
- Agent config must include id, name, and type
- agent must be an object
- Agent has reached max concurrent tasks
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/ea1cbc7172877816.
Report an issue: GitHub.
Appendix: source
Thrown at v3/@claude-flow/cli/src/services/bounded-worker-pool.ts:32
results: BoundedTaskResult<T>[];
peakConcurrency: number;
durationMs: number;
}
/**
* 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;View on GitHub (pinned to fa13ee4ad6)