Retain Machines provider causes instead of flattening them into strings

Merged
VA
2d ago
+99-16

Class

G: lost causes. machines::identity::map_error dropped the provider cause:

ProviderErrorBefore
Rejected(msg)Invalid(msg), which can't be told apart from a malformed request
FailedStorage("Machines operation failed")
CancelledStorage("Machines operation was cancelled")
CommandIndeterminate(machine)Storage(format!(.. {machine})); the machine id survives only as text
Indeterminate(_) or Unavailable with no operationStorage("Machines outcome is unknown")

Storage is also the class that the accepted-effect arms treat as uncertain. So a definitive provider Failed was indistinguishable from a lost reply, and clients had to parse strings to recover anything.

Invariant

A Machines failure keeps its exact ProviderError until it reaches the wire.

  • The cause is kept. New Error::Machines(ProviderError) (behind the machines feature, with #[source]) carries every cause that has no operation receipt.
  • One exhaustive match derives the wire class: contract_errors::machines_error_code.
    • NotFound, Conflict, Unsupported and Invalid map to themselves; Rejected maps to Invalid.
    • All three indeterminate forms map to Indeterminate.
    • Unavailable, Failed and Cancelled map to Storage. Cancelled becomes Cancelled once #864 lands.
  • Only receipted uncertainty becomes Error::Indeterminate. That means OperationIndeterminate(op) (the provider operation), or an uncertain mutation (Indeterminate, Unavailable, CommandIndeterminate) whose harness operation is known. Everything else is retained, never turned into text.
  • Not a native failure: Error::Machines is excluded from is_native_failure.

Why it can't recur

  • map_error has a single catch-all arm, (cause, _) => Error::Machines(cause), which can only keep the cause. It no longer builds any string.
  • A new ProviderError variant fails to compile in machines_error_code until it is classified.

Historical findings

  • Same collapse as rev-833-verdict.md:3: a typed failure becomes text plus a generic code, so clients parse strings.
  • The coordinator flagged this site after #864.

Deletions

16 lines deleted: 6 string-building conversions and a three-way, operation-dependent Storage fallback. 98 lines added, 62 of them tests.

Verification

All on bb1, debug, --features filesystem-local,native-execution,grpc,machines,test-support.

FilterResultTime
machines::5 passed18 ms
executor::82 passed4.5 s
grpc::3 passed78 ms
  • New exhaustive table: provider_causes_survive_and_only_receipted_uncertainty_becomes_indeterminate runs all ten ProviderError variants, each with and without a receipt. With no operation, each must be retained as Error::Machines(cause) with the expected wire code. With an operation, exactly the three uncertain causes must become Indeterminate(receipt); every definitive cause, Failed included, must stay retained. Mapping Failed to Indeterminate, or any cause to a string variant, fails it.
  • Real-path check: verifier_rejects_* reads a destroyed checkpoint through SimulatedMachines. It now asserts Error::Machines(ProviderError::NotFound(_)) with code NotFound.
  • Builds: the test build took 861 s. The Windows check (cargo check --locked -p acyclic-harness --lib --tests --features <same> --target x86_64-pc-windows-gnu, 249 s) and the WASM check (--features wasm,filesystem --target wasm32-unknown-unknown, 36 s) passed.
  • Unrelated failure: the fork filter ran 46 tests and one failed, filesystem::memory::summary_tests::publication::summary_fork_captures_private_payloads_and_binds_publication. It asserts a success payload, and that path neither uses Machines nor goes through map_error.

Decisions

  • Overlap with #864: #864 edits the old Cancelled arm in identity.rs, which this PR replaces. Whichever merges second needs a one-line resolution. If #864 merges first, I'll merge main and map ProviderError::Cancelled to ErrorCode::Cancelled in machines_error_code. The contract_errors.rs hunks don't overlap with #864 or #833.
  • Kept as harness variants: local validation errors (foreign provider, bad key length) stay Invalid, and the "wrong outcome" protocol violations in host.rs stay Storage. Neither comes from a provider cause.

🤖 Generated with Claude Code


Summary by cubic

Stops flattening Machines provider causes into strings and generic storage errors. map_error now keeps the exact ProviderError in a new Error::Machines variant, so definitive failures are no longer indistinguishable from lost replies.

Key changes

  • Adds Error::Machines(ProviderError) behind the machines feature, carrying the cause with #[source].
  • machines_error_code derives the wire code from the cause via one exhaustive match: NotFound, Conflict, Unsupported, and Invalid map to themselves; Rejected to Invalid; Cancelled to Cancelled; the three indeterminate forms to Indeterminate; Unavailable and Failed to Storage.
  • Only an uncertain mutation with a known harness operation becomes Error::Indeterminate; OperationIndeterminate keeps its own operation receipt.
  • Retained causes are excluded from is_native_failure, so they still propagate through the normal error path.