{"record":{"id":"f6bedbb2fa10de21","repo":"JuliusBrussee/caveman","slug":"cave-subagent-concurrency-limit","errorCode":null,"errorMessage":"cave_subagent_concurrency_limit","messagePattern":"cave_subagent_concurrency_limit","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/agent/src/runtime.ts","lineNumber":3834,"sourceCode":"  const body = appliedPlan.bodies.get(segment.bodyHandle);\n  if (!body) throw new Error(`cave_context_body_missing:tool.${definition.name}`);\n  const parsed = JSON.parse(new TextDecoder().decode(body)) as unknown;\n  if (!isRecord(parsed) || parsed.name !== definition.name ||\n      typeof parsed.description !== \"string\" || !isRecord(parsed.input)) {\n    throw new Error(`cave_tool_schema_invalid:${definition.name}`);\n  }\n  return { description: parsed.description, input: parsed.input as TSchema };\n}\n\nfunction admitSubagent(state: InvocationState): () => void {\n  const ledger = state.ledger;\n  if (ledger.maxInvocations !== undefined && ledger.admitted >= ledger.maxInvocations) {\n    ledger.invocationRejections++;\n    throw new Error(\"cave_subagent_invocation_limit\");\n  }\n  if (ledger.maxConcurrent !== undefined && ledger.active >= ledger.maxConcurrent) {\n    ledger.concurrencyRejections++;\n    throw new Error(\"cave_subagent_concurrency_limit\");\n  }\n  ledger.admitted++;\n  ledger.active++;\n  ledger.peakActive = Math.max(ledger.peakActive, ledger.active);\n  let released = false;\n  return () => {\n    if (released) return;\n    released = true;\n    ledger.active = Math.max(0, ledger.active - 1);\n  };\n}\n\nfunction childInvocationTrace(parent: InvocationTrace): InvocationTrace {\n  return Object.freeze({\n    traceId: parent.traceId,\n    spanId: randomBytes(8).toString(\"hex\"),\n    parentSpanId: parent.spanId,\n  });","sourceCodeStart":3816,"sourceCodeEnd":3852,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/packages/agent/src/runtime.ts#L3816-L3852","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// before\nawait Promise.all(items.map((item) => agent.tools.dispatch(item))); // 10 at once, cap is 4\n\n// after\nconst batches = chunk(items, maxConcurrentSubagents);\nfor (const batch of batches) await Promise.all(batch.map((item) => agent.tools.dispatch(item)));","handlingStrategy":"retry","validationCode":"// Before spawning child K+1, track in-flight children yourself:\nif (opts.maxConcurrentSubagents !== undefined && inflight >= opts.maxConcurrentSubagents) {\n  await Promise.race([settledChild(), sleep(0)]); // wait for a slot instead of racing the ledger\n}","typeGuard":"const isConcurrencyLimit = (e: unknown): boolean =>\n  e instanceof Error && e.message === \"cave_subagent_concurrency_limit\";","tryCatchPattern":"async function withSlot<T>(spawn: () => Promise<T>, maxAttempts = 3): Promise<T> {\n  for (let attempt = 1; ; attempt++) {\n    try {\n      return await spawn();\n    } catch (error) {\n      if (error instanceof Error && error.message === \"cave_subagent_concurrency_limit\" && attempt < maxAttempts) {\n        await new Promise((r) => setTimeout(r, 250 * attempt)); // slots free as children settle\n        continue;\n      }\n      throw error;\n    }\n  }\n}","preventionTips":["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."],"tags":["subagent","concurrency","limits","runtime"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}