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