Skip to content

feat(listReservations): add from/to created-time range filters#97

Merged
amavashev merged 2 commits into
mainfrom
feat/listreservations-from-to-filters
May 21, 2026
Merged

feat(listReservations): add from/to created-time range filters#97
amavashev merged 2 commits into
mainfrom
feat/listreservations-from-to-filters

Conversation

@amavashev
Copy link
Copy Markdown
Contributor

@amavashev amavashev commented May 21, 2026

Draft — opening for review while the issue author confirms naming and the five open questions raised in cycles-server#159 (comment).

Summary

Adds two optional query parameters to listReservations (GET /v1/reservations) so clients can request a created-time slice (e.g., last 24 hours) without the page-size-escalation workaround.

  • from: ISO 8601 date-time. Inclusive lower bound on created_at_ms.
  • to: ISO 8601 date-time. Inclusive upper bound on 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 + window-by-created_at_ms is well-defined.

Closes runcycles/cycles-server#159.

Naming and wire-type rationale

Matches the family-wide from/to + format: date-time convention already in use on every other list endpoint in the suite:

Endpoint Spec location
listAuditLogs cycles-governance-admin-v0.1.25.yaml:5833-5842
listWebhookDeliveries :6508-6517
listEvents (admin) :6639-6648
listTenantEvents :7600-7609
listTenantWebhookDeliveries :7690-7699

Bespoke names (created_after/created_before) or Unix-epoch wire types would split the convention for clients and codegen. Server converts on ingest; entity-side created_at_ms (int64 epoch ms) is unchanged.

Validation

  • from > to MUST return HTTP 400 INVALID_REQUEST.
  • Either parameter alone is valid; absent parameter means "no bound on that side."
  • Malformed date-time values MUST be rejected with HTTP 400.

Additive-parameter guarantee

Servers that don't recognize the new parameters MUST ignore them without error and return the unfiltered set. Older clients that never send them get the pre-revision wire behavior byte-for-byte. Matches the trace_id / request_id additive treatment on listEvents.

Changed files

  • cycles-protocol-v0.yaml — two new query params on listReservations plus a TIME-RANGE FILTERS (NORMATIVE, ADDITIVE) prose block in the operation description.
  • changelogs/cycles-protocol-v0.md — new ## v0.1.25 — 2026-05-21 entry at the top, following the existing same-version-multiple-revisions convention.
  • CONFORMANCE.mdlistReservations SHOULD bullet updated to mention the time-window query path.

info.version stays at 0.1.25 per the existing revision convention (multiple dated v0.1.25 entries already coexist in the changelog: 2026-04-18 trace_id, 2026-04-16 server-side sort, etc.). Happy to bump to 0.1.26 if reviewers prefer.

Open questions (for issue author confirmation before un-drafting)

These are answered in the spec as written, but I want explicit confirmation from @Prikis on the comment thread:

  1. Naming: from/to vs. created_after/created_before — going with the family convention.
  2. Inclusivity: ≥ / ≤ (closed interval) — confirmed in description.
  3. from > to: server returns 400 INVALID_REQUEST — confirmed in description.
  4. Open intervals: from alone valid (no upper bound), to alone valid (no lower bound), absent = no bound — confirmed in description.
  5. Sort binding: filter always binds to created_at_ms regardless of sort_by — confirmed in description.

Plus a rationale nit: the issue's "cleanup expired/abandoned reservations" use case actually wants expires_at/finalized_at filters, not created_at. Out of scope here — happy to file a follow-up if there's appetite.

Verification

  • npx spectral lint cycles-protocol-v0.yaml --fail-severity=error → 0 errors. 20 pre-existing warnings on top-level schema descriptions, unchanged from main (verified with git stash round-trip).
  • python scripts/validate_changelogs.py → all 5 specs OK.

