You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
### 2026-04-10 — v0.1.25.6: Distinguish UNIT_MISMATCH from BUDGET_NOT_FOUND on reserve/event
10
+
11
+
**Bug (runcycles/cycles-client-rust#8):**`POST /v1/reservations` with `Amount::tokens(1000)` against a scope whose budget was stored in `USD_MICROCENTS` returned `404 BUDGET_NOT_FOUND`. The client could not distinguish "no budget at this scope" from "budget exists but in a different unit" and had no hint toward the fix. `/v1/events` had the same latent bug.
12
+
13
+
**Root cause:**`reserve.lua` / `event.lua` key budgets by `budget:<scope>:<unit>`. When the requested unit doesn't match the stored unit, the key doesn't exist, the scope is silently skipped, and `#budgeted_scopes == 0` falls through to `BUDGET_NOT_FOUND`. The existing `UNIT_MISMATCH` branch in `event.lua` only caught an internal inconsistency between key suffix and stored `unit` field — it did not catch the cross-unit case.
14
+
15
+
**Fix:** On the empty-budgeted-scopes error path only, both scripts now probe the fixed `UnitEnum` set (`USD_MICROCENTS`, `TOKENS`, `CREDITS`, `RISK_POINTS`) via `EXISTS budget:<scope>:<unit_alt>` for each affected scope. If any alternate-unit budget exists, the script returns `UNIT_MISMATCH` (400) with `scope`, `requested_unit`, and `expected_units` in the error payload so the client can self-correct. Otherwise falls through to the existing `BUDGET_NOT_FOUND` (404).
16
+
17
+
Cascade semantics preserved: scopes without a budget at the requested unit are still silently skipped during the main validation loop — the probe only fires when every affected scope missed. No hot-path change; the cost is paid once on the error path only.
18
+
19
+
`evaluateDryRun` and `/v1/decide` (the non-Lua Java paths) get the symmetric probe via a shared `probeAlternateUnits` helper and throw `UNIT_MISMATCH` (400) to match the reserve/event behavior. Spec line 1131-1134 only prohibits 409 on `/decide` for debt/overdraft conditions; 400 for a request-validity error (wrong unit) is permitted and is consistent across all four entry points.
20
+
21
+
**Modified files:**
22
+
-`reserve.lua` — new `ARGV[15] = units_csv`; scopes now start at ARGV[16]; alternate-unit probe added to the empty-budgeted-scopes branch
23
+
-`event.lua` — new `ARGV[14] = units_csv`; scopes now start at ARGV[15]; symmetric probe
24
+
-`RedisReservationRepository.java` — `UNIT_CSV` constant derived once from `Enums.UnitEnum.values()`; passed into both `createReservation` and `createEvent` args; `evaluateDryRun` and `decide` mirror the probe via a shared `probeAlternateUnits(jedis, scope, requestedUnit)` helper; `handleScriptError` extracts `scope` / `requested_unit` / `expected_units` for reserve/event and falls back to the no-detail factory for commit.lua's legacy form
-`ReservationLifecycleIntegrationTest.java` — `shouldRejectWhenNoBudgetExistsForUnit` renamed to `shouldRejectWithUnitMismatchWhenBudgetExistsInDifferentUnit` and flipped to expect 400 + details; added `shouldReturnBudgetNotFoundWhenNoBudgetAtAnyUnit` regression guard and `shouldReturnUnitMismatchOnDryRunWhenBudgetExistsInDifferentUnit`
27
+
-`DecisionAndEventIntegrationTest.java` — `shouldRejectEventWithUnitMismatch` flipped from 404 `NOT_FOUND` to 400 `UNIT_MISMATCH` + details; added `shouldReturnBudgetNotFoundWhenNoBudgetAtAnyUnitOnEvent`, `shouldRejectDecideWithUnitMismatchWhenBudgetExistsInDifferentUnit`, and `shouldReturnDenyBudgetNotFoundOnDecideWhenNoBudgetAtAnyUnit`
**Out of scope:** Client-side rust SDK changes (not needed — structured error is enough for the user to correct their call). Protocol YAML spec update lives in `runcycles/cycles-protocol` and is handled on a coordinated branch (adds `"404"` to `/v1/reservations` POST and `/v1/events` POST response lists + broadens normative UNIT_MISMATCH wording to cover reserve).
36
+
9
37
### 2026-04-07 — v0.1.25.4: Event data payload completeness
10
38
11
39
**Compliance review** against protocol spec v0.1.25 + admin spec v0.1.25 found 5 event data payload gaps. Core protocol (endpoints, schemas, error codes, Lua scripts, idempotency, scope derivation, auth/tenancy) was fully compliant.
@@ -276,7 +304,7 @@ Two-pass audit covering:
276
304
- RESERVATION_FINALIZED → 409
277
305
- RESERVATION_EXPIRED → 410
278
306
- NOT_FOUND → 404
279
-
- UNIT_MISMATCH → 400 (enforced in commit.lua and event.lua)
307
+
- UNIT_MISMATCH → 400 (enforced in reserve.lua, commit.lua, event.lua; reserve/event paths return `scope`, `requested_unit`, `expected_units` in details so the client can self-correct — see v0.1.25.6)
The server starts on **port 7878**. Interactive API docs: http://localhost:7878/swagger-ui.html
@@ -585,7 +585,7 @@ All errors use this envelope:
585
585
| Code | HTTP | Meaning |
586
586
|---|---|---|
587
587
|`INVALID_REQUEST`| 400 | Missing or invalid field |
588
-
|`UNIT_MISMATCH`| 400 |Commit unit differs from reservation unit, or event unit not supported for target scope |
588
+
|`UNIT_MISMATCH`| 400 |Requested unit does not match the stored budget unit for the target scope. Raised by reserve/commit/event. Reserve and event responses include `details.scope`, `details.requested_unit`, and `details.expected_units` so the client can self-correct; commit uses the legacy no-detail form.|
589
589
|`UNAUTHORIZED`| 401 | Missing or invalid API key |
590
590
|`FORBIDDEN`| 403 | Tenant in request does not match API key |
591
591
|`NOT_FOUND`| 404 | Reservation, budget, or resource not found |
Copy file name to clipboardExpand all lines: cycles-protocol-service/cycles-protocol-service-api/src/test/java/io/runcycles/protocol/api/DecisionAndEventIntegrationTest.java
Copy file name to clipboardExpand all lines: cycles-protocol-service/cycles-protocol-service-api/src/test/java/io/runcycles/protocol/api/ReservationLifecycleIntegrationTest.java
Copy file name to clipboardExpand all lines: cycles-protocol-service/cycles-protocol-service-data/src/main/java/io/runcycles/protocol/data/exception/CyclesProtocolException.java
+12Lines changed: 12 additions & 0 deletions
Original file line number
Diff line number
Diff line change
@@ -2,6 +2,8 @@
2
2
3
3
importio.runcycles.protocol.model.Enums;
4
4
importlombok.Getter;
5
+
importjava.util.LinkedHashMap;
6
+
importjava.util.List;
5
7
importjava.util.Map;
6
8
7
9
/** Cycles Protocol v0.1.25 */
@@ -55,6 +57,16 @@ public static CyclesProtocolException idempotencyMismatch() {
0 commit comments