Files
PaperClipAI/server
DottaandClaude Opus 5.5 6d1ecfc420 fix(accounting): close a remote run's lost receipt once its sandboxes are terminated (#19606)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Budget admission waits while completed runs await accounting; #19497
made that a hold on new work, never a hard stop
> - #19497 also closes a lost usage receipt once a local provider is
verified stopped, so the hold ends
> - A remote (Daytona) run has no local stop event and keeps its
recorded remote process identity, so its lost receipt never closed. The
company then held all new work until a receipt that can no longer arrive
> - This pull request closes a remote run's lost receipt once every
sandbox it used carries a provider termination receipt
> - The benefit is that a restart that kills a Daytona turn no longer
stops a company's new work

## Linked Issues or Issue Description

Follow-up to #19497. Refs #19544 (Daytona chaos harness).

**What happened?**
In a Daytona restart wave, run `a261c30b` failed after native recovery
was blocked. Its usage receipt stayed incomplete
(`accountingReceiptReady: false`). Both of its sandbox leases were
released with a `destroyed` termination receipt, but accounting stayed
pending. Admission then refused new company work with "New work must
wait while a native run recovers its unfinished accounting".

**What did you expect?**
Once every sandbox of the run is stopped or destroyed, no runner can
report usage for it. The known part of its usage should settle as an
unpriced charge, as it does for a verified local stop, and new work
should be admitted.

**Steps to reproduce**
1. Run a native task on a Daytona environment with a hard-stop budget.
2. Kill the control plane during the provider turn so the turn's receipt
is lost and native recovery ends in terminal failure.
3. After the sandbox lease is released with a termination receipt,
observe that the run stays `cost_accounting_pending` and new work is
held.

## What Changed

- `run-cost-accounting.ts`: the closed-receipt predicate gains a remote
branch. A native run in terminal failure without a result (failed,
cancelled, timed out or interrupted) is closed when it has at least one
environment lease and every lease carries a
`paperclip.remote-termination.v1` receipt bound to that lease, run,
company, provider and provider lease ID, with state `stopped` or
`destroyed`, after a successful release. This mirrors
`hasRemoteTerminationReceipt`. One SQL predicate still serves the
reconciler scan and settlement.
- The local branch is unchanged: a failed run with a verified local stop
event and a cleared process identity.
- A sandbox that may still run (no receipt, lease still active), a
receipt bound to another sandbox, or a run with no lease keeps its debt
pending.
- `budget-enforcement-worker.ts`: the accounting worker also wakes when
environment-cleanup or retained-session-cleanup work commits. Those are
the signals the lease writers already send when they release or stop a
sandbox.
- `doc/SPEC-implementation.md`: documents the remote closure rule.

## Verification

- `closed-native-receipt-accounting.test.ts` (embedded Postgres):
- A failed and a cancelled remote run, with one destroyed and one
stopped sandbox, settle as one unpriced charge, settle the reservation,
and clear admission. These are red on master.
- Debt stays pending for an active sandbox with no receipt, for one of
two sandboxes without a receipt, for a receipt bound to another sandbox,
and for a run with no lease.
- `budget-enforcement-worker.test.ts`: the accounting worker wakes on
environment-cleanup and retained-session-cleanup commits, and stops
waking after `stop()`. These are red on master.
- Existing suites pass locally: native-stop-accounting,
budget-enforcement-scoping, heartbeat-cost-accounting,
run-cost-accounting, cost-data-quality, budgets-service. Server
typecheck passes.
- Live Daytona restart wave: results in the comments.

## Risks

- A cancelled remote run now closes once its sandboxes are terminated.
Stop recovery could otherwise reattach a recorder, but a stopped or
destroyed sandbox has no runner to reattach to. Local cancelled runs
keep their drain.
- Workspace export can restart a stopped sandbox and removes its
termination receipt first. A receipt that arrives after settlement still
posts through late-receipt reconciliation.
- Reusable-lease chains (`resolveRemoteTerminationLease`) and
recovered-cleanup stop receipts are not mirrored, so those runs stay
pending. That is the conservative side.
- Low query cost: the predicate runs only for pending terminal runs.

## Model Used

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

## 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:17:11 -06:00
..