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
@@ -211,6 +211,11 @@ import {
211
211
  TOOL_EXECUTE_RUNNER_ACCEPTS_VALUE,
212
212
  TOOL_EXECUTE_PROVIDER_RATE_SCOPE_HEADER,
213
213
  TOOL_EXECUTE_PROVIDER_RATE_RETRY_SNAPSHOT_HEADER,
214
+ TOOL_EXECUTE_PROVIDER_PERMIT_PATH_SUFFIX,
215
+ TOOL_EXECUTE_PROVIDER_PERMIT_RECEIVER_HEADER,
216
+ isProviderPermitExecuteReceiverUnavailable,
217
+ TOOL_EXECUTE_APP_PACING_REFUSAL_HEADER,
218
+ TOOL_EXECUTE_APP_PACING_REFUSAL_VALUE,
214
219
  isAttestedProvider429Response,
215
220
  classifyToolExecuteHttpFailure,
216
221
  createToolExecuteHttpFailureAttemptTracker,
@@ -362,6 +367,8 @@ import {
362
367
  type RuntimeMapHeapAdmissionSnapshot,
363
368
  } from './map-memory-limits';
364
369
  import { RuntimeMapRowFeed } from './runtime-map-row-feed';
370
+ import { ProviderRowRetryQueue } from './provider-row-retry-queue';
371
+ import type { ProviderRowRetry } from './provider-row-retry-queue';
365
372
  import type { RuntimeMapRowFeedSnapshot } from './runtime-map-row-feed';
366
373
  import { createRuntimeDispatchFlow, createRuntimeFlow } from './runtime-flow';
367
374
  import {
@@ -523,8 +530,62 @@ const rowContext = new AsyncLocalStorage<{
523
530
  resolverGap?: RowResolverGapTracker;
524
531
  /** Shared through nested fields, reset for each row execution attempt. */
525
532
  providerBackpressure?: { pending: PlayRowProviderBackpressureError | null };
533
+ replayCallIndexes?: RowReplayCallIndexes;
534
+ replayStepCaptures?: Map<string, number>[];
526
535
  }>();
527
536
 
537
+ /** Re-enter a held row with the call positions from its original attempt. */
538
+ class RowReplayCallIndexes {
539
+ private readonly recorded = new Map<string, number[]>();
540
+ private readonly positions = new Map<string, number>();
541
+ private readonly completedSteps = new Map<string, Map<string, number>>();
542
+
543
+ resetAttempt(): void {
544
+ this.positions.clear();
545
+ }
546
+
547
+ next(key: string, freshIndex: number): number {
548
+ const position = this.positions.get(key) ?? 0;
549
+ this.positions.set(key, position + 1);
550
+ let recorded = this.recorded.get(key);
551
+ if (!recorded) {
552
+ recorded = [];
553
+ this.recorded.set(key, recorded);
554
+ }
555
+ if (recorded[position] === undefined) recorded[position] = freshIndex;
556
+ return recorded[position]!;
557
+ }
558
+
559
+ completeStep(boundaryId: string, advances: Map<string, number>): void {
560
+ this.completedSteps.set(boundaryId, advances);
561
+ }
562
+
563
+ recoverStep(boundaryId: string): boolean {
564
+ // A cached step does not run its body. Consume its recorded nested calls
565
+ // so the next sibling cannot inherit one of their invocation positions.
566
+ const advances = this.completedSteps.get(boundaryId);
567
+ if (!advances) return false;
568
+ for (const [key, advance] of advances) {
569
+ this.positions.set(key, (this.positions.get(key) ?? 0) + advance);
570
+ recordReplayCallAdvance(key, advance);
571
+ }
572
+ return true;
573
+ }
574
+ }
575
+
576
+ function recordReplayCallAdvance(key: string, advance: number): void {
577
+ for (const capture of rowContext.getStore()?.replayStepCaptures ?? []) {
578
+ capture.set(key, (capture.get(key) ?? 0) + advance);
579
+ }
580
+ }
581
+
582
+ function nextReplayableCallIndex(key: string, freshIndex: number): number {
583
+ const indexes = rowContext.getStore()?.replayCallIndexes;
584
+ if (!indexes) return freshIndex;
585
+ recordReplayCallAdvance(key, 1);
586
+ return indexes.next(key, freshIndex);
587
+ }
588
+
528
589
  /**
529
590
  * The open end of one `rowResolverGap` segment.
530
591
  *
@@ -2714,6 +2775,8 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
2714
2775
  private toolDispatcherFailure: unknown | null = null;
2715
2776
  /** Active provider HTTP attempts in this Play execution, grouped by provider. */
2716
2777
  private readonly inFlightProviderRequests = new Map<string, number>();
2778
+ /** A row replay must not restart this call's spent live-sandbox wait budget. */
2779
+ private readonly exhaustedProviderResidentWaitCalls = new Set<string>();
2717
2780
  /**
2718
2781
  * Deepline's own zero-credit denial is run-fatal. Keep it independently of
2719
2782
  * customer control flow so a broad `try/catch` cannot turn it into a
@@ -9260,12 +9323,18 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9260
9323
  // aliases ride on non-enumerable props after stripCsvProjectionMetadata,
9261
9324
  // so a spread would silently drop them before the sheet write and the
9262
9325
  // aliases would never persist as visible input cells.
9263
- const mapStartRows = rawItems.map((row, index) => ({
9264
- ...toSerializableCsvAliasedRow(row),
9265
- ...mapRowOutcomeRuntimeFields({
9266
- key: seedRowIdentity(row, index),
9267
- }),
9268
- }));
9326
+ // A resumed checkpoint contains only pending rows. Preserve their
9327
+ // original indexes through admission instead of renumbering the subset.
9328
+ const mapStartRows = rawItems.map((row, index) => {
9329
+ const inputIndex = itemOriginalIndexes[index] ?? index;
9330
+ return {
9331
+ ...toSerializableCsvAliasedRow(row),
9332
+ ...mapRowOutcomeRuntimeFields({
9333
+ key: seedRowIdentity(row, inputIndex),
9334
+ inputIndex,
9335
+ }),
9336
+ };
9337
+ });
9269
9338
  const mapStartResult = await this.#options.onMapStart(
9270
9339
  mapStartRows,
9271
9340
  resolvedTableNamespace,
@@ -9303,7 +9372,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9303
9372
  const seededItems = materializedItems
9304
9373
  .map((item, index) => ({
9305
9374
  row: this.toOutputRow(item as Record<string, unknown>),
9306
- index,
9375
+ index: originalInputIndexByItem.get(item) ?? index,
9307
9376
  }))
9308
9377
  .map(({ row, index }) => {
9309
9378
  // Look up by the SAME seed identity the stamp above used — the
@@ -9332,7 +9401,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9332
9401
  )
9333
9402
  : row,
9334
9403
  index,
9335
- originalIndex: itemOriginalIndexes[index] ?? index,
9404
+ // `index` already names the authored input, not a position in
9405
+ // the pending subset. Looking it up again aliases resumed rows.
9406
+ originalIndex: index,
9336
9407
  };
9337
9408
  });
9338
9409
  const admittedItems =
@@ -10268,16 +10339,31 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
10268
10339
  );
10269
10340
  let nextRowIndex = 0;
10270
10341
  let firstRowError: unknown = null;
10271
- const providerRetryDeadlines: Array<{
10272
- provider: string;
10273
- retryAtMs: number;
10274
- }> = [];
10275
- let providerBackpressureRowCount = 0;
10276
- const waitingBaseRows = new Map<number, Record<string, unknown>>();
10342
+ const providerRowRetries = new ProviderRowRetryQueue();
10343
+ const parkedProviderRows = new ProviderRowRetryQueue();
10344
+ const nextProviderRetry = (includeParked: boolean) => {
10345
+ const resident = providerRowRetries.peek();
10346
+ const parked = includeParked ? parkedProviderRows.peek() : undefined;
10347
+ return !resident || (parked && parked.readyAtMs < resident.readyAtMs)
10348
+ ? parked
10349
+ : resident;
10350
+ };
10351
+ const retryWakeFeed = rowFeed ?? new RuntimeMapRowFeed();
10352
+ const waitingBaseRows = new Map<
10353
+ number,
10354
+ {
10355
+ baseRow: Record<string, unknown>;
10356
+ callIndexes: RowReplayCallIndexes;
10357
+ }
10358
+ >();
10277
10359
 
10278
10360
  const runRow = async (
10279
10361
  idx: number,
10280
10362
  baseRow: Record<string, unknown>,
10363
+ retryAfterRelease: {
10364
+ pending: ProviderRowRetry | null;
10365
+ callIndexes: RowReplayCallIndexes;
10366
+ },
10281
10367
  ): Promise<MapRowExecutionResult> => {
10282
10368
  const item = items[idx];
10283
10369
  const rowKey = executionRowKey(baseRow, idx);
@@ -10350,6 +10436,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
10350
10436
  mapStallResolverToken: resolverObservation?.token,
10351
10437
  resolverGap,
10352
10438
  providerBackpressure,
10439
+ replayCallIndexes: retryAfterRelease.callIndexes,
10353
10440
  },
10354
10441
  async () => {
10355
10442
  try {
@@ -10559,7 +10646,10 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
10559
10646
  return COMPLETED_ROW;
10560
10647
  } catch (error) {
10561
10648
  if (isPlayRowExecutionSuspendedError(error)) {
10562
- waitingBaseRows.set(idx, baseRow);
10649
+ waitingBaseRows.set(idx, {
10650
+ baseRow,
10651
+ callIndexes: retryAfterRelease.callIndexes,
10652
+ });
10563
10653
  this.pendingRowEventBoundaries.push(error.boundary);
10564
10654
  mapProgress.update({
10565
10655
  pendingRowKey: rowKey,
@@ -10582,14 +10672,21 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
10582
10672
  return WAITING_ROW;
10583
10673
  }
10584
10674
  if (error instanceof PlayRowProviderBackpressureError) {
10585
- waitingBaseRows.set(idx, baseRow);
10675
+ waitingBaseRows.set(idx, {
10676
+ baseRow,
10677
+ callIndexes: retryAfterRelease.callIndexes,
10678
+ });
10586
10679
  // Each row retains its own deadline. Provider names are not rate
10587
10680
  // scopes: two credentials for one provider can recover separately.
10588
- providerRetryDeadlines.push({
10681
+ retryAfterRelease.pending = {
10682
+ rowIndex: idx,
10589
10683
  provider: error.provider,
10590
10684
  retryAtMs: error.retryAtMs,
10591
- });
10592
- providerBackpressureRowCount += 1;
10685
+ readyAtMs: Math.max(Date.now() + 1_000, error.retryAtMs),
10686
+ ...(error.residentWaitBudgetExhausted
10687
+ ? { residentWaitBudgetExhausted: true }
10688
+ : {}),
10689
+ };
10593
10690
  mapProgress.update({ pendingRowKey: rowKey });
10594
10691
  const fieldName = activeFieldName;
10595
10692
  if (!fieldName) {
@@ -10703,7 +10800,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
10703
10800
  this.#options.rowAdmissionSignal?.reason ??
10704
10801
  new Error('Row admission stopped.');
10705
10802
  heapAdmission.close(firstRowError);
10706
- rowFeed?.fail(firstRowError);
10803
+ retryWakeFeed.fail(firstRowError);
10707
10804
  };
10708
10805
  this.#options.rowAdmissionSignal?.addEventListener('abort', stopAdmission, {
10709
10806
  once: true,
@@ -10718,7 +10815,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
10718
10815
  if (firstRowError == null) firstRowError = cause;
10719
10816
  this.wakeToolDispatcher();
10720
10817
  heapAdmission.close(firstRowError);
10721
- rowFeed?.fail(firstRowError);
10818
+ retryWakeFeed.fail(firstRowError);
10722
10819
  };
10723
10820
  const unsubscribePersistenceLatch = onRuntimePersistenceLatchTripped(
10724
10821
  this.persistenceLatch,
@@ -10736,29 +10833,109 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
10736
10833
  PROGRESS_HEARTBEAT_INTERVAL_MS,
10737
10834
  );
10738
10835
  mapActivityTimer.unref();
10836
+ // Short holds should reuse the live sandbox after every row returns its
10837
+ // leases. Bound total idle residency from the first all-waiting transition,
10838
+ // rather than resetting it for each successive provider response.
10839
+ let providerIdleWaitStartedAt: number | null = null;
10840
+ const canWaitForProviderRetry = (retry: ProviderRowRetry) => {
10841
+ const elapsedMs =
10842
+ providerIdleWaitStartedAt === null
10843
+ ? 0
10844
+ : performance.now() - providerIdleWaitStartedAt;
10845
+ return (
10846
+ Math.max(1, retry.readyAtMs - Date.now()) <=
10847
+ Math.max(0, TOOL_PROVIDER_RESIDENT_WAIT_BUDGET_MS - elapsedMs)
10848
+ );
10849
+ };
10739
10850
  const workers = Array.from({ length: workerCount }, async () => {
10740
10851
  while (firstRowError == null) {
10741
- if (nextRowIndex >= items.length) {
10852
+ // A short provider hold must not leave this row behind the entire
10853
+ // fresh backlog. Its previous flow released both admission leases;
10854
+ // take a new one after its own deadline while other rows make progress.
10855
+ const nextReadyRetry = nextProviderRetry(true);
10856
+ const providerRetry = nextReadyRetry?.residentWaitBudgetExhausted
10857
+ ? parkedProviderRows.takeReady(Date.now())
10858
+ : providerRowRetries.takeReady(Date.now());
10859
+ if (!providerRetry && nextRowIndex >= items.length) {
10860
+ // Spent calls may join productive work, but only an unspent call can
10861
+ // keep an otherwise idle sandbox waiting for its next deadline.
10862
+ const nextRetry = nextProviderRetry(mapProgress.activeResolvers > 0);
10863
+ if (
10864
+ nextRetry &&
10865
+ (mapProgress.activeResolvers > 0 ||
10866
+ canWaitForProviderRetry(nextRetry)) &&
10867
+ (!rowFeed || rowFeed.closed)
10868
+ ) {
10869
+ if (
10870
+ mapProgress.activeResolvers === 0 &&
10871
+ providerIdleWaitStartedAt === null
10872
+ )
10873
+ providerIdleWaitStartedAt = performance.now();
10874
+ await retryWakeFeed.waitForWorkerWake(nextRetry.readyAtMs);
10875
+ continue;
10876
+ }
10742
10877
  if (!rowFeed) return;
10878
+ if (nextRetry) rowFeed.wakeWorkersAt(nextRetry.readyAtMs);
10743
10879
  const more = await rowFeed.waitForRows(nextRowIndex);
10744
- if (!more) return;
10880
+ const pendingRetry = nextProviderRetry(
10881
+ mapProgress.activeResolvers > 0,
10882
+ );
10883
+ if (
10884
+ !more &&
10885
+ !(
10886
+ pendingRetry &&
10887
+ (mapProgress.activeResolvers > 0 ||
10888
+ canWaitForProviderRetry(pendingRetry))
10889
+ )
10890
+ )
10891
+ return;
10745
10892
  continue;
10746
10893
  }
10747
- const idx = nextRowIndex;
10748
- nextRowIndex += 1;
10749
- rowFeed?.markDispatched();
10894
+ const idx = providerRetry?.rowIndex ?? nextRowIndex++;
10895
+ if (!providerRetry) rowFeed?.markDispatched();
10750
10896
  try {
10751
- const baseRow = this.toOutputRow(items[idx]);
10752
- releaseDispatchedInputRow?.(idx);
10897
+ const waitingRow = providerRetry
10898
+ ? waitingBaseRows.get(idx)
10899
+ : undefined;
10900
+ const baseRow = providerRetry
10901
+ ? waitingRow?.baseRow
10902
+ : this.toOutputRow(items[idx]);
10903
+ if (!baseRow) {
10904
+ throw new Error(
10905
+ `Provider retry row ${idx} lost its resumable input.`,
10906
+ );
10907
+ }
10908
+ if (providerRetry) waitingBaseRows.delete(idx);
10909
+ else releaseDispatchedInputRow?.(idx);
10910
+ const callIndexes =
10911
+ waitingRow?.callIndexes ?? new RowReplayCallIndexes();
10912
+ callIndexes.resetAttempt();
10913
+ const retryAfterRelease = {
10914
+ pending: null as ProviderRowRetry | null,
10915
+ callIndexes,
10916
+ };
10753
10917
  settledResults[idx] = await mapRowFlow.run({ baseRow }, async () => {
10754
10918
  this.assertRowAdmissionOpen();
10755
- mapProgress.resolverStarted();
10919
+ mapProgress.resolverStarted({ retry: Boolean(providerRetry) });
10756
10920
  try {
10757
- return await runRow(idx, baseRow);
10921
+ return await runRow(idx, baseRow, retryAfterRelease);
10758
10922
  } finally {
10759
10923
  mapProgress.resolverFinished();
10760
10924
  }
10761
10925
  });
10926
+ // Publish only after both flow leases have returned. Another worker
10927
+ // must never re-admit this index while the old attempt is unwinding.
10928
+ if (retryAfterRelease.pending) {
10929
+ if (retryAfterRelease.pending.residentWaitBudgetExhausted) {
10930
+ parkedProviderRows.add(retryAfterRelease.pending);
10931
+ } else {
10932
+ providerRowRetries.add(retryAfterRelease.pending);
10933
+ }
10934
+ rowFeed?.wakeWorkersAt(nextProviderRetry(true)!.readyAtMs);
10935
+ } else if (settledResults[idx] !== WAITING_ROW) {
10936
+ providerIdleWaitStartedAt = null;
10937
+ }
10938
+ if (mapProgress.activeResolvers === 0) retryWakeFeed.wakeWorkers();
10762
10939
  } catch (error) {
10763
10940
  // RuntimeFlow has already returned the row slot and memory lease.
10764
10941
  firstRowError = this.pendingDurableSuspension
@@ -10766,7 +10943,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
10766
10943
  : (firstRowError ?? error);
10767
10944
  this.wakeToolDispatcher();
10768
10945
  heapAdmission.close(firstRowError);
10769
- rowFeed?.fail(firstRowError);
10946
+ retryWakeFeed.fail(firstRowError);
10770
10947
  throw error;
10771
10948
  }
10772
10949
  }
@@ -10852,6 +11029,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
10852
11029
  }
10853
11030
  } finally {
10854
11031
  clearInterval(mapActivityTimer);
11032
+ retryWakeFeed.wakeWorkers();
10855
11033
  publishMapActivity();
10856
11034
  datasetStep.items = currentTotalRows();
10857
11035
  this.processedRowCount = currentTotalRows();
@@ -10932,19 +11110,21 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
10932
11110
  }
10933
11111
  if (
10934
11112
  this.pendingRowEventBoundaries.length > 0 ||
10935
- providerRetryDeadlines.length > 0
11113
+ providerRowRetries.size > 0 ||
11114
+ parkedProviderRows.size > 0
10936
11115
  ) {
11116
+ const waitingProviderRows =
11117
+ providerRowRetries.size + parkedProviderRows.size;
10937
11118
  const waitingRowKeys = new Set<string>();
10938
11119
  const previousSuspendedInputs =
10939
11120
  this.checkpoint.mapFrames?.[mapScope.mapInvocationId]
10940
11121
  ?.suspendedInputRows;
10941
11122
  const suspendedInputRows =
10942
- rowFeed ||
10943
- (providerRetryDeadlines.length === 0 && !previousSuspendedInputs)
11123
+ rowFeed || (waitingProviderRows === 0 && !previousSuspendedInputs)
10944
11124
  ? undefined
10945
11125
  : rowResults.flatMap((result, index) => {
10946
11126
  if (result !== WAITING_ROW) return [];
10947
- const baseRow = waitingBaseRows.get(index);
11127
+ const baseRow = waitingBaseRows.get(index)?.baseRow;
10948
11128
  if (!baseRow) {
10949
11129
  throw new Error(
10950
11130
  `Waiting row ${index} lost its resumable input before map suspension.`,
@@ -10976,13 +11156,13 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
10976
11156
  ]),
10977
11157
  ).values(),
10978
11158
  ];
10979
- const earliestHold = providerRetryDeadlines.reduce<
10980
- (typeof providerRetryDeadlines)[number] | null
10981
- >(
10982
- (earliest, next) =>
10983
- !earliest || next.retryAtMs < earliest.retryAtMs ? next : earliest,
10984
- null,
10985
- );
11159
+ const residentHold = providerRowRetries.peek();
11160
+ const parkedHold = parkedProviderRows.peek();
11161
+ const earliestHold =
11162
+ !residentHold ||
11163
+ (parkedHold && parkedHold.retryAtMs < residentHold.retryAtMs)
11164
+ ? parkedHold
11165
+ : residentHold;
10986
11166
  const providerWakeAtMs = earliestHold
10987
11167
  ? Math.max(Date.now() + 1_000, earliestHold.retryAtMs)
10988
11168
  : null;
@@ -11005,7 +11185,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
11005
11185
  provider: earliestHold.provider,
11006
11186
  retry_at: new Date(providerWakeAtMs).toISOString(),
11007
11187
  wait_ms: Math.max(0, providerWakeAtMs - Date.now()),
11008
- waiting_rows: providerBackpressureRowCount,
11188
+ waiting_rows: waitingProviderRows,
11009
11189
  table_namespace: normalizedTableNamespace,
11010
11190
  })}`,
11011
11191
  { level: 'warn' },
@@ -12534,8 +12714,15 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
12534
12714
  ].join(':')
12535
12715
  : 'workflow';
12536
12716
  const callIndexKey = `${this.currentExecutionScope.receipt.namespace}:${logicalCallScope}:${normalizedKey}:${toolRequestIdentity}`;
12537
- const callIndex = this.toolCallIndexByKey.get(callIndexKey) ?? 0;
12538
- this.toolCallIndexByKey.set(callIndexKey, callIndex + 1);
12717
+ const freshCallIndex = this.toolCallIndexByKey.get(callIndexKey) ?? 0;
12718
+ const callIndex = nextReplayableCallIndex(
12719
+ `tool:${callIndexKey}`,
12720
+ freshCallIndex,
12721
+ );
12722
+ this.toolCallIndexByKey.set(
12723
+ callIndexKey,
12724
+ Math.max(freshCallIndex, callIndex + 1),
12725
+ );
12539
12726
  const logicalCallId = stableDigest(`${callIndexKey}:${callIndex}`);
12540
12727
  this.resourceGovernor.recordLogicalToolCall();
12541
12728
  let executionAuthScopeDigest =
@@ -14399,8 +14586,14 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
14399
14586
  : Math.max(0, Math.round(ms));
14400
14587
  if (this.#options.durableBoundaries) {
14401
14588
  const rowStore = rowContext.getStore();
14402
- const legacyIndex = this.sleepBoundaryIndex;
14403
- this.sleepBoundaryIndex += 1;
14589
+ const legacyIndex = nextReplayableCallIndex(
14590
+ 'sleep:legacy',
14591
+ this.sleepBoundaryIndex,
14592
+ );
14593
+ this.sleepBoundaryIndex = Math.max(
14594
+ this.sleepBoundaryIndex,
14595
+ legacyIndex + 1,
14596
+ );
14404
14597
  const legacyBoundaryId = this.durableBoundaryId(
14405
14598
  `sleep-${legacyIndex}-${delayMs}`,
14406
14599
  );
@@ -14414,9 +14607,16 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
14414
14607
  ? `${rowStore.mapScope.mapInvocationId}:${rowStore.rowKey ?? rowStore.rowId}`
14415
14608
  : `row:${rowStore.rowKey ?? rowStore.rowId}`;
14416
14609
  const scopedIndexKey = `${this.currentExecutionScope.receipt.namespace}:${rowScope}`;
14417
- const scopedIndex =
14610
+ const freshScopedIndex =
14418
14611
  this.sleepBoundaryIndexByScope.get(scopedIndexKey) ?? 0;
14419
- this.sleepBoundaryIndexByScope.set(scopedIndexKey, scopedIndex + 1);
14612
+ const scopedIndex = nextReplayableCallIndex(
14613
+ `sleep:${scopedIndexKey}`,
14614
+ freshScopedIndex,
14615
+ );
14616
+ this.sleepBoundaryIndexByScope.set(
14617
+ scopedIndexKey,
14618
+ Math.max(freshScopedIndex, scopedIndex + 1),
14619
+ );
14420
14620
  const scopedBoundaryId = this.durableBoundaryId(
14421
14621
  `sleep-${stableDigest(`${rowScope}:${scopedIndex}`)}-${delayMs}`,
14422
14622
  );
@@ -14771,8 +14971,15 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
14771
14971
  const rowStore = rowContext.getStore();
14772
14972
  const scope = rowStore ? `row-${rowStore.rowId}` : 'workflow';
14773
14973
  const callIndexKey = `${this.currentExecutionScope.receipt.namespace}:${scope}:${normalizedKey}`;
14774
- const callIndex = this.stepCallIndexByKey.get(callIndexKey) ?? 0;
14775
- this.stepCallIndexByKey.set(callIndexKey, callIndex + 1);
14974
+ const freshCallIndex = this.stepCallIndexByKey.get(callIndexKey) ?? 0;
14975
+ const callIndex = nextReplayableCallIndex(
14976
+ `step:${callIndexKey}`,
14977
+ freshCallIndex,
14978
+ );
14979
+ this.stepCallIndexByKey.set(
14980
+ callIndexKey,
14981
+ Math.max(freshCallIndex, callIndex + 1),
14982
+ );
14776
14983
  const boundarySuffix = callIndex === 0 ? '' : `:${callIndex}`;
14777
14984
  const boundaryId = this.durableBoundaryId(
14778
14985
  `step-${scope}:${normalizedKey}${boundarySuffix}`,
@@ -14783,6 +14990,8 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
14783
14990
  this.checkpoint.resolvedBoundaries?.[boundaryId],
14784
14991
  );
14785
14992
  if (existing?.kind === 'step' && 'output' in existing) {
14993
+ const replayedCalls =
14994
+ rowStore?.replayCallIndexes?.recoverStep(boundaryId) ?? false;
14786
14995
  this.runtimeLog(
14787
14996
  `ctx.step(${normalizedKey}): recovered result from checkpoint`,
14788
14997
  );
@@ -14792,6 +15001,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
14792
15001
  // the next sibling operation. This is deliberately run-local state;
14793
15002
  // it is not a cross-run sleep cache or an input-derived identity.
14794
15003
  if (
15004
+ !replayedCalls &&
14795
15005
  Number.isSafeInteger(existing.sleepCount) &&
14796
15006
  existing.sleepCount !== undefined &&
14797
15007
  existing.sleepCount > 0
@@ -14802,7 +15012,19 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
14802
15012
  }
14803
15013
 
14804
15014
  const sleepBoundaryIndexBefore = this.sleepBoundaryIndex;
14805
- const output = await run();
15015
+ const callAdvances = new Map<string, number>();
15016
+ const output = rowStore?.replayCallIndexes
15017
+ ? await rowContext.run(
15018
+ {
15019
+ ...rowStore,
15020
+ replayStepCaptures: [
15021
+ ...(rowStore.replayStepCaptures ?? []),
15022
+ callAdvances,
15023
+ ],
15024
+ },
15025
+ run,
15026
+ )
15027
+ : await run();
14806
15028
  assertJsonSerializableStepOutput(normalizedKey, output);
14807
15029
  // A caught durable suspension is still the invocation's outcome. Never
14808
15030
  // publish an enclosing step fallback that would make a later resume skip
@@ -14814,11 +15036,14 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
14814
15036
  kind: 'step',
14815
15037
  stepId: normalizedKey,
14816
15038
  output,
14817
- sleepCount: this.sleepBoundaryIndex - sleepBoundaryIndexBefore,
15039
+ sleepCount: rowStore?.replayCallIndexes
15040
+ ? (callAdvances.get('sleep:legacy') ?? 0)
15041
+ : this.sleepBoundaryIndex - sleepBoundaryIndexBefore,
14818
15042
  completedAt: Date.now(),
14819
15043
  },
14820
15044
  };
14821
15045
  this.#options.onBatchComplete?.(this.checkpoint);
15046
+ rowStore?.replayCallIndexes?.completeStep(boundaryId, callAdvances);
14822
15047
  return output;
14823
15048
  };
14824
15049
 
@@ -17607,9 +17832,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
17607
17832
  : options?.providerOperationForceRefreshIdentities;
17608
17833
  const executionRequestNonce = crypto.randomUUID();
17609
17834
  // Transport ambiguity must retain the billing identity. An ordinary
17610
- // (unfenced) call advances it only after a definite provider-owned
17611
- // 429 released that attempt's reservation. Fenced calls retain it:
17612
- // the gateway-qualified retry owns their distinct charge lifecycle.
17835
+ // (unfenced) call advances it after a definite rejection released
17836
+ // that attempt's reservation, including app pacing refusals. Fenced
17837
+ // calls retain it: gateway-qualified retries own their distinct charge lifecycle.
17613
17838
  let deeplineRequestId = `ctx-tool-${stableDigest(executionRequestNonce).slice(0, 32)}`;
17614
17839
  // The provider operation has a semantic identity independent of
17615
17840
  // the response receipt. Its key is supplied by the caller that owns
@@ -17769,10 +17994,17 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
17769
17994
  ? { integration_mode: this.#options.integrationMode }
17770
17995
  : {}),
17771
17996
  });
17997
+ // Legacy retry migration retains this transport identity even when
17998
+ // the current checkpoint cursor index belongs to a newer batch.
17999
+ const providerResidentWaitKey = durableCallReceiptKey;
17772
18000
  let transportAttempt = 0;
17773
18001
  let invocationAttempt =
17774
18002
  savedProviderRetryCursor?.invocationAttempt ?? 0;
17775
18003
  clearProviderRetryCursor = () => {
18004
+ if (providerResidentWaitKey)
18005
+ this.exhaustedProviderResidentWaitCalls.delete(
18006
+ providerResidentWaitKey,
18007
+ );
17776
18008
  if (
17777
18009
  !providerRetryCursorKey ||
17778
18010
  !this.checkpoint.providerRetryCursors
@@ -17838,6 +18070,13 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
17838
18070
  // work. Repeated zero or expired holds must also yield durably.
17839
18071
  let residentProviderWaitStartedAt: number | null = null;
17840
18072
  const canWaitForProviderInSandbox = (waitMs: number) => {
18073
+ if (
18074
+ providerResidentWaitKey &&
18075
+ this.exhaustedProviderResidentWaitCalls.has(
18076
+ providerResidentWaitKey,
18077
+ )
18078
+ )
18079
+ return false;
17841
18080
  const elapsedMs =
17842
18081
  residentProviderWaitStartedAt === null
17843
18082
  ? 0
@@ -17847,6 +18086,21 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
17847
18086
  Math.max(0, TOOL_PROVIDER_RESIDENT_WAIT_BUDGET_MS - elapsedMs)
17848
18087
  );
17849
18088
  };
18089
+ const markProviderResidentWaitBudgetExhausted = () => {
18090
+ const exhausted =
18091
+ residentProviderWaitStartedAt !== null ||
18092
+ Boolean(
18093
+ providerResidentWaitKey &&
18094
+ this.exhaustedProviderResidentWaitCalls.has(
18095
+ providerResidentWaitKey,
18096
+ ),
18097
+ );
18098
+ if (exhausted && providerResidentWaitKey)
18099
+ this.exhaustedProviderResidentWaitCalls.add(
18100
+ providerResidentWaitKey,
18101
+ );
18102
+ return exhausted;
18103
+ };
17850
18104
  const waitForToolRetry = async (
17851
18105
  waitMs: number,
17852
18106
  providerHold: boolean,
@@ -17947,6 +18201,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
17947
18201
  // the tool-keyed dispatch identity so terminal attribution can join
17948
18202
  // provider admission with the response residence it admitted.
17949
18203
  let providerTelemetryKey: string | null = null;
18204
+ let providerPermitReceiverUnavailable = false;
17950
18205
  let providerRunInFlightAtDispatch: number | null = null;
17951
18206
  const acquireProviderPermit = async (signal?: AbortSignal) => {
17952
18207
  options?.onProviderAdmissionPending?.();
@@ -17955,6 +18210,10 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
17955
18210
  toolId,
17956
18211
  signal,
17957
18212
  yieldProviderHolds: this.#options.durableBoundaries === true,
18213
+ ...(invocationAttempt > 0 ||
18214
+ residentProviderWaitStartedAt !== null
18215
+ ? { retryPriority: true }
18216
+ : {}),
17958
18217
  });
17959
18218
  providerConcurrencyGrant = permit.concurrencyGrant ?? null;
17960
18219
  providerPacingGrant = permit.pacingGrant ?? null;
@@ -18000,6 +18259,14 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
18000
18259
  release: () => void;
18001
18260
  } | null = null;
18002
18261
  let fetchDispatched = false;
18262
+ let dispatchedWithProviderPermit = false;
18263
+ let providerPermitReceiverRejected = false;
18264
+ let providerDispatchCounted = false;
18265
+ const recordProviderDispatch = () => {
18266
+ if (providerDispatchCounted) return;
18267
+ providerDispatchCounted = true;
18268
+ this.resourceGovernor.recordProviderDispatch();
18269
+ };
18003
18270
  // A provider 429 must be reported while its server-minted
18004
18271
  // admission lease is still live. Keep its parsed outcome here so
18005
18272
  // the ordinary retry boundary can reuse it after Flow releases the
@@ -18142,6 +18409,8 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
18142
18409
  signal: abortController?.signal,
18143
18410
  requiresTransportPermit: !fixtureExecution,
18144
18411
  deferProviderPermitValidation: true,
18412
+ providerDispatchAtGateway:
18413
+ !providerPermitReceiverUnavailable,
18145
18414
  onTransportAdmitted: (waitMs) => {
18146
18415
  transportSlotWaitMs = waitMs;
18147
18416
  },
@@ -18327,63 +18596,90 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
18327
18596
  logProvider429RetryPhase('http_dispatch_started');
18328
18597
  activeProviderRequest =
18329
18598
  this.beginProviderRequest(provider);
18330
- // Count at dispatch, including ambiguous transport
18331
- // failures that never produce a residence sample.
18332
- this.resourceGovernor.recordProviderDispatch();
18333
- const dispatchedResponse = await fetch(url, {
18334
- method: 'POST',
18335
- signal: abortController?.signal,
18336
- headers: {
18337
- 'Content-Type': 'application/json',
18338
- Authorization: `Bearer ${this.#options.executorToken}`,
18339
- [EXECUTE_RESPONSE_CONTRACT_HEADER]:
18340
- RAW_V2_TOOL_RESPONSE_CONTRACT,
18341
- [EXECUTE_RESPONSE_INTENT_HEADER]: 'dataset',
18342
- [EXECUTE_TOOL_METADATA_HEADER]: 'true',
18343
- [TOOL_EXECUTION_ERROR_SCHEMA_HEADER]: String(
18344
- toolErrorSchemaVersion,
18345
- ),
18346
- // Tell the gateway which protective outcomes this
18347
- // runner can absorb. The relay may only shed a
18348
- // provider execute with the Deepline concurrency
18349
- // shape when this token is present; without it the
18350
- // relay must queue, because an older artifact would
18351
- // settle the row as failed instead of pacing.
18352
- [TOOL_EXECUTE_RUNNER_ACCEPTS_HEADER]:
18353
- TOOL_EXECUTE_RUNNER_ACCEPTS_VALUE,
18354
- ...(providerGatewayDispatchProof
18355
- ? {
18356
- [TOOL_EXECUTE_PROVIDER_RATE_SCOPE_HEADER]:
18357
- providerGatewayDispatchProof.rateScopeToken,
18358
- [TOOL_EXECUTE_PROVIDER_RATE_RETRY_SNAPSHOT_HEADER]:
18359
- String(
18360
- providerGatewayDispatchProof.retryAtMsAtGrant,
18361
- ),
18362
- }
18363
- : {}),
18364
- 'x-deepline-request-id': deeplineRequestId,
18365
- ...(providerExecutionFenceKey
18366
- ? {
18367
- 'x-deepline-idempotency-key':
18368
- providerExecutionFenceKey,
18369
- }
18370
- : {}),
18371
- ...protectionHeaders,
18372
- ...(this.#options.runtimeTestFaultHeader
18373
- ? {
18374
- 'x-deepline-test-fault':
18375
- this.#options.runtimeTestFaultHeader,
18376
- }
18377
- : {}),
18599
+
18600
+ // The new action binds delegated validation to the
18601
+ // actual receiver. Older gateways refuse this path
18602
+ // before admission; direct app routes do not serve it.
18603
+ dispatchedWithProviderPermit =
18604
+ !providerPermitReceiverUnavailable &&
18605
+ providerGatewayDispatchProof?.dispatchValidationSupported ===
18606
+ true;
18607
+ if (!dispatchedWithProviderPermit)
18608
+ recordProviderDispatch();
18609
+ const dispatchedResponse = await fetch(
18610
+ dispatchedWithProviderPermit
18611
+ ? `${url}${TOOL_EXECUTE_PROVIDER_PERMIT_PATH_SUFFIX}`
18612
+ : url,
18613
+ {
18614
+ method: 'POST',
18615
+ signal: abortController?.signal,
18616
+ headers: {
18617
+ 'Content-Type': 'application/json',
18618
+ Authorization: `Bearer ${this.#options.executorToken}`,
18619
+ [EXECUTE_RESPONSE_CONTRACT_HEADER]:
18620
+ RAW_V2_TOOL_RESPONSE_CONTRACT,
18621
+ [EXECUTE_RESPONSE_INTENT_HEADER]: 'dataset',
18622
+ [EXECUTE_TOOL_METADATA_HEADER]: 'true',
18623
+ [TOOL_EXECUTION_ERROR_SCHEMA_HEADER]: String(
18624
+ toolErrorSchemaVersion,
18625
+ ),
18626
+ // Tell the gateway which protective outcomes this
18627
+ // runner can absorb. The relay may only shed a
18628
+ // provider execute with the Deepline concurrency
18629
+ // shape when this token is present; without it the
18630
+ // relay must queue, because an older artifact would
18631
+ // settle the row as failed instead of pacing.
18632
+ [TOOL_EXECUTE_RUNNER_ACCEPTS_HEADER]:
18633
+ TOOL_EXECUTE_RUNNER_ACCEPTS_VALUE,
18634
+ ...(!usesDurableInvocationFence &&
18635
+ !providerExecutionFenceKey
18636
+ ? {
18637
+ [TOOL_EXECUTE_APP_PACING_REFUSAL_HEADER]:
18638
+ TOOL_EXECUTE_APP_PACING_REFUSAL_VALUE,
18639
+ }
18640
+ : {}),
18641
+ ...(providerGatewayDispatchProof
18642
+ ? {
18643
+ [TOOL_EXECUTE_PROVIDER_RATE_SCOPE_HEADER]:
18644
+ providerGatewayDispatchProof.rateScopeToken,
18645
+ [TOOL_EXECUTE_PROVIDER_RATE_RETRY_SNAPSHOT_HEADER]:
18646
+ String(
18647
+ providerGatewayDispatchProof.retryAtMsAtGrant,
18648
+ ),
18649
+ }
18650
+ : {}),
18651
+ 'x-deepline-request-id': deeplineRequestId,
18652
+ ...(providerExecutionFenceKey
18653
+ ? {
18654
+ 'x-deepline-idempotency-key':
18655
+ providerExecutionFenceKey,
18656
+ }
18657
+ : {}),
18658
+ ...protectionHeaders,
18659
+ ...(this.#options.runtimeTestFaultHeader
18660
+ ? {
18661
+ 'x-deepline-test-fault':
18662
+ this.#options.runtimeTestFaultHeader,
18663
+ }
18664
+ : {}),
18665
+ },
18666
+ body: serializedRequestBody(invocationAttempt),
18378
18667
  },
18379
- body: serializedRequestBody(invocationAttempt),
18380
- });
18668
+ );
18381
18669
  // Error classification needs the status and headers even
18382
18670
  // when the body is truncated. Keep the response visible
18383
18671
  // to the enclosing retry/billing boundary as soon as
18384
18672
  // headers arrive; decoded body data stays scoped until
18385
18673
  // it has been read and verified below.
18386
18674
  response = dispatchedResponse;
18675
+ if (
18676
+ dispatchedResponse.headers.has(
18677
+ TOOL_EXECUTE_PROVIDER_PERMIT_RECEIVER_HEADER,
18678
+ ) ||
18679
+ (dispatchedResponse.status !== 403 &&
18680
+ dispatchedResponse.status !== 404)
18681
+ )
18682
+ recordProviderDispatch();
18387
18683
  if (dispatchedResponse.status === 429) {
18388
18684
  const observedAt = Date.now();
18389
18685
  const retryAfter =
@@ -18539,6 +18835,14 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
18539
18835
  );
18540
18836
  providerBodyAdmissionWaitMs += bodyRead.waitMs;
18541
18837
  }
18838
+ providerPermitReceiverRejected =
18839
+ dispatchedWithProviderPermit &&
18840
+ isProviderPermitExecuteReceiverUnavailable(
18841
+ dispatchedResponse,
18842
+ dispatchedResponseErrorText ?? '',
18843
+ );
18844
+ if (!providerPermitReceiverRejected)
18845
+ recordProviderDispatch();
18542
18846
  if (dispatchedResponse.status === 429) {
18543
18847
  const bodyText = dispatchedResponseErrorText ?? '';
18544
18848
  let attempt = httpFailureAttempts.next({
@@ -18616,6 +18920,8 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
18616
18920
  // diagnostic counter now; the flow still holds the
18617
18921
  // real provider lease until this callback returns.
18618
18922
  releaseProviderExecution();
18923
+ if (providerPermitReceiverRejected)
18924
+ return 'not_dispatched' as const;
18619
18925
  const failure =
18620
18926
  preclassifiedHttpFailure.current?.failure;
18621
18927
  if (
@@ -18658,6 +18964,10 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
18658
18964
  },
18659
18965
  );
18660
18966
  } catch (error) {
18967
+ // A failed exchange whose dispatch is unknown remains a
18968
+ // physical attempt, including failure before headers.
18969
+ if (fetchDispatched && !providerPermitReceiverRejected)
18970
+ recordProviderDispatch();
18661
18971
  // `request()` can fail while waiting for its transport
18662
18972
  // lease, before its beforeRelease callback runs.
18663
18973
  activeProviderRequest?.release();
@@ -18693,24 +19003,25 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
18693
19003
  providerCallElapsedMs =
18694
19004
  providerBodyCompletedAtMs - providerCallStartedAt;
18695
19005
  options?.onProviderBodyCompleted?.(providerBodyCompletedAtMs);
18696
- this.resourceGovernor.observe({
18697
- toolId,
18698
- providerResourceKey: `tool:${toolId}`,
18699
- providerTelemetryKey,
18700
- providerConcurrencyGrant,
18701
- providerPacingGrant,
18702
- providerRunInFlightAtDispatch,
18703
- providerLatencyMs: providerCallElapsedMs,
18704
- fixtureDelayMs,
18705
- transportSlotWaitMs,
18706
- fetchToHeadersMs,
18707
- executeServerTiming,
18708
- bodyReadMs,
18709
- providerBodyAdmissionWaitMs:
18710
- dispatched.providerBodyAdmissionWaitMs,
18711
- providerSuccess: response.ok,
18712
- provider429: response.status === 429,
18713
- });
19006
+ if (!providerPermitReceiverRejected)
19007
+ this.resourceGovernor.observe({
19008
+ toolId,
19009
+ providerResourceKey: `tool:${toolId}`,
19010
+ providerTelemetryKey,
19011
+ providerConcurrencyGrant,
19012
+ providerPacingGrant,
19013
+ providerRunInFlightAtDispatch,
19014
+ providerLatencyMs: providerCallElapsedMs,
19015
+ fixtureDelayMs,
19016
+ transportSlotWaitMs,
19017
+ fetchToHeadersMs,
19018
+ executeServerTiming,
19019
+ bodyReadMs,
19020
+ providerBodyAdmissionWaitMs:
19021
+ dispatched.providerBodyAdmissionWaitMs,
19022
+ providerSuccess: response.ok,
19023
+ provider429: response.status === 429,
19024
+ });
18714
19025
  } finally {
18715
19026
  receiptHeartbeatSupervisor?.stop();
18716
19027
  }
@@ -18747,6 +19058,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
18747
19058
  throw new PlayRowProviderBackpressureError(
18748
19059
  error.provider,
18749
19060
  Number.isFinite(retryAtMs) ? retryAtMs : Date.now(),
19061
+ markProviderResidentWaitBudgetExhausted(),
18750
19062
  );
18751
19063
  }
18752
19064
  // This is a provider admission hold, not a row outcome.
@@ -18904,6 +19216,13 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
18904
19216
  }
18905
19217
  span.setAttribute('plays.http_status_code', response.status);
18906
19218
 
19219
+ if (providerPermitReceiverRejected) {
19220
+ // This named action was rejected before provider work. Retry the
19221
+ // same invocation once through the legacy worker validation path.
19222
+ providerPermitReceiverUnavailable = true;
19223
+ continue;
19224
+ }
19225
+
18907
19226
  if (!response.ok) {
18908
19227
  const text = responseErrorText ?? '';
18909
19228
  const developerTunnelOrigin502 =
@@ -19075,6 +19394,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
19075
19394
  throw new PlayRowProviderBackpressureError(
19076
19395
  provider,
19077
19396
  retryAtMs,
19397
+ markProviderResidentWaitBudgetExhausted(),
19078
19398
  );
19079
19399
  }
19080
19400
  await this.waitForProviderBackpressure(provider, retryAtMs);
@@ -19185,14 +19505,19 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
19185
19505
  if (
19186
19506
  !usesDurableInvocationFence &&
19187
19507
  (failure.backpressureDelayMs !== null ||
19508
+ failure.reason === 'provider_pacing_wait' ||
19188
19509
  (failure.reason === 'rate_limit' &&
19189
19510
  failure.knownNotDispatched))
19190
19511
  ) {
19191
- // A classified provider rejection has no completed value to
19192
- // charge. A credential guard refusal also releases its charge
19193
- // before replying. The next attempt needs a new reservation;
19194
- // durable result and provider-operation keys stay unchanged.
19195
- deeplineRequestId = `ctx-tool-${stableDigest(`${executionRequestNonce}:provider-rate-limit-retry:${invocationAttempt}`).slice(0, 32)}`;
19512
+ // These refusals release the prior reservation. App pacing
19513
+ // does not advance invocationAttempt, so use its own HTTP
19514
+ // failure count to avoid reusing a released retry reservation.
19515
+ // Durable result and provider-operation keys stay unchanged.
19516
+ const billingRetryIdentity =
19517
+ failure.reason === 'provider_pacing_wait'
19518
+ ? `app-pacing-refusal:${httpFailureAttempt}`
19519
+ : `provider-rate-limit-retry:${invocationAttempt}`;
19520
+ deeplineRequestId = `ctx-tool-${stableDigest(`${executionRequestNonce}:${billingRetryIdentity}`).slice(0, 32)}`;
19196
19521
  }
19197
19522
  this.emitExecutionEvent({
19198
19523
  type: RUN_EVENT_KIND.activityObserved,