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

  1. Retry the subagent call after in-flight children settle — active capacity is released on success, error, or abort.
  2. Bound the tool's own parallelism (e.g. process in batches of maxConcurrentSubagents) so admissions never exceed the cap.
  3. 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

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


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/f6bedbb2fa10de21. Report an issue: GitHub.