mcp-usage-control 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +141 -0
  3. package/dist/index.d.ts +533 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +2131 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/observability.d.ts +154 -0
  8. package/dist/observability.d.ts.map +1 -0
  9. package/dist/observability.js +171 -0
  10. package/dist/observability.js.map +1 -0
  11. package/dist/operational.d.ts +87 -0
  12. package/dist/operational.d.ts.map +1 -0
  13. package/dist/operational.js +193 -0
  14. package/dist/operational.js.map +1 -0
  15. package/dist/progressive-store-conformance.d.ts +4 -0
  16. package/dist/progressive-store-conformance.d.ts.map +1 -0
  17. package/dist/progressive-store-conformance.js +333 -0
  18. package/dist/progressive-store-conformance.js.map +1 -0
  19. package/dist/reconciliation-conformance.d.ts +36 -0
  20. package/dist/reconciliation-conformance.d.ts.map +1 -0
  21. package/dist/reconciliation-conformance.js +211 -0
  22. package/dist/reconciliation-conformance.js.map +1 -0
  23. package/dist/settlement-outcomes.d.ts +24 -0
  24. package/dist/settlement-outcomes.d.ts.map +1 -0
  25. package/dist/settlement-outcomes.js +68 -0
  26. package/dist/settlement-outcomes.js.map +1 -0
  27. package/dist/store-conformance.d.ts +50 -0
  28. package/dist/store-conformance.d.ts.map +1 -0
  29. package/dist/store-conformance.js +378 -0
  30. package/dist/store-conformance.js.map +1 -0
  31. package/dist/thresholds.d.ts +26 -0
  32. package/dist/thresholds.d.ts.map +1 -0
  33. package/dist/thresholds.js +56 -0
  34. package/dist/thresholds.js.map +1 -0
  35. package/dist/vector-store-conformance.d.ts +4 -0
  36. package/dist/vector-store-conformance.d.ts.map +1 -0
  37. package/dist/vector-store-conformance.js +297 -0
  38. package/dist/vector-store-conformance.js.map +1 -0
  39. package/dist/weighted-credits.d.ts +41 -0
  40. package/dist/weighted-credits.d.ts.map +1 -0
  41. package/dist/weighted-credits.js +143 -0
  42. package/dist/weighted-credits.js.map +1 -0
  43. package/dist/windowed-budget-keys.d.ts +36 -0
  44. package/dist/windowed-budget-keys.d.ts.map +1 -0
  45. package/dist/windowed-budget-keys.js +178 -0
  46. package/dist/windowed-budget-keys.js.map +1 -0
  47. package/package.json +113 -0
package/dist/index.js ADDED
@@ -0,0 +1,2131 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { emitUsageEvent, usageErrorName, } from './observability.js';
3
+ export * from './observability.js';
4
+ export * from './weighted-credits.js';
5
+ export * from './windowed-budget-keys.js';
6
+ export const USAGE_INPUT_LIMITS = Object.freeze({
7
+ identifierBytes: 1024,
8
+ scalarBudgets: 64,
9
+ vectorDimensions: 32,
10
+ vectorBudgetsPerDimension: 32,
11
+ vectorBudgetsTotal: 128,
12
+ });
13
+ export function assertUsageIdentifier(value, name) {
14
+ if (typeof value !== 'string' || value.length === 0) {
15
+ throw new RangeError(`${name} must be a non-empty string`);
16
+ }
17
+ if (new TextEncoder().encode(value).byteLength > USAGE_INPUT_LIMITS.identifierBytes) {
18
+ throw new RangeError(`${name} exceeds the ${USAGE_INPUT_LIMITS.identifierBytes}-byte input limit`);
19
+ }
20
+ }
21
+ export function validateUsageRequestEnvelope(request) {
22
+ if (request === null || typeof request !== 'object' || Array.isArray(request)) {
23
+ throw new RangeError('request must be an object');
24
+ }
25
+ if (request.principal === null || typeof request.principal !== 'object' || Array.isArray(request.principal)) {
26
+ throw new RangeError('principal must be an object');
27
+ }
28
+ assertUsageIdentifier(request.operationId, 'operationId');
29
+ assertUsageIdentifier(request.principal.id, 'principal.id');
30
+ assertUsageIdentifier(request.tool, 'tool');
31
+ if (request.principal.tenantId !== undefined) {
32
+ assertUsageIdentifier(request.principal.tenantId, 'principal.tenantId');
33
+ }
34
+ if (request.principal.plan !== undefined) {
35
+ assertUsageIdentifier(request.principal.plan, 'principal.plan');
36
+ }
37
+ }
38
+ export function validateUsageBudgetEnvelope(budgets) {
39
+ if (!Array.isArray(budgets))
40
+ throw new TypeError('budgets must be an array');
41
+ if (budgets.length === 0)
42
+ throw new RangeError('budgets must contain at least one budget');
43
+ if (budgets.length > USAGE_INPUT_LIMITS.scalarBudgets) {
44
+ throw new RangeError(`budgets exceed the ${USAGE_INPUT_LIMITS.scalarBudgets}-budget input limit`);
45
+ }
46
+ for (const budget of budgets) {
47
+ if (budget === null || typeof budget !== 'object' || Array.isArray(budget)) {
48
+ throw new TypeError('budget must be an object');
49
+ }
50
+ assertUsageIdentifier(budget.key, 'budget.key');
51
+ }
52
+ }
53
+ export function validateUsageVectorEnvelope(dimensions) {
54
+ if (!Array.isArray(dimensions))
55
+ throw new TypeError('dimensions must be an array');
56
+ if (dimensions.length === 0)
57
+ throw new RangeError('dimensions must contain at least one dimension');
58
+ if (dimensions.length > USAGE_INPUT_LIMITS.vectorDimensions) {
59
+ throw new RangeError(`dimensions exceed the ${USAGE_INPUT_LIMITS.vectorDimensions}-dimension input limit`);
60
+ }
61
+ let totalBudgets = 0;
62
+ for (const dimension of dimensions) {
63
+ if (dimension === null || typeof dimension !== 'object' || Array.isArray(dimension)) {
64
+ throw new TypeError('dimension must be an object');
65
+ }
66
+ assertUsageIdentifier(dimension.key, 'dimension.key');
67
+ if (!Array.isArray(dimension.budgets) || dimension.budgets.length === 0) {
68
+ throw new RangeError('dimension.budgets must contain at least one budget');
69
+ }
70
+ if (dimension.budgets.length > USAGE_INPUT_LIMITS.vectorBudgetsPerDimension) {
71
+ throw new RangeError(`dimension budgets exceed the ${USAGE_INPUT_LIMITS.vectorBudgetsPerDimension}-budget input limit`);
72
+ }
73
+ totalBudgets += dimension.budgets.length;
74
+ if (totalBudgets > USAGE_INPUT_LIMITS.vectorBudgetsTotal) {
75
+ throw new RangeError(`vector topology exceeds the ${USAGE_INPUT_LIMITS.vectorBudgetsTotal}-budget input limit`);
76
+ }
77
+ validateUsageBudgetEnvelope(dimension.budgets);
78
+ }
79
+ }
80
+ export function validateUsageVectorGrowthEnvelope(dimensions) {
81
+ validateUsageVectorEnvelope(dimensions.map(dimension => ({
82
+ key: dimension.key,
83
+ units: dimension.additionalUnits,
84
+ budgets: dimension.budgets,
85
+ })));
86
+ }
87
+ export class UsageStateError extends Error {
88
+ constructor(message) {
89
+ super(message);
90
+ this.name = 'UsageStateError';
91
+ }
92
+ }
93
+ export class UsageDeniedError extends Error {
94
+ reason;
95
+ constructor(reason) {
96
+ super('Usage denied by usage policy');
97
+ this.reason = reason;
98
+ this.name = 'UsageDeniedError';
99
+ }
100
+ }
101
+ export class UsageLease {
102
+ store;
103
+ reservation;
104
+ ttlMs;
105
+ observer;
106
+ metadata;
107
+ unresolvedGrowth;
108
+ constructor(store, reservation, ttlMs, observer, metadata, unresolvedGrowth) {
109
+ this.store = store;
110
+ this.reservation = reservation;
111
+ this.ttlMs = ttlMs;
112
+ this.observer = observer;
113
+ this.metadata = metadata;
114
+ this.unresolvedGrowth =
115
+ unresolvedGrowth === undefined ? undefined : canonicalizeGrowthRequest(unresolvedGrowth);
116
+ }
117
+ get reservedUnits() {
118
+ return this.reservation.reservedUnits;
119
+ }
120
+ /** Export a detached snapshot for trusted server-side suspend/resume workflows. */
121
+ toResumeState() {
122
+ return {
123
+ reservation: cloneReservationRecord(this.reservation),
124
+ ttlMs: this.ttlMs,
125
+ ...(this.metadata === undefined ? {} : { metadata: { ...this.metadata } }),
126
+ ...(this.unresolvedGrowth === undefined
127
+ ? {}
128
+ : { unresolvedGrowth: cloneGrowthRequest(this.unresolvedGrowth) }),
129
+ };
130
+ }
131
+ /**
132
+ * Increase capacity on the same logical reservation.
133
+ *
134
+ * A thrown Store/provider error is ambiguous. After one, this lease permits only an exact
135
+ * retry of the same increment until an authoritative accepted/denied result is returned.
136
+ */
137
+ async grow(input) {
138
+ const request = canonicalizeGrowthRequest(input);
139
+ if (this.unresolvedGrowth && !sameGrowthRequest(this.unresolvedGrowth, request)) {
140
+ throw new UsageStateError('A reservation growth attempt is unresolved; retry the same incrementId and parameters');
141
+ }
142
+ if (!isProgressiveUsageStore(this.store)) {
143
+ throw new UsageStateError('UsageStore does not support progressive reservation growth');
144
+ }
145
+ const expectedGrowthCursor = this.reservation.growthCursor;
146
+ if (!expectedGrowthCursor) {
147
+ throw new UsageStateError('Reservation was not created with progressive growth support');
148
+ }
149
+ this.unresolvedGrowth = request;
150
+ try {
151
+ const result = validateStoreGrowthResult(await this.store.growReservation({
152
+ reservationId: this.reservation.id,
153
+ incrementId: request.incrementId,
154
+ expectedGrowthCursor,
155
+ additionalUnits: request.additionalUnits,
156
+ budgets: request.budgets,
157
+ }), this.reservation, request);
158
+ this.reservation.growthCursor = result.growthCursor;
159
+ if (result.accepted)
160
+ this.reservation.reservedUnits = result.reservedUnits;
161
+ this.unresolvedGrowth = undefined;
162
+ return result;
163
+ }
164
+ catch (error) {
165
+ if (error instanceof UsageStateError) {
166
+ // Built-in Stores use UsageStateError only for authoritative state rejection.
167
+ // Transport/provider ambiguity keeps the attempt pinned for exact retry.
168
+ this.unresolvedGrowth = undefined;
169
+ }
170
+ throw error;
171
+ }
172
+ }
173
+ async markLiable() {
174
+ try {
175
+ const marked = await this.store.markLiable({ reservationId: this.reservation.id });
176
+ this.reservation.expiresAt = marked.expiresAt;
177
+ return marked;
178
+ }
179
+ catch (error) {
180
+ emitUsageEvent(this.observer, {
181
+ type: 'operation.error',
182
+ timestamp: Date.now(),
183
+ phase: 'mark_liable',
184
+ source: 'store',
185
+ reservationId: this.reservation.id,
186
+ ...reservationIdentity(this.reservation),
187
+ errorName: usageErrorName(error),
188
+ ...(this.metadata === undefined ? {} : { metadata: this.metadata }),
189
+ });
190
+ throw error;
191
+ }
192
+ }
193
+ async renew(ttlMs = this.ttlMs) {
194
+ assertPositiveInteger(ttlMs, 'ttlMs');
195
+ try {
196
+ const renewed = await this.store.renew({ reservationId: this.reservation.id, ttlMs });
197
+ this.reservation.expiresAt = renewed.expiresAt;
198
+ return renewed;
199
+ }
200
+ catch (error) {
201
+ emitUsageEvent(this.observer, {
202
+ type: 'operation.error',
203
+ timestamp: Date.now(),
204
+ phase: 'renew',
205
+ source: 'store',
206
+ reservationId: this.reservation.id,
207
+ ...reservationIdentity(this.reservation),
208
+ errorName: usageErrorName(error),
209
+ ...(this.metadata === undefined ? {} : { metadata: this.metadata }),
210
+ });
211
+ throw error;
212
+ }
213
+ }
214
+ async settle(actualUnits, outcome) {
215
+ try {
216
+ const settlement = await this.store.settle({
217
+ reservationId: this.reservation.id,
218
+ actualUnits,
219
+ outcome,
220
+ });
221
+ emitUsageEvent(this.observer, {
222
+ type: 'settlement.completed',
223
+ timestamp: Date.now(),
224
+ reservationId: this.reservation.id,
225
+ ...reservationIdentity(this.reservation),
226
+ budgetKeys: [...this.reservation.budgetKeys],
227
+ reservedUnits: settlement.reservedUnits,
228
+ actualUnits: settlement.actualUnits,
229
+ releasedUnits: settlement.releasedUnits,
230
+ outcome: settlement.outcome,
231
+ ...(this.metadata === undefined ? {} : { metadata: this.metadata }),
232
+ });
233
+ return settlement;
234
+ }
235
+ catch (error) {
236
+ emitUsageEvent(this.observer, {
237
+ type: 'operation.error',
238
+ timestamp: Date.now(),
239
+ phase: 'settle',
240
+ source: 'store',
241
+ reservationId: this.reservation.id,
242
+ ...reservationIdentity(this.reservation),
243
+ errorName: usageErrorName(error),
244
+ ...(this.metadata === undefined ? {} : { metadata: this.metadata }),
245
+ });
246
+ throw error;
247
+ }
248
+ }
249
+ }
250
+ export class VectorUsageLease {
251
+ store;
252
+ reservation;
253
+ ttlMs;
254
+ observer;
255
+ metadata;
256
+ unresolvedGrowth;
257
+ constructor(store, reservation, ttlMs, observer, metadata, unresolvedGrowth) {
258
+ this.store = store;
259
+ this.reservation = reservation;
260
+ this.ttlMs = ttlMs;
261
+ this.observer = observer;
262
+ this.metadata = metadata;
263
+ this.unresolvedGrowth =
264
+ unresolvedGrowth === undefined
265
+ ? undefined
266
+ : canonicalizeVectorGrowthRequest(unresolvedGrowth);
267
+ }
268
+ get reservedByDimension() {
269
+ return this.reservation.dimensions.map(dimension => ({
270
+ key: dimension.key,
271
+ reservedUnits: dimension.reservedUnits,
272
+ }));
273
+ }
274
+ toResumeState() {
275
+ return {
276
+ reservation: cloneVectorReservationRecord(this.reservation),
277
+ ttlMs: this.ttlMs,
278
+ ...(this.metadata === undefined ? {} : { metadata: { ...this.metadata } }),
279
+ ...(this.unresolvedGrowth === undefined
280
+ ? {}
281
+ : { unresolvedGrowth: cloneVectorGrowthRequest(this.unresolvedGrowth) }),
282
+ };
283
+ }
284
+ async grow(input) {
285
+ const request = canonicalizeVectorGrowthRequest(input);
286
+ if (this.unresolvedGrowth && !sameVectorGrowthRequest(this.unresolvedGrowth, request)) {
287
+ throw new UsageStateError('A vector growth attempt is unresolved; retry the same incrementId and parameters');
288
+ }
289
+ if (!isVectorUsageStore(this.store)) {
290
+ throw new UsageStateError('UsageStore does not support atomic vector usage');
291
+ }
292
+ const expectedGrowthCursor = this.reservation.growthCursor;
293
+ if (!expectedGrowthCursor) {
294
+ throw new UsageStateError('Vector reservation was not created with growth support');
295
+ }
296
+ this.unresolvedGrowth = request;
297
+ try {
298
+ const result = validateStoreVectorGrowthResult(await this.store.growVectorReservation({
299
+ reservationId: this.reservation.id,
300
+ incrementId: request.incrementId,
301
+ expectedGrowthCursor,
302
+ dimensions: request.dimensions,
303
+ }), this.reservation, request);
304
+ this.reservation.growthCursor = result.growthCursor;
305
+ if (result.accepted) {
306
+ const byKey = new Map(result.reservedByDimension.map(item => [item.key, item.reservedUnits]));
307
+ for (const dimension of this.reservation.dimensions) {
308
+ const reservedUnits = byKey.get(dimension.key);
309
+ if (reservedUnits === undefined) {
310
+ throw new UsageStateError('Vector growth result omitted a reservation dimension');
311
+ }
312
+ dimension.reservedUnits = reservedUnits;
313
+ }
314
+ }
315
+ this.unresolvedGrowth = undefined;
316
+ return result;
317
+ }
318
+ catch (error) {
319
+ if (error instanceof UsageStateError)
320
+ this.unresolvedGrowth = undefined;
321
+ throw error;
322
+ }
323
+ }
324
+ async markLiable() {
325
+ try {
326
+ const marked = await this.store.markLiable({ reservationId: this.reservation.id });
327
+ this.reservation.expiresAt = marked.expiresAt;
328
+ return marked;
329
+ }
330
+ catch (error) {
331
+ emitUsageEvent(this.observer, {
332
+ type: 'operation.error',
333
+ timestamp: Date.now(),
334
+ phase: 'mark_liable',
335
+ source: 'store',
336
+ reservationId: this.reservation.id,
337
+ ...vectorReservationIdentity(this.reservation),
338
+ errorName: usageErrorName(error),
339
+ ...(this.metadata === undefined ? {} : { metadata: this.metadata }),
340
+ });
341
+ throw error;
342
+ }
343
+ }
344
+ async renew(ttlMs = this.ttlMs) {
345
+ assertPositiveInteger(ttlMs, 'ttlMs');
346
+ try {
347
+ const renewed = await this.store.renew({ reservationId: this.reservation.id, ttlMs });
348
+ this.reservation.expiresAt = renewed.expiresAt;
349
+ return renewed;
350
+ }
351
+ catch (error) {
352
+ emitUsageEvent(this.observer, {
353
+ type: 'operation.error',
354
+ timestamp: Date.now(),
355
+ phase: 'renew',
356
+ source: 'store',
357
+ reservationId: this.reservation.id,
358
+ ...vectorReservationIdentity(this.reservation),
359
+ errorName: usageErrorName(error),
360
+ ...(this.metadata === undefined ? {} : { metadata: this.metadata }),
361
+ });
362
+ throw error;
363
+ }
364
+ }
365
+ async settle(actualByDimension, outcome) {
366
+ if (!isVectorUsageStore(this.store)) {
367
+ throw new UsageStateError('UsageStore does not support atomic vector usage');
368
+ }
369
+ const actual = canonicalizeVectorActuals(actualByDimension, this.reservation.dimensions);
370
+ try {
371
+ const settlement = await this.store.settleVector({
372
+ reservationId: this.reservation.id,
373
+ actualByDimension: actual,
374
+ outcome,
375
+ });
376
+ emitUsageEvent(this.observer, {
377
+ type: 'vector.settlement.completed',
378
+ timestamp: Date.now(),
379
+ reservationId: this.reservation.id,
380
+ ...vectorReservationIdentity(this.reservation),
381
+ dimensions: settlement.dimensions.map(dimension => ({ ...dimension })),
382
+ outcome: settlement.outcome,
383
+ ...(this.metadata === undefined ? {} : { metadata: this.metadata }),
384
+ });
385
+ return settlement;
386
+ }
387
+ catch (error) {
388
+ emitUsageEvent(this.observer, {
389
+ type: 'operation.error',
390
+ timestamp: Date.now(),
391
+ phase: 'settle',
392
+ source: 'store',
393
+ reservationId: this.reservation.id,
394
+ ...vectorReservationIdentity(this.reservation),
395
+ errorName: usageErrorName(error),
396
+ ...(this.metadata === undefined ? {} : { metadata: this.metadata }),
397
+ });
398
+ throw error;
399
+ }
400
+ }
401
+ }
402
+ export class UsageControl {
403
+ store;
404
+ policy;
405
+ defaultReservationTtlMs;
406
+ observer;
407
+ metadata;
408
+ constructor(store, policy, optionsOrTtl = 60_000) {
409
+ this.store = store;
410
+ this.policy = policy;
411
+ if (typeof optionsOrTtl === 'number') {
412
+ this.defaultReservationTtlMs = optionsOrTtl;
413
+ }
414
+ else {
415
+ this.defaultReservationTtlMs = optionsOrTtl.defaultReservationTtlMs ?? 60_000;
416
+ this.observer = optionsOrTtl.observer;
417
+ this.metadata = optionsOrTtl.metadata;
418
+ }
419
+ assertPositiveInteger(this.defaultReservationTtlMs, 'defaultReservationTtlMs');
420
+ }
421
+ /**
422
+ * Reattach to an existing reservation without calling policy.quote() or reserve().
423
+ *
424
+ * The caller must obtain `state` from a trusted server-side `UsageLease` snapshot.
425
+ * The underlying store remains authoritative: the first renew/settle still fails
426
+ * closed if the reservation expired, was settled, or otherwise conflicts.
427
+ */
428
+ resumeLease(state) {
429
+ assertPositiveInteger(state.ttlMs, 'ttlMs');
430
+ const reservation = cloneReservationRecord(state.reservation);
431
+ return new UsageLease(this.store, reservation, state.ttlMs, this.observer, state.metadata === undefined ? undefined : { ...state.metadata }, state.unresolvedGrowth);
432
+ }
433
+ async reserve(request) {
434
+ validateRequestIdentity(request);
435
+ const requestForPolicy = request;
436
+ const metadata = resolveMetadata(this.metadata, requestForPolicy);
437
+ let quote;
438
+ try {
439
+ quote = normalizeUsagePolicyQuote(await this.policy.quote(requestForPolicy));
440
+ }
441
+ catch (error) {
442
+ emitUsageEvent(this.observer, {
443
+ type: 'operation.error',
444
+ timestamp: Date.now(),
445
+ phase: 'quote',
446
+ source: 'policy',
447
+ ...requestIdentity(requestForPolicy),
448
+ errorName: usageErrorName(error),
449
+ ...(metadata === undefined ? {} : { metadata }),
450
+ });
451
+ throw error;
452
+ }
453
+ if (quote.decision === 'deny') {
454
+ emitUsageEvent(this.observer, {
455
+ type: 'reserve.denied',
456
+ timestamp: Date.now(),
457
+ ...requestIdentity(requestForPolicy),
458
+ reason: quote.reason,
459
+ ...(metadata === undefined ? {} : { metadata }),
460
+ });
461
+ return { allowed: false, reason: quote.reason };
462
+ }
463
+ const budgets = quote.budgets;
464
+ const ttlMs = quote.reservationTtlMs ?? this.defaultReservationTtlMs;
465
+ assertPositiveInteger(ttlMs, 'reservationTtlMs');
466
+ let result;
467
+ try {
468
+ result = validateStoreReserveResult(await this.store.reserve({
469
+ request: requestForPolicy,
470
+ units: quote.units,
471
+ budgets,
472
+ ttlMs,
473
+ }), requestForPolicy, quote.units, budgets);
474
+ }
475
+ catch (error) {
476
+ emitUsageEvent(this.observer, {
477
+ type: 'operation.error',
478
+ timestamp: Date.now(),
479
+ phase: 'reserve',
480
+ source: 'store',
481
+ ...requestIdentity(requestForPolicy),
482
+ errorName: usageErrorName(error),
483
+ ...(metadata === undefined ? {} : { metadata }),
484
+ });
485
+ throw error;
486
+ }
487
+ if (!result.accepted) {
488
+ emitUsageEvent(this.observer, {
489
+ type: 'reserve.denied',
490
+ timestamp: Date.now(),
491
+ ...requestIdentity(requestForPolicy),
492
+ reason: result.reason,
493
+ ...(result.limitingBudgetKey === undefined
494
+ ? {}
495
+ : { limitingBudgetKey: result.limitingBudgetKey }),
496
+ ...(result.remaining === undefined ? {} : { remaining: result.remaining }),
497
+ ...(metadata === undefined ? {} : { metadata }),
498
+ });
499
+ return {
500
+ allowed: false,
501
+ reason: result.reason,
502
+ ...(result.limitingBudgetKey === undefined
503
+ ? {}
504
+ : { limitingBudgetKey: result.limitingBudgetKey }),
505
+ ...(result.remaining === undefined ? {} : { remaining: result.remaining }),
506
+ };
507
+ }
508
+ emitUsageEvent(this.observer, {
509
+ type: 'reserve.accepted',
510
+ timestamp: Date.now(),
511
+ ...requestIdentity(requestForPolicy),
512
+ reservationId: result.reservation.id,
513
+ budgetKeys: [...result.reservation.budgetKeys],
514
+ reservedUnits: result.reservation.reservedUnits,
515
+ remainingByBudget: result.remainingByBudget.map(balance => ({ ...balance })),
516
+ ...(metadata === undefined ? {} : { metadata }),
517
+ });
518
+ return {
519
+ allowed: true,
520
+ lease: new UsageLease(this.store, result.reservation, ttlMs, this.observer, metadata),
521
+ remainingByBudget: result.remainingByBudget.map(balance => ({ ...balance })),
522
+ };
523
+ }
524
+ }
525
+ export class VectorUsageControl {
526
+ store;
527
+ policy;
528
+ defaultReservationTtlMs;
529
+ observer;
530
+ metadata;
531
+ constructor(store, policy, optionsOrTtl = 60_000) {
532
+ this.store = store;
533
+ this.policy = policy;
534
+ if (typeof optionsOrTtl === 'number') {
535
+ this.defaultReservationTtlMs = optionsOrTtl;
536
+ }
537
+ else {
538
+ this.defaultReservationTtlMs = optionsOrTtl.defaultReservationTtlMs ?? 60_000;
539
+ this.observer = optionsOrTtl.observer;
540
+ this.metadata = optionsOrTtl.metadata;
541
+ }
542
+ assertPositiveInteger(this.defaultReservationTtlMs, 'defaultReservationTtlMs');
543
+ }
544
+ resumeLease(state) {
545
+ if (!isVectorUsageStore(this.store)) {
546
+ throw new UsageStateError('UsageStore does not support atomic vector usage');
547
+ }
548
+ assertPositiveInteger(state.ttlMs, 'ttlMs');
549
+ return new VectorUsageLease(this.store, cloneVectorReservationRecord(state.reservation), state.ttlMs, this.observer, state.metadata === undefined ? undefined : { ...state.metadata }, state.unresolvedGrowth);
550
+ }
551
+ async reserve(request) {
552
+ validateRequestIdentity(request);
553
+ if (!isVectorUsageStore(this.store)) {
554
+ throw new UsageStateError('UsageStore does not support atomic vector usage');
555
+ }
556
+ const requestForPolicy = request;
557
+ const metadata = resolveMetadata(this.metadata, requestForPolicy);
558
+ let quote;
559
+ try {
560
+ quote = normalizeVectorUsagePolicyQuote(await this.policy.quote(requestForPolicy));
561
+ }
562
+ catch (error) {
563
+ emitUsageEvent(this.observer, {
564
+ type: 'operation.error',
565
+ timestamp: Date.now(),
566
+ phase: 'quote',
567
+ source: 'policy',
568
+ ...requestIdentity(requestForPolicy),
569
+ errorName: usageErrorName(error),
570
+ ...(metadata === undefined ? {} : { metadata }),
571
+ });
572
+ throw error;
573
+ }
574
+ if (quote.decision === 'deny') {
575
+ emitUsageEvent(this.observer, {
576
+ type: 'vector.reserve.denied',
577
+ timestamp: Date.now(),
578
+ ...requestIdentity(requestForPolicy),
579
+ reason: quote.reason,
580
+ ...(metadata === undefined ? {} : { metadata }),
581
+ });
582
+ return { allowed: false, reason: quote.reason };
583
+ }
584
+ const dimensions = quote.dimensions;
585
+ const ttlMs = quote.reservationTtlMs ?? this.defaultReservationTtlMs;
586
+ assertPositiveInteger(ttlMs, 'reservationTtlMs');
587
+ let result;
588
+ try {
589
+ result = validateStoreVectorReserveResult(await this.store.reserveVector({ request: requestForPolicy, dimensions, ttlMs }), requestForPolicy, dimensions);
590
+ }
591
+ catch (error) {
592
+ emitUsageEvent(this.observer, {
593
+ type: 'operation.error',
594
+ timestamp: Date.now(),
595
+ phase: 'reserve',
596
+ source: 'store',
597
+ ...requestIdentity(requestForPolicy),
598
+ errorName: usageErrorName(error),
599
+ ...(metadata === undefined ? {} : { metadata }),
600
+ });
601
+ throw error;
602
+ }
603
+ if (!result.accepted) {
604
+ emitUsageEvent(this.observer, {
605
+ type: 'vector.reserve.denied',
606
+ timestamp: Date.now(),
607
+ ...requestIdentity(requestForPolicy),
608
+ reason: result.reason,
609
+ ...(result.limitingDimensionKey === undefined
610
+ ? {}
611
+ : { limitingDimensionKey: result.limitingDimensionKey }),
612
+ ...(result.limitingBudgetKey === undefined
613
+ ? {}
614
+ : { limitingBudgetKey: result.limitingBudgetKey }),
615
+ ...(result.remaining === undefined ? {} : { remaining: result.remaining }),
616
+ ...(metadata === undefined ? {} : { metadata }),
617
+ });
618
+ return {
619
+ allowed: false,
620
+ reason: result.reason,
621
+ ...(result.limitingDimensionKey === undefined
622
+ ? {}
623
+ : { limitingDimensionKey: result.limitingDimensionKey }),
624
+ ...(result.limitingBudgetKey === undefined
625
+ ? {}
626
+ : { limitingBudgetKey: result.limitingBudgetKey }),
627
+ ...(result.remaining === undefined ? {} : { remaining: result.remaining }),
628
+ };
629
+ }
630
+ emitUsageEvent(this.observer, {
631
+ type: 'vector.reserve.accepted',
632
+ timestamp: Date.now(),
633
+ ...requestIdentity(requestForPolicy),
634
+ reservationId: result.reservation.id,
635
+ dimensions: result.reservation.dimensions.map(dimension => ({
636
+ key: dimension.key,
637
+ reservedUnits: dimension.reservedUnits,
638
+ budgetKeys: [...dimension.budgetKeys],
639
+ })),
640
+ remainingByBudget: result.remainingByBudget.map(balance => ({ ...balance })),
641
+ ...(metadata === undefined ? {} : { metadata }),
642
+ });
643
+ return {
644
+ allowed: true,
645
+ lease: new VectorUsageLease(this.store, result.reservation, ttlMs, this.observer, metadata),
646
+ remainingByBudget: result.remainingByBudget.map(balance => ({ ...balance })),
647
+ };
648
+ }
649
+ }
650
+ const DEFAULT_MEMORY_MAX_RETAINED_OPERATIONS = 100_000;
651
+ const DEFAULT_MEMORY_MAX_RETAINED_BUDGET_KEYS = 100_000;
652
+ export class MemoryUsageStoreCapacityError extends UsageStateError {
653
+ resource;
654
+ limit;
655
+ constructor(resource, limit) {
656
+ super(resource === 'operations'
657
+ ? `MemoryUsageStore operation retention limit reached (${limit}); refusing to evict replay/accounting state`
658
+ : `MemoryUsageStore budget-key retention limit reached (${limit}); refusing to evict authoritative usage state`);
659
+ this.resource = resource;
660
+ this.limit = limit;
661
+ this.name = 'MemoryUsageStoreCapacityError';
662
+ }
663
+ }
664
+ export class MemoryUsageStore {
665
+ used = new Map();
666
+ reservations = new Map();
667
+ operations = new Map();
668
+ idempotencyTtlMs;
669
+ maxRetainedOperations;
670
+ maxRetainedBudgetKeys;
671
+ observer;
672
+ nextRecoveryAt = Number.POSITIVE_INFINITY;
673
+ constructor(options = {}) {
674
+ this.idempotencyTtlMs = options.idempotencyTtlMs ?? 86_400_000;
675
+ this.maxRetainedOperations =
676
+ options.maxRetainedOperations ?? DEFAULT_MEMORY_MAX_RETAINED_OPERATIONS;
677
+ this.maxRetainedBudgetKeys =
678
+ options.maxRetainedBudgetKeys ?? DEFAULT_MEMORY_MAX_RETAINED_BUDGET_KEYS;
679
+ this.observer = options.observer;
680
+ assertPositiveInteger(this.idempotencyTtlMs, 'idempotencyTtlMs');
681
+ assertPositiveInteger(this.maxRetainedOperations, 'maxRetainedOperations');
682
+ assertPositiveInteger(this.maxRetainedBudgetKeys, 'maxRetainedBudgetKeys');
683
+ }
684
+ /** Current bounded-retention counters for health checks and operational monitoring. */
685
+ stats() {
686
+ this.recoverExpired(Date.now());
687
+ return {
688
+ retainedOperations: this.reservations.size,
689
+ retainedBudgetKeys: this.used.size,
690
+ maxRetainedOperations: this.maxRetainedOperations,
691
+ maxRetainedBudgetKeys: this.maxRetainedBudgetKeys,
692
+ };
693
+ }
694
+ /**
695
+ * Explicitly retire one budget key after its accounting window is permanently over.
696
+ *
697
+ * This is never automatic because forgetting non-zero usage can reset quota semantics.
698
+ * Active reservations block retirement. Callers must ensure the same key will not later
699
+ * be reused for the same accounting window.
700
+ */
701
+ retireBudgetKey(budgetKey) {
702
+ if (typeof budgetKey !== 'string' || budgetKey.length === 0) {
703
+ throw new RangeError('budgetKey must be a non-empty string');
704
+ }
705
+ this.recoverExpired(Date.now());
706
+ for (const reservation of this.reservations.values()) {
707
+ if (reservation.state !== 'settled' &&
708
+ internalReservationBudgetKeys(reservation).includes(budgetKey)) {
709
+ throw new UsageStateError('Cannot retire a budget key referenced by an active reservation');
710
+ }
711
+ }
712
+ return this.used.delete(budgetKey);
713
+ }
714
+ async reconcileOperation(input) {
715
+ assertNonNegativeInteger(input.units, 'units');
716
+ const budgets = canonicalizeBudgets(input.budgets);
717
+ validateRequestIdentity(input.request);
718
+ const now = Date.now();
719
+ const reservationId = operationKeyFor(input.request);
720
+ const reservation = this.reservations.get(reservationId);
721
+ if (!reservation)
722
+ return { status: 'absent', reservationId };
723
+ if (reservation.mode !== 'scalar') {
724
+ throw new UsageStateError('Scalar operation reconciliation cannot target a vector reservation');
725
+ }
726
+ if (reservation.reservedUnits !== input.units ||
727
+ !sameBudgetKeys(reservation.budgetKeys, budgets)) {
728
+ throw new UsageStateError('Operation reconciliation input does not match retained reservation state');
729
+ }
730
+ if (reservation.state === 'pending' || reservation.state === 'liable') {
731
+ if (reservation.expiresAt <= now) {
732
+ return {
733
+ status: 'expired',
734
+ state: reservation.state,
735
+ reservationId: reservation.id,
736
+ expiredAt: reservation.expiresAt,
737
+ };
738
+ }
739
+ return {
740
+ status: 'active',
741
+ state: reservation.state,
742
+ reservation: cloneReservationRecord(reservation),
743
+ };
744
+ }
745
+ if (reservation.tombstoneExpiresAt !== undefined &&
746
+ reservation.tombstoneExpiresAt <= now) {
747
+ return { status: 'absent', reservationId };
748
+ }
749
+ if (reservation.outcome === 'lease_expired_after_execution_started') {
750
+ return {
751
+ status: 'expired',
752
+ state: 'liable',
753
+ reservationId: reservation.id,
754
+ expiredAt: reservation.expiresAt,
755
+ };
756
+ }
757
+ if (reservation.actualUnits === undefined || reservation.tombstoneExpiresAt === undefined) {
758
+ throw new UsageStateError('Settled reservation is missing reconciliation state');
759
+ }
760
+ return {
761
+ status: 'settled',
762
+ reservationId: reservation.id,
763
+ reservedUnits: reservation.reservedUnits,
764
+ actualUnits: reservation.actualUnits,
765
+ tombstoneExpiresAt: reservation.tombstoneExpiresAt,
766
+ };
767
+ }
768
+ async reconcileVectorOperation(input) {
769
+ validateRequestIdentity(input.request);
770
+ const dimensions = canonicalizeUsageDimensions(input.dimensions);
771
+ const now = Date.now();
772
+ const reservationId = operationKeyFor(input.request);
773
+ const reservation = this.reservations.get(reservationId);
774
+ if (!reservation)
775
+ return { status: 'absent', reservationId };
776
+ if (reservation.mode !== 'vector') {
777
+ throw new UsageStateError('Vector operation reconciliation cannot target a scalar reservation');
778
+ }
779
+ if (!sameVectorReconciliationTopology(reservation.dimensions, dimensions)) {
780
+ throw new UsageStateError('Vector reconciliation input does not match retained reservation state');
781
+ }
782
+ if (reservation.state === 'pending' || reservation.state === 'liable') {
783
+ if (reservation.expiresAt <= now) {
784
+ return {
785
+ status: 'expired',
786
+ state: reservation.state,
787
+ reservationId: reservation.id,
788
+ expiredAt: reservation.expiresAt,
789
+ };
790
+ }
791
+ return {
792
+ status: 'active',
793
+ state: reservation.state,
794
+ reservation: cloneVectorReservationRecord(reservation),
795
+ };
796
+ }
797
+ if (reservation.tombstoneExpiresAt !== undefined && reservation.tombstoneExpiresAt <= now) {
798
+ return { status: 'absent', reservationId };
799
+ }
800
+ if (reservation.outcome === 'lease_expired_after_execution_started') {
801
+ return {
802
+ status: 'expired',
803
+ state: 'liable',
804
+ reservationId: reservation.id,
805
+ expiredAt: reservation.expiresAt,
806
+ };
807
+ }
808
+ if (reservation.actualByDimension === undefined || reservation.tombstoneExpiresAt === undefined) {
809
+ throw new UsageStateError('Settled vector reservation is missing reconciliation state');
810
+ }
811
+ return {
812
+ status: 'settled',
813
+ reservationId: reservation.id,
814
+ reservedByDimension: reservation.dimensions.map(dimension => ({
815
+ key: dimension.key,
816
+ reservedUnits: dimension.reservedUnits,
817
+ })),
818
+ actualByDimension: reservation.actualByDimension.map(item => ({ ...item })),
819
+ tombstoneExpiresAt: reservation.tombstoneExpiresAt,
820
+ };
821
+ }
822
+ async reserve(input) {
823
+ assertNonNegativeInteger(input.units, 'units');
824
+ const budgets = canonicalizeBudgets(input.budgets);
825
+ assertPositiveInteger(input.ttlMs, 'ttlMs');
826
+ validateRequestIdentity(input.request);
827
+ const now = Date.now();
828
+ const expiresAt = safeAdd(now, input.ttlMs, 'reservation expiry');
829
+ this.recoverExpired(now);
830
+ const operationKey = operationKeyFor(input.request);
831
+ if (this.operations.has(operationKey)) {
832
+ return { accepted: false, reason: 'duplicate_operation' };
833
+ }
834
+ const remainingByBudget = budgets.map(budget => ({
835
+ key: budget.key,
836
+ remaining: Math.max(0, budget.limit - (this.used.get(budget.key) ?? 0)),
837
+ }));
838
+ const limiting = remainingByBudget.find(balance => input.units > balance.remaining);
839
+ if (limiting) {
840
+ return {
841
+ accepted: false,
842
+ reason: 'quota_exceeded',
843
+ limitingBudgetKey: limiting.key,
844
+ remaining: limiting.remaining,
845
+ };
846
+ }
847
+ this.assertOperationCapacity();
848
+ if (input.units > 0)
849
+ this.assertBudgetCapacity(budgets);
850
+ const reservation = {
851
+ mode: 'scalar',
852
+ id: operationKey,
853
+ operationId: input.request.operationId,
854
+ principalId: input.request.principal.id,
855
+ ...(input.request.principal.tenantId === undefined
856
+ ? {}
857
+ : { tenantId: input.request.principal.tenantId }),
858
+ ...(input.request.principal.plan === undefined ? {} : { plan: input.request.principal.plan }),
859
+ tool: input.request.tool,
860
+ budgetKeys: budgets.map(budget => budget.key),
861
+ reservedUnits: input.units,
862
+ expiresAt,
863
+ growthCursor: newGrowthCursor(),
864
+ operationKey,
865
+ state: 'pending',
866
+ };
867
+ if (input.units > 0) {
868
+ for (const budget of budgets) {
869
+ this.used.set(budget.key, (this.used.get(budget.key) ?? 0) + input.units);
870
+ }
871
+ }
872
+ this.reservations.set(reservation.id, reservation);
873
+ this.operations.set(operationKey, reservation.id);
874
+ this.trackRecoveryAt(reservation.expiresAt);
875
+ return {
876
+ accepted: true,
877
+ reservation: cloneReservationRecord(reservation),
878
+ remainingByBudget: remainingByBudget.map(balance => ({
879
+ key: balance.key,
880
+ remaining: balance.remaining - input.units,
881
+ })),
882
+ };
883
+ }
884
+ async reserveVector(input) {
885
+ assertPositiveInteger(input.ttlMs, 'ttlMs');
886
+ validateRequestIdentity(input.request);
887
+ const dimensions = canonicalizeUsageDimensions(input.dimensions);
888
+ const now = Date.now();
889
+ const expiresAt = safeAdd(now, input.ttlMs, 'reservation expiry');
890
+ this.recoverExpired(now);
891
+ const operationKey = operationKeyFor(input.request);
892
+ if (this.operations.has(operationKey)) {
893
+ return { accepted: false, reason: 'duplicate_operation' };
894
+ }
895
+ const remainingByBudget = [];
896
+ let limiting;
897
+ for (const dimension of dimensions) {
898
+ for (const budget of dimension.budgets) {
899
+ const remaining = Math.max(0, budget.limit - (this.used.get(budget.key) ?? 0));
900
+ remainingByBudget.push({
901
+ dimensionKey: dimension.key,
902
+ budgetKey: budget.key,
903
+ remaining,
904
+ });
905
+ if (!limiting && dimension.units > remaining) {
906
+ limiting = { dimensionKey: dimension.key, budgetKey: budget.key, remaining };
907
+ }
908
+ }
909
+ }
910
+ if (limiting) {
911
+ return {
912
+ accepted: false,
913
+ reason: 'quota_exceeded',
914
+ limitingDimensionKey: limiting.dimensionKey,
915
+ limitingBudgetKey: limiting.budgetKey,
916
+ remaining: limiting.remaining,
917
+ };
918
+ }
919
+ this.assertOperationCapacity();
920
+ const nonZeroBudgets = dimensions.flatMap(dimension => dimension.units > 0 ? dimension.budgets : []);
921
+ if (nonZeroBudgets.length > 0)
922
+ this.assertBudgetCapacity(nonZeroBudgets);
923
+ const reservation = {
924
+ mode: 'vector',
925
+ id: operationKey,
926
+ operationId: input.request.operationId,
927
+ principalId: input.request.principal.id,
928
+ ...(input.request.principal.tenantId === undefined
929
+ ? {}
930
+ : { tenantId: input.request.principal.tenantId }),
931
+ ...(input.request.principal.plan === undefined ? {} : { plan: input.request.principal.plan }),
932
+ tool: input.request.tool,
933
+ dimensions: dimensions.map(dimension => ({
934
+ key: dimension.key,
935
+ budgetKeys: dimension.budgets.map(budget => budget.key),
936
+ reservedUnits: dimension.units,
937
+ })),
938
+ expiresAt,
939
+ growthCursor: newGrowthCursor(),
940
+ operationKey,
941
+ state: 'pending',
942
+ };
943
+ for (const dimension of dimensions) {
944
+ if (dimension.units === 0)
945
+ continue;
946
+ for (const budget of dimension.budgets) {
947
+ this.used.set(budget.key, safeAdd(this.used.get(budget.key) ?? 0, dimension.units, `usage (${budget.key})`));
948
+ }
949
+ }
950
+ this.reservations.set(reservation.id, reservation);
951
+ this.operations.set(operationKey, reservation.id);
952
+ this.trackRecoveryAt(reservation.expiresAt);
953
+ const unitsByDimension = new Map(dimensions.map(dimension => [dimension.key, dimension.units]));
954
+ return {
955
+ accepted: true,
956
+ reservation: cloneVectorReservationRecord(reservation),
957
+ remainingByBudget: remainingByBudget.map(balance => ({
958
+ ...balance,
959
+ remaining: balance.remaining - (unitsByDimension.get(balance.dimensionKey) ?? 0),
960
+ })),
961
+ };
962
+ }
963
+ async growVectorReservation(input) {
964
+ validateVectorGrowthInput(input);
965
+ const dimensions = canonicalizeVectorGrowthDimensions(input.dimensions);
966
+ const fingerprint = vectorGrowthFingerprint(dimensions);
967
+ const now = Date.now();
968
+ this.recoverExpired(now);
969
+ const reservation = this.reservations.get(input.reservationId);
970
+ if (!reservation)
971
+ throw new UsageStateError('Reservation not found or expired');
972
+ if (reservation.mode !== 'vector') {
973
+ throw new UsageStateError('Vector growth cannot target a scalar reservation');
974
+ }
975
+ if (reservation.state === 'settled') {
976
+ throw new UsageStateError('Cannot grow a settled or expired vector reservation');
977
+ }
978
+ const lastGrowth = reservation.lastVectorGrowth;
979
+ if (lastGrowth?.incrementId === input.incrementId) {
980
+ if (lastGrowth.expectedGrowthCursor !== input.expectedGrowthCursor ||
981
+ lastGrowth.fingerprint !== fingerprint) {
982
+ throw new UsageStateError('Vector growth increment was already attempted with different parameters');
983
+ }
984
+ return replayVectorGrowthResult(lastGrowth.result);
985
+ }
986
+ if (!reservation.growthCursor) {
987
+ throw new UsageStateError('Vector reservation does not support progressive growth');
988
+ }
989
+ if (reservation.growthCursor !== input.expectedGrowthCursor) {
990
+ throw new UsageStateError('Vector growth cursor is stale or conflicts with reservation state');
991
+ }
992
+ if (!sameVectorTopology(reservation.dimensions, dimensions)) {
993
+ throw new UsageStateError('Vector growth dimensions and budgets must match the reservation');
994
+ }
995
+ const remainingByBudget = [];
996
+ let limiting;
997
+ for (const dimension of dimensions) {
998
+ for (const budget of dimension.budgets) {
999
+ const remaining = Math.max(0, budget.limit - (this.used.get(budget.key) ?? 0));
1000
+ remainingByBudget.push({
1001
+ dimensionKey: dimension.key,
1002
+ budgetKey: budget.key,
1003
+ remaining,
1004
+ });
1005
+ if (!limiting && dimension.additionalUnits > remaining) {
1006
+ limiting = { dimensionKey: dimension.key, budgetKey: budget.key, remaining };
1007
+ }
1008
+ }
1009
+ }
1010
+ const nextGrowthCursor = newGrowthCursor();
1011
+ if (limiting) {
1012
+ const result = {
1013
+ accepted: false,
1014
+ reason: 'quota_exceeded',
1015
+ replayed: false,
1016
+ reservationId: reservation.id,
1017
+ incrementId: input.incrementId,
1018
+ growthCursor: nextGrowthCursor,
1019
+ limitingDimensionKey: limiting.dimensionKey,
1020
+ limitingBudgetKey: limiting.budgetKey,
1021
+ remaining: limiting.remaining,
1022
+ };
1023
+ reservation.growthCursor = nextGrowthCursor;
1024
+ reservation.lastVectorGrowth = {
1025
+ incrementId: input.incrementId,
1026
+ expectedGrowthCursor: input.expectedGrowthCursor,
1027
+ fingerprint,
1028
+ result: cloneVectorGrowthResult(result),
1029
+ };
1030
+ return result;
1031
+ }
1032
+ const newlyNonZeroBudgets = dimensions.flatMap(dimension => dimension.additionalUnits > 0 ? dimension.budgets : []);
1033
+ if (newlyNonZeroBudgets.length > 0)
1034
+ this.assertBudgetCapacity(newlyNonZeroBudgets);
1035
+ const previousReservedByDimension = reservation.dimensions.map(dimension => ({
1036
+ key: dimension.key,
1037
+ reservedUnits: dimension.reservedUnits,
1038
+ }));
1039
+ const growthByKey = new Map(dimensions.map(dimension => [dimension.key, dimension]));
1040
+ for (const reservedDimension of reservation.dimensions) {
1041
+ const growth = growthByKey.get(reservedDimension.key);
1042
+ if (growth.additionalUnits > 0) {
1043
+ for (const budget of growth.budgets) {
1044
+ this.used.set(budget.key, safeAdd(this.used.get(budget.key) ?? 0, growth.additionalUnits, `usage (${budget.key})`));
1045
+ }
1046
+ }
1047
+ reservedDimension.reservedUnits = safeAdd(reservedDimension.reservedUnits, growth.additionalUnits, `reservedUnits (${reservedDimension.key})`);
1048
+ }
1049
+ reservation.growthCursor = nextGrowthCursor;
1050
+ const reservedByDimension = reservation.dimensions.map(dimension => ({
1051
+ key: dimension.key,
1052
+ reservedUnits: dimension.reservedUnits,
1053
+ }));
1054
+ const addedByDimension = new Map(dimensions.map(dimension => [dimension.key, dimension.additionalUnits]));
1055
+ const result = {
1056
+ accepted: true,
1057
+ replayed: false,
1058
+ reservationId: reservation.id,
1059
+ incrementId: input.incrementId,
1060
+ previousReservedByDimension,
1061
+ reservedByDimension,
1062
+ growthCursor: nextGrowthCursor,
1063
+ remainingByBudget: remainingByBudget.map(balance => ({
1064
+ ...balance,
1065
+ remaining: balance.remaining - (addedByDimension.get(balance.dimensionKey) ?? 0),
1066
+ })),
1067
+ };
1068
+ reservation.lastVectorGrowth = {
1069
+ incrementId: input.incrementId,
1070
+ expectedGrowthCursor: input.expectedGrowthCursor,
1071
+ fingerprint,
1072
+ result: cloneVectorGrowthResult(result),
1073
+ };
1074
+ return result;
1075
+ }
1076
+ async settleVector(input) {
1077
+ const now = Date.now();
1078
+ const tombstoneExpiresAt = safeAdd(now, this.idempotencyTtlMs, 'tombstone expiry');
1079
+ this.recoverExpired(now);
1080
+ const reservation = this.reservations.get(input.reservationId);
1081
+ if (!reservation)
1082
+ throw new UsageStateError('Reservation not found or expired');
1083
+ if (reservation.mode !== 'vector') {
1084
+ throw new UsageStateError('Vector settlement cannot target a scalar reservation');
1085
+ }
1086
+ const actualByDimension = canonicalizeVectorActuals(input.actualByDimension, reservation.dimensions);
1087
+ if (reservation.state === 'settled') {
1088
+ if (reservation.outcome !== input.outcome ||
1089
+ !sameVectorActuals(reservation.actualByDimension, actualByDimension)) {
1090
+ throw new UsageStateError('Vector reservation was already settled with a different result');
1091
+ }
1092
+ return toVectorSettlement(reservation);
1093
+ }
1094
+ const actualByKey = new Map(actualByDimension.map(item => [item.key, item.actualUnits]));
1095
+ for (const dimension of reservation.dimensions) {
1096
+ const actualUnits = actualByKey.get(dimension.key);
1097
+ const releasedUnits = dimension.reservedUnits - actualUnits;
1098
+ if (releasedUnits > 0)
1099
+ this.releaseAcrossBudgets(dimension.budgetKeys, releasedUnits);
1100
+ }
1101
+ reservation.state = 'settled';
1102
+ reservation.actualByDimension = actualByDimension.map(item => ({ ...item }));
1103
+ reservation.outcome = input.outcome;
1104
+ reservation.tombstoneExpiresAt = tombstoneExpiresAt;
1105
+ this.trackRecoveryAt(tombstoneExpiresAt);
1106
+ return toVectorSettlement(reservation);
1107
+ }
1108
+ async markLiable(input) {
1109
+ const now = Date.now();
1110
+ this.recoverExpired(now);
1111
+ const reservation = this.reservations.get(input.reservationId);
1112
+ if (!reservation || reservation.state === 'settled') {
1113
+ throw new UsageStateError('Active reservation not found or expired');
1114
+ }
1115
+ reservation.state = 'liable';
1116
+ return { reservationId: reservation.id, expiresAt: reservation.expiresAt };
1117
+ }
1118
+ async growReservation(input) {
1119
+ validateGrowthInput(input);
1120
+ const budgets = canonicalizeBudgets(input.budgets);
1121
+ const fingerprint = growthFingerprint(input.additionalUnits, budgets);
1122
+ const now = Date.now();
1123
+ this.recoverExpired(now);
1124
+ const reservation = this.reservations.get(input.reservationId);
1125
+ if (!reservation)
1126
+ throw new UsageStateError('Reservation not found or expired');
1127
+ if (reservation.mode !== 'scalar') {
1128
+ throw new UsageStateError('Scalar growth cannot target a vector reservation');
1129
+ }
1130
+ if (reservation.state === 'settled') {
1131
+ throw new UsageStateError('Cannot grow a settled or expired reservation');
1132
+ }
1133
+ const lastGrowth = reservation.lastGrowth;
1134
+ if (lastGrowth?.incrementId === input.incrementId) {
1135
+ if (lastGrowth.expectedGrowthCursor !== input.expectedGrowthCursor ||
1136
+ lastGrowth.fingerprint !== fingerprint) {
1137
+ throw new UsageStateError('Growth increment was already attempted with different parameters');
1138
+ }
1139
+ return replayGrowthResult(lastGrowth.result);
1140
+ }
1141
+ if (!reservation.growthCursor) {
1142
+ throw new UsageStateError('Reservation does not support progressive growth');
1143
+ }
1144
+ if (reservation.growthCursor !== input.expectedGrowthCursor) {
1145
+ throw new UsageStateError('Growth cursor is stale or conflicts with reservation state');
1146
+ }
1147
+ if (!sameBudgetKeys(reservation.budgetKeys, budgets)) {
1148
+ throw new UsageStateError('Growth budgets must exactly match the reservation budget set');
1149
+ }
1150
+ const remainingByBudget = budgets.map(budget => ({
1151
+ key: budget.key,
1152
+ remaining: Math.max(0, budget.limit - (this.used.get(budget.key) ?? 0)),
1153
+ }));
1154
+ const limiting = remainingByBudget.find(balance => input.additionalUnits > balance.remaining);
1155
+ const nextGrowthCursor = newGrowthCursor();
1156
+ if (limiting) {
1157
+ const result = {
1158
+ accepted: false,
1159
+ reason: 'quota_exceeded',
1160
+ replayed: false,
1161
+ reservationId: reservation.id,
1162
+ incrementId: input.incrementId,
1163
+ growthCursor: nextGrowthCursor,
1164
+ limitingBudgetKey: limiting.key,
1165
+ remaining: limiting.remaining,
1166
+ };
1167
+ reservation.growthCursor = nextGrowthCursor;
1168
+ reservation.lastGrowth = {
1169
+ incrementId: input.incrementId,
1170
+ expectedGrowthCursor: input.expectedGrowthCursor,
1171
+ fingerprint,
1172
+ result: cloneGrowthResult(result),
1173
+ };
1174
+ return result;
1175
+ }
1176
+ this.assertBudgetCapacity(budgets);
1177
+ const previousReservedUnits = reservation.reservedUnits;
1178
+ const reservedUnits = safeAdd(previousReservedUnits, input.additionalUnits, 'reservedUnits');
1179
+ for (const budget of budgets) {
1180
+ this.used.set(budget.key, safeAdd(this.used.get(budget.key) ?? 0, input.additionalUnits, `usage (${budget.key})`));
1181
+ }
1182
+ reservation.reservedUnits = reservedUnits;
1183
+ reservation.growthCursor = nextGrowthCursor;
1184
+ const result = {
1185
+ accepted: true,
1186
+ replayed: false,
1187
+ reservationId: reservation.id,
1188
+ incrementId: input.incrementId,
1189
+ previousReservedUnits,
1190
+ reservedUnits,
1191
+ growthCursor: nextGrowthCursor,
1192
+ remainingByBudget: remainingByBudget.map(balance => ({
1193
+ key: balance.key,
1194
+ remaining: balance.remaining - input.additionalUnits,
1195
+ })),
1196
+ };
1197
+ reservation.lastGrowth = {
1198
+ incrementId: input.incrementId,
1199
+ expectedGrowthCursor: input.expectedGrowthCursor,
1200
+ fingerprint,
1201
+ result: cloneGrowthResult(result),
1202
+ };
1203
+ return result;
1204
+ }
1205
+ async renew(input) {
1206
+ assertPositiveInteger(input.ttlMs, 'ttlMs');
1207
+ const now = Date.now();
1208
+ const expiresAt = safeAdd(now, input.ttlMs, 'reservation expiry');
1209
+ this.recoverExpired(now);
1210
+ const reservation = this.reservations.get(input.reservationId);
1211
+ if (!reservation || reservation.state === 'settled') {
1212
+ throw new UsageStateError('Active reservation not found or expired');
1213
+ }
1214
+ reservation.expiresAt = expiresAt;
1215
+ this.trackRecoveryAt(expiresAt);
1216
+ return { reservationId: reservation.id, expiresAt };
1217
+ }
1218
+ async settle(input) {
1219
+ assertNonNegativeInteger(input.actualUnits, 'actualUnits');
1220
+ const now = Date.now();
1221
+ const tombstoneExpiresAt = safeAdd(now, this.idempotencyTtlMs, 'tombstone expiry');
1222
+ this.recoverExpired(now);
1223
+ const reservation = this.reservations.get(input.reservationId);
1224
+ if (!reservation)
1225
+ throw new UsageStateError('Reservation not found or expired');
1226
+ if (reservation.mode !== 'scalar') {
1227
+ throw new UsageStateError('Scalar settlement cannot target a vector reservation');
1228
+ }
1229
+ if (input.actualUnits > reservation.reservedUnits) {
1230
+ throw new UsageStateError('actualUnits cannot exceed reservedUnits');
1231
+ }
1232
+ if (reservation.state === 'settled') {
1233
+ if (reservation.actualUnits !== input.actualUnits || reservation.outcome !== input.outcome) {
1234
+ throw new UsageStateError('Reservation was already settled with a different result');
1235
+ }
1236
+ return toSettlement(reservation);
1237
+ }
1238
+ const released = reservation.reservedUnits - input.actualUnits;
1239
+ if (released > 0)
1240
+ this.releaseAcrossBudgets(reservation.budgetKeys, released);
1241
+ reservation.state = 'settled';
1242
+ reservation.actualUnits = input.actualUnits;
1243
+ reservation.outcome = input.outcome;
1244
+ reservation.tombstoneExpiresAt = tombstoneExpiresAt;
1245
+ this.trackRecoveryAt(tombstoneExpiresAt);
1246
+ return toSettlement(reservation);
1247
+ }
1248
+ recoverExpired(now) {
1249
+ if (now < this.nextRecoveryAt)
1250
+ return;
1251
+ // Preflight every liable transition before mutating any authoritative state.
1252
+ // This keeps recovery atomic with respect to unsafe tombstone arithmetic.
1253
+ for (const reservation of this.reservations.values()) {
1254
+ if (reservation.state === 'liable' &&
1255
+ reservation.expiresAt <= now) {
1256
+ safeAdd(now, this.idempotencyTtlMs, 'tombstone expiry');
1257
+ }
1258
+ }
1259
+ let nextRecoveryAt = Number.POSITIVE_INFINITY;
1260
+ for (const [id, reservation] of this.reservations) {
1261
+ if (reservation.state === 'settled') {
1262
+ const tombstoneExpiresAt = reservation.tombstoneExpiresAt ?? Number.POSITIVE_INFINITY;
1263
+ if (tombstoneExpiresAt <= now) {
1264
+ this.operations.delete(reservation.operationKey);
1265
+ this.reservations.delete(id);
1266
+ }
1267
+ else {
1268
+ nextRecoveryAt = Math.min(nextRecoveryAt, tombstoneExpiresAt);
1269
+ }
1270
+ continue;
1271
+ }
1272
+ if (reservation.expiresAt > now) {
1273
+ nextRecoveryAt = Math.min(nextRecoveryAt, reservation.expiresAt);
1274
+ continue;
1275
+ }
1276
+ if (reservation.state === 'pending') {
1277
+ if (reservation.mode === 'scalar') {
1278
+ this.releaseAcrossBudgets(reservation.budgetKeys, reservation.reservedUnits);
1279
+ emitUsageEvent(this.observer, {
1280
+ type: 'reservation.recovered',
1281
+ timestamp: now,
1282
+ store: 'memory',
1283
+ recovery: 'pending_released',
1284
+ reservationId: reservation.id,
1285
+ principalId: reservation.principalId,
1286
+ ...(reservation.tenantId === undefined ? {} : { tenantId: reservation.tenantId }),
1287
+ tool: reservation.tool,
1288
+ budgetIdentifiers: [...reservation.budgetKeys],
1289
+ reservedUnits: reservation.reservedUnits,
1290
+ count: 1,
1291
+ });
1292
+ }
1293
+ else {
1294
+ for (const dimension of reservation.dimensions) {
1295
+ this.releaseAcrossBudgets(dimension.budgetKeys, dimension.reservedUnits);
1296
+ }
1297
+ emitUsageEvent(this.observer, {
1298
+ type: 'vector.reservation.recovered',
1299
+ timestamp: now,
1300
+ store: 'memory',
1301
+ recovery: 'pending_released',
1302
+ reservationId: reservation.id,
1303
+ principalId: reservation.principalId,
1304
+ ...(reservation.tenantId === undefined ? {} : { tenantId: reservation.tenantId }),
1305
+ tool: reservation.tool,
1306
+ dimensionCount: reservation.dimensions.length,
1307
+ budgetCount: internalReservationBudgetKeys(reservation).length,
1308
+ count: 1,
1309
+ });
1310
+ }
1311
+ this.operations.delete(reservation.operationKey);
1312
+ this.reservations.delete(id);
1313
+ continue;
1314
+ }
1315
+ // Once execution has been marked liable, expiry is conservative: retain
1316
+ // the full reservation as consumed so a process crash cannot become a refund.
1317
+ reservation.state = 'settled';
1318
+ reservation.outcome = 'lease_expired_after_execution_started';
1319
+ reservation.tombstoneExpiresAt = safeAdd(now, this.idempotencyTtlMs, 'tombstone expiry');
1320
+ nextRecoveryAt = Math.min(nextRecoveryAt, reservation.tombstoneExpiresAt);
1321
+ if (reservation.mode === 'scalar') {
1322
+ reservation.actualUnits = reservation.reservedUnits;
1323
+ emitUsageEvent(this.observer, {
1324
+ type: 'reservation.recovered',
1325
+ timestamp: now,
1326
+ store: 'memory',
1327
+ recovery: 'liable_retained',
1328
+ reservationId: reservation.id,
1329
+ principalId: reservation.principalId,
1330
+ ...(reservation.tenantId === undefined ? {} : { tenantId: reservation.tenantId }),
1331
+ tool: reservation.tool,
1332
+ budgetIdentifiers: [...reservation.budgetKeys],
1333
+ reservedUnits: reservation.reservedUnits,
1334
+ count: 1,
1335
+ });
1336
+ }
1337
+ else {
1338
+ reservation.actualByDimension = reservation.dimensions.map(dimension => ({
1339
+ key: dimension.key,
1340
+ actualUnits: dimension.reservedUnits,
1341
+ }));
1342
+ emitUsageEvent(this.observer, {
1343
+ type: 'vector.reservation.recovered',
1344
+ timestamp: now,
1345
+ store: 'memory',
1346
+ recovery: 'liable_retained',
1347
+ reservationId: reservation.id,
1348
+ principalId: reservation.principalId,
1349
+ ...(reservation.tenantId === undefined ? {} : { tenantId: reservation.tenantId }),
1350
+ tool: reservation.tool,
1351
+ dimensionCount: reservation.dimensions.length,
1352
+ budgetCount: internalReservationBudgetKeys(reservation).length,
1353
+ count: 1,
1354
+ });
1355
+ }
1356
+ }
1357
+ this.nextRecoveryAt = nextRecoveryAt;
1358
+ }
1359
+ assertOperationCapacity() {
1360
+ if (this.reservations.size >= this.maxRetainedOperations) {
1361
+ throw new MemoryUsageStoreCapacityError('operations', this.maxRetainedOperations);
1362
+ }
1363
+ }
1364
+ assertBudgetCapacity(budgets) {
1365
+ const newBudgetKeys = new Set();
1366
+ for (const budget of budgets) {
1367
+ if (!this.used.has(budget.key))
1368
+ newBudgetKeys.add(budget.key);
1369
+ }
1370
+ if (this.used.size + newBudgetKeys.size > this.maxRetainedBudgetKeys) {
1371
+ throw new MemoryUsageStoreCapacityError('budget_keys', this.maxRetainedBudgetKeys);
1372
+ }
1373
+ }
1374
+ trackRecoveryAt(expiresAt) {
1375
+ this.nextRecoveryAt = Math.min(this.nextRecoveryAt, expiresAt);
1376
+ }
1377
+ releaseAcrossBudgets(budgetKeys, units) {
1378
+ for (const budgetKey of budgetKeys) {
1379
+ const next = Math.max(0, (this.used.get(budgetKey) ?? 0) - units);
1380
+ if (next === 0)
1381
+ this.used.delete(budgetKey);
1382
+ else
1383
+ this.used.set(budgetKey, next);
1384
+ }
1385
+ }
1386
+ }
1387
+ function internalReservationBudgetKeys(reservation) {
1388
+ return reservation.mode === 'scalar'
1389
+ ? [...reservation.budgetKeys]
1390
+ : reservation.dimensions.flatMap(dimension => [...dimension.budgetKeys]);
1391
+ }
1392
+ function validateVectorGrowthInput(input) {
1393
+ assertUsageIdentifier(input.reservationId, 'reservationId');
1394
+ assertUsageIdentifier(input.incrementId, 'incrementId');
1395
+ assertUsageIdentifier(input.expectedGrowthCursor, 'expectedGrowthCursor');
1396
+ }
1397
+ function sameVectorReconciliationTopology(reserved, expected) {
1398
+ return reserved.length === expected.length && reserved.every((dimension, index) => {
1399
+ const candidate = expected[index];
1400
+ return candidate !== undefined &&
1401
+ dimension.key === candidate.key &&
1402
+ dimension.reservedUnits === candidate.units &&
1403
+ sameBudgetKeys(dimension.budgetKeys, candidate.budgets);
1404
+ });
1405
+ }
1406
+ function sameVectorTopology(reserved, growth) {
1407
+ if (reserved.length !== growth.length)
1408
+ return false;
1409
+ return reserved.every((dimension, index) => {
1410
+ const candidate = growth[index];
1411
+ return (candidate !== undefined &&
1412
+ dimension.key === candidate.key &&
1413
+ sameBudgetKeys(dimension.budgetKeys, candidate.budgets));
1414
+ });
1415
+ }
1416
+ function cloneVectorGrowthResult(result) {
1417
+ if (!result.accepted)
1418
+ return { ...result };
1419
+ return {
1420
+ ...result,
1421
+ previousReservedByDimension: result.previousReservedByDimension.map(item => ({ ...item })),
1422
+ reservedByDimension: result.reservedByDimension.map(item => ({ ...item })),
1423
+ remainingByBudget: result.remainingByBudget.map(item => ({ ...item })),
1424
+ };
1425
+ }
1426
+ function replayVectorGrowthResult(result) {
1427
+ return { ...cloneVectorGrowthResult(result), replayed: true };
1428
+ }
1429
+ function sameVectorActuals(left, right) {
1430
+ return (left !== undefined &&
1431
+ left.length === right.length &&
1432
+ left.every((item, index) => item.key === right[index].key && item.actualUnits === right[index].actualUnits));
1433
+ }
1434
+ function toVectorSettlement(reservation) {
1435
+ const actual = reservation.actualByDimension ??
1436
+ reservation.dimensions.map(dimension => ({
1437
+ key: dimension.key,
1438
+ actualUnits: dimension.reservedUnits,
1439
+ }));
1440
+ const actualByKey = new Map(actual.map(item => [item.key, item.actualUnits]));
1441
+ return {
1442
+ reservationId: reservation.id,
1443
+ dimensions: reservation.dimensions.map(dimension => {
1444
+ const actualUnits = actualByKey.get(dimension.key) ?? dimension.reservedUnits;
1445
+ return {
1446
+ key: dimension.key,
1447
+ reservedUnits: dimension.reservedUnits,
1448
+ actualUnits,
1449
+ releasedUnits: dimension.reservedUnits - actualUnits,
1450
+ };
1451
+ }),
1452
+ outcome: reservation.outcome ?? 'unknown',
1453
+ };
1454
+ }
1455
+ function operationKeyFor(request) {
1456
+ return JSON.stringify([
1457
+ request.principal.tenantId ?? null,
1458
+ request.principal.id,
1459
+ request.tool,
1460
+ request.operationId,
1461
+ ]);
1462
+ }
1463
+ const MAX_POLICY_DENIAL_REASON_LENGTH = 128;
1464
+ function normalizeUsagePolicyQuote(value) {
1465
+ const quote = requireRecord(value, 'Usage policy quote');
1466
+ if (quote.decision === 'deny') {
1467
+ return { decision: 'deny', reason: validatePolicyDenialReason(quote.reason) };
1468
+ }
1469
+ if (quote.decision !== 'allow') {
1470
+ throw new TypeError('Usage policy quote decision must be allow or deny');
1471
+ }
1472
+ assertNonNegativeInteger(quote.units, 'units');
1473
+ const hasBudget = hasOwn(quote, 'budget');
1474
+ const hasBudgets = hasOwn(quote, 'budgets');
1475
+ if (hasBudget === hasBudgets) {
1476
+ throw new TypeError('Usage policy allow quote must contain exactly one of budget or budgets');
1477
+ }
1478
+ const budgets = hasBudgets
1479
+ ? canonicalizeBudgets(quote.budgets)
1480
+ : canonicalizeBudgets([quote.budget]);
1481
+ if (quote.reservationTtlMs !== undefined) {
1482
+ assertPositiveInteger(quote.reservationTtlMs, 'reservationTtlMs');
1483
+ }
1484
+ return {
1485
+ decision: 'allow',
1486
+ units: quote.units,
1487
+ budgets,
1488
+ ...(quote.reservationTtlMs === undefined
1489
+ ? {}
1490
+ : { reservationTtlMs: quote.reservationTtlMs }),
1491
+ };
1492
+ }
1493
+ function normalizeVectorUsagePolicyQuote(value) {
1494
+ const quote = requireRecord(value, 'Vector usage policy quote');
1495
+ if (quote.decision === 'deny') {
1496
+ return { decision: 'deny', reason: validatePolicyDenialReason(quote.reason) };
1497
+ }
1498
+ if (quote.decision !== 'allow') {
1499
+ throw new TypeError('Vector usage policy quote decision must be allow or deny');
1500
+ }
1501
+ if (!Array.isArray(quote.dimensions)) {
1502
+ throw new TypeError('Vector usage policy allow quote dimensions must be an array');
1503
+ }
1504
+ const dimensions = canonicalizeUsageDimensions(quote.dimensions);
1505
+ if (quote.reservationTtlMs !== undefined) {
1506
+ assertPositiveInteger(quote.reservationTtlMs, 'reservationTtlMs');
1507
+ }
1508
+ return {
1509
+ decision: 'allow',
1510
+ dimensions,
1511
+ ...(quote.reservationTtlMs === undefined
1512
+ ? {}
1513
+ : { reservationTtlMs: quote.reservationTtlMs }),
1514
+ };
1515
+ }
1516
+ function validatePolicyDenialReason(value) {
1517
+ if (typeof value !== 'string' ||
1518
+ value.length === 0 ||
1519
+ value.length > MAX_POLICY_DENIAL_REASON_LENGTH) {
1520
+ throw new TypeError(`Usage policy denial reason must be a non-empty string no longer than ${MAX_POLICY_DENIAL_REASON_LENGTH} characters`);
1521
+ }
1522
+ return value;
1523
+ }
1524
+ function validateStoreReserveResult(value, request, units, budgets) {
1525
+ const result = requireStoreResult(value, 'reserve');
1526
+ if (result.accepted === false) {
1527
+ validateReserveDenial(result, false);
1528
+ return result;
1529
+ }
1530
+ validateScalarAcceptedReservation(result, request, units, budgets);
1531
+ return result;
1532
+ }
1533
+ function validateStoreVectorReserveResult(value, request, dimensions) {
1534
+ const result = requireStoreResult(value, 'vector reserve');
1535
+ if (result.accepted === false) {
1536
+ validateReserveDenial(result, true);
1537
+ return result;
1538
+ }
1539
+ validateVectorAcceptedReservation(result, request, dimensions);
1540
+ return result;
1541
+ }
1542
+ function validateStoreGrowthResult(value, reservation, request) {
1543
+ const result = requireStoreResult(value, 'growth');
1544
+ validateGrowthCommon(result, reservation.id, request.incrementId);
1545
+ if (result.accepted === false) {
1546
+ if (result.reason !== 'quota_exceeded') {
1547
+ throw new TypeError('UsageStore growth denial reason is invalid');
1548
+ }
1549
+ validateNonEmptyString(result.limitingBudgetKey, 'UsageStore growth limitingBudgetKey');
1550
+ assertNonNegativeStoreInteger(result.remaining, 'UsageStore growth remaining');
1551
+ if (!request.budgets.some(budget => budget.key === result.limitingBudgetKey)) {
1552
+ throw new TypeError('UsageStore growth limiting budget did not match the request');
1553
+ }
1554
+ return result;
1555
+ }
1556
+ assertNonNegativeStoreInteger(result.previousReservedUnits, 'UsageStore growth previousReservedUnits');
1557
+ assertNonNegativeStoreInteger(result.reservedUnits, 'UsageStore growth reservedUnits');
1558
+ if (result.previousReservedUnits !== reservation.reservedUnits) {
1559
+ throw new TypeError('UsageStore growth previous reservation units did not match local state');
1560
+ }
1561
+ const expectedReservedUnits = safeAdd(reservation.reservedUnits, request.additionalUnits, 'expected growth reservedUnits');
1562
+ if (result.reservedUnits !== expectedReservedUnits) {
1563
+ throw new TypeError('UsageStore growth reserved units did not match the requested increment');
1564
+ }
1565
+ validateBudgetRemainingTopology(result.remainingByBudget, request.budgets.map(budget => budget.key), 'UsageStore growth remainingByBudget');
1566
+ return result;
1567
+ }
1568
+ function validateStoreVectorGrowthResult(value, reservation, request) {
1569
+ const result = requireStoreResult(value, 'vector growth');
1570
+ validateGrowthCommon(result, reservation.id, request.incrementId);
1571
+ if (result.accepted === false) {
1572
+ if (result.reason !== 'quota_exceeded') {
1573
+ throw new TypeError('UsageStore vector growth denial reason is invalid');
1574
+ }
1575
+ validateNonEmptyString(result.limitingDimensionKey, 'UsageStore vector growth limitingDimensionKey');
1576
+ validateNonEmptyString(result.limitingBudgetKey, 'UsageStore vector growth limitingBudgetKey');
1577
+ assertNonNegativeStoreInteger(result.remaining, 'UsageStore vector growth remaining');
1578
+ const dimension = request.dimensions.find(item => item.key === result.limitingDimensionKey);
1579
+ if (!dimension?.budgets.some(budget => budget.key === result.limitingBudgetKey)) {
1580
+ throw new TypeError('UsageStore vector growth limiting topology did not match the request');
1581
+ }
1582
+ return result;
1583
+ }
1584
+ validateReservedDimensionTopology(result.previousReservedByDimension, reservation.dimensions.map(dimension => ({
1585
+ key: dimension.key,
1586
+ reservedUnits: dimension.reservedUnits,
1587
+ })), 'UsageStore vector growth previousReservedByDimension');
1588
+ const growthByKey = new Map(request.dimensions.map(dimension => [dimension.key, dimension]));
1589
+ validateReservedDimensionTopology(result.reservedByDimension, reservation.dimensions.map(dimension => ({
1590
+ key: dimension.key,
1591
+ reservedUnits: safeAdd(dimension.reservedUnits, growthByKey.get(dimension.key)?.additionalUnits ?? 0, 'expected vector growth reservedUnits'),
1592
+ })), 'UsageStore vector growth reservedByDimension');
1593
+ validateVectorRemainingTopology(result.remainingByBudget, request.dimensions, 'UsageStore vector growth remainingByBudget');
1594
+ return result;
1595
+ }
1596
+ function requireStoreResult(value, operation) {
1597
+ const result = requireRecord(value, `UsageStore ${operation} result`);
1598
+ if (typeof result.accepted !== 'boolean') {
1599
+ throw new TypeError(`UsageStore ${operation} result accepted must be a boolean`);
1600
+ }
1601
+ return result;
1602
+ }
1603
+ function validateGrowthCommon(result, reservationId, incrementId) {
1604
+ if (typeof result.replayed !== 'boolean') {
1605
+ throw new TypeError('UsageStore growth replayed must be a boolean');
1606
+ }
1607
+ if (result.reservationId !== reservationId || result.incrementId !== incrementId) {
1608
+ throw new TypeError('UsageStore growth result identity did not match the request');
1609
+ }
1610
+ validateNonEmptyString(result.growthCursor, 'UsageStore growthCursor');
1611
+ }
1612
+ function validateReserveDenial(result, vector) {
1613
+ if (result.reason !== 'quota_exceeded' && result.reason !== 'duplicate_operation') {
1614
+ throw new TypeError('UsageStore reserve denial reason is invalid');
1615
+ }
1616
+ if (result.limitingBudgetKey !== undefined) {
1617
+ validateNonEmptyString(result.limitingBudgetKey, 'UsageStore limitingBudgetKey');
1618
+ }
1619
+ if (vector && result.limitingDimensionKey !== undefined) {
1620
+ validateNonEmptyString(result.limitingDimensionKey, 'UsageStore limitingDimensionKey');
1621
+ }
1622
+ if (result.remaining !== undefined) {
1623
+ assertNonNegativeStoreInteger(result.remaining, 'UsageStore remaining');
1624
+ }
1625
+ }
1626
+ function validateScalarAcceptedReservation(result, request, units, budgets) {
1627
+ const reservation = requireRecord(result.reservation, 'UsageStore accepted reservation');
1628
+ validateNonEmptyString(reservation.id, 'UsageStore reservation id');
1629
+ if (reservation.operationId !== request.operationId ||
1630
+ reservation.principalId !== request.principal.id ||
1631
+ reservation.tenantId !== request.principal.tenantId ||
1632
+ reservation.plan !== request.principal.plan ||
1633
+ reservation.tool !== request.tool) {
1634
+ throw new TypeError('UsageStore accepted reservation identity did not match the request');
1635
+ }
1636
+ assertNonNegativeStoreInteger(reservation.reservedUnits, 'UsageStore reservation reservedUnits');
1637
+ if (reservation.reservedUnits !== units) {
1638
+ throw new TypeError('UsageStore accepted reservation units did not match the request');
1639
+ }
1640
+ assertPositiveStoreInteger(reservation.expiresAt, 'UsageStore reservation expiresAt');
1641
+ validateOptionalNonEmptyString(reservation.growthCursor, 'UsageStore reservation growthCursor');
1642
+ validateStringSet(reservation.budgetKeys, budgets.map(budget => budget.key), 'UsageStore reservation budgetKeys');
1643
+ validateBudgetRemainingTopology(result.remainingByBudget, budgets.map(budget => budget.key), 'UsageStore remainingByBudget');
1644
+ }
1645
+ function validateVectorAcceptedReservation(result, request, dimensions) {
1646
+ const reservation = requireRecord(result.reservation, 'UsageStore accepted vector reservation');
1647
+ validateNonEmptyString(reservation.id, 'UsageStore vector reservation id');
1648
+ if (reservation.operationId !== request.operationId ||
1649
+ reservation.principalId !== request.principal.id ||
1650
+ reservation.tenantId !== request.principal.tenantId ||
1651
+ reservation.plan !== request.principal.plan ||
1652
+ reservation.tool !== request.tool) {
1653
+ throw new TypeError('UsageStore accepted vector reservation identity did not match the request');
1654
+ }
1655
+ assertPositiveStoreInteger(reservation.expiresAt, 'UsageStore vector reservation expiresAt');
1656
+ validateOptionalNonEmptyString(reservation.growthCursor, 'UsageStore vector reservation growthCursor');
1657
+ if (!Array.isArray(reservation.dimensions) || reservation.dimensions.length !== dimensions.length) {
1658
+ throw new TypeError('UsageStore accepted vector reservation dimensions did not match the request');
1659
+ }
1660
+ const expectedByKey = new Map(dimensions.map(dimension => [dimension.key, dimension]));
1661
+ const seen = new Set();
1662
+ for (const raw of reservation.dimensions) {
1663
+ const dimension = requireRecord(raw, 'UsageStore vector reservation dimension');
1664
+ validateNonEmptyString(dimension.key, 'UsageStore vector reservation dimension key');
1665
+ const expected = expectedByKey.get(dimension.key);
1666
+ if (!expected || seen.has(dimension.key)) {
1667
+ throw new TypeError('UsageStore accepted vector reservation topology did not match the request');
1668
+ }
1669
+ seen.add(dimension.key);
1670
+ assertNonNegativeStoreInteger(dimension.reservedUnits, 'UsageStore vector reservation reservedUnits');
1671
+ if (dimension.reservedUnits !== expected.units) {
1672
+ throw new TypeError('UsageStore accepted vector reservation units did not match the request');
1673
+ }
1674
+ validateStringSet(dimension.budgetKeys, expected.budgets.map(budget => budget.key), 'UsageStore vector reservation budgetKeys');
1675
+ }
1676
+ validateVectorRemainingTopology(result.remainingByBudget, dimensions, 'UsageStore vector remainingByBudget');
1677
+ }
1678
+ function validateBudgetRemainingTopology(value, expectedKeys, name) {
1679
+ if (!Array.isArray(value) || value.length !== expectedKeys.length) {
1680
+ throw new TypeError(`${name} did not match requested budgets`);
1681
+ }
1682
+ const expected = new Set(expectedKeys);
1683
+ const seen = new Set();
1684
+ for (const raw of value) {
1685
+ const balance = requireRecord(raw, name);
1686
+ validateNonEmptyString(balance.key, `${name} key`);
1687
+ if (!expected.has(balance.key) || seen.has(balance.key)) {
1688
+ throw new TypeError(`${name} did not match requested budgets`);
1689
+ }
1690
+ seen.add(balance.key);
1691
+ assertNonNegativeStoreInteger(balance.remaining, `${name} remaining`);
1692
+ }
1693
+ }
1694
+ function validateVectorRemainingTopology(value, dimensions, name) {
1695
+ const expected = new Set(dimensions.flatMap(dimension => dimension.budgets.map(budget => `${dimension.key}\u0000${budget.key}`)));
1696
+ if (!Array.isArray(value) || value.length !== expected.size) {
1697
+ throw new TypeError(`${name} did not match requested vector budgets`);
1698
+ }
1699
+ const seen = new Set();
1700
+ for (const raw of value) {
1701
+ const balance = requireRecord(raw, name);
1702
+ validateNonEmptyString(balance.dimensionKey, `${name} dimensionKey`);
1703
+ validateNonEmptyString(balance.budgetKey, `${name} budgetKey`);
1704
+ const key = `${balance.dimensionKey}\u0000${balance.budgetKey}`;
1705
+ if (!expected.has(key) || seen.has(key)) {
1706
+ throw new TypeError(`${name} did not match requested vector budgets`);
1707
+ }
1708
+ seen.add(key);
1709
+ assertNonNegativeStoreInteger(balance.remaining, `${name} remaining`);
1710
+ }
1711
+ }
1712
+ function validateReservedDimensionTopology(value, expected, name) {
1713
+ if (!Array.isArray(value) || value.length !== expected.length) {
1714
+ throw new TypeError(`${name} did not match reservation dimensions`);
1715
+ }
1716
+ const expectedByKey = new Map(expected.map(dimension => [dimension.key, dimension.reservedUnits]));
1717
+ const seen = new Set();
1718
+ for (const raw of value) {
1719
+ const dimension = requireRecord(raw, name);
1720
+ validateNonEmptyString(dimension.key, `${name} key`);
1721
+ assertNonNegativeStoreInteger(dimension.reservedUnits, `${name} reservedUnits`);
1722
+ if (seen.has(dimension.key) ||
1723
+ expectedByKey.get(dimension.key) !== dimension.reservedUnits) {
1724
+ throw new TypeError(`${name} did not match reservation dimensions`);
1725
+ }
1726
+ seen.add(dimension.key);
1727
+ }
1728
+ }
1729
+ function validateStringSet(value, expected, name) {
1730
+ if (!Array.isArray(value) || value.length !== expected.length) {
1731
+ throw new TypeError(`${name} did not match requested identifiers`);
1732
+ }
1733
+ const expectedSet = new Set(expected);
1734
+ const seen = new Set();
1735
+ for (const item of value) {
1736
+ validateNonEmptyString(item, name);
1737
+ if (!expectedSet.has(item) || seen.has(item)) {
1738
+ throw new TypeError(`${name} did not match requested identifiers`);
1739
+ }
1740
+ seen.add(item);
1741
+ }
1742
+ }
1743
+ function requireRecord(value, name) {
1744
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
1745
+ throw new TypeError(`${name} must be an object`);
1746
+ }
1747
+ return value;
1748
+ }
1749
+ function hasOwn(value, key) {
1750
+ return Object.prototype.hasOwnProperty.call(value, key);
1751
+ }
1752
+ function validateNonEmptyString(value, name) {
1753
+ if (typeof value !== 'string' || value.length === 0) {
1754
+ throw new TypeError(`${name} must be a non-empty string`);
1755
+ }
1756
+ }
1757
+ function validateOptionalNonEmptyString(value, name) {
1758
+ if (value !== undefined)
1759
+ validateNonEmptyString(value, name);
1760
+ }
1761
+ function assertNonNegativeStoreInteger(value, name) {
1762
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {
1763
+ throw new TypeError(`${name} must be a non-negative safe integer`);
1764
+ }
1765
+ }
1766
+ function assertPositiveStoreInteger(value, name) {
1767
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value <= 0) {
1768
+ throw new TypeError(`${name} must be a positive safe integer`);
1769
+ }
1770
+ }
1771
+ function canonicalizeBudgets(budgets) {
1772
+ validateUsageBudgetEnvelope(budgets);
1773
+ const normalized = budgets.map(budget => {
1774
+ if (budget === null || typeof budget !== 'object' || Array.isArray(budget)) {
1775
+ throw new TypeError('budget must be an object');
1776
+ }
1777
+ if (typeof budget.key !== 'string' || budget.key.length === 0) {
1778
+ throw new RangeError('budget.key must be a non-empty string');
1779
+ }
1780
+ assertNonNegativeInteger(budget.limit, `budget.limit (${budget.key})`);
1781
+ return { key: budget.key, limit: budget.limit };
1782
+ });
1783
+ normalized.sort((a, b) => a.key.localeCompare(b.key));
1784
+ for (let i = 1; i < normalized.length; i += 1) {
1785
+ if (normalized[i - 1].key === normalized[i].key) {
1786
+ throw new RangeError(`duplicate budget key: ${normalized[i].key}`);
1787
+ }
1788
+ }
1789
+ return normalized;
1790
+ }
1791
+ function validateRequestIdentity(request) {
1792
+ validateUsageRequestEnvelope(request);
1793
+ }
1794
+ function cloneReservationRecord(reservation) {
1795
+ if (typeof reservation.id !== 'string' || reservation.id.length === 0) {
1796
+ throw new UsageStateError('Resume reservation id must be non-empty');
1797
+ }
1798
+ if (typeof reservation.operationId !== 'string' || reservation.operationId.length === 0) {
1799
+ throw new UsageStateError('Resume operationId must be non-empty');
1800
+ }
1801
+ if (typeof reservation.principalId !== 'string' || reservation.principalId.length === 0) {
1802
+ throw new UsageStateError('Resume principalId must be non-empty');
1803
+ }
1804
+ if (typeof reservation.tool !== 'string' || reservation.tool.length === 0) {
1805
+ throw new UsageStateError('Resume tool must be non-empty');
1806
+ }
1807
+ if (!Array.isArray(reservation.budgetKeys) || reservation.budgetKeys.length === 0) {
1808
+ throw new UsageStateError('Resume reservation must contain budget keys');
1809
+ }
1810
+ if (reservation.budgetKeys.some(key => typeof key !== 'string' || key.length === 0)) {
1811
+ throw new UsageStateError('Resume budget keys must be non-empty strings');
1812
+ }
1813
+ assertNonNegativeInteger(reservation.reservedUnits, 'reservedUnits');
1814
+ assertPositiveInteger(reservation.expiresAt, 'expiresAt');
1815
+ if (reservation.growthCursor !== undefined &&
1816
+ (typeof reservation.growthCursor !== 'string' || reservation.growthCursor.length === 0)) {
1817
+ throw new UsageStateError('Resume growthCursor must be a non-empty string when present');
1818
+ }
1819
+ return {
1820
+ id: reservation.id,
1821
+ operationId: reservation.operationId,
1822
+ principalId: reservation.principalId,
1823
+ ...(reservation.tenantId === undefined ? {} : { tenantId: reservation.tenantId }),
1824
+ ...(reservation.plan === undefined ? {} : { plan: reservation.plan }),
1825
+ tool: reservation.tool,
1826
+ budgetKeys: [...reservation.budgetKeys],
1827
+ reservedUnits: reservation.reservedUnits,
1828
+ expiresAt: reservation.expiresAt,
1829
+ ...(reservation.growthCursor === undefined
1830
+ ? {}
1831
+ : { growthCursor: reservation.growthCursor }),
1832
+ };
1833
+ }
1834
+ function isProgressiveUsageStore(store) {
1835
+ return typeof store.growReservation === 'function';
1836
+ }
1837
+ function isVectorUsageStore(store) {
1838
+ const candidate = store;
1839
+ return (typeof candidate.reserveVector === 'function' &&
1840
+ typeof candidate.growVectorReservation === 'function' &&
1841
+ typeof candidate.settleVector === 'function');
1842
+ }
1843
+ function canonicalizeUsageDimensions(dimensions) {
1844
+ validateUsageVectorEnvelope(dimensions);
1845
+ const normalized = dimensions.map(dimension => {
1846
+ if (dimension === null || typeof dimension !== 'object' || Array.isArray(dimension)) {
1847
+ throw new TypeError('dimension must be an object');
1848
+ }
1849
+ if (typeof dimension.key !== 'string' || dimension.key.length === 0) {
1850
+ throw new RangeError('dimension.key must be a non-empty string');
1851
+ }
1852
+ assertNonNegativeInteger(dimension.units, `dimension.units (${dimension.key})`);
1853
+ return {
1854
+ key: dimension.key,
1855
+ units: dimension.units,
1856
+ budgets: canonicalizeBudgets(dimension.budgets),
1857
+ };
1858
+ });
1859
+ normalized.sort((a, b) => a.key.localeCompare(b.key));
1860
+ assertUniqueVectorTopology(normalized.map(dimension => ({
1861
+ key: dimension.key,
1862
+ budgets: dimension.budgets,
1863
+ })));
1864
+ return normalized;
1865
+ }
1866
+ function canonicalizeVectorGrowthDimensions(dimensions) {
1867
+ validateUsageVectorGrowthEnvelope(dimensions);
1868
+ const normalized = dimensions.map(dimension => {
1869
+ if (typeof dimension.key !== 'string' || dimension.key.length === 0) {
1870
+ throw new RangeError('dimension.key must be a non-empty string');
1871
+ }
1872
+ assertNonNegativeInteger(dimension.additionalUnits, `dimension.additionalUnits (${dimension.key})`);
1873
+ return {
1874
+ key: dimension.key,
1875
+ additionalUnits: dimension.additionalUnits,
1876
+ budgets: canonicalizeBudgets(dimension.budgets),
1877
+ };
1878
+ });
1879
+ normalized.sort((a, b) => a.key.localeCompare(b.key));
1880
+ assertUniqueVectorTopology(normalized.map(dimension => ({
1881
+ key: dimension.key,
1882
+ budgets: dimension.budgets,
1883
+ })));
1884
+ if (!normalized.some(dimension => dimension.additionalUnits > 0)) {
1885
+ throw new RangeError('vector growth must add units to at least one dimension');
1886
+ }
1887
+ return normalized;
1888
+ }
1889
+ function assertUniqueVectorTopology(dimensions) {
1890
+ const dimensionKeys = new Set();
1891
+ const budgetKeys = new Set();
1892
+ for (const dimension of dimensions) {
1893
+ if (dimensionKeys.has(dimension.key)) {
1894
+ throw new RangeError(`duplicate dimension key: ${dimension.key}`);
1895
+ }
1896
+ dimensionKeys.add(dimension.key);
1897
+ for (const budget of dimension.budgets) {
1898
+ if (budgetKeys.has(budget.key)) {
1899
+ throw new RangeError(`budget key cannot appear in multiple vector dimensions: ${budget.key}`);
1900
+ }
1901
+ budgetKeys.add(budget.key);
1902
+ }
1903
+ }
1904
+ }
1905
+ function cloneVectorReservationRecord(reservation) {
1906
+ if (typeof reservation.id !== 'string' || reservation.id.length === 0) {
1907
+ throw new UsageStateError('Resume vector reservation id must be non-empty');
1908
+ }
1909
+ if (typeof reservation.operationId !== 'string' || reservation.operationId.length === 0) {
1910
+ throw new UsageStateError('Resume vector operationId must be non-empty');
1911
+ }
1912
+ if (typeof reservation.principalId !== 'string' || reservation.principalId.length === 0) {
1913
+ throw new UsageStateError('Resume vector principalId must be non-empty');
1914
+ }
1915
+ if (typeof reservation.tool !== 'string' || reservation.tool.length === 0) {
1916
+ throw new UsageStateError('Resume vector tool must be non-empty');
1917
+ }
1918
+ if (!Array.isArray(reservation.dimensions) || reservation.dimensions.length === 0) {
1919
+ throw new UsageStateError('Resume vector reservation must contain dimensions');
1920
+ }
1921
+ const dimensions = reservation.dimensions.map(dimension => {
1922
+ if (typeof dimension.key !== 'string' || dimension.key.length === 0) {
1923
+ throw new UsageStateError('Resume vector dimension keys must be non-empty strings');
1924
+ }
1925
+ if (!Array.isArray(dimension.budgetKeys) || dimension.budgetKeys.length === 0) {
1926
+ throw new UsageStateError('Resume vector dimension must contain budget keys');
1927
+ }
1928
+ if (dimension.budgetKeys.some(key => typeof key !== 'string' || key.length === 0)) {
1929
+ throw new UsageStateError('Resume vector budget keys must be non-empty strings');
1930
+ }
1931
+ assertNonNegativeInteger(dimension.reservedUnits, `reservedUnits (${dimension.key})`);
1932
+ return {
1933
+ key: dimension.key,
1934
+ budgetKeys: [...dimension.budgetKeys].sort((a, b) => a.localeCompare(b)),
1935
+ reservedUnits: dimension.reservedUnits,
1936
+ };
1937
+ });
1938
+ dimensions.sort((a, b) => a.key.localeCompare(b.key));
1939
+ assertUniqueVectorTopology(dimensions.map(dimension => ({
1940
+ key: dimension.key,
1941
+ budgets: dimension.budgetKeys.map(key => ({ key, limit: 0 })),
1942
+ })));
1943
+ assertPositiveInteger(reservation.expiresAt, 'expiresAt');
1944
+ if (reservation.growthCursor !== undefined &&
1945
+ (typeof reservation.growthCursor !== 'string' || reservation.growthCursor.length === 0)) {
1946
+ throw new UsageStateError('Resume vector growthCursor must be a non-empty string when present');
1947
+ }
1948
+ return {
1949
+ id: reservation.id,
1950
+ operationId: reservation.operationId,
1951
+ principalId: reservation.principalId,
1952
+ ...(reservation.tenantId === undefined ? {} : { tenantId: reservation.tenantId }),
1953
+ ...(reservation.plan === undefined ? {} : { plan: reservation.plan }),
1954
+ tool: reservation.tool,
1955
+ dimensions,
1956
+ expiresAt: reservation.expiresAt,
1957
+ ...(reservation.growthCursor === undefined ? {} : { growthCursor: reservation.growthCursor }),
1958
+ };
1959
+ }
1960
+ function canonicalizeVectorGrowthRequest(input) {
1961
+ if (typeof input.incrementId !== 'string' || input.incrementId.length === 0) {
1962
+ throw new RangeError('incrementId must be a non-empty string');
1963
+ }
1964
+ return {
1965
+ incrementId: input.incrementId,
1966
+ dimensions: canonicalizeVectorGrowthDimensions(input.dimensions),
1967
+ };
1968
+ }
1969
+ function cloneVectorGrowthRequest(input) {
1970
+ return {
1971
+ incrementId: input.incrementId,
1972
+ dimensions: input.dimensions.map(dimension => ({
1973
+ key: dimension.key,
1974
+ additionalUnits: dimension.additionalUnits,
1975
+ budgets: dimension.budgets.map(budget => ({ ...budget })),
1976
+ })),
1977
+ };
1978
+ }
1979
+ function sameVectorGrowthRequest(left, right) {
1980
+ return (left.incrementId === right.incrementId &&
1981
+ vectorGrowthFingerprint(left.dimensions) === vectorGrowthFingerprint(right.dimensions));
1982
+ }
1983
+ function vectorGrowthFingerprint(dimensions) {
1984
+ return JSON.stringify(dimensions.map(dimension => [
1985
+ dimension.key,
1986
+ dimension.additionalUnits,
1987
+ dimension.budgets.map(budget => [budget.key, budget.limit]),
1988
+ ]));
1989
+ }
1990
+ function canonicalizeVectorActuals(actualByDimension, reservedDimensions) {
1991
+ if (actualByDimension.length !== reservedDimensions.length) {
1992
+ throw new UsageStateError('Vector settlement must report every reservation dimension exactly once');
1993
+ }
1994
+ const normalized = actualByDimension.map(item => {
1995
+ if (typeof item.key !== 'string' || item.key.length === 0) {
1996
+ throw new RangeError('actual dimension key must be a non-empty string');
1997
+ }
1998
+ assertNonNegativeInteger(item.actualUnits, `actualUnits (${item.key})`);
1999
+ return { key: item.key, actualUnits: item.actualUnits };
2000
+ });
2001
+ normalized.sort((a, b) => a.key.localeCompare(b.key));
2002
+ for (let index = 0; index < normalized.length; index += 1) {
2003
+ const actual = normalized[index];
2004
+ const reserved = reservedDimensions[index];
2005
+ if (actual.key !== reserved.key) {
2006
+ throw new UsageStateError('Vector settlement dimension keys do not match the reservation');
2007
+ }
2008
+ if (actual.actualUnits > reserved.reservedUnits) {
2009
+ throw new UsageStateError(`actualUnits cannot exceed reservedUnits for dimension ${actual.key}`);
2010
+ }
2011
+ }
2012
+ return normalized;
2013
+ }
2014
+ function canonicalizeGrowthRequest(input) {
2015
+ if (typeof input.incrementId !== 'string' || input.incrementId.length === 0) {
2016
+ throw new RangeError('incrementId must be a non-empty string');
2017
+ }
2018
+ assertPositiveInteger(input.additionalUnits, 'additionalUnits');
2019
+ return {
2020
+ incrementId: input.incrementId,
2021
+ additionalUnits: input.additionalUnits,
2022
+ budgets: canonicalizeBudgets(input.budgets),
2023
+ };
2024
+ }
2025
+ function cloneGrowthRequest(input) {
2026
+ return {
2027
+ incrementId: input.incrementId,
2028
+ additionalUnits: input.additionalUnits,
2029
+ budgets: input.budgets.map(budget => ({ ...budget })),
2030
+ };
2031
+ }
2032
+ function sameGrowthRequest(left, right) {
2033
+ return (left.incrementId === right.incrementId &&
2034
+ growthFingerprint(left.additionalUnits, left.budgets) ===
2035
+ growthFingerprint(right.additionalUnits, right.budgets));
2036
+ }
2037
+ function validateGrowthInput(input) {
2038
+ assertUsageIdentifier(input.reservationId, 'reservationId');
2039
+ assertUsageIdentifier(input.incrementId, 'incrementId');
2040
+ assertUsageIdentifier(input.expectedGrowthCursor, 'expectedGrowthCursor');
2041
+ assertPositiveInteger(input.additionalUnits, 'additionalUnits');
2042
+ }
2043
+ function growthFingerprint(additionalUnits, budgets) {
2044
+ return JSON.stringify([additionalUnits, budgets.map(budget => [budget.key, budget.limit])]);
2045
+ }
2046
+ function sameBudgetKeys(reservationBudgetKeys, budgets) {
2047
+ return (reservationBudgetKeys.length === budgets.length &&
2048
+ reservationBudgetKeys.every((key, index) => key === budgets[index].key));
2049
+ }
2050
+ function newGrowthCursor() {
2051
+ return `g1.${randomUUID()}`;
2052
+ }
2053
+ function replayGrowthResult(result) {
2054
+ return { ...cloneGrowthResult(result), replayed: true };
2055
+ }
2056
+ function cloneGrowthResult(result) {
2057
+ if (result.accepted) {
2058
+ return {
2059
+ ...result,
2060
+ remainingByBudget: result.remainingByBudget.map(balance => ({ ...balance })),
2061
+ };
2062
+ }
2063
+ return { ...result };
2064
+ }
2065
+ function safeAdd(left, right, name) {
2066
+ const result = left + right;
2067
+ if (!Number.isSafeInteger(result) || result < 0) {
2068
+ throw new RangeError(`${name} exceeds safe integer range`);
2069
+ }
2070
+ return result;
2071
+ }
2072
+ function requestIdentity(request) {
2073
+ return {
2074
+ principalId: request.principal.id,
2075
+ ...(request.principal.tenantId === undefined ? {} : { tenantId: request.principal.tenantId }),
2076
+ ...(request.principal.plan === undefined ? {} : { plan: request.principal.plan }),
2077
+ tool: request.tool,
2078
+ operationId: request.operationId,
2079
+ };
2080
+ }
2081
+ function vectorReservationIdentity(reservation) {
2082
+ return {
2083
+ principalId: reservation.principalId,
2084
+ ...(reservation.tenantId === undefined ? {} : { tenantId: reservation.tenantId }),
2085
+ ...(reservation.plan === undefined ? {} : { plan: reservation.plan }),
2086
+ tool: reservation.tool,
2087
+ operationId: reservation.operationId,
2088
+ };
2089
+ }
2090
+ function reservationIdentity(reservation) {
2091
+ return {
2092
+ principalId: reservation.principalId,
2093
+ ...(reservation.tenantId === undefined ? {} : { tenantId: reservation.tenantId }),
2094
+ ...(reservation.plan === undefined ? {} : { plan: reservation.plan }),
2095
+ tool: reservation.tool,
2096
+ operationId: reservation.operationId,
2097
+ };
2098
+ }
2099
+ function resolveMetadata(metadata, request) {
2100
+ if (!metadata)
2101
+ return undefined;
2102
+ if (typeof metadata !== 'function')
2103
+ return metadata;
2104
+ try {
2105
+ return metadata(request);
2106
+ }
2107
+ catch {
2108
+ return undefined;
2109
+ }
2110
+ }
2111
+ function toSettlement(reservation) {
2112
+ const actualUnits = reservation.actualUnits ?? reservation.reservedUnits;
2113
+ return {
2114
+ reservationId: reservation.id,
2115
+ reservedUnits: reservation.reservedUnits,
2116
+ actualUnits,
2117
+ releasedUnits: reservation.reservedUnits - actualUnits,
2118
+ outcome: reservation.outcome ?? 'unknown',
2119
+ };
2120
+ }
2121
+ function assertNonNegativeInteger(value, name) {
2122
+ if (!Number.isSafeInteger(value) || value < 0) {
2123
+ throw new RangeError(`${name} must be a non-negative safe integer`);
2124
+ }
2125
+ }
2126
+ function assertPositiveInteger(value, name) {
2127
+ if (!Number.isSafeInteger(value) || value <= 0) {
2128
+ throw new RangeError(`${name} must be a positive safe integer`);
2129
+ }
2130
+ }
2131
+ //# sourceMappingURL=index.js.map