# ADR 0110: A reconciled cost belongs to the application, not to the core

> Source: https://github.com/max-listov/stitchkit/blob/master/docs/decisions/0110-a-reconciled-cost-belongs-to-the-application.md

---

# ADR 0110 — A reconciled cost belongs to the application

## Context

ADR 0109 made the runtime's spend figure honest: a total says it was added up, an
unknown says `unavailable`, and a run that ended before the provider reported
anything says so instead of staying silent. That leaves a real question open.
Whether a provider bills for a call aborted mid-flight cannot be known inside the
process — the authoritative number arrives later, from the provider's own
accounting. A figure reported `unavailable` stays `unavailable` forever unless
something can attach the real one afterwards.

The obvious shape is to amend the terminal run. It runs into the store's central
invariant. A terminal run is an absorbing state: `commitRunTerminal` requires
`running | interrupt_requested`, `checkpointRunAssistant` requires `running`,
`recoverRun` requires `queued | running | interrupt_requested`. Nothing may touch
a run once it is terminal, and the fencing and CAS design rests on that.

Worse than the guard is the coupling. Every mutation compare-and-swaps the
**conversation head**, so a late billing write would bump the conversation
version and make a concurrent `replaceCompactedRange` conflict — and a
conflicting compaction discards the summary it just paid a model to produce. **An
accounting write would be able to throw away a model-generated summary.** That is
decisive on its own, independent of taste.

The append-only alternative avoids the coupling and costs more than the feature:
a new driver namespace, a new table, its own pagination and its own entry in the
public conformance kit — a second write path every adapter must implement. The
cheap shape has the bad coupling and the good shape is expensive, which is itself
a signal about where the feature belongs.

## Decision

**A cost learned after a run closed is joined in the application's ledger, keyed
by `runId`. The core neither stores it nor accepts it.**

The core's duty is to report what the process observed and to name what it did
not. Nothing else can see that; the core is the only place it can come from. An
externally-sourced authoritative number is a different kind of fact — it is
accounting, and ADR 0002 keeps billing out of the core.

The tell is the question the alternative needs answered next: *what happens to a
reconciled figure for a run the application can no longer find?* That is a
retention question. Retention is a ledger's problem, not a runtime's, and a
runtime that has to answer it has become one.

Everything reconciliation needs is already there and already durable: a stable,
externally-resolvable `runId`, and — since ADR 0109 — a per-run figure that is
either true or honestly `unavailable`. The application writes its own spend row
keyed by that id and updates it when the provider's accounting arrives.

**If this is ever revisited**, the shape is fixed in advance by the argument
above: append-only, keyed by `runId`, **outside** the conversation aggregate and
with no head CAS — precisely so accounting can never conflict with a conversation
operation. It would need its own ADR superseding this one.

## Consequences

Three requests follow this one predictably, and all three are refused here rather
than argued again later:

- **The core records a currency; it never converts one.** `AgentCostValue`
  carries an ISO code so a reader knows what it is holding. Two costs in
  different currencies report `unavailable` rather than a converted total.
- **The core never aggregates.** No sums by user, model, day or purpose. A spend
  figure belongs to one run.
- **The core never enforces a budget.** Refusing work because a limit was reached
  is a product decision, and ADR 0002's "no billing in the core" is the literal
  precedent.

What the core does owe, and what stays open, is that the figure it reports should
survive the channel that carries it — today usage exists only on two bounded,
drop-on-overflow sinks with no durable counterpart. That is a reporting duty, not
a ledger, and it is tracked separately.

## Alternatives considered

**Amend the terminal run.** Cheapest for an adapter — the run is an opaque JSON
payload, so a new optional field needs no migration — and it is the one that lets
an accounting write conflict a compaction into discarding a paid-for summary.

**An append-only amendment log inside the store.** Correct in isolation and a
second write path for every adapter, to hold data the core has no other reason to
know.

**Do nothing and leave `unavailable` as a dead end.** This is what is chosen —
but only because it is *not* a dead end: the application joins on `runId`. It
would be one if the core had no stable external identity for a run, and it does.
