deepline 0.3.183 → 0.3.185

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 (34) hide show
  1. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  2. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +53 -0
  3. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +751 -193
  4. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +4 -0
  5. package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +3 -1
  6. package/dist/bundling-sources/shared_libs/play-runtime/fixture-behavior.ts +1 -0
  7. package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +437 -150
  8. package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +47 -4
  9. package/dist/bundling-sources/shared_libs/play-runtime/governor/policy.ts +3 -1
  10. package/dist/bundling-sources/shared_libs/play-runtime/governor/rate-state-backend.ts +18 -0
  11. package/dist/bundling-sources/shared_libs/play-runtime/map-execution-frame.ts +3 -0
  12. package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +22 -2
  13. package/dist/bundling-sources/shared_libs/play-runtime/resource-governor.ts +6 -1
  14. package/dist/bundling-sources/shared_libs/play-runtime/run-failure.ts +11 -5
  15. package/dist/bundling-sources/shared_libs/play-runtime/runtime-flow.ts +102 -12
  16. package/dist/bundling-sources/shared_libs/play-runtime/runtime-operation-contract.ts +8 -0
  17. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +136 -28
  18. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-scope.ts +80 -0
  19. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts +897 -207
  20. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +33 -1
  21. package/dist/bundling-sources/shared_libs/play-runtime/suspension.ts +21 -0
  22. package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +120 -34
  23. package/dist/bundling-sources/shared_libs/play-runtime/tool-result.ts +82 -2
  24. package/dist/bundling-sources/shared_libs/plays/tool-result-types.ts +7 -2
  25. package/dist/cli/index.js +28 -3
  26. package/dist/cli/index.mjs +28 -3
  27. package/dist/index.js +1 -1
  28. package/dist/index.mjs +1 -1
  29. package/dist/install-integrity.json +1 -0
  30. package/dist/release.d.mts +1 -1
  31. package/dist/release.d.ts +1 -1
  32. package/dist/release.js +1 -1
  33. package/dist/release.mjs +1 -1
  34. package/package.json +1 -1
@@ -104,12 +104,12 @@ import {
104
104
  type PacingResolver,
105
105
  type PlayExecutionGovernor,
106
106
  } from './governor/governor';
107
- import { resolveExecutionPolicy } from './governor/policy';
108
107
  import {
109
108
  createRuntimeResourceGovernor,
110
109
  type RuntimeResourceGovernor,
111
110
  type RuntimeResourceObservation,
112
111
  type RuntimeResourceSnapshot,
112
+ type RuntimeProviderPermit,
113
113
  } from './resource-governor';
114
114
  import { CTX_FETCH_EGRESS_TOOL_ID } from './builtin-pacing';
115
115
  import {
@@ -192,6 +192,8 @@ import {
192
192
  TOOL_EXECUTE_TRANSPORT_MAX_ATTEMPTS,
193
193
  TOOL_EXECUTE_RUNNER_ACCEPTS_HEADER,
194
194
  TOOL_EXECUTE_RUNNER_ACCEPTS_VALUE,
195
+ TOOL_EXECUTE_PROVIDER_RATE_SCOPE_HEADER,
196
+ TOOL_EXECUTE_PROVIDER_RATE_RETRY_SNAPSHOT_HEADER,
195
197
  classifyToolExecuteHttpFailure,
196
198
  createToolExecuteHttpFailureAttemptTracker,
197
199
  parseToolExecuteRetryAfterMs,
@@ -358,6 +360,7 @@ import { DISALLOWED_RUN_JAVASCRIPT_TOOL_MESSAGE } from '../plays/runtime-constra
358
360
  import {
359
361
  PlayExecutionSuspendedError,
360
362
  PlayRowExecutionSuspendedError,
363
+ PlayRowProviderBackpressureError,
361
364
  isPlayExecutionSuspendedError,
362
365
  isPlayRowExecutionSuspendedError,
363
366
  type PlayExecutionSuspension,
@@ -1374,10 +1377,11 @@ export async function reconcileNodeRuntimeMapResultsWithPersistedSheet(input: {
1374
1377
  expectedRows: number;
1375
1378
  currentRows: Record<string, unknown>[];
1376
1379
  failedRowCount: number;
1380
+ rowMode?: 'all' | 'output';
1377
1381
  readPersistedRows: (input: {
1378
1382
  limit: number;
1379
1383
  offset: number;
1380
- rowMode: 'all';
1384
+ rowMode: 'all' | 'output';
1381
1385
  }) => Promise<Record<string, unknown>[]>;
1382
1386
  log?: (line: string) => void;
1383
1387
  }): Promise<Record<string, unknown>[]> {
@@ -1395,7 +1399,7 @@ export async function reconcileNodeRuntimeMapResultsWithPersistedSheet(input: {
1395
1399
  const page = await input.readPersistedRows({
1396
1400
  limit: Math.min(10_000, input.expectedRows - persistedRows.length),
1397
1401
  offset,
1398
- rowMode: 'all',
1402
+ rowMode: input.rowMode ?? 'all',
1399
1403
  });
1400
1404
  if (page.length === 0) break;
1401
1405
  persistedRows.push(...page);
@@ -1924,6 +1928,8 @@ type ToolExecutionApiOptions = {
1924
1928
  * inside a row's async scope, where the scope is read from there instead.
1925
1929
  */
1926
1930
  memoryLedgerRowScopes?: readonly (MemoryLedgerRowScope | null | undefined)[];
1931
+ /** Runner-local map ownership; this must never be serialized to the gateway. */
1932
+ providerBackpressureMapRows?: boolean;
1927
1933
  /**
1928
1934
  * Present only on a native batch's one physical call. The members share a
1929
1935
  * single body whose per-member sizes the splitter has not produced yet, so
@@ -2512,6 +2518,7 @@ class RuntimeDatasetBuilder<T extends Record<string, unknown>> {
2512
2518
  }
2513
2519
 
2514
2520
  const WAITING_ROW = Symbol('deepline.waiting_row');
2521
+
2515
2522
  const FAILED_ROW = Symbol('deepline.failed_row');
2516
2523
  const COMPLETED_ROW = Symbol('deepline.completed_row');
2517
2524
  type MapRowExecutionResult =
@@ -2604,6 +2611,9 @@ function cloneMapFrame(frame: MapExecutionFrame): MapExecutionFrame {
2604
2611
  suspendedInputRows: frame.suspendedInputRows.map((row) => ({
2605
2612
  inputIndex: row.inputIndex,
2606
2613
  data: cloneCsvAliasedRow(row.data),
2614
+ ...(row.authoredInputDigest
2615
+ ? { authoredInputDigest: row.authoredInputDigest }
2616
+ : {}),
2607
2617
  })),
2608
2618
  }
2609
2619
  : {}),
@@ -2613,7 +2623,8 @@ function cloneMapFrame(frame: MapExecutionFrame): MapExecutionFrame {
2613
2623
  /**
2614
2624
  * Adapt this runtime's per-tool queue hints into the Play Execution Governor's
2615
2625
  * pacing resolver. The Governor owns per-(org, provider) pacing; this only maps
2616
- * the existing queue-hint metadata into the Governor's `PacingRule` shape.
2626
+ * declared hints into `PacingRule`s. An empty rule list deliberately leaves
2627
+ * the substrate-specific default to the Governor.
2617
2628
  */
2618
2629
  function createPacingResolver(
2619
2630
  getToolQueueHints: ContextOptions['getToolQueueHints'],
@@ -2627,10 +2638,7 @@ function createPacingResolver(
2627
2638
  if (declared) return declared;
2628
2639
  const provider = (await getToolProvider?.(toolId))?.trim();
2629
2640
  if (!provider) return null;
2630
- return {
2631
- ...defaultPacingForTool(toolId, resolveExecutionPolicy('cjs_node20')),
2632
- provider,
2633
- };
2641
+ return { provider, rules: [] };
2634
2642
  };
2635
2643
  }
2636
2644
 
@@ -2817,6 +2825,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
2817
2825
  boundaryId: string;
2818
2826
  eventKey: string;
2819
2827
  timeoutMs: number;
2828
+ eventExpiresAtMs?: number;
2820
2829
  messageRef?: { channel: string; ts: string };
2821
2830
  }> = [];
2822
2831
  private processedRowCount = 0;
@@ -6726,6 +6735,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
6726
6735
  fieldName: input.request.fieldName,
6727
6736
  },
6728
6737
  ],
6738
+ providerBackpressureMapRows: Boolean(
6739
+ input.request.tableNamespace && input.request.rowKey,
6740
+ ),
6729
6741
  // Same reason, for the delivery clock: this request really did
6730
6742
  // make a provider call, so its row's delivery site has a body
6731
6743
  // completion to measure from. A retry re-stamps; the last body is
@@ -8578,10 +8590,36 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
8578
8590
  // before a stateful source query can decide that a sibling action is no
8579
8591
  // longer eligible because an earlier action already completed.
8580
8592
  const replayMapInvocationId = `${normalizedMapNamespace}:${this.mapInvocationIndex}`;
8593
+ const replayMapFrame = this.checkpoint.mapFrames?.[replayMapInvocationId];
8594
+ const canReadPersistedMapResults = Boolean(
8595
+ this.#options.onMapRowsCompleted &&
8596
+ this.#options.dbSessionStrategy &&
8597
+ this.#options.baseUrl &&
8598
+ this.#options.executorToken &&
8599
+ this.#options.playName &&
8600
+ this.#options.runId,
8601
+ );
8602
+ // A suspended map's admitted pending inputs remain authoritative even
8603
+ // when a stateful authored source returns fewer rows after a wake.
8581
8604
  const suspendedInputRows = shouldStreamRuntimeBackedDataset
8582
8605
  ? null
8583
- : (this.checkpoint.mapFrames?.[replayMapInvocationId]
8584
- ?.suspendedInputRows ?? null);
8606
+ : (replayMapFrame?.suspendedInputRows ?? null);
8607
+ // Completed siblings require either the Runtime Sheet or a complete
8608
+ // authored replay. Never return only the pending subset as a full result.
8609
+ const replayWithoutSheet = Boolean(
8610
+ suspendedInputRows?.length && !canReadPersistedMapResults,
8611
+ );
8612
+ const resumedPendingRowsOnly = Boolean(
8613
+ suspendedInputRows?.length &&
8614
+ (canReadPersistedMapResults ||
8615
+ (replayMapFrame?.completedRowKeys.length ?? 0) === 0),
8616
+ );
8617
+ const previouslyCompletedRowKeys = resumedPendingRowsOnly
8618
+ ? (replayMapFrame?.completedRowKeys ?? [])
8619
+ : [];
8620
+ const previouslyFailedRowsCount = resumedPendingRowsOnly
8621
+ ? (replayMapFrame?.failedRowsCount ?? 0)
8622
+ : 0;
8585
8623
  if (shouldStreamRuntimeBackedDataset) {
8586
8624
  // Net-new admission happens one source page at a time. Its progress and
8587
8625
  // returned dataset describe admitted rows, not every provider/CSV row
@@ -9392,18 +9430,19 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9392
9430
  },
9393
9431
  });
9394
9432
  }
9395
- const rawMaterializedEntries = suspendedInputRows?.length
9396
- ? suspendedInputRows.map((row) => ({
9397
- item: cloneCsvAliasedRow(row.data) as T,
9398
- inputIndex: row.inputIndex,
9399
- }))
9400
- : (
9401
- await materializePlayDatasetInput(items, {
9402
- onRow: (row) => {
9403
- materializationMemoryTracker.track(row);
9404
- },
9405
- })
9406
- ).map((item, inputIndex) => ({ item, inputIndex }));
9433
+ const rawMaterializedEntries =
9434
+ resumedPendingRowsOnly && suspendedInputRows
9435
+ ? suspendedInputRows.map((row) => ({
9436
+ item: cloneCsvAliasedRow(row.data) as T,
9437
+ inputIndex: row.inputIndex,
9438
+ }))
9439
+ : (
9440
+ await materializePlayDatasetInput(items, {
9441
+ onRow: (row) => {
9442
+ materializationMemoryTracker.track(row);
9443
+ },
9444
+ })
9445
+ ).map((item, inputIndex) => ({ item, inputIndex }));
9407
9446
  const rawMaterializedItems = rawMaterializedEntries.map(
9408
9447
  (entry) => entry.item,
9409
9448
  );
@@ -9434,7 +9473,105 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9434
9473
  );
9435
9474
  }
9436
9475
  const materializedItems = dedupedMaterialized.rows;
9437
- totalInputCount = materializedItems.length;
9476
+ const priorAuthoredDigestsByIndex = new Map(
9477
+ (suspendedInputRows ?? []).flatMap((row) =>
9478
+ row.authoredInputDigest
9479
+ ? [[row.inputIndex, row.authoredInputDigest] as const]
9480
+ : [],
9481
+ ),
9482
+ );
9483
+ const authoredInputDigestsByIndex = new Map<number, string>();
9484
+ if (!canReadPersistedMapResults) {
9485
+ for (const [index, item] of materializedItems.entries()) {
9486
+ const inputIndex = originalInputIndexByItem.get(item) ?? index;
9487
+ const priorDigest = priorAuthoredDigestsByIndex.get(inputIndex);
9488
+ // A legacy pending-only checkpoint contains post-merge row data, not
9489
+ // the original authored input. Do not manufacture a digest from it.
9490
+ if (resumedPendingRowsOnly) {
9491
+ if (priorDigest)
9492
+ authoredInputDigestsByIndex.set(inputIndex, priorDigest);
9493
+ continue;
9494
+ }
9495
+ const authored = Object.fromEntries(
9496
+ Object.entries(
9497
+ toSerializableCsvAliasedRow(
9498
+ this.toOutputRow(item as Record<string, unknown>),
9499
+ ),
9500
+ ).filter(([field]) => !field.startsWith('__deepline')),
9501
+ );
9502
+ authoredInputDigestsByIndex.set(
9503
+ inputIndex,
9504
+ stableDigest(stableStringify(authored)),
9505
+ );
9506
+ }
9507
+ }
9508
+ if (replayWithoutSheet && !resumedPendingRowsOnly) {
9509
+ // A no-sheet replay can only rebuild completed output by executing the
9510
+ // same admitted input again. The previous failed row's identity/output
9511
+ // is not in the checkpoint, so re-executing it would change its terminal
9512
+ // outcome; require a readable sheet instead of silently reclassifying it.
9513
+ if ((replayMapFrame?.failedRowsCount ?? 0) > 0) {
9514
+ throw new Error(
9515
+ `Cannot reconstruct the complete result for ctx.dataset("${normalizedMapNamespace}") after suspension: ` +
9516
+ 'a prior failed row has no readable Runtime Sheet.',
9517
+ );
9518
+ }
9519
+ const authoredRowsByKey = new Map(
9520
+ materializedItems.map((item, index) => {
9521
+ const inputIndex = originalInputIndexByItem.get(item) ?? index;
9522
+ const row = this.toOutputRow(item as Record<string, unknown>);
9523
+ return [
9524
+ rowIdentity(row, inputIndex),
9525
+ this.toPersistedOutputRow(row),
9526
+ ] as const;
9527
+ }),
9528
+ );
9529
+ const missingCompleted = replayMapFrame!.completedRowKeys.some(
9530
+ (key) => !authoredRowsByKey.has(key),
9531
+ );
9532
+ const changedPending = suspendedInputRows!.some(
9533
+ ({ data, inputIndex, authoredInputDigest }) => {
9534
+ const authored = authoredRowsByKey.get(rowIdentity(data, inputIndex));
9535
+ if (!authored) return true;
9536
+ if (authoredInputDigest) {
9537
+ return (
9538
+ authoredInputDigestsByIndex.get(inputIndex) !==
9539
+ authoredInputDigest
9540
+ );
9541
+ }
9542
+ // A saved waiting row may include output columns restored by
9543
+ // onMapStart. Legacy checkpoints lack the original input digest,
9544
+ // so compare their stable input projection and supplied fields.
9545
+ if (
9546
+ stableStringify(stableInputRow(authored)) !==
9547
+ stableStringify(stableInputRow(data))
9548
+ ) {
9549
+ return true;
9550
+ }
9551
+ return Object.entries(authored).some(
9552
+ ([field, value]) =>
9553
+ !field.startsWith('__deepline') &&
9554
+ (!(field in data) ||
9555
+ stableStringify(value) !== stableStringify(data[field])),
9556
+ );
9557
+ },
9558
+ );
9559
+ if (
9560
+ missingCompleted ||
9561
+ changedPending ||
9562
+ materializedItems.length !== replayMapFrame!.totalRows
9563
+ ) {
9564
+ throw new Error(
9565
+ `Cannot reconstruct the complete result for ctx.dataset("${normalizedMapNamespace}") after suspension: ` +
9566
+ 'the authored input changed and this run has no readable Runtime Sheet.',
9567
+ );
9568
+ }
9569
+ }
9570
+ // A suspended checkpoint retains only waiting inputs, but its original
9571
+ // total and completed keys still describe the full result of this run.
9572
+ totalInputCount = resumedPendingRowsOnly
9573
+ ? Math.max(replayMapFrame?.totalRows ?? 0, materializedItems.length)
9574
+ : materializedItems.length;
9438
9575
  rawItems = materializedItems.map((item) => this.toOutputRow(item));
9439
9576
  itemsToProcess = materializedItems.map((item) =>
9440
9577
  this.toOutputRow(item as Record<string, unknown>),
@@ -9546,7 +9683,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9546
9683
  rawItems = admittedItems.map((item) => item.row);
9547
9684
  itemsToProcess = admittedItems.map((item) => item.row);
9548
9685
  itemOriginalIndexes = admittedItems.map((item) => item.index);
9549
- totalInputCount = admittedItems.length;
9686
+ totalInputCount = resumedPendingRowsOnly
9687
+ ? Math.max(replayMapFrame?.totalRows ?? 0, admittedItems.length)
9688
+ : admittedItems.length;
9550
9689
  }
9551
9690
 
9552
9691
  const mapScope = this.createMapExecutionScope({
@@ -9554,7 +9693,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9554
9693
  artifactTableNamespace: resolvedTableNamespace,
9555
9694
  explicitKey: explicitKeyResolver,
9556
9695
  });
9557
- const completedRowKeys: string[] = [];
9696
+ const completedRowKeys: string[] = [...previouslyCompletedRowKeys];
9558
9697
  const pendingRowKeys = itemsToProcess.map((item, index) =>
9559
9698
  rowIdentity(this.toOutputRow(item), itemOriginalIndexes[index] ?? index),
9560
9699
  );
@@ -9567,8 +9706,10 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9567
9706
  totalRows: totalInputCount,
9568
9707
  completedRowKeys,
9569
9708
  pendingRowKeys,
9570
- failedRowsCount: 0,
9571
- startedAt: Date.now(),
9709
+ failedRowsCount: previouslyFailedRowsCount,
9710
+ startedAt: resumedPendingRowsOnly
9711
+ ? (replayMapFrame?.startedAt ?? Date.now())
9712
+ : Date.now(),
9572
9713
  updatedAt: Date.now(),
9573
9714
  });
9574
9715
  this.emitExecutionEvent({
@@ -9690,7 +9831,8 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9690
9831
  options?.description,
9691
9832
  {
9692
9833
  totalRows: totalInputCount,
9693
- completedRows: duplicateReuseCount,
9834
+ completedRows:
9835
+ duplicateReuseCount + previouslyCompletedRowKeys.length,
9694
9836
  },
9695
9837
  {
9696
9838
  emitTerminalEvent: false,
@@ -9699,8 +9841,16 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9699
9841
  executionRowIndexes: rowsToExecuteEntries.map(
9700
9842
  (entry) => entry.originalIndex,
9701
9843
  ),
9844
+ ...(!canReadPersistedMapResults
9845
+ ? {
9846
+ authoredInputDigests: rowsToExecuteEntries.map((entry) =>
9847
+ authoredInputDigestsByIndex.get(entry.originalIndex),
9848
+ ),
9849
+ }
9850
+ : {}),
9702
9851
  retainedRowsMemoryBudgetBytes:
9703
9852
  mapMemoryLimits.materializedBudgetBytes,
9853
+ progressFailedOffset: previouslyFailedRowsCount,
9704
9854
  incrementalPersistence,
9705
9855
  },
9706
9856
  );
@@ -9751,7 +9901,10 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9751
9901
  this.toPublicOutputRow(resultsByKey.get(rowKey) ?? rawItem),
9752
9902
  ];
9753
9903
  });
9754
- const executedCount = rowsToExecute.length;
9904
+ const executedCount =
9905
+ rowsToExecute.length +
9906
+ previouslyCompletedRowKeys.length +
9907
+ previouslyFailedRowsCount;
9755
9908
  const reusedCount = duplicateReuseCount;
9756
9909
 
9757
9910
  // Persist executed rows to the tenant runtime sheet — the sheet is the
@@ -9794,9 +9947,12 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9794
9947
 
9795
9948
  const durableCompletedRows = Math.max(
9796
9949
  0,
9797
- reusedCount + visibleCompletedRows.length,
9950
+ previouslyCompletedRowKeys.length +
9951
+ reusedCount +
9952
+ visibleCompletedRows.length,
9798
9953
  );
9799
- const durableFailedRows = visibleFailedRows.length;
9954
+ const durableFailedRows =
9955
+ previouslyFailedRowsCount + visibleFailedRows.length;
9800
9956
  const terminalFrame = this.checkpoint.mapFrames?.[mapScope.mapInvocationId];
9801
9957
  if (terminalFrame) {
9802
9958
  this.setMapFrame({
@@ -9829,6 +9985,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9829
9985
  });
9830
9986
 
9831
9987
  if (this.#options.onMapRowsCompleted) {
9988
+ // Output mode selects enriched rows when this replay's earlier
9989
+ // invocation also persisted isolated failures.
9990
+ const finalizationRowMode = resumedPendingRowsOnly ? 'output' : 'all';
9832
9991
  const finalizationPageReader = createRuntimeSheetDatasetPageReader({
9833
9992
  context: {
9834
9993
  baseUrl: this.#options.baseUrl!,
@@ -9840,15 +9999,21 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9840
9999
  },
9841
10000
  tableNamespace: resolvedTableNamespace,
9842
10001
  runId: this.#options.runId!,
9843
- rowMode: 'all',
10002
+ rowMode: finalizationRowMode,
9844
10003
  });
9845
10004
  results = await reconcileNodeRuntimeMapResultsWithPersistedSheet({
9846
10005
  mapName: normalizedMapNamespace,
9847
10006
  tableNamespace: resolvedTableNamespace,
9848
10007
  runId: this.#options.runId,
9849
- expectedRows: Math.max(0, totalInputCount - staleCompletionKeys.size),
10008
+ expectedRows: Math.max(
10009
+ 0,
10010
+ totalInputCount -
10011
+ staleCompletionKeys.size -
10012
+ (resumedPendingRowsOnly ? durableFailedRows : 0),
10013
+ ),
9850
10014
  currentRows: results,
9851
- failedRowCount: visibleFailedRows.length,
10015
+ failedRowCount: resumedPendingRowsOnly ? 0 : visibleFailedRows.length,
10016
+ rowMode: finalizationRowMode,
9852
10017
  log: (line) => this.runtimeLog(line),
9853
10018
  readPersistedRows: async (readInput) => {
9854
10019
  if (
@@ -9968,7 +10133,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9968
10133
  reused: reusedCount,
9969
10134
  skipped: reusedCount,
9970
10135
  pending: 0,
9971
- failed: visibleFailedRows.length,
10136
+ failed: durableFailedRows,
9972
10137
  ...(duplicateReuseCount > 0
9973
10138
  ? { duplicates: { exact: duplicateReuseCount } }
9974
10139
  : {}),
@@ -10008,6 +10173,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
10008
10173
  onRowError?: 'isolate' | 'fail';
10009
10174
  executionRowKeys?: string[];
10010
10175
  executionRowIndexes?: number[];
10176
+ authoredInputDigests?: Array<string | undefined>;
10011
10177
  /**
10012
10178
  * Opt-in cap on how many row resolvers run their body concurrently. When
10013
10179
  * unset, the Governor's rowDefault is used. Excess rows stay pending as
@@ -10552,6 +10718,12 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
10552
10718
  );
10553
10719
  let nextRowIndex = 0;
10554
10720
  let firstRowError: unknown = null;
10721
+ const providerRetryDeadlines: Array<{
10722
+ provider: string;
10723
+ retryAtMs: number;
10724
+ }> = [];
10725
+ let providerBackpressureRowCount = 0;
10726
+ const waitingBaseRows = new Map<number, Record<string, unknown>>();
10555
10727
 
10556
10728
  const runRow = async (
10557
10729
  idx: number,
@@ -10644,7 +10816,8 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
10644
10816
  } catch (error) {
10645
10817
  if (
10646
10818
  isPlayRowExecutionSuspendedError(error) ||
10647
- error instanceof PlayExecutionSuspendedError
10819
+ error instanceof PlayExecutionSuspendedError ||
10820
+ error instanceof PlayRowProviderBackpressureError
10648
10821
  ) {
10649
10822
  throw error;
10650
10823
  }
@@ -10831,6 +11004,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
10831
11004
  return COMPLETED_ROW;
10832
11005
  } catch (error) {
10833
11006
  if (isPlayRowExecutionSuspendedError(error)) {
11007
+ waitingBaseRows.set(idx, baseRow);
10834
11008
  this.pendingRowEventBoundaries.push(error.boundary);
10835
11009
  updateMapFrameProgress({
10836
11010
  pendingRowKey: rowKey,
@@ -10852,6 +11026,47 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
10852
11026
  });
10853
11027
  return WAITING_ROW;
10854
11028
  }
11029
+ if (error instanceof PlayRowProviderBackpressureError) {
11030
+ waitingBaseRows.set(idx, baseRow);
11031
+ // Each row retains its own deadline. Provider names are not rate
11032
+ // scopes: two credentials for one provider can recover separately.
11033
+ providerRetryDeadlines.push({
11034
+ provider: error.provider,
11035
+ retryAtMs: error.retryAtMs,
11036
+ });
11037
+ providerBackpressureRowCount += 1;
11038
+ updateMapFrameProgress({ pendingRowKey: rowKey });
11039
+ const fieldName = activeFieldName;
11040
+ if (!fieldName) {
11041
+ throw new Error(
11042
+ 'Provider backpressure escaped a mapped row without an active field.',
11043
+ { cause: error },
11044
+ );
11045
+ }
11046
+ this.emitScopedFieldMetaUpdate({
11047
+ rowId: idx,
11048
+ key: rowKey,
11049
+ tableNamespace: normalizedTableNamespace,
11050
+ fieldName,
11051
+ status: 'running',
11052
+ rowStatus: 'running',
11053
+ stage: 'waiting_for_provider',
11054
+ provider: error.provider,
11055
+ error: null,
11056
+ dataPatch: {},
11057
+ });
11058
+ this.runtimeLog(
11059
+ `[runtime.provider.row_wait] ${JSON.stringify({
11060
+ provider: error.provider,
11061
+ retry_at: new Date(error.retryAtMs).toISOString(),
11062
+ wait_ms: Math.max(0, error.retryAtMs - Date.now()),
11063
+ row_key_digest: stableDigest(rowKey),
11064
+ field: fieldName,
11065
+ })}`,
11066
+ { level: 'warn' },
11067
+ );
11068
+ return WAITING_ROW;
11069
+ }
10855
11070
  if (isPlayExecutionSuspendedError(error)) {
10856
11071
  const completedKeys = new Set(
10857
11072
  completedRowsToPersist.map((row) => row.key),
@@ -10866,6 +11081,12 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
10866
11081
  data: this.toPersistedOutputRow(
10867
11082
  this.toOutputRow(item as Record<string, unknown>),
10868
11083
  ),
11084
+ ...(runtimeOptions?.authoredInputDigests?.[index]
11085
+ ? {
11086
+ authoredInputDigest:
11087
+ runtimeOptions.authoredInputDigests[index],
11088
+ }
11089
+ : {}),
10869
11090
  },
10870
11091
  ];
10871
11092
  });
@@ -11089,8 +11310,8 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
11089
11310
  settled = await Promise.allSettled(workers);
11090
11311
  }
11091
11312
  } finally {
11092
- datasetStep.items = items.length;
11093
- this.processedRowCount = items.length;
11313
+ datasetStep.items = currentTotalRows();
11314
+ this.processedRowCount = currentTotalRows();
11094
11315
  mapStallObserver?.stopHeartbeat();
11095
11316
  unsubscribePersistenceLatch();
11096
11317
  this.#options.rowAdmissionSignal?.removeEventListener(
@@ -11166,36 +11387,44 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
11166
11387
  accountedRowIndexes.add(row.inputIndex);
11167
11388
  }
11168
11389
  }
11169
- if (this.pendingRowEventBoundaries.length > 0) {
11170
- const completedRowKeys = new Set<string>();
11390
+ if (
11391
+ this.pendingRowEventBoundaries.length > 0 ||
11392
+ providerRetryDeadlines.length > 0
11393
+ ) {
11394
+ const waitingRowKeys = new Set<string>();
11395
+ const previousSuspendedInputs =
11396
+ this.checkpoint.mapFrames?.[mapScope.mapInvocationId]
11397
+ ?.suspendedInputRows;
11398
+ const suspendedInputRows =
11399
+ rowFeed ||
11400
+ (providerRetryDeadlines.length === 0 && !previousSuspendedInputs)
11401
+ ? undefined
11402
+ : rowResults.flatMap((result, index) => {
11403
+ if (result !== WAITING_ROW) return [];
11404
+ const baseRow = waitingBaseRows.get(index);
11405
+ if (!baseRow) {
11406
+ throw new Error(
11407
+ `Waiting row ${index} lost its resumable input before map suspension.`,
11408
+ );
11409
+ }
11410
+ return [
11411
+ {
11412
+ inputIndex: executionRowIndex(index),
11413
+ data: this.toPersistedOutputRow(baseRow),
11414
+ ...(runtimeOptions?.authoredInputDigests?.[index]
11415
+ ? {
11416
+ authoredInputDigest:
11417
+ runtimeOptions.authoredInputDigests[index],
11418
+ }
11419
+ : {}),
11420
+ },
11421
+ ];
11422
+ });
11171
11423
  for (let index = 0; index < rowResults.length; index += 1) {
11172
- const result = rowResults[index];
11173
- const key = keyForIndex(index);
11174
- if (result === WAITING_ROW) {
11175
- if (key) {
11176
- completedRowKeys.add(key);
11177
- }
11178
- accountedRowIndexes.add(executionRowIndex(index));
11179
- continue;
11424
+ if (rowResults[index] === WAITING_ROW) {
11425
+ waitingRowKeys.add(keyForIndex(index));
11180
11426
  }
11181
- if (key) completedRowKeys.add(key);
11182
11427
  }
11183
- this.setMapFrame({
11184
- ...(this.checkpoint.mapFrames?.[mapScope.mapInvocationId] ?? {
11185
- mapInvocationId: mapScope.mapInvocationId,
11186
- logicalNamespace: mapScope.logicalNamespace,
11187
- artifactTableNamespace: mapScope.artifactTableNamespace,
11188
- status: 'suspended' as const,
11189
- totalRows: currentTotalRows(),
11190
- completedRowKeys: [...completedRowKeys],
11191
- pendingRowKeys: items.map((_item, index) => keyForIndex(index)),
11192
- startedAt: Date.now(),
11193
- updatedAt: Date.now(),
11194
- }),
11195
- status: 'suspended',
11196
- completedRowKeys: [...completedRowKeys],
11197
- updatedAt: Date.now(),
11198
- });
11199
11428
  const uniqueBoundaries = [
11200
11429
  ...new Map(
11201
11430
  this.pendingRowEventBoundaries.map((boundary) => [
@@ -11204,19 +11433,69 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
11204
11433
  ]),
11205
11434
  ).values(),
11206
11435
  ];
11436
+ const earliestHold = providerRetryDeadlines.reduce<
11437
+ (typeof providerRetryDeadlines)[number] | null
11438
+ >(
11439
+ (earliest, next) =>
11440
+ !earliest || next.retryAtMs < earliest.retryAtMs ? next : earliest,
11441
+ null,
11442
+ );
11443
+ const providerWakeAtMs = earliestHold
11444
+ ? Math.max(Date.now() + 1_000, earliestHold.retryAtMs)
11445
+ : null;
11207
11446
  const checkpointUpdates = this.activeMapCheckpointUpdatesForTable(
11208
11447
  normalizedTableNamespace,
11209
11448
  );
11210
- // A suspension is not observable until both terminal rows and partial
11211
- // completed-cell patches cross the same writer's durability barrier.
11449
+ // Both terminal siblings and partial waiting-cell patches must cross
11450
+ // the writer's durability barrier before this map becomes parked.
11212
11451
  await incrementalPersistence?.checkpoint(checkpointUpdates);
11213
11452
  await incrementalPersistence?.flush();
11214
11453
  await this.runtimeStepReceiptCompletionWriter.flush();
11215
- this.pendingRowEventBoundaries = [];
11216
- this.suspendDurably({
11217
- kind: 'integration_event_batch',
11218
- boundaries: uniqueBoundaries,
11454
+ updateMapFrameProgress({
11455
+ status: 'suspended',
11456
+ activeBoundaryId: null,
11457
+ emitEventType: 'map.suspended',
11219
11458
  });
11459
+ const frame = this.checkpoint.mapFrames?.[mapScope.mapInvocationId];
11460
+ if (!frame) {
11461
+ throw new Error('Suspended map lost its durable execution frame.');
11462
+ }
11463
+ this.setMapFrame({
11464
+ ...frame,
11465
+ completedRowKeys: frame.completedRowKeys.filter(
11466
+ (key) => !waitingRowKeys.has(key),
11467
+ ),
11468
+ pendingRowKeys: rowFeed ? [] : [...waitingRowKeys],
11469
+ suspendedInputRows,
11470
+ activeBoundaryId: null,
11471
+ updatedAt: Date.now(),
11472
+ });
11473
+ if (earliestHold && providerWakeAtMs !== null) {
11474
+ this.runtimeLog(
11475
+ `[runtime.provider.map_wait] ${JSON.stringify({
11476
+ provider: earliestHold.provider,
11477
+ retry_at: new Date(providerWakeAtMs).toISOString(),
11478
+ wait_ms: Math.max(0, providerWakeAtMs - Date.now()),
11479
+ waiting_rows: providerBackpressureRowCount,
11480
+ table_namespace: normalizedTableNamespace,
11481
+ })}`,
11482
+ { level: 'warn' },
11483
+ );
11484
+ }
11485
+ this.pendingRowEventBoundaries = [];
11486
+ if (uniqueBoundaries.length > 0) {
11487
+ this.suspendDurably({
11488
+ kind: 'integration_event_batch',
11489
+ boundaries: uniqueBoundaries,
11490
+ ...(providerWakeAtMs !== null ? { providerWakeAtMs } : {}),
11491
+ });
11492
+ }
11493
+ if (earliestHold && providerWakeAtMs !== null) {
11494
+ await this.waitForProviderBackpressure(
11495
+ earliestHold.provider,
11496
+ providerWakeAtMs,
11497
+ );
11498
+ }
11220
11499
  }
11221
11500
  for (let index = 0; index < rowResults.length; index += 1) {
11222
11501
  if (rowResults[index] !== WAITING_ROW) {
@@ -13491,6 +13770,16 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
13491
13770
  throw error;
13492
13771
  }
13493
13772
  if (suspendAfterTimeoutMs !== undefined) {
13773
+ const finalizedAtMs = existing.completedAt ?? Date.now();
13774
+ if (existing.completedAt === undefined) {
13775
+ this.checkpoint.resolvedBoundaries = {
13776
+ ...(this.checkpoint.resolvedBoundaries ?? {}),
13777
+ [preparedBoundary.boundaryId]: {
13778
+ ...existing,
13779
+ completedAt: finalizedAtMs,
13780
+ },
13781
+ };
13782
+ }
13494
13783
  this.runtimeLog(
13495
13784
  `Integration event ${preparedBoundary.boundaryId}: timeout finalized externally; keeping Play parked`,
13496
13785
  );
@@ -13499,6 +13788,8 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
13499
13788
  boundaryId: preparedBoundary.boundaryId,
13500
13789
  eventKey: preparedBoundary.eventKey,
13501
13790
  timeoutMs: suspendAfterTimeoutMs,
13791
+ eventExpiresAtMs: finalizedAtMs + suspendAfterTimeoutMs,
13792
+ postTimeoutPark: true,
13502
13793
  ...(existing.messageRef ? { messageRef: existing.messageRef } : {}),
13503
13794
  });
13504
13795
  }
@@ -13530,6 +13821,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
13530
13821
  boundaryId: preparedBoundary.boundaryId,
13531
13822
  eventKey: existing.eventKey,
13532
13823
  timeoutMs: existing.timeoutMs,
13824
+ ...(existing.eventExpiresAtMs !== undefined
13825
+ ? { eventExpiresAtMs: existing.eventExpiresAtMs }
13826
+ : {}),
13533
13827
  ...(existing.messageRef ? { messageRef: existing.messageRef } : {}),
13534
13828
  };
13535
13829
  if (rowScope) {
@@ -13544,6 +13838,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
13544
13838
  boundary: preparedBoundary,
13545
13839
  ...(force ? { force: true } : {}),
13546
13840
  });
13841
+ // An unanswered checkpoint suspends above without re-arming the provider.
13842
+ // New boundaries receive one absolute expiry, persisted for later replays.
13843
+ const eventExpiresAtMs = Date.now() + boundary.timeoutMs;
13547
13844
 
13548
13845
  if (boundary.resolvedOutput) {
13549
13846
  this.checkpoint.resolvedBoundaries = {
@@ -13552,6 +13849,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
13552
13849
  kind: 'integration_event',
13553
13850
  eventKey: boundary.eventKey,
13554
13851
  timeoutMs: boundary.timeoutMs,
13852
+ eventExpiresAtMs,
13555
13853
  provider: boundary.provider,
13556
13854
  toolId: boundary.toolId,
13557
13855
  output: boundary.resolvedOutput,
@@ -13597,6 +13895,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
13597
13895
  kind: 'integration_event',
13598
13896
  eventKey: boundary.eventKey,
13599
13897
  timeoutMs: boundary.timeoutMs,
13898
+ eventExpiresAtMs,
13600
13899
  provider: boundary.provider,
13601
13900
  toolId: boundary.toolId,
13602
13901
  ...(boundary.requestKey ? { requestKey: boundary.requestKey } : {}),
@@ -13626,6 +13925,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
13626
13925
  boundaryId: boundary.boundaryId,
13627
13926
  eventKey: boundary.eventKey,
13628
13927
  timeoutMs: boundary.timeoutMs,
13928
+ eventExpiresAtMs,
13629
13929
  ...(boundary.messageRef ? { messageRef: boundary.messageRef } : {}),
13630
13930
  });
13631
13931
  }
@@ -13635,6 +13935,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
13635
13935
  boundaryId: boundary.boundaryId,
13636
13936
  eventKey: boundary.eventKey,
13637
13937
  timeoutMs: boundary.timeoutMs,
13938
+ eventExpiresAtMs,
13638
13939
  ...(boundary.messageRef ? { messageRef: boundary.messageRef } : {}),
13639
13940
  });
13640
13941
  }
@@ -14176,6 +14477,60 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
14176
14477
  );
14177
14478
  }
14178
14479
 
14480
+ private async waitForProviderBackpressure(
14481
+ provider: string,
14482
+ retryAtMs: number,
14483
+ ): Promise<void> {
14484
+ this.assertNoPendingDurableSuspension();
14485
+ const prefix = this.durableBoundaryId(
14486
+ `provider-backpressure-${stableDigest(provider)}-`,
14487
+ );
14488
+ let completedWaits = 0;
14489
+ for (const [boundaryId, value] of Object.entries(
14490
+ this.checkpoint.resolvedBoundaries ?? {},
14491
+ )) {
14492
+ if (!boundaryId.startsWith(prefix)) continue;
14493
+ const boundary = assertKnownResolvedBoundary(boundaryId, value);
14494
+ if (boundary?.kind !== 'sleep') {
14495
+ throw new Error(
14496
+ `Provider backpressure boundary ${boundaryId} is not a sleep.`,
14497
+ );
14498
+ }
14499
+ if (boundary.completedAt !== undefined) completedWaits += 1;
14500
+ }
14501
+ // Count completed waits, not this process's call order. A parked Play is
14502
+ // re-executed by a new sandbox with fresh in-memory counters, and another
14503
+ // provider quota window must not reuse the previous completed sleep.
14504
+ const boundaryId = `${prefix}${completedWaits}`;
14505
+ const existing = assertKnownResolvedBoundary(
14506
+ boundaryId,
14507
+ this.checkpoint.resolvedBoundaries?.[boundaryId],
14508
+ );
14509
+ if (existing?.kind === 'sleep' && existing.completedAt !== undefined) {
14510
+ throw new Error(
14511
+ `Provider backpressure boundary ${boundaryId} was already completed.`,
14512
+ );
14513
+ }
14514
+ const delayMs =
14515
+ existing?.kind === 'sleep'
14516
+ ? existing.delayMs
14517
+ : Math.max(1_000, Math.ceil(retryAtMs - Date.now()));
14518
+ this.checkpoint.resolvedBoundaries = {
14519
+ ...(this.checkpoint.resolvedBoundaries ?? {}),
14520
+ [boundaryId]: { kind: 'sleep', delayMs },
14521
+ };
14522
+ this.runtimeLog(
14523
+ `[runtime.provider.wait] ${JSON.stringify({
14524
+ provider,
14525
+ retry_at: new Date(Date.now() + delayMs).toISOString(),
14526
+ wait_ms: delayMs,
14527
+ boundary_id: boundaryId,
14528
+ })}`,
14529
+ { level: 'warn' },
14530
+ );
14531
+ this.suspendDurably({ kind: 'sleep', boundaryId, delayMs });
14532
+ }
14533
+
14179
14534
  async sleep(ms: number): Promise<void> {
14180
14535
  if (!rowContext.getStore()) this.assertRowAdmissionOpen();
14181
14536
  this.assertInlineChildContract('suspending_child');
@@ -15834,6 +16189,10 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
15834
16189
  rowKey: member.rowKey,
15835
16190
  fieldName: member.fieldName,
15836
16191
  })),
16192
+ providerBackpressureMapRows: detail.members.some(
16193
+ (member) =>
16194
+ Boolean(member.tableNamespace && member.rowKey),
16195
+ ),
15837
16196
  nativeBatchBodyAttribution: bodyAttribution,
15838
16197
  playNodeScope: playNodeScopeForToolCallRequest(
15839
16198
  detail.members[0]!,
@@ -16109,7 +16468,8 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
16109
16468
  // provider. Keep infrastructure and contract failures loud.
16110
16469
  if (
16111
16470
  isProviderExhaustedFailure(entry.error) ||
16112
- isProviderUnavailable(entry.error)
16471
+ isProviderUnavailable(entry.error) ||
16472
+ entry.error instanceof PlayRowProviderBackpressureError
16113
16473
  ) {
16114
16474
  continue;
16115
16475
  }
@@ -16295,6 +16655,50 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
16295
16655
  ): Promise<void> => {
16296
16656
  const followers = liveFollowersByOwnerCallId.get(owner.callId) ?? [];
16297
16657
  liveFollowersByOwnerCallId.delete(owner.callId);
16658
+ if (error instanceof PlayRowProviderBackpressureError) {
16659
+ let releaseFailure: unknown = null;
16660
+ const receiptKey = owner.receiptKey?.trim() || null;
16661
+ try {
16662
+ if (receiptKey && this.durableMappedToolResultsBackedByReceipts) {
16663
+ const leaseId = owner.receiptLeaseId?.trim();
16664
+ if (!leaseId) {
16665
+ throw new Error(
16666
+ `Provider wait cannot release receipt ${receiptKey} without its owned lease.`,
16667
+ );
16668
+ }
16669
+ const released = await this.releaseRuntimeStepReceipt(
16670
+ receiptKey,
16671
+ this.currentReceiptOwnerRunId,
16672
+ leaseId,
16673
+ );
16674
+ if (
16675
+ !released ||
16676
+ (released.status !== 'queued' &&
16677
+ released.status !== 'pending')
16678
+ ) {
16679
+ throw new Error(
16680
+ `Provider wait could not release owned receipt ${receiptKey}.`,
16681
+ );
16682
+ }
16683
+ }
16684
+ } catch (failure) {
16685
+ releaseFailure = failure;
16686
+ }
16687
+ // A provider deadline is a nonterminal control signal. The row
16688
+ // catch records waiting metadata after the resolver unwinds; no
16689
+ // follower may see or persist a failed receipt in between. If the
16690
+ // lease could not be released, wake every follower with that loud
16691
+ // infrastructure failure rather than stranding their promises.
16692
+ for (const request of [owner, ...followers]) {
16693
+ const resolver = this.toolCallResolvers.get(request.callId);
16694
+ if (resolver) {
16695
+ resolver.reject(releaseFailure ?? error);
16696
+ this.toolCallResolvers.delete(request.callId);
16697
+ }
16698
+ }
16699
+ if (releaseFailure) throw releaseFailure;
16700
+ return;
16701
+ }
16298
16702
  const shouldPersistReceiptFailure =
16299
16703
  !(error instanceof ToolExecuteAuthScopeChangedError) &&
16300
16704
  !this.persistenceLatch.tripped;
@@ -17363,9 +17767,16 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
17363
17767
  onChunkComplete: async (chunkResults) => {
17364
17768
  for (const entry of chunkResults) {
17365
17769
  if (entry.error !== undefined) {
17366
- for (const request of entry.request.memberRequests) {
17367
- await rejectWithLiveFollowers(request, entry.error);
17368
- }
17770
+ const settlements = await Promise.allSettled(
17771
+ entry.request.memberRequests.map((request) =>
17772
+ rejectWithLiveFollowers(request, entry.error),
17773
+ ),
17774
+ );
17775
+ const rejected = settlements.find(
17776
+ (settlement): settlement is PromiseRejectedResult =>
17777
+ settlement.status === 'rejected',
17778
+ );
17779
+ if (rejected) throw rejected.reason;
17369
17780
  continue;
17370
17781
  }
17371
17782
  const batchExecution = entry.result;
@@ -17543,11 +17954,16 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
17543
17954
  const failedEntries = dispatchResults.filter(
17544
17955
  (entry) => entry.error !== undefined,
17545
17956
  );
17546
- await Promise.allSettled(
17957
+ const failedSettlements = await Promise.allSettled(
17547
17958
  failedEntries.map((entry) =>
17548
17959
  rejectWithLiveFollowers(entry.job, entry.error),
17549
17960
  ),
17550
17961
  );
17962
+ const failedRejection = failedSettlements.find(
17963
+ (settlement): settlement is PromiseRejectedResult =>
17964
+ settlement.status === 'rejected',
17965
+ );
17966
+ if (failedRejection) throw failedRejection.reason;
17551
17967
  recordToolStep(pendingRequests);
17552
17968
  this.#options.onBatchComplete?.(this.checkpoint);
17553
17969
  }
@@ -18500,6 +18916,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
18500
18916
 
18501
18917
  let providerConcurrencyGrant: ProviderConcurrencyGrant | null = null;
18502
18918
  let providerPacingGrant: ProviderPacingGrant | null = null;
18919
+ let providerGatewayDispatchProof: NonNullable<
18920
+ RuntimeProviderPermit['gatewayDispatchProof']
18921
+ > | null = null;
18503
18922
  // The permit returns the canonical pacing bucket. Keep it alongside
18504
18923
  // the tool-keyed dispatch identity so terminal attribution can join
18505
18924
  // provider admission with the response residence it admitted.
@@ -18509,9 +18928,11 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
18509
18928
  const permit = await this.resourceGovernor.acquireProviderPermit({
18510
18929
  toolId,
18511
18930
  signal,
18931
+ yieldProviderHolds: this.#options.durableBoundaries === true,
18512
18932
  });
18513
18933
  providerConcurrencyGrant = permit.concurrencyGrant ?? null;
18514
18934
  providerPacingGrant = permit.pacingGrant ?? null;
18935
+ providerGatewayDispatchProof = permit.gatewayDispatchProof ?? null;
18515
18936
  providerTelemetryKey = permit.provider;
18516
18937
  return permit;
18517
18938
  };
@@ -18529,6 +18950,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
18529
18950
  providerConcurrencyGrant = null;
18530
18951
  providerPacingGrant = null;
18531
18952
  let response: Response | null = null;
18953
+ let providerRetryAtMs: number | undefined;
18532
18954
  let responseData: Record<string, unknown> | null = null;
18533
18955
  let responseErrorText: string | null = null;
18534
18956
  let providerCallStartedAt: number | null = null;
@@ -18647,6 +19069,8 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
18647
19069
  if (!permit) return null;
18648
19070
  providerConcurrencyGrant = permit.concurrencyGrant ?? null;
18649
19071
  providerPacingGrant = permit.pacingGrant ?? null;
19072
+ providerGatewayDispatchProof =
19073
+ permit.gatewayDispatchProof ?? null;
18650
19074
  providerTelemetryKey = permit.provider;
18651
19075
  return permit;
18652
19076
  },
@@ -18674,6 +19098,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
18674
19098
  providerResourceKey: `tool:${toolId}`,
18675
19099
  signal: abortController?.signal,
18676
19100
  requiresTransportPermit: !fixtureExecution,
19101
+ deferProviderPermitValidation: true,
18677
19102
  onTransportAdmitted: (waitMs) => {
18678
19103
  transportSlotWaitMs = waitMs;
18679
19104
  },
@@ -18702,117 +19127,151 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
18702
19127
  };
18703
19128
  const releaseProviderExecution = () => {
18704
19129
  const execution = providerExecution.current;
19130
+ providerExecution.current = null;
18705
19131
  if (execution) execution.release();
18706
19132
  };
18707
- // Provider admission is our queue, not provider execution.
18708
- // Start the tool deadline only after admission so a busy
18709
- // runtime cannot consume the remote-call budget while this
18710
- // attempt is still waiting for permission to leave.
18711
- if (
18712
- timeoutMs &&
18713
- abortController &&
18714
- !abortController.signal.aborted
18715
- ) {
18716
- timeoutHandle = setTimeout(() => {
18717
- abortController.abort(
18718
- new Error(
18719
- `Tool ${toolId} runtime API call timed out after ${timeoutMs}ms.`,
18720
- ),
18721
- );
18722
- }, timeoutMs);
18723
- }
18724
- const fixtureBehavior = this.#options.fixtureBehavior;
18725
- if (
18726
- fixtureExecution &&
18727
- fixtureBehavior &&
18728
- shouldRouteFixtureToolId(toolId)
18729
- ) {
18730
- const canonicalProvider =
18731
- (
18732
- await this.#options.getToolProvider?.(toolId)
18733
- )?.trim() || provider;
18734
- if (shouldRouteFixtureProvider(canonicalProvider)) {
18735
- const canonicalOperation =
18736
- (
18737
- await this.#options.getToolOperation?.(toolId)
18738
- )?.trim() || toolId;
18739
- // A fixture schedule needs a stable logical-call
18740
- // identity, not a cache receipt. Ordinary /execute
18741
- // calls deliberately do not create durable receipts;
18742
- // deeplineRequestId remains stable across physical
18743
- // transport retries.
18744
- const fixtureRequestIdentity =
18745
- durableCallReceiptKey ?? deeplineRequestId;
18746
- const stableRequestKey = `${canonicalProvider}:${canonicalOperation}:${fixtureRequestIdentity}`;
18747
- const behaviorDigest = stableDigest(
18748
- JSON.stringify(fixtureBehavior),
18749
- );
18750
- const requestKeyDigest = stableDigest(stableRequestKey);
18751
- let selected: {
18752
- delayMs: number;
18753
- sampleIndex: number;
18754
- } | null = null;
18755
- beginProviderExecution();
18756
- const delayStartedAt = Date.now();
18757
- providerCallStartedAt = delayStartedAt;
18758
- try {
18759
- selected = await waitForFixtureResponseDelay({
18760
- behavior: fixtureBehavior,
18761
- stableRequestKey,
18762
- signal: abortController?.signal,
18763
- onSelected: (value) => {
18764
- selected = value;
19133
+ try {
19134
+ return await request(
19135
+ async (beforeProviderDispatch) => {
19136
+ // Fixture delay models provider residence. The shared
19137
+ // permit may still need re-admission afterwards, so
19138
+ // pause this budget around that queueing boundary.
19139
+ let fixtureTimeoutUsedMs = 0;
19140
+ const armProviderTimeout = () => {
19141
+ const controller = abortController;
19142
+ if (
19143
+ !timeoutMs ||
19144
+ !controller ||
19145
+ controller.signal.aborted
19146
+ )
19147
+ return;
19148
+ timeoutHandle = setTimeout(
19149
+ () => {
19150
+ controller.abort(
19151
+ new Error(
19152
+ `Tool ${toolId} runtime API call timed out after ${timeoutMs}ms.`,
19153
+ ),
19154
+ );
19155
+ },
19156
+ Math.max(1, timeoutMs - fixtureTimeoutUsedMs),
19157
+ );
19158
+ };
19159
+ const fixtureBehavior = this.#options.fixtureBehavior;
19160
+ if (
19161
+ fixtureExecution &&
19162
+ fixtureBehavior &&
19163
+ shouldRouteFixtureToolId(toolId)
19164
+ ) {
19165
+ const canonicalProvider =
19166
+ (
19167
+ await this.#options.getToolProvider?.(toolId)
19168
+ )?.trim() || provider;
19169
+ if (shouldRouteFixtureProvider(canonicalProvider)) {
19170
+ const canonicalOperation =
19171
+ (
19172
+ await this.#options.getToolOperation?.(toolId)
19173
+ )?.trim() || toolId;
19174
+ // A fixture schedule needs a stable logical-call
19175
+ // identity, not a cache receipt. Ordinary /execute
19176
+ // calls deliberately do not create durable receipts;
19177
+ // deeplineRequestId remains stable across physical
19178
+ // transport retries.
19179
+ const fixtureRequestIdentity =
19180
+ durableCallReceiptKey ?? deeplineRequestId;
19181
+ const stableRequestKey = `${canonicalProvider}:${canonicalOperation}:${fixtureRequestIdentity}`;
19182
+ const behaviorDigest = stableDigest(
19183
+ JSON.stringify(fixtureBehavior),
19184
+ );
19185
+ const requestKeyDigest =
19186
+ stableDigest(stableRequestKey);
19187
+ let selected: {
19188
+ delayMs: number;
19189
+ sampleIndex: number;
19190
+ } | null = null;
19191
+ beginProviderExecution();
19192
+ const delayStartedAt = Date.now();
19193
+ providerCallStartedAt = delayStartedAt;
19194
+ armProviderTimeout();
19195
+ try {
19196
+ selected = await waitForFixtureResponseDelay({
19197
+ behavior: fixtureBehavior,
19198
+ stableRequestKey,
19199
+ signal: abortController?.signal,
19200
+ onSelected: (value) => {
19201
+ selected = value;
19202
+ this.runtimeLog(
19203
+ `[fixture.response_delay] ${JSON.stringify(
19204
+ {
19205
+ outcome: 'scheduled',
19206
+ provider: canonicalProvider,
19207
+ operation: canonicalOperation,
19208
+ behavior_digest: `sha256_${behaviorDigest}`,
19209
+ request_key_digest: `sha256_${requestKeyDigest}`,
19210
+ delay_ms: value.delayMs,
19211
+ sample_index: value.sampleIndex,
19212
+ physical_attempt: physicalAttempt,
19213
+ },
19214
+ )}`,
19215
+ );
19216
+ },
19217
+ });
19218
+ } catch (error) {
19219
+ this.runtimeLog(
19220
+ `[fixture.response_delay] ${JSON.stringify({
19221
+ outcome: 'aborted',
19222
+ provider: canonicalProvider,
19223
+ operation: canonicalOperation,
19224
+ behavior_digest: `sha256_${behaviorDigest}`,
19225
+ request_key_digest: `sha256_${requestKeyDigest}`,
19226
+ delay_ms: selected?.delayMs ?? null,
19227
+ sample_index: selected?.sampleIndex ?? null,
19228
+ physical_attempt: physicalAttempt,
19229
+ elapsed_ms: Date.now() - delayStartedAt,
19230
+ })}`,
19231
+ );
19232
+ releaseProviderExecution();
19233
+ throw error;
19234
+ }
19235
+ fixtureDelayMs = Date.now() - delayStartedAt;
19236
+ fixtureTimeoutUsedMs = fixtureDelayMs;
18765
19237
  this.runtimeLog(
18766
19238
  `[fixture.response_delay] ${JSON.stringify({
18767
- outcome: 'scheduled',
19239
+ outcome: 'completed',
18768
19240
  provider: canonicalProvider,
18769
19241
  operation: canonicalOperation,
18770
19242
  behavior_digest: `sha256_${behaviorDigest}`,
18771
19243
  request_key_digest: `sha256_${requestKeyDigest}`,
18772
- delay_ms: value.delayMs,
18773
- sample_index: value.sampleIndex,
19244
+ delay_ms: selected.delayMs,
19245
+ sample_index: selected.sampleIndex,
18774
19246
  physical_attempt: physicalAttempt,
19247
+ elapsed_ms: Date.now() - delayStartedAt,
18775
19248
  })}`,
18776
19249
  );
18777
- },
18778
- });
18779
- } catch (error) {
18780
- this.runtimeLog(
18781
- `[fixture.response_delay] ${JSON.stringify({
18782
- outcome: 'aborted',
18783
- provider: canonicalProvider,
18784
- operation: canonicalOperation,
18785
- behavior_digest: `sha256_${behaviorDigest}`,
18786
- request_key_digest: `sha256_${requestKeyDigest}`,
18787
- delay_ms: selected?.delayMs ?? null,
18788
- sample_index: selected?.sampleIndex ?? null,
18789
- physical_attempt: physicalAttempt,
18790
- elapsed_ms: Date.now() - delayStartedAt,
18791
- })}`,
19250
+ }
19251
+ }
19252
+ if (timeoutHandle) {
19253
+ clearTimeout(timeoutHandle);
19254
+ timeoutHandle = null;
19255
+ }
19256
+ const dispatchBoundary = beforeProviderDispatch(
19257
+ releaseProviderExecution,
18792
19258
  );
18793
- releaseProviderExecution();
18794
- throw error;
18795
- }
18796
- fixtureDelayMs = Date.now() - delayStartedAt;
18797
- this.runtimeLog(
18798
- `[fixture.response_delay] ${JSON.stringify({
18799
- outcome: 'completed',
18800
- provider: canonicalProvider,
18801
- operation: canonicalOperation,
18802
- behavior_digest: `sha256_${behaviorDigest}`,
18803
- request_key_digest: `sha256_${requestKeyDigest}`,
18804
- delay_ms: selected.delayMs,
18805
- sample_index: selected.sampleIndex,
18806
- physical_attempt: physicalAttempt,
18807
- elapsed_ms: Date.now() - delayStartedAt,
18808
- })}`,
18809
- );
18810
- }
18811
- }
18812
- beginProviderExecution();
18813
- try {
18814
- return await request(
18815
- async () => {
19259
+ const permitReacquired =
19260
+ typeof dispatchBoundary === 'boolean'
19261
+ ? dispatchBoundary
19262
+ : await dispatchBoundary;
19263
+ if (permitReacquired) {
19264
+ // A shared Retry-After may have arrived while
19265
+ // fixture/request preparation was suspended. The
19266
+ // old residence was not provider work; restart the
19267
+ // latency clock with the fresh permit.
19268
+ providerCallStartedAt = Date.now();
19269
+ }
19270
+ // A stale permit can take arbitrarily long to
19271
+ // replace. Only fixture residence and the physical
19272
+ // HTTP exchange spend the tool execution deadline.
19273
+ armProviderTimeout();
19274
+ beginProviderExecution();
18816
19275
  const fetchedAt = Date.now();
18817
19276
  const dispatchedProviderCallStartedAt =
18818
19277
  providerCallStartedAt ?? fetchedAt;
@@ -18842,6 +19301,16 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
18842
19301
  // settle the row as failed instead of pacing.
18843
19302
  [TOOL_EXECUTE_RUNNER_ACCEPTS_HEADER]:
18844
19303
  TOOL_EXECUTE_RUNNER_ACCEPTS_VALUE,
19304
+ ...(providerGatewayDispatchProof
19305
+ ? {
19306
+ [TOOL_EXECUTE_PROVIDER_RATE_SCOPE_HEADER]:
19307
+ providerGatewayDispatchProof.rateScopeToken,
19308
+ [TOOL_EXECUTE_PROVIDER_RATE_RETRY_SNAPSHOT_HEADER]:
19309
+ String(
19310
+ providerGatewayDispatchProof.retryAtMsAtGrant,
19311
+ ),
19312
+ }
19313
+ : {}),
18845
19314
  'x-deepline-request-id': deeplineRequestId,
18846
19315
  ...(providerExecutionFenceKey
18847
19316
  ? {
@@ -18868,6 +19337,19 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
18868
19337
  if (dispatchedResponse.status === 429) {
18869
19338
  providerRequestsInFlightAt429 =
18870
19339
  activeProviderRequest?.inFlightCount() ?? null;
19340
+ const observedAt = Date.now();
19341
+ const retryAfter =
19342
+ dispatchedResponse.headers.get('retry-after');
19343
+ // Without a provider deadline the classifier owns
19344
+ // the conservative bare-429 cooldown. Do not replace
19345
+ // it with the generic one-second HTTP retry fallback.
19346
+ providerRetryAtMs = retryAfter?.trim()
19347
+ ? observedAt +
19348
+ parseToolExecuteRetryAfterMs(
19349
+ retryAfter,
19350
+ observedAt,
19351
+ )
19352
+ : undefined;
18871
19353
  }
18872
19354
  const dispatchedFetchToHeadersMs =
18873
19355
  Date.now() - fetchedAt;
@@ -19056,6 +19538,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
19056
19538
  ? null
19057
19539
  : Date.now() - providerCallStartedAt,
19058
19540
  providerConcurrencyGrant,
19541
+ providerRetryAtMs,
19059
19542
  );
19060
19543
  providerBackpressureReported = true;
19061
19544
  } else if (
@@ -19076,6 +19559,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
19076
19559
  ? null
19077
19560
  : Date.now() - providerCallStartedAt,
19078
19561
  providerConcurrencyGrant,
19562
+ providerRetryAtMs,
19079
19563
  );
19080
19564
  providerBackpressureReported = true;
19081
19565
  }
@@ -19144,6 +19628,36 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
19144
19628
  }
19145
19629
  if (error instanceof ProviderExhaustedError) {
19146
19630
  const retryAtMs = Date.parse(error.retryAt);
19631
+ if (!this.#options.durableBoundaries) {
19632
+ await this.sleepWithCheckpointHeartbeat(
19633
+ Math.max(
19634
+ 1_000,
19635
+ Number.isFinite(retryAtMs)
19636
+ ? Math.ceil(retryAtMs - Date.now())
19637
+ : 1_000,
19638
+ ),
19639
+ );
19640
+ continue;
19641
+ }
19642
+ if (this.#options.durableBoundaries) {
19643
+ const rowScope = rowContext.getStore();
19644
+ if (options?.providerBackpressureMapRows || rowScope) {
19645
+ throw new PlayRowProviderBackpressureError(
19646
+ error.provider,
19647
+ Number.isFinite(retryAtMs) ? retryAtMs : Date.now(),
19648
+ );
19649
+ }
19650
+ // This is a provider admission hold, not a row outcome.
19651
+ // Release the sandbox and resume the same Play after the
19652
+ // shared pacer's deadline. The receipt executor releases
19653
+ // this call's claim on suspension, so completed work remains
19654
+ // reusable and this tool can re-admit safely.
19655
+ await this.waitForProviderBackpressure(
19656
+ error.provider,
19657
+ Number.isFinite(retryAtMs) ? retryAtMs : Date.now(),
19658
+ );
19659
+ continue;
19660
+ }
19147
19661
  const providerFailure =
19148
19662
  error.cause instanceof ToolExecutionError
19149
19663
  ? error.cause
@@ -19411,22 +19925,41 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
19411
19925
  failure.backpressureDelayMs,
19412
19926
  providerCallElapsedMs,
19413
19927
  providerConcurrencyGrant,
19928
+ providerRetryAtMs,
19414
19929
  );
19415
19930
  providerBackpressureReported = true;
19416
- // A Retry-After beyond the durable pacer's maximum wait has
19417
- // no useful in-process retry. It is already an exhausted
19418
- // provider outcome: surface the same typed error the next
19419
- // provider admission would produce, without sleeping before a
19420
- // waterfall can choose an independent fallback.
19931
+ }
19932
+ // The normal 429 path reports its hold before releasing the
19933
+ // provider lease. A mapped row must now yield even for a short
19934
+ // hold: sleeping here would occupy its row slot and prevent
19935
+ // unrelated rows from making progress.
19936
+ const durableProviderWaitMs =
19937
+ failure.providerPacingWaitMs ?? failure.backpressureDelayMs;
19938
+ if (
19939
+ this.#options.durableBoundaries &&
19940
+ durableProviderWaitMs !== null &&
19941
+ durableProviderWaitMs > 0
19942
+ ) {
19943
+ const retryAtMs =
19944
+ (failure.providerPacingWaitMs !== null
19945
+ ? Date.now() + failure.providerPacingWaitMs
19946
+ : providerRetryAtMs) ?? Date.now() + durableProviderWaitMs;
19947
+ const rowScope = rowContext.getStore();
19421
19948
  if (
19422
- failure.backpressureDelayMs > PROVIDER_EXHAUSTED_MAX_WAIT_MS
19949
+ this.#options.durableBoundaries &&
19950
+ (options?.providerBackpressureMapRows || rowScope)
19423
19951
  ) {
19424
- throw new ProviderExhaustedError({
19952
+ throw new PlayRowProviderBackpressureError(
19425
19953
  provider,
19426
- retryAtMs: Date.now() + failure.backpressureDelayMs,
19427
- cause: surfacedError,
19428
- });
19954
+ retryAtMs,
19955
+ );
19429
19956
  }
19957
+ await this.waitForProviderBackpressure(provider, retryAtMs);
19958
+ throw new ProviderExhaustedError({
19959
+ provider,
19960
+ retryAtMs,
19961
+ cause: surfacedError,
19962
+ });
19430
19963
  }
19431
19964
  // A Deepline concurrency shed is issued by our own gateway/relay
19432
19965
  // before it opens any provider connection and before any durable
@@ -19438,18 +19971,25 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
19438
19971
  // 5xx retries safe — otherwise a protective limit on the relay
19439
19972
  // turns straight into customer row failures.
19440
19973
  const shedKnownNotDispatched =
19441
- failure.reason === 'concurrency_backpressure';
19974
+ failure.reason === 'concurrency_backpressure' ||
19975
+ failure.reason === 'provider_pacing_wait';
19442
19976
  if (
19443
19977
  failure.shouldRetry &&
19444
19978
  (failure.knownNotDispatched ||
19445
19979
  failure.reason === 'gateway_invocation_in_progress' ||
19446
19980
  ambiguousTransportReplayAllowed ||
19447
19981
  retrySafeTransientHttp ||
19448
- fixtureReplaySafe)
19982
+ fixtureReplaySafe ||
19983
+ // An explicit provider-owned 429 is a rejected operation,
19984
+ // independently of whether generic 5xx replay is safe.
19985
+ // The gateway attests the prior 429 before accepting a
19986
+ // fresh fenced invocation attempt; never extend this to
19987
+ // platform/caller 429s or ambiguous transport failures.
19988
+ failure.backpressureDelayMs !== null)
19449
19989
  ) {
19450
19990
  // A gateway follower is retrying its owner's durable
19451
- // invocation, while a concurrency shed wrote no invocation at
19452
- // all. Both must reuse this logical attempt: advancing it
19991
+ // invocation, while a concurrency or provider-pacing refusal
19992
+ // wrote no invocation at all. Both reuse this logical attempt: advancing it
19453
19993
  // either bypasses the live-owner replay fence or makes the
19454
19994
  // prior-attempt sweep see an intentionally absent receipt.
19455
19995
  if (
@@ -19466,9 +20006,12 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
19466
20006
  if (failure.chargeRetryBudget) {
19467
20007
  await this.governor.chargeBudget('retry');
19468
20008
  }
19469
- const retryAttributePrefix = failure.isRateLimit
19470
- ? 'rate_limit'
19471
- : 'transient_http';
20009
+ const retryAttributePrefix =
20010
+ failure.reason === 'provider_pacing_wait'
20011
+ ? 'provider_pacing_wait'
20012
+ : failure.isRateLimit
20013
+ ? 'rate_limit'
20014
+ : 'transient_http';
19472
20015
  span.setAttribute(
19473
20016
  `plays.${retryAttributePrefix}_retry_after_ms`,
19474
20017
  failure.retryDelayMs,
@@ -19499,7 +20042,10 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
19499
20042
  reason: failure.isRateLimit
19500
20043
  ? 'rate_limit'
19501
20044
  : 'provider_error',
19502
- retryAt: Date.now() + failure.retryDelayMs,
20045
+ retryAt:
20046
+ failure.providerPacingWaitMs !== null
20047
+ ? Date.now() + failure.providerPacingWaitMs
20048
+ : Date.now() + failure.retryDelayMs,
19503
20049
  attempt: httpFailureAttempt,
19504
20050
  },
19505
20051
  observedAt: Date.now(),
@@ -19614,6 +20160,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
19614
20160
  retryAfterMs: number,
19615
20161
  latencyMs: number | null = null,
19616
20162
  concurrencyGrant: ProviderConcurrencyGrant | null = null,
20163
+ retryAtMs = Date.now() + Math.max(0, retryAfterMs),
19617
20164
  ): Promise<void> {
19618
20165
  const hints: readonly PlayQueueHint[] = this.#options.getToolQueueHints
19619
20166
  ? await this.#options.getToolQueueHints(toolId)
@@ -19623,9 +20170,20 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
19623
20170
  provider,
19624
20171
  toolId,
19625
20172
  retryAfterMs,
20173
+ retryAtMs,
19626
20174
  latencyMs,
19627
20175
  concurrencyGrant,
19628
20176
  });
20177
+ this.runtimeLog(
20178
+ `[runtime.provider.backpressure_report] ${JSON.stringify({
20179
+ toolId,
20180
+ provider,
20181
+ retryAfterMs,
20182
+ retryAtMs,
20183
+ outcome: 'recorded',
20184
+ })}`,
20185
+ { level: 'warn' },
20186
+ );
19629
20187
  }
19630
20188
 
19631
20189
  private async sleepWithCheckpointHeartbeat(ms: number): Promise<void> {