JuliusBrussee/caveman · error
cave_subagent_spend_evidence_incomplete
cave_subagent_spend_evidence_incomplete
Error message
cave_subagent_spend_evidence_incomplete
What it means
Thrown when the run ends while pendingSpendReservations is non-empty: a spend reservation was taken for a provider call but never settled with usage evidence. The ledgers are marked incomplete first, so spend accounting fails closed rather than silently under-reporting.
Source
Thrown at packages/agent/src/runtime.ts:2101
yield { type: "pi", runId, event: queued.shift()! };
}
}
await execution;
if (efficiencyPlan && reasoningUsageUnavailable) {
throw new Error("cave_reasoning_usage_unavailable");
}
if (ladderFailure) throw ladderFailure;
if (spendFailure) throw spendFailure;
if (usageFailure !== undefined &&
(efficiencyPlan !== undefined ||
executionContext.spendLedgers.length > 0 ||
usageFailure.message === "cave_provider_model_identity_mismatch" ||
usageFailure.message === "cave_provider_identity_missing")) {
throw usageFailure;
}
if (pendingSpendReservations.length > 0) {
markSpendIncomplete(executionContext.spendLedgers);
throw new Error("cave_subagent_spend_evidence_incomplete");
}
// A run stopped before its first call has no assistant message, and that is
// the honest outcome rather than missing evidence: the caller gets an empty
// answer, zero usage, and the reason the runtime declined to spend.
if (!finalMessage && stopReason === undefined) {
throw new Error("cave_incomplete_evidence: Pi emitted no final assistant message");
}
if (finalMessage &&
(finalMessage.stopReason === "error" || finalMessage.stopReason === "aborted")) {
throw new Error(`cave_provider_terminal_${finalMessage.stopReason}`);
}
const text = finalMessage === undefined ? "" : assistantText(finalMessage);
if (definition.output?.schema && finalMessage !== undefined) {
let parsed: unknown;
try {
parsed = JSON.parse(text);
} catch {
throw new Error("cave_output_schema_invalid_json");View on GitHub (pinned to 27d5a3981a)
Solutions
- Identify why the run terminated between reservation and settlement: check ladderFailure/spendFailure/usageFailure which are thrown earlier and are the root cause
- If aborting mid-call is intentional in your app, catch this error and treat the run as incomplete (the ledger is already marked incomplete for you)
- Retry the run — reservations are per-run, so a fresh run re-reserves cleanly
- Audit custom streamFn wrappers to ensure they don't swallow usage events after the call starts
Defensive patterns
Strategy: try-catch
Type guard
function isSpendEvidenceIncomplete(e: unknown): e is Error {
return e instanceof Error && e.message === "cave_subagent_spend_evidence_incomplete";
} Try / catch
try {
await agent.run(input, opts);
} catch (e) {
if (isSpendEvidenceIncomplete(e)) {
// ledger already marked incomplete; treat run as failed/incomplete and retry fresh
} else throw e;
} Prevention
- Don't abort runs mid-call unless you accept incomplete spend accounting
- Fix earlier failures (ladder/spend/usage) that terminate runs between reservation and settlement
- Treat this error as a symptom — always log the preceding failure too
When it happens
Trigger: A provider call is reserved and then the run terminates (error, abort, exception in the stream) before the reservation is spent or released — e.g. streamFn throws mid-call, an AbortSignal fires between reservation and settlement, or a nested subagent run dies with unsettled reservations.
Common situations: AbortSignal triggered mid-request; provider connection drop after reservation; a bug in a custom streamFn that throws after the runtime reserved budget; nested subagent failures bubbling up.
Related errors
- cave_retry_accounting_invalid
- cave_budget_reservation_double_settle
- cave_nested_usage_incomplete
- cave_provider_terminal
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/1fa3d40a5cdfef9a.
Report an issue: GitHub.