Is your feature request related to a problem? Please describe.
The from / to filter shipped in #159 / #160 is intentionally locked to created_at_ms (spec PR cycles-protocol#97 makes this normative). That covers "what happened in the last 24h" and daily spend tallies cleanly, but not the operational use case that surfaced in the #159 thread as part of the rationale:
cleanup routines on expired or abandoned reservations
To find expired or finalized reservations you actually want to bound expires_at_ms or finalized_at_ms, not created_at_ms. A reservation created at T-7d that just expired this morning is invisible to a 24h created_at window — but it's exactly what a cleanup sweeper is looking for.
Describe the solution you'd like
Add two additional query parameters on GET /v1/reservations:
| Parameter |
Semantics |
expires_from / expires_to |
Inclusive bounds on expires_at_ms. ISO 8601 date-time. |
finalized_from / finalized_to |
Inclusive bounds on finalized_at_ms. ISO 8601 date-time. |
expires_at_ms is required on every ReservationSummary / ReservationDetail; finalized_at_ms is only populated on COMMITTED / RELEASED rows. The window predicate on finalized_* therefore implies a status IN (COMMITTED, RELEASED) filter (or excludes ACTIVE rows from results — same outcome).
Naming and shape consistency with v0.1.25.20
The 2026-05-21 from / to revision chose the shortest possible names because created_at is the default sort_by and the binding is unambiguous from context. For these new fields the binding has to be encoded in the parameter name itself (expires_from vs finalized_from) since from is already taken for created_at. Suggested prefixes:
expires_from / expires_to — explicit, matches the entity field root (expires_at_ms).
- Alternative:
expires_at_from / expires_at_to — more verbose but mirrors the entity field exactly. Slight discoverability win.
I'd lean toward the shorter form for consistency with from / to, but no strong preference.
Validation rules (mirror the v0.1.25.20 contract)
- ISO 8601 date-time. Malformed → 400
INVALID_REQUEST.
expires_from > expires_to → 400. Same for finalized_*.
- Either bound alone valid (open interval).
- Blank strings treated as unset.
- Multiple windows compose with AND semantics (e.g.,
from=A&to=B&expires_from=C&expires_to=D returns reservations created in [A,B] and expiring in [C,D]).
- Additive-parameter guarantee: servers that don't recognize the params MUST ignore without error.
Cursor invalidation
Same shape as v0.1.25.20: fold the new ms values into FilterHasher.hash(...) so sorted-path cursors invalidate on window change. Legacy SCAN cursors remain unvalidated (matching how they treat every other filter).
Describe alternatives you've considered
- Per-tenant expiry sweep via createEvent. Doesn't help the use case — sweepers need to find expired rows first.
- Wait for v0.2.0 with a richer filter DSL. Possible, but the same
from/to-style shape now would cover 90% of the demand at minimal spec surface cost.
Out of scope (for this issue)
- Other timestamp fields on the reservation lifecycle.
created_at_ms, expires_at_ms, and finalized_at_ms are the only timestamps on the wire today; adding more is a separate concern.
- Strict-window edge cases (e.g., should
expires_to=NOW include reservations expiring exactly at NOW? — same closed-interval contract as v0.1.25.20).
Context
Surfaced during the v0.1.25.20 review of #160. I noted it in the PR body and in the cycles-protocol#97 description so the discussion has a trail; filing here so it's tracked as work, not as a comment.
Is your feature request related to a problem? Please describe.
The
from/tofilter shipped in #159 / #160 is intentionally locked tocreated_at_ms(spec PR cycles-protocol#97 makes this normative). That covers "what happened in the last 24h" and daily spend tallies cleanly, but not the operational use case that surfaced in the #159 thread as part of the rationale:To find expired or finalized reservations you actually want to bound
expires_at_msorfinalized_at_ms, notcreated_at_ms. A reservation created at T-7d that just expired this morning is invisible to a 24hcreated_atwindow — but it's exactly what a cleanup sweeper is looking for.Describe the solution you'd like
Add two additional query parameters on
GET /v1/reservations:expires_from/expires_toexpires_at_ms. ISO 8601 date-time.finalized_from/finalized_tofinalized_at_ms. ISO 8601 date-time.expires_at_msis required on everyReservationSummary/ReservationDetail;finalized_at_msis only populated on COMMITTED / RELEASED rows. The window predicate onfinalized_*therefore implies astatus IN (COMMITTED, RELEASED)filter (or excludes ACTIVE rows from results — same outcome).Naming and shape consistency with v0.1.25.20
The 2026-05-21
from/torevision chose the shortest possible names becausecreated_atis the defaultsort_byand the binding is unambiguous from context. For these new fields the binding has to be encoded in the parameter name itself (expires_fromvsfinalized_from) sincefromis already taken forcreated_at. Suggested prefixes:expires_from/expires_to— explicit, matches the entity field root (expires_at_ms).expires_at_from/expires_at_to— more verbose but mirrors the entity field exactly. Slight discoverability win.I'd lean toward the shorter form for consistency with
from/to, but no strong preference.Validation rules (mirror the v0.1.25.20 contract)
INVALID_REQUEST.expires_from > expires_to→ 400. Same forfinalized_*.from=A&to=B&expires_from=C&expires_to=Dreturns reservations created in [A,B] and expiring in [C,D]).Cursor invalidation
Same shape as v0.1.25.20: fold the new ms values into
FilterHasher.hash(...)so sorted-path cursors invalidate on window change. Legacy SCAN cursors remain unvalidated (matching how they treat every other filter).Describe alternatives you've considered
from/to-style shape now would cover 90% of the demand at minimal spec surface cost.Out of scope (for this issue)
created_at_ms,expires_at_ms, andfinalized_at_msare the only timestamps on the wire today; adding more is a separate concern.expires_to=NOWinclude reservations expiring exactly at NOW? — same closed-interval contract as v0.1.25.20).Context
Surfaced during the v0.1.25.20 review of #160. I noted it in the PR body and in the cycles-protocol#97 description so the discussion has a trail; filing here so it's tracked as work, not as a comment.