Skip to content

[Feature Suggestion] Add expires_at / finalized_at range filtering to listReservations #162

@amavashev

Description

@amavashev

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions