deepline 0.3.200 → 0.3.202

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 (43) hide show
  1. package/dist/bundling-sources/sdk/src/client.ts +62 -1
  2. package/dist/bundling-sources/sdk/src/http.ts +55 -6
  3. package/dist/bundling-sources/sdk/src/index.ts +1 -0
  4. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  5. package/dist/bundling-sources/sdk/src/types.ts +10 -2
  6. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +4 -2
  7. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +460 -96
  8. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +11 -1
  9. package/dist/bundling-sources/shared_libs/play-runtime/execution-gateway-invocation-fence.ts +6 -2
  10. package/dist/bundling-sources/shared_libs/play-runtime/fixture-behavior.ts +401 -2
  11. package/dist/bundling-sources/shared_libs/play-runtime/output-size-limits.ts +458 -0
  12. package/dist/bundling-sources/shared_libs/play-runtime/provider-no-progress.ts +40 -0
  13. package/dist/bundling-sources/shared_libs/play-runtime/receipt-sql.ts +7 -0
  14. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +25 -3
  15. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +1 -0
  16. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +119 -171
  17. package/dist/bundling-sources/shared_libs/play-runtime/runtime-work-counters.ts +37 -0
  18. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-deadline-reconciliation.ts +160 -58
  19. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +111 -6
  20. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd.ts +51 -46
  21. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts +16 -7
  22. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +304 -33
  23. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-gateway.ts +21 -0
  24. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-receipt-value-storage.ts +64 -15
  25. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-sandbox-capacity.ts +133 -5
  26. package/dist/bundling-sources/shared_libs/play-runtime/sheet-attempt-sql.ts +5 -5
  27. package/dist/bundling-sources/shared_libs/play-runtime/tool-call/index.ts +1 -0
  28. package/dist/bundling-sources/shared_libs/play-runtime/tool-call/result-receipt-policy.ts +46 -0
  29. package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +14 -5
  30. package/dist/bundling-sources/shared_libs/play-runtime/tool-http-errors.ts +5 -1
  31. package/dist/bundling-sources/shared_libs/play-runtime/work-receipt-state-machine.ts +3 -9
  32. package/dist/cli/index.js +364 -39
  33. package/dist/cli/index.mjs +367 -41
  34. package/dist/index.d.mts +131 -3
  35. package/dist/index.d.ts +131 -3
  36. package/dist/index.js +64 -7
  37. package/dist/index.mjs +64 -7
  38. package/dist/install-integrity.json +3 -0
  39. package/dist/release.d.mts +1 -1
  40. package/dist/release.d.ts +1 -1
  41. package/dist/release.js +1 -1
  42. package/dist/release.mjs +1 -1
  43. package/package.json +1 -1
@@ -125,6 +125,11 @@ import {
125
125
  ProviderExhaustedError,
126
126
  } from './run-failure';
127
127
  import { PROVIDER_EXHAUSTED_MAX_WAIT_MS } from './governor/app-runtime-rate-state-backend';
128
+ import {
129
+ observeProviderNoProgress,
130
+ providerNoProgressExpired,
131
+ PROVIDER_NO_PROGRESS_MAX_MS,
132
+ } from './provider-no-progress';
128
133
  import {
129
134
  buildPlayContractCompatibility,
130
135
  normalizePlayContractCompatibility,
@@ -174,6 +179,7 @@ import {
174
179
  createToolCallReceiptCohortJob,
175
180
  createRuntimeStepToolResultReceipts,
176
181
  createToolCallJob,
182
+ resolveToolCallResultReceiptPolicy,
177
183
  ToolCallReceiptLeaseLostError,
178
184
  type ToolCallReceiptCohortStore,
179
185
  type ToolCallReceiptCohortTimings,
@@ -197,6 +203,7 @@ import {
197
203
  classifyToolExecuteHttpFailure,
198
204
  createToolExecuteHttpFailureAttemptTracker,
199
205
  parseToolExecuteRetryAfterMs,
206
+ parseToolExecuteRetryAtMs,
200
207
  parseToolExecuteAuthScopeChangedError,
201
208
  ToolExecuteAuthScopeChangedError,
202
209
  type ToolExecuteHttpFailureOutcome,
@@ -494,6 +501,8 @@ const rowContext = new AsyncLocalStorage<{
494
501
  * null between segments. Measurement only; see `RowResolverGapTracker`.
495
502
  */
496
503
  resolverGap?: RowResolverGapTracker;
504
+ /** Shared through nested fields, reset for each row execution attempt. */
505
+ providerBackpressure?: { pending: PlayRowProviderBackpressureError | null };
497
506
  }>();
498
507
 
499
508
  /**
@@ -562,6 +571,7 @@ const TOOL_RETRY_AFTER_FALLBACK_MS = 1_000;
562
571
  // broken gateway-to-runner connection, not slow provider work.
563
572
  export const TOOL_EXECUTE_RESPONSE_BODY_TIMEOUT_MS = 30_000;
564
573
  const TOOL_RETRY_HEARTBEAT_INTERVAL_MS = 30_000;
574
+ const TOOL_PROVIDER_RESIDENT_WAIT_BUDGET_MS = 5_000;
565
575
  const DEEPLINEAGENT_TOOL_RUNTIME_TIMEOUT_MS = 15 * 60 * 1000;
566
576
  // Every runtime-API tool fetch needs a client-side deadline. Without one, a
567
577
  // stalled execute request (e.g. a credential-less call whose gateway connection
@@ -3104,15 +3114,23 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
3104
3114
  /**
3105
3115
  * Called at ctx effect boundaries and by the runner before it can settle a
3106
3116
  * normal result. This makes suspension sticky even when authored code uses a
3107
- * broad catch around `await ctx.sleep(...)`.
3117
+ * broad catch around a sleep or provider hold. A row hold stays row-local
3118
+ * until its siblings drain and the map commits the scheduler handoff.
3108
3119
  */
3109
3120
  assertNoPendingDurableSuspension(): void {
3121
+ this.#options.executionSignal?.throwIfAborted();
3110
3122
  this.assertNoSlackHitlTimeoutFailure();
3123
+ this.assertNoRowProviderBackpressure();
3111
3124
  if (this.pendingDurableSuspension) {
3112
3125
  throw new PlayExecutionSuspendedError(this.pendingDurableSuspension);
3113
3126
  }
3114
3127
  }
3115
3128
 
3129
+ private assertNoRowProviderBackpressure(): void {
3130
+ const pending = rowContext.getStore()?.providerBackpressure?.pending;
3131
+ if (pending) throw pending;
3132
+ }
3133
+
3116
3134
  /**
3117
3135
  * The single seam that hands this invocation back to the scheduler. The
3118
3136
  * first suspension wins: a later boundary reached by code that caught the
@@ -6795,7 +6813,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
6795
6813
  execution: {
6796
6814
  mode: 'map',
6797
6815
  ownerRunId: this.currentReceiptOwnerRunId,
6798
- force: input.request.force === true,
6816
+ force: input.resultReceipts
6817
+ ? this.mappedToolResultReceiptPolicy(input.request).force
6818
+ : input.request.force === true,
6799
6819
  },
6800
6820
  };
6801
6821
  if (alwaysFresh) {
@@ -6829,8 +6849,8 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
6829
6849
  }
6830
6850
 
6831
6851
  /**
6832
- * A forced call bypasses the immutable Tool Result Receipt, so its physical
6833
- * gateway invocation must bypass that receipt's replay identity too. Keep
6852
+ * A forced call bypasses an earlier run's shared result, so its physical
6853
+ * gateway invocation must bypass that result's replay identity too. Keep
6834
6854
  * the transport key deterministic for one logical call: a retry of that
6835
6855
  * call still converges, while a later explicit force gets a fresh identity.
6836
6856
  */
@@ -6847,6 +6867,22 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
6847
6867
  })}${stableDigest(`forced-provider-invocation:${input.providerOperationKey}`)}`;
6848
6868
  }
6849
6869
 
6870
+ private mappedToolResultReceiptPolicy(request: ToolCallRequest) {
6871
+ if (!request.receiptKey || !request.logicalCallId) {
6872
+ throw new Error(
6873
+ 'Mapped Tool Call needs stable result and call identities.',
6874
+ );
6875
+ }
6876
+ return resolveToolCallResultReceiptPolicy({
6877
+ orgId: this.#options.orgId,
6878
+ toolId: request.toolId,
6879
+ resultReceiptKey: request.receiptKey,
6880
+ runId: this.currentRunId,
6881
+ logicalCallId: request.logicalCallId,
6882
+ force: request.force === true,
6883
+ });
6884
+ }
6885
+
6850
6886
  /**
6851
6887
  * Ordinary operations do not carry provider-idempotency metadata, but the
6852
6888
  * execution relay still needs a semantic signal that an explicit force is
@@ -10733,6 +10769,12 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
10733
10769
  const rowKey = executionRowKey(baseRow, idx);
10734
10770
  const rowIndex = executionRowIndex(idx);
10735
10771
  const computedFields: Record<string, unknown> = {};
10772
+ // A provider hold is scheduler control, not a catchable row outcome.
10773
+ // Keep its intent outside authored code so catch/fallback logic cannot
10774
+ // publish a successful row that silently omitted an unfinished call.
10775
+ const providerBackpressure = {
10776
+ pending: null as PlayRowProviderBackpressureError | null,
10777
+ };
10736
10778
  let activeFieldName: string | null = null;
10737
10779
  // ADR 0019: the row's read order, computed once. Every cell reads a
10738
10780
  // prefix of it — the inputs, then each column this run wrote — so this
@@ -10793,6 +10835,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
10793
10835
  mapStallObserver: mapStallObserver ?? undefined,
10794
10836
  mapStallResolverToken: resolverObservation?.token,
10795
10837
  resolverGap,
10838
+ providerBackpressure,
10796
10839
  },
10797
10840
  async () => {
10798
10841
  try {
@@ -12675,7 +12718,18 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
12675
12718
  return resolver;
12676
12719
  }
12677
12720
 
12678
- const value = await resolver(row, this, fields, index, previousCell);
12721
+ let value: unknown;
12722
+ try {
12723
+ value = await resolver(row, this, fields, index, previousCell);
12724
+ } catch (error) {
12725
+ // Cancellation and persistence/ownership failures stay run-fatal. A
12726
+ // provider wait must never convert them into another scheduled retry.
12727
+ if (isRowIsolationExemptError(error)) throw error;
12728
+ // Also retain the wait when authored fallback code throws a different
12729
+ // error after catching it. This check is local to the held row.
12730
+ this.assertNoRowProviderBackpressure();
12731
+ throw error;
12732
+ }
12679
12733
  // This is the common user-code return seam for every mapped field path
12680
12734
  // (incremental, pure, and step-program adapters). Check before callers
12681
12735
  // serialize a cell, emit a completion event, or advance a map frame.
@@ -12772,6 +12826,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
12772
12826
  options?: ToolCallOptions,
12773
12827
  ): Promise<unknown> {
12774
12828
  if (!rowContext.getStore()) this.assertRowAdmissionOpen();
12829
+ this.assertNoPendingDurableSuspension();
12775
12830
  if (options?.description !== undefined) {
12776
12831
  validatePlayAuthoringField(
12777
12832
  'ctx.tools.execute.description',
@@ -13184,6 +13239,21 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
13184
13239
  toolId,
13185
13240
  })}${stableDigest(`${this.currentRunId}:${logicalCallId}:always-fresh`)}`;
13186
13241
  const resultReceiptKey = directResultReceiptKey;
13242
+ const resultReceiptPolicy = resolveToolCallResultReceiptPolicy({
13243
+ orgId: this.#options.orgId,
13244
+ toolId,
13245
+ resultReceiptKey,
13246
+ runId: this.currentRunId,
13247
+ logicalCallId,
13248
+ force: toolCachePolicy.force,
13249
+ canStoreResult: Boolean(
13250
+ this.#options.claimRuntimeStepReceipt &&
13251
+ (this.#options.getRuntimeStepReceipt ||
13252
+ this.#options.getRuntimeStepReceipts) &&
13253
+ (this.#options.completeRuntimeStepReceipt ||
13254
+ this.#options.completeRuntimeStepReceipts),
13255
+ ),
13256
+ });
13187
13257
  const toolRetryPolicy = await this.#options
13188
13258
  .getToolRetryPolicy?.(toolId, input)
13189
13259
  .catch(() => null);
@@ -13337,7 +13407,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
13337
13407
  },
13338
13408
  }
13339
13409
  : {}),
13340
- recover: ({ result, resultReceiptKey, source }) =>
13410
+ recover: ({ result, source }) =>
13341
13411
  cloneToolExecuteResultWithExecution(
13342
13412
  result,
13343
13413
  toolExecutionMetadataForOutcome(
@@ -13372,14 +13442,14 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
13372
13442
  execution: {
13373
13443
  mode: 'direct',
13374
13444
  ownerRunId: this.currentReceiptOwnerRunId,
13375
- force: toolCachePolicy.force,
13445
+ force: resultReceiptPolicy.force,
13376
13446
  },
13377
13447
  };
13378
13448
  const wrapped = toolCall
13379
13449
  ? await toolCall.call({
13380
13450
  ...call,
13381
13451
  compatibility: {
13382
- resultReceiptKey,
13452
+ resultReceiptKey: resultReceiptPolicy.resultReceiptKey,
13383
13453
  providerOperationKey,
13384
13454
  },
13385
13455
  })
@@ -13593,7 +13663,20 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
13593
13663
  };
13594
13664
 
13595
13665
  if (store || !cacheableToolResult) {
13596
- return await executeTool();
13666
+ try {
13667
+ return await executeTool();
13668
+ } catch (error) {
13669
+ if (
13670
+ error instanceof PlayRowProviderBackpressureError &&
13671
+ store?.providerBackpressure
13672
+ ) {
13673
+ // The batch dispatcher has no reliable single-row async scope.
13674
+ // Capture the signal at delivery to its actual requesting row,
13675
+ // before authored code can catch the rejected tool promise.
13676
+ store.providerBackpressure.pending ??= error;
13677
+ }
13678
+ throw error;
13679
+ }
13597
13680
  }
13598
13681
 
13599
13682
  for (
@@ -14554,6 +14637,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
14554
14637
 
14555
14638
  async sleep(ms: number): Promise<void> {
14556
14639
  if (!rowContext.getStore()) this.assertRowAdmissionOpen();
14640
+ this.assertNoPendingDurableSuspension();
14557
14641
  this.assertInlineChildContract('suspending_child');
14558
14642
  const delayMs =
14559
14643
  this.currentRuntimeSemantics.validation === 'strict'
@@ -15555,7 +15639,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
15555
15639
  const request = input.requestForReceipt.get(receiptKey);
15556
15640
  const candidates = [
15557
15641
  receiptKey,
15558
- ...(request?.legacyReceiptKeys ?? []),
15642
+ ...(request?.force === true
15643
+ ? []
15644
+ : (request?.legacyReceiptKeys ?? [])),
15559
15645
  ];
15560
15646
  for (const candidate of candidates) {
15561
15647
  const record = await asRecord(
@@ -15577,8 +15663,10 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
15577
15663
  ...new Set(
15578
15664
  resultReceiptKeys.flatMap((receiptKey) => [
15579
15665
  receiptKey,
15580
- ...(input.requestForReceipt.get(receiptKey)
15581
- ?.legacyReceiptKeys ?? []),
15666
+ ...(input.requestForReceipt.get(receiptKey)?.force === true
15667
+ ? []
15668
+ : (input.requestForReceipt.get(receiptKey)
15669
+ ?.legacyReceiptKeys ?? [])),
15582
15670
  ]),
15583
15671
  ),
15584
15672
  ];
@@ -15777,12 +15865,8 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
15777
15865
  const requestsForReceipt = new Map<string, ToolCallRequest[]>();
15778
15866
  const requestByCallId = new Map<string, ToolCallRequest>();
15779
15867
  for (const request of requests) {
15780
- const receiptKey = request.receiptKey?.trim();
15781
- if (!receiptKey) {
15782
- throw new Error(
15783
- 'Cacheable non-native map Tool Call cohort requires receipt keys.',
15784
- );
15785
- }
15868
+ const receiptKey =
15869
+ this.mappedToolResultReceiptPolicy(request).resultReceiptKey;
15786
15870
  requestForReceipt.set(receiptKey, request);
15787
15871
  const shared = requestsForReceipt.get(receiptKey);
15788
15872
  if (shared) shared.push(request);
@@ -15806,8 +15890,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
15806
15890
  },
15807
15891
  members: requests.map((request) => ({
15808
15892
  memberId: request.callId,
15809
- resultReceiptKey: request.receiptKey!,
15810
- force: request.force === true,
15893
+ ...this.mappedToolResultReceiptPolicy(request),
15811
15894
  forceFailedRefresh: request.forceFailedRefresh === true,
15812
15895
  waitMaxAttempts: resolveRuntimeToolReceiptWaitMaxAttempts(
15813
15896
  typeof request.receiptWaitMs === 'number'
@@ -15990,7 +16073,8 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
15990
16073
  request,
15991
16074
  dispatchToolId: toolId,
15992
16075
  dispatchPayload: request.input,
15993
- resultReceiptKey: request.receiptKey!,
16076
+ resultReceiptKey:
16077
+ this.mappedToolResultReceiptPolicy(request).resultReceiptKey,
15994
16078
  providerOperationKey,
15995
16079
  resultReceipts: receipts,
15996
16080
  apiOptions: {
@@ -16052,12 +16136,8 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
16052
16136
  const requestsForReceipt = new Map<string, ToolCallRequest[]>();
16053
16137
  const requestByCallId = new Map<string, ToolCallRequest>();
16054
16138
  for (const request of requests) {
16055
- const receiptKey = request.receiptKey?.trim();
16056
- if (!receiptKey) {
16057
- throw new Error(
16058
- 'Cacheable native map Tool Call cohort requires receipt keys.',
16059
- );
16060
- }
16139
+ const receiptKey =
16140
+ this.mappedToolResultReceiptPolicy(request).resultReceiptKey;
16061
16141
  requestForReceipt.set(receiptKey, request);
16062
16142
  const shared = requestsForReceipt.get(receiptKey);
16063
16143
  if (shared) shared.push(request);
@@ -16101,12 +16181,22 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
16101
16181
  const successful: ToolCallRequest[] = [];
16102
16182
 
16103
16183
  const deliver = (request: ToolCallRequest, result: ToolExecuteResult) => {
16184
+ const presented =
16185
+ request.force === true
16186
+ ? cloneToolExecuteResultWithExecution(result, {
16187
+ ...result._metadata.execution,
16188
+ receiptKey: request.receiptKey!,
16189
+ ...(result._metadata.execution.attachedToReceiptKey
16190
+ ? { attachedToReceiptKey: request.receiptKey! }
16191
+ : {}),
16192
+ })
16193
+ : result;
16104
16194
  if (request.cacheable !== false) {
16105
- this.cacheToolResult(toolId, request.cacheKey, result);
16195
+ this.cacheToolResult(toolId, request.cacheKey, presented);
16106
16196
  }
16107
16197
  const resolver = this.toolCallResolvers.get(request.callId);
16108
16198
  if (resolver) {
16109
- resolver.resolve(result);
16199
+ resolver.resolve(presented);
16110
16200
  this.toolCallResolvers.delete(request.callId);
16111
16201
  }
16112
16202
  this.emitScopedFieldMetaUpdate({
@@ -16115,12 +16205,14 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
16115
16205
  tableNamespace: request.tableNamespace ?? null,
16116
16206
  fieldName: request.fieldName,
16117
16207
  status:
16118
- result._metadata.execution.cached === true ? 'cached' : 'running',
16208
+ presented._metadata.execution.cached === true ? 'cached' : 'running',
16119
16209
  rowStatus: 'running',
16120
16210
  stage: toolId,
16121
16211
  provider: null,
16122
16212
  error: null,
16123
- ...(result._metadata.execution.cached === true ? { reused: true } : {}),
16213
+ ...(presented._metadata.execution.cached === true
16214
+ ? { reused: true }
16215
+ : {}),
16124
16216
  dataPatch: {},
16125
16217
  });
16126
16218
  successful.push(request);
@@ -16309,10 +16401,12 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
16309
16401
  execution: {
16310
16402
  mode: 'map',
16311
16403
  ownerRunId: this.currentReceiptOwnerRunId,
16312
- force: request.force === true,
16404
+ force: this.mappedToolResultReceiptPolicy(request).force,
16313
16405
  },
16314
16406
  compatibility: {
16315
- resultReceiptKey: request.receiptKey!,
16407
+ resultReceiptKey:
16408
+ this.mappedToolResultReceiptPolicy(request)
16409
+ .resultReceiptKey,
16316
16410
  providerOperationKey: request.receiptKey!,
16317
16411
  },
16318
16412
  },
@@ -16328,7 +16422,10 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
16328
16422
  compiled.batchOperation,
16329
16423
  )
16330
16424
  )?.trim() ?? '';
16331
- const receiptKeys = owned.map((request) => request.receiptKey!);
16425
+ const receiptKeys = owned.map(
16426
+ (request) =>
16427
+ this.mappedToolResultReceiptPolicy(request).resultReceiptKey,
16428
+ );
16332
16429
  const aggregateReceiptKey = buildDurableToolAggregateReceiptKey({
16333
16430
  receiptKeys,
16334
16431
  prefix: 'batch',
@@ -16501,14 +16598,17 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
16501
16598
  // before they are delivered to rows.
16502
16599
  const durable = await store.read({
16503
16600
  resultReceiptKeys: entry.request.memberRequests.map(
16504
- (request) => request.receiptKey!,
16601
+ (request) =>
16602
+ this.mappedToolResultReceiptPolicy(request).resultReceiptKey,
16505
16603
  ),
16506
16604
  });
16507
16605
  const byReceipt = new Map(
16508
16606
  durable.map((record) => [record.resultReceiptKey, record]),
16509
16607
  );
16510
16608
  for (const request of entry.request.memberRequests) {
16511
- const record = byReceipt.get(request.receiptKey!);
16609
+ const record = byReceipt.get(
16610
+ this.mappedToolResultReceiptPolicy(request).resultReceiptKey,
16611
+ );
16512
16612
  if (
16513
16613
  record &&
16514
16614
  (record.status === 'completed' ||
@@ -18557,6 +18657,52 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
18557
18657
  const executionBaseUrl =
18558
18658
  this.#options.executionGatewayBaseUrl?.trim() || this.#options.baseUrl;
18559
18659
  const toolErrorSchemaVersion = this.currentToolErrorSchemaVersion;
18660
+ const providerNoProgressScope = stableDigest(
18661
+ `${provider}\u0000${executionAuthScopeDigest ?? ''}`,
18662
+ );
18663
+ const providerNoProgressError = (retryAtMs: number) =>
18664
+ createToolHttpError(
18665
+ toolErrorSchemaVersion,
18666
+ `Provider ${provider} made no progress within ${PROVIDER_NO_PROGRESS_MAX_MS / 60_000} minutes. Completed work was saved; retry the failed work after the provider recovers.`,
18667
+ null,
18668
+ 429,
18669
+ 'repairable',
18670
+ {
18671
+ toolId,
18672
+ provider,
18673
+ operation: toolId,
18674
+ code: 'PROVIDER_EXHAUSTED',
18675
+ origin: 'provider',
18676
+ category: 'rate_limit',
18677
+ retryable: true,
18678
+ statusCode: 429,
18679
+ requestId: null,
18680
+ retryAfterMs: Math.max(0, retryAtMs - Date.now()),
18681
+ networkKind: null,
18682
+ networkScope: null,
18683
+ },
18684
+ );
18685
+ const observeProviderWait = (retryAtMs: number) => {
18686
+ const startedAtMsByScope =
18687
+ (this.checkpoint.providerNoProgressStartedAtMsByScope ??= {});
18688
+ const outcome = observeProviderNoProgress({
18689
+ startedAtMsByScope,
18690
+ scope: providerNoProgressScope,
18691
+ retryAtMs,
18692
+ nowMs: Date.now(),
18693
+ });
18694
+ if (!outcome.exhausted) return;
18695
+ this.runtimeLog(
18696
+ `[runtime.provider.no_progress] ${JSON.stringify({
18697
+ provider,
18698
+ started_at: new Date(outcome.startedAtMs).toISOString(),
18699
+ deadline_at: new Date(outcome.deadlineMs).toISOString(),
18700
+ retry_at: new Date(retryAtMs).toISOString(),
18701
+ })}`,
18702
+ { level: 'warn' },
18703
+ );
18704
+ throw providerNoProgressError(retryAtMs);
18705
+ };
18560
18706
  const timeoutMs = resolveToolRuntimeTimeoutMs(
18561
18707
  toolId,
18562
18708
  options?.timeoutMs,
@@ -18594,6 +18740,8 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
18594
18740
  const toolInputSnapshotMemory: {
18595
18741
  current: RuntimeMemoryLedgerHandle | null;
18596
18742
  } = { current: null };
18743
+ let preserveProviderRetryCursorForSuspension = false;
18744
+ let clearProviderRetryCursor = () => {};
18597
18745
  try {
18598
18746
  return await withActiveSpan(
18599
18747
  'plays.tool.execute',
@@ -18703,11 +18851,22 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
18703
18851
  `transport:${this.currentRunId}:${toolId}:${crypto.randomUUID()}`,
18704
18852
  )}`
18705
18853
  : null);
18854
+ // A durable row replay reconstructs this function from scratch. Keep
18855
+ // the gateway's invocation sequence in the checkpoint, keyed by the
18856
+ // stable result receipt identity, until this logical call completes.
18857
+ const providerRetryCursorKey =
18858
+ this.#options.durableBoundaries && durableCallReceiptKey
18859
+ ? stableDigest(`provider-retry:${durableCallReceiptKey}`)
18860
+ : null;
18861
+ const savedProviderRetryCursor = providerRetryCursorKey
18862
+ ? this.checkpoint.providerRetryCursors?.[providerRetryCursorKey]
18863
+ : undefined;
18706
18864
  const executionRequestNonce = crypto.randomUUID();
18707
- // This identifies one logical tool call to billing. Physical retry
18708
- // count remains in request metadata, so a transport retry cannot
18709
- // reserve or charge the same call twice.
18710
- const deeplineRequestId = `ctx-tool-${stableDigest(executionRequestNonce).slice(0, 32)}`;
18865
+ // Transport ambiguity must retain the billing identity. An ordinary
18866
+ // (unfenced) call advances it only after a definite provider-owned
18867
+ // 429 released that attempt's reservation. Fenced calls retain it:
18868
+ // the gateway-qualified retry owns their distinct charge lifecycle.
18869
+ let deeplineRequestId = `ctx-tool-${stableDigest(executionRequestNonce).slice(0, 32)}`;
18711
18870
  // The provider operation has a semantic identity independent of
18712
18871
  // the response receipt. Its key is supplied by the caller that owns
18713
18872
  // that compatibility contract; never substitute request/billing
@@ -18758,7 +18917,8 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
18758
18917
  // *durable* provider attempt. The execution gateway validates and
18759
18918
  // attests it before a retried call can use its fresh charge
18760
18919
  // lifecycle key; it is not a general retry counter.
18761
- let providerRateLimitRetryFromInvocationAttempt: number | null = null;
18920
+ let providerRateLimitRetryFromInvocationAttempt: number | null =
18921
+ savedProviderRetryCursor?.retryFromInvocationAttempt ?? null;
18762
18922
  const serializedRequestBody = (invocationAttempt: number) =>
18763
18923
  JSON.stringify({
18764
18924
  payload: shouldResolveAsyncOperationInRuntime
@@ -18859,8 +19019,89 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
18859
19019
  : {}),
18860
19020
  });
18861
19021
  let transportAttempt = 0;
18862
- let invocationAttempt = 0;
19022
+ let invocationAttempt =
19023
+ savedProviderRetryCursor?.invocationAttempt ?? 0;
19024
+ clearProviderRetryCursor = () => {
19025
+ if (
19026
+ !providerRetryCursorKey ||
19027
+ !this.checkpoint.providerRetryCursors
19028
+ ) {
19029
+ return;
19030
+ }
19031
+ delete this.checkpoint.providerRetryCursors[providerRetryCursorKey];
19032
+ if (
19033
+ Object.keys(this.checkpoint.providerRetryCursors).length === 0
19034
+ ) {
19035
+ delete this.checkpoint.providerRetryCursors;
19036
+ }
19037
+ };
19038
+ const persistProviderRetryCursor = (retryFrom: number) => {
19039
+ if (!providerRetryCursorKey) return;
19040
+ const cursor = {
19041
+ invocationAttempt: retryFrom + 1,
19042
+ retryFromInvocationAttempt: retryFrom,
19043
+ };
19044
+ this.checkpoint.providerRetryCursors ??= {};
19045
+ this.checkpoint.providerRetryCursors[providerRetryCursorKey] =
19046
+ cursor;
19047
+ };
18863
19048
  let physicalAttempt = 0;
19049
+ // Once this logical call receives an HTTP 429, keep a small
19050
+ // phase trace for its retry so a stalled retry can be located without
19051
+ // logging request, receipt, organization, or provider identifiers.
19052
+ let provider429RetryDiagnosticNextInvocationAttempt: number | null =
19053
+ null;
19054
+ let provider429RetryDiagnosticStartedAt: number | null = null;
19055
+ const logProvider429RetryPhase = (phase: string) => {
19056
+ if (
19057
+ provider429RetryDiagnosticNextInvocationAttempt === null ||
19058
+ provider429RetryDiagnosticStartedAt === null
19059
+ )
19060
+ return;
19061
+ this.runtimeLog(
19062
+ `[runtime.provider_429_retry_phase] ${JSON.stringify({
19063
+ next_invocation_attempt:
19064
+ provider429RetryDiagnosticNextInvocationAttempt,
19065
+ phase,
19066
+ elapsed_ms: Math.round(
19067
+ performance.now() - provider429RetryDiagnosticStartedAt,
19068
+ ),
19069
+ })}`,
19070
+ );
19071
+ };
19072
+ const executionSignal = this.#options.executionSignal;
19073
+ const toolAbortSignal =
19074
+ executionSignal && options?.abortSignal
19075
+ ? AbortSignal.any([executionSignal, options.abortSignal])
19076
+ : (executionSignal ?? options?.abortSignal);
19077
+ // A short provider hold costs less than checkpointing, parking, and
19078
+ // starting another sandbox. Once we wait locally, bound all elapsed
19079
+ // residency for this logical call, including subsequent HTTP/pacer
19080
+ // work. Repeated zero or expired holds must also yield durably.
19081
+ let residentProviderWaitStartedAt: number | null = null;
19082
+ const canWaitForProviderInSandbox = (waitMs: number) => {
19083
+ const elapsedMs =
19084
+ residentProviderWaitStartedAt === null
19085
+ ? 0
19086
+ : performance.now() - residentProviderWaitStartedAt;
19087
+ return (
19088
+ Math.max(1, waitMs) <=
19089
+ Math.max(0, TOOL_PROVIDER_RESIDENT_WAIT_BUDGET_MS - elapsedMs)
19090
+ );
19091
+ };
19092
+ const waitForToolRetry = async (
19093
+ waitMs: number,
19094
+ providerHold: boolean,
19095
+ ) => {
19096
+ await options?.parkProviderCall?.();
19097
+ if (providerHold && residentProviderWaitStartedAt === null) {
19098
+ residentProviderWaitStartedAt = performance.now();
19099
+ }
19100
+ await this.sleepWithCheckpointHeartbeat(
19101
+ waitMs,
19102
+ options?.abortSignal,
19103
+ );
19104
+ };
18864
19105
  const retryToolTransportFailure = async (input: {
18865
19106
  error: unknown;
18866
19107
  elapsedMs: number;
@@ -18946,6 +19187,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
18946
19187
  let providerTelemetryKey: string | null = null;
18947
19188
  let providerRunInFlightAtDispatch: number | null = null;
18948
19189
  const acquireProviderPermit = async (signal?: AbortSignal) => {
19190
+ logProvider429RetryPhase('provider_permit_wait_started');
18949
19191
  const permit = await this.resourceGovernor.acquireProviderPermit({
18950
19192
  toolId,
18951
19193
  signal,
@@ -18955,6 +19197,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
18955
19197
  providerPacingGrant = permit.pacingGrant ?? null;
18956
19198
  providerGatewayDispatchProof = permit.gatewayDispatchProof ?? null;
18957
19199
  providerTelemetryKey = permit.provider;
19200
+ logProvider429RetryPhase('provider_permit_acquired');
18958
19201
  return permit;
18959
19202
  };
18960
19203
 
@@ -18967,6 +19210,17 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
18967
19210
  }
18968
19211
 
18969
19212
  while (true) {
19213
+ toolAbortSignal?.throwIfAborted();
19214
+ if (
19215
+ providerNoProgressExpired({
19216
+ startedAtMsByScope:
19217
+ this.checkpoint.providerNoProgressStartedAtMsByScope,
19218
+ scope: providerNoProgressScope,
19219
+ nowMs: Date.now(),
19220
+ })
19221
+ ) {
19222
+ throw providerNoProgressError(Date.now());
19223
+ }
18970
19224
  physicalAttempt += 1;
18971
19225
  providerConcurrencyGrant = null;
18972
19226
  providerPacingGrant = null;
@@ -19012,28 +19266,28 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
19012
19266
  // not share the coordinator's TTL environment.
19013
19267
  const hasReceiptHeartbeat = Boolean(options?.heartbeatReceipt);
19014
19268
  const abortController =
19015
- timeoutMs || hasReceiptHeartbeat || options?.abortSignal
19269
+ timeoutMs || hasReceiptHeartbeat || toolAbortSignal
19016
19270
  ? new AbortController()
19017
19271
  : null;
19018
19272
  const forwardAbortSignal = () => {
19019
- if (!options?.abortSignal?.aborted) return;
19020
- abortController?.abort(options.abortSignal.reason);
19273
+ if (!toolAbortSignal?.aborted) return;
19274
+ abortController?.abort(toolAbortSignal.reason);
19021
19275
  };
19022
19276
  const receiptHeartbeat = options?.heartbeatReceipt;
19023
19277
  let heartbeatFailure: unknown = null;
19024
19278
  let timeoutHandle: ReturnType<typeof setTimeout> | null = null;
19025
19279
  try {
19026
- if (options?.abortSignal?.aborted) {
19280
+ if (toolAbortSignal?.aborted) {
19027
19281
  forwardAbortSignal();
19028
19282
  } else {
19029
- options?.abortSignal?.addEventListener(
19030
- 'abort',
19031
- forwardAbortSignal,
19032
- { once: true },
19033
- );
19283
+ toolAbortSignal?.addEventListener('abort', forwardAbortSignal, {
19284
+ once: true,
19285
+ });
19034
19286
  }
19035
19287
  const ownershipStartedAt = Date.now();
19288
+ logProvider429RetryPhase('receipt_ownership_check_started');
19036
19289
  await options?.beforeProviderCall?.();
19290
+ logProvider429RetryPhase('receipt_ownership_check_completed');
19037
19291
  if (runtimeReceiptReadTraceEnabled) {
19038
19292
  this.runtimeLog(
19039
19293
  `[perf] tool call id=${toolId} phase=before_provider_ownership elapsed_ms=${Date.now() - ownershipStartedAt}`,
@@ -19083,6 +19337,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
19083
19337
  acquireProviderPermit: async ({ signal }) =>
19084
19338
  await acquireProviderPermit(signal),
19085
19339
  tryAcquireProviderPermit: () => {
19340
+ logProvider429RetryPhase(
19341
+ 'provider_permit_acquire_attempted',
19342
+ );
19086
19343
  const permit =
19087
19344
  this.resourceGovernor.tryAcquireProviderPermit({
19088
19345
  toolId,
@@ -19093,6 +19350,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
19093
19350
  providerGatewayDispatchProof =
19094
19351
  permit.gatewayDispatchProof ?? null;
19095
19352
  providerTelemetryKey = permit.provider;
19353
+ logProvider429RetryPhase('provider_permit_acquired');
19096
19354
  return permit;
19097
19355
  },
19098
19356
  tryAcquireExecutionSlot: ({ providerResourceKey }) =>
@@ -19299,6 +19557,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
19299
19557
  providerCallStartedAt =
19300
19558
  dispatchedProviderCallStartedAt;
19301
19559
  fetchDispatched = true;
19560
+ logProvider429RetryPhase('http_dispatch_started');
19302
19561
  activeProviderRequest =
19303
19562
  this.beginProviderRequest(provider);
19304
19563
  const dispatchedResponse = await fetch(url, {
@@ -19356,6 +19615,13 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
19356
19615
  // it has been read and verified below.
19357
19616
  response = dispatchedResponse;
19358
19617
  if (dispatchedResponse.status === 429) {
19618
+ provider429RetryDiagnosticStartedAt =
19619
+ performance.now();
19620
+ provider429RetryDiagnosticNextInvocationAttempt =
19621
+ invocationAttempt + 1;
19622
+ logProvider429RetryPhase(
19623
+ 'http_429_response_received',
19624
+ );
19359
19625
  providerRequestsInFlightAt429 =
19360
19626
  activeProviderRequest?.inFlightCount() ?? null;
19361
19627
  const observedAt = Date.now();
@@ -19365,8 +19631,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
19365
19631
  // the conservative bare-429 cooldown. Do not replace
19366
19632
  // it with the generic one-second HTTP retry fallback.
19367
19633
  providerRetryAtMs = retryAfter?.trim()
19368
- ? observedAt +
19369
- parseToolExecuteRetryAfterMs(
19634
+ ? parseToolExecuteRetryAtMs(
19370
19635
  retryAfter,
19371
19636
  observedAt,
19372
19637
  )
@@ -19495,16 +19760,34 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
19495
19760
  }
19496
19761
  if (dispatchedResponse.status === 429) {
19497
19762
  const bodyText = dispatchedResponseErrorText ?? '';
19498
- const attempt = httpFailureAttempts.next({
19763
+ let attempt = httpFailureAttempts.next({
19499
19764
  toolId,
19500
19765
  status: dispatchedResponse.status,
19501
19766
  bodyText,
19502
19767
  transientHttpRetrySafe:
19503
19768
  retrySafeTransientHttp || fixtureReplaySafe,
19504
19769
  });
19505
- preclassifiedHttpFailure.current = {
19770
+ let failure = classifyToolExecuteHttpFailure({
19771
+ toolId,
19772
+ status: dispatchedResponse.status,
19506
19773
  attempt,
19507
- failure: classifyToolExecuteHttpFailure({
19774
+ bodyText,
19775
+ schemaVersion: toolErrorSchemaVersion,
19776
+ retryAfterHeader:
19777
+ dispatchedResponse.headers.get('retry-after'),
19778
+ transientHttpRetrySafe:
19779
+ retrySafeTransientHttp || fixtureReplaySafe,
19780
+ providerLatencyMs:
19781
+ Date.now() - dispatchedProviderCallStartedAt,
19782
+ });
19783
+ if (failure.backpressureDelayMs !== null) {
19784
+ // The durable invocation attempt survives a map
19785
+ // suspension; use its absolute 1-based count for
19786
+ // the provider retry budget instead of the
19787
+ // process-local HTTP tracker, which resets when
19788
+ // a new runner resumes this row.
19789
+ attempt = invocationAttempt + 1;
19790
+ failure = classifyToolExecuteHttpFailure({
19508
19791
  toolId,
19509
19792
  status: dispatchedResponse.status,
19510
19793
  attempt,
@@ -19516,7 +19799,11 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
19516
19799
  retrySafeTransientHttp || fixtureReplaySafe,
19517
19800
  providerLatencyMs:
19518
19801
  Date.now() - dispatchedProviderCallStartedAt,
19519
- }),
19802
+ });
19803
+ }
19804
+ preclassifiedHttpFailure.current = {
19805
+ attempt,
19806
+ failure,
19520
19807
  };
19521
19808
  }
19522
19809
  activeProviderRequest?.release();
@@ -19649,14 +19936,21 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
19649
19936
  }
19650
19937
  if (error instanceof ProviderExhaustedError) {
19651
19938
  const retryAtMs = Date.parse(error.retryAt);
19652
- if (!this.#options.durableBoundaries) {
19653
- await this.sleepWithCheckpointHeartbeat(
19654
- Math.max(
19655
- 1_000,
19656
- Number.isFinite(retryAtMs)
19657
- ? Math.ceil(retryAtMs - Date.now())
19658
- : 1_000,
19659
- ),
19939
+ observeProviderWait(
19940
+ Number.isFinite(retryAtMs) ? retryAtMs : Date.now() + 1_000,
19941
+ );
19942
+ const waitMs = Number.isFinite(retryAtMs)
19943
+ ? Math.max(0, Math.ceil(retryAtMs - Date.now()))
19944
+ : 1_000;
19945
+ if (
19946
+ !this.#options.durableBoundaries ||
19947
+ canWaitForProviderInSandbox(waitMs)
19948
+ ) {
19949
+ await waitForToolRetry(
19950
+ this.#options.durableBoundaries
19951
+ ? waitMs
19952
+ : Math.max(1_000, waitMs),
19953
+ true,
19660
19954
  );
19661
19955
  continue;
19662
19956
  }
@@ -19812,10 +20106,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
19812
20106
  if (timeoutHandle) {
19813
20107
  clearTimeout(timeoutHandle);
19814
20108
  }
19815
- options?.abortSignal?.removeEventListener(
19816
- 'abort',
19817
- forwardAbortSignal,
19818
- );
20109
+ toolAbortSignal?.removeEventListener('abort', forwardAbortSignal);
19819
20110
  }
19820
20111
 
19821
20112
  if (!response) {
@@ -19950,26 +20241,47 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
19950
20241
  );
19951
20242
  providerBackpressureReported = true;
19952
20243
  }
19953
- // The normal 429 path reports its hold before releasing the
19954
- // provider lease. A mapped row must now yield even for a short
19955
- // hold: sleeping here would occupy its row slot and prevent
19956
- // unrelated rows from making progress.
20244
+ // Provider permits are already released. Brief holds can use
20245
+ // the existing retry loop; longer or accumulated holds yield
20246
+ // the row slot and resume from durable completed receipts.
19957
20247
  const durableProviderWaitMs =
19958
20248
  failure.providerPacingWaitMs ?? failure.backpressureDelayMs;
20249
+ const providerHoldRetryAtMs =
20250
+ durableProviderWaitMs === null
20251
+ ? null
20252
+ : ((failure.providerPacingWaitMs !== null
20253
+ ? Date.now() + failure.providerPacingWaitMs
20254
+ : providerRetryAtMs) ??
20255
+ Date.now() + durableProviderWaitMs);
20256
+ const remainingProviderWaitMs =
20257
+ providerHoldRetryAtMs === null
20258
+ ? null
20259
+ : Math.max(0, Math.ceil(providerHoldRetryAtMs - Date.now()));
19959
20260
  if (
20261
+ failure.backpressureDelayMs !== null ||
20262
+ failure.providerPacingWaitMs !== null
20263
+ ) {
20264
+ observeProviderWait(
20265
+ providerHoldRetryAtMs ?? Date.now() + 1_000,
20266
+ );
20267
+ }
20268
+ if (
20269
+ failure.shouldRetry &&
19960
20270
  this.#options.durableBoundaries &&
19961
- durableProviderWaitMs !== null &&
19962
- durableProviderWaitMs > 0
20271
+ remainingProviderWaitMs !== null &&
20272
+ !canWaitForProviderInSandbox(remainingProviderWaitMs)
19963
20273
  ) {
19964
- const retryAtMs =
19965
- (failure.providerPacingWaitMs !== null
19966
- ? Date.now() + failure.providerPacingWaitMs
19967
- : providerRetryAtMs) ?? Date.now() + durableProviderWaitMs;
20274
+ if (failure.backpressureDelayMs !== null) {
20275
+ persistProviderRetryCursor(invocationAttempt);
20276
+ }
20277
+ const retryAtMs = providerHoldRetryAtMs!;
19968
20278
  const rowScope = rowContext.getStore();
19969
20279
  if (
19970
20280
  this.#options.durableBoundaries &&
19971
20281
  (options?.providerBackpressureMapRows || rowScope)
19972
20282
  ) {
20283
+ preserveProviderRetryCursorForSuspension =
20284
+ failure.backpressureDelayMs !== null;
19973
20285
  throw new PlayRowProviderBackpressureError(
19974
20286
  provider,
19975
20287
  retryAtMs,
@@ -20018,6 +20330,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
20018
20330
  !shedKnownNotDispatched
20019
20331
  ) {
20020
20332
  const previousInvocationAttempt = invocationAttempt;
20333
+ if (failure.backpressureDelayMs !== null) {
20334
+ persistProviderRetryCursor(previousInvocationAttempt);
20335
+ }
20021
20336
  invocationAttempt += 1;
20022
20337
  providerRateLimitRetryFromInvocationAttempt =
20023
20338
  failure.backpressureDelayMs !== null
@@ -20027,6 +20342,8 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
20027
20342
  if (failure.chargeRetryBudget) {
20028
20343
  await this.governor.chargeBudget('retry');
20029
20344
  }
20345
+ const retryDelayMs =
20346
+ remainingProviderWaitMs ?? failure.retryDelayMs;
20030
20347
  const retryAttributePrefix =
20031
20348
  failure.reason === 'provider_pacing_wait'
20032
20349
  ? 'provider_pacing_wait'
@@ -20035,14 +20352,14 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
20035
20352
  : 'transient_http';
20036
20353
  span.setAttribute(
20037
20354
  `plays.${retryAttributePrefix}_retry_after_ms`,
20038
- failure.retryDelayMs,
20355
+ retryDelayMs,
20039
20356
  );
20040
20357
  span.setAttribute(
20041
20358
  `plays.${retryAttributePrefix}_attempt`,
20042
20359
  httpFailureAttempt,
20043
20360
  );
20044
20361
  this.runtimeLog(
20045
- `Tool ${toolId} returned ${response.status} (${failure.reason}); retrying after ${failure.retryDelayMs}ms ` +
20362
+ `Tool ${toolId} returned ${response.status} (${failure.reason}); retrying after ${retryDelayMs}ms ` +
20046
20363
  `(request_id=${deeplineRequestId ?? 'none'} gateway=${transportGatewayOriginForDiagnostic(url)})`,
20047
20364
  { level: 'warn' },
20048
20365
  );
@@ -20064,16 +20381,26 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
20064
20381
  ? 'rate_limit'
20065
20382
  : 'provider_error',
20066
20383
  retryAt:
20067
- failure.providerPacingWaitMs !== null
20068
- ? Date.now() + failure.providerPacingWaitMs
20069
- : Date.now() + failure.retryDelayMs,
20384
+ providerHoldRetryAtMs ?? Date.now() + retryDelayMs,
20070
20385
  attempt: httpFailureAttempt,
20071
20386
  },
20072
20387
  observedAt: Date.now(),
20073
20388
  },
20074
20389
  });
20075
- await options?.parkProviderCall?.();
20076
- await this.sleepWithCheckpointHeartbeat(failure.retryDelayMs);
20390
+ await waitForToolRetry(
20391
+ retryDelayMs,
20392
+ remainingProviderWaitMs !== null,
20393
+ );
20394
+ logProvider429RetryPhase('retry_sleep_completed');
20395
+ if (
20396
+ !usesDurableInvocationFence &&
20397
+ failure.backpressureDelayMs !== null
20398
+ ) {
20399
+ // A classified provider rejection has no completed value to
20400
+ // charge. The next physical attempt needs a new reservation;
20401
+ // durable result and provider-operation keys stay unchanged.
20402
+ deeplineRequestId = `ctx-tool-${stableDigest(`${executionRequestNonce}:provider-rate-limit-retry:${invocationAttempt}`).slice(0, 32)}`;
20403
+ }
20077
20404
  this.emitExecutionEvent({
20078
20405
  type: 'activity.observed',
20079
20406
  observation: {
@@ -20141,13 +20468,30 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
20141
20468
  : typeof resolved.result,
20142
20469
  });
20143
20470
  toolCallSucceeded = true;
20471
+ if (this.checkpoint.providerNoProgressStartedAtMsByScope) {
20472
+ delete this.checkpoint.providerNoProgressStartedAtMsByScope[
20473
+ providerNoProgressScope
20474
+ ];
20475
+ }
20144
20476
  return receiptOutputRef
20145
20477
  ? { ...resolved, receiptOutputRef }
20146
20478
  : resolved;
20147
20479
  }
20148
20480
  },
20149
20481
  );
20482
+ } catch (error) {
20483
+ // The checkpoint becomes durable with the scheduler's suspension handoff.
20484
+ // Keep cursors only on that path; terminal failures and cancellation must
20485
+ // not leave retry state behind for an unrelated future execution.
20486
+ preserveProviderRetryCursorForSuspension =
20487
+ preserveProviderRetryCursorForSuspension ||
20488
+ error instanceof PlayRowProviderBackpressureError ||
20489
+ isPlayExecutionSuspendedError(error);
20490
+ throw error;
20150
20491
  } finally {
20492
+ if (!preserveProviderRetryCursorForSuspension) {
20493
+ clearProviderRetryCursor();
20494
+ }
20151
20495
  toolInputSnapshotMemory.current?.release();
20152
20496
  if (retryActivityEmitted) {
20153
20497
  this.emitExecutionEvent({
@@ -20207,21 +20551,41 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
20207
20551
  );
20208
20552
  }
20209
20553
 
20210
- private async sleepWithCheckpointHeartbeat(ms: number): Promise<void> {
20554
+ private async sleepWithCheckpointHeartbeat(
20555
+ ms: number,
20556
+ callerSignal?: AbortSignal,
20557
+ ): Promise<void> {
20211
20558
  const waitMs = Math.max(1, Math.ceil(ms));
20212
- const startedAt = Date.now();
20213
-
20214
- while (Date.now() - startedAt < waitMs) {
20559
+ const startedAt = performance.now();
20560
+ const executionSignal = this.#options.executionSignal;
20561
+ const signal =
20562
+ executionSignal && callerSignal
20563
+ ? AbortSignal.any([executionSignal, callerSignal])
20564
+ : (executionSignal ?? callerSignal);
20565
+
20566
+ signal?.throwIfAborted();
20567
+ while (performance.now() - startedAt < waitMs) {
20215
20568
  this.#options.onBatchComplete?.(this.checkpoint);
20216
- const remainingMs = waitMs - (Date.now() - startedAt);
20217
- await new Promise((resolve) =>
20218
- setTimeout(
20219
- resolve,
20569
+ signal?.throwIfAborted();
20570
+ const remainingMs = waitMs - (performance.now() - startedAt);
20571
+ await new Promise<void>((resolve, reject) => {
20572
+ const onAbort = () => {
20573
+ clearTimeout(timer);
20574
+ signal?.removeEventListener('abort', onAbort);
20575
+ reject(signal?.reason ?? new DOMException('Aborted', 'AbortError'));
20576
+ };
20577
+ const timer = setTimeout(
20578
+ () => {
20579
+ signal?.removeEventListener('abort', onAbort);
20580
+ resolve();
20581
+ },
20220
20582
  Math.min(TOOL_RETRY_HEARTBEAT_INTERVAL_MS, Math.max(1, remainingMs)),
20221
- ),
20222
- );
20583
+ );
20584
+ signal?.addEventListener('abort', onAbort, { once: true });
20585
+ });
20223
20586
  }
20224
20587
 
20588
+ signal?.throwIfAborted();
20225
20589
  this.#options.onBatchComplete?.(this.checkpoint);
20226
20590
  }
20227
20591
  }