- **Date:** 2026-07-17 (v0.2.7 — commit-retry wiring fix: `CommitRetryEngine` (`src/retry.rs`) existed with complete backoff logic but carried `#[allow(dead_code)]` and was never instantiated outside its own unit tests, so the documented `retry_*` config knobs (builder methods, `CYCLES_RETRY_*` env vars, README) were silent no-ops — and a transient commit failure permanently leaked the reservation until server-side TTL expiry, because `guard.commit()` sets `finalized = true` and consumes the guard before the network call, so `Drop` performs no best-effort release and the caller cannot retry. `ReservationGuard::commit` now retries retryable failures (transport errors, 5xx, error codes the protocol classifies transient per `Error::is_retryable`, incl. the `Unknown` forward-compat arm) **inline** with exponential backoff — the fire-and-forget design originally sketched in the dead code was rejected in adversarial review because it (a) let the retry window outlive the cancelled heartbeat and die on `RESERVATION_EXPIRED`, (b) broke the "commit `Err` is final" invariant, enabling double-charge via caller compensation racing a late background commit, and (c) silently lost pending retries on runtime shutdown (detached `tokio::spawn`). Inline semantics restore all three properties: the heartbeat now stays alive until the commit outcome is final (cancelled after, with `Drop` as backstop), `Ok`/`Err` from `commit()` is definitive, and no detached task exists. Retries reuse the original `CommitRequest` — same idempotency key — so a commit that already landed server-side cannot double-charge. Both `#[allow(dead_code)]` attributes removed; `CyclesClientBuilder` gained the three missing retry setters (`retry_initial_delay`, `retry_multiplier`, `retry_max_delay`). `tests/retry_test.rs` — previously titled "Tests for CommitRetryEngine" while never exercising the engine — rewritten as four end-to-end reserve→commit wiremock tests (retry-until-success with idempotency-key-reuse and upper-bound `.expect` assertions, exhaustion, non-retryable-final, retry-disabled-final); shared reserve-mock scaffolding extracted to `tests/common/mod.rs` (also used by `guard_test.rs`). README documents the retry semantics. Coverage 96.12%. Follow-up same day: `Cargo.lock` `quinn-proto` 0.11.14 → 0.11.16 for RUSTSEC-2026-0185 (remote memory exhaustion; flagged by the scheduled cargo-audit run of 2026-07-13) and `anyhow` 1.0.102 → 1.0.103 for RUSTSEC-2026-0190 (`Error::downcast_mut()` unsoundness; the CI gate runs `cargo audit --deny warnings`, so unsound-warnings fail too). Both transitive dependencies.), 2026-07-10 (v0.2.7 — `TENANT_CLOSED` error-code support per runtime spec v0.1.25.13 (`cycles-protocol-v0.yaml`, runcycles/cycles-protocol#125): `ErrorCode::TenantClosed` variant with serde string mapping `"TENANT_CLOSED"`, plus `Error::is_tenant_closed()` helper mirroring `is_budget_exceeded()`. Purely additive — previously the code hit the `#[serde(other)] Unknown` forward-compat arm, which deserialized cleanly but reported the 409 as retryable via `ErrorCode::Unknown.is_retryable()`; now typed and non-retryable. The 409→`Error::BudgetExceeded` classification is intentionally unchanged (TENANT_CLOSED is tenant-state, not budget-family; it surfaces as `Error::Api`). Serde roundtrip + `Error` helper + wiremock regression tests added. Also `LIMIT_EXCEEDED` per runtime spec v0.1.25.12 (revision 2026-07-04, HTTP 429 rate limiting on the public evidence/JWKS endpoints, `Retry-After` / `X-RateLimit-Reset` headers): `ErrorCode::LimitExceeded` variant added in spec declaration order (`TenantClosed` relocated after it to mirror the spec exactly), classified retryable by `ErrorCode::is_retryable()` — 429 is transient; `Error::is_retryable()` inherits this via the code arm, preserving the prior `Unknown → retryable` fallback semantics, now typed. Enum-only, matching the `BudgetFrozen`/`BudgetClosed` sibling pattern (no `Error` helper, no 409-classification change). Serde roundtrip + retryability + wiremock 429 tests added.), 2026-07-04 (v0.2.7 — `reserve()` no longer panics on additive `Decision` values (fleet audit, #56 item 1): an unknown decision deserializes to `Decision::Unknown` via `#[serde(other)]`, bypassed `is_denied()`, and hit `.expect("reservation_id must be present…")`. Unknown/additive decisions now return `Error::Validation` regardless of `reservation_id` presence — `reserve()` gates on positive `Decision::is_allowed()`, not merely non-denial (review follow-up on the first cut, which still built a guard when an id happened to be present). Wiremock regression test added; full suite green. Remaining audit findings tracked in #56.), 2026-05-22 (v0.2.6 — `expires_*` / `finalized_*` ISO-8601 window-filter fields added to `ListReservationsParams` plus optional `finalized_at_ms` field added to `ReservationSummary` per `cycles-protocol-v0.yaml` revision 2026-05-22 (runcycles/cycles-protocol#98); closes the Rust-client side of runcycles/cycles-server#162. Four new `Option<String>` fields on the params struct (`expires_from`, `expires_to`, `finalized_from`, `finalized_to`), one new `Option<u64>` field on the response struct (`finalized_at_ms`, with `#[serde(default)]` for back-compat with pre-v0.1.25.21 servers). Wire-format regression tests + finalized_at_ms deserialization tests added. 134 tests pass; clippy + doc-tests clean.), 2026-05-21 (v0.2.5 — `from` / `to` ISO-8601 window-filter fields added to `ListReservationsParams` per `cycles-protocol-v0.yaml` revision 2026-05-21; closes the Rust-client side of runcycles/cycles-server#159. Both `Option<String>`, both inclusive bounds on `created_at_ms`, both serialize via `#[serde(rename = "...")]` to land on the wire under the spec-mandated names. Pure additive struct change — callers using `Default::default()` or struct-update syntax stay compile-clean. Wire-format regression test added using wiremock's `query_param` matcher. 134 tests pass; clippy + doc-tests clean.), 2026-04-10 (protocol conformance), 2026-04-19 (supply-chain coverage — cargo-audit workflow added), 2026-05-08 (crates.io metadata refresh — description and keywords broadened to cover spend / risk / audit, no behavioral changes)
0 commit comments