Test plan

  • Issue author confirms from/to naming and the five open questions
  • Reviewer sign-off on the prose block
  • cycles-server impl PR opens against this spec (out of scope for this PR)
  • cycles-client-{python,typescript,rust} regen + smoke (additive params don't break codegen, but worth verifying)

amavashev added 2 commits May 21, 2026 10:06
Closes runcycles/cycles-server#159.

Adds two optional query parameters to GET /v1/reservations:

  * from: ISO 8601 date-time. Inclusive lower bound on created_at_ms.
  * to:   ISO 8601 date-time. Inclusive upper bound on 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 + time-window-by-created_at_ms is
well-defined.

Naming matches the family-wide convention already in use on
listAuditLogs, listEvents, listWebhookDeliveries, listTenantEvents,
and listTenantWebhookDeliveries (all `from`/`to` + format: date-time).
Bespoke names (`created_after`/`created_before`) or Unix-epoch wire
types would split the convention for clients and codegen.

Validation:
  * `from > to` MUST return 400 INVALID_REQUEST.
  * Either alone is valid; absent means "no bound on that side."
  * Malformed date-time values MUST return 400.

Additive-parameter guarantee: servers that don't recognize the
parameters MUST ignore without error and return the unfiltered set.
Backward compatible — purely additive, no request or response schema
changes, both ApiKeyAuth and AdminKeyAuth callers see them.

Spec changes:
  - cycles-protocol-v0.yaml: two new query params on listReservations
    plus a new TIME-RANGE FILTERS (NORMATIVE, ADDITIVE) prose block
    in the operation description.
  - changelogs/cycles-protocol-v0.md: new v0.1.25 — 2026-05-21 entry.
  - CONFORMANCE.md: listReservations SHOULD bullet updated to mention
    the new time-window query path.

info.version stays at 0.1.25 per the existing revision convention
(multiple dated v0.1.25 entries already coexist in the changelog).

Verification:
  - npx spectral lint cycles-protocol-v0.yaml --fail-severity=error
    → 0 errors. 20 pre-existing warnings on top-level schema
    descriptions, unchanged from main.
  - python scripts/validate_changelogs.py → all 5 specs OK.

Opening as DRAFT pending issue author confirmation on the `from`/`to`
naming and five open questions raised in
runcycles/cycles-server#159 (comment 4508958838): inclusivity,
from>to=400, open intervals, sort_by binding, rationale nit on the
expires_at use case.
Companion to a510f29 — the merge-check CI step (`make merge-check` /
`python scripts/merge_specs.py` + git-diff guard on `merged/`) fails
when a source spec is modified without rerunning the merge script.

This commit picks up the `from`/`to` query params on listReservations
and the TIME-RANGE FILTERS prose block in the merged protocol
artifact. No semantic changes beyond what's already in
cycles-protocol-v0.yaml — pure mechanical regeneration via
scripts/merge_specs.py.

Verified:
  - `python scripts/merge_specs.py` writes both merged artifacts; only
    cycles-openapi-protocol-merged.yaml changed (admin merge already
    excludes the runtime base).
  - `git diff merged/` shows the same TIME-RANGE FILTERS block and
    two new query params, just propagated through the merge.
  - `npx spectral lint merged/cycles-openapi-protocol-merged.yaml
    --fail-severity=error` → 0 errors (pre-existing warnings only).
@amavashev amavashev marked this pull request as ready for review May 21, 2026 14:12
@amavashev amavashev merged commit 91e118d into main May 21, 2026
5 checks passed
@amavashev amavashev deleted the feat/listreservations-from-to-filters branch May 21, 2026 14:14
amavashev added a commit to runcycles/cycles-server that referenced this pull request May 21, 2026
Closes #159. Implements cycles-protocol-v0.yaml revision 2026-05-21
(runcycles/cycles-protocol#97).

Two new optional query parameters on GET /v1/reservations:

  * from: ISO 8601 date-time. Inclusive lower bound on created_at_ms.
  * to:   ISO 8601 date-time. Inclusive upper bound on 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 + window-by-created_at_ms is
well-defined.

Implementation:

  * Controller: parses ISO-8601 via Instant.parse; malformed → 400
    INVALID_REQUEST; from > to → 400 before any repository call;
    blank-string values treated as unset.
  * Repository: window predicate applied in both legacy SCAN-cursor
    and sorted paths. Predicate body shared via createdAtInWindow
    helper. Missing/unparseable created_at rows defensively excluded
    when either bound is supplied.
  * FilterHasher: fromMs/toMs folded into canonical hash so cursors
    invalidate on window change (same contract as v0.1.25.12
    sort_by/sort_dir/filter-mismatch).
  * Java signature change: listReservations gains trailing
    Long fromMs, Long toMs (12 → 14 args). No wire change for
    clients that omit the new params.

Coverage:

  * FilterHasherTest: 2 new cases (from/to distinct hash, positional).
  * RedisReservationQueryTest: 7 new cases under TimeWindowFilter
    nested class.
  * ReservationControllerTest: 7 new cases under ListReservations
    nested class (malformed-from/to, reversed-window, from-only,
    to-only, equal-bounds, sort-key independence, blank-as-unset).
  * 537 tests pass (374 data + 163 api).

Docs:

  * AUDIT.md: new dated entry walks through naming, sort-binding,
    cursor invalidation, validation choices, defensive read-side,
    coverage, and out-of-scope notes (expires_at follow-up).
  * CHANGELOG.md: [0.1.25.19] entry under Keep-a-Changelog format.
  * pom.xml revision bumped 0.1.25.18 → 0.1.25.19.
amavashev added a commit to runcycles/cycles-client-typescript that referenced this pull request May 21, 2026
…0.3.2) (#103)

Client-side companion to cycles-protocol-v0.yaml revision 2026-05-21
(runcycles/cycles-protocol#97) and runcycles/cycles-server#160. Closes
the TypeScript-client side of issue #159.

The existing `listReservations(params?: Record<string, string>)`
signature already forwards arbitrary keys to the URL query string,
so the new `from` and `to` ISO-8601 date-time params work over the
wire today without a code change. This commit adds a regression test
that pins the contract: URL-encoded form
"from=2026-05-21T00%3A00%3A00Z&to=2026-05-22T00%3A00%3A00Z" must
land in fetch's call. Future tightening of the Record signature
cannot silently drop the new params.

Unlike Python, TypeScript has no reserved-keyword issue — callers
can write `client.listReservations({ from: "...", to: "..." })`
directly.

No protocol or wire-format change; servers older than v0.1.25.20
silently ignore the new params per the additive-parameter guarantee
in cycles-protocol-v0.yaml. 316 tests pass at 98.4% statement
coverage / 99.62% line coverage (gate ≥95%).

Bumped to 0.3.2, updated AUDIT.md and CHANGELOG.md.
amavashev added a commit to runcycles/cycles-client-rust that referenced this pull request May 21, 2026
Client-side companion to cycles-protocol-v0.yaml revision 2026-05-21
(runcycles/cycles-protocol#97) and runcycles/cycles-server#160. Closes
the Rust-client side of issue #159.

`ListReservationsParams` is strongly-typed (unlike the loose
**kwargs / Record<string, string> shapes in the Python and TS
clients), so this PR adds two new `Option<String>` fields with
`#[serde(rename = ...)]` to project to the spec-mandated query-string
names:

  /// Inclusive lower bound on `created_at_ms`. ISO 8601 date-time.
  #[serde(rename = "from", skip_serializing_if = "Option::is_none")]
  pub from: Option<String>,

  /// Inclusive upper bound on `created_at_ms`. ISO 8601 date-time.
  #[serde(rename = "to", skip_serializing_if = "Option::is_none")]
  pub to: Option<String>,

Pure additive struct change. Callers using `ListReservationsParams::default()`
or `..Default::default()` continue to compile and behave identically;
new fields default to `None` and are skipped during serialization.

Adds `list_reservations_forwards_from_to_window` in tests/client_test.rs
that uses wiremock's `query_param` matcher to assert the new fields
land on the wire under the spec-mandated names ("from" / "to", not
the Rust struct field names — though they happen to match here).

Pre-existing drift on `ListReservationsParams` (missing `workspace` /
`workflow` / `toolset` subject filters, missing `sort_by` / `sort_dir`
from the v0.1.25.12 spec revision, missing `idempotency_key`) is NOT
addressed in this PR — kept scope tight. Worth a follow-up to bring
the struct to full v0.1.25 spec parity, but separate from the
#159 chain.

No protocol or wire-format change; servers older than v0.1.25.20
silently ignore the new params per the additive-parameter guarantee
in cycles-protocol-v0.yaml. 134 tests pass, clippy + doc-tests clean.

Bumped to 0.2.5, updated AUDIT.md and CHANGELOG.md.
amavashev added a commit that referenced this pull request May 22, 2026
…oses runcycles/cycles-server#162) (#98)

* feat(listReservations): add expires_* / finalized_* range filters

Closes runcycles/cycles-server#162. Follow-up to #97
(the 2026-05-21 from/to revision) addressing the use case that
revision intentionally left out: cleanup sweepers that need to
locate reservations expiring or already finalized within a window.

Adds four optional query parameters to GET /v1/reservations,
mirroring the shape of the from/to window filter:

  * expires_from / expires_to    — bound on expires_at_ms.
  * finalized_from / finalized_to — bound on finalized_at_ms.

All ISO 8601 date-time strings. Either side of each pair may be
supplied alone (open interval) or paired (closed window). The
three window filters (from/to + expires_* + finalized_*) compose
with AND semantics; a row must satisfy every supplied predicate
to be returned.

Each pair always binds to its target field regardless of sort_by,
matching the 2026-05-21 sort-key-independence rule for from/to.

finalized_at_ms is OPTIONAL on ReservationSummary /
ReservationDetail. Rows where the field is absent (typically
ACTIVE reservations not yet finalized) MUST be excluded from
results when either finalized_from or finalized_to is supplied —
the predicate naturally fails on field-absent rows; making the
exclusion normative ensures all conformant servers agree.

Validation mirrors the 2026-05-21 contract: from > to within
each pair → 400 INVALID_REQUEST, malformed date-time → 400,
either alone is valid, additive-parameter guarantee preserved.

Sorted-path cursor invalidation extends to all six window-bound
values via FilterHasher (server-side impl concern). Legacy SCAN
cursors do not carry filter state and are not window-validated
(matching how the legacy path already treats every other filter).

TIME-RANGE FILTERS prose block in the listReservations operation
description rewritten to cover all three field bindings and the
AND-composition rule.

Spec changes:
  - cycles-protocol-v0.yaml: 4 new query params on listReservations
    plus the expanded TIME-RANGE FILTERS (NORMATIVE, ADDITIVE)
    prose block.
  - changelogs/cycles-protocol-v0.md: new v0.1.25 — 2026-05-22 entry.
  - CONFORMANCE.md: listReservations SHOULD bullet updated.
  - merged/cycles-openapi-protocol-merged.yaml: mechanically
    regenerated by scripts/merge_specs.py.

info.version stays at 0.1.25 per the existing revision convention
(multiple dated v0.1.25 entries coexist; the validator only
requires the topmost heading to match info.version).

Verification:
  - npx spectral lint cycles-protocol-v0.yaml --fail-severity=error
    → 0 errors. 20 pre-existing warnings on top-level schema
    descriptions, unchanged from main.
  - python scripts/validate_changelogs.py → all 5 specs OK.
  - python scripts/merge_specs.py → merged artifact regenerated
    cleanly (no merge-check drift).

* fix(listReservations): address reviewer P2/P3 findings on #98

Three real findings from the review pass:

P2 — ReservationSummary had no `finalized_at_ms` field. The prose
in the operation description and the changelog claimed it was
OPTIONAL on the summary, but the schema has `additionalProperties:
false` and didn't declare the field at all. listReservations
returns ReservationSummary, so conformant servers could not have
included the timestamp in list results and the filter would have
been useful only via a follow-up getReservation call per row.

Added `finalized_at_ms` as an OPTIONAL int64 property on
ReservationSummary with the same shape as on ReservationDetail
plus a normative description of population semantics. Old clients
with strict schemas remain compatible because the field is
OPTIONAL (absent in pre-revision responses, valid under the new
schema when present).

P2 — finalized_at_ms EXPIRED-row semantics were ambiguous. The
prose said the field was populated when a reservation reached a
terminal state "commit, release, expiry" — but issue #162 (and
the current cycles-server runtime impl in
RedisReservationRepository.buildReservationSummary) only writes
finalized_at_ms from committed_at or released_at, never from an
expired-sweep timestamp. EXPIRED rows have the field absent.

Narrowed the contract: finalized_at_ms is populated ONLY on
COMMITTED and RELEASED rows. ACTIVE and EXPIRED rows have it
absent and are naturally excluded from `finalized_*` filter
results. Added a normative pointer telling callers who want a
window over EXPIRED rows to use `expires_*` (which works on
every row since `expires_at_ms` is required).

Updated three places in lockstep so the contract is consistent:
  - The TIME-RANGE FILTERS prose block on the listReservations
    operation
  - The `finalized_from` / `finalized_to` parameter descriptions
  - The `finalized_at_ms` field descriptions on both
    ReservationSummary and ReservationDetail

P3 — changelog wording "No request or response schema changes"
was wrong, even before this fix-up (the PR adds four query
parameters), and is now further wrong because the
ReservationSummary fix above adds an OPTIONAL response-body
property. Reworded to: "Four new query parameters on
listReservations; one new OPTIONAL property (finalized_at_ms)
on ReservationSummary mirroring the same field already on
ReservationDetail. No request-body schema changes."

Spec changes:
  - cycles-protocol-v0.yaml: ReservationSummary.finalized_at_ms
    added; ReservationDetail.finalized_at_ms gains a description;
    TIME-RANGE FILTERS prose + finalized_from / finalized_to
    descriptions tightened.
  - changelogs/cycles-protocol-v0.md: finalized_* bullet and the
    backward-compat bullet rewritten to reflect the new contract
    and the ReservationSummary addition.
  - merged/cycles-openapi-protocol-merged.yaml: mechanically
    regenerated by scripts/merge_specs.py.

Verification:
  - npx spectral lint cycles-protocol-v0.yaml --fail-severity=error
    → 0 errors. 20 pre-existing warnings on top-level schema
    descriptions, unchanged from main.
  - python scripts/validate_changelogs.py → all 5 specs OK.
  - python scripts/merge_specs.py → merged artifact regenerated;
    git diff --exit-code --quiet merged/ now clean post-commit.

No version bump — staying at info.version 0.1.25 with the
revision-2026-05-22 entry.

* docs(listReservations): make blank-string-as-unset normative for window bounds

Real P2 from the review pass: issue cycles-server#162 specified
that blank-string window-bound values are treated as unset, but
the spec PR didn't carry that contract through normatively. With
`format: date-time` declared on each bound, strict implementers
could reasonably 400 on `?expires_from=&finalized_to=` as
malformed — diverging from the cycles-server reference
implementation that's shipped this behavior since the original
from/to revision (v0.1.25.20).

Same gap existed in the v0.1.25.20 spec: the original from/to
contract didn't explicitly call out blanks either; the server
just handled it via `parseIsoToEpochMs` returning null on
`raw.isBlank()`. This PR adds the normative rule once, in the
shared TIME-RANGE FILTERS validation block, covering all six
bounds — both the new `expires_*` / `finalized_*` pairs from
this revision and the v0.1.25.20 `from` / `to` pair. So the rule
becomes retroactively explicit for the original revision too,
which closes a latent divergence hazard.

Rationale included in the spec prose: clients commonly emit
unconditional query strings whose values come from
possibly-unset variables (e.g. `?from=${maybeUnset}`), and an
unset variable rendering as `""` is the common failure mode.
Rejecting these as malformed surfaces a cryptic 400 that adds
nothing over treating them as unset. The additive-parameter
guarantee is designed to avoid exactly this class of paper-cut
divergence.

Spec changes:
  - cycles-protocol-v0.yaml: new normative bullet in the
    TIME-RANGE FILTERS (NORMATIVE, ADDITIVE) validation
    sub-list. Servers MUST treat blank-string bounds as unset
    and MUST NOT 400 on them despite the format: date-time
    declaration.
  - changelogs/cycles-protocol-v0.md: matching bullet under
    the Validation section.
  - merged/cycles-openapi-protocol-merged.yaml: mechanically
    regenerated.

The per-param descriptions are intentionally not duplicated —
the shared block is normative, and a six-fold prose copy would
just be drift bait.

Verification:
  - npx spectral lint cycles-protocol-v0.yaml --fail-severity=error
    → 0 errors. 20 pre-existing warnings unchanged.
  - python scripts/validate_changelogs.py → all 5 specs OK.
  - python scripts/merge_specs.py → clean.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Suggestion] Add created_at Range Filtering (before / after) to listReservations

1 participant