deepline 0.3.322 → 0.3.324

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 (113) hide show
  1. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  2. package/dist/bundling-sources/shared_libs/observability/scheduled-jobs.json +0 -18
  3. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +454 -129
  4. package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +407 -37
  5. package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +18 -4
  6. package/dist/bundling-sources/shared_libs/play-runtime/governor/rate-state-backend.ts +16 -1
  7. package/dist/bundling-sources/shared_libs/play-runtime/map-execution-frame.ts +6 -2
  8. package/dist/bundling-sources/shared_libs/play-runtime/play-http-request.ts +1 -1
  9. package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +2 -0
  10. package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/gradient2-concurrency-controller.ts +15 -5
  11. package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/provider-capacity-policy.ts +1 -0
  12. package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/provider-concurrency-policy.ts +7 -2
  13. package/dist/bundling-sources/shared_libs/play-runtime/provider-row-retry-queue.ts +65 -0
  14. package/dist/bundling-sources/shared_libs/play-runtime/resource-governor.ts +2 -0
  15. package/dist/bundling-sources/shared_libs/play-runtime/runtime-flow.ts +34 -5
  16. package/dist/bundling-sources/shared_libs/play-runtime/runtime-map-row-feed.ts +116 -15
  17. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-scope.ts +59 -6
  18. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts +294 -37
  19. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +73 -0
  20. package/dist/bundling-sources/shared_libs/play-runtime/suspension.ts +2 -0
  21. package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +41 -3
  22. package/dist/cli/{auth-VSXORUWX.mjs → auth-6KWNR6AI.mjs} +4 -4
  23. package/dist/cli/auth-LKSTC3YV.js +31 -0
  24. package/dist/cli/{billing-Q6WO5E2R.js → billing-CLLWHIAT.js} +70 -70
  25. package/dist/cli/{billing-EH74RDBF.mjs → billing-VQ2NUJLB.mjs} +4 -4
  26. package/dist/cli/{chunk-M4TZIO5U.js → chunk-2UQUOUAW.js} +2 -2
  27. package/dist/cli/{chunk-JWEYKLOV.mjs → chunk-3ZKAKQYC.mjs} +1 -1
  28. package/dist/cli/{chunk-6JV2VSQN.mjs → chunk-5CQ554BM.mjs} +1 -1
  29. package/dist/cli/{chunk-Z5TQC2ID.mjs → chunk-5QZFWY7C.mjs} +2 -2
  30. package/dist/cli/{chunk-A3THNBGB.mjs → chunk-AKM5JVPG.mjs} +2 -2
  31. package/dist/cli/{chunk-U6N6MS6N.js → chunk-D5YJHGST.js} +4 -4
  32. package/dist/cli/{chunk-SISYT2DM.mjs → chunk-DO2VA5UP.mjs} +2 -2
  33. package/dist/cli/{chunk-H52C7AHC.mjs → chunk-EHSI2DMO.mjs} +4 -4
  34. package/dist/cli/{chunk-CNO37AI3.js → chunk-EXKRHZRG.js} +220 -220
  35. package/dist/cli/{chunk-JCZX5ZQW.js → chunk-G4F2AIQ5.js} +6 -6
  36. package/dist/cli/{chunk-GAFGAIVW.js → chunk-GMO3NBE2.js} +60 -60
  37. package/dist/cli/{chunk-4F4XU2Z5.js → chunk-GROXVGGW.js} +6 -6
  38. package/dist/cli/{chunk-I7UUOAZN.mjs → chunk-HHZDWBSQ.mjs} +1 -1
  39. package/dist/cli/{chunk-ZHIPP7YD.mjs → chunk-I7RLL7LN.mjs} +1 -1
  40. package/dist/cli/{chunk-PTOQKD2Y.js → chunk-IXYR65PW.js} +1 -1
  41. package/dist/cli/{chunk-LUAF3MIC.mjs → chunk-LHJKYMTS.mjs} +1 -1
  42. package/dist/cli/{chunk-5RPFAX7O.js → chunk-N5E66DP3.js} +6 -6
  43. package/dist/cli/{chunk-CMRWPMZS.js → chunk-NC2ZXG2Y.js} +26 -26
  44. package/dist/cli/{chunk-LUTXK4ON.mjs → chunk-NIOFMRYW.mjs} +5 -5
  45. package/dist/cli/{chunk-PMTJYMDJ.mjs → chunk-NQJQ3CLX.mjs} +3 -3
  46. package/dist/cli/{chunk-KAUABQOG.js → chunk-PAA4FYCF.js} +15 -15
  47. package/dist/cli/{chunk-QXIPQYZZ.js → chunk-QM2Z4FQF.js} +6 -6
  48. package/dist/cli/{chunk-WARD6DBW.mjs → chunk-TJGJTB5T.mjs} +1 -1
  49. package/dist/cli/{chunk-KHRWZU6B.js → chunk-TZZNI2A4.js} +50 -50
  50. package/dist/cli/{chunk-6PE2VHSG.mjs → chunk-U2RPP53P.mjs} +6 -6
  51. package/dist/cli/{chunk-Y3LPZJFE.js → chunk-V5PA6TCT.js} +48 -48
  52. package/dist/cli/{client-XRO4X5XF.mjs → client-VHUKTDAZ.mjs} +3 -3
  53. package/dist/cli/client-WMSTRUGP.js +20 -0
  54. package/dist/cli/{csv-ALZ44QHM.js → csv-5MZY7HU5.js} +12 -12
  55. package/dist/cli/{csv-5B6JDXCN.mjs → csv-7DA4APB6.mjs} +3 -3
  56. package/dist/cli/{db-HJECAUNU.mjs → db-7UPW3N5U.mjs} +4 -4
  57. package/dist/cli/{db-D6UHGVOU.js → db-ICG3FUVE.js} +20 -20
  58. package/dist/cli/{describe-YAN46NBC.js → describe-VDRY2JCE.js} +15 -15
  59. package/dist/cli/{describe-FBAWTMDS.mjs → describe-ZAPGM5ND.mjs} +8 -8
  60. package/dist/cli/{executions-XSV7ZE4N.js → executions-D5GYCY3A.js} +6 -6
  61. package/dist/cli/{executions-LWHMAUHH.mjs → executions-TK3C2FAQ.mjs} +4 -4
  62. package/dist/cli/{feedback-RKT6F6W6.js → feedback-PZAZZUXF.js} +6 -6
  63. package/dist/cli/{feedback-EPXMI55U.mjs → feedback-T4EIP5QR.mjs} +3 -3
  64. package/dist/cli/git-73N6EUZ3.js +23 -0
  65. package/dist/cli/{git-BOXANJBI.mjs → git-WBFWIVO5.mjs} +4 -4
  66. package/dist/cli/index.js +75 -75
  67. package/dist/cli/index.mjs +31 -31
  68. package/dist/cli/{legacy-noops-VV7DRGA7.mjs → legacy-noops-DUCK7OKN.mjs} +4 -4
  69. package/dist/cli/{legacy-noops-7EBEYUQZ.js → legacy-noops-LRBJWXRK.js} +7 -7
  70. package/dist/cli/{monitors-RDRG3E4Y.js → monitors-7S2SL62G.js} +85 -85
  71. package/dist/cli/{monitors-NJPHRFYT.mjs → monitors-WYH5AQIO.mjs} +4 -4
  72. package/dist/cli/{org-ZFWDBQ7R.js → org-EQNGGHPV.js} +60 -60
  73. package/dist/cli/{org-USE3QRY6.mjs → org-XCSMLTUW.mjs} +4 -4
  74. package/dist/cli/{play-3VEIZZJ5.mjs → play-A5ISFBJM.mjs} +7 -7
  75. package/dist/cli/play-CLC2Z5SO.js +152 -0
  76. package/dist/cli/{providers-SXWPIPKO.js → providers-DR2CLD3N.js} +7 -7
  77. package/dist/cli/{providers-PP4L5HLM.mjs → providers-KHTZFQ5I.mjs} +4 -4
  78. package/dist/cli/{quickstart-QCSPS2D5.mjs → quickstart-KT4LKORA.mjs} +3 -3
  79. package/dist/cli/{quickstart-WVLEQQXO.js → quickstart-NIRWI6YU.js} +5 -5
  80. package/dist/cli/search-DM7IQS5F.js +26 -0
  81. package/dist/cli/{search-LTC6R6IS.mjs → search-WCEXPBAT.mjs} +5 -5
  82. package/dist/cli/{secrets-R5FGK6UC.js → secrets-QSFEZGVE.js} +13 -13
  83. package/dist/cli/{secrets-LV4FJNAL.mjs → secrets-TN7DW3QR.mjs} +4 -4
  84. package/dist/cli/{sessions-N6CMP6XZ.js → sessions-LJS6W67T.js} +5 -5
  85. package/dist/cli/{sessions-CFNRSRYX.mjs → sessions-WSE5FN4H.mjs} +4 -4
  86. package/dist/cli/{settings-RNEJQGBL.js → settings-FGS3VYJN.js} +74 -74
  87. package/dist/cli/{settings-ADEYLHV7.mjs → settings-Z3V53A6J.mjs} +7 -7
  88. package/dist/cli/{setup-YLEOL7E7.mjs → setup-GMH4VJS3.mjs} +6 -6
  89. package/dist/cli/{setup-NJCUXN4H.js → setup-ZA234HEY.js} +41 -41
  90. package/dist/cli/skills-6APAQV6W.js +28 -0
  91. package/dist/cli/{skills-WABXOGDA.mjs → skills-7G2RZMYH.mjs} +4 -4
  92. package/dist/cli/{tools-XQCGEX5J.mjs → tools-7EE4BGXO.mjs} +6 -6
  93. package/dist/cli/tools-EPYUFWPB.js +38 -0
  94. package/dist/cli/update-FPVX3XJC.js +28 -0
  95. package/dist/cli/{update-3W2NBTST.mjs → update-O5QEWS7K.mjs} +6 -6
  96. package/dist/cli/{workflow-GKFGFQ4D.mjs → workflow-KE54RUTR.mjs} +5 -5
  97. package/dist/cli/{workflow-NUVM7LQR.js → workflow-M5PAAW3O.js} +15 -15
  98. package/dist/index.js +6 -6
  99. package/dist/index.mjs +3 -3
  100. package/dist/install-integrity.json +75 -74
  101. package/dist/release.d.mts +1 -1
  102. package/dist/release.d.ts +1 -1
  103. package/dist/release.js +2 -2
  104. package/dist/release.mjs +1 -1
  105. package/package.json +1 -1
  106. package/dist/cli/auth-MKFIE6KN.js +0 -31
  107. package/dist/cli/client-NITVYMZK.js +0 -20
  108. package/dist/cli/git-B3L74RFF.js +0 -23
  109. package/dist/cli/play-EONETVYE.js +0 -152
  110. package/dist/cli/search-XK2KJXQG.js +0 -26
  111. package/dist/cli/skills-VNAPDBFA.js +0 -28
  112. package/dist/cli/tools-XF5EQO4O.js +0 -38
  113. package/dist/cli/update-3TQMEJO5.js +0 -28
