mcp-usage-control-redis 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.
@@ -0,0 +1,719 @@
1
+ const COMMON = String.raw `
2
+ local MAX_SAFE_INTEGER = 9007199254740991
3
+ local REDIS_RESERVATION_SCHEMA_VERSION = 1
4
+
5
+ local function safeTimeAdd(base, delta)
6
+ if not base or not delta or base < 0 or delta <= 0 then return nil end
7
+ if base > MAX_SAFE_INTEGER or delta > (MAX_SAFE_INTEGER - base) then return nil end
8
+ return base + delta
9
+ end
10
+
11
+ local function saturatingSafeAdd(base, delta)
12
+ if delta >= (MAX_SAFE_INTEGER - base) then return MAX_SAFE_INTEGER end
13
+ return base + delta
14
+ end
15
+
16
+ local function safeIntegerString(value)
17
+ return string.format('%.0f', value)
18
+ end
19
+
20
+ local function hasSupportedRecordSchema(record)
21
+ return record.schemaVersion == nil or record.schemaVersion == REDIS_RESERVATION_SCHEMA_VERSION
22
+ end
23
+
24
+ local function subtractUsed(budgetHashes, amount)
25
+ if amount <= 0 then return end
26
+ for _, budgetHash in ipairs(budgetHashes) do
27
+ local nextUsed = redis.call('HINCRBY', KEYS[1], budgetHash, -amount)
28
+ if tonumber(nextUsed) <= 0 then redis.call('HDEL', KEYS[1], budgetHash) end
29
+ end
30
+ end
31
+
32
+ local function isVector(record)
33
+ return record.mode == 'vector'
34
+ end
35
+
36
+ local function vectorBudgetCount(record)
37
+ local count = 0
38
+ for _, dimension in ipairs(record.dimensions or {}) do
39
+ count = count + #(dimension.budgetHashes or {})
40
+ end
41
+ return count
42
+ end
43
+
44
+ local function releaseRecord(record)
45
+ if isVector(record) then
46
+ for _, dimension in ipairs(record.dimensions or {}) do
47
+ subtractUsed(dimension.budgetHashes, tonumber(dimension.reservedUnits))
48
+ end
49
+ return
50
+ end
51
+ subtractUsed(record.budgetHashes, tonumber(record.reservedUnits))
52
+ end
53
+
54
+ local function retainLiable(record)
55
+ record.state = 'settled'
56
+ record.outcome = 'lease_expired_after_execution_started'
57
+ if isVector(record) then
58
+ local actual = {}
59
+ for _, dimension in ipairs(record.dimensions or {}) do
60
+ table.insert(actual, { hash = dimension.hash, actualUnits = tonumber(dimension.reservedUnits) })
61
+ end
62
+ record.actualByDimensions = actual
63
+ else
64
+ record.actualUnits = tonumber(record.reservedUnits)
65
+ end
66
+ end
67
+
68
+ local function directExpiryReply(record, expiredState)
69
+ if isVector(record) then
70
+ return { 'expired_vector', expiredState, tostring(#(record.dimensions or {})), tostring(vectorBudgetCount(record)) }
71
+ end
72
+ return { 'expired', expiredState, tostring(tonumber(record.reservedUnits)) }
73
+ end
74
+ `;
75
+ const CLEANUP = String.raw `
76
+ local recoveredPendingCount = 0
77
+ local recoveredPendingUnits = 0
78
+ local recoveredLiableCount = 0
79
+ local recoveredLiableUnits = 0
80
+ local recoveredVectorPendingCount = 0
81
+ local recoveredVectorLiableCount = 0
82
+
83
+ local function withRecovery(reply)
84
+ table.insert(reply, 'recovery')
85
+ table.insert(reply, safeIntegerString(recoveredPendingCount))
86
+ table.insert(reply, safeIntegerString(recoveredPendingUnits))
87
+ table.insert(reply, safeIntegerString(recoveredLiableCount))
88
+ table.insert(reply, safeIntegerString(recoveredLiableUnits))
89
+ table.insert(reply, safeIntegerString(recoveredVectorPendingCount))
90
+ table.insert(reply, safeIntegerString(recoveredVectorLiableCount))
91
+ return reply
92
+ end
93
+
94
+ local expiredReservations = redis.call('ZRANGEBYSCORE', KEYS[2], '-inf', now, 'LIMIT', 0, cleanupLimit)
95
+ local expiredOperations = redis.call('ZRANGEBYSCORE', KEYS[5], '-inf', now, 'LIMIT', 0, cleanupLimit)
96
+
97
+ -- Redis Lua errors do not roll back writes already performed by a script. Validate
98
+ -- every record in this cleanup batch before mutating any accounting state.
99
+ for _, rid in ipairs(expiredReservations) do
100
+ local raw = redis.call('HGET', KEYS[3], rid)
101
+ if raw then
102
+ local record = cjson.decode(raw)
103
+ if not hasSupportedRecordSchema(record) then return { 'unsupported_schema_version' } end
104
+ end
105
+ end
106
+ for _, op in ipairs(expiredOperations) do
107
+ local rid = redis.call('HGET', KEYS[4], op)
108
+ if rid then
109
+ local raw = redis.call('HGET', KEYS[3], rid)
110
+ if raw then
111
+ local record = cjson.decode(raw)
112
+ if not hasSupportedRecordSchema(record) then return { 'unsupported_schema_version' } end
113
+ end
114
+ end
115
+ end
116
+
117
+ for _, rid in ipairs(expiredReservations) do
118
+ local raw = redis.call('HGET', KEYS[3], rid)
119
+ if raw then
120
+ local record = cjson.decode(raw)
121
+ if record.state == 'pending' then
122
+ releaseRecord(record)
123
+ redis.call('HDEL', KEYS[4], record.operationKey)
124
+ redis.call('HDEL', KEYS[3], rid)
125
+ if isVector(record) then
126
+ recoveredVectorPendingCount = saturatingSafeAdd(recoveredVectorPendingCount, 1)
127
+ else
128
+ recoveredPendingCount = saturatingSafeAdd(recoveredPendingCount, 1)
129
+ recoveredPendingUnits = saturatingSafeAdd(recoveredPendingUnits, tonumber(record.reservedUnits))
130
+ end
131
+ elseif record.state == 'liable' then
132
+ retainLiable(record)
133
+ redis.call('HSET', KEYS[3], rid, cjson.encode(record))
134
+ redis.call('ZADD', KEYS[5], tombstoneExpiresAt, record.operationKey)
135
+ if isVector(record) then
136
+ recoveredVectorLiableCount = saturatingSafeAdd(recoveredVectorLiableCount, 1)
137
+ else
138
+ recoveredLiableCount = saturatingSafeAdd(recoveredLiableCount, 1)
139
+ recoveredLiableUnits = saturatingSafeAdd(recoveredLiableUnits, tonumber(record.reservedUnits))
140
+ end
141
+ end
142
+ end
143
+ redis.call('ZREM', KEYS[2], rid)
144
+ end
145
+
146
+ for _, op in ipairs(expiredOperations) do
147
+ local rid = redis.call('HGET', KEYS[4], op)
148
+ if rid then
149
+ local raw = redis.call('HGET', KEYS[3], rid)
150
+ if raw then
151
+ local record = cjson.decode(raw)
152
+ if record.state == 'settled' then redis.call('HDEL', KEYS[3], rid) end
153
+ end
154
+ redis.call('HDEL', KEYS[4], op)
155
+ end
156
+ redis.call('ZREM', KEYS[5], op)
157
+ end
158
+ `;
159
+ export const RESERVE_SCRIPT = String.raw `
160
+ local units = tonumber(ARGV[1])
161
+ local ttlMs = tonumber(ARGV[2])
162
+ local reservationId = ARGV[3]
163
+ local operationKey = ARGV[4]
164
+ local cleanupLimit = tonumber(ARGV[5])
165
+ local idempotencyTtlMs = tonumber(ARGV[6])
166
+ local budgets = cjson.decode(ARGV[7])
167
+ local growthCursor = ARGV[8]
168
+ local redisTime = redis.call('TIME')
169
+ local now = tonumber(redisTime[1]) * 1000 + math.floor(tonumber(redisTime[2]) / 1000)
170
+ ${COMMON}
171
+ local expiresAt = safeTimeAdd(now, ttlMs)
172
+ local tombstoneExpiresAt = safeTimeAdd(now, idempotencyTtlMs)
173
+ if not expiresAt or not tombstoneExpiresAt then return { 'invalid_time' } end
174
+ ${CLEANUP}
175
+ if redis.call('HEXISTS', KEYS[4], operationKey) == 1 then return withRecovery({ 'duplicate_operation' }) end
176
+ local remainingByHash = {}
177
+ for _, budget in ipairs(budgets) do
178
+ local used = tonumber(redis.call('HGET', KEYS[1], budget.hash) or '0')
179
+ local remaining = tonumber(budget.limit) - used
180
+ if remaining < 0 then remaining = 0 end
181
+ remainingByHash[budget.hash] = remaining
182
+ if units > remaining then return withRecovery({ 'quota_exceeded', budget.hash, tostring(remaining) }) end
183
+ end
184
+ local budgetHashes = {}
185
+ local reply = { 'accepted' }
186
+ table.insert(reply, tostring(expiresAt))
187
+ for _, budget in ipairs(budgets) do
188
+ table.insert(budgetHashes, budget.hash)
189
+ if units > 0 then redis.call('HINCRBY', KEYS[1], budget.hash, units) end
190
+ table.insert(reply, budget.hash)
191
+ table.insert(reply, tostring(remainingByHash[budget.hash] - units))
192
+ end
193
+ local record = cjson.encode({
194
+ schemaVersion = REDIS_RESERVATION_SCHEMA_VERSION,
195
+ state = 'pending', operationKey = operationKey, reservedUnits = units,
196
+ expiresAt = expiresAt, budgetHashes = budgetHashes, growthCursor = growthCursor
197
+ })
198
+ redis.call('HSET', KEYS[3], reservationId, record)
199
+ redis.call('HSET', KEYS[4], operationKey, reservationId)
200
+ redis.call('ZADD', KEYS[2], expiresAt, reservationId)
201
+ return withRecovery(reply)
202
+ `;
203
+ export const RESERVE_VECTOR_SCRIPT = String.raw `
204
+ local ttlMs = tonumber(ARGV[1])
205
+ local reservationId = ARGV[2]
206
+ local operationKey = ARGV[3]
207
+ local cleanupLimit = tonumber(ARGV[4])
208
+ local idempotencyTtlMs = tonumber(ARGV[5])
209
+ local dimensions = cjson.decode(ARGV[6])
210
+ local growthCursor = ARGV[7]
211
+ local redisTime = redis.call('TIME')
212
+ local now = tonumber(redisTime[1]) * 1000 + math.floor(tonumber(redisTime[2]) / 1000)
213
+ ${COMMON}
214
+ local expiresAt = safeTimeAdd(now, ttlMs)
215
+ local tombstoneExpiresAt = safeTimeAdd(now, idempotencyTtlMs)
216
+ if not expiresAt or not tombstoneExpiresAt then return { 'invalid_time' } end
217
+ ${CLEANUP}
218
+ if redis.call('HEXISTS', KEYS[4], operationKey) == 1 then return withRecovery({ 'duplicate_operation' }) end
219
+ local balances = {}
220
+ for _, dimension in ipairs(dimensions) do
221
+ for _, budget in ipairs(dimension.budgets) do
222
+ local used = tonumber(redis.call('HGET', KEYS[1], budget.hash) or '0')
223
+ local remaining = tonumber(budget.limit) - used
224
+ if remaining < 0 then remaining = 0 end
225
+ table.insert(balances, { dimensionHash = dimension.hash, budgetHash = budget.hash, remaining = remaining - tonumber(dimension.units) })
226
+ if tonumber(dimension.units) > remaining then
227
+ return withRecovery({ 'quota_exceeded', dimension.hash, budget.hash, tostring(remaining) })
228
+ end
229
+ end
230
+ end
231
+ local storedDimensions = {}
232
+ for _, dimension in ipairs(dimensions) do
233
+ local hashes = {}
234
+ for _, budget in ipairs(dimension.budgets) do
235
+ table.insert(hashes, budget.hash)
236
+ if tonumber(dimension.units) > 0 then redis.call('HINCRBY', KEYS[1], budget.hash, tonumber(dimension.units)) end
237
+ end
238
+ table.insert(storedDimensions, { hash = dimension.hash, reservedUnits = tonumber(dimension.units), budgetHashes = hashes })
239
+ end
240
+ local record = cjson.encode({
241
+ schemaVersion = REDIS_RESERVATION_SCHEMA_VERSION,
242
+ mode = 'vector', state = 'pending', operationKey = operationKey,
243
+ dimensions = storedDimensions, expiresAt = expiresAt, growthCursor = growthCursor
244
+ })
245
+ redis.call('HSET', KEYS[3], reservationId, record)
246
+ redis.call('HSET', KEYS[4], operationKey, reservationId)
247
+ redis.call('ZADD', KEYS[2], expiresAt, reservationId)
248
+ return withRecovery({ 'accepted', tostring(expiresAt), cjson.encode(balances) })
249
+ `;
250
+ export const MARK_LIABLE_SCRIPT = String.raw `
251
+ local reservationId = ARGV[1]
252
+ local idempotencyTtlMs = tonumber(ARGV[2])
253
+ local redisTime = redis.call('TIME')
254
+ local now = tonumber(redisTime[1]) * 1000 + math.floor(tonumber(redisTime[2]) / 1000)
255
+ ${COMMON}
256
+ local tombstoneExpiresAt = safeTimeAdd(now, idempotencyTtlMs)
257
+ if not tombstoneExpiresAt then return { 'invalid_time' } end
258
+ local raw = redis.call('HGET', KEYS[3], reservationId)
259
+ if not raw then return { 'not_found' } end
260
+ local record = cjson.decode(raw)
261
+ if not hasSupportedRecordSchema(record) then return { 'unsupported_schema_version' } end
262
+ if record.state == 'settled' then return { 'not_pending' } end
263
+ if tonumber(record.expiresAt) <= now then
264
+ local expiredState = record.state
265
+ if record.state == 'pending' then
266
+ releaseRecord(record)
267
+ redis.call('HDEL', KEYS[4], record.operationKey)
268
+ redis.call('HDEL', KEYS[3], reservationId)
269
+ elseif record.state == 'liable' then
270
+ retainLiable(record)
271
+ redis.call('HSET', KEYS[3], reservationId, cjson.encode(record))
272
+ redis.call('ZADD', KEYS[5], tombstoneExpiresAt, record.operationKey)
273
+ end
274
+ redis.call('ZREM', KEYS[2], reservationId)
275
+ return directExpiryReply(record, expiredState)
276
+ end
277
+ if record.state == 'pending' then
278
+ record.state = 'liable'
279
+ redis.call('HSET', KEYS[3], reservationId, cjson.encode(record))
280
+ elseif record.state ~= 'liable' then return { 'not_pending' } end
281
+ return { 'marked', tostring(record.expiresAt) }
282
+ `;
283
+ export const RENEW_SCRIPT = String.raw `
284
+ local ttlMs = tonumber(ARGV[1])
285
+ local reservationId = ARGV[2]
286
+ local idempotencyTtlMs = tonumber(ARGV[3])
287
+ local redisTime = redis.call('TIME')
288
+ local now = tonumber(redisTime[1]) * 1000 + math.floor(tonumber(redisTime[2]) / 1000)
289
+ ${COMMON}
290
+ local expiresAt = safeTimeAdd(now, ttlMs)
291
+ local tombstoneExpiresAt = safeTimeAdd(now, idempotencyTtlMs)
292
+ if not expiresAt or not tombstoneExpiresAt then return { 'invalid_time' } end
293
+ local raw = redis.call('HGET', KEYS[3], reservationId)
294
+ if not raw then return { 'not_found' } end
295
+ local record = cjson.decode(raw)
296
+ if not hasSupportedRecordSchema(record) then return { 'unsupported_schema_version' } end
297
+ if record.state == 'settled' then return { 'not_pending' } end
298
+ if tonumber(record.expiresAt) <= now then
299
+ local expiredState = record.state
300
+ if record.state == 'pending' then
301
+ releaseRecord(record)
302
+ redis.call('HDEL', KEYS[4], record.operationKey)
303
+ redis.call('HDEL', KEYS[3], reservationId)
304
+ elseif record.state == 'liable' then
305
+ retainLiable(record)
306
+ redis.call('HSET', KEYS[3], reservationId, cjson.encode(record))
307
+ redis.call('ZADD', KEYS[5], tombstoneExpiresAt, record.operationKey)
308
+ end
309
+ redis.call('ZREM', KEYS[2], reservationId)
310
+ return directExpiryReply(record, expiredState)
311
+ end
312
+ if record.state ~= 'pending' and record.state ~= 'liable' then return { 'not_pending' } end
313
+ record.expiresAt = expiresAt
314
+ redis.call('HSET', KEYS[3], reservationId, cjson.encode(record))
315
+ redis.call('ZADD', KEYS[2], expiresAt, reservationId)
316
+ return { 'renewed', tostring(expiresAt) }
317
+ `;
318
+ export const SETTLE_SCRIPT = String.raw `
319
+ local reservationId = ARGV[1]
320
+ local actualUnits = tonumber(ARGV[2])
321
+ local outcome = ARGV[3]
322
+ local idempotencyTtlMs = tonumber(ARGV[4])
323
+ local redisTime = redis.call('TIME')
324
+ local now = tonumber(redisTime[1]) * 1000 + math.floor(tonumber(redisTime[2]) / 1000)
325
+ ${COMMON}
326
+ local tombstoneExpiresAt = safeTimeAdd(now, idempotencyTtlMs)
327
+ if not tombstoneExpiresAt then return { 'invalid_time' } end
328
+ local raw = redis.call('HGET', KEYS[3], reservationId)
329
+ if not raw then return { 'not_found' } end
330
+ local record = cjson.decode(raw)
331
+ if not hasSupportedRecordSchema(record) then return { 'unsupported_schema_version' } end
332
+ if isVector(record) then return { 'mode_mismatch' } end
333
+ local reservedUnits = tonumber(record.reservedUnits)
334
+ if record.state == 'settled' then
335
+ if tonumber(record.actualUnits) == actualUnits and record.outcome == outcome then
336
+ return { 'idempotent', tostring(reservedUnits), tostring(actualUnits), tostring(reservedUnits - actualUnits) }
337
+ end
338
+ return { 'conflict' }
339
+ end
340
+ if record.state ~= 'pending' and record.state ~= 'liable' then return { 'not_pending' } end
341
+ if tonumber(record.expiresAt) <= now then
342
+ local expiredState = record.state
343
+ if record.state == 'pending' then
344
+ releaseRecord(record)
345
+ redis.call('HDEL', KEYS[4], record.operationKey)
346
+ redis.call('HDEL', KEYS[3], reservationId)
347
+ else
348
+ retainLiable(record)
349
+ redis.call('HSET', KEYS[3], reservationId, cjson.encode(record))
350
+ redis.call('ZADD', KEYS[5], tombstoneExpiresAt, record.operationKey)
351
+ end
352
+ redis.call('ZREM', KEYS[2], reservationId)
353
+ return directExpiryReply(record, expiredState)
354
+ end
355
+ if actualUnits > reservedUnits then return { 'invalid_units' } end
356
+ local released = reservedUnits - actualUnits
357
+ subtractUsed(record.budgetHashes, released)
358
+ record.state = 'settled'
359
+ record.actualUnits = actualUnits
360
+ record.outcome = outcome
361
+ redis.call('HSET', KEYS[3], reservationId, cjson.encode(record))
362
+ redis.call('ZREM', KEYS[2], reservationId)
363
+ redis.call('ZADD', KEYS[5], tombstoneExpiresAt, record.operationKey)
364
+ return { 'settled', tostring(reservedUnits), tostring(actualUnits), tostring(released) }
365
+ `;
366
+ export const SETTLE_VECTOR_SCRIPT = String.raw `
367
+ local reservationId = ARGV[1]
368
+ local actuals = cjson.decode(ARGV[2])
369
+ local outcome = ARGV[3]
370
+ local idempotencyTtlMs = tonumber(ARGV[4])
371
+ local redisTime = redis.call('TIME')
372
+ local now = tonumber(redisTime[1]) * 1000 + math.floor(tonumber(redisTime[2]) / 1000)
373
+ ${COMMON}
374
+ local tombstoneExpiresAt = safeTimeAdd(now, idempotencyTtlMs)
375
+ if not tombstoneExpiresAt then return { 'invalid_time' } end
376
+ local raw = redis.call('HGET', KEYS[3], reservationId)
377
+ if not raw then return { 'not_found' } end
378
+ local record = cjson.decode(raw)
379
+ if not hasSupportedRecordSchema(record) then return { 'unsupported_schema_version' } end
380
+ if not isVector(record) then return { 'mode_mismatch' } end
381
+ local function sameActuals(left, right)
382
+ if not left or #left ~= #right then return false end
383
+ for index, item in ipairs(right) do
384
+ if left[index].hash ~= item.hash or tonumber(left[index].actualUnits) ~= tonumber(item.actualUnits) then return false end
385
+ end
386
+ return true
387
+ end
388
+ local function settlementJson()
389
+ local result = {}
390
+ for index, dimension in ipairs(record.dimensions) do
391
+ local actual = tonumber(actuals[index].actualUnits)
392
+ local reserved = tonumber(dimension.reservedUnits)
393
+ table.insert(result, { hash = dimension.hash, reservedUnits = reserved, actualUnits = actual, releasedUnits = reserved - actual })
394
+ end
395
+ return cjson.encode(result)
396
+ end
397
+ if record.state == 'settled' then
398
+ if record.outcome == outcome and sameActuals(record.actualByDimensions, actuals) then return { 'idempotent', settlementJson() } end
399
+ return { 'conflict' }
400
+ end
401
+ if record.state ~= 'pending' and record.state ~= 'liable' then return { 'not_pending' } end
402
+ if tonumber(record.expiresAt) <= now then
403
+ local expiredState = record.state
404
+ if record.state == 'pending' then
405
+ releaseRecord(record)
406
+ redis.call('HDEL', KEYS[4], record.operationKey)
407
+ redis.call('HDEL', KEYS[3], reservationId)
408
+ else
409
+ retainLiable(record)
410
+ redis.call('HSET', KEYS[3], reservationId, cjson.encode(record))
411
+ redis.call('ZADD', KEYS[5], tombstoneExpiresAt, record.operationKey)
412
+ end
413
+ redis.call('ZREM', KEYS[2], reservationId)
414
+ return directExpiryReply(record, expiredState)
415
+ end
416
+ if #actuals ~= #record.dimensions then return { 'dimension_mismatch' } end
417
+ for index, actual in ipairs(actuals) do
418
+ local dimension = record.dimensions[index]
419
+ if actual.hash ~= dimension.hash then return { 'dimension_mismatch' } end
420
+ if tonumber(actual.actualUnits) > tonumber(dimension.reservedUnits) then return { 'invalid_units' } end
421
+ end
422
+ for index, dimension in ipairs(record.dimensions) do
423
+ local released = tonumber(dimension.reservedUnits) - tonumber(actuals[index].actualUnits)
424
+ subtractUsed(dimension.budgetHashes, released)
425
+ end
426
+ record.state = 'settled'
427
+ record.actualByDimensions = actuals
428
+ record.outcome = outcome
429
+ redis.call('HSET', KEYS[3], reservationId, cjson.encode(record))
430
+ redis.call('ZREM', KEYS[2], reservationId)
431
+ redis.call('ZADD', KEYS[5], tombstoneExpiresAt, record.operationKey)
432
+ return { 'settled', settlementJson() }
433
+ `;
434
+ export const GROW_SCRIPT = String.raw `
435
+ local reservationId = ARGV[1]
436
+ local incrementHash = ARGV[2]
437
+ local expectedGrowthCursor = ARGV[3]
438
+ local additionalUnits = tonumber(ARGV[4])
439
+ local idempotencyTtlMs = tonumber(ARGV[5])
440
+ local budgets = cjson.decode(ARGV[6])
441
+ local fingerprint = ARGV[7]
442
+ local nextGrowthCursor = ARGV[8]
443
+ local redisTime = redis.call('TIME')
444
+ local now = tonumber(redisTime[1]) * 1000 + math.floor(tonumber(redisTime[2]) / 1000)
445
+ ${COMMON}
446
+ local tombstoneExpiresAt = safeTimeAdd(now, idempotencyTtlMs)
447
+ if not tombstoneExpiresAt then return { 'invalid_time' } end
448
+ local raw = redis.call('HGET', KEYS[3], reservationId)
449
+ if not raw then return { 'not_found' } end
450
+ local record = cjson.decode(raw)
451
+ if not hasSupportedRecordSchema(record) then return { 'unsupported_schema_version' } end
452
+ if isVector(record) then return { 'mode_mismatch' } end
453
+ local reservedUnits = tonumber(record.reservedUnits)
454
+ if record.state == 'settled' or (record.state ~= 'pending' and record.state ~= 'liable') then return { 'terminal' } end
455
+ if tonumber(record.expiresAt) <= now then
456
+ local expiredState = record.state
457
+ if record.state == 'pending' then
458
+ releaseRecord(record)
459
+ redis.call('HDEL', KEYS[4], record.operationKey)
460
+ redis.call('HDEL', KEYS[3], reservationId)
461
+ else
462
+ retainLiable(record)
463
+ redis.call('HSET', KEYS[3], reservationId, cjson.encode(record))
464
+ redis.call('ZADD', KEYS[5], tombstoneExpiresAt, record.operationKey)
465
+ end
466
+ redis.call('ZREM', KEYS[2], reservationId)
467
+ return directExpiryReply(record, expiredState)
468
+ end
469
+ local lastGrowth = record.lastGrowth
470
+ if lastGrowth and lastGrowth.incrementHash == incrementHash then
471
+ if lastGrowth.expectedGrowthCursor ~= expectedGrowthCursor or lastGrowth.fingerprint ~= fingerprint then return { 'conflict' } end
472
+ if lastGrowth.accepted == true then
473
+ return { 'accepted_replay', tostring(lastGrowth.previousReservedUnits), tostring(lastGrowth.reservedUnits), lastGrowth.nextGrowthCursor, cjson.encode(lastGrowth.remainingByHashes) }
474
+ end
475
+ return { 'quota_replay', lastGrowth.nextGrowthCursor, lastGrowth.limitingBudgetHash, tostring(lastGrowth.remaining) }
476
+ end
477
+ if not record.growthCursor then return { 'not_supported' } end
478
+ if record.growthCursor ~= expectedGrowthCursor then return { 'stale_cursor' } end
479
+ if #record.budgetHashes ~= #budgets then return { 'budget_mismatch' } end
480
+ for index, budget in ipairs(budgets) do if record.budgetHashes[index] ~= budget.hash then return { 'budget_mismatch' } end end
481
+ local remainingByHashes = {}
482
+ local limitingBudgetHash = nil
483
+ local limitingRemaining = nil
484
+ for _, budget in ipairs(budgets) do
485
+ local used = tonumber(redis.call('HGET', KEYS[1], budget.hash) or '0')
486
+ local remaining = tonumber(budget.limit) - used
487
+ if remaining < 0 then remaining = 0 end
488
+ table.insert(remainingByHashes, { hash = budget.hash, remaining = remaining - additionalUnits })
489
+ if not limitingBudgetHash and additionalUnits > remaining then limitingBudgetHash = budget.hash; limitingRemaining = remaining end
490
+ end
491
+ if limitingBudgetHash then
492
+ record.growthCursor = nextGrowthCursor
493
+ record.lastGrowth = {
494
+ accepted = false, incrementHash = incrementHash, expectedGrowthCursor = expectedGrowthCursor,
495
+ fingerprint = fingerprint, nextGrowthCursor = nextGrowthCursor,
496
+ limitingBudgetHash = limitingBudgetHash, remaining = limitingRemaining
497
+ }
498
+ redis.call('HSET', KEYS[3], reservationId, cjson.encode(record))
499
+ return { 'quota_exceeded', nextGrowthCursor, limitingBudgetHash, tostring(limitingRemaining) }
500
+ end
501
+ local previousReservedUnits = reservedUnits
502
+ local nextReservedUnits = reservedUnits + additionalUnits
503
+ for _, budget in ipairs(budgets) do redis.call('HINCRBY', KEYS[1], budget.hash, additionalUnits) end
504
+ record.reservedUnits = nextReservedUnits
505
+ record.growthCursor = nextGrowthCursor
506
+ record.lastGrowth = {
507
+ accepted = true, incrementHash = incrementHash, expectedGrowthCursor = expectedGrowthCursor,
508
+ fingerprint = fingerprint, nextGrowthCursor = nextGrowthCursor,
509
+ previousReservedUnits = previousReservedUnits, reservedUnits = nextReservedUnits,
510
+ remainingByHashes = remainingByHashes
511
+ }
512
+ redis.call('HSET', KEYS[3], reservationId, cjson.encode(record))
513
+ return { 'accepted', tostring(previousReservedUnits), tostring(nextReservedUnits), nextGrowthCursor, cjson.encode(remainingByHashes) }
514
+ `;
515
+ export const GROW_VECTOR_SCRIPT = String.raw `
516
+ local reservationId = ARGV[1]
517
+ local incrementHash = ARGV[2]
518
+ local expectedGrowthCursor = ARGV[3]
519
+ local idempotencyTtlMs = tonumber(ARGV[4])
520
+ local dimensions = cjson.decode(ARGV[5])
521
+ local fingerprint = ARGV[6]
522
+ local nextGrowthCursor = ARGV[7]
523
+ local redisTime = redis.call('TIME')
524
+ local now = tonumber(redisTime[1]) * 1000 + math.floor(tonumber(redisTime[2]) / 1000)
525
+ ${COMMON}
526
+ local tombstoneExpiresAt = safeTimeAdd(now, idempotencyTtlMs)
527
+ if not tombstoneExpiresAt then return { 'invalid_time' } end
528
+ local raw = redis.call('HGET', KEYS[3], reservationId)
529
+ if not raw then return { 'not_found' } end
530
+ local record = cjson.decode(raw)
531
+ if not hasSupportedRecordSchema(record) then return { 'unsupported_schema_version' } end
532
+ if not isVector(record) then return { 'mode_mismatch' } end
533
+ if record.state == 'settled' or (record.state ~= 'pending' and record.state ~= 'liable') then return { 'terminal' } end
534
+ if tonumber(record.expiresAt) <= now then
535
+ local expiredState = record.state
536
+ if record.state == 'pending' then
537
+ releaseRecord(record)
538
+ redis.call('HDEL', KEYS[4], record.operationKey)
539
+ redis.call('HDEL', KEYS[3], reservationId)
540
+ else
541
+ retainLiable(record)
542
+ redis.call('HSET', KEYS[3], reservationId, cjson.encode(record))
543
+ redis.call('ZADD', KEYS[5], tombstoneExpiresAt, record.operationKey)
544
+ end
545
+ redis.call('ZREM', KEYS[2], reservationId)
546
+ return directExpiryReply(record, expiredState)
547
+ end
548
+ local lastGrowth = record.lastVectorGrowth
549
+ if lastGrowth and lastGrowth.incrementHash == incrementHash then
550
+ if lastGrowth.expectedGrowthCursor ~= expectedGrowthCursor or lastGrowth.fingerprint ~= fingerprint then return { 'conflict' } end
551
+ if lastGrowth.accepted == true then
552
+ return { 'accepted_replay', lastGrowth.nextGrowthCursor, cjson.encode(lastGrowth.previousReservedByDimensions), cjson.encode(lastGrowth.reservedByDimensions), cjson.encode(lastGrowth.remainingByHashes) }
553
+ end
554
+ return { 'quota_replay', lastGrowth.nextGrowthCursor, lastGrowth.limitingDimensionHash, lastGrowth.limitingBudgetHash, tostring(lastGrowth.remaining) }
555
+ end
556
+ if not record.growthCursor then return { 'not_supported' } end
557
+ if record.growthCursor ~= expectedGrowthCursor then return { 'stale_cursor' } end
558
+ if #record.dimensions ~= #dimensions then return { 'dimension_mismatch' } end
559
+ for index, dimension in ipairs(dimensions) do
560
+ local stored = record.dimensions[index]
561
+ if stored.hash ~= dimension.hash or #stored.budgetHashes ~= #dimension.budgets then return { 'dimension_mismatch' } end
562
+ for budgetIndex, budget in ipairs(dimension.budgets) do
563
+ if stored.budgetHashes[budgetIndex] ~= budget.hash then return { 'dimension_mismatch' } end
564
+ end
565
+ end
566
+ local remainingByHashes = {}
567
+ local limitingDimensionHash = nil
568
+ local limitingBudgetHash = nil
569
+ local limitingRemaining = nil
570
+ for _, dimension in ipairs(dimensions) do
571
+ for _, budget in ipairs(dimension.budgets) do
572
+ local used = tonumber(redis.call('HGET', KEYS[1], budget.hash) or '0')
573
+ local remaining = tonumber(budget.limit) - used
574
+ if remaining < 0 then remaining = 0 end
575
+ table.insert(remainingByHashes, { dimensionHash = dimension.hash, budgetHash = budget.hash, remaining = remaining - tonumber(dimension.additionalUnits) })
576
+ if not limitingBudgetHash and tonumber(dimension.additionalUnits) > remaining then
577
+ limitingDimensionHash = dimension.hash
578
+ limitingBudgetHash = budget.hash
579
+ limitingRemaining = remaining
580
+ end
581
+ end
582
+ end
583
+ if limitingBudgetHash then
584
+ record.growthCursor = nextGrowthCursor
585
+ record.lastVectorGrowth = {
586
+ accepted = false, incrementHash = incrementHash, expectedGrowthCursor = expectedGrowthCursor,
587
+ fingerprint = fingerprint, nextGrowthCursor = nextGrowthCursor,
588
+ limitingDimensionHash = limitingDimensionHash, limitingBudgetHash = limitingBudgetHash,
589
+ remaining = limitingRemaining
590
+ }
591
+ redis.call('HSET', KEYS[3], reservationId, cjson.encode(record))
592
+ return { 'quota_exceeded', nextGrowthCursor, limitingDimensionHash, limitingBudgetHash, tostring(limitingRemaining) }
593
+ end
594
+ local previous = {}
595
+ local current = {}
596
+ for index, dimension in ipairs(dimensions) do
597
+ local stored = record.dimensions[index]
598
+ table.insert(previous, { hash = stored.hash, reservedUnits = tonumber(stored.reservedUnits) })
599
+ local nextReserved = tonumber(stored.reservedUnits) + tonumber(dimension.additionalUnits)
600
+ for _, budget in ipairs(dimension.budgets) do
601
+ if tonumber(dimension.additionalUnits) > 0 then redis.call('HINCRBY', KEYS[1], budget.hash, tonumber(dimension.additionalUnits)) end
602
+ end
603
+ stored.reservedUnits = nextReserved
604
+ table.insert(current, { hash = stored.hash, reservedUnits = nextReserved })
605
+ end
606
+ record.growthCursor = nextGrowthCursor
607
+ record.lastVectorGrowth = {
608
+ accepted = true, incrementHash = incrementHash, expectedGrowthCursor = expectedGrowthCursor,
609
+ fingerprint = fingerprint, nextGrowthCursor = nextGrowthCursor,
610
+ previousReservedByDimensions = previous, reservedByDimensions = current,
611
+ remainingByHashes = remainingByHashes
612
+ }
613
+ redis.call('HSET', KEYS[3], reservationId, cjson.encode(record))
614
+ return { 'accepted', nextGrowthCursor, cjson.encode(previous), cjson.encode(current), cjson.encode(remainingByHashes) }
615
+ `;
616
+ export const RECONCILE_OPERATION_SCRIPT = String.raw `
617
+ local reservationId = ARGV[1]
618
+ local redisTime = redis.call('TIME')
619
+ local now = tonumber(redisTime[1]) * 1000 + math.floor(tonumber(redisTime[2]) / 1000)
620
+ local raw = redis.call('HGET', KEYS[1], reservationId)
621
+ if not raw then return { 'absent' } end
622
+ local record = cjson.decode(raw)
623
+ if record.schemaVersion ~= nil and record.schemaVersion ~= 1 then return { 'unsupported_schema_version' } end
624
+ if record.mode == 'vector' then return { 'mode_mismatch' } end
625
+ local state = record.state
626
+ local reservedUnits = tonumber(record.reservedUnits)
627
+ local expiresAt = tonumber(record.expiresAt)
628
+ local budgetHashes = cjson.encode(record.budgetHashes or {})
629
+ local growthCursor = record.growthCursor or ''
630
+ if state == 'pending' or state == 'liable' then
631
+ if expiresAt <= now then
632
+ return { 'expired', state, tostring(reservedUnits), tostring(expiresAt), budgetHashes, growthCursor }
633
+ end
634
+ return { 'active', state, tostring(reservedUnits), tostring(expiresAt), budgetHashes, growthCursor }
635
+ end
636
+ if state ~= 'settled' then return { 'invalid_state' } end
637
+ local tombstoneScore = redis.call('ZSCORE', KEYS[2], record.operationKey)
638
+ if not tombstoneScore then return { 'invalid_state' } end
639
+ local tombstoneExpiresAt = tonumber(tombstoneScore)
640
+ if tombstoneExpiresAt <= now then return { 'absent' } end
641
+ if record.outcome == 'lease_expired_after_execution_started' then
642
+ return { 'expired', 'liable', tostring(reservedUnits), tostring(expiresAt), budgetHashes, growthCursor }
643
+ end
644
+ local actualUnits = tonumber(record.actualUnits)
645
+ if not actualUnits then return { 'invalid_state' } end
646
+ return { 'settled', tostring(reservedUnits), tostring(actualUnits), tostring(tombstoneExpiresAt), budgetHashes }
647
+ `;
648
+ export const RECONCILE_VECTOR_OPERATION_SCRIPT = String.raw `
649
+ local reservationId = ARGV[1]
650
+ local redisTime = redis.call('TIME')
651
+ local now = tonumber(redisTime[1]) * 1000 + math.floor(tonumber(redisTime[2]) / 1000)
652
+ local raw = redis.call('HGET', KEYS[1], reservationId)
653
+ if not raw then return { 'absent' } end
654
+ local record = cjson.decode(raw)
655
+ if record.schemaVersion ~= nil and record.schemaVersion ~= 1 then return { 'unsupported_schema_version' } end
656
+ if record.mode ~= 'vector' then return { 'mode_mismatch' } end
657
+ local state = record.state
658
+ local expiresAt = tonumber(record.expiresAt)
659
+ local dimensions = cjson.encode(record.dimensions or {})
660
+ local growthCursor = record.growthCursor or ''
661
+ if state == 'pending' or state == 'liable' then
662
+ if expiresAt <= now then
663
+ return { 'expired', state, tostring(expiresAt), dimensions, growthCursor }
664
+ end
665
+ return { 'active', state, tostring(expiresAt), dimensions, growthCursor }
666
+ end
667
+ if state ~= 'settled' then return { 'invalid_state' } end
668
+ local tombstoneScore = redis.call('ZSCORE', KEYS[2], record.operationKey)
669
+ if not tombstoneScore then return { 'invalid_state' } end
670
+ local tombstoneExpiresAt = tonumber(tombstoneScore)
671
+ if tombstoneExpiresAt <= now then return { 'absent' } end
672
+ if record.outcome == 'lease_expired_after_execution_started' then
673
+ return { 'expired', 'liable', tostring(expiresAt), dimensions, growthCursor }
674
+ end
675
+ local actual = cjson.encode(record.actualByDimensions or {})
676
+ return { 'settled', tostring(tombstoneExpiresAt), dimensions, actual }
677
+ `;
678
+ export const RETIRE_HISTORICAL_BUDGETS_SCRIPT = String.raw `
679
+ local candidates = cjson.decode(ARGV[1])
680
+ local maxReservations = tonumber(ARGV[2])
681
+ local reservationCount = redis.call('HLEN', KEYS[2])
682
+ if reservationCount > maxReservations then
683
+ return { 'scan_limit', tostring(reservationCount) }
684
+ end
685
+ local candidateSet = {}
686
+ for _, hash in ipairs(candidates) do candidateSet[hash] = true end
687
+ local values = redis.call('HVALS', KEYS[2])
688
+ for _, raw in ipairs(values) do
689
+ local record = cjson.decode(raw)
690
+ if record.schemaVersion ~= nil and record.schemaVersion ~= 1 then
691
+ return { 'unsupported_schema_version' }
692
+ end
693
+ if record.state == 'pending' or record.state == 'liable' then
694
+ if record.mode == 'vector' then
695
+ for _, dimension in ipairs(record.dimensions or {}) do
696
+ for _, budgetHash in ipairs(dimension.budgetHashes or {}) do
697
+ if candidateSet[budgetHash] then return { 'active_reference' } end
698
+ end
699
+ end
700
+ else
701
+ for _, budgetHash in ipairs(record.budgetHashes or {}) do
702
+ if candidateSet[budgetHash] then return { 'active_reference' } end
703
+ end
704
+ end
705
+ end
706
+ end
707
+ local retired = 0
708
+ local missing = 0
709
+ for _, hash in ipairs(candidates) do
710
+ if redis.call('HEXISTS', KEYS[1], hash) == 1 then
711
+ redis.call('HDEL', KEYS[1], hash)
712
+ retired = retired + 1
713
+ else
714
+ missing = missing + 1
715
+ end
716
+ end
717
+ return { 'ok', tostring(retired), tostring(missing) }
718
+ `;
719
+ //# sourceMappingURL=scripts.js.map