Releases: runcycles/cycles-server
v0.1.25.18 — drop tomcat override (SB 3.5.14 BOM-managed) + Jedis fleet alignment
[benchmark-skip]
Dependency hygiene release. No application-level code or wire-format changes — pom-only patch. Benchmark gate bypassed because a dep-only release exercises only environmental noise on the perf path (precedent: v0.1.25.9, .10, .11).
Changed
- Spring Boot 3.5.13 → 3.5.14. Patch upgrade picking up upstream security hardening (constant-time DevTools secret comparison,
RandomValuePropertySourceSecureRandom, consistent SSL hostname verification,ApplicationPidFileWriter/ApplicationTempsymlink fixes). - Drop
<tomcat.version>10.1.54</tomcat.version>override. SB 3.5.14's BOM now manages Tomcat 10.1.54 directly (verified againstspring-boot-dependencies-3.5.14.pom). The explicit pin from v0.1.25.16 (closing CVE-2026-34483 / CVE-2026-34487) is now redundant. - Jedis 7.4.1 → 6.2.0. Aligns with
cycles-server-events(6.2.0) andcycles-server-admin(6.2.0) on a single Redis-client major across the fleet, simplifying coordinated dependency upgrades. All 152 tests pass on 6.2.0. commons-lang3 3.18.0override retained — SB 3.5.14's BOM still manages 3.17.0 (CVE-2025-48924 unfixed there). Comment updated to reference SB 3.5.14.
See CHANGELOG.md for the full entry.
Fleet alignment
Matching releases: cycles-server-events v0.1.25.12, cycles-server-admin v0.1.25.41.
v0.1.25.17 — supply-chain CVE fix (commons-lang3 CVE-2025-48924)
v0.1.25.17 — supply-chain CVE fix (commons-lang3 CVE-2025-48924) [benchmark-skip]
Closes CVE-2025-48924 (Trivy HIGH) on the commons-lang3-3.17.0 jar
that ships transitively in the fat-jar image via swagger-core-jakarta
(OpenAPI UI). Spring Boot 3.5.13's BOM manages commons-lang3 at
3.17.0; the override is removable once Spring Boot ships a managed
version of 3.18.0+.
Fixed (security)
- Pin
commons-lang3.version=3.18.0in
cycles-protocol-service/pom.xml.
Notes
- No code, API, or Lua-script changes. Benchmark gate skipped — the bump
is in a non-hot-path transitive dep and benchmark deltas would only
measure environmental noise. - All 152 tests pass.
- Follow-on to v0.1.25.16 (
spring-boot-starter-parent 3.5.11 → 3.5.13tomcat.version=10.1.54pin, closing five HIGH/CRITICAL CVEs in
spring-security-webandtomcat-embed-core)..16was a
pom-version-only bump with no standalone GitHub release; its changes
ship cumulatively in.17.
Wire format
Unchanged from v0.1.25.15. Clients and dashboard on .15+ remain
fully compatible.
v0.1.25.15 — audit-log retention TTL (rolls up v0.1.25.14 trace_id correlation)
Rolled-up release covering both v0.1.25.14 (W3C Trace Context cross-surface correlation) and v0.1.25.15 (audit-log retention TTL fix). Cumulative changelog below; per-version detail in CHANGELOG.md.
v0.1.25.15 — Fixed
- Runtime-written audit-log entries now respect a configurable retention TTL (default 400 days). Previously,
AuditRepository.log()wroteaudit:log:{id}keys with noEXPIRE, so runtime-written rows persisted indefinitely until Redis eviction — silently failing to participate in the 400-day retention tier the admin plane applies to authenticated audit rows. Matchescycles-server-admin'saudit.retention.authenticated.days=400. Runtime never writes the admin-plane__admin__/__unauth__sentinels, so a single tier suffices.
v0.1.25.15 — Added
audit.retention.daysconfig (default400, envAUDIT_RETENTION_DAYS). Set to0for indefinite retention (legal hold, archive-store deployments).audit.sweep.cronconfig (default0 0 3 * * *, envAUDIT_SWEEP_CRON). Daily@Scheduledsweep prunes staleaudit:logs:{tenantId}andaudit:logs:_allZSET pointers whose targetaudit:log:{id}key has TTL-expired. Self-contained and idempotent — safe to run alongside admin's sweep.
v0.1.25.15 — Internal
AuditRepository.LOG_AUDIT_LUAnow readsARGV[4]as an optional TTL in seconds (0or negative = noEX). Same shape as admin's script, minus the sentinel branching.
v0.1.25.14 — Added
- W3C Trace Context correlation per
cycles-protocol-v0.yamlrevision 2026-04-18. Every response now carries anX-Cycles-Trace-Idheader. The server accepts atraceparent(W3C version 00) orX-Cycles-Trace-Idheader on inbound requests and echoes back the sametrace_id; when neither is present it generates a fresh 128-bit id (32 lowercase hex). Malformed headers are silently ignored. trace_idfield onErrorResponse,Event,WebhookDelivery, andAuditLogEntrybodies. Optional for wire back-compat; conformant servers populate it on every payload causally downstream of the request.trace_flags(^[0-9a-f]{2}$) andtraceparent_inbound_valid(boolean) onWebhookDeliveryper governance-admin spec v0.1.25.28. These preserve the upstream W3C sampling decision so the events sidecar can reconstruct an outboundtraceparentwith the correct trace-flags byte instead of defaulting to01.- SLF4J MDC now carries
traceIdalongsiderequestIdfor every request. ReservationExpiryServicemints a freshtrace_idper sweep batch soreservation.expiredevents emitted in the same sweep correlate to each other.
v0.1.25.14 — Internal
- New
TraceContextFilter(@Order(0)) runs beforeRequestIdFilterand sets thecyclesTraceIdrequest attribute for downstream code. EventEmitterService.emit(...)gains a finalString traceIdparameter (full-arityemitBalanceEvents(...)likewise). Three prior overloads kept as delegating wrappers (traceId = null) for source compatibility.BaseControllerexposes protectedresolveRequestIdandresolveTraceIdhelpers.
Compatibility
- Wire-additive only — no breaking changes for existing clients. All new fields are optional.
- No DB migration required.
- Backward TTL behavior: rows written before v0.1.25.15 stay un-TTL'd until Redis memory pressure evicts them; new writes get the configured TTL.
v0.1.25.13 — hydration cap + enum wire annotations
v0.1.25.13 — hydration cap + enum wire annotations
Two defensive fixes on the v0.1.25.12 sorted-list feature, ported from cycles-server-admin v0.1.25.24. No spec change, no wire-format change, no hot-path performance impact.
Fixed
- P1 —
listReservationsSortedhydration cap. The sorted path no longer hydrates an unbounded reservation population before the in-memory sort.SORTED_HYDRATE_CAP = 2000mirrors the admin-plane pattern: a labeled break exits the SCAN loop oncematching.size() >= cap, a WARN is logged naming the tenant + sort tuple, and the downstream sort/slice/cursor path operates on the capped slice. Page still fills,has_more+next_cursorstill populate — the cap is a heap-safety bound, not a correctness bound. Legacy no-sort-params path intentionally uncapped (it streams page-by-page via the SCAN cursor). - P2 — Jackson wire annotations on
ReservationSortBy+SortDirection. Both enums now carry@JsonValue getWire()+@JsonCreator fromWire(String)matching the admin plane'sSortSpec/SortDirectioncontract. Wire form stays lowercase, parsing stays case-insensitive,null → null. Controller-level validation unchanged: unknown tokens still surface as HTTP 400INVALID_REQUEST.
Operator guidance
Callers that outgrow the 2000-row cap should narrow filters (status, idempotency_key, scope segments: workspace/app/workflow/agent/toolset). The longer-term path is the deferred per-tenant ZSET index ADR at docs/deferred-optimizations/sorted-list-zset-indices.md, scheduled when a tenant crosses ~10k active reservations.
Tests
RedisReservationQueryTest#sortedHydrationStopsAtCap— mocks SCAN page withcap + 10keys, asserts exactly 5 rows in ascendingcreated_at_msorder andhas_more=true,next_cursor != null.EnumsTest(new, cycles-protocol-service-model) — 12 tests covering getWire lowercase emission, fromWire canonical+case-insensitive parsing, null pass-through,IllegalArgumentExceptionon unknown tokens, round-trip identity.- Full build green: 358 (data) + 137 (api) = all tests passing; JaCoCo ≥ 95%.
Backward compatibility
Full. Behaviour-visible only for tenants whose sorted-list query previously returned >2000 matching rows — those rows beyond row 2000 in the capped slice are now unreachable without narrowing filters. Same trade-off the admin plane established in v0.1.25.24.
Docker image
ghcr.io/runcycles/cycles-server:0.1.25.13
[benchmark-skip] — no hot-path write changes. Reserve / commit / release / extend / decide / event paths byte-identical to v0.1.25.12. Sorted-list read path adds a bounds check (1 comparison per hydrated row) with no observable latency impact. Full benchmark sweep scheduled when the deferred ZSET optimization lands.
v0.1.25.12 sort_by + sort_dir on GET /v1/reservations
What's Changed
- feat(reservations): sort_by + sort_dir on GET /v1/reservations (v0.1.25.12) by @amavashev in #104
Implements cycles-protocol-v0.yaml revision 2026-04-16. GET /v1/reservations now accepts optional sort_by (reservation_id, tenant, scope_path, status, reserved, created_at_ms, expires_at_ms) and sort_dir (asc, desc — default desc). Invalid enum values return HTTP 400 INVALID_REQUEST. The cursor binds the (sort_by, sort_dir, filters) tuple; mismatched reuse returns 400.
Wire format: backward compatible. Clients omitting both params see byte-identical behaviour to v0.1.25.11 (legacy Redis-SCAN cursor path preserved).
[benchmark-skip]
Sorted path is opt-in and the legacy path is byte-identical. Benchmarks would only measure environmental noise, so the release gate is bypassed per the documented convention.
Full Changelog: v0.1.25.11...v0.1.25.12
v0.1.25.11 concurrent idempotency + counter-accuracy tests
What's Changed
- test(v0.1.25.11): concurrent idempotency + counter-accuracy tests by @amavashev in #99
Closes two gaps flagged in the v0.1.25.10 review:
- Thundering-herd retry on expired idempotency cache — fires 10 concurrent retries with the same idempotency key after cache expiry, asserts exactly one reservation is created and metric tags split correctly (1 ×
reason=OK+ 9 ×reason=IDEMPOTENT_REPLAY). - Concurrent custom-counter accuracy — asserts
cycles.reservations.reservecounter is accurate under 8-thread × 10-request load with zero lost increments.
Both are regression gates rather than live bug fixes — correctness holds today because Redis Lua execution is single-threaded and Micrometer counters use lock-free AtomicLong internally. Without these tests, a future refactor (idempotency logic moving out of Lua into Java; tag-building aspect with shared state) could silently violate those guarantees.
Wire format unchanged vs v0.1.25.10 — no client changes required.
Full Changelog: v0.1.25.10...v0.1.25.11
v0.1.25.10 custom Micrometer counters + Redis-disconnect test
What's Changed
- feat(metrics): v0.1.25.10 — custom Micrometer counters + Redis-disconnect test by @amavashev in #97
Adds seven domain-level counters under the cycles_* namespace (reserve, commit, release, extend, expired, events, overdraft.incurred) so operators can answer "how many denials in the last 5 minutes by reason and tenant" without reverse-engineering it from HTTP status codes. New RedisDisconnectResilienceIntegrationTest pauses its Testcontainers Redis mid-request to prove the service fails cleanly and recovers.
Dormant bug fixed: ReservationExpiryService.emitExpiredEvent used the wrong Redis key prefix, silently no-op'ing on every expiry since v0.1.25.3. Surfaced immediately by the new cycles.reservations.expired counter test.
Wire format unchanged vs v0.1.25.9 — no request/response body changes.
Full Changelog: v0.1.25.9...v0.1.25.10
v0.1.25.8 — admin-on-behalf-of dual-auth on reservations
Stage 2 of the dashboard ops gap rollout. Admin operators can now read and force-release reservations via the existing endpoints without a tenant API key — closes the runtime-plane side of the gap.
Spec alignment
Implements cycles-protocol revision 2026-04-13 + audit-discoverability clarification. All NORMATIVE requirements verified by contract tests against `cycles-protocol@main`.
Endpoints with new dual-auth
- `GET /v1/reservations` (listReservations) — admin: `tenant` query param REQUIRED as filter; 400 if missing
- `GET /v1/reservations/{id}` (getReservation) — admin bypasses tenant ownership; reservation_id pins the owner
- `POST /v1/reservations/{id}/release` (releaseReservation) — admin force-release + audit-log write
`create` / `commit` / `extend` remain ApiKeyAuth-only by design.
Admin audit-log writing
On admin-driven release, the server writes an audit-log entry with `metadata.actor_type=admin_on_behalf_of` to the store that the governance plane's `GET /v1/admin/audit/logs` reads from — so admin-driven releases surface in the dashboard's existing Audit view without any cross-service plumbing. User-controlled `reason` field is CR/LF-sanitized before recording to prevent log-line forgery.
Tenant-self-service releases do NOT write audit entries (audit remains admin-action-focused, matching the governance plane's existing pattern).
New env var
`ADMIN_API_KEY=` — same value cycles-server-admin uses. Unset → X-Admin-API-Key on allowlisted paths returns 500 with a clear "server misconfiguration" message.
Tests
125/125 pass (was 105; +20 net) — filter admit/reject coverage, controller branching, audit entry verification, CR/LF sanitization regression, fall-through for non-allowlisted admin paths. Contract validation passes against post-merge spec. JaCoCo coverage check passes.
Image
`ghcr.io/runcycles/cycles-server:0.1.25.8` (also `:latest`).
Downstream
Stage 2.3: dashboard PR adding a ReservationsView that surfaces these endpoints. Depends on this image being published.
v0.1.25.7 typed Enums.ReasonCode + flaky EventEmitterServiceTest fix
What's Changed
- refactor: type DecisionResponse / ReservationCreateResponse reason_code as Enums.ReasonCode by @amavashev in #83
- test: de-flake EventEmitterServiceTest by replacing Thread.sleep with Mockito timeout/after by @amavashev in #82
Closes #81. Companion spec PR: runcycles/cycles-protocol#26.
Two quality improvements bundled, no wire-format change — JSON output is byte-identical to v0.1.25.6 on every response.
Typed Enums.ReasonCode refactor (#83): DecisionResponse.reasonCode and ReservationCreateResponse.reasonCode retyped from free-form String to a typed enum with 6 values (BUDGET_EXCEEDED, BUDGET_FROZEN, BUDGET_CLOSED, BUDGET_NOT_FOUND, OVERDRAFT_LIMIT_EXCEEDED, DEBT_OUTSTANDING). Mirrors the DecisionReasonCode schema added to cycles-protocol-v0.yaml in runcycles/cycles-protocol#26. Jackson serializes enum to name(), so JSON output is byte-identical to v0.1.25.6. Compile-time safety against drift from the spec enum.
De-flaked EventEmitterServiceTest (#82): 13 Thread.sleep(200) + verify() race-condition patterns replaced with Mockito timeout(5000) / after(200).never() verification modes. Test-only, no runtime impact.
No spec bump required. The companion DecisionReasonCode enum in runcycles/cycles-protocol#26 is forward-compatible regardless of merge order.
Full Changelog: v0.1.25.6...v0.1.25.7
v0.1.25.6 distinguish UNIT_MISMATCH from BUDGET_NOT_FOUND on reserve/event/decide
What's Changed
- fix: distinguish UNIT_MISMATCH from BUDGET_NOT_FOUND on reserve/event/decide by @amavashev in #80
Closes #79. Addresses runcycles/cycles-client-rust#8.
reserve.lua, event.lua, and the Java-side decide / evaluateDryRun paths now probe alternate units when no budget is found at the requested unit. If a budget exists at the scope in a different unit, returns 400 UNIT_MISMATCH with {scope, requested_unit, expected_units} in details so clients can self-correct. 404 BUDGET_NOT_FOUND remains for the truly-missing case.
No hot-path change — the probe fires only on the empty-budgeted-scopes error path. 291 tests / 0 failures / jacoco coverage checks met.
Companion spec update: runcycles/cycles-protocol#25 (broadens normative UNIT_MISMATCH wording, documents 404 on reserve + event endpoints).
Full Changelog: v0.1.25.5...v0.1.25.6