diff --git a/CONFORMANCE.md b/CONFORMANCE.md index 24ad123..e50e787 100644 --- a/CONFORMANCE.md +++ b/CONFORMANCE.md @@ -96,7 +96,7 @@ A conformant implementation SHOULD: - Emit events for budget-state changes (reservation.*, budget.*, quota.*) matching the `EventType` enum. Implementations MAY sample or filter which events they emit, but emitted events MUST follow the schema. - Propagate `X-Cycles-Trace-Id` and W3C `traceparent` headers per `cycles-protocol-v0.yaml` §CORRELATION AND TRACING. Trace correlation is central to multi-service debugging. - Implement `POST /v1/decide` — marked OPTIONAL in the v0 spec, but agent frameworks need soft-landing signals for graceful degradation. -- Implement `GET /v1/reservations` (**listReservations**) — marked OPTIONAL in v0; useful for reservation recovery (re-discover a lost `reservation_id` via `idempotency_key`), for identifying stuck `ACTIVE` reservations, and for time-window queries via the additive `from`/`to` parameters (v0.1.25, revision 2026-05-21). +- Implement `GET /v1/reservations` (**listReservations**) — marked OPTIONAL in v0; useful for reservation recovery (re-discover a lost `reservation_id` via `idempotency_key`), for identifying stuck `ACTIVE` reservations, and for time-window queries via the additive `from`/`to` (v0.1.25, revision 2026-05-21), `expires_from`/`expires_to`, and `finalized_from`/`finalized_to` (v0.1.25, revision 2026-05-22) parameters. - Implement `GET /v1/reservations/{reservation_id}` (**getReservation**) — marked "optional, for debugging" in v0; valuable for support / monitoring of long-running reservations. - Implement `POST /v1/events` (**createEvent**) — marked OPTIONAL in v0; the post-only accounting path for cases where pre-estimation is unavailable (bills-later providers, receipt ingestion). diff --git a/changelogs/cycles-protocol-v0.md b/changelogs/cycles-protocol-v0.md index 8f5304b..3f9bac5 100644 --- a/changelogs/cycles-protocol-v0.md +++ b/changelogs/cycles-protocol-v0.md @@ -6,6 +6,101 @@ New entries are added directly to this file. See `scripts/validate_changelogs.py --- +## v0.1.25 — 2026-05-22 + +_(revision 2026-05-22 — `expires_*` / `finalized_*` time-range filters on listReservations)_ + +- Adds four optional query parameters to `listReservations` + (`GET /v1/reservations`), mirroring the shape of the + `from`/`to` window filter shipped in revision 2026-05-21: + * `expires_from`: ISO 8601 date-time. Inclusive lower bound + on `expires_at_ms`. May be supplied alone (open upper bound) + or paired with `expires_to`. + * `expires_to`: ISO 8601 date-time. Inclusive upper bound on + `expires_at_ms`. May be supplied alone (open lower bound) + or paired with `expires_from`. + * `finalized_from`: ISO 8601 date-time. Inclusive lower bound + on `finalized_at_ms`. + * `finalized_to`: ISO 8601 date-time. Inclusive upper bound + on `finalized_at_ms`. +- Closes the use case left out of the 2026-05-21 revision: cleanup + sweepers that need to locate reservations expiring (or already + expired/finalized) within a window. The original `from`/`to` + binds to `created_at_ms`, which is unhelpful for "find what's + expiring in the next hour" or "find all rows finalized today". +- All three windows compose with AND semantics. A row must satisfy + every supplied window predicate to be returned. Each pair is + independently bound to its target field — `expires_*` binds to + `expires_at_ms` regardless of `sort_by` (just like `from`/`to` + binds to `created_at_ms`), and `finalized_*` likewise. +- `finalized_at_ms` is OPTIONAL on `ReservationSummary` / + `ReservationDetail` and is populated ONLY on COMMITTED and + RELEASED rows (absent on ACTIVE and EXPIRED). Rows where the + field is absent 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 on the behavior. + Callers who want a window over EXPIRED rows should use + `expires_from` / `expires_to` against `expires_at_ms`, which + is required on every row. +- `finalized_at_ms` added as an OPTIONAL property to + `ReservationSummary`. Pre-revision the field was declared only + on `ReservationDetail` while `ReservationSummary` carried + `additionalProperties: false` — meaning servers could not + legally include it in list results, and the proposed filter + would have produced rows whose timestamps callers could not + see without a follow-up `getReservation` call. The summary now + carries the same field with the same population semantics as + the detail; existing clients with strict schemas remain + compatible because the field is OPTIONAL (absent in pre-revision + responses, valid under the new schema when present). +- Validation (mirrors revision 2026-05-21): + * Servers MUST reject `expires_from > expires_to` and + `finalized_from > finalized_to` with HTTP 400 + INVALID_REQUEST. + * Either side of each pair may be supplied alone. + * Malformed date-time values MUST be rejected with HTTP 400 + INVALID_REQUEST. + * Blank-string values for any window bound MUST be treated + as unset (NORMATIVE; applies to all six bounds — `from`, + `to`, `expires_from`, `expires_to`, `finalized_from`, + `finalized_to`). A client sending `?expires_from=` MUST + be handled identically to one omitting the param. This + makes normative the behavior the cycles-server reference + implementation has shipped since v0.1.25.20 (the original + `from`/`to` revision) — strict implementers would + otherwise reasonably 400 on `""` per the `format: date-time` + declaration, and divergence between conformant servers on + this common client-side pattern (`?from=${maybeUnset}`) + is the kind of cryptic-400 the additive-parameter + guarantee is designed to avoid. +- 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. +- Cursor invalidation extends the v0.1.25.20 `FilterHasher` shape: + sorted-path cursors fold all six window-bound values into the + canonical filter hash, so reusing a sorted cursor under a + different `(from, to, expires_from, expires_to, finalized_from, + finalized_to)` tuple returns HTTP 400 INVALID_REQUEST. Legacy + Redis-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 (`from`/`to` on `created_at_ms`, `expires_*` on + `expires_at_ms`, `finalized_*` on `finalized_at_ms`) and the + AND-composition rule. +- Backward compatible: purely additive. 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. Old clients ignore the new query params and tolerate + the new response field (absent on pre-revision servers). Both + ApiKeyAuth and AdminKeyAuth callers see the new parameters. + +--- + ## v0.1.25 — 2026-05-21 _(revision 2026-05-21 — `from`/`to` created-time range filters on listReservations)_ diff --git a/cycles-protocol-v0.yaml b/cycles-protocol-v0.yaml index d8cbea7..8b5cc9e 100644 --- a/cycles-protocol-v0.yaml +++ b/cycles-protocol-v0.yaml @@ -1146,6 +1146,13 @@ components: finalized_at_ms: type: integer format: int64 + description: >- + Wall-clock time at which the reservation reached a terminal + state. Populated on COMMITTED and RELEASED rows only; + absent on ACTIVE and EXPIRED rows. The `finalized_from` / + `finalized_to` window filter on `listReservations` + (revision 2026-05-22) operates against this same field + with the same population semantics. scope_path: type: string description: Canonical scope path (server-derived) @@ -1275,6 +1282,19 @@ components: type: integer format: int64 minimum: 0 + finalized_at_ms: + type: integer + format: int64 + minimum: 0 + description: >- + Wall-clock time at which the reservation reached a terminal + state. Populated on COMMITTED and RELEASED rows only; + absent on ACTIVE and EXPIRED rows. OPTIONAL per the + `additionalProperties: false` invariant — older summaries + sent without this field remain valid. Mirrors the same field + on `ReservationDetail`. Added in revision 2026-05-22 to + support the `finalized_from` / `finalized_to` window filter + on `listReservations`. scope_path: type: string affected_scopes: @@ -1471,18 +1491,65 @@ paths: - Filtering on Subject.dimensions is out of scope for v0 unless explicitly implemented by the server. TIME-RANGE FILTERS (NORMATIVE, ADDITIVE): - - Query parameters `from` and `to` (ISO 8601 date-time strings) - bound the `created_at_ms` of returned reservations, inclusive - on both ends. - - The filter is fixed to `created_at_ms` regardless of `sort_by`; - sorting by `expires_at_ms` while filtering by `from`/`to` is - valid and well-defined. - - Either may be supplied alone (open interval) or together - (closed window). `from > to` MUST return 400 INVALID_REQUEST. - - Both are additive parameters: servers that don't recognize - them MUST ignore without error and return the unfiltered set. - This matches the family-wide `from`/`to` convention on - `listAuditLogs`, `listEvents`, and `listWebhookDeliveries`. + Three independent inclusive time-window filters are available, + each bound to a specific timestamp field on the reservation + entity. All bounds are ISO 8601 date-time strings. All are + additive parameters: servers that don't recognize them MUST + ignore without error. + + - `from` / `to` (revision 2026-05-21) — bound on + `created_at_ms`. The original window filter; matches + the family-wide convention on `listAuditLogs`, + `listEvents`, and `listWebhookDeliveries`. Always binds + to `created_at_ms` regardless of `sort_by`. + + - `expires_from` / `expires_to` — bound on `expires_at_ms`. + Primary use case: locate reservations that have expired + (or will expire) within a window — e.g. cleanup sweepers + that need to discover abandoned ACTIVE reservations. The + field is required on every `ReservationSummary` / + `ReservationDetail`, so this filter applies to every + row regardless of `status`. + + - `finalized_from` / `finalized_to` — bound on + `finalized_at_ms`. The `finalized_at_ms` field is OPTIONAL + on `ReservationSummary` / `ReservationDetail` and is + populated ONLY on COMMITTED and RELEASED rows (absent on + ACTIVE and EXPIRED). Rows where the field is absent MUST + be excluded from results when either `finalized_from` or + `finalized_to` is supplied — the predicate naturally fails + on field-absent rows. Callers who want a window over + EXPIRED rows should use `expires_from` / `expires_to` + against `expires_at_ms`, which is required on every row. + + Validation (applies to all three pairs): + - For each pair, `from > to` MUST return 400 INVALID_REQUEST + (e.g., `expires_from > expires_to`). + - Either side may be supplied alone (open interval). + - Blank-string values for any window bound MUST be treated + as unset (NORMATIVE). A client sending + `?expires_from=&finalized_to=` MUST be handled identically + to one omitting both parameters entirely. This applies to + all six bounds (`from`, `to`, `expires_from`, `expires_to`, + `finalized_from`, `finalized_to`). Servers MUST NOT 400 on + empty-string values despite the `format: date-time` + declaration. Rationale: clients commonly emit unconditional + query strings whose values come from possibly-unset + variables (e.g., `?from=${maybeUnset}&to=${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 three pairs combine with AND semantics: a row must + satisfy every supplied window predicate to be returned. + + Cursor invalidation: sorted-path cursors fold the supplied + window bounds into the canonical filter hash. Reusing a + sorted cursor under a different `(from, to, expires_from, + expires_to, finalized_from, finalized_to)` tuple returns + 400 INVALID_REQUEST. Legacy SCAN cursors do not carry filter + state; callers paginating without `sort_by` must keep all + window bounds stable across pages, matching the legacy + path's treatment of every other filter. TENANCY (NORMATIVE): - Under ApiKeyAuth: the server MUST scope results to the effective @@ -1570,6 +1637,88 @@ paths: bound) or paired with `from`. Servers MUST reject `from > to` with HTTP 400 INVALID_REQUEST. + Additive parameter — servers that don't recognize it MUST + ignore without error. + schema: + type: string + format: date-time + - name: expires_from + in: query + required: false + description: >- + Inclusive lower bound on reservation expiry time. ISO 8601 + date-time. When set, the server MUST return only reservations + whose `expires_at_ms` is greater than or equal to this + timestamp. The filter ALWAYS binds to `expires_at_ms`, + independent of `sort_by` and independent of the `from`/`to` + window on `created_at_ms`. May be supplied alone (no upper + bound) or paired with `expires_to`. Servers MUST reject + `expires_from > expires_to` with HTTP 400 INVALID_REQUEST. + + Use case: cleanup sweepers locating reservations that have + expired or will expire within a window. Applies to all rows + regardless of `status` since `expires_at_ms` is required. + + Additive parameter — servers that don't recognize it MUST + ignore without error. + schema: + type: string + format: date-time + - name: expires_to + in: query + required: false + description: >- + Inclusive upper bound on reservation expiry time. ISO 8601 + date-time. When set, the server MUST return only reservations + whose `expires_at_ms` is less than or equal to this + timestamp. Same binding and open-interval rules as + `expires_from`. Servers MUST reject `expires_from > + expires_to` with HTTP 400 INVALID_REQUEST. + + Additive parameter — servers that don't recognize it MUST + ignore without error. + schema: + type: string + format: date-time + - name: finalized_from + in: query + required: false + description: >- + Inclusive lower bound on reservation finalization time. + ISO 8601 date-time. When set, the server MUST return only + reservations whose `finalized_at_ms` is greater than or + equal to this timestamp. The filter ALWAYS binds to + `finalized_at_ms`, independent of `sort_by`. May be supplied + alone (no upper bound) or paired with `finalized_to`. + Servers MUST reject `finalized_from > finalized_to` with + HTTP 400 INVALID_REQUEST. + + Behavior on rows without `finalized_at_ms` (NORMATIVE): + the field is OPTIONAL on reservation responses and is + populated ONLY on COMMITTED and RELEASED rows (absent on + ACTIVE and EXPIRED). Rows where the field is absent MUST + be excluded from results when either `finalized_from` or + `finalized_to` is supplied. Callers who want a window over + EXPIRED rows should use `expires_from` / `expires_to` + against `expires_at_ms`. + + Additive parameter — servers that don't recognize it MUST + ignore without error. + schema: + type: string + format: date-time + - name: finalized_to + in: query + required: false + description: >- + Inclusive upper bound on reservation finalization time. + ISO 8601 date-time. When set, the server MUST return only + reservations whose `finalized_at_ms` is less than or equal + to this timestamp. Same binding, open-interval, and + ACTIVE-row-exclusion rules as `finalized_from`. Servers MUST + reject `finalized_from > finalized_to` with HTTP 400 + INVALID_REQUEST. + Additive parameter — servers that don't recognize it MUST ignore without error. schema: diff --git a/merged/cycles-openapi-protocol-merged.yaml b/merged/cycles-openapi-protocol-merged.yaml index 0f2101c..3981b47 100644 --- a/merged/cycles-openapi-protocol-merged.yaml +++ b/merged/cycles-openapi-protocol-merged.yaml @@ -765,6 +765,10 @@ components: finalized_at_ms: type: integer format: int64 + description: Wall-clock time at which the reservation reached a terminal state. Populated on + COMMITTED and RELEASED rows only; absent on ACTIVE and EXPIRED rows. The `finalized_from` + / `finalized_to` window filter on `listReservations` (revision 2026-05-22) operates against + this same field with the same population semantics. scope_path: type: string description: Canonical scope path (server-derived) @@ -883,6 +887,15 @@ components: type: integer format: int64 minimum: 0 + finalized_at_ms: + type: integer + format: int64 + minimum: 0 + description: 'Wall-clock time at which the reservation reached a terminal state. Populated on + COMMITTED and RELEASED rows only; absent on ACTIVE and EXPIRED rows. OPTIONAL per the `additionalProperties: + false` invariant — older summaries sent without this field remain valid. Mirrors the same + field on `ReservationDetail`. Added in revision 2026-05-22 to support the `finalized_from` + / `finalized_to` window filter on `listReservations`.' scope_path: type: string affected_scopes: @@ -2187,18 +2200,65 @@ paths: - Filtering on Subject.dimensions is out of scope for v0 unless explicitly implemented by the server. TIME-RANGE FILTERS (NORMATIVE, ADDITIVE): - - Query parameters `from` and `to` (ISO 8601 date-time strings) - bound the `created_at_ms` of returned reservations, inclusive - on both ends. - - The filter is fixed to `created_at_ms` regardless of `sort_by`; - sorting by `expires_at_ms` while filtering by `from`/`to` is - valid and well-defined. - - Either may be supplied alone (open interval) or together - (closed window). `from > to` MUST return 400 INVALID_REQUEST. - - Both are additive parameters: servers that don't recognize - them MUST ignore without error and return the unfiltered set. - This matches the family-wide `from`/`to` convention on - `listAuditLogs`, `listEvents`, and `listWebhookDeliveries`. + Three independent inclusive time-window filters are available, + each bound to a specific timestamp field on the reservation + entity. All bounds are ISO 8601 date-time strings. All are + additive parameters: servers that don't recognize them MUST + ignore without error. + + - `from` / `to` (revision 2026-05-21) — bound on + `created_at_ms`. The original window filter; matches + the family-wide convention on `listAuditLogs`, + `listEvents`, and `listWebhookDeliveries`. Always binds + to `created_at_ms` regardless of `sort_by`. + + - `expires_from` / `expires_to` — bound on `expires_at_ms`. + Primary use case: locate reservations that have expired + (or will expire) within a window — e.g. cleanup sweepers + that need to discover abandoned ACTIVE reservations. The + field is required on every `ReservationSummary` / + `ReservationDetail`, so this filter applies to every + row regardless of `status`. + + - `finalized_from` / `finalized_to` — bound on + `finalized_at_ms`. The `finalized_at_ms` field is OPTIONAL + on `ReservationSummary` / `ReservationDetail` and is + populated ONLY on COMMITTED and RELEASED rows (absent on + ACTIVE and EXPIRED). Rows where the field is absent MUST + be excluded from results when either `finalized_from` or + `finalized_to` is supplied — the predicate naturally fails + on field-absent rows. Callers who want a window over + EXPIRED rows should use `expires_from` / `expires_to` + against `expires_at_ms`, which is required on every row. + + Validation (applies to all three pairs): + - For each pair, `from > to` MUST return 400 INVALID_REQUEST + (e.g., `expires_from > expires_to`). + - Either side may be supplied alone (open interval). + - Blank-string values for any window bound MUST be treated + as unset (NORMATIVE). A client sending + `?expires_from=&finalized_to=` MUST be handled identically + to one omitting both parameters entirely. This applies to + all six bounds (`from`, `to`, `expires_from`, `expires_to`, + `finalized_from`, `finalized_to`). Servers MUST NOT 400 on + empty-string values despite the `format: date-time` + declaration. Rationale: clients commonly emit unconditional + query strings whose values come from possibly-unset + variables (e.g., `?from=${maybeUnset}&to=${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 three pairs combine with AND semantics: a row must + satisfy every supplied window predicate to be returned. + + Cursor invalidation: sorted-path cursors fold the supplied + window bounds into the canonical filter hash. Reusing a + sorted cursor under a different `(from, to, expires_from, + expires_to, finalized_from, finalized_to)` tuple returns + 400 INVALID_REQUEST. Legacy SCAN cursors do not carry filter + state; callers paginating without `sort_by` must keep all + window bounds stable across pages, matching the legacy + path's treatment of every other filter. TENANCY (NORMATIVE): - Under ApiKeyAuth: the server MUST scope results to the effective @@ -2278,6 +2338,44 @@ paths: schema: type: string format: date-time + - name: expires_from + in: query + required: false + description: |- + Inclusive lower bound on reservation expiry time. ISO 8601 date-time. When set, the server MUST return only reservations whose `expires_at_ms` is greater than or equal to this timestamp. The filter ALWAYS binds to `expires_at_ms`, independent of `sort_by` and independent of the `from`/`to` window on `created_at_ms`. May be supplied alone (no upper bound) or paired with `expires_to`. Servers MUST reject `expires_from > expires_to` with HTTP 400 INVALID_REQUEST. + Use case: cleanup sweepers locating reservations that have expired or will expire within a window. Applies to all rows regardless of `status` since `expires_at_ms` is required. + Additive parameter — servers that don't recognize it MUST ignore without error. + schema: + type: string + format: date-time + - name: expires_to + in: query + required: false + description: |- + Inclusive upper bound on reservation expiry time. ISO 8601 date-time. When set, the server MUST return only reservations whose `expires_at_ms` is less than or equal to this timestamp. Same binding and open-interval rules as `expires_from`. Servers MUST reject `expires_from > expires_to` with HTTP 400 INVALID_REQUEST. + Additive parameter — servers that don't recognize it MUST ignore without error. + schema: + type: string + format: date-time + - name: finalized_from + in: query + required: false + description: |- + Inclusive lower bound on reservation finalization time. ISO 8601 date-time. When set, the server MUST return only reservations whose `finalized_at_ms` is greater than or equal to this timestamp. The filter ALWAYS binds to `finalized_at_ms`, independent of `sort_by`. May be supplied alone (no upper bound) or paired with `finalized_to`. Servers MUST reject `finalized_from > finalized_to` with HTTP 400 INVALID_REQUEST. + Behavior on rows without `finalized_at_ms` (NORMATIVE): the field is OPTIONAL on reservation responses and is populated ONLY on COMMITTED and RELEASED rows (absent on ACTIVE and EXPIRED). Rows where the field is absent MUST be excluded from results when either `finalized_from` or `finalized_to` is supplied. Callers who want a window over EXPIRED rows should use `expires_from` / `expires_to` against `expires_at_ms`. + Additive parameter — servers that don't recognize it MUST ignore without error. + schema: + type: string + format: date-time + - name: finalized_to + in: query + required: false + description: |- + Inclusive upper bound on reservation finalization time. ISO 8601 date-time. When set, the server MUST return only reservations whose `finalized_at_ms` is less than or equal to this timestamp. Same binding, open-interval, and ACTIVE-row-exclusion rules as `finalized_from`. Servers MUST reject `finalized_from > finalized_to` with HTTP 400 INVALID_REQUEST. + Additive parameter — servers that don't recognize it MUST ignore without error. + schema: + type: string + format: date-time - name: sort_by in: query required: false