Files
PaperClipAI/doc
DottaandClaude Opus 5.5 06bbcea12f fix(budgets): price lost receipts and stop restart-derived accounting from hard-stopping companies (#19673)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Budget admission and run accounting decide whether any task in a
company may start
> - #19497 made pending accounting an admission hold and settles a
stopped run's lost receipt as an unpriced charge
> - Restart-chaos waves still stopped whole companies. A lost Codex
receipt with complete token counts settled as unpriced because the rate
card only prices complete receipts. Under the default blocking policy
that hard-stopped the company at 0.38 of 3000 cents. In another storm, a
resumed run's continuation was refused by its own task's
recovering-accounting hold. Separately, unpriced charges with zero
tokens or a known partial cost hard-stopped a company at 120 of 2000
cents
> - This pull request prices a lost receipt's observed usage from the
rate card, lets a task's continuation pass its own recovering
accounting, and stops restart-derived or evidence-free unpriced charges
from tripping the blocking policy
> - The benefit is that a restart no longer pauses a company by itself,
while genuinely unpriced usage still blocks

## Linked Issues or Issue Description

Follow-up to #19497. Refs #19606.

**What happened?**
- Wave A (K6 crash loop): native Codex run `5037b8d1` was killed
mid-turn. Its receipt was lost, but `usage_json` held gpt-6.1-sol with
28690 input and 220 output tokens. The charge was written
`cost_status=unpriced`, `pricing_provenance.source=unknown`, because
`priceCodexReceipt` returns early when `!receipt.complete`. 0.2 s later
the default `unpricedUsagePolicy=block` opened a hard incident at 0.38
of 3000 cents. The company was paused, and 6/6 tasks were blocked with
`canRetry=false`.
- Wave B (K1 storm): Claude run `2a7a8abf` resumed after the restart.
`reserveRunBudget` refused its interaction continuation with "New work
must wait while a native run recovers its unfinished accounting", so the
task could not settle its own recovery. A 0-token unpriced charge and an
unpriced charge with a known 0.81-cent cost then hard-stopped the
company at 120 of 2000 cents.

**What did you expect?**
A restart-lost receipt with known numbers is priced as an estimate, and
no restart-derived accounting state pauses a company by itself. A task's
own continuation is never held by its own accounting. Real usage above
the limit, and genuinely unpriced usage, still stop work.

**Steps to reproduce**
1. Give a company a hard-stop budget with the default unpriced-usage
policy.
2. Kill a native Codex turn after it has reported tokens, so the final
receipt is lost and native recovery ends in terminal failure with a
verified stop.
3. Observe an unpriced charge for known tokens, a hard incident, and a
paused company.

## What Changed

- `run-cost-accounting.ts`: when a lost receipt settles and its usage
has known tokens and no known cost, price it with
`priceCodexReceipt`/`priceAnthropicReceipt` as a complete per-run
checkpoint. The charge is `estimated`, its provenance keeps the
rate-card source, and its evidence says "final receipt lost; observed
usage is a lower bound". The estimate is also written to the run's usage
so a later receipt reconciles against it. Zero observed tokens, and
per-model usage, stay unpriced (unknown is never shown as zero, and
aggregate totals never take one model's rate).
- `budget-exposure.ts`:
- The pending/recovering accounting hold excludes the run being admitted
and earlier runs of the same task (same `native_issue_id`). A task's
continuation and finalization can therefore settle its own accounting.
Other tasks stay held, and those runs still count as spend.
- The blocking unpriced-usage policy no longer counts these unpriced
charges: the settled final charge of a lost receipt (restart-derived,
any usage; later growth still counts), a charge with a known partial
cost, and metered API or subscription billing with no observed tokens.
Unknown billing with no tokens (for example Cursor Cloud) and
complete-receipt usage that has no price still block.
- `budgets.ts` / `budget-reservations.ts`: admission accepts the
admitted `runId` and passes it to the exposure read. `reserveRunBudget`,
the final dispatch recheck and the read-only admitted recheck all supply
it, so a continuation that reserves can also dispatch.
- Daytona wave s12 (run f1d98917, a blocked remote recovery with a
0-cent held reservation and every lease destroyed) is closed by #19606.
A Retry for that same task passes the hold through the same-task
exclusion here.

## Verification

- `closed-native-receipt-accounting.test.ts` (embedded Postgres):
- Lost Codex (gpt-6.1-sol) and Claude (claude-sonnet-5) receipts with
observed tokens settle as `estimated` rate-card charges with the
lower-bound evidence. The company stays active and admission is clear
under the blocking policy.
- A lost receipt for a model with no rate card stays unpriced but opens
no incident and no pause.
- A task's continuation reserves budget past its own recovering
accounting, while another task's run is still held.
- Unpriced charges with no tokens on metered billing, or with a known
partial cost, do not hard-stop.
  - Complete-receipt usage with no price still blocks.
  - The six new cases fail on master and pass with this change.
- Existing budget and accounting suites pass locally:
budget-enforcement-scoping, budgets-service, heartbeat-cost-accounting,
run-cost-accounting, native-stop-accounting, cost-data-quality,
budget-live-attempts, budget-live-usage. Server typecheck passes.

## Risks

- A lost receipt's estimate is a lower bound. The provenance says so,
and a later complete receipt still reconciles.
- Restart-derived unpriced charges no longer trip the blocking policy.
Usage lost with a killed provider is not counted against that policy,
which is consistent with budget enforcement being best effort.
Unknown-billing and complete-receipt unpriced usage still block.
- The same-task exemption relies on `native_issue_id`. Legacy runs
without it keep the old behavior.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, via Claude Code with
tool use (shell, file editing, embedded Postgres tests).

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 18:55:26 -06:00
..
…
…
…
…
…