deepline 0.3.128 → 0.3.129

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/client.ts +34 -9
  2. package/dist/bundling-sources/sdk/src/http.ts +22 -0
  3. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  4. package/dist/bundling-sources/shared_libs/play-data-plane/r2.ts +22 -0
  5. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +265 -2
  6. package/dist/bundling-sources/shared_libs/play-runtime/bounded-batch-function.ts +682 -0
  7. package/dist/bundling-sources/shared_libs/play-runtime/child-execution-strategy.ts +21 -5
  8. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +5390 -1732
  9. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +107 -1
  10. package/dist/bundling-sources/shared_libs/play-runtime/deployment/commands/roll-execution-relay.ts +7 -0
  11. package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-machines.ts +229 -7
  12. package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-registry.ts +25 -4
  13. package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-gateway-capacity.ts +71 -0
  14. package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-gateway-drain.ts +1 -1
  15. package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-topology.ts +4 -1
  16. package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +694 -28
  17. package/dist/bundling-sources/shared_libs/play-runtime/egress-bandwidth-probe.ts +167 -0
  18. package/dist/bundling-sources/shared_libs/play-runtime/execute-server-timing.ts +103 -0
  19. package/dist/bundling-sources/shared_libs/play-runtime/execution-gateway-receipt-output-reference.ts +34 -0
  20. package/dist/bundling-sources/shared_libs/play-runtime/execution-gateway-response-size.ts +27 -0
  21. package/dist/bundling-sources/shared_libs/play-runtime/gateway-postgres-admission.ts +66 -1
  22. package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +425 -71
  23. package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +210 -32
  24. package/dist/bundling-sources/shared_libs/play-runtime/governor/in-memory-rate-state-backend.ts +27 -13
  25. package/dist/bundling-sources/shared_libs/play-runtime/governor/policy.ts +19 -6
  26. package/dist/bundling-sources/shared_libs/play-runtime/governor/rate-state-backend.ts +61 -2
  27. package/dist/bundling-sources/shared_libs/play-runtime/integration-operation-reader-fence/contracts.ts +33 -1
  28. package/dist/bundling-sources/shared_libs/play-runtime/integration-operation-reader-fence/job.ts +42 -1
  29. package/dist/bundling-sources/shared_libs/play-runtime/launch-source-policy.ts +11 -11
  30. package/dist/bundling-sources/shared_libs/play-runtime/ledger-safe-payload.ts +49 -16
  31. package/dist/bundling-sources/shared_libs/play-runtime/map-memory-limits.ts +1653 -98
  32. package/dist/bundling-sources/shared_libs/play-runtime/memory-ledger.ts +807 -0
  33. package/dist/bundling-sources/shared_libs/play-runtime/memory-pressure-signal.ts +138 -0
  34. package/dist/bundling-sources/shared_libs/play-runtime/output-size-limits.ts +85 -3
  35. package/dist/bundling-sources/shared_libs/play-runtime/persistence-latch.ts +60 -2
  36. package/dist/bundling-sources/shared_libs/play-runtime/play-run-recovery-policy.ts +46 -1
  37. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/contracts.ts +2 -0
  38. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/job.ts +1 -0
  39. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/ports.ts +1 -0
  40. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/postgres-adapter.ts +1 -0
  41. package/dist/bundling-sources/shared_libs/play-runtime/play-runtime-semantics.ts +70 -27
  42. package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +56 -0
  43. package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/adaptive-provider-capacity-controller.ts +192 -0
  44. package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/epoch-aimd-rate-controller.ts +194 -0
  45. package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/fixture-capacity-simulator.ts +220 -0
  46. package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/gradient2-concurrency-controller.ts +214 -0
  47. package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/provider-capacity-policy.ts +43 -0
  48. package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/provider-concurrency-policy.ts +30 -0
  49. package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/provider-rate-policy.ts +95 -0
  50. package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/provider-throttle-classifier.ts +40 -0
  51. package/dist/bundling-sources/shared_libs/play-runtime/resource-governor.ts +1535 -11
  52. package/dist/bundling-sources/shared_libs/play-runtime/run-failure.ts +49 -2
  53. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +6 -1
  54. package/dist/bundling-sources/shared_libs/play-runtime/run-log-buffer.ts +55 -0
  55. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/egress-probe.ts +221 -0
  56. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +339 -29
  57. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-deadline.ts +28 -1
  58. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-progress-push.ts +192 -165
  59. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts +6 -0
  60. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-payload-transport.ts +90 -5
  61. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-runtime-watchdog.ts +281 -6
  62. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +27 -1
  63. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/local-process.ts +19 -0
  64. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/modal.ts +108 -0
  65. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/runtime-sandbox-reconciliation.ts +40 -0
  66. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/types.ts +11 -5
  67. package/dist/bundling-sources/shared_libs/play-runtime/runtime-actions.ts +30 -0
  68. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api-compression.ts +276 -0
  69. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +723 -115
  70. package/dist/bundling-sources/shared_libs/play-runtime/runtime-capacity-policy.ts +98 -2
  71. package/dist/bundling-sources/shared_libs/play-runtime/runtime-contract.ts +3 -2
  72. package/dist/bundling-sources/shared_libs/play-runtime/runtime-environment.ts +41 -4
  73. package/dist/bundling-sources/shared_libs/play-runtime/runtime-flow.ts +448 -0
  74. package/dist/bundling-sources/shared_libs/play-runtime/runtime-map-row-feed.ts +215 -0
  75. package/dist/bundling-sources/shared_libs/play-runtime/runtime-operation-contract.ts +11 -0
  76. package/dist/bundling-sources/shared_libs/play-runtime/runtime-receipt-store-adapter.ts +161 -14
  77. package/dist/bundling-sources/shared_libs/play-runtime/runtime-receipt-value.ts +184 -0
  78. package/dist/bundling-sources/shared_libs/play-runtime/runtime-receipt-writer.ts +607 -121
  79. package/dist/bundling-sources/shared_libs/play-runtime/runtime-sandbox-placement-policy.ts +4 -0
  80. package/dist/bundling-sources/shared_libs/play-runtime/runtime-sheet-row-writer.ts +21 -0
  81. package/dist/bundling-sources/shared_libs/play-runtime/sandbox-runs/contract.ts +15 -2
  82. package/dist/bundling-sources/shared_libs/play-runtime/sandbox-runs/runner-backend-adapter.ts +71 -14
  83. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-play-deploy-task.ts +2 -2
  84. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +288 -119
  85. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-shared.ts +8 -6
  86. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-worker-required-env.ts +28 -3
  87. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/canonical-integration-operation-store.ts +179 -0
  88. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/gateway-heavy-request-admission.ts +80 -2
  89. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/gateway-progress-registry.ts +31 -2
  90. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/integration-operation-store-contract.ts +104 -0
  91. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/integration-operation-value-reference.ts +47 -0
  92. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/integration-operation-value-retirement.ts +193 -0
  93. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/integration-operation-value-storage.ts +420 -0
  94. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-pool.ts +41 -0
  95. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-progress.ts +134 -3
  96. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts +1657 -95
  97. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +885 -73
  98. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-completion-coalescer.ts +452 -0
  99. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-gateway.ts +601 -42
  100. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-terminal-transition.ts +1 -0
  101. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-receipt-value-storage.ts +371 -0
  102. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/worker-resource-metrics.ts +86 -7
  103. package/dist/bundling-sources/shared_libs/play-runtime/secret-redaction.ts +153 -22
  104. package/dist/bundling-sources/shared_libs/play-runtime/tool-call/index.ts +1 -0
  105. package/dist/bundling-sources/shared_libs/play-runtime/tool-call/receipt-cohort.ts +188 -38
  106. package/dist/bundling-sources/shared_libs/play-runtime/tool-call/runtime-step-receipts-adapter.ts +28 -13
  107. package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +44 -1
  108. package/dist/bundling-sources/shared_libs/play-runtime/tool-result.ts +56 -4
  109. package/dist/bundling-sources/shared_libs/play-runtime/work-receipts.ts +2 -0
  110. package/dist/bundling-sources/shared_libs/plays/authoring-contract.ts +16 -10
  111. package/dist/bundling-sources/shared_libs/plays/dataset.ts +4 -1
  112. package/dist/cli/index.js +250 -38
  113. package/dist/cli/index.mjs +250 -38
  114. package/dist/{compiler-manifest-DfSnbCTW.d.mts → compiler-manifest-BIqRyj5m.d.mts} +1 -1
  115. package/dist/{compiler-manifest-DfSnbCTW.d.ts → compiler-manifest-BIqRyj5m.d.ts} +1 -1
  116. package/dist/index.d.mts +4 -2
  117. package/dist/index.d.ts +4 -2
  118. package/dist/index.js +231 -35
  119. package/dist/index.mjs +231 -35
  120. package/dist/install-integrity.json +31 -2
  121. package/dist/plays/bundle-play-file.d.mts +2 -2
  122. package/dist/plays/bundle-play-file.d.ts +2 -2
  123. package/dist/plays/bundle-play-file.mjs +65 -1
  124. package/dist/release.d.mts +1 -1
  125. package/dist/release.d.ts +1 -1
  126. package/dist/release.js +1 -1
  127. package/dist/release.mjs +1 -1
  128. package/package.json +1 -1
@@ -4,10 +4,13 @@ import {
4
4
  releaseRateStateViaAppRuntime,
5
5
  type WorkerRuntimeApiContext,
6
6
  } from '../app-runtime-api';
7
+ import { randomUUID } from 'node:crypto';
7
8
  import {
8
9
  DEFAULT_ADVISORY_PROVIDER_MAX_RPS,
9
10
  noopPacingPermit,
10
11
  type PacingPermit,
12
+ type ProviderConcurrencyGrant,
13
+ type ProviderPacingGrant,
11
14
  type PacingRule,
12
15
  type RateStateBackend,
13
16
  type RateStateBackendKind,
@@ -29,6 +32,13 @@ import {
29
32
  * so keep their sleep-toward-it behavior byte-for-byte.
30
33
  */
31
34
  export const PROVIDER_EXHAUSTED_MAX_WAIT_MS = 60_000;
35
+ /**
36
+ * Total time one acquirer may spend cycling through window-exhausted refills.
37
+ * Each refill's sleep is bounded, but nothing bounded how many of them a row
38
+ * could take; with several runs sharing one org's provider window a late
39
+ * starter parked here for twenty minutes with its core idle.
40
+ */
41
+ export const RATE_ACQUIRE_MAX_WAIT_MS = 300_000;
32
42
 
33
43
  /**
34
44
  * Permits leased per app-runtime round trip for the direct Node substrate.
@@ -73,6 +83,12 @@ export const APP_RUNTIME_RATE_STATE_LEASE_BLOCK_SIZE = 8;
73
83
  const LEASE_BLOCK_MIN_TTL_MS = 250;
74
84
  const LEASE_BLOCK_MAX_TTL_MS = 5_000;
75
85
  const MAX_ACQUIRE_SLEEP_MS = 5_000;
86
+ /**
87
+ * Horizon used to size the next lease block from the rate actually in force.
88
+ * Kept short so a block stays inside its own TTL and a stalled runner can never
89
+ * hold more than a moment of a shared provider window.
90
+ */
91
+ const RATE_BLOCK_HORIZON_MS = 1_000;
76
92
  const STORE_FAILURE_RETRY_DELAYS_MS = [100, 250] as const;
77
93
  /**
78
94
  * Jitter fraction applied to a server-supplied `waitMs` so a synchronized wave
@@ -90,13 +106,30 @@ export interface RateStateCooldown {
90
106
  }
91
107
 
92
108
  interface LeasedBlock {
93
- permits: Array<[scheduledAtMs: number, leaseId: string | null]>;
109
+ permits: Array<
110
+ [
111
+ scheduledAtMs: number,
112
+ leaseId: string | null,
113
+ reservationId: string | null,
114
+ concurrencyGrant: ProviderConcurrencyGrant | null,
115
+ pacingGrant: ProviderPacingGrant | null,
116
+ ]
117
+ >;
94
118
  rateScopeToken: string | null;
95
119
  expiresAt: number;
96
120
  rulesKey: string;
97
121
  rules: PacingRule[];
98
122
  }
99
123
 
124
+ type PendingConcurrencyObservations = {
125
+ batch: {
126
+ observerId: string;
127
+ laneId: string;
128
+ sequence: number;
129
+ };
130
+ observations: Array<{ kind: 'success'; latencyMs: number }>;
131
+ };
132
+
100
133
  interface PendingRelease {
101
134
  bucketId: string;
102
135
  rateScopeToken: string | null;
@@ -148,6 +181,21 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
148
181
  private readonly cooldowns = new Map<string, RateStateCooldown>();
149
182
  private readonly effectiveRps = new Map<string, number>();
150
183
  private readonly pendingSuccesses = new Map<string, number>();
184
+ /**
185
+ * Completion samples travel with the next lease refill. This keeps the
186
+ * Gradient2 checkpoint current without adding one gateway request per row.
187
+ */
188
+ private readonly pendingConcurrencyObservations = new Map<
189
+ string,
190
+ PendingConcurrencyObservations
191
+ >();
192
+ /** One opaque identity per runtime backend; never derived from customer data. */
193
+ private readonly concurrencyObserverId = randomUUID();
194
+ private readonly concurrencyObservationLanes = new Map<string, string>();
195
+ private readonly nextConcurrencyObservationSequence = new Map<
196
+ string,
197
+ number
198
+ >();
151
199
  private pendingReleaseFailure: Error | null = null;
152
200
 
153
201
  constructor(context: WorkerRuntimeApiContext, options: Options = {}) {
@@ -184,11 +232,16 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
184
232
  * otherwise reports `coolUntilMs`. No-op when the bucket has no live hold.
185
233
  */
186
234
  private throwIfPacerExhausted(bucketId: string): void {
235
+ const exhausted = this.pacerExhaustion(bucketId);
236
+ if (exhausted) throw exhausted;
237
+ }
238
+
239
+ private pacerExhaustion(bucketId: string): ProviderExhaustedError | null {
187
240
  const cooldown = this.cooldowns.get(bucketId);
188
- if (!cooldown) return;
241
+ if (!cooldown) return null;
189
242
  const now = this.now();
190
243
  const coolActive = cooldown.coolUntilMs - now > 0;
191
- if (!coolActive) return;
244
+ if (!coolActive) return null;
192
245
  const coolWait = cooldown.coolUntilMs - now;
193
246
  const claimActive = cooldown.claimedRetryAtMs - now > 0;
194
247
  // Duration alone decides. Neither hold is over threshold → let it sleep.
@@ -199,7 +252,7 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
199
252
  cooldown.claimedRetryAtMs - now > PROVIDER_EXHAUSTED_MAX_WAIT_MS
200
253
  )
201
254
  ) {
202
- return;
255
+ return null;
203
256
  }
204
257
  // Retry deadline: the server Retry-After verbatim when present, else the
205
258
  // pacer's own cooldown deadline.
@@ -207,12 +260,36 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
207
260
  cooldown.claimedRetryAtMs > 0
208
261
  ? cooldown.claimedRetryAtMs
209
262
  : cooldown.coolUntilMs;
210
- throw new ProviderExhaustedError({
263
+ return new ProviderExhaustedError({
211
264
  provider: providerNameFromBucketId(bucketId),
212
265
  retryAtMs,
213
266
  });
214
267
  }
215
268
 
269
+ tryAcquire(input: {
270
+ bucketId: string;
271
+ rateScopeToken?: string | null;
272
+ rules: readonly PacingRule[];
273
+ }): PacingPermit | null {
274
+ const { bucketId, rules } = input;
275
+ if (rules.length === 0) return noopPacingPermit();
276
+ // A probe reports nothing and repairs nothing: a pending release failure
277
+ // or an exhausted pacer surfaces from the next blocking acquire instead.
278
+ if (this.pendingReleaseFailure || this.pacerExhaustion(bucketId)) {
279
+ return null;
280
+ }
281
+ const laneKey = rateLaneKey(
282
+ bucketId,
283
+ rulesSignature([...rules].sort((a, b) => a.ruleId.localeCompare(b.ruleId))),
284
+ );
285
+ const block = this.liveBlock(bucketId, laneKey);
286
+ if (!block) return null;
287
+ const next = block.permits[0];
288
+ if (!next || next[0] > this.now()) return null;
289
+ block.permits.shift();
290
+ return this.permitFor(bucketId, laneKey, block, next);
291
+ }
292
+
216
293
  async acquire(input: {
217
294
  bucketId: string;
218
295
  rateScopeToken?: string | null;
@@ -239,6 +316,7 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
239
316
  // sleep) serves the whole matching wave, then everyone re-draws. This replaces the old
240
317
  // per-bucket lock that held across the RPC + up-to-5s sleep and thereby
241
318
  // serialized every map row behind one sleeping acquirer.
319
+ const acquireStartedAt = this.now();
242
320
  while (true) {
243
321
  if (signal?.aborted) {
244
322
  throw signal.reason instanceof Error
@@ -257,6 +335,13 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
257
335
  }
258
336
  return localPermit.permit;
259
337
  }
338
+ const waitedMs = this.now() - acquireStartedAt;
339
+ if (waitedMs >= RATE_ACQUIRE_MAX_WAIT_MS) {
340
+ throw new ProviderExhaustedError({
341
+ provider: providerNameFromBucketId(bucketId),
342
+ retryAtMs: this.now() + MAX_ACQUIRE_SLEEP_MS,
343
+ });
344
+ }
260
345
  // Count this acquirer as waiting on the refill so the single-flight refill
261
346
  // can size its block to the live wave. Decremented once a block lands (or
262
347
  // the refill errors) — see the finally in `refillBlock`.
@@ -291,37 +376,24 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
291
376
  laneKey: string,
292
377
  rules: readonly PacingRule[],
293
378
  ): number {
379
+ const floor = APP_RUNTIME_RATE_STATE_LEASE_BLOCK_SIZE;
294
380
  const configuredRps = Math.min(
295
381
  ...rules.map((rule) => (rule.requestsPerWindow / rule.windowMs) * 1_000),
296
382
  );
297
- const effectiveRps = Math.max(
298
- 1,
299
- this.effectiveRps.get(laneKey) ?? configuredRps,
300
- );
301
- // Catalogued rates are advisory exploration seeds. At a low seed, a
302
- // normal eight-permit prefetch books tens of seconds of dispatch time
303
- // before the first healthy response can authorize AIMD recovery. Start
304
- // with only the rate that can be spent within the short lease horizon;
305
- // ordinary and explicitly capped rules retain the bounded prefetch floor.
306
- const hasAdvisoryExplorationCeiling = rules.some((rule) => {
307
- const baseRps = (rule.requestsPerWindow / rule.windowMs) * 1_000;
308
- return rule.adaptiveMaxRps != null && rule.adaptiveMaxRps > baseRps;
309
- });
310
- const floor = hasAdvisoryExplorationCeiling
311
- ? Math.min(
312
- APP_RUNTIME_RATE_STATE_LEASE_BLOCK_SIZE,
313
- Math.max(1, Math.ceil(effectiveRps)),
314
- )
315
- : APP_RUNTIME_RATE_STATE_LEASE_BLOCK_SIZE;
316
- const horizonBudget = Math.max(
383
+ // Size the block from the rate ACTUALLY in force (the store's learned
384
+ // `refill_per_sec`, falling back to the declared rate) over a short horizon,
385
+ // and never past what this block's own TTL can spend at that rate — a permit
386
+ // the block cannot hand out before it expires is a provider token burned for
387
+ // nothing. Clamping to `requestsPerWindow` instead pinned a ramped lane
388
+ // (10 rps declared, 100 rps learned) to ten permits per round trip, which on
389
+ // a hot map is a round trip per permit. The store's own token floor, not
390
+ // this number, is what bounds the grant.
391
+ const horizonMs = Math.min(RATE_BLOCK_HORIZON_MS, leasedBlockTtlMs(rules));
392
+ const rateBudget = Math.max(
317
393
  1,
318
- Math.ceil(effectiveRps),
319
- );
320
- const rateBudget = Math.min(
321
- horizonBudget,
322
- ...rules
323
- .map((rule) => rule.requestsPerWindow)
324
- .filter((value) => Number.isFinite(value) && value > 0),
394
+ Math.floor(
395
+ ((this.effectiveRps.get(laneKey) ?? configuredRps) * horizonMs) / 1_000,
396
+ ),
325
397
  );
326
398
  if (!Number.isFinite(rateBudget) || rateBudget <= 0) return floor;
327
399
  const waiters = this.waiters.get(laneKey) ?? 1;
@@ -461,20 +533,25 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
461
533
  try {
462
534
  const successKey = pendingSuccessKey(bucketId, ordered);
463
535
  const observedSuccesses = this.pendingSuccesses.get(successKey) ?? 0;
464
- // Use the same prepared rules for local lease sizing and the durable
465
- // controller. Catalogued rules receive their advisory AIMD ceiling in
466
- // this step, so inspecting the raw rule here would retain the fixed
467
- // eight-permit floor for exactly the provider lanes that need fast
468
- // feedback to recover.
469
- const rateRules = withAdaptiveMaxRps(ordered);
536
+ const pendingConcurrency =
537
+ this.pendingConcurrencyObservations.get(successKey);
538
+ const concurrencyObservations = [
539
+ ...(pendingConcurrency?.observations ?? []),
540
+ ];
470
541
  const response = await this.port.acquire({
471
542
  bucketId,
472
543
  rateScopeToken,
473
- // Stamp the advisory ceiling so the durable DB row uses the same
474
- // catalogued-rate AIMD range as this in-memory controller.
475
- rules: rateRules,
476
- requested: this.requestedForBucket(laneKey, rateRules),
544
+ // Stamp adaptiveMaxRps on default-pacing rules so the DB row's ceiling
545
+ // matches the old in-memory AIMD range.
546
+ rules: withAdaptiveMaxRps(ordered),
547
+ requested: this.requestedForBucket(laneKey, ordered),
477
548
  observedSuccesses,
549
+ ...(pendingConcurrency
550
+ ? {
551
+ concurrencyObservations,
552
+ concurrencyObservationBatch: pendingConcurrency.batch,
553
+ }
554
+ : {}),
478
555
  schedulerSchema: this.schedulerSchema,
479
556
  });
480
557
  if (observedSuccesses > 0) {
@@ -485,6 +562,40 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
485
562
  if (remaining > 0) this.pendingSuccesses.set(successKey, remaining);
486
563
  else this.pendingSuccesses.delete(successKey);
487
564
  }
565
+ if (pendingConcurrency) {
566
+ const pending = this.pendingConcurrencyObservations.get(successKey);
567
+ const remainingSuccesses = this.pendingSuccesses.get(successKey) ?? 0;
568
+ if (
569
+ pending &&
570
+ pending.batch.sequence === pendingConcurrency.batch.sequence
571
+ ) {
572
+ const remaining = pending.observations.slice(
573
+ concurrencyObservations.length,
574
+ );
575
+ if (remainingSuccesses > 0 || remaining.length > 0) {
576
+ // Observations that arrived while this acquire was in flight need
577
+ // a new identity. Reusing the acknowledged sequence would make
578
+ // the gateway correctly discard their rate and Gradient2 input.
579
+ this.pendingConcurrencyObservations.set(
580
+ successKey,
581
+ this.newPendingConcurrencyObservations(successKey, remaining),
582
+ );
583
+ } else {
584
+ this.pendingConcurrencyObservations.delete(successKey);
585
+ }
586
+ } else if (
587
+ pending &&
588
+ pending.observations.length > concurrencyObservations.length
589
+ ) {
590
+ const remaining = pending.observations.slice(
591
+ concurrencyObservations.length,
592
+ );
593
+ this.pendingConcurrencyObservations.set(
594
+ successKey,
595
+ this.newPendingConcurrencyObservations(successKey, remaining),
596
+ );
597
+ }
598
+ }
488
599
  this.recordCooldown(bucketId, response);
489
600
  if (
490
601
  Number.isFinite(response.effectiveRequestsPerSecond) &&
@@ -568,6 +679,9 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
568
679
  response: {
569
680
  granted: number;
570
681
  leaseIds?: unknown;
682
+ reservationIds?: unknown;
683
+ concurrencyGrants?: unknown;
684
+ effectiveRequestsPerSecond?: unknown;
571
685
  serverNowMs?: unknown;
572
686
  scheduledAtMs?: unknown;
573
687
  },
@@ -589,6 +703,35 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
589
703
  `Rate-state store returned ${leaseIds.length} lease tokens for pure-rate bucket ${bucketId}.`,
590
704
  );
591
705
  }
706
+ const concurrencyGrants = normalizeConcurrencyGrants({
707
+ value: response.concurrencyGrants,
708
+ granted: response.granted,
709
+ hasConcurrency,
710
+ bucketId,
711
+ });
712
+ const effectiveRequestsPerSecond = Number(
713
+ response.effectiveRequestsPerSecond,
714
+ );
715
+ const pacingGrant: ProviderPacingGrant | null =
716
+ Number.isFinite(effectiveRequestsPerSecond) &&
717
+ effectiveRequestsPerSecond > 0
718
+ ? { targetRequestsPerSecond: effectiveRequestsPerSecond }
719
+ : null;
720
+ const reservationIds = Array.isArray(response.reservationIds)
721
+ ? response.reservationIds.filter(
722
+ (reservationId): reservationId is string =>
723
+ typeof reservationId === 'string' &&
724
+ reservationId.trim().length > 0,
725
+ )
726
+ : [];
727
+ if (
728
+ reservationIds.length > 0 &&
729
+ reservationIds.length !== response.granted
730
+ ) {
731
+ throw new Error(
732
+ `Rate-state store granted ${response.granted} permits for ${bucketId} with ${reservationIds.length} refundable reservation ids.`,
733
+ );
734
+ }
592
735
  // Rolling deploy compatibility: an older gateway returns no schedule. It
593
736
  // has already debited the block under the legacy token-bucket contract, so
594
737
  // keep those permits immediately usable until every gateway serves the new
@@ -598,11 +741,20 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
598
741
  : Array.from({ length: response.granted }, () => this.now());
599
742
  const serverNowMs = Number(response.serverNowMs);
600
743
  const receivedAtMs = this.now();
744
+ let firstGatewayScheduledAtMs = Number.POSITIVE_INFINITY;
745
+ for (const scheduledAtMs of gatewayScheduledAtMs) {
746
+ firstGatewayScheduledAtMs = Math.min(
747
+ firstGatewayScheduledAtMs,
748
+ scheduledAtMs,
749
+ );
750
+ }
601
751
  const scheduledAtMs =
602
752
  Number.isFinite(serverNowMs) && serverNowMs >= 0
603
753
  ? gatewayScheduledAtMs.map(
604
754
  (scheduledAtMs) =>
605
- receivedAtMs + Math.max(0, scheduledAtMs - serverNowMs),
755
+ receivedAtMs +
756
+ Math.max(0, firstGatewayScheduledAtMs - serverNowMs) +
757
+ (scheduledAtMs - firstGatewayScheduledAtMs),
606
758
  )
607
759
  : gatewayScheduledAtMs;
608
760
  if (
@@ -616,10 +768,24 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
616
768
  this.mergeBlock(
617
769
  bucketId,
618
770
  rateScopeToken,
619
- scheduledAtMs.map((scheduledAtMs, index): [number, string | null] => [
620
- scheduledAtMs,
621
- hasConcurrency ? (leaseIds[index] ?? null) : null,
622
- ]),
771
+ scheduledAtMs.map(
772
+ (
773
+ scheduledAtMs,
774
+ index,
775
+ ): [
776
+ number,
777
+ string | null,
778
+ string | null,
779
+ ProviderConcurrencyGrant | null,
780
+ ProviderPacingGrant | null,
781
+ ] => [
782
+ scheduledAtMs,
783
+ hasConcurrency ? (leaseIds[index] ?? null) : null,
784
+ reservationIds[index] ?? null,
785
+ concurrencyGrants[index] ?? null,
786
+ pacingGrant,
787
+ ],
788
+ ),
623
789
  rulesKey,
624
790
  laneKey,
625
791
  ordered,
@@ -630,6 +796,9 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
630
796
  bucketId: string;
631
797
  rateScopeToken?: string | null;
632
798
  cooldownMs: number;
799
+ rules?: readonly PacingRule[];
800
+ latencyMs?: number | null;
801
+ concurrencyGrant?: ProviderConcurrencyGrant | null;
633
802
  }): Promise<void> {
634
803
  if (input.cooldownMs <= 0) return;
635
804
  this.drainBlocksForBucket(input.bucketId);
@@ -638,6 +807,11 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
638
807
  bucketId: input.bucketId,
639
808
  rateScopeToken: input.rateScopeToken?.trim() || null,
640
809
  cooldownMs: input.cooldownMs,
810
+ ...(input.rules ? { rules: [...input.rules] } : {}),
811
+ ...(input.latencyMs != null ? { latencyMs: input.latencyMs } : {}),
812
+ ...(input.concurrencyGrant
813
+ ? { concurrencyGrant: input.concurrencyGrant }
814
+ : {}),
641
815
  schedulerSchema: this.schedulerSchema,
642
816
  });
643
817
  } catch (error) {
@@ -655,28 +829,91 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
655
829
  observeSuccess(input: {
656
830
  bucketId: string;
657
831
  rules: readonly PacingRule[];
832
+ latencyMs?: number | null;
658
833
  }): void {
659
834
  const key = pendingSuccessKey(input.bucketId, input.rules);
660
835
  this.pendingSuccesses.set(
661
836
  key,
662
837
  Math.min(1_000_000, (this.pendingSuccesses.get(key) ?? 0) + 1),
663
838
  );
839
+ const pending =
840
+ this.pendingConcurrencyObservations.get(key) ??
841
+ this.newPendingConcurrencyObservations(key);
842
+ if (
843
+ input.latencyMs != null &&
844
+ Number.isFinite(input.latencyMs) &&
845
+ input.latencyMs > 0
846
+ ) {
847
+ // This is only an amortization buffer. It has an explicit byte-equivalent
848
+ // item cap; overflowing samples remain successful rate observations but
849
+ // cannot grow sandbox memory indefinitely.
850
+ if (pending.observations.length < 4_096) {
851
+ pending.observations.push({
852
+ kind: 'success',
853
+ latencyMs: input.latencyMs,
854
+ });
855
+ }
856
+ }
857
+ // The same batch identity also protects the rate-success observation.
858
+ // A provider can report a valid success without a usable residence time;
859
+ // retaining an empty Gradient2 list still makes a lost acquire response
860
+ // unable to replay that rate recovery signal.
861
+ this.pendingConcurrencyObservations.set(key, pending);
862
+ }
863
+
864
+ private newPendingConcurrencyObservations(
865
+ key: string,
866
+ observations: Array<{ kind: 'success'; latencyMs: number }> = [],
867
+ ): PendingConcurrencyObservations {
868
+ const laneId =
869
+ this.concurrencyObservationLanes.get(key) ??
870
+ (() => {
871
+ const created = randomUUID();
872
+ this.concurrencyObservationLanes.set(key, created);
873
+ return created;
874
+ })();
875
+ const sequence =
876
+ (this.nextConcurrencyObservationSequence.get(key) ?? 0) + 1;
877
+ this.nextConcurrencyObservationSequence.set(key, sequence);
878
+ return {
879
+ batch: {
880
+ observerId: this.concurrencyObserverId,
881
+ laneId,
882
+ sequence,
883
+ },
884
+ observations,
885
+ };
664
886
  }
665
887
 
666
888
  private permitFor(
667
889
  bucketId: string,
668
- rateScopeToken: string | null,
669
- rules: PacingRule[],
670
- leaseId: string | null,
890
+ laneKey: string,
891
+ block: LeasedBlock,
892
+ scheduled: LeasedBlock['permits'][number],
671
893
  ): PacingPermit {
672
- if (!rules.some((rule) => rule.maxConcurrency != null)) {
673
- return noopPacingPermit();
674
- }
675
- let released = false;
894
+ const { rateScopeToken, rules } = block;
895
+ const [, leaseId, reservationId, concurrencyGrant, pacingGrant] = scheduled;
896
+ let settled = false;
897
+ const refund = async (): Promise<void> => {
898
+ if (!reservationId) {
899
+ throw new Error(
900
+ `Rate-state gateway returned an unrefundable permit for ${bucketId}.`,
901
+ );
902
+ }
903
+ await this.cancelReserved({
904
+ bucketId,
905
+ rateScopeToken,
906
+ rules,
907
+ leaseIds: leaseId ? [leaseId] : [],
908
+ reservationId,
909
+ });
910
+ };
676
911
  return {
912
+ concurrencyGrant,
913
+ pacingGrant,
677
914
  release: () => {
678
- if (released) return;
679
- released = true;
915
+ if (settled) return;
916
+ settled = true;
680
917
  this.releaseReserved(
681
918
  bucketId,
682
919
  rateScopeToken,
@@ -684,6 +921,24 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
684
921
  leaseId ? [leaseId] : [],
685
922
  );
686
923
  },
924
+ cancel: async () => {
925
+ if (settled) return;
926
+ settled = true;
927
+ await refund();
928
+ },
929
+ park: async () => {
930
+ if (settled) return;
931
+ settled = true;
932
+ // Only the block this permit was drawn from may take it back: a lane
933
+ // whose block expired, was drained by a penalty, or was replaced has
934
+ // already given up its grant, and the permit must be refunded instead.
935
+ if (this.blocks.get(laneKey) === block && block.expiresAt > this.now()) {
936
+ block.permits.push(scheduled);
937
+ block.permits.sort((a, b) => a[0] - b[0]);
938
+ return;
939
+ }
940
+ await refund();
941
+ },
687
942
  };
688
943
  }
689
944
 
@@ -725,10 +980,8 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
725
980
  });
726
981
  }
727
982
 
728
- private drawFromBlock(
729
- bucketId: string,
730
- laneKey: string,
731
- ): { permit: PacingPermit; scheduledAtMs: number } | null {
983
+ /** The lane's block, evicting (and releasing the leases of) an expired one. */
984
+ private liveBlock(bucketId: string, laneKey: string): LeasedBlock | null {
732
985
  const block = this.blocks.get(laneKey);
733
986
  if (!block) return null;
734
987
  if (block.expiresAt <= this.now()) {
@@ -745,19 +998,21 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
745
998
  );
746
999
  return null;
747
1000
  }
1001
+ return block;
1002
+ }
1003
+
1004
+ private drawFromBlock(
1005
+ bucketId: string,
1006
+ laneKey: string,
1007
+ ): { permit: PacingPermit; scheduledAtMs: number } | null {
1008
+ const block = this.liveBlock(bucketId, laneKey);
1009
+ if (!block) return null;
1010
+ // An emptied block stays until it expires: the next refill merges into it,
1011
+ // and a parked permit can only return to the block it was drawn from.
748
1012
  const scheduled = block.permits.shift();
749
- if (!scheduled) {
750
- this.blocks.delete(laneKey);
751
- return null;
752
- }
753
- if (block.permits.length === 0) this.blocks.delete(laneKey);
1013
+ if (!scheduled) return null;
754
1014
  return {
755
- permit: this.permitFor(
756
- bucketId,
757
- block.rateScopeToken,
758
- block.rules,
759
- scheduled[1],
760
- ),
1015
+ permit: this.permitFor(bucketId, laneKey, block, scheduled),
761
1016
  scheduledAtMs: scheduled[0],
762
1017
  };
763
1018
  }
@@ -776,6 +1031,40 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
776
1031
  }
777
1032
  }
778
1033
 
1034
+ /**
1035
+ * Return one permit that never reached the provider. This is deliberately a
1036
+ * synchronous acknowledgement boundary: the caller must not re-admit until
1037
+ * the original reservation is durably refunded, or a saturated local slot can
1038
+ * still turn into unbounded rate-token loss.
1039
+ */
1040
+ private async cancelReserved(input: {
1041
+ bucketId: string;
1042
+ rateScopeToken: string | null;
1043
+ rules: readonly PacingRule[];
1044
+ leaseIds: readonly string[];
1045
+ reservationId: string;
1046
+ }): Promise<void> {
1047
+ try {
1048
+ await this.port.release({
1049
+ bucketId: input.bucketId,
1050
+ rateScopeToken: input.rateScopeToken,
1051
+ rules: [...input.rules],
1052
+ leaseIds: [...input.leaseIds],
1053
+ refundReservationIds: [input.reservationId],
1054
+ schedulerSchema: this.schedulerSchema,
1055
+ });
1056
+ } catch (error) {
1057
+ const normalized = normalizeError(error);
1058
+ this.onStoreFailure({
1059
+ bucketId: input.bucketId,
1060
+ error: normalized.message,
1061
+ });
1062
+ throw new Error(
1063
+ `Rate-state cancellation failed for ${input.bucketId}: ${normalized.message}`,
1064
+ );
1065
+ }
1066
+ }
1067
+
779
1068
  private releaseReserved(
780
1069
  bucketId: string,
781
1070
  rateScopeToken: string | null,
@@ -887,6 +1176,71 @@ function pendingSuccessKey(
887
1176
  return rateLaneKey(bucketId, rulesSignature(rules));
888
1177
  }
889
1178
 
1179
+ function normalizeConcurrencyGrants(input: {
1180
+ value: unknown;
1181
+ granted: number;
1182
+ hasConcurrency: boolean;
1183
+ bucketId: string;
1184
+ }): Array<ProviderConcurrencyGrant | null> {
1185
+ if (!input.hasConcurrency) {
1186
+ if (Array.isArray(input.value) && input.value.length > 0) {
1187
+ throw new Error(
1188
+ `Rate-state store returned concurrency attribution for pure-rate bucket ${input.bucketId}.`,
1189
+ );
1190
+ }
1191
+ return Array.from({ length: input.granted }, () => null);
1192
+ }
1193
+ // The field is additive. A pre-expand gateway cannot provide attribution, so
1194
+ // keep its declared-cap behavior and decline to infer a throttle dimension.
1195
+ if (input.value === undefined) {
1196
+ return Array.from({ length: input.granted }, () => null);
1197
+ }
1198
+ if (!Array.isArray(input.value) || input.value.length !== input.granted) {
1199
+ throw new Error(
1200
+ `Rate-state store granted ${input.granted} concurrency permits for ${input.bucketId} with invalid attribution tokens.`,
1201
+ );
1202
+ }
1203
+ return input.value.map((value) => {
1204
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
1205
+ throw new Error(
1206
+ `Rate-state store returned an invalid concurrency attribution token for ${input.bucketId}.`,
1207
+ );
1208
+ }
1209
+ const candidate = value as Record<string, unknown>;
1210
+ const admissionId =
1211
+ typeof candidate.admissionId === 'string'
1212
+ ? candidate.admissionId.trim()
1213
+ : '';
1214
+ const inFlightAtStart = Number(candidate.inFlightAtStart);
1215
+ const targetRequestsPerSecond = candidate.targetRequestsPerSecond;
1216
+ const minimumRttMs = candidate.minimumRttMs;
1217
+ if (
1218
+ !admissionId ||
1219
+ admissionId.length > 64 ||
1220
+ !Number.isSafeInteger(inFlightAtStart) ||
1221
+ inFlightAtStart < 1 ||
1222
+ (targetRequestsPerSecond !== null &&
1223
+ (!Number.isFinite(targetRequestsPerSecond) ||
1224
+ Number(targetRequestsPerSecond) <= 0)) ||
1225
+ (minimumRttMs !== null &&
1226
+ (!Number.isFinite(minimumRttMs) || Number(minimumRttMs) <= 0))
1227
+ ) {
1228
+ throw new Error(
1229
+ `Rate-state store returned an invalid concurrency attribution token for ${input.bucketId}.`,
1230
+ );
1231
+ }
1232
+ return {
1233
+ admissionId,
1234
+ inFlightAtStart,
1235
+ targetRequestsPerSecond:
1236
+ targetRequestsPerSecond === null
1237
+ ? null
1238
+ : Number(targetRequestsPerSecond),
1239
+ minimumRttMs: minimumRttMs === null ? null : Number(minimumRttMs),
1240
+ };
1241
+ });
1242
+ }
1243
+
890
1244
  function leasedBlockTtlMs(rules: readonly PacingRule[]): number {
891
1245
  const shortestWindowMs = Math.min(
892
1246
  ...rules