open-standards/jcp/jcp-rs
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Transaction Science 80971b27d4 jcp §4.11: an orchestrator's spend died with its process, so a restart was a fresh budget
`jcp-orchestrate`'s module doc claims "the whole run is an ordered ledger of
sealed decisions — the audit a bare meta-harness has no way to produce." That
was true for as long as the process lived. Item states, the receipts, and — the
part that matters — each harness's spend were held in memory: spend lives in
`self.runtime`, and a fresh `Runtime` starts at zero. A crash loop could spend a
harness's joule budget over and over, and a task already paid for and run would
be dispatched again.

A run now emits a `JournalEntry` for every transition through a `Journal` the
caller supplies. `MemoryJournal` is the default and keeps exactly the old
behaviour; a deployment that wants the run to survive its process writes the
entries somewhere that does. `Orchestrator::replay` rebuilds a run from them:
items, attempts, the receipt ledger, and the spend.

**Spend is re-derived, not asserted.** Each allowed dispatch goes back through
`Runtime::decide`, so the budgets land where the original run left them. Two
reasons for replaying rather than restoring a total: a replay cannot produce a
spend the gate would not have produced, and a stored total is one more figure
that can be wrong. The grants reproduce exactly because `register` mints them
with a fixed nonce, so an identical registration yields identical grant ids.
`Runtime` has no spend setter and did not need one.

The test that matters is not that the number comes back: it is that the resumed
budget still **binds**. A harness with 1 000 µJ that has spent 900 must refuse
the next 300 µJ dispatch, and does.

**The interrupted case is left unresolved on purpose.** The dispatch is
journalled *before* the harness runs, because the decision has already debited
the grant and recording it afterwards would lose the debit for anything
interrupted between the two. So a `Dispatched` with no `Settled` means the
process died while a harness held the task, and whether the work happened is not
knowable from the journal — only that harness knows. Such an item resumes as
`WorkState::Interrupted { harness, attempt }`, is counted separately in the
report, keeps its joules debited, and is **not** re-dispatched: the loop only
offers `Pending` items. Retrying risks paying twice for work already done,
discarding risks losing it, and choosing between those without asking the
harness is not a decision this crate can make.

A journal naming an unregistered harness, or a dispatch for a task no
`Submitted` entry introduced, is refused rather than guessed — the grant the
decision was made against cannot be reproduced.

SPEC §4.11 makes it normative beyond this crate, since the hole is in any
enforcement point that can restart: record every allowed decision durably,
re-establish the spend before deciding anything new (replay RECOMMENDED over a
stored total), record the decision before the action, and never silently retry
or mis-report an interrupted one.

jcp-rs workspace: 409 passed, clippy clean on jcp-orchestrate.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-09 02:50:39 -04:00
..
conformance/v1 jcp: JCR-1 receipts bind their payload — jcp/2 + version dispatch (SPEC §8, C23) 2026-07-18 03:04:19 -04:00
crates jcp §4.11: an orchestrator's spend died with its process, so a restart was a fresh budget 2026-10-09 02:50:39 -04:00
Cargo.lock sandbox-meter consumers: a figure keeps its span's provenance, and work that ran is never 0 J 2026-09-29 11:56:05 -04:00
Cargo.toml jcp-action-receipt: AR-1, the composition envelope 2026-08-28 08:44:07 -04:00