Releases: runcycles/cycles-server
v0.1.25.21 — expires_* / finalized_* time-window filters on listReservations
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_REQUESTwith distinctInvalid {param_name}message identifying which parameter failed. expires_from > expires_toandfinalized_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/topair).
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.ReservationSummarymodel gainsfinalizedAtMsfield;toSummary(...)projection updated.- Two new predicate helpers:
expiresAtInWindowandfinalizedAtInWindow. Both applied in legacy SCAN-cursor and sorted paths after the existing scope/status/tenant predicates andcreatedAtInWindow. finalizedAtInWindowresolves its timestamp via the newresolveFinalizedAtStrhelper, shared withbuildReservationSummary's projection. Both paths agree on released-wins when bothcommitted_atandreleased_atare 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 underExpiresAndFinalizedWindowFilter(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 underListReservations(4 malformed-*, 2 reversed-window, expires/finalized propagation withverify(...)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 (permissiveRecord<string, string>)cycles-client-java-spring(Spring Boot starter) — passthrough test (permissiveMap<String, String>)runcycles(Rust) — strongly-typed; needs 4 newOption<String>fields onListReservationsParamsplus a newOption<String>field onReservationSummaryforfinalized_at_ms
v0.1.25.20 — from/to ISO-8601 window filter on listReservations (rolls up .19 Tomcat hygiene)
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:
fromandto, bothstringformat: date-time(ISO 8601), both inclusive bounds on the reservation'screated_at_ms. Either may be supplied alone (open interval) or together (closed window). The filter always binds tocreated_at_msregardless ofsort_by, sosort_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(...)foldsfromMs/toMsinto 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 byFilterHasherTest.preservesPreWindowHashWhenBothBoundsNull(golden hash2f397ea0e8fb53b7).- Validation: malformed values →
400 INVALID_REQUEST;from > to→400before any repository call; blank strings treated as unset; missing/unparseablecreated_atrows defensively excluded when either bound is supplied. - Naming matches the family-wide
from/to+format: date-timeconvention already in use onlistAuditLogs,listEvents,listWebhookDeliveries,listTenantEvents, andlistTenantWebhookDeliveries.
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:
runcycles(Python) 0.4.2 — runcycles/cycles-client-python#66runcycles(TypeScript) 0.3.2 — runcycles/cycles-client-typescript#103runcycles(Rust) 0.2.5 — runcycles/cycles-client-rust#39cycles-client-java-spring(Spring Boot starter) 0.2.3 — runcycles/cycles-spring-boot-starter#78
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 upv0.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_msindependence, 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
[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.