JuliusBrussee/caveman · error
cave_subagent_concurrency_limit
cave_subagent_concurrency_limit
Error message
cave_subagent_concurrency_limit
What it means
This library enforces per-invocation-state concurrency ledgers for subagent dispatch. The admitSubagent gate throws cave_subagent_concurrency_limit when the number of currently active subagents equals the configured maxConcurrent cap, refusing admission until an earlier subagent releases its slot. It is a deliberate back-pressure mechanism to prevent unbounded parallel subagent fan-out.
Solutions
- Reduce the number of subagent tool calls dispatched in parallel in a single turn
- Raise the maxConcurrent limit in the runtime/subagent configuration
- Wait for previously dispatched subagents to finish before dispatching more (their release() decrements ledger.active)
- Check for leaked admission slots: if peakActive never decrements, ensure subagent runs always release via the returned release function, including on error
Example fix
// before: dispatch all subagents at once await Promise.all(tasks.map(t => dispatchSubagent(t))); // after: limit in-flight subagents await pLimit(maxConcurrent, tasks.map(t => () => dispatchSubagent(t)));
Defensive patterns
Strategy: try-catch
Validate before calling
const ledger = getInvocationLedger(executionContext.invocationState);
if (ledger.maxConcurrent !== undefined && ledger.active >= ledger.maxConcurrent) {
throw new Error("would exceed subagent concurrency limit");
} Try / catch
try {
await dispatchSubagent({ task });
} catch (e) {
if (e.message === "cave_subagent_concurrency_limit") {
await waitForSubagentSlot(); // then retry
} else throw e;
} Prevention
- Use a semaphore/p-limit around subagent dispatch sized to maxConcurrent
- Track in-flight subagent count before issuing tool calls
- Always release the admission slot in a finally block
- Monitor ledger.peakActive to size maxConcurrent correctly
When it happens
Trigger: Calling a subagent tool while ledger.active >= ledger.maxConcurrent for the current invocationState — e.g. dispatching more parallel subagent tool calls in one assistant turn than the runtime's maxConcurrent subagent setting allows.
Common situations: Orchestrator prompts that spawn N subagents in parallel where N exceeds the configured cap; a stuck subagent that never released its admission slot; shared invocationState reused across turns so slots are still held; misconfigured maxConcurrent left at a small default.
Related errors
- cave_subagent_call_budget
- cave_subagent_context_budget
- cave_subagent_depth_limit
- cave_subagent_input_limit
- MCP transaction state kept changing; refusing mutation
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/f6bedbb2fa10de21.
Report an issue: GitHub.
Appendix: 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 3ee70a1026)