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

  1. Identify why the run terminated between reservation and settlement: check ladderFailure/spendFailure/usageFailure which are thrown earlier and are the root cause
  2. 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)
  3. Retry the run — reservations are per-run, so a fresh run re-reserves cleanly
  4. 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

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


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/1fa3d40a5cdfef9a. Report an issue: GitHub.