jcp §4.11: an orchestrator's spend died with its process, so a restart was a fresh budget #189

Merged
dcharlot merged 2 commits from feat/orchestrate-journal into main 2026-10-09 08:46:22 -04:00

2026-10-09

Transaction Science
jouledesk: an interrupted tag is not a failed one, and is not sealed as waste
All checks were successful
standards workspaces build + test / jcp-rs (pull_request) Successful in 1m28s
standards workspaces build + test / joule-ui-rs (pull_request) Successful in 1m8s
standards workspaces build + test / joulehook-rs (pull_request) Successful in 1m13s
standards workspaces build + test / proof-rs (pull_request) Successful in 1m15s
standards workspaces build + test / sandbox-rs (pull_request) Successful in 2m13s
standards workspaces build + test / sense-rs (pull_request) Successful in 1m6s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 11m7s
standards workspaces build + test / smart-byte-rs (pull_request) Successful in 3m36s
WAI conformance / cargo-class corpora · linux (pull_request) Successful in 6m34s
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Successful in 17s
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Successful in 12m58s
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Successful in 8s
WAI conformance / cross-arch byte-equality · linux (pull_request) Successful in 24s
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Successful in 46s
WAI energy receipts / energy receipts · linux (pull_request) Successful in 1m46s
WAI JPEG-AI whole decode (pinned inputs, required) / fetch pinned JPEG-AI inputs → whole decode + pixel hash, required (pull_request) Successful in 1m18s
WAI neural sink gates / model-free refusals of the neural sink (pull_request) Successful in 1m6s
wai-pack / wai-pack · does this change touch it (pull_request) Successful in 4s
wai-pack / wai-pack · linux (pull_request) Has been skipped
wai-pack / wai-pack · linux-arm64 (pull_request) Has been skipped
wai-pack / wai-pack · macos (pull_request) Has been skipped
wai-pack / wai-pack · independent verifier (Python) (pull_request) Has been skipped
wai-pack / wai-pack · the floor through a media prober (pull_request) Has been skipped
wai-pack / wai-pack · dependency licences (pull_request) Has been skipped
WAI mbt2018_mean integer decode (regenerated vectors) / regenerate mbt2018_mean integer vectors → Rust decode, required (pull_request) Successful in 4m21s
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Successful in 2m51s
workflow hygiene / every workflow supersedes its own queue (pull_request) Successful in 4s
WAI conformance / cargo-class corpora · macos (pull_request) Successful in 2h2m13s
WAI conformance / cross-arch byte-equality · macos (pull_request) Successful in 23m45s
WAI energy receipts / energy receipts · macos (pull_request) Successful in 2h1m9s
This completes #189 at its only consumer, and closes a hole that PR opened.

`jouledesk` has a path dependency on `jcp-orchestrate` and matches on
`WorkState`. Adding `WorkState::Interrupted` did not break the build — the match
has a catch-all — so an interrupted task fell through it and came out
`TagOutcome::Failed("no terminal state")`. Then the next branch sealed it:

    // a `Failed` is spend that bought no result (waste)
    if matches!(outcome, TagOutcome::Done | TagOutcome::Failed(_)) {

So a task whose work may well have completed, whose joules were debited, and
whose outcome nobody knows, would have been reported to a person as failed and
sealed in an `efficiency/1` receipt as waste. That is exactly what §4.11 — which
#189 writes — forbids: an implementation MUST NOT report such an action as
completed or as failed.

A compiler could not catch it, and nor could CI: the catch-all made it compile,
and the still-pending `jouledesk-rs` job would have passed.

`TagOutcome::Interrupted { harness }` now carries the harness that held the task,
because that is who can settle it. The sealing branch excludes it alongside
`Denied` — a denied tag ran nothing, an interrupted one may have run everything,
and sealing it either way states as fact something unknown. Its joules stay
debited on the grant regardless; the dispatch decision that incurred them is
sealed in the orchestrator's own ledger.

Five consumers updated. Four were exhaustive matches the compiler found,
including `jouledesk-github`'s `seal`, which now reports `interrupted (held by
<harness>)` rather than `failed`. Every surface renders it, so no binding tells a
different story about the same outcome.

**Falsifying it caught my own test being worthless.** The first version
asserted `!matches!(interrupted, Done | Failed(_))` — a property of the *enum*,
not of the code — so changing the sealing branch, or deleting the match arm
entirely, left it green. The same vacuousness I had just caught in #190.

The obstacle was real: `WorkState::Interrupted` could not arise through
`Teammate`'s API, because `tag` submits and runs to a terminal state in one
process. So the fix was to make it reachable rather than to assert around it.
`Teammate` gains the minimum resume surface over #189's journal —
`with_journal`, `replay`, and `outcome_of` — which is the composition that
journal exists for, and jouledesk's whole purpose is composing
`jcp-orchestrate`. The mapping and the sealing rule are now named functions
(`outcome_for`, `seals_efficiency`) used by the real code paths, so a test reads
the rule instead of restating it.

The test is now end to end: tag once with a journal, truncate it after the
dispatch — the crash this models — replay into a fresh teammate, and assert the
tag reports `Interrupted { harness }` and seals nothing. Both earlier
falsifications now fail it: sealing an interrupted tag as waste, and removing the
match arm so it falls back to the catch-all.

jouledesk-rs workspace: 40 passed, clippy clean.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-09 02:50:39 -04:00
Transaction Science
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