deepline 0.3.323 → 0.3.325

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 (128) hide show
  1. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  2. package/dist/bundling-sources/sdk/src/types.ts +6 -0
  3. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +2 -0
  4. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +454 -129
  5. package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +407 -37
  6. package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +18 -4
  7. package/dist/bundling-sources/shared_libs/play-runtime/governor/rate-state-backend.ts +16 -1
  8. package/dist/bundling-sources/shared_libs/play-runtime/map-execution-frame.ts +6 -2
  9. package/dist/bundling-sources/shared_libs/play-runtime/play-http-request.ts +1 -1
  10. package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +2 -0
  11. package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/gradient2-concurrency-controller.ts +15 -5
  12. package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/provider-capacity-policy.ts +1 -0
  13. package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/provider-concurrency-policy.ts +7 -2
  14. package/dist/bundling-sources/shared_libs/play-runtime/provider-row-retry-queue.ts +65 -0
  15. package/dist/bundling-sources/shared_libs/play-runtime/resource-governor.ts +2 -0
  16. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +7 -1
  17. package/dist/bundling-sources/shared_libs/play-runtime/runtime-actions.ts +1 -0
  18. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +5 -0
  19. package/dist/bundling-sources/shared_libs/play-runtime/runtime-flow.ts +34 -5
  20. package/dist/bundling-sources/shared_libs/play-runtime/runtime-map-row-feed.ts +116 -15
  21. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-scope.ts +59 -6
  22. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts +294 -37
  23. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +73 -0
  24. package/dist/bundling-sources/shared_libs/play-runtime/suspension.ts +2 -0
  25. package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +41 -3
  26. package/dist/bundling-sources/shared_libs/plays/artifact-types.ts +9 -0
  27. package/dist/cli/{auth-4TYSEEPE.mjs → auth-PIZESRPC.mjs} +4 -4
  28. package/dist/cli/auth-RXCD4GOV.js +31 -0
  29. package/dist/cli/{billing-HYX3OWXB.js → billing-DQKFCQV5.js} +70 -70
  30. package/dist/cli/{billing-QDER3JLM.mjs → billing-HSL6VTCM.mjs} +4 -4
  31. package/dist/cli/{chunk-VN433ENM.mjs → chunk-5FHU7TV4.mjs} +2 -2
  32. package/dist/cli/{chunk-ZF6WOSMW.mjs → chunk-5TYU3UIS.mjs} +1 -1
  33. package/dist/cli/{chunk-YY6EASRE.mjs → chunk-6RMKHN7S.mjs} +4 -4
  34. package/dist/cli/{chunk-CXJND6CJ.mjs → chunk-6VZDSBK6.mjs} +2 -2
  35. package/dist/cli/{chunk-JGTV5QNG.mjs → chunk-7MD453WV.mjs} +6 -6
  36. package/dist/cli/{chunk-3OTHDXTV.mjs → chunk-AQUNQJ5Y.mjs} +1 -1
  37. package/dist/cli/{chunk-FNKBVKQX.js → chunk-CV5GDCKN.js} +15 -15
  38. package/dist/cli/{chunk-WOG5IKUF.js → chunk-DLW7X2UK.js} +220 -220
  39. package/dist/cli/{chunk-BC3E7GEW.mjs → chunk-H5TJBRSC.mjs} +5 -5
  40. package/dist/cli/{chunk-LXUY7LRE.js → chunk-HPTQEWCF.js} +60 -60
  41. package/dist/cli/{chunk-GUPKB4UE.js → chunk-HUKWYHBG.js} +2 -2
  42. package/dist/cli/{chunk-BEG62AQW.js → chunk-K7FC5IUU.js} +1 -1
  43. package/dist/cli/{chunk-I7TAO4XH.js → chunk-KZNFYOHX.js} +6 -6
  44. package/dist/cli/{chunk-UMUH2FVZ.js → chunk-MNKDSR2J.js} +6 -6
  45. package/dist/cli/{chunk-UE55ECMI.mjs → chunk-NWBARG3F.mjs} +1 -1
  46. package/dist/cli/{chunk-KLIHE4X5.js → chunk-OOPSX57I.js} +6 -6
  47. package/dist/cli/{chunk-3I4KDUKY.mjs → chunk-OPERWJR6.mjs} +3 -3
  48. package/dist/cli/{chunk-HUQVA5MR.js → chunk-PBNZXZCW.js} +48 -48
  49. package/dist/cli/{chunk-HSZD4VBE.js → chunk-PVDQ55PS.js} +4 -4
  50. package/dist/cli/{chunk-HJN5KR42.mjs → chunk-PWV2M4LW.mjs} +1 -1
  51. package/dist/cli/{chunk-M2WAAIM4.js → chunk-QQ2FG5W3.js} +6 -6
  52. package/dist/cli/{chunk-OQPG3ND3.js → chunk-UYFWTIKF.js} +26 -26
  53. package/dist/cli/{chunk-2HYJDTQT.mjs → chunk-WW4XZIVM.mjs} +2 -2
  54. package/dist/cli/{chunk-FGD2M25K.mjs → chunk-XIVUB36F.mjs} +1 -1
  55. package/dist/cli/{chunk-W2V624T6.mjs → chunk-YE3O2UHM.mjs} +1 -1
  56. package/dist/cli/{chunk-EWFKMQFO.js → chunk-YXRPWMGN.js} +50 -50
  57. package/dist/cli/client-562MULXK.js +20 -0
  58. package/dist/cli/{client-CEX7EHUM.mjs → client-57SR4KVM.mjs} +3 -3
  59. package/dist/cli/{csv-GBPRJ6YU.js → csv-5DJSC6BD.js} +12 -12
  60. package/dist/cli/{csv-LR5KWOIK.mjs → csv-G2FRMOXL.mjs} +3 -3
  61. package/dist/cli/{db-VFKXAHQA.js → db-NVO7P4VO.js} +20 -20
  62. package/dist/cli/{db-4BMCUU34.mjs → db-WWZEXM3N.mjs} +4 -4
  63. package/dist/cli/{describe-VTLU5KX5.mjs → describe-26OMF3A4.mjs} +8 -8
  64. package/dist/cli/{describe-BYWGWALB.js → describe-M26LHDBQ.js} +15 -15
  65. package/dist/cli/{executions-NGREVSY6.mjs → executions-2LLHY3IL.mjs} +4 -4
  66. package/dist/cli/{executions-DU7I6YEU.js → executions-KI47LPKE.js} +6 -6
  67. package/dist/cli/{feedback-PHPAWVHC.js → feedback-2Y26C2BZ.js} +6 -6
  68. package/dist/cli/{feedback-GUY2CCGF.mjs → feedback-O26VL2OF.mjs} +3 -3
  69. package/dist/cli/git-KGPX5YPD.js +23 -0
  70. package/dist/cli/{git-22YV3DFJ.mjs → git-UVPGZN3K.mjs} +4 -4
  71. package/dist/cli/index.js +75 -75
  72. package/dist/cli/index.mjs +31 -31
  73. package/dist/cli/{legacy-noops-V72GCF5R.js → legacy-noops-H6Y3SEVB.js} +7 -7
  74. package/dist/cli/{legacy-noops-YEQDVI6U.mjs → legacy-noops-IK5CH72U.mjs} +4 -4
  75. package/dist/cli/{monitors-MVJBQDQG.js → monitors-JL6TTQCI.js} +85 -85
  76. package/dist/cli/{monitors-QOCAMBRV.mjs → monitors-SFVVMAJT.mjs} +4 -4
  77. package/dist/cli/{org-DMIOMJK6.js → org-OVMAZCQS.js} +60 -60
  78. package/dist/cli/{org-A53XV2UQ.mjs → org-SSDONL57.mjs} +4 -4
  79. package/dist/cli/play-3DB4MSHN.js +152 -0
  80. package/dist/cli/{play-73L74CUW.mjs → play-X6NPACUO.mjs} +7 -7
  81. package/dist/cli/{providers-PEOAZYM6.js → providers-E24JDWSR.js} +7 -7
  82. package/dist/cli/{providers-6LDD5X5G.mjs → providers-ONUBYYY4.mjs} +4 -4
  83. package/dist/cli/{quickstart-PXMC66UG.mjs → quickstart-B7NIXACV.mjs} +3 -3
  84. package/dist/cli/{quickstart-33IPDK2I.js → quickstart-TB7C3DLG.js} +5 -5
  85. package/dist/cli/{search-FJEJDKPA.mjs → search-N3QUIATW.mjs} +5 -5
  86. package/dist/cli/search-NJ637YGQ.js +26 -0
  87. package/dist/cli/{secrets-3SCAG3T7.js → secrets-CPCVFDYB.js} +13 -13
  88. package/dist/cli/{secrets-Q3ZWHE5S.mjs → secrets-V7Z4M67T.mjs} +4 -4
  89. package/dist/cli/{sessions-QJB4LSNZ.js → sessions-E4FIZLQP.js} +5 -5
  90. package/dist/cli/{sessions-RY3SCVN7.mjs → sessions-PDOBKAKO.mjs} +4 -4
  91. package/dist/cli/{settings-VTYJKMI6.mjs → settings-QYHYHZDI.mjs} +7 -7
  92. package/dist/cli/{settings-DLAE6POM.js → settings-UVM2WDE4.js} +74 -74
  93. package/dist/cli/{setup-LM2VWSFE.js → setup-3JFAXQLJ.js} +41 -41
  94. package/dist/cli/{setup-UYQ4V4GP.mjs → setup-4RLAETDW.mjs} +6 -6
  95. package/dist/cli/skills-BYRYG64R.js +28 -0
  96. package/dist/cli/{skills-2OKHLQF7.mjs → skills-ZX2JSWOZ.mjs} +4 -4
  97. package/dist/cli/{tools-F5RYANCG.mjs → tools-EGXCVIPJ.mjs} +6 -6
  98. package/dist/cli/tools-TAWOAY5X.js +38 -0
  99. package/dist/cli/{update-5FDHGSNE.mjs → update-7FEKRQFE.mjs} +6 -6
  100. package/dist/cli/update-H24GEBXM.js +28 -0
  101. package/dist/cli/{workflow-EQMKPCUV.js → workflow-54VCKIG2.js} +15 -15
  102. package/dist/cli/{workflow-TWQH53ZO.mjs → workflow-7V7TTTKK.mjs} +5 -5
  103. package/dist/{client-BV68GMPo.d.ts → client-Bb4Jjzmj.d.ts} +7 -1
  104. package/dist/{client-D9ACjiRC.d.mts → client-COa-3GHK.d.mts} +7 -1
  105. package/dist/{compiler-manifest-BXcZ4P4W.d.mts → compiler-manifest-CsnWj-AK.d.mts} +6 -0
  106. package/dist/{compiler-manifest-BXcZ4P4W.d.ts → compiler-manifest-CsnWj-AK.d.ts} +6 -0
  107. package/dist/index.d.mts +4 -4
  108. package/dist/index.d.ts +4 -4
  109. package/dist/index.js +6 -6
  110. package/dist/index.mjs +3 -3
  111. package/dist/install-integrity.json +79 -78
  112. package/dist/plays/bundle-play-file.d.mts +2 -2
  113. package/dist/plays/bundle-play-file.d.ts +2 -2
  114. package/dist/plays/run-request.d.mts +2 -2
  115. package/dist/plays/run-request.d.ts +2 -2
  116. package/dist/release.d.mts +1 -1
  117. package/dist/release.d.ts +1 -1
  118. package/dist/release.js +2 -2
  119. package/dist/release.mjs +1 -1
  120. package/package.json +1 -1
  121. package/dist/cli/auth-KWPQ4JVV.js +0 -31
  122. package/dist/cli/client-YMQ2QFV7.js +0 -20
  123. package/dist/cli/git-W5NEI2BP.js +0 -23
  124. package/dist/cli/play-NBFCGFVC.js +0 -152
  125. package/dist/cli/search-FPUK2H6Y.js +0 -26
  126. package/dist/cli/skills-5NH6JX5H.js +0 -28
  127. package/dist/cli/tools-ZPGDY3I2.js +0 -38
  128. package/dist/cli/update-W5KBXMKC.js +0 -28
@@ -10,6 +10,7 @@ import type {
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
  DEFAULT_UNKNOWN_PROVIDER_MIN_RPS,
14
15
  PROVIDER_AIMD_COOLDOWN_CAP_MS,
15
16
  type PacingRule,
@@ -34,6 +35,7 @@ const RATE_RESERVATION_BUCKET_EXPIRY_INDEX =
34
35
  'runtime_scheduler_rate_reservation_bucket_expiry_idx';
35
36
  const RATE_REQUESTER_TABLE = 'rate_requesters';
36
37
  const RATE_SCOPE_HOLD_TABLE = 'rate_scope_holds';
38
+ const PHYSICAL_DISPATCH_RULE_ID = 'runtime:physical-dispatch';
37
39
 
38
40
  const MIN_CONCURRENCY_WAIT_MS = 10;
39
41
  /**
@@ -53,6 +55,10 @@ const FAIR_SHARE_REQUESTER_IDLE_WINDOWS = 2;
53
55
  const FAIR_SHARE_REQUESTER_CLEANUP_BATCH_SIZE = 64;
54
56
  /** Conservative warm-start width for a previously unobserved provider scope. */
55
57
  const INITIAL_CONCURRENCY_DISCOVERY_LIMIT = 8;
58
+ // Rate spacing cannot bound requests delayed between admission and the
59
+ // physical provider. Unknown scopes discover with at most two shared leases
60
+ // until a productive rate epoch establishes that the provider can sustain more.
61
+ const UNKNOWN_PROVIDER_CONCURRENCY_DISCOVERY_LIMIT = 2;
56
62
  /** A transport batch must stay bounded even when a runner completes huge maps. */
57
63
  const MAX_CONCURRENCY_OBSERVATIONS_PER_ACQUIRE = 4_096;
58
64
  /** Keep retry identities long enough for failed runtime work to recover. */
@@ -68,6 +74,7 @@ const MIN_ADAPTIVE_RPS_FLOOR = 5 / 60;
68
74
  const MIN_NO_PROGRESS_PROBE_RPS = 1 / 60;
69
75
  // Used only to keep wait-time division finite for persisted legacy state.
70
76
  const MIN_SCHEDULE_RPS = 1 / 86_400;
77
+ const PRODUCTIVE_UNKNOWN_PROVIDER_DECREASE_FACTOR = 0.9;
71
78
  /**
72
79
  * Horizon a granted=0 caller is allowed to sleep toward a WHOLE block instead of
73
80
  * a single token. Waiting for one token means one permit per store round trip,
@@ -115,8 +122,8 @@ export type PostgresRateStatePenaltyTelemetry = {
115
122
  * persisting a learned capacity would make every warm acquire look like drift
116
123
  * and re-seed the row. The bucket the grant actually spends is therefore
117
124
  * derived, not stored: one second of `refill_per_sec` before a throttle and
118
- * 100ms of it afterwards. Before a throttle the declared burst is a floor;
119
- * after a throttle only a single probe is the floor.
125
+ * a short bounded slice afterwards. Before a throttle the declared burst is a
126
+ * floor; after a throttle only a single probe is the floor.
120
127
  * Retaining the catalog burst after learning a slow rate lets a fresh runtime
121
128
  * reserve minutes of future slots before another run can register demand.
122
129
  *
@@ -128,12 +135,17 @@ export type PostgresRateStatePenaltyTelemetry = {
128
135
  * calls/s no matter how high `refill_per_sec` climbed.
129
136
  *
130
137
  * The burst stays bounded by the learned rate and `max_rps`. The dispatch
131
- * cursor still spaces every permit at 1/`refill_per_sec`, so provider arrivals
132
- * never exceed the rate in force. The shorter post-throttle horizon limits
133
- * fixed-window boundary overshoot while retaining small batches on fast lanes.
138
+ * cursor still spaces every permit at 1/`refill_per_sec`. The shorter
139
+ * post-throttle horizon limits fixed-window boundary overshoot while retaining
140
+ * small batches on fast lanes.
134
141
  */
135
142
  const ADAPTIVE_BURST_HORIZON_MS = 1_000;
136
143
  const BACKPRESSURED_BURST_HORIZON_MS = 100;
144
+ // Unknown providers recover near 10 RPS, where a 100ms slice leases only one
145
+ // permit. A shared admission round trip over 100ms would then cap actual work
146
+ // below the recovered target. Lease half a second of scheduled permits after
147
+ // the first probe; their dispatch times and the final shared hold still apply.
148
+ const UNKNOWN_PROVIDER_BACKPRESSURED_BURST_HORIZON_MS = 500;
137
149
 
138
150
  /**
139
151
  * Effective (spendable) bucket capacity for a rate-row alias. Derived per
@@ -142,7 +154,11 @@ const BACKPRESSURED_BURST_HORIZON_MS = 100;
142
154
  function effectiveCapacitySql(
143
155
  alias: string,
144
156
  refillColumn = 'refill_per_sec',
157
+ unknownProvider = false,
145
158
  ): string {
159
+ const backpressuredBurstHorizonMs = unknownProvider
160
+ ? UNKNOWN_PROVIDER_BACKPRESSURED_BURST_HORIZON_MS
161
+ : BACKPRESSURED_BURST_HORIZON_MS;
146
162
  // After a provider 429, Retry-After marks the earliest safe probe time; it
147
163
  // does not authorize banking a full rate-sized burst while the hold runs.
148
164
  // rate_observation_started_at_ms stays NULL until the first post-hold grant,
@@ -158,7 +174,7 @@ function effectiveCapacitySql(
158
174
  ELSE GREATEST(
159
175
  CASE WHEN ${alias}.seen_backpressure OR ${alias}.${refillColumn} < ${alias}.base_refill_per_sec THEN 1 ELSE ${alias}.capacity END,
160
176
  ${alias}.${refillColumn} * CASE
161
- WHEN ${alias}.seen_backpressure THEN ${BACKPRESSURED_BURST_HORIZON_MS}
177
+ WHEN ${alias}.seen_backpressure THEN ${backpressuredBurstHorizonMs}
162
178
  ELSE ${ADAPTIVE_BURST_HORIZON_MS}
163
179
  END / 1000.0
164
180
  )
@@ -753,6 +769,7 @@ async function seedBucketRows(
753
769
  AND existing.capacity = sc.capacity
754
770
  ) AS unlearned_unknown_provider
755
771
  FROM ${bucketTableName(input)} WHERE bucket_id = $1
772
+ AND rule_id <> '${PHYSICAL_DISPATCH_RULE_ID}'
756
773
  ), seed_scope_hold AS (
757
774
  -- Preserve actual holds, but never import poisoned per-rule learning.
758
775
  -- A legacy throttled scope restarts at at most one probe/second.
@@ -1279,6 +1296,9 @@ async function acquirePureRate(
1279
1296
  requested: number,
1280
1297
  ): Promise<InternalAcquireOutcome> {
1281
1298
  const ruleIds = rules.map((rule) => rule.ruleId);
1299
+ const unknownProvider = rules.some(
1300
+ (rule) => rule.ruleId === 'default:provider-scope',
1301
+ );
1282
1302
  const requesterId = normalizeRequesterId(input);
1283
1303
  const fair = requesterId
1284
1304
  ? fairShareSql(input, {
@@ -1325,6 +1345,7 @@ async function acquirePureRate(
1325
1345
  scopeHoldTableName(input),
1326
1346
  '$5',
1327
1347
  minimumRateSqlForRules(rules),
1348
+ unknownProvider,
1328
1349
  )},
1329
1350
  reclaim_reservations AS (
1330
1351
  DELETE FROM ${reservationTableName(input)}
@@ -1374,10 +1395,10 @@ async function acquirePureRate(
1374
1395
  -- Spendable burst: one second of the rate in force, floored at the
1375
1396
  -- declared window burst. The normalized row is what this acquire
1376
1397
  -- spends; the stored row remains available for drift reporting.
1377
- ${effectiveCapacitySql('normalized')} AS effective_capacity,
1378
- ${effectiveCapacitySql('normalized', 'next_refill_per_sec')} AS scheduled_capacity,
1398
+ ${effectiveCapacitySql('normalized', 'refill_per_sec', unknownProvider)} AS effective_capacity,
1399
+ ${effectiveCapacitySql('normalized', 'next_refill_per_sec', unknownProvider)} AS scheduled_capacity,
1379
1400
  LEAST(
1380
- ${effectiveCapacitySql('normalized')},
1401
+ ${effectiveCapacitySql('normalized', 'refill_per_sec', unknownProvider)},
1381
1402
  ph.tokens_after_refill,
1382
1403
  LEAST(b.tokens, sc.capacity)
1383
1404
  + GREATEST(0, nv.now_ms - b.refilled_at_ms) / 1000.0
@@ -1479,7 +1500,7 @@ async function acquirePureRate(
1479
1500
  GREATEST(1, FLOOR(MIN(next_refill_per_sec) / 100.0))::bigint AS burst_size
1480
1501
  FROM refreshed
1481
1502
  ),
1482
- ${updateRateScopeSql(scopeHoldTableName(input))},
1503
+ ${updateRateScopeSql(scopeHoldTableName(input), unknownProvider)},
1483
1504
  updated AS (
1484
1505
  UPDATE ${bucketTableName(input)} b
1485
1506
  SET
@@ -1508,8 +1529,9 @@ async function acquirePureRate(
1508
1529
  -- AIMD ramp, folded in. Reaching this refill proves a local lease
1509
1530
  -- block was exhausted; only a confirmed provider success can turn
1510
1531
  -- that demand into a rate increase. Slow-start doubles until the
1511
- -- first 429, then increases by at most 25% per successful recovery
1512
- -- step. Never exceeds max_rps.
1532
+ -- first 429. Known-provider recovery uses 25% steps; unknown scopes
1533
+ -- rediscover below their seed and probe gently above it. Never
1534
+ -- exceeds max_rps.
1513
1535
  refill_per_sec = LEAST(r.max_rps, (SELECT next_refill_per_sec FROM provider_hold)),
1514
1536
  last_ramp_at_ms = (SELECT next_ramp_at_ms FROM provider_hold)
1515
1537
  FROM refreshed r
@@ -1702,8 +1724,10 @@ async function acquirePureRate(
1702
1724
  * Mixed / concurrency-bearing acquire. The token bucket still governs the
1703
1725
  * request rate; the durable lease table governs the simultaneous-in-flight cap.
1704
1726
  * Expired-lease reclaim + active count + rate refill + grant + token deduction +
1705
- * lease minting all execute in ONE multi-CTE statement (one implicit
1706
- * transaction), so a crash can never deduct tokens without minting the matching
1727
+ * lease minting all execute in one multi-CTE statement after taking the scope
1728
+ * lock in a separate READ COMMITTED statement. Waiting for that lock must finish
1729
+ * before the lease count takes its snapshot, or concurrent acquires miss leases
1730
+ * committed during their wait. A crash cannot deduct tokens without minting the matching
1707
1731
  * lease rows. Lease ids are generated in SQL (gen_random_uuid) and returned to
1708
1732
  * the client, one per granted permit, releasable independently. With a
1709
1733
  * requester identity the grant is also bounded by that run's fair share of
@@ -1716,8 +1740,44 @@ async function acquireWithConcurrency(
1716
1740
  rules: AcquirePacingRule[],
1717
1741
  seeds: RateBucketSeed[],
1718
1742
  requested: number,
1743
+ ): Promise<InternalAcquireOutcome> {
1744
+ await client.query('BEGIN ISOLATION LEVEL READ COMMITTED');
1745
+ try {
1746
+ await client.query(
1747
+ `SELECT bucket_id FROM ${scopeHoldTableName(input)}
1748
+ WHERE bucket_id = $1 FOR UPDATE`,
1749
+ [input.bucketId],
1750
+ );
1751
+ const outcome = await grantWithConcurrency(
1752
+ client,
1753
+ input,
1754
+ rules,
1755
+ seeds,
1756
+ requested,
1757
+ );
1758
+ await client.query('COMMIT');
1759
+ return outcome;
1760
+ } catch (error) {
1761
+ try {
1762
+ await client.query('ROLLBACK');
1763
+ } catch {
1764
+ // Preserve the original admission/storage error.
1765
+ }
1766
+ throw error;
1767
+ }
1768
+ }
1769
+
1770
+ async function grantWithConcurrency(
1771
+ client: PostgresSchedulerQueryClient,
1772
+ input: RateStateAcquireInput,
1773
+ rules: AcquirePacingRule[],
1774
+ seeds: RateBucketSeed[],
1775
+ requested: number,
1719
1776
  ): Promise<InternalAcquireOutcome> {
1720
1777
  const ruleIds = rules.map((rule) => rule.ruleId);
1778
+ const unknownProvider = rules.some(
1779
+ (rule) => rule.ruleId === 'default:provider-scope',
1780
+ );
1721
1781
  const concurrencyRules = rules.filter((rule) => rule.maxConcurrency != null);
1722
1782
  const concurrencyRuleIds = concurrencyRules.map((rule) => rule.ruleId);
1723
1783
  const requesterId = normalizeRequesterId(input);
@@ -1741,10 +1801,10 @@ async function acquireWithConcurrency(
1741
1801
  // One multi-CTE statement: reclaim expired leases, refill the rate rows,
1742
1802
  // count live leases, compute the grant bounded by rate AND free concurrency
1743
1803
  // slots, deduct the tokens, AND mint the lease rows — atomically. Grant +
1744
- // deduction + lease minting share one implicit transaction, so a crash can
1804
+ // deduction + lease minting share the scope-lock transaction, so a crash can
1745
1805
  // never deduct tokens without the matching lease rows (which would otherwise
1746
- // over-admit the provider until the phantom leases "expired"). PgBouncer-safe:
1747
- // autocommit, deterministic row locking, no lock_timeout, no session state.
1806
+ // over-admit the provider until the phantom leases "expired"). PgBouncer's
1807
+ // transaction pooling keeps the scope lock through this fresh snapshot.
1748
1808
  const result = await client.query<{
1749
1809
  granted: string | number;
1750
1810
  wait_ms: string | number;
@@ -1769,6 +1829,7 @@ async function acquireWithConcurrency(
1769
1829
  scopeHoldTableName(input),
1770
1830
  '$10',
1771
1831
  minimumRateSqlForRules(rules),
1832
+ unknownProvider,
1772
1833
  )},
1773
1834
  reclaim_reservations AS (
1774
1835
  DELETE FROM ${reservationTableName(input)}
@@ -1823,6 +1884,7 @@ async function acquireWithConcurrency(
1823
1884
  b.concurrency_minimum_rtt_ms,
1824
1885
  ph.cool_until_ms,
1825
1886
  ph.last_ramp_at_ms,
1887
+ ph.rate_observation_started_at_ms,
1826
1888
  GREATEST(b.next_dispatch_at_ms, ph.next_dispatch_at_ms) AS next_dispatch_at_ms,
1827
1889
  ph.rate_recovery_interval_ms,
1828
1890
  ph.observation_was_idle AS observation_was_idle,
@@ -1838,10 +1900,10 @@ async function acquireWithConcurrency(
1838
1900
  ph.retry_at_ms AS scope_retry_at_ms,
1839
1901
  -- Same derived burst as the pure-rate path. The declared
1840
1902
  -- maxConcurrency is untouched: free_slots still bounds the grant.
1841
- ${effectiveCapacitySql('normalized')} AS effective_capacity,
1842
- ${effectiveCapacitySql('normalized', 'next_refill_per_sec')} AS scheduled_capacity,
1903
+ ${effectiveCapacitySql('normalized', 'refill_per_sec', unknownProvider)} AS effective_capacity,
1904
+ ${effectiveCapacitySql('normalized', 'next_refill_per_sec', unknownProvider)} AS scheduled_capacity,
1843
1905
  LEAST(
1844
- ${effectiveCapacitySql('normalized')},
1906
+ ${effectiveCapacitySql('normalized', 'refill_per_sec', unknownProvider)},
1845
1907
  ph.tokens_after_refill,
1846
1908
  LEAST(b.tokens, sc.capacity)
1847
1909
  + GREATEST(0, nv.now_ms - b.refilled_at_ms) / 1000.0
@@ -1891,10 +1953,17 @@ async function acquireWithConcurrency(
1891
1953
  lim.rule_id,
1892
1954
  LEAST(
1893
1955
  lim.max_concurrency,
1956
+ CASE WHEN bucket.rule_id = 'default:provider-scope'
1957
+ AND NOT bucket.seen_backpressure
1958
+ AND bucket.last_ramp_at_ms <= COALESCE(bucket.rate_observation_started_at_ms, bucket.now_ms)
1959
+ THEN ${UNKNOWN_PROVIDER_CONCURRENCY_DISCOVERY_LIMIT}
1960
+ ELSE lim.max_concurrency END,
1894
1961
  CEIL(
1895
1962
  COALESCE(
1896
1963
  bucket.learned_concurrency_limit,
1897
- LEAST(lim.max_concurrency, ${INITIAL_CONCURRENCY_DISCOVERY_LIMIT})
1964
+ LEAST(lim.max_concurrency, CASE WHEN bucket.rule_id = 'default:provider-scope'
1965
+ THEN ${UNKNOWN_PROVIDER_CONCURRENCY_DISCOVERY_LIMIT}
1966
+ ELSE ${INITIAL_CONCURRENCY_DISCOVERY_LIMIT} END)
1898
1967
  )
1899
1968
  )
1900
1969
  )::int AS max_concurrency
@@ -1999,7 +2068,7 @@ async function acquireWithConcurrency(
1999
2068
  GREATEST(1, FLOOR(MIN(next_refill_per_sec) / 100.0))::bigint AS burst_size
2000
2069
  FROM refreshed
2001
2070
  ),
2002
- ${updateRateScopeSql(scopeHoldTableName(input))},
2071
+ ${updateRateScopeSql(scopeHoldTableName(input), unknownProvider)},
2003
2072
  updated AS (
2004
2073
  UPDATE ${bucketTableName(input)} b
2005
2074
  SET
@@ -2035,6 +2104,7 @@ async function acquireWithConcurrency(
2035
2104
  AND b.rule_id = r.rule_id
2036
2105
  RETURNING
2037
2106
  r.tokens_after_refill,
2107
+ r.effective_capacity AS capacity,
2038
2108
  b.refill_per_sec AS new_refill_per_sec
2039
2109
  ),
2040
2110
  lease_tokens AS (
@@ -2177,7 +2247,19 @@ async function acquireWithConcurrency(
2177
2247
  WHEN (SELECT free FROM free_slots) <= 0 THEN $7::bigint
2178
2248
  ELSE (
2179
2249
  SELECT CEIL(
2180
- MAX((1.0 - tokens_after_refill) / GREATEST(new_refill_per_sec, ${MIN_SCHEDULE_RPS}) * 1000)
2250
+ MAX(
2251
+ (
2252
+ GREATEST(
2253
+ 1.0,
2254
+ LEAST(
2255
+ $4::numeric,
2256
+ FLOOR(capacity),
2257
+ FLOOR(new_refill_per_sec * ${BLOCK_WAIT_HORIZON_MS} / 1000.0),
2258
+ (SELECT free FROM free_slots)
2259
+ )
2260
+ ) - tokens_after_refill
2261
+ ) / GREATEST(new_refill_per_sec, ${MIN_SCHEDULE_RPS}) * 1000
2262
+ )
2181
2263
  )::bigint
2182
2264
  FROM updated
2183
2265
  )
@@ -2320,6 +2402,124 @@ export async function validatePostgresRateStatePermit(
2320
2402
  });
2321
2403
  }
2322
2404
 
2405
+ /**
2406
+ * Space unknown-provider sends after app preparation, where network/charge
2407
+ * delays can collapse previously spaced permits. The existing scope remains
2408
+ * the only learner; this internal rule row stores only the physical cadence.
2409
+ * Legacy/uninitialized and declared scopes retain hold-only validation.
2410
+ */
2411
+ export async function claimPostgresProviderDispatch(
2412
+ client: PostgresSchedulerQueryClient,
2413
+ input: PlayRunnerRateStateValidateInput & { nowMs?: number },
2414
+ ): Promise<
2415
+ PlayRunnerRateStateValidateResult & { physicalCadenceWait: boolean }
2416
+ > {
2417
+ return withRateStateAvailabilityGuard(input, async () => {
2418
+ const result = await client.query<{
2419
+ server_now_ms: string | number;
2420
+ retry_at_ms: string | number;
2421
+ allowed: boolean;
2422
+ cadence_wait: boolean;
2423
+ }>(
2424
+ `
2425
+ WITH now_value AS (
2426
+ SELECT COALESCE($2::bigint,
2427
+ FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint) AS now_ms
2428
+ ), scope_rows AS (
2429
+ SELECT h.retry_at_ms, h.cool_until_ms, h.seen_backpressure,
2430
+ CASE WHEN EXISTS (
2431
+ SELECT 1 FROM ${bucketTableName(input)} b
2432
+ WHERE b.bucket_id = h.bucket_id AND b.rule_id = 'default:provider-scope'
2433
+ ) THEN h.refill_per_sec ELSE NULL END AS physical_rps
2434
+ FROM ${scopeHoldTableName(input)} h WHERE h.bucket_id = $1
2435
+ FOR SHARE OF h
2436
+ ), cadence AS (
2437
+ SELECT nv.now_ms,
2438
+ GREATEST(COALESCE(s.retry_at_ms, 0), COALESCE(s.cool_until_ms, 0)) AS hold_until_ms,
2439
+ s.physical_rps,
2440
+ COALESCE(s.seen_backpressure, false) AS preserve_cadence_phase,
2441
+ GREATEST(1, FLOOR(s.physical_rps / 100))::int AS burst_size,
2442
+ GREATEST(1, CEIL(1000.0 / s.physical_rps))::bigint AS spacing_ms
2443
+ FROM now_value nv LEFT JOIN scope_rows s ON true
2444
+ ), cursor AS (
2445
+ INSERT INTO ${bucketTableName(input)} AS existing (
2446
+ bucket_id, rule_id, tokens, capacity, refill_per_sec,
2447
+ base_refill_per_sec, min_rps, max_rps, refilled_at_ms,
2448
+ next_dispatch_at_ms, cool_until_ms
2449
+ )
2450
+ SELECT $1, '${PHYSICAL_DISPATCH_RULE_ID}', c.burst_size - 1,
2451
+ c.burst_size, c.physical_rps, c.physical_rps,
2452
+ ${MIN_SCHEDULE_RPS}, c.physical_rps, c.now_ms,
2453
+ c.now_ms + c.spacing_ms * c.burst_size, 0
2454
+ FROM cadence c
2455
+ WHERE c.hold_until_ms <= c.now_ms AND c.physical_rps > 0
2456
+ ON CONFLICT (bucket_id, rule_id) DO UPDATE SET
2457
+ -- Always return the locked current cursor, including a denial.
2458
+ -- A snapshot read after a blocked upsert could miss a concurrent
2459
+ -- insert and report the wrong retry deadline.
2460
+ tokens = CASE
2461
+ WHEN existing.next_dispatch_at_ms <= EXCLUDED.refilled_at_ms
2462
+ THEN EXCLUDED.capacity - 1
2463
+ WHEN LEAST(existing.tokens, EXCLUDED.capacity) >= 1
2464
+ THEN LEAST(existing.tokens, EXCLUDED.capacity) - 1
2465
+ ELSE existing.tokens END,
2466
+ cool_until_ms = CASE
2467
+ WHEN existing.next_dispatch_at_ms <= EXCLUDED.refilled_at_ms
2468
+ OR LEAST(existing.tokens, EXCLUDED.capacity) >= 1 THEN 0
2469
+ ELSE LEAST(existing.next_dispatch_at_ms, EXCLUDED.next_dispatch_at_ms) END,
2470
+ -- Persist debt decay at the new pace so repeated probes converge
2471
+ -- to this deadline instead of moving it forward on every denial.
2472
+ next_dispatch_at_ms = CASE
2473
+ WHEN existing.next_dispatch_at_ms <= EXCLUDED.refilled_at_ms
2474
+ -- Keep the cadence phase when a request reaches its slot
2475
+ -- late. Reanchoring every grant to now adds query/network
2476
+ -- jitter to every interval and lowers sustained throughput.
2477
+ -- Expire missed slots and retain only the first future slot,
2478
+ -- so idle time never creates a catch-up burst.
2479
+ THEN CASE WHEN (SELECT preserve_cadence_phase FROM cadence)
2480
+ THEN (existing.next_dispatch_at_ms::numeric
2481
+ + (
2482
+ FLOOR(
2483
+ (EXCLUDED.refilled_at_ms::numeric - existing.next_dispatch_at_ms::numeric)
2484
+ / (EXCLUDED.next_dispatch_at_ms - EXCLUDED.refilled_at_ms)
2485
+ ) + 1
2486
+ ) * (EXCLUDED.next_dispatch_at_ms - EXCLUDED.refilled_at_ms))::bigint
2487
+ -- Cold discovery retains strict spacing until provider
2488
+ -- backpressure establishes an adaptive cadence.
2489
+ ELSE EXCLUDED.next_dispatch_at_ms END
2490
+ ELSE LEAST(existing.next_dispatch_at_ms, EXCLUDED.next_dispatch_at_ms) END,
2491
+ capacity = EXCLUDED.capacity,
2492
+ refill_per_sec = EXCLUDED.refill_per_sec,
2493
+ base_refill_per_sec = EXCLUDED.base_refill_per_sec,
2494
+ max_rps = EXCLUDED.max_rps,
2495
+ refilled_at_ms = EXCLUDED.refilled_at_ms
2496
+ RETURNING cool_until_ms
2497
+ )
2498
+ SELECT c.now_ms AS server_now_ms,
2499
+ GREATEST(c.hold_until_ms, COALESCE(cursor.cool_until_ms, 0)) AS retry_at_ms,
2500
+ c.hold_until_ms <= c.now_ms AND (
2501
+ c.physical_rps IS NULL OR cursor.cool_until_ms = 0
2502
+ ) AS allowed,
2503
+ c.hold_until_ms <= c.now_ms AND COALESCE(cursor.cool_until_ms > c.now_ms, false) AS cadence_wait
2504
+ FROM cadence c LEFT JOIN cursor ON true
2505
+ `,
2506
+ [input.bucketId, input.nowMs ?? null],
2507
+ );
2508
+ const row = result.rows[0];
2509
+ if (!row || typeof row.allowed !== 'boolean') {
2510
+ throw new Error(
2511
+ `Physical provider admission returned no decision for ${input.bucketId}.`,
2512
+ );
2513
+ }
2514
+ return {
2515
+ allowed: row.allowed,
2516
+ physicalCadenceWait: row.cadence_wait,
2517
+ serverNowMs: Number(row.server_now_ms),
2518
+ retryAtMs: Number(row.retry_at_ms),
2519
+ };
2520
+ });
2521
+ }
2522
+
2323
2523
  type ConcurrencyObservation = Gradient2Observation & {
2324
2524
  kind: 'success' | 'throttled';
2325
2525
  };
@@ -2723,7 +2923,10 @@ async function applyConcurrencyObservations(
2723
2923
  maximumLimit,
2724
2924
  Math.max(
2725
2925
  1,
2726
- row.learned_concurrency_limit ?? INITIAL_CONCURRENCY_DISCOVERY_LIMIT,
2926
+ row.learned_concurrency_limit ??
2927
+ (rule.ruleId === 'default:provider-scope'
2928
+ ? UNKNOWN_PROVIDER_CONCURRENCY_DISCOVERY_LIMIT
2929
+ : INITIAL_CONCURRENCY_DISCOVERY_LIMIT),
2727
2930
  ),
2728
2931
  );
2729
2932
  limitsBefore.push(initialLimit);
@@ -2731,6 +2934,7 @@ async function applyConcurrencyObservations(
2731
2934
  providerConcurrencyControllerOptions({
2732
2935
  initialLimit,
2733
2936
  maximumLimit,
2937
+ unknownProvider: rule.ruleId === 'default:provider-scope',
2734
2938
  }),
2735
2939
  row.learned_concurrency_limit == null
2736
2940
  ? undefined
@@ -2745,7 +2949,7 @@ async function applyConcurrencyObservations(
2745
2949
  ),
2746
2950
  windowStartedAtLimit: Math.max(
2747
2951
  1,
2748
- Math.trunc(row.concurrency_window_limit ?? initialLimit),
2952
+ row.concurrency_window_limit ?? initialLimit,
2749
2953
  ),
2750
2954
  hasObservedThrottle:
2751
2955
  row.concurrency_has_observed_throttle === true,
@@ -2826,7 +3030,8 @@ async function acquireRateStateStatements(
2826
3030
  : acquirePureRate(client, grantInput, rules, seeds, requested);
2827
3031
 
2828
3032
  // Cold path only: seed missing rows / refresh changed configuration. A warm
2829
- // acquire is exactly one statement.
3033
+ // pure-rate acquire is exactly one statement. Concurrency-bearing acquires
3034
+ // take the scope lock before the lease-count statement gets its snapshot.
2830
3035
  let seeded = false;
2831
3036
  if (!warmRateBuckets.has(warmKey)) {
2832
3037
  await seedBucketRows(client, input, seeds, nowMs);
@@ -3067,7 +3272,7 @@ async function penalizeRateStateStatements(
3067
3272
  // one-minute probe interval so the five-RPM floor cannot turn into a retry
3068
3273
  // loop.
3069
3274
  // - refill_per_sec: multiplicative decrease, but ONLY once per active
3070
- // cooldown window (halve-once guard: skip if already cooling), floored at
3275
+ // cooldown window (skip if already cooling), floored at
3071
3276
  // min_rps.
3072
3277
  // - seen_backpressure flips the ramp from slow-start (x2) to additive.
3073
3278
  // - tokens drained: no more grants until refill re-accrues past the cooldown.
@@ -3095,9 +3300,37 @@ async function penalizeRateStateStatements(
3095
3300
  ) AS now_ms
3096
3301
  ), held AS (
3097
3302
  SELECT * FROM ${scopeHoldTableName(input)} WHERE bucket_id = $1 FOR UPDATE
3303
+ ), seeded_hold AS (
3304
+ SELECT h.*, CASE WHEN cardinality($6::text[]) > 0
3305
+ THEN 'default:provider-scope' = ANY($6::text[])
3306
+ AND NOT EXISTS (
3307
+ SELECT 1 FROM unnest($6::text[]) AS active(rule_id)
3308
+ WHERE active.rule_id NOT LIKE 'default:%'
3309
+ )
3310
+ ELSE EXISTS (
3311
+ SELECT 1 FROM ${bucketTableName(input)} b
3312
+ WHERE b.bucket_id = h.bucket_id
3313
+ AND b.rule_id = 'default:provider-scope'
3314
+ ) AND NOT EXISTS (
3315
+ SELECT 1 FROM ${bucketTableName(input)} b
3316
+ WHERE b.bucket_id = h.bucket_id
3317
+ AND b.rule_id NOT LIKE 'default:%'
3318
+ AND b.rule_id <> '${PHYSICAL_DISPATCH_RULE_ID}'
3319
+ ) END AS unknown_provider
3320
+ FROM held h
3098
3321
  ), evidence AS (
3099
3322
  SELECT h.*,
3100
3323
  COALESCE(h.refill_per_sec, 1) AS refill_before,
3324
+ -- A productive unknown-provider probe establishes useful capacity.
3325
+ -- Preserve 90% of its target instead of discarding half after each
3326
+ -- exploratory overshoot. Failed or sparse probes keep the stronger
3327
+ -- decrease and no-progress policy. Prefer the attempt's active rules.
3328
+ -- Early app feedback carries no rules: only an exclusively unknown
3329
+ -- durable seed can select this policy, not an obsolete default row
3330
+ -- beside declared operation rows.
3331
+ h.unknown_provider AND h.refill_per_sec >= ${DEFAULT_UNKNOWN_PROVIDER_FAST_RECOVERY_RPS}
3332
+ AND h.rate_successes >= ${DEFAULT_UNKNOWN_PROVIDER_COLD_START_RPS}
3333
+ AS productive_unknown_probe,
3101
3334
  GREATEST(h.retry_at_ms, COALESCE($5::bigint, nv.now_ms + $2::bigint)) AS deadline,
3102
3335
  (
3103
3336
  COALESCE($5::bigint, nv.now_ms + $2::bigint) > nv.now_ms
@@ -3122,17 +3355,25 @@ async function penalizeRateStateStatements(
3122
3355
  - COALESCE(h.rate_observation_started_at_ms, nv.now_ms)
3123
3356
  END) AS cycle_ms,
3124
3357
  CASE
3125
- WHEN h.rate_successes <= 0 AND ($5::bigint IS NOT NULL OR h.seen_backpressure) THEN
3358
+ -- A first unknown-provider rejection establishes its rate floor,
3359
+ -- not a failed probe streak. Respect its explicit deadline even
3360
+ -- when it expires in transit; the reduced dispatch budget still
3361
+ -- paces the next request. Only a subsequent admitted probe with
3362
+ -- no successes justifies the shared exponential pause.
3363
+ WHEN h.rate_successes <= 0
3364
+ AND ($5::bigint IS NOT NULL OR h.seen_backpressure)
3365
+ AND (NOT h.unknown_provider OR h.seen_backpressure) THEN
3126
3366
  CASE WHEN h.seen_backpressure THEN
3127
3367
  LEAST(60000, GREATEST(1000, h.rate_recovery_interval_ms * 2))
3128
3368
  ELSE 1000 END
3129
3369
  ELSE NULL
3130
3370
  END AS no_progress_probe_interval_ms,
3131
3371
  nv.now_ms
3132
- FROM held h CROSS JOIN now_value nv
3372
+ FROM seeded_hold h CROSS JOIN now_value nv
3133
3373
  ), throttle_ceiling AS (
3134
3374
  SELECT e.*,
3135
- GREATEST(${minimumRateRps},
3375
+ GREATEST(CASE WHEN e.unknown_provider
3376
+ THEN ${DEFAULT_UNKNOWN_PROVIDER_MIN_RPS} ELSE ${minimumRateRps} END,
3136
3377
  CASE WHEN e.seen_backpressure AND e.rate_successes = 0 THEN
3137
3378
  CASE WHEN e.no_progress_probe_interval_ms IS NOT NULL
3138
3379
  -- A fresh provider deadline is not a throughput measurement.
@@ -3143,6 +3384,7 @@ async function penalizeRateStateStatements(
3143
3384
  THEN GREATEST(${noProgressProbeRateRps},
3144
3385
  1000.0 / e.no_progress_probe_interval_ms)
3145
3386
  ELSE e.refill_before END
3387
+ WHEN e.productive_unknown_probe THEN e.refill_before * ${PRODUCTIVE_UNKNOWN_PROVIDER_DECREASE_FACTOR}
3146
3388
  ELSE e.refill_before / 2 END
3147
3389
  ) AS next_ceiling_rps
3148
3390
  FROM evidence e
@@ -3151,11 +3393,12 @@ async function penalizeRateStateStatements(
3151
3393
  CASE WHEN e.new_episode THEN GREATEST(
3152
3394
  CASE WHEN e.seen_backpressure AND e.rate_successes = 0
3153
3395
  THEN ${noProgressProbeRateRps}
3154
- ELSE ${minimumRateRps} END,
3155
- LEAST(
3396
+ ELSE CASE WHEN e.unknown_provider
3397
+ THEN ${DEFAULT_UNKNOWN_PROVIDER_MIN_RPS} ELSE ${minimumRateRps} END END,
3398
+ CASE WHEN e.productive_unknown_probe THEN e.next_ceiling_rps ELSE LEAST(
3156
3399
  e.next_ceiling_rps,
3157
3400
  GREATEST(1, e.rate_successes) * 1000.0 / e.cycle_ms
3158
- )) ELSE e.refill_before END AS refill_after
3401
+ ) END) ELSE e.refill_before END AS refill_after
3159
3402
  FROM throttle_ceiling e
3160
3403
  ), provider_hold AS (
3161
3404
  UPDATE ${scopeHoldTableName(input)} h SET
@@ -3165,21 +3408,33 @@ async function penalizeRateStateStatements(
3165
3408
  -- A repeated no-success throttle can impose a slower local probe
3166
3409
  -- interval than the normal adaptive floor. Keep that pause separate
3167
3410
  -- from retry_at_ms, which remains the provider's absolute deadline.
3168
- cool_until_ms = GREATEST(e.cool_until_ms, LEAST(
3411
+ -- Only a newly admitted probe can start another backoff episode.
3412
+ -- Gateway/runner copies and late in-flight siblings still extend
3413
+ -- retry_at_ms, but must not slide the local no-progress pause.
3414
+ cool_until_ms = CASE WHEN e.new_episode THEN GREATEST(e.cool_until_ms, LEAST(
3169
3415
  e.now_ms + $4::bigint,
3170
3416
  GREATEST(
3171
3417
  e.deadline,
3172
- e.now_ms + LEAST($2::bigint, $4::bigint),
3418
+ -- cooldownMs was captured before transport/pool admission. When
3419
+ -- an absolute deadline exists, adding that stale duration to
3420
+ -- the database clock would restart part of the same hold.
3421
+ -- Legacy duration-only callers still start their hold here.
3422
+ CASE WHEN $5::bigint IS NULL
3423
+ THEN e.now_ms + LEAST($2::bigint, $4::bigint)
3424
+ ELSE e.now_ms END,
3173
3425
  e.now_ms + COALESCE(e.no_progress_probe_interval_ms, 0)
3174
3426
  )
3175
- )),
3427
+ )) ELSE e.cool_until_ms END,
3176
3428
  tokens = CASE WHEN e.new_episode THEN 0 ELSE e.tokens END,
3177
3429
  rate_successes = e.rate_successes,
3178
3430
  rate_throttle_ceiling_rps = CASE WHEN e.new_episode
3179
3431
  THEN e.next_ceiling_rps ELSE e.rate_throttle_ceiling_rps END,
3180
3432
  rate_observation_started_at_ms = CASE WHEN e.new_episode THEN NULL ELSE e.rate_observation_started_at_ms END,
3181
3433
  rate_recovery_interval_ms = CASE WHEN e.new_episode
3182
- THEN COALESCE(e.no_progress_probe_interval_ms, e.cycle_ms)
3434
+ THEN CASE WHEN e.unknown_provider AND e.rate_successes > 0
3435
+ -- Retry-After is a deadline, not time spent proving capacity.
3436
+ THEN GREATEST(1000, e.now_ms - COALESCE(e.rate_observation_started_at_ms, e.now_ms))
3437
+ ELSE COALESCE(e.no_progress_probe_interval_ms, e.cycle_ms) END
3183
3438
  ELSE e.rate_recovery_interval_ms END,
3184
3439
  last_ramp_at_ms = CASE WHEN e.new_episode THEN e.deadline ELSE e.last_ramp_at_ms END,
3185
3440
  -- All pre-429 permits are revalidated against this durable hold
@@ -3193,6 +3448,7 @@ async function penalizeRateStateStatements(
3193
3448
  SELECT b.bucket_id, b.rule_id, b.refill_per_sec AS refill_before
3194
3449
  FROM ${bucketTableName(input)} b
3195
3450
  JOIN provider_hold h ON h.bucket_id = b.bucket_id
3451
+ WHERE b.rule_id <> '${PHYSICAL_DISPATCH_RULE_ID}'
3196
3452
  ORDER BY b.rule_id FOR UPDATE OF b
3197
3453
  ), updated AS (
3198
3454
  -- Compatibility projections only: no per-rule learning history is
@@ -3235,6 +3491,7 @@ async function penalizeRateStateStatements(
3235
3491
  input.nowMs ?? null,
3236
3492
  PROVIDER_AIMD_COOLDOWN_CAP_MS,
3237
3493
  retryAtMs,
3494
+ rules.map((rule) => rule.ruleId),
3238
3495
  ],
3239
3496
  );
3240
3497
  const rateRow = rateStateResult.rows[0];