{"record":{"id":"1fa3d40a5cdfef9a","repo":"JuliusBrussee/caveman","slug":"cave-subagent-spend-evidence-incomplete","errorCode":"cave_subagent_spend_evidence_incomplete","errorMessage":"cave_subagent_spend_evidence_incomplete","messagePattern":"cave_subagent_spend_evidence_incomplete","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/agent/src/runtime.ts","lineNumber":2101,"sourceCode":"        yield { type: \"pi\", runId, event: queued.shift()! };\n      }\n    }\n    await execution;\n    if (efficiencyPlan && reasoningUsageUnavailable) {\n      throw new Error(\"cave_reasoning_usage_unavailable\");\n    }\n    if (ladderFailure) throw ladderFailure;\n    if (spendFailure) throw spendFailure;\n    if (usageFailure !== undefined &&\n        (efficiencyPlan !== undefined ||\n          executionContext.spendLedgers.length > 0 ||\n          usageFailure.message === \"cave_provider_model_identity_mismatch\" ||\n          usageFailure.message === \"cave_provider_identity_missing\")) {\n      throw usageFailure;\n    }\n    if (pendingSpendReservations.length > 0) {\n      markSpendIncomplete(executionContext.spendLedgers);\n      throw new Error(\"cave_subagent_spend_evidence_incomplete\");\n    }\n    // A run stopped before its first call has no assistant message, and that is\n    // the honest outcome rather than missing evidence: the caller gets an empty\n    // answer, zero usage, and the reason the runtime declined to spend.\n    if (!finalMessage && stopReason === undefined) {\n      throw new Error(\"cave_incomplete_evidence: Pi emitted no final assistant message\");\n    }\n    if (finalMessage &&\n        (finalMessage.stopReason === \"error\" || finalMessage.stopReason === \"aborted\")) {\n      throw new Error(`cave_provider_terminal_${finalMessage.stopReason}`);\n    }\n    const text = finalMessage === undefined ? \"\" : assistantText(finalMessage);\n    if (definition.output?.schema && finalMessage !== undefined) {\n      let parsed: unknown;\n      try {\n        parsed = JSON.parse(text);\n      } catch {\n        throw new Error(\"cave_output_schema_invalid_json\");","sourceCodeStart":2083,"sourceCodeEnd":2119,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/agent/src/runtime.ts#L2083-L2119","documentation":"'cave_subagent_spend_evidence_incomplete' is thrown when the run ends while spend reservations are still pending (pendingSpendReservations.length > 0), meaning provider responses never arrived that could be settled against reserved spend (runtime.ts:2099-2101). The runtime marks the spend ledgers incomplete and fails closed so a subagent cannot report a result whose cost evidence is missing.","triggerScenarios":"A subagent (or nested) run that reserved spend for provider calls but terminated before those calls' assistant messages were accounted — e.g. the provider stream errored or the run was aborted mid-call after reservation, leaving reservations unsettled when streamAgentInternal reaches its final checks.","commonSituations":"Provider connection drops mid-subagent-call; caller aborts the run signal after a nested spawn reserved its turn; a provider bug yields assistant messages that never flow through the accounting path; timeout kills a call between reservation and response.","solutions":["Check RunResult/receipt and provider connectivity for the call that died between reservation and settlement; fix the underlying provider/stream failure first.","Retry the run; reservations are per-run, so a fresh run rebuilds its ledger.","Remove the caller abort (options.signal) trigger if the abort fires between subagent spawn and first response.","If reproducible, inspect markSpendIncomplete ledger output in the receipt to identify which call never settled."],"exampleFix":"// before: aborting while a subagent call is still in flight leaves reservations unsettled\nconst run = streamAgent(agent, input, { signal: controller.signal });\nsetTimeout(() => controller.abort(), 1000);\n\n// after: abort only after the stream settles, or use deadlineMs for bounded runs\nconst result = await streamAgent(agent, input, { deadlineMs: 30000 });","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  await runAgent(agent, input);\n} catch (e) {\n  if (e instanceof Error && e.message === \"cave_subagent_spend_evidence_incomplete\") {\n    // a subagent died between spend reservation and settlement — retry the run\n    await runAgent(agent, input);\n  }\n}","preventionTips":["Don't abort the run signal between subagent spawn and first provider response.","Use deadlineMs instead of external aborts so stops land between calls.","Monitor provider stream stability; flaky connections leave reservations unsettled.","Inspect receipt spend ledgers after failures to identify the unsettled call."],"tags":["spend-ledger","subagent","evidence","fail-closed"],"backgroundTag":"internal-invariant-violation","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"}