{"record":{"id":"f6bedbb2fa10de21","repo":"JuliusBrussee/caveman","slug":"cave-subagent-concurrency-limit","errorCode":"cave_subagent_concurrency_limit","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/3ee70a102609e550bd2e68004bf5990a9341c851/packages/agent/src/runtime.ts#L3816-L3852","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before: dispatch all subagents at once\nawait Promise.all(tasks.map(t => dispatchSubagent(t)));\n// after: limit in-flight subagents\nawait pLimit(maxConcurrent, tasks.map(t => () => dispatchSubagent(t)));","handlingStrategy":"try-catch","validationCode":"const ledger = getInvocationLedger(executionContext.invocationState);\nif (ledger.maxConcurrent !== undefined && ledger.active >= ledger.maxConcurrent) {\n  throw new Error(\"would exceed subagent concurrency limit\");\n}","typeGuard":null,"tryCatchPattern":"try {\n  await dispatchSubagent({ task });\n} catch (e) {\n  if (e.message === \"cave_subagent_concurrency_limit\") {\n    await waitForSubagentSlot(); // then retry\n  } else throw e;\n}","preventionTips":["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"],"tags":["subagent","concurrency","limit"],"backgroundTag":"rate-limit-exceeded","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}