JuliusBrussee/caveman · error

cave_subagent_spend_evidence_incomplete

cave_subagent_spend_evidence_incomplete

Error message

cave_subagent_spend_evidence_incomplete

What it means

'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.

Solutions

  1. Check RunResult/receipt and provider connectivity for the call that died between reservation and settlement; fix the underlying provider/stream failure first.
  2. Retry the run; reservations are per-run, so a fresh run rebuilds its ledger.
  3. Remove the caller abort (options.signal) trigger if the abort fires between subagent spawn and first response.
  4. If reproducible, inspect markSpendIncomplete ledger output in the receipt to identify which call never settled.

Example fix

// before: aborting while a subagent call is still in flight leaves reservations unsettled
const run = streamAgent(agent, input, { signal: controller.signal });
setTimeout(() => controller.abort(), 1000);

// after: abort only after the stream settles, or use deadlineMs for bounded runs
const result = await streamAgent(agent, input, { deadlineMs: 30000 });
Defensive patterns

Strategy: retry

Try / catch

try {
  await runAgent(agent, input);
} catch (e) {
  if (e instanceof Error && e.message === "cave_subagent_spend_evidence_incomplete") {
    // a subagent died between spend reservation and settlement — retry the run
    await runAgent(agent, input);
  }
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/1fa3d40a5cdfef9a. Report an issue: GitHub.

Appendix: 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 3ee70a1026)