@@ -10,11 +10,13 @@ import { randomUUID } from 'node:crypto';
10
10
  import {
11
11
  DEFAULT_ADVISORY_PROVIDER_MAX_RPS,
12
12
  DEFAULT_UNKNOWN_PROVIDER_COLD_START_RPS,
13
+ DEFAULT_UNKNOWN_PROVIDER_FAST_RECOVERY_RPS,
13
14
  noopPacingPermit,
14
15
  PROVIDER_AIMD_COOLDOWN_CAP_MS,
15
16
  type PacingPermit,
16
17
  type ProviderConcurrencyGrant,
17
18
  type ProviderPacingGrant,
19
+ type ProviderPermitValidationOptions,
18
20
  type PacingRule,
19
21
  type RateStateBackend,
20
22
  type RateStateBackendKind,
@@ -105,7 +107,11 @@ function abortReason(signal: AbortSignal): Error {
105
107
  : new Error('Rate-state acquire aborted.');
106
108
  }
107
109
 
108
- function sleepWithAbort(ms: number, signal?: AbortSignal): Promise<void> {
110
+ function sleepWithAbort(
111
+ ms: number,
112
+ signal?: AbortSignal,
113
+ interrupted?: Promise<void>,
114
+ ): Promise<void> {
109
115
  if (signal?.aborted) return Promise.reject(abortReason(signal));
110
116
  return new Promise((resolve, reject) => {
111
117
  const cleanup = () => signal?.removeEventListener('abort', onAbort);
@@ -120,6 +126,11 @@ function sleepWithAbort(ms: number, signal?: AbortSignal): Promise<void> {
120
126
  };
121
127
  signal?.addEventListener('abort', onAbort, { once: true });
122
128
  if (signal?.aborted) onAbort();
129
+ void interrupted?.then(() => {
130
+ clearTimeout(timer);
131
+ cleanup();
132
+ resolve();
133
+ });
123
134
  });
124
135
  }
125
136
 
@@ -129,6 +140,7 @@ function sleepWithAbort(ms: number, signal?: AbortSignal): Promise<void> {
129
140
  * hold more than a moment of a shared provider window.
130
141
  */
131
142
  const RATE_BLOCK_HORIZON_MS = 1_000;
143
+ const UNKNOWN_PROVIDER_RECOVERY_BLOCK_HORIZON_MS = 500;
132
144
  const STORE_FAILURE_RETRY_DELAYS_MS = [100, 250] as const;
133
145
  /**
134
146
  * Jitter fraction applied to a server-supplied `waitMs` so a synchronized wave
@@ -154,6 +166,7 @@ interface LeasedBlock {
154
166
  concurrencyGrant: ProviderConcurrencyGrant | null,
155
167
  pacingGrant: ProviderPacingGrant | null,
156
168
  scopeRetryAtMsAtGrant: number,
169
+ dispatchValidationSupported: boolean,
157
170
  ]
158
171
  >;
159
172
  rateScopeToken: string | null;
@@ -209,7 +222,11 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
209
222
  private readonly port: AppRuntimeRatePort;
210
223
  private readonly schedulerSchema: string | null;
211
224
  private readonly now: () => number;
212
- private readonly sleep: (ms: number, signal?: AbortSignal) => Promise<void>;
225
+ private readonly sleep: (
226
+ ms: number,
227
+ signal?: AbortSignal,
228
+ interrupted?: Promise<void>,
229
+ ) => Promise<void>;
213
230
  private readonly jitter: () => number;
214
231
  private readonly onStoreFailure: (info: {
215
232
  bucketId: string;
@@ -218,15 +235,33 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
218
235
  private readonly blocks = new Map<string, LeasedBlock>();
219
236
  private readonly refills = new Map<string, Promise<void>>();
220
237
  private readonly pendingReleases = new Map<string, PendingRelease>();
238
+ private readonly leaseReleaseSignals = new Map<
239
+ string,
240
+ { generation: number; waiters: Set<() => void> }
241
+ >();
221
242
  private readonly validationFlights = new Map<
222
243
  string,
223
244
  { generation: number; promise: Promise<PlayRunnerRateStateValidateResult> }
224
245
  >();
225
246
  private readonly validationGenerations = new Map<string, number>();
247
+ private readonly validatedDispatchCadences = new Map<
248
+ string,
249
+ {
250
+ atMs: number;
251
+ lastAtMs: number;
252
+ used: number;
253
+ burstSize: number;
254
+ spacingMs: number;
255
+ }
256
+ >();
226
257
  private readonly validationScopes = new Set<string>();
227
258
  private readonly pendingPenaltyWrites = new Map<string, Promise<void>>();
228
259
  private readonly serverClockOffsetsMs = new Map<string, number>();
229
260
  private readonly localProviderRetryAtMs = new Map<string, number>();
261
+ private readonly scheduledPermitWaits = new Map<
262
+ string,
263
+ Set<{ retryAtMsAtGrant: number; invalidate: () => void }>
264
+ >();
230
265
  /**
231
266
  * Last locally synthesized dispatch slot per lane. During a rolling deploy,
232
267
  * a gateway that omits its global schedule can return another block while
@@ -240,6 +275,8 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
240
275
  * instead of a fixed floor, so a 200-row map fans into ~one round trip.
241
276
  */
242
277
  private readonly waiters = new Map<string, number>();
278
+ /** Due retries precede fresh draws; already assigned tickets keep ownership. */
279
+ private readonly retryAdmissions = new Map<string, Set<Promise<void>>>();
243
280
  /** Last-known server cooldown per bucket (surfaced for PROVIDER_EXHAUSTED). */
244
281
  private readonly cooldowns = new Map<string, RateStateCooldown>();
245
282
  private readonly providerStalls = new Map<
@@ -396,6 +433,7 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
396
433
  [...rules].sort((a, b) => a.ruleId.localeCompare(b.ruleId)),
397
434
  ),
398
435
  );
436
+ if (this.retryAdmissions.get(laneKey)?.size) return null;
399
437
  const block = this.liveBlock(bucketId, laneKey);
400
438
  if (!block) return null;
401
439
  const next = block.permits[0];
@@ -410,6 +448,75 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
410
448
  rules: readonly PacingRule[];
411
449
  signal?: AbortSignal;
412
450
  yieldProviderHolds?: boolean;
451
+ retryPriority?: boolean;
452
+ }): Promise<PacingPermit> {
453
+ if (!input.retryPriority || input.rules.length === 0)
454
+ return this.acquireFromLane(input);
455
+ const ordered = [...input.rules].sort((a, b) =>
456
+ a.ruleId.localeCompare(b.ruleId),
457
+ );
458
+ const laneKey = rateLaneKey(input.bucketId, rulesSignature(ordered));
459
+ let complete!: () => void;
460
+ const pending = new Promise<void>((resolve) => {
461
+ complete = resolve;
462
+ });
463
+ const retries =
464
+ this.retryAdmissions.get(laneKey) ?? new Set<Promise<void>>();
465
+ retries.add(pending);
466
+ this.retryAdmissions.set(laneKey, retries);
467
+ try {
468
+ return await this.acquireFromLane(input);
469
+ } finally {
470
+ retries.delete(pending);
471
+ if (retries.size === 0) this.retryAdmissions.delete(laneKey);
472
+ complete();
473
+ }
474
+ }
475
+
476
+ private async waitForRetryAdmissions(
477
+ laneKey: string,
478
+ bucketId: string,
479
+ acquireStartedAt: number,
480
+ signal?: AbortSignal,
481
+ ): Promise<void> {
482
+ const retries = this.retryAdmissions.get(laneKey);
483
+ if (!retries?.size) return;
484
+ const remainingMs = Math.max(
485
+ 0,
486
+ PROVIDER_ADMISSION_WAIT_BUDGET_MS - (this.now() - acquireStartedAt),
487
+ );
488
+ await new Promise<void>((resolve, reject) => {
489
+ const onAbort = () => finish(abortReason(signal!));
490
+ const timer = setTimeout(
491
+ () =>
492
+ finish(
493
+ new ProviderAdmissionWaitTimeoutError({
494
+ provider: providerNameFromBucketId(bucketId),
495
+ waitBudgetMs: PROVIDER_ADMISSION_WAIT_BUDGET_MS,
496
+ }),
497
+ ),
498
+ remainingMs,
499
+ );
500
+ timer.unref();
501
+ const finish = (error?: unknown) => {
502
+ clearTimeout(timer);
503
+ signal?.removeEventListener('abort', onAbort);
504
+ if (error !== undefined) reject(error);
505
+ else resolve();
506
+ };
507
+ signal?.addEventListener('abort', onAbort, { once: true });
508
+ if (signal?.aborted) onAbort();
509
+ else void Promise.all([...retries]).then(() => finish());
510
+ });
511
+ }
512
+
513
+ private async acquireFromLane(input: {
514
+ bucketId: string;
515
+ rateScopeToken?: string | null;
516
+ rules: readonly PacingRule[];
517
+ signal?: AbortSignal;
518
+ yieldProviderHolds?: boolean;
519
+ retryPriority?: boolean;
413
520
  }): Promise<PacingPermit> {
414
521
  const { bucketId, rules, signal } = input;
415
522
  if (rules.length === 0) return noopPacingPermit();
@@ -439,6 +546,19 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
439
546
  ? signal.reason
440
547
  : new Error('Rate-state acquire aborted.');
441
548
  }
