Releases: runcycles/cycles-client-rust
Release list
v0.2.7 - Commit-retry wiring fix, plus TENANT_CLOSED + LIMIT_EXCEEDED error-code support.
Commit-retry wiring fix, plus TENANT_CLOSED + LIMIT_EXCEEDED error-code support.
Fixed
-
Commit retry engine wired into
guard.commit()(#62).CommitRetryEngineexisted since 0.2.0 with complete backoff logic but was#[allow(dead_code)]and never invoked, so the documentedretry_*config knobs (builder methods,CYCLES_RETRY_*env vars) were silent no-ops and a transient commit failure permanently leaked the reservation until server-side TTL expiry, never recording the actual spend.guard.commit()now retries retryable failures (transport errors, 5xx, and error codes the protocol classifies transient perError::is_retryable) inline with exponential backoff:- the heartbeat keeps extending the reservation TTL until the commit outcome is final, so retries cannot lose the reservation to expiry;
- the returned
Ok/Erris definitive — no commit activity survives the call, so callers may safely compensate onErr; - no detached background task exists, so nothing is silently lost on runtime shutdown;
- retries reuse the original
CommitRequest(same idempotency key), so a commit that already landed server-side cannot double-charge.
Under a persistent outage
commit()blocks for the full backoff schedule; tune theretry_*knobs or setretry_enabled(false)for fail-fast single-attempt commits. -
Cargo.lock:quinn-proto0.11.14 → 0.11.16 for RUSTSEC-2026-0185 (remote memory exhaustion via unbounded out-of-order stream reassembly) andanyhow1.0.102 → 1.0.103 for RUSTSEC-2026-0190 (unsoundness inError::downcast_mut()); both transitive dependencies, flagged by thecargo audit --deny warningsCI gate.
TENANT_CLOSED + LIMIT_EXCEEDED error-code support. TENANT_CLOSED implements the runtime spec v0.1.25.13 revision of cycles-protocol-v0.yaml (runcycles/cycles-protocol#125): servers return HTTP 409 error=TENANT_CLOSED on reservation create/commit/release/extend when the owning tenant is CLOSED (mirrors governance spec Rule 2). LIMIT_EXCEEDED closes the same class of gap for the runtime spec v0.1.25.12 revision (2026-07-04): HTTP 429 rate-limit responses (public evidence/JWKS endpoints) carry error=LIMIT_EXCEEDED plus Retry-After / X-RateLimit-Reset headers.
Added
ErrorCode::TenantClosedvariant (serde string mapping"TENANT_CLOSED").ErrorCodeis#[non_exhaustive]with a#[serde(other)] Unknownarm, so this is source- and wire-compatible.Error::is_tenant_closed()helper, mirroringError::is_budget_exceeded(): matchesError::Api { code: Some(ErrorCode::TenantClosed), .. }.- Regression tests: serde roundtrip for the new variant (
src/models/enums.rs),Errorhelper behavior (tests/error_test.rs), and a wiremock test pinning that a 409TENANT_CLOSEDbody surfaces asError::Apiwith the typed code — not theBudgetExceededconvenience variant — and is non-retryable (tests/client_test.rs). CyclesClientBuilder::retry_initial_delay(),::retry_multiplier(),::retry_max_delay()— the retry knobs that previously existed only as config fields / env vars now have builder setters (#62).ErrorCode::LimitExceededvariant (serde string"LIMIT_EXCEEDED"), added in spec declaration order (afterMaxExtensionsExceeded;TenantClosedrelocated after it so the enum mirrors the spec exactly). Classified retryable byErrorCode::is_retryable()— 429 is transient and the spec instructs retry after the indicated delay;Error::is_retryable()picks this up via the code-based arm (the status-based arm only covers ≥500). This preserves the prior#[serde(other)] Unknown → retryablefallback behavior, now typed instead of accidental. Enum-only by design, matching theBudgetFrozen/BudgetClosedpattern: not a reservation-lifecycle denial, so noErrorhelper or 409 classification change. Serde roundtrip +Errorretryability + wiremock 429 regression tests added.
Notes
- Purely additive; no wire-format change. Before this release, a server returning
TENANT_CLOSEDdeserialized toErrorCode::Unknownvia the#[serde(other)]forward-compat arm — deserialization never failed, butErrorCode::Unknown.is_retryable()istrue, soError::is_retryable()reported a 409 TENANT_CLOSED as retryable. With this release the code is typed and correctly non-retryable. - The 409 classification in
client.rs(BUDGET_EXCEEDED / OVERDRAFT_LIMIT_EXCEEDED / DEBT_OUTSTANDING →Error::BudgetExceeded) is intentionally unchanged: TENANT_CLOSED is a tenant-state error, not a budget-family error, and surfaces asError::Api.
v0.2.4 - meta data update
Changed
- Crates.io description and keywords broadened to cover the three pillars of
Cycles' runtime authority: spend, risky tool actions, and audit gaps. Prior
framing ("budget-management protocol — deterministic spend control") only
surfaced the spend dimension and missed search-intent traffic for action
control and audit-trail use cases. - Keyword set updated from
["cycles", "budget", "llm", "ai-agents", "cost-control"]to["ai-agents", "llm", "budget", "governance", "audit-log"]. Same five-keyword cap, broader coverage. README.mdopening reorganized around the three pillars (spend / risky
actions / audit gaps), each with a one-line concrete affordance, instead
of leading with budget enforcement only.
No behavioral changes. API surface, wire protocol, and conformance audit
results are identical to 0.2.3.
v0.2.3 — 404 unit-mismatch diagnostic
Fixed
-
Misleading 404 on reserve / decide / event when the request unit does not match the stored budget's unit (#8). The server indexes budgets by the composite
(scope, unit)key, so reserving in the wrong unit surfaces as"Budget not found for provided scope: …"even when the scope itself has an ACTIVE budget.create_reservation,create_reservation_with_metadata,decide, andcreate_eventnow enrich such 404s in-flight with the unit that was sent, so the mismatch is self-diagnosing. All otherError::Apifields (status,code,request_id,retry_after,details) are preserved unchanged — no behavioral change for other errors.Example enriched error:
Api { status: 404, code: Some(NotFound), message: "Budget not found for provided scope: tenant:rider (request was sent with unit=TOKENS; verify an ACTIVE budget exists at this scope AND unit — the server indexes budgets by (scope, unit), so a mismatched unit surfaces as a 404 NOT_FOUND)", request_id: Some("..."), ... }
Docs
Amount,WithCyclesConfig::new, thewith_cycles_usageexample, and the README Quick Start all note the(scope, unit)budget indexing invariant (spec line 667) so new users don't hit this.
Tests
- 5 new unit tests (
client::tests::enrich_budget_not_found_*) and 4 new wiremock integration tests (create_reservation_404_*,decide_404_*,create_event_404_*, plus a non-matching 404 pass-through control). Coverage 95.55% (472/494 lines), above the 95% project threshold.
Server-side follow-up
The underlying misleading 404 is a server diagnostic gap, tracked as runcycles/cycles-server#79. Once the server fix (v0.1.25.6) is deployed, wrong-unit requests return structured 400 UNIT_MISMATCH with details.{scope, requested_unit, expected_units} directly — the client enrichment in this release remains forward-compatible and will simply stop firing on the wrong-unit case (still fires for truly-missing 404s). Retirement of the workaround is tracked in #10 and is non-urgent.
Install
[dependencies]
runcycles = "0.2.3"Full diff: v0.2.2...v0.2.3 · PR: #9 · crates.io: runcycles 0.2.3 · docs.rs: runcycles 0.2.3
v0.2.2 API helpers, ergonomics
What's Changed
- fix: API ergonomics from dev feedback (0.2.2) by @amavashev in #5
Full Changelog: v0.2.1...v0.2.2
v0.2.1 doc update
Updated README, version
Full Changelog: v0.2.0...v0.2.1
v0.2.0 - Initial public release
What's Changed
- Add complete Rust client for Cycles budget authority protocol by @amavashev in #1
- fix(ci): bump MSRV from 1.75 to 1.88 by @amavashev in #2
- fix(ci): pass rust-versions explicitly to avoid MSRV mismatch by @amavashev in #3
- chore: exclude internal files from crates.io package by @amavashev in #4
New Contributors
- @amavashev made their first contribution in #1
Full Changelog: https://github.com/runcycles/cycles-client-rust/commits/v0.2.0