Skip to content

Releases: runcycles/cycles-server

v0.1.25.21 — expires_* / finalized_* time-window filters on listReservations

22 May 12:25
f99c5bc

Choose a tag to compare

v0.1.25.21 — expires_* / finalized_* time-window filters on listReservations

Implements cycles-protocol-v0.yaml revision 2026-05-22 and closes #162 via #163. Follow-up to v0.1.25.20 — addresses the operational use case (cleanup sweepers locating reservations expiring or already finalized within a window) that revision intentionally left out by binding from/to to created_at_ms.

Feature: four new query parameters on GET /v1/reservations

Param Bound to Notes
expires_from / expires_to expires_at_ms Required field → applies to every row regardless of status.
finalized_from / finalized_to finalized_at_ms Populated only on COMMITTED / RELEASED. ACTIVE and EXPIRED rows are normatively excluded when this is set.

All ISO 8601 format: date-time, all optional, all inclusive bounds. Each pair binds to its target field regardless of sort_by. The three windows (from/to + expires_* + finalized_*) compose with AND semantics.

Schema addition

ReservationSummary gains an optional finalized_at_ms field so clients filtering with finalized_* can see the timestamp they're filtering on without a follow-up getReservation call. @JsonInclude(NON_NULL) — pre-revision response shapes go out byte-for-byte for rows where the field is absent (i.e., ACTIVE/EXPIRED rows that dominate unfiltered list calls).

Validation

  • Malformed values → 400 INVALID_REQUEST with distinct Invalid {param_name} message identifying which parameter failed.
  • expires_from > expires_to and finalized_from > finalized_to → 400 before any repository call.
  • Blank-string values for any of the six bounds treated as unset (normative per the 2026-05-22 spec carve-out, which also retroactively normatived this behavior for the v0.1.25.20 from/to pair).

Cursor back-compat (the centerpiece)

FilterHasher.hash(...) 10 → 14 args with independent gated emission per pair. Each window pair emits its canonical block only when at least one of its bounds is non-null. This preserves byte-exact back-compat for both prior cursor generations:

Cursor era Canonical form Golden hash for (acme, all-other-fields-empty)
v0.1.25.12 – v0.1.25.18 8 string fields only 2f397ea0e8fb53b7 (locked in v0.1.25.20)
v0.1.25.20 with from=100&to=200 + |fr=100|to=200 ad7204d521cfd133 (newly locked here)
v0.1.25.21 with only expires_* set + |ef=…|et=… (no |fr=|to=) byte-exact under gated emission

A v0.1.25.18 client mid-pagination after a v0.1.25.21 deployment still resolves its cursor, just as v0.1.25.20 promised for the same scenario.

Internal Java signature changes

  • RedisReservationRepository.listReservations(...) 14 → 18 args; listReservationsSorted(...) mirrors.
  • FilterHasher.hash(...) 10 → 14 args.
  • ReservationSummary model gains finalizedAtMs field; toSummary(...) projection updated.
  • Two new predicate helpers: expiresAtInWindow and finalizedAtInWindow. Both applied in legacy SCAN-cursor and sorted paths after the existing scope/status/tenant predicates and createdAtInWindow.
  • finalizedAtInWindow resolves its timestamp via the new resolveFinalizedAtStr helper, shared with buildReservationSummary's projection. Both paths agree on released-wins when both committed_at and released_at are somehow set on a malformed row — fixed a subtle disagreement caught during review where the predicate would have filtered using one timestamp while the projection emitted another.

Wire compatibility

Purely additive at the wire level. Clients that don't send the new params get exactly the v0.1.25.20 response byte-for-byte. The single new response field (finalized_at_ms on ReservationSummary) is optional and NON_NULL-serialized, so v0.1.25.20-shape responses go out byte-for-byte for rows where the field is absent.

Verification

  • 558 protocol-service tests pass (385 data + 173 api), +20 vs v0.1.25.20's 538.
  • New coverage: FilterHasherTest +3 (expires/finalized distinctness, finalized vs from/to, v0.1.25.20 golden lockdown), RedisReservationQueryTest +7 under ExpiresAndFinalizedWindowFilter (legacy expires_from below, legacy expires_to above, finalized excludes ACTIVE, finalized resolves from released_at, all-three AND composition, cursor mismatch on expires window change, malformed-row resolver agreement), ReservationControllerTest +10 under ListReservations (4 malformed-*, 2 reversed-window, expires/finalized propagation with verify(...) locks, all-three combined, blank-as-unset for new windows).
  • JaCoCo 95% bundle gate met.
  • Container scan green against the locally-built v0.1.25.21 image.

Client SDK releases (companion artifacts, landing next)

Will track the same shape as the v0.1.25.20 release chain:

  • runcycles (Python) — passthrough test (permissive **query_params)
  • runcycles (TypeScript) — passthrough test (permissive Record<string, string>)
  • cycles-client-java-spring (Spring Boot starter) — passthrough test (permissive Map<String, String>)
  • runcycles (Rust) — strongly-typed; needs 4 new Option<String> fields on ListReservationsParams plus a new Option<String> field on ReservationSummary for finalized_at_ms

v0.1.25.20 — from/to ISO-8601 window filter on listReservations (rolls up .19 Tomcat hygiene)

21 May 17:17
8a7810e

Choose a tag to compare

v0.1.25.20 — from / to ISO-8601 window filter on listReservations

Rolls up v0.1.25.19 (Tomcat 10.1.55 CVE hygiene) and v0.1.25.20 (the feature) into a single release, following the precedent set by v0.1.25.15 (which rolled up v0.1.25.14) and v0.1.25.17 (which rolled up v0.1.25.16).

Feature: from / to time-window filter on GET /v1/reservations

Implements cycles-protocol-v0.yaml revision 2026-05-21 and closes #159 via #160.

  • Two new optional query parameters: from and to, both string format: date-time (ISO 8601), both inclusive bounds on the reservation's created_at_ms. Either may be supplied alone (open interval) or together (closed window). The filter always binds to created_at_ms regardless of sort_by, so sort_by=expires_at_ms&from=…&to=… returns reservations created in the window, ordered by expiry.
  • Implemented in both the legacy SCAN-cursor path and the sorted path.
  • FilterHasher.hash(...) folds fromMs/toMs into the sorted-path cursor's canonical hash with gated emission (only when at least one bound is non-null), preserving v0.1.25.18 cursor back-compat byte-exactly when callers don't send the new params. Locked down by FilterHasherTest.preservesPreWindowHashWhenBothBoundsNull (golden hash 2f397ea0e8fb53b7).
  • Validation: malformed values → 400 INVALID_REQUEST; from > to400 before any repository call; blank strings treated as unset; missing/unparseable created_at rows defensively excluded when either bound is supplied.
  • Naming matches the family-wide from/to + format: date-time convention already in use on listAuditLogs, listEvents, listWebhookDeliveries, listTenantEvents, and listTenantWebhookDeliveries.

Security: Tomcat 10.1.55 (rolled up from v0.1.25.19, #161)

Re-pinned <tomcat.version>10.1.55</tomcat.version> to close seven CVEs flagged by Trivy against org.apache.tomcat.embed:tomcat-embed-core 10.1.54 (the version Spring Boot 3.5.14's BOM manages today):

Severity CVE Fixed in
CRITICAL CVE-2026-43512 10.1.55 / 11.0.22
CRITICAL CVE-2026-43515 10.1.55 / 11.0.22
CRITICAL CVE-2026-41293 10.1.55 / 11.0.22
HIGH CVE-2026-43513 10.1.55 / 11.0.22
HIGH CVE-2026-42498 10.1.55 / 11.0.22
HIGH CVE-2026-41284 10.1.55 / 11.0.22
LOW CVE-2026-43514 10.1.55 / 11.0.22

The override is removable once Spring Boot ships a release whose BOM manages 10.1.55+.

Wire compatibility

Purely additive at the wire level. Clients that don't send from / to get exactly the v0.1.25.18 response byte-for-byte (including sorted-path cursors mid-pagination — the gated-emission canonical form preserves the v0.1.25.12 8-field hash byte-exactly when neither bound is supplied). Older runtime clients silently ignore the new params per the additive-parameter guarantee in cycles-protocol-v0.yaml.

Client SDK releases (companion artifacts)

All four reference clients have matching releases landing now:

Verification

  • 538 protocol-service tests pass (375 data + 163 api), 95% JaCoCo bundle gate met.
  • Container scan green against the locally-built v0.1.25.20 image (Trivy HIGH/CRITICAL count: 0 after the 10.1.55 pin).
  • End-to-end smoke test against docker compose up v0.1.25.20: all validation paths (malformed → 400, reversed → 400) and all happy-path combinations (from-only, to-only, both, equal-bounds, sort_by=expires_at_ms independence, blank-as-unset, legacy SCAN cursor + active filter) confirmed.

Follow-up

runcycles/cycles-server#162 — feature suggestion for expires_from/expires_to and finalized_from/finalized_to window filters to support cleanup-of-expired-reservations workflows (out of scope for v0.1.25.20).

v0.1.25.18 — drop tomcat override (SB 3.5.14 BOM-managed) + Jedis fleet alignment

26 Apr 13:31
abc7d1c

Choose a tag to compare

[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, RandomValuePropertySource SecureRandom, consistent SSL hostname verification, ApplicationPidFileWriter/ApplicationTemp symlink fixes).
  • Drop <tomcat.version>10.1.54</tomcat.version> override. SB 3.5.14's BOM now manages Tomcat 10.1.54 directly (verified against spring-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) and cycles-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.0 override 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)

22 Apr 13:01
646d09f

Choose a tag to compare

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.0 in
    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.13
    • tomcat.version=10.1.54 pin, closing five HIGH/CRITICAL CVEs in
      spring-security-web and tomcat-embed-core). .16 was 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)

18 Apr 23:37
f5a5773

Choose a tag to compare

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() wrote audit:log:{id} keys with no EXPIRE, 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. Matches cycles-server-admin's audit.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.days config (default 400, env AUDIT_RETENTION_DAYS). Set to 0 for indefinite retention (legal hold, archive-store deployments).
  • audit.sweep.cron config (default 0 0 3 * * *, env AUDIT_SWEEP_CRON). Daily @Scheduled sweep prunes stale audit:logs:{tenantId} and audit:logs:_all ZSET pointers whose target audit: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_LUA now reads ARGV[4] as an optional TTL in seconds (0 or negative = no EX). Same shape as admin's script, minus the sentinel branching.

v0.1.25.14 — Added

  • W3C Trace Context correlation per cycles-protocol-v0.yaml revision 2026-04-18. Every response now carries an X-Cycles-Trace-Id header. The server accepts a traceparent (W3C version 00) or X-Cycles-Trace-Id header on inbound requests and echoes back the same trace_id; when neither is present it generates a fresh 128-bit id (32 lowercase hex). Malformed headers are silently ignored.
  • trace_id field on ErrorResponse, Event, WebhookDelivery, and AuditLogEntry bodies. Optional for wire back-compat; conformant servers populate it on every payload causally downstream of the request.
  • trace_flags (^[0-9a-f]{2}$) and traceparent_inbound_valid (boolean) on WebhookDelivery per governance-admin spec v0.1.25.28. These preserve the upstream W3C sampling decision so the events sidecar can reconstruct an outbound traceparent with the correct trace-flags byte instead of defaulting to 01.
  • SLF4J MDC now carries traceId alongside requestId for every request.
  • ReservationExpiryService mints a fresh trace_id per sweep batch so reservation.expired events emitted in the same sweep correlate to each other.

v0.1.25.14 — Internal

  • New TraceContextFilter (@Order(0)) runs before RequestIdFilter and sets the cyclesTraceId request attribute for downstream code.
  • EventEmitterService.emit(...) gains a final String traceId parameter (full-arity emitBalanceEvents(...) likewise). Three prior overloads kept as delegating wrappers (traceId = null) for source compatibility.
  • BaseController exposes protected resolveRequestId and resolveTraceId helpers.

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

17 Apr 01:52
c5ffb08

Choose a tag to compare

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 — listReservationsSorted hydration cap. The sorted path no longer hydrates an unbounded reservation population before the in-memory sort. SORTED_HYDRATE_CAP = 2000 mirrors the admin-plane pattern: a labeled break exits the SCAN loop once matching.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_cursor still 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's SortSpec/SortDirection contract. Wire form stays lowercase, parsing stays case-insensitive, null → null. Controller-level validation unchanged: unknown tokens still surface as HTTP 400 INVALID_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 with cap + 10 keys, asserts exactly 5 rows in ascending created_at_ms order and has_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, IllegalArgumentException on 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

17 Apr 00:00
93f26c8

Choose a tag to compare

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

14 Apr 20:12
ba820b7

Choose a tag to compare

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.reserve counter 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

14 Apr 19:28
53cb43b

Choose a tag to compare

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

13 Apr 19:42
7625235

Choose a tag to compare

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.