549
+ if (!input.retryPriority && this.retryAdmissions.get(laneKey)?.size) {
550
+ await this.waitForRetryAdmissions(
551
+ laneKey,
552
+ bucketId,
553
+ acquireStartedAt,
554
+ signal,
555
+ );
556
+ // A new retry may have joined while the previous preferred call drew
557
+ // its permit. Never let a fresh continuation consume that next grant.
558
+ if (this.retryAdmissions.get(laneKey)?.size) continue;
559
+ this.throwIfPacerExhausted(bucketId, input.yieldProviderHolds === true);
560
+ signal?.throwIfAborted();
561
+ }
442
562
  const localPermit = this.drawFromBlock(bucketId, laneKey);
443
563
  if (localPermit) {
444
564
  const waitMs = localPermit.scheduledAtMs - this.now();
@@ -458,7 +578,20 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
458
578
  });
459
579
  }
460
580
  try {
461
- if (waitMs > 0) await this.sleep(waitMs, signal);
581
+ if (
582
+ waitMs > 0 &&
583
+ !(await this.waitForScheduledPermit(
584
+ bucketId,
585
+ localPermit.retryAtMsAtGrant,
586
+ waitMs,
587
+ signal,
588
+ ))
589
+ ) {
590
+ await this.cancelUndispatchedPermit(localPermit);
591
+ const pendingPenalty = this.pendingPenaltyWrites.get(bucketId);
592
+ if (pendingPenalty) await pendingPenalty;
593
+ continue;
594
+ }
462
595
  } catch (error) {
463
596
  await this.cancelUndispatchedPermit(localPermit);
464
597
  throw error;
@@ -468,7 +601,8 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
468
601
  throw abortReason(signal);
469
602
  }
470
603
  if (
471
- this.now() - acquireStartedAt >= PROVIDER_ADMISSION_WAIT_BUDGET_MS
604
+ this.now() - acquireStartedAt >=
605
+ PROVIDER_ADMISSION_WAIT_BUDGET_MS
472
606
  ) {
473
607
  await this.cancelUndispatchedPermit(localPermit);
474
608
  throw new ProviderAdmissionWaitTimeoutError({
@@ -532,12 +666,24 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
532
666
  // (10 rps declared, 100 rps learned) to ten permits per round trip, which on
533
667
  // a hot map is a round trip per permit. The store's own token floor, not
534
668
  // this number, is what bounds the grant.
535
- const horizonMs = Math.min(RATE_BLOCK_HORIZON_MS, leasedBlockTtlMs(rules));
669
+ const learnedRps = this.effectiveRps.get(laneKey);
670
+ const recoveringUnknownProvider =
671
+ learnedRps !== undefined &&
672
+ learnedRps < DEFAULT_UNKNOWN_PROVIDER_FAST_RECOVERY_RPS &&
673
+ rules.some((rule) => rule.ruleId === 'default:provider-scope') &&
674
+ rules.every((rule) => rule.ruleId.startsWith('default:'));
675
+ // Below recovered capacity, the next clean observation can double the
676
+ // pace after 500ms. Do not reserve a full second at the old slower pace:
677
+ // that schedule debt would delay grants even after learning succeeds.
678
+ const horizonMs = Math.min(
679
+ recoveringUnknownProvider
680
+ ? UNKNOWN_PROVIDER_RECOVERY_BLOCK_HORIZON_MS
681
+ : RATE_BLOCK_HORIZON_MS,
682
+ leasedBlockTtlMs(rules),
683
+ );
536
684
  const rateBudget = Math.max(
537
685
  1,
538
- Math.floor(
539
- ((this.effectiveRps.get(laneKey) ?? configuredRps) * horizonMs) / 1_000,
540
- ),
686
+ Math.floor(((learnedRps ?? configuredRps) * horizonMs) / 1_000),
541
687
  );
542
688
  if (!Number.isFinite(rateBudget) || rateBudget <= 0) return floor;
543
689
  const waiters = this.waiters.get(laneKey) ?? 1;
@@ -596,6 +742,16 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
596
742
  return;
597
743
  }
598
744
  const existing = this.cooldowns.get(bucketId);
745
+ // Expiry ends the wait, not the hold's fence. An acquire may discover a
746
+ // remote throttle after its deadline; wake tickets minted before it now.
747
+ if (claimedRetryAtMs > (this.localProviderRetryAtMs.get(bucketId) ?? 0)) {
748
+ this.recordLocalProviderRetryAt(bucketId, claimedRetryAtMs);
749
+ this.validationGenerations.set(
750
+ bucketId,
751
+ (this.validationGenerations.get(bucketId) ?? 0) + 1,
752
+ );
753
+ this.validationFlights.delete(bucketId);
754
+ }
599
755
  this.cooldowns.set(bucketId, {
600
756
  coolUntilMs: Math.max(
601
757
  existing && existing.coolUntilMs > now ? existing.coolUntilMs : 0,
@@ -612,6 +768,50 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
612
768
  });
613
769
  }
614
770
 
771
+ private recordLocalProviderRetryAt(
772
+ bucketId: string,
773
+ retryAtMs: number,
774
+ ): void {
775
+ if (retryAtMs <= (this.localProviderRetryAtMs.get(bucketId) ?? 0)) return;
776
+ this.localProviderRetryAtMs.set(bucketId, retryAtMs);
777
+ for (const waiting of this.scheduledPermitWaits.get(bucketId) ?? []) {
778
+ if (waiting.retryAtMsAtGrant < retryAtMs) waiting.invalidate();
779
+ }
780
+ }
781
+
782
+ /** A newer hold invalidates old tickets, including their obsolete sleep. */
783
+ private async waitForScheduledPermit(
784
+ bucketId: string,
785
+ retryAtMsAtGrant: number,
786
+ waitMs: number,
787
+ signal?: AbortSignal,
788
+ ): Promise<boolean> {
789
+ let invalidated = false;
790
+ let invalidate!: () => void;
791
+ const interrupted = new Promise<void>((resolve) => {
792
+ invalidate = () => {
793
+ invalidated = true;
794
+ resolve();
795
+ };
796
+ });
797
+ const waiting = { retryAtMsAtGrant, invalidate };
798
+ const waiters = this.scheduledPermitWaits.get(bucketId) ?? new Set();
799
+ waiters.add(waiting);
800
+ this.scheduledPermitWaits.set(bucketId, waiters);
801
+ try {
802
+ if ((this.localProviderRetryAtMs.get(bucketId) ?? 0) > retryAtMsAtGrant)
803
+ return false;
804
+ await Promise.race([
805
+ this.sleep(waitMs, signal, interrupted),
806
+ interrupted,
807
+ ]);
808
+ return !invalidated;
809
+ } finally {
810
+ waiters.delete(waiting);
811
+ if (waiters.size === 0) this.scheduledPermitWaits.delete(bucketId);
812
+ }
813
+ }
814
+
615
815
  /**
616
816
  * Revalidate a cached ticket against the shared provider hold. Concurrent
617
817
  * rows in this runner share one in-flight read per provider bucket; completed
@@ -622,6 +822,7 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
622
822
  bucketId: string,
623
823
  rateScopeToken: string | null,
624
824
  scopeRetryAtMsAtGrant: number,
825
+ dispatchValidationAtGateway = false,
625
826
  ): Promise<boolean> {
626
827
  const validate = this.port.validate;
627
828
  if (!validate || !this.validationScopes.has(bucketId)) return true;
@@ -638,6 +839,18 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
638
839
  return false;
639
840
  }
640
841
 
842
+ // Integration execute carries the signed scope and grant snapshot to
843
+ // this receiver's existing post-queue validator. A preceding worker RPC
844
+ // only adds another admission round trip; it cannot make that later
845
+ // check fresher. Direct HTTP and pre-expansion receivers retain the read.
846
+ if (dispatchValidationAtGateway && rateScopeToken) {
847
+ if (localRetryAtMs > scopeRetryAtMsAtGrant) {
848
+ this.drainBlocksForBucket(bucketId, localRetryAtMs);
849
+ return false;
850
+ }
851
+ return true;
852
+ }
853
+
641
854
  const generation = this.validationGenerations.get(bucketId) ?? 0;
642
855
  let flight = this.validationFlights.get(bucketId);
643
856
  if (!flight || flight.generation !== generation) {
@@ -664,7 +877,11 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
664
877
  }
665
878
  if (!result.allowed) {
666
879
  const retryAtMs = result.retryAtMs;
667
- this.localProviderRetryAtMs.set(bucketId, retryAtMs);
880
+ if (retryAtMs > (this.localProviderRetryAtMs.get(bucketId) ?? 0)) {
881
+ this.validationGenerations.set(bucketId, generation + 1);
882
+ this.validationFlights.delete(bucketId);
883
+ }
884
+ this.recordLocalProviderRetryAt(bucketId, retryAtMs);
668
885
  this.drainBlocksForBucket(bucketId);
669
886
  this.recordCooldown(bucketId, {
670
887
  coolUntilMs: Math.min(
@@ -676,13 +893,12 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
676
893
  });
677
894
  return false;
678
895
  }
679
- if (localRetryAtMs <= result.serverNowMs) {
680
- this.localProviderRetryAtMs.delete(bucketId);
681
- }
682
896
  // The hold's deadline is monotonic. Even after it expires, a ticket
683
897
  // minted before that hold belongs to the old pace and must be replaced.
684
898
  // Preserve newer tickets another waiter may already have acquired.
685
899
  if (result.retryAtMs > scopeRetryAtMsAtGrant) {
900
+ this.validationGenerations.set(bucketId, generation + 1);
901
+ this.validationFlights.delete(bucketId);
686
902
  this.drainBlocksForBucket(bucketId, result.retryAtMs);
687
903
  return false;
688
904
  }
@@ -781,7 +997,25 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
781
997
  await Promise.resolve();
782
998
  if (signal?.aborted) return;
783
999
  }
1000
+ // A legacy receiver cannot revalidate a grant at dispatch. Persist the
1001
+ // known hold before asking the store for any replacement, including a
1002
+ // newer penalty registered while an earlier one was completing.
1003
+ const pendingPenalty = this.pendingPenaltyWrites.get(bucketId);
1004
+ if (pendingPenalty) {
1005
+ await pendingPenalty;
1006
+ continue;
1007
+ }
784
1008
  try {
1009
+ let releaseSignal = this.leaseReleaseSignals.get(bucketId);
1010
+ if (
1011
+ !releaseSignal &&
1012
+ ordered.some((rule) => rule.maxConcurrency != null)
1013
+ ) {
1014
+ releaseSignal = { generation: 0, waiters: new Set() };
1015
+ this.leaseReleaseSignals.set(bucketId, releaseSignal);
1016
+ }
1017
+ // A completed release can make an in-flight zero-grant response stale.
1018
+ const releaseGeneration = releaseSignal?.generation;
785
1019
  const successKey = pendingSuccessKey(bucketId, ordered);
786
1020
  const feedback = this.snapshotSuccessFeedback(successKey);
787
1021
  const response = await this.port.acquire({
@@ -854,7 +1088,32 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
854
1088
  // and single-flight another refill if still empty. ±10% jitter so a
855
1089
  // synchronized wave re-probes at spread-out times; capped by
856
1090
  // MAX_ACQUIRE_SLEEP_MS.
857
- await this.sleep(this.jitteredWaitMs(response.waitMs));
1091
+ const waitMs = this.jitteredWaitMs(response.waitMs);
1092
+ const serverNowMs = response.serverNowMs ?? this.serverNow(bucketId);
1093
+ const authoritativeHold =
1094
+ (response.claimedRetryAtMs ?? 0) > serverNowMs ||
1095
+ (response.coolUntilMs ?? 0) > serverNowMs;
1096
+ if (!releaseSignal || authoritativeHold) {
1097
+ await this.sleep(waitMs);
1098
+ } else {
1099
+ // Lease expiry predicts when a saturated scope can admit again;
1100
+ // our own confirmed release is fresher evidence. Re-query the store,
1101
+ // which still owns rate/concurrency admission and provider holds.
1102
+ let wake!: () => void;
1103
+ const released = new Promise<void>((resolve) => {
1104
+ wake = resolve;
1105
+ });
1106
+ releaseSignal.waiters.add(wake);
1107
+ try {
1108
+ if (releaseSignal.generation !== releaseGeneration) return;
1109
+ await Promise.race([
1110
+ this.sleep(waitMs, undefined, released),
1111
+ released,
1112
+ ]);
1113
+ } finally {
1114
+ releaseSignal.waiters.delete(wake);
1115
+ }
1116
+ }
858
1117
  return;
859
1118
  } catch (error) {
860
1119
  // A provider hold is not a store failure. Preserve its deadline for
@@ -898,6 +1157,7 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
898
1157
  serverNowMs?: unknown;
899
1158
  scheduledAtMs?: unknown;
900
1159
  claimedRetryAtMs?: number;
1160
+ dispatchValidationSupported?: boolean;
901
1161
  },
902
1162
  ): void {
903
1163
  const hasConcurrency = ordered.some((rule) => rule.maxConcurrency != null);
@@ -1103,6 +1363,7 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
1103
1363
  concurrencyGrants[index] ?? null,
1104
1364
  pacingGrant,
1105
1365
  Math.max(0, response.claimedRetryAtMs ?? 0),
1366
+ response.dispatchValidationSupported === true,
1106
1367
  ],
1107
1368
  ),
1108
1369
  rulesKey,
@@ -1145,23 +1406,6 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
1145
1406
  });
1146
1407
  }
1147
1408
 
1148
- this.validationGenerations.set(
1149
- input.bucketId,
1150
- (this.validationGenerations.get(input.bucketId) ?? 0) + 1,
1151
- );
1152
- this.validationFlights.delete(input.bucketId);
1153
- this.localProviderRetryAtMs.set(
1154
- input.bucketId,
1155
- Math.max(this.localProviderRetryAtMs.get(input.bucketId) ?? 0, retryAtMs),
1156
- );
1157
- this.recordCooldown(input.bucketId, {
1158
- coolUntilMs: Math.min(
1159
- retryAtMs,
1160
- this.serverNow(input.bucketId) + PROVIDER_AIMD_COOLDOWN_CAP_MS,
1161
- ),
1162
- claimedRetryAtMs: retryAtMs,
1163
- });
1164
- this.drainBlocksForBucket(input.bucketId);
1165
1409
  const successKey = pendingSuccessKey(input.bucketId, input.rules ?? []);
1166
1410
  const feedback = this.snapshotSuccessFeedback(successKey);
1167
1411
  const previousPenalty = this.pendingPenaltyWrites.get(input.bucketId);
@@ -1185,6 +1429,21 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
1185
1429
  }),
1186
1430
  ]).then(() => undefined);
1187
1431
  this.pendingPenaltyWrites.set(input.bucketId, penaltyWrite);
1432
+ // Register the durable intent before invalidation wakes assigned callers.
1433
+ this.validationGenerations.set(
1434
+ input.bucketId,
1435
+ (this.validationGenerations.get(input.bucketId) ?? 0) + 1,
1436
+ );
1437
+ this.validationFlights.delete(input.bucketId);
1438
+ this.recordLocalProviderRetryAt(input.bucketId, retryAtMs);
1439
+ this.recordCooldown(input.bucketId, {
1440
+ coolUntilMs: Math.min(
1441
+ retryAtMs,
1442
+ this.serverNow(input.bucketId) + PROVIDER_AIMD_COOLDOWN_CAP_MS,
1443
+ ),
1444
+ claimedRetryAtMs: retryAtMs,
1445
+ });
1446
+ this.drainBlocksForBucket(input.bucketId);
1188
1447
  try {
1189
1448
  await penaltyWrite;
1190
1449
  } catch (error) {
@@ -1305,6 +1564,7 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
1305
1564
  concurrencyGrant,
1306
1565
  pacingGrant,
1307
1566
  scopeRetryAtMsAtGrant,
1567
+ dispatchValidationSupported,
1308
1568
  ] = scheduled;
1309
1569
  let settled = false;
1310
1570
  const refund = async (): Promise<void> => {
@@ -1329,17 +1589,108 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
1329
1589
  bucketId,
1330
1590
  rateScopeToken,
1331
1591
  retryAtMsAtGrant: scopeRetryAtMsAtGrant,
1592
+ ...(dispatchValidationSupported
1593
+ ? { dispatchValidationSupported: true }
1594
+ : {}),
1332
1595
  }
1333
1596
  : null,
1334
- validate: async () => {
1597
+ validate: async (
1598
+ signal?: AbortSignal,
1599
+ options?: ProviderPermitValidationOptions,
1600
+ ) => {
1335
1601
  try {
1336
- return await this.validateProviderPermit(
1337
- bucketId,
1338
- rateScopeToken,
1339
- scopeRetryAtMsAtGrant,
1340
- );
1602
+ const startedAtMs = this.now();
1603
+ while (true) {
1604
+ signal?.throwIfAborted();
1605
+ if (settled) return false;
1606
+ if (
1607
+ !(await this.validateProviderPermit(
1608
+ bucketId,
1609
+ rateScopeToken,
1610
+ scopeRetryAtMsAtGrant,
1611
+ dispatchValidationSupported &&
1612
+ options?.dispatchValidationAtGateway === true,
1613
+ ))
1614
+ )
1615
+ return false;
1616
+ const generation = this.validationGenerations.get(bucketId) ?? 0;
1617
+ while (true) {
1618
+ signal?.throwIfAborted();
1619
+ if (settled) return false;
1620
+ if (
1621
+ (this.validationGenerations.get(bucketId) ?? 0) !== generation
1622
+ )
1623
+ break;
1624
+ if (
1625
+ (this.localProviderRetryAtMs.get(bucketId) ?? 0) >
1626
+ this.serverNow(bucketId)
1627
+ )
1628
+ return false;
1629
+ const targetRequestsPerSecond =
1630
+ this.effectiveRps.get(laneKey) ??
1631
+ pacingGrant?.targetRequestsPerSecond;
1632
+ if (targetRequestsPerSecond == null) return true;
1633
+ // A delayed shared validation must not collapse reserved slots
1634
+ // into a burst. Advance this cursor only when a send is ready;
1635
+ // sleeping/cancelled callers never reserve future positions.
1636
+ // The database owns admission and the app rechecks provider
1637
+ // holds at arrival. This local fence only delays spent permits.
1638
+ const spacingMs = Math.max(
1639
+ 1,
1640
+ Math.ceil(1_000 / targetRequestsPerSecond),
1641
+ );
1642
+ const burstSize = Math.max(
1643
+ 1,
1644
+ Math.floor(targetRequestsPerSecond / 100),
1645
+ );
1646
+ const previous = this.validatedDispatchCadences.get(bucketId);
1647
+ const nowMs = this.now();
1648
+ const sameCadence =
1649
+ previous?.spacingMs === spacingMs &&
1650
+ previous.burstSize === burstSize;
1651
+ // A fresh authoritative rate also governs already prepared
1652
+ // permits. Carry debt from the last send when the rate changes;
1653
+ // a staggered burst's first send can precede it by an interval.
1654
+ const previousBoundaryMs = previous
1655
+ ? (sameCadence ? previous.atMs : previous.lastAtMs) +
1656
+ spacingMs * burstSize
1657
+ : nowMs;
1658
+ const inBurst =
1659
+ sameCadence &&
1660
+ previous.used < burstSize &&
1661
+ nowMs < previousBoundaryMs;
1662
+ const waitMs = inBurst
1663
+ ? 0
1664
+ : Math.max(0, previousBoundaryMs - nowMs);
1665
+ if (
1666
+ nowMs - startedAtMs + waitMs >=
1667
+ PROVIDER_ADMISSION_WAIT_BUDGET_MS
1668
+ ) {
1669
+ throw new ProviderAdmissionWaitTimeoutError({
1670
+ provider: providerNameFromBucketId(bucketId),
1671
+ waitBudgetMs: PROVIDER_ADMISSION_WAIT_BUDGET_MS,
1672
+ });
1673
+ }
1674
+ if (waitMs > 0) {
1675
+ await this.sleep(waitMs, signal);
1676
+ continue;
1677
+ }
1678
+ this.validatedDispatchCadences.set(bucketId, {
1679
+ atMs: inBurst ? previous.atMs : nowMs,
1680
+ lastAtMs: nowMs,
1681
+ used: inBurst ? previous.used + 1 : 1,
1682
+ burstSize,
1683
+ spacingMs,
1684
+ });
1685
+ return true;
1686
+ }
1687
+ }
1341
1688
  } catch (error) {
1342
- if (error instanceof RateStateValidationUnavailableError) {
1689
+ if (
1690
+ signal?.aborted ||
1691
+ error instanceof ProviderAdmissionWaitTimeoutError ||
1692
+ error instanceof RateStateValidationUnavailableError
1693
+ ) {
1343
1694
  throw error;
1344
1695
  }
1345
1696
  const normalized = normalizeError(error);
@@ -1450,6 +1801,7 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
1450
1801
  permit: PacingPermit;
1451
1802
  scheduledAtMs: number;
1452
1803
  reservationId: string | null;
1804
+ retryAtMsAtGrant: number;
1453
1805
  } | null {
1454
1806
  const block = this.liveBlock(bucketId, laneKey);
1455
1807
  if (!block) return null;
@@ -1461,6 +1813,15 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
1461
1813
  permit: this.permitFor(bucketId, laneKey, block, scheduled),
1462
1814
  scheduledAtMs: scheduled[0],
1463
1815
  reservationId: scheduled[2],
1816
+ // Legacy gateways do not attest a hold snapshot. Their newly assigned
1817
+ // tickets retain existing behavior after an expired local hold; a later
1818
+ // local 429 still wakes them. Modern snapshots invalidate stale grants.
1819
+ retryAtMsAtGrant: this.validationScopes.has(bucketId)
1820
+ ? scheduled[5]
1821
+ : Math.max(
1822
+ scheduled[5],
1823
+ this.localProviderRetryAtMs.get(bucketId) ?? 0,
1824
+ ),
1464
1825
  };
1465
1826
  }
1466
1827
 
@@ -1529,6 +1890,7 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
1529
1890
  refundReservationIds: [input.reservationId],
1530
1891
  schedulerSchema: this.schedulerSchema,
1531
1892
  });
1893
+ if (input.leaseIds.length > 0) this.signalLeaseRelease(input.bucketId);
1532
1894
  } catch (error) {
1533
1895
  const normalized = normalizeError(error);
1534
1896
  this.onStoreFailure({
@@ -1582,6 +1944,7 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
1582
1944
  leaseIds,
1583
1945
  schedulerSchema: this.schedulerSchema,
1584
1946
  });
1947
+ this.signalLeaseRelease(pending.bucketId);
1585
1948
  } catch (error) {
1586
1949
  const normalized = normalizeError(error);
1587
1950
  this.pendingReleaseFailure = new Error(
@@ -1610,6 +1973,13 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
1610
1973
  this.pendingReleaseFailure = null;
1611
1974
  throw error;
1612
1975
  }
1976
+
1977
+ private signalLeaseRelease(bucketId: string): void {
1978
+ const signal = this.leaseReleaseSignals.get(bucketId);
1979
+ if (!signal) return;
1980
+ signal.generation += 1;
1981
+ for (const wake of signal.waiters) wake();
1982
+ }
1613
1983
  }
1614
1984
 
1615
1985
  /**