JuliusBrussee/caveman · error
cave_subagent_concurrency_limit
Error message
cave_subagent_concurrency_limit
What it means
Thrown by admitSubagent when the number of currently active descendants has reached ledger.maxConcurrent. It enforces RunOptions.maxConcurrentSubagents: simultaneous active subagent executions across the whole tree. Unlike the invocation cap, capacity is released after success, error, or abort, so the condition is transient and a retry can succeed once a slot frees.
Source
Thrown at packages/agent/src/runtime.ts:3834
const body = appliedPlan.bodies.get(segment.bodyHandle);
if (!body) throw new Error(`cave_context_body_missing:tool.${definition.name}`);
const parsed = JSON.parse(new TextDecoder().decode(body)) as unknown;
if (!isRecord(parsed) || parsed.name !== definition.name ||
typeof parsed.description !== "string" || !isRecord(parsed.input)) {
throw new Error(`cave_tool_schema_invalid:${definition.name}`);
}
return { description: parsed.description, input: parsed.input as TSchema };
}
function admitSubagent(state: InvocationState): () => void {
const ledger = state.ledger;
if (ledger.maxInvocations !== undefined && ledger.admitted >= ledger.maxInvocations) {
ledger.invocationRejections++;
throw new Error("cave_subagent_invocation_limit");
}
if (ledger.maxConcurrent !== undefined && ledger.active >= ledger.maxConcurrent) {
ledger.concurrencyRejections++;
throw new Error("cave_subagent_concurrency_limit");
}
ledger.admitted++;
ledger.active++;
ledger.peakActive = Math.max(ledger.peakActive, ledger.active);
let released = false;
return () => {
if (released) return;
released = true;
ledger.active = Math.max(0, ledger.active - 1);
};
}
function childInvocationTrace(parent: InvocationTrace): InvocationTrace {
return Object.freeze({
traceId: parent.traceId,
spanId: randomBytes(8).toString("hex"),
parentSpanId: parent.spanId,
});View on GitHub (pinned to 27d5a3981a)
Solutions
- Retry the subagent call after in-flight children settle — active capacity is released on success, error, or abort.
- Bound the tool's own parallelism (e.g. process in batches of maxConcurrentSubagents) so admissions never exceed the cap.
- Raise RunOptions.maxConcurrentSubagents if the host can genuinely sustain more simultaneous children.
Example fix
// before await Promise.all(items.map((item) => agent.tools.dispatch(item))); // 10 at once, cap is 4 // after const batches = chunk(items, maxConcurrentSubagents); for (const batch of batches) await Promise.all(batch.map((item) => agent.tools.dispatch(item)));
Defensive patterns
Strategy: retry
Validate before calling
// Before spawning child K+1, track in-flight children yourself:
if (opts.maxConcurrentSubagents !== undefined && inflight >= opts.maxConcurrentSubagents) {
await Promise.race([settledChild(), sleep(0)]); // wait for a slot instead of racing the ledger
} Type guard
const isConcurrencyLimit = (e: unknown): boolean => e instanceof Error && e.message === "cave_subagent_concurrency_limit";
Try / catch
async function withSlot<T>(spawn: () => Promise<T>, maxAttempts = 3): Promise<T> {
for (let attempt = 1; ; attempt++) {
try {
return await spawn();
} catch (error) {
if (error instanceof Error && error.message === "cave_subagent_concurrency_limit" && attempt < maxAttempts) {
await new Promise((r) => setTimeout(r, 250 * attempt)); // slots free as children settle
continue;
}
throw error;
}
}
} Prevention
- Batch parallel spawns to at most maxConcurrentSubagents instead of racing the ledger.
- Remember capacity is released on success, error, and abort — backoff-then-retry is correct, permanent failure is not.
- Distinguish this from the invocation limit: concurrency rejections are transient, invocation-limit rejections are permanent for the run.
When it happens
Trigger: RunOptions.maxConcurrentSubagents set to K while K subagents are still running and another admission is attempted (parallel tool dispatch spawning a (K+1)-th concurrent child).
Common situations: A fan-out tool maps over N items and spawns N children while maxConcurrentSubagents is smaller; long-running children hold slots and later admissions are rejected mid-run.
Related errors
- cave_subagent_invocation_limit
- caveman agent: subagent maxInputChars must be a positive int
- cave_subagent_framework_runner_required
- option not found
- cave_harness_adapter_version_invalid
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/f6bedbb2fa10de21.
Report an issue: GitHub.