{"record":{"id":"4135bea9d9548534","repo":"JuliusBrussee/caveman","slug":"cave-subagent-wallet-unavailable","errorCode":"cave_subagent_wallet_unavailable","errorMessage":"cave_subagent_wallet_unavailable","messagePattern":"cave_subagent_wallet_unavailable","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/agent/src/runtime.ts","lineNumber":3897,"sourceCode":"  const depthLimit = Math.min(\n    parentOptions.maxSubagentDepth ?? DEFAULT_SUBAGENT_DEPTH_LIMIT,\n    ABSOLUTE_SUBAGENT_DEPTH_LIMIT,\n  );\n  if (depth + 1 > depthLimit) throw new Error(\"cave_subagent_depth_limit\");\n  // The wallet is carved here, still synchronously, for the same reason: two\n  // subagents dispatched in one turn must not both be funded out of the same\n  // remaining budget.\n  const walletAmount = parentMeter === undefined\n    ? undefined\n    : parentMeter.denomination === \"usd\" ? runtime.maxCostUsd : runtime.maxTokens;\n  if (parentMeter !== undefined && walletAmount === undefined) {\n    throw new Error(\"cave_subagent_wallet_denomination_unavailable\");\n  }\n  const carve = parentMeter === undefined || walletAmount === undefined\n    ? undefined\n    : parentMeter.carve(walletAmount);\n  if (parentMeter !== undefined && carve === undefined) {\n    throw new Error(\"cave_subagent_wallet_unavailable\");\n  }\n  let releaseAdmission: (() => void) | undefined;\n  try {\n    releaseAdmission = admitSubagent(executionContext.invocationState);\n    return await runSubagent({\n      toolDefinition,\n      runtime,\n      task,\n      signal,\n      parentOptions,\n      usage,\n      executionContext,\n      depth,\n      childMeter: carve?.child,\n      parentDeadlineAt,\n    });\n  } finally {\n    releaseAdmission?.();","sourceCodeStart":3879,"sourceCodeEnd":3915,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/766dce6b1394ebb56a3090748d5a0240a5aefb36/packages/agent/src/runtime.ts#L3879-L3915","documentation":"A subagent whose runtime declares a spend cap (maxCostUsd or maxTokens) must have that entire cap carved out of the parent's remaining budget as a wallet, synchronously at spawn, so parallel children cannot double-spend the same remainder. carve() (budget.ts:376) returns undefined when the parent meter is breached/revoked, when the amount is not finite/positive (or not a safe integer in token denomination), or when the amount exceeds the parent's remaining budget. This error means the parent cannot fund the child's wallet, so dispatch aborts before the child is admitted.","triggerScenarios":"Parent run started with RunOptions.budget (or maxCostUsd) and a subagent() declares maxCostUsd/maxTokens greater than the parent's remaining budget; dispatching two subagents in one turn whose combined caps exceed the remainder (the first carve consumes it); parent meter already capBreached or revoked; parent budget in tokens while the child's maxTokens is fractional/non-integer.","commonSituations":"Giving the child the same cap as the parent after the parent already spent on earlier calls; sizing wallets with zero headroom then adding one more parallel subagent; mixing denominations between parent budget and child cap; retrying a subagent dispatch after the parent budget was nearly drained.","solutions":["Lower the subagent's maxCostUsd/maxTokens below the parent's remaining budget (check the run receipt's spend/tranche history to see what remains)","Dispatch fewer subagents per turn, or size them so the sum of caps fits the remainder","Top up the parent budget via createBudgetController() checkpoints (release() is max-bounded) before dispatching more children","Align denominations: parent budget maxUsd pairs with child maxCostUsd, maxTokens pairs with integer maxTokens","Do not retry dispatch after a capBreached parent; inspect RunResult.capBreached/stopReason first"],"exampleFix":"// before: child cap equals parent budget already partly spent\nconst parent = agent({ ... });\nconst child = parent.subagent({ maxCostUsd: 5 }); // parent already spent $3 of $5\n\n// after: size wallet against remaining budget with headroom\nconst remaining = 5 - spentUsdFromReceipt;\nconst child = parent.subagent({ maxCostUsd: Math.max(0.5, remaining * 0.8) });","handlingStrategy":"try-catch","validationCode":"// Before dispatching budgeted subagents, size the wallet against remaining parent budget\nconst parentBudgetUsd = 5;\nconst spentUsd = sumSpentFromReceipt(rootReceipt); // roll up receipts of finished runs\nconst remaining = parentBudgetUsd - spentUsd;\nconst wallet = Math.min(subagentCap, remaining * 0.8); // headroom\nif (wallet <= 0) throw new Error('no parent budget left for subagent wallet');","typeGuard":null,"tryCatchPattern":"try {\n  const out = await child(task);\n} catch (error) {\n  if (error instanceof Error && error.message === 'cave_subagent_wallet_unavailable') {\n    // parent meter cannot fund this cap: shrink the wallet or stop dispatching\n    logger.warn('subagent wallet unfundable; remaining parent budget too small');\n    return fallbackWithoutSubagent();\n  }\n  throw error;\n}","preventionTips":["Size every subagent cap below the parent's remaining budget, never equal to the full parent budget","Sum caps of subagents dispatched in one turn and keep the total under the parent remainder (carves are synchronous and stacking)","Match denominations: parent maxUsd budget with child maxCostUsd, token budget with integer maxTokens","Check RunResult.capBreached/stopReason before dispatching further children"],"tags":["subagent","budget","wallet","metering"],"backgroundTag":"budget-insufficient-remaining","analyzedSha":"766dce6b1394ebb56a3090748d5a0240a5aefb36","analyzedAt":"2026-08-18T03:14:35.516Z","contentChangedAt":"2026-08-18T03:14:35.516Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}