deepline 0.2.9 → 0.2.11

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.
@@ -1331,6 +1331,8 @@ export class PlayContextImpl {
1331
1331
  #options: ContextOptions;
1332
1332
  private readonly executionScope: RunExecutionScope;
1333
1333
  private logBuffer: string[] = [];
1334
+ private fixtureProviderPacingBypassLogged = false;
1335
+ private fixtureProviderPacingEnforcementLogged = false;
1334
1336
  private checkpoint: PlayCheckpoint;
1335
1337
  /**
1336
1338
  * Durable tool receipts are the replay/cache authority for the execution
@@ -4329,6 +4331,18 @@ export class PlayContextImpl {
4329
4331
  // streaming and sheet-backed.
4330
4332
  const materializedResultRowLimit = resolveMaterializeLimitCap();
4331
4333
  const immediateMaterializedRows: Record<string, unknown>[] = [];
4334
+ let immediateMaterializedResidentBytes = 0;
4335
+ let immediateMaterializedCacheEnabled = true;
4336
+
4337
+ const disableImmediateMaterializedCache = (reason: string) => {
4338
+ if (!immediateMaterializedCacheEnabled) return;
4339
+ immediateMaterializedRows.length = 0;
4340
+ immediateMaterializedResidentBytes = 0;
4341
+ immediateMaterializedCacheEnabled = false;
4342
+ this.log(
4343
+ `Dataset ${normalizedMapNamespace} result cache disabled (${reason}); using the durable runtime sheet.`,
4344
+ );
4345
+ };
4332
4346
 
4333
4347
  const persistMapRows = async (rows: PersistableMapRow[]) => {
4334
4348
  if (!this.#options.onMapRowsCompleted || rows.length === 0) {
@@ -4630,8 +4644,47 @@ export class PlayContextImpl {
4630
4644
  for (const row of mapResult.completedRows) {
4631
4645
  const materializedRow = this.toMaterializedOutputRow(row.data);
4632
4646
  if (previewRows.length < 5) previewRows.push(materializedRow);
4633
- if (immediateMaterializedRows.length < materializedResultRowLimit) {
4634
- immediateMaterializedRows.push(materializedRow);
4647
+ if (
4648
+ immediateMaterializedCacheEnabled &&
4649
+ immediateMaterializedRows.length < materializedResultRowLimit
4650
+ ) {
4651
+ const estimatedRowResidentBytes =
4652
+ runtimeMapJsonByteLength(materializedRow) *
4653
+ NODE_RUNTIME_MAP_MATERIALIZED_MEMORY_MULTIPLIER +
4654
+ NODE_RUNTIME_MAP_ROW_OVERHEAD_BYTES;
4655
+ const observedRowCount = immediateMaterializedRows.length + 1;
4656
+ const observedResidentBytes =
4657
+ immediateMaterializedResidentBytes + estimatedRowResidentBytes;
4658
+ // `net_new` deliberately reports only admitted rows, so its
4659
+ // running total cannot predict the next page. Do not pre-scan an
4660
+ // unknown-count dataset just to estimate this cache. Instead,
4661
+ // bound the cache and active source page together while that page
4662
+ // remains resident.
4663
+ const projectedResidentBytes =
4664
+ options?.mode === 'net_new'
4665
+ ? observedResidentBytes +
4666
+ pageMemoryEstimate.estimatedResidentBytes
4667
+ : Math.ceil(
4668
+ (observedResidentBytes / observedRowCount) *
4669
+ Math.max(observedRowCount, totalInputCount - failedCount),
4670
+ );
4671
+ if (
4672
+ projectedResidentBytes > mapMemoryLimits.materializedBudgetBytes
4673
+ ) {
4674
+ // All-or-nothing cache: retaining a prefix that cannot fit once
4675
+ // the known input finishes gives authored code no complete fast
4676
+ // path. More importantly, that prefix would overlap the next
4677
+ // active page and can exhaust a fixed-size sandbox before the
4678
+ // cache reaches its own independent budget. Project from the
4679
+ // rows observed so far and move to the already-durable sheet
4680
+ // early instead of waiting for resident memory to reach a cliff.
4681
+ disableImmediateMaterializedCache(
4682
+ `projected ${projectedResidentBytes} bytes exceeds ${mapMemoryLimits.materializedBudgetBytes}-byte budget`,
4683
+ );
4684
+ } else {
4685
+ immediateMaterializedRows.push(materializedRow);
4686
+ immediateMaterializedResidentBytes = observedResidentBytes;
4687
+ }
4635
4688
  }
4636
4689
  }
4637
4690
  executedCount += rowsToExecute.length;
@@ -4738,6 +4791,7 @@ export class PlayContextImpl {
4738
4791
  materializedResultRowLimit,
4739
4792
  );
4740
4793
  if (
4794
+ immediateMaterializedCacheEnabled &&
4741
4795
  immediateMaterializedRows.length === availableResultRows &&
4742
4796
  (limit !== undefined || successfulCount <= materializedResultRowLimit)
4743
4797
  ) {
@@ -4808,6 +4862,7 @@ export class PlayContextImpl {
4808
4862
  },
4809
4863
  previewRows,
4810
4864
  residentRows:
4865
+ immediateMaterializedCacheEnabled &&
4811
4866
  immediateMaterializedRows.length === successfulCount
4812
4867
  ? immediateMaterializedRows
4813
4868
  : null,
@@ -4828,8 +4883,9 @@ export class PlayContextImpl {
4828
4883
  peek: async (limit) =>
4829
4884
  limit <= 0
4830
4885
  ? []
4831
- : immediateMaterializedRows.length >=
4832
- Math.min(limit, successfulCount)
4886
+ : immediateMaterializedCacheEnabled &&
4887
+ immediateMaterializedRows.length >=
4888
+ Math.min(limit, successfulCount)
4833
4889
  ? immediateMaterializedRows.slice(0, limit)
4834
4890
  : await readRuntimeBackedMapRows({
4835
4891
  limit,
@@ -4840,7 +4896,10 @@ export class PlayContextImpl {
4840
4896
  iterate: () =>
4841
4897
  ({
4842
4898
  async *[Symbol.asyncIterator]() {
4843
- if (immediateMaterializedRows.length === successfulCount) {
4899
+ if (
4900
+ immediateMaterializedCacheEnabled &&
4901
+ immediateMaterializedRows.length === successfulCount
4902
+ ) {
4844
4903
  for (const row of immediateMaterializedRows) yield row;
4845
4904
  return;
4846
4905
  }
@@ -7945,6 +8004,113 @@ export class PlayContextImpl {
7945
8004
 
7946
8005
  // ——— Batched tool call execution ———
7947
8006
 
8007
+ private startPendingToolReceiptHeartbeat(requests: ToolCallRequest[]): {
8008
+ stop: () => void;
8009
+ leaseLost: () => unknown | null;
8010
+ } | null {
8011
+ const heartbeatReceipts = this.#options.heartbeatRuntimeStepReceipts;
8012
+ const ownedRequests = requests.filter(
8013
+ (request) => request.receiptKey && request.receiptLeaseId,
8014
+ );
8015
+ if (!heartbeatReceipts || ownedRequests.length === 0) return null;
8016
+
8017
+ const earliestLeaseExpiry = ownedRequests.reduce<string | null>(
8018
+ (earliest, request) => {
8019
+ const expiresAt = request.receiptLeaseExpiresAt ?? null;
8020
+ if (!expiresAt) return earliest;
8021
+ if (!earliest) return expiresAt;
8022
+ return Date.parse(expiresAt) < Date.parse(earliest)
8023
+ ? expiresAt
8024
+ : earliest;
8025
+ },
8026
+ null,
8027
+ );
8028
+ let leaseLost: unknown | null = null;
8029
+ const supervisor = createRuntimeReceiptHeartbeatSupervisor({
8030
+ intervalMs: runtimeLeaseHeartbeatIntervalFromExpiry({
8031
+ leaseExpiresAt: earliestLeaseExpiry,
8032
+ fallbackTtlMs: PLAY_RUNTIME_WORK_RECEIPT_LEASE_TTL_MS,
8033
+ }),
8034
+ heartbeat: async () => {
8035
+ // Receipt claims are acquired in bulk before the provider-shaped
8036
+ // dispatcher admits every call. Renew the undispatched tail as well as
8037
+ // in-flight calls; otherwise a slow provider can let later claims
8038
+ // expire while they are still waiting behind the concurrency gate.
8039
+ const unsettled = ownedRequests.filter((request) =>
8040
+ this.toolCallResolvers.has(request.callId),
8041
+ );
8042
+ if (unsettled.length === 0) return 'terminal';
8043
+ const byLeaseId = new Map<string, Map<string, ToolCallRequest[]>>();
8044
+ for (const request of unsettled) {
8045
+ const leaseId = request.receiptLeaseId!;
8046
+ const byReceiptKey = byLeaseId.get(leaseId) ?? new Map();
8047
+ const receiptKey = request.receiptKey!;
8048
+ const group = byReceiptKey.get(receiptKey) ?? [];
8049
+ group.push(request);
8050
+ byReceiptKey.set(receiptKey, group);
8051
+ byLeaseId.set(leaseId, byReceiptKey);
8052
+ }
8053
+ await Promise.all(
8054
+ [...byLeaseId].map(async ([leaseId, byReceiptKey]) => {
8055
+ // One content-addressed receipt can have many same-run followers.
8056
+ // The store renews unique receipt rows, not request positions, so
8057
+ // duplicate keys in one heartbeat would yield null duplicate
8058
+ // positions and falsely stop the supervisor as if ownership were
8059
+ // lost.
8060
+ const entries = [...byReceiptKey];
8061
+ const keys = entries.map(([receiptKey]) => receiptKey);
8062
+ const receipts = await heartbeatReceipts({
8063
+ runId: this.currentReceiptOwnerRunId,
8064
+ runAttempt: this.currentRunAttempt,
8065
+ leaseId,
8066
+ keys,
8067
+ });
8068
+ for (let index = 0; index < entries.length; index += 1) {
8069
+ const [receiptKey, requests] = entries[index]!;
8070
+ // Completion can race this bulk heartbeat response. A settled
8071
+ // receipt group no longer needs ownership and must not turn that
8072
+ // race into a false lease-loss failure.
8073
+ if (
8074
+ !requests.some((request) =>
8075
+ this.toolCallResolvers.has(request.callId),
8076
+ )
8077
+ ) {
8078
+ continue;
8079
+ }
8080
+ if (
8081
+ !this.runtimeToolReceiptStillOwned(
8082
+ receipts[index] ?? null,
8083
+ leaseId,
8084
+ )
8085
+ ) {
8086
+ throw new RuntimeReceiptLeaseLostError({
8087
+ receiptKey,
8088
+ runId: this.currentReceiptOwnerRunId,
8089
+ leaseId,
8090
+ });
8091
+ }
8092
+ }
8093
+ }),
8094
+ );
8095
+ return 'active';
8096
+ },
8097
+ isLeaseLost: (error) => error instanceof RuntimeReceiptLeaseLostError,
8098
+ onLeaseLost: (error) => {
8099
+ leaseLost ??= error;
8100
+ },
8101
+ onTransientFailure: (error) => {
8102
+ this.log(
8103
+ `Pending tool receipt heartbeat transport failed; retrying: ${error instanceof Error ? error.message : String(error)}`,
8104
+ );
8105
+ },
8106
+ });
8107
+ supervisor.start();
8108
+ return {
8109
+ stop: () => supervisor.stop(),
8110
+ leaseLost: () => leaseLost,
8111
+ };
8112
+ }
8113
+
7948
8114
  private async executeBatchedToolCalls(
7949
8115
  queuedToolCalls: ToolCallRequest[],
7950
8116
  ): Promise<void> {
@@ -8615,277 +8781,290 @@ export class PlayContextImpl {
8615
8781
  recordToolStep(durableRecoveredRequests);
8616
8782
  }
8617
8783
 
8618
- if (pendingRequests.length > 0) {
8619
- const strategy =
8620
- this.#options.getBatchOperationStrategy?.(toolId) ?? null;
8621
-
8622
- if (strategy) {
8623
- const compiledBatches = compileRequestsWithStrategy({
8624
- requests: pendingRequests,
8625
- strategy,
8626
- getPayload: (request: ToolCallRequest) => request.input,
8627
- });
8784
+ const pendingReceiptHeartbeat =
8785
+ this.startPendingToolReceiptHeartbeat(pendingRequests);
8786
+ try {
8787
+ if (pendingRequests.length > 0) {
8788
+ const strategy =
8789
+ this.#options.getBatchOperationStrategy?.(toolId) ?? null;
8790
+
8791
+ if (strategy) {
8792
+ const compiledBatches = compileRequestsWithStrategy({
8793
+ requests: pendingRequests,
8794
+ strategy,
8795
+ getPayload: (request: ToolCallRequest) => request.input,
8796
+ });
8628
8797
 
8629
- const batchSize =
8630
- compiledBatches.length > 0
8631
- ? await this.resourceGovernor.suggestedToolParallelism(
8632
- compiledBatches[0]!.batchOperation,
8633
- this.governor.policy.pacing
8634
- .workerToolBatchDefaultParallelism,
8635
- )
8636
- : this.governor.policy.pacing.workerToolBatchDefaultParallelism;
8637
- await executeChunkedRequests({
8638
- requests: compiledBatches,
8639
- batchSize,
8640
- execute: async (batch) => {
8641
- // Circuit breaker: skip dispatching this batch's provider call
8642
- // once a persistence failure has occurred in this run.
8643
- if (this.persistenceLatch.tripped) {
8644
- this.persistenceLatch.preventedCallCount +=
8645
- batch.memberRequests.length;
8646
- throw new RuntimePersistenceCircuitOpenError(
8647
- this.persistenceLatch,
8798
+ const batchSize =
8799
+ compiledBatches.length > 0
8800
+ ? await this.resourceGovernor.suggestedToolParallelism(
8801
+ compiledBatches[0]!.batchOperation,
8802
+ this.governor.policy.pacing
8803
+ .workerToolBatchDefaultParallelism,
8804
+ )
8805
+ : this.governor.policy.pacing
8806
+ .workerToolBatchDefaultParallelism;
8807
+ await executeChunkedRequests({
8808
+ requests: compiledBatches,
8809
+ batchSize,
8810
+ execute: async (batch) => {
8811
+ // Circuit breaker: skip dispatching this batch's provider call
8812
+ // once a persistence failure has occurred in this run.
8813
+ if (this.persistenceLatch.tripped) {
8814
+ this.persistenceLatch.preventedCallCount +=
8815
+ batch.memberRequests.length;
8816
+ throw new RuntimePersistenceCircuitOpenError(
8817
+ this.persistenceLatch,
8818
+ );
8819
+ }
8820
+ const receiptKeys = batch.memberRequests.map(
8821
+ (request) => request.cacheKey,
8648
8822
  );
8649
- }
8650
- const receiptKeys = batch.memberRequests.map(
8651
- (request) => request.cacheKey,
8652
- );
8653
- const aggregateReceiptKey = buildDurableToolAggregateReceiptKey(
8654
- {
8655
- receiptKeys,
8656
- prefix: 'batch',
8657
- aggregateReceiptPrefix: buildDurableToolReceiptPrefix({
8658
- orgId: this.#options.orgId,
8659
- toolId: batch.batchOperation,
8660
- }),
8661
- },
8662
- );
8663
- return await this.callToolAPI(
8664
- batch.batchOperation,
8665
- batch.batchPayload,
8666
- {
8667
- durableCallReceiptKey: aggregateReceiptKey,
8668
- executionAuthScopeDigest:
8669
- batch.memberRequests[0]?.executionAuthScopeDigest ?? null,
8670
- providerIdempotencyKey:
8671
- buildDurableToolAggregateProviderIdempotencyKey({
8672
- aggregateReceiptKey,
8673
- receiptKeys,
8674
- providerIdempotencyKeys: batch.memberRequests.map(
8675
- (request) =>
8676
- this.providerIdempotencyKeyForToolCall({
8677
- cacheKey: request.cacheKey,
8678
- force: request.force === true,
8679
- leaseId: request.receiptLeaseId,
8680
- }),
8681
- ),
8823
+ const aggregateReceiptKey =
8824
+ buildDurableToolAggregateReceiptKey({
8825
+ receiptKeys,
8826
+ prefix: 'batch',
8827
+ aggregateReceiptPrefix: buildDurableToolReceiptPrefix({
8828
+ orgId: this.#options.orgId,
8829
+ toolId: batch.batchOperation,
8682
8830
  }),
8683
- receiptLeaseExpiresAt: batch.memberRequests.reduce<
8684
- string | null
8685
- >((earliest, request) => {
8686
- const expiresAt = request.receiptLeaseExpiresAt ?? null;
8687
- if (!expiresAt) return earliest;
8688
- if (!earliest) return expiresAt;
8689
- return Date.parse(expiresAt) < Date.parse(earliest)
8690
- ? expiresAt
8691
- : earliest;
8692
- }, null),
8693
- timeoutMs: resolveRuntimeTimeoutMsForClaimedOwners(
8694
- batch.memberRequests,
8695
- ),
8696
- beforeProviderCall: () =>
8697
- this.assertRuntimeToolReceiptOwnership(
8698
- batch.memberRequests,
8699
- ),
8700
- heartbeatReceipt: () =>
8701
- this.renewRuntimeToolReceiptOwnership(
8831
+ });
8832
+ return await this.callToolAPI(
8833
+ batch.batchOperation,
8834
+ batch.batchPayload,
8835
+ {
8836
+ durableCallReceiptKey: aggregateReceiptKey,
8837
+ executionAuthScopeDigest:
8838
+ batch.memberRequests[0]?.executionAuthScopeDigest ??
8839
+ null,
8840
+ providerIdempotencyKey:
8841
+ buildDurableToolAggregateProviderIdempotencyKey({
8842
+ aggregateReceiptKey,
8843
+ receiptKeys,
8844
+ providerIdempotencyKeys: batch.memberRequests.map(
8845
+ (request) =>
8846
+ this.providerIdempotencyKeyForToolCall({
8847
+ cacheKey: request.cacheKey,
8848
+ force: request.force === true,
8849
+ leaseId: request.receiptLeaseId,
8850
+ }),
8851
+ ),
8852
+ }),
8853
+ receiptLeaseExpiresAt: batch.memberRequests.reduce<
8854
+ string | null
8855
+ >((earliest, request) => {
8856
+ const expiresAt = request.receiptLeaseExpiresAt ?? null;
8857
+ if (!expiresAt) return earliest;
8858
+ if (!earliest) return expiresAt;
8859
+ return Date.parse(expiresAt) < Date.parse(earliest)
8860
+ ? expiresAt
8861
+ : earliest;
8862
+ }, null),
8863
+ timeoutMs: resolveRuntimeTimeoutMsForClaimedOwners(
8702
8864
  batch.memberRequests,
8703
8865
  ),
8704
- },
8705
- );
8706
- },
8707
- onChunkComplete: async (chunkResults) => {
8708
- for (const entry of chunkResults) {
8709
- if (entry.error !== undefined) {
8710
- for (const request of entry.request.memberRequests) {
8711
- await rejectWithLiveFollowers(request, entry.error);
8866
+ beforeProviderCall: () =>
8867
+ this.assertRuntimeToolReceiptOwnership(
8868
+ batch.memberRequests,
8869
+ ),
8870
+ heartbeatReceipt: () =>
8871
+ this.renewRuntimeToolReceiptOwnership(
8872
+ batch.memberRequests,
8873
+ ),
8874
+ },
8875
+ );
8876
+ },
8877
+ onChunkComplete: async (chunkResults) => {
8878
+ for (const entry of chunkResults) {
8879
+ if (entry.error !== undefined) {
8880
+ for (const request of entry.request.memberRequests) {
8881
+ await rejectWithLiveFollowers(request, entry.error);
8882
+ }
8883
+ continue;
8884
+ }
8885
+ const splitResults =
8886
+ entry.result != null
8887
+ ? entry.request.splitResults(entry.result)
8888
+ : entry.request.memberRequests.map(() => null);
8889
+ const resolvedResults =
8890
+ await this.resolveToolCallBatchResults(
8891
+ toolId,
8892
+ entry.request.memberRequests.map((request, index) => ({
8893
+ request,
8894
+ result: splitResults[index] ?? null,
8895
+ })),
8896
+ );
8897
+
8898
+ for (
8899
+ let index = 0;
8900
+ index < entry.request.memberRequests.length;
8901
+ index += 1
8902
+ ) {
8903
+ const request = entry.request.memberRequests[index]!;
8904
+ if (resolvedResults[index] != null) {
8905
+ successfulLiveStepCallIds.add(request.callId);
8906
+ }
8907
+ resolveLiveFollowers(request, resolvedResults[index]);
8712
8908
  }
8713
- continue;
8714
8909
  }
8715
- const splitResults =
8716
- entry.result != null
8717
- ? entry.request.splitResults(entry.result)
8718
- : entry.request.memberRequests.map(() => null);
8910
+
8911
+ recordToolStep(
8912
+ chunkResults.flatMap(
8913
+ (entry) => entry.request.memberRequests,
8914
+ ),
8915
+ );
8916
+ this.#options.onBatchComplete?.(this.checkpoint);
8917
+ },
8918
+ });
8919
+ } else {
8920
+ const completionBuffer: Array<{
8921
+ request: ToolCallRequest;
8922
+ result: unknown | null;
8923
+ metadata?: ToolResultMetadataInput | null;
8924
+ jobId?: string;
8925
+ meta?: Record<string, unknown>;
8926
+ resolve: (value: unknown) => void;
8927
+ reject: (error: unknown) => void;
8928
+ }> = [];
8929
+ const completionFailedCallIds = new Set<string>();
8930
+ let completionFlushScheduled = false;
8931
+ const flushCompletionBuffer = async (): Promise<void> => {
8932
+ const entries = completionBuffer.splice(0);
8933
+ if (entries.length === 0) return;
8934
+ try {
8719
8935
  const resolvedResults =
8720
8936
  await this.resolveToolCallBatchResults(
8721
8937
  toolId,
8722
- entry.request.memberRequests.map((request, index) => ({
8723
- request,
8724
- result: splitResults[index] ?? null,
8938
+ entries.map((entry) => ({
8939
+ request: entry.request,
8940
+ result: entry.result,
8941
+ metadata: entry.metadata,
8942
+ jobId: entry.jobId,
8943
+ meta: entry.meta,
8725
8944
  })),
8726
8945
  );
8727
-
8728
- for (
8729
- let index = 0;
8730
- index < entry.request.memberRequests.length;
8731
- index += 1
8732
- ) {
8733
- const request = entry.request.memberRequests[index]!;
8734
- if (resolvedResults[index] != null) {
8735
- successfulLiveStepCallIds.add(request.callId);
8946
+ for (let index = 0; index < entries.length; index += 1) {
8947
+ const entry = entries[index]!;
8948
+ const result = resolvedResults[index];
8949
+ if (result != null) {
8950
+ successfulLiveStepCallIds.add(entry.request.callId);
8736
8951
  }
8737
- resolveLiveFollowers(request, resolvedResults[index]);
8952
+ resolveLiveFollowers(entry.request, result);
8953
+ entry.resolve(result);
8738
8954
  }
8739
- }
8740
-
8741
- recordToolStep(
8742
- chunkResults.flatMap((entry) => entry.request.memberRequests),
8743
- );
8744
- this.#options.onBatchComplete?.(this.checkpoint);
8745
- },
8746
- });
8747
- } else {
8748
- const completionBuffer: Array<{
8749
- request: ToolCallRequest;
8750
- result: unknown | null;
8751
- metadata?: ToolResultMetadataInput | null;
8752
- jobId?: string;
8753
- meta?: Record<string, unknown>;
8754
- resolve: (value: unknown) => void;
8755
- reject: (error: unknown) => void;
8756
- }> = [];
8757
- const completionFailedCallIds = new Set<string>();
8758
- let completionFlushScheduled = false;
8759
- const flushCompletionBuffer = async (): Promise<void> => {
8760
- const entries = completionBuffer.splice(0);
8761
- if (entries.length === 0) return;
8762
- try {
8763
- const resolvedResults = await this.resolveToolCallBatchResults(
8764
- toolId,
8765
- entries.map((entry) => ({
8766
- request: entry.request,
8767
- result: entry.result,
8768
- metadata: entry.metadata,
8769
- jobId: entry.jobId,
8770
- meta: entry.meta,
8771
- })),
8772
- );
8773
- for (let index = 0; index < entries.length; index += 1) {
8774
- const entry = entries[index]!;
8775
- const result = resolvedResults[index];
8776
- if (result != null) {
8777
- successfulLiveStepCallIds.add(entry.request.callId);
8955
+ } catch (error) {
8956
+ for (const entry of entries) {
8957
+ completionFailedCallIds.add(entry.request.callId);
8958
+ await this.rejectToolCall(toolId, entry.request, error, {
8959
+ persistReceiptFailure: false,
8960
+ });
8961
+ entry.reject(error);
8778
8962
  }
8779
- resolveLiveFollowers(entry.request, result);
8780
- entry.resolve(result);
8781
8963
  }
8782
- } catch (error) {
8783
- for (const entry of entries) {
8784
- completionFailedCallIds.add(entry.request.callId);
8785
- await this.rejectToolCall(toolId, entry.request, error, {
8786
- persistReceiptFailure: false,
8964
+ };
8965
+ const enqueueCompletion = (
8966
+ request: ToolCallRequest,
8967
+ execution: ParsedToolExecuteResponse,
8968
+ ): Promise<unknown> =>
8969
+ new Promise((resolve, reject) => {
8970
+ completionBuffer.push({
8971
+ request,
8972
+ result: execution.result ?? null,
8973
+ metadata: execution.metadata ?? null,
8974
+ jobId: execution.jobId,
8975
+ meta: execution.meta,
8976
+ resolve,
8977
+ reject,
8787
8978
  });
8788
- entry.reject(error);
8789
- }
8790
- }
8791
- };
8792
- const enqueueCompletion = (
8793
- request: ToolCallRequest,
8794
- execution: ParsedToolExecuteResponse,
8795
- ): Promise<unknown> =>
8796
- new Promise((resolve, reject) => {
8797
- completionBuffer.push({
8798
- request,
8799
- result: execution.result ?? null,
8800
- metadata: execution.metadata ?? null,
8801
- jobId: execution.jobId,
8802
- meta: execution.meta,
8803
- resolve,
8804
- reject,
8979
+ if (completionFlushScheduled) return;
8980
+ completionFlushScheduled = true;
8981
+ setTimeout(() => {
8982
+ completionFlushScheduled = false;
8983
+ void flushCompletionBuffer();
8984
+ }, 0);
8805
8985
  });
8806
- if (completionFlushScheduled) return;
8807
- completionFlushScheduled = true;
8808
- setTimeout(() => {
8809
- completionFlushScheduled = false;
8810
- void flushCompletionBuffer();
8811
- }, 0);
8812
- });
8813
- // Seed the dispatch width from the governor's provider-shaped
8814
- // parallelism instead of the flat policy.concurrency.toolCalls
8815
- // ceiling. The flat width launched every pending row at once into
8816
- // the pacer, so unhinted providers 429'd on the first burst before
8817
- // AIMD could halve the rate. The shaped estimate is derived from the
8818
- // provider's RPS/maxConcurrency pacing rules (see
8819
- // suggestedParallelism in governor.ts), floored at 1 and capped by
8820
- // the global tool-call concurrency ceiling. The pacer still gates
8821
- // each in-flight call, so this only trims the launch burst.
8822
- const toolCallConcurrencyCeiling =
8823
- this.governor.policy.concurrency.toolCalls;
8824
- const shapedToolParallelism =
8825
- await this.resourceGovernor.suggestedToolParallelism(
8826
- toolId,
8986
+ // Seed the dispatch width from the governor's provider-shaped
8987
+ // parallelism instead of the flat policy.concurrency.toolCalls
8988
+ // ceiling. The flat width launched every pending row at once into
8989
+ // the pacer, so unhinted providers 429'd on the first burst before
8990
+ // AIMD could halve the rate. The shaped estimate is derived from the
8991
+ // provider's RPS/maxConcurrency pacing rules (see
8992
+ // suggestedParallelism in governor.ts), floored at 1 and capped by
8993
+ // the global tool-call concurrency ceiling. The pacer still gates
8994
+ // each in-flight call, so this only trims the launch burst.
8995
+ const toolCallConcurrencyCeiling =
8996
+ this.governor.policy.concurrency.toolCalls;
8997
+ const shapedToolParallelism =
8998
+ await this.resourceGovernor.suggestedToolParallelism(
8999
+ toolId,
9000
+ toolCallConcurrencyCeiling,
9001
+ );
9002
+ const dispatchWidth = Math.min(
8827
9003
  toolCallConcurrencyCeiling,
9004
+ Math.max(1, shapedToolParallelism),
8828
9005
  );
8829
- const dispatchWidth = Math.min(
8830
- toolCallConcurrencyCeiling,
8831
- Math.max(1, shapedToolParallelism),
8832
- );
8833
- const dispatchResults = await dispatchBoundedSettled(
8834
- pendingRequests,
8835
- dispatchWidth,
8836
- async (request) => {
8837
- // Circuit breaker: do not dispatch this provider call once a
8838
- // persistence failure has occurred in this run.
8839
- if (this.persistenceLatch.tripped) {
8840
- this.persistenceLatch.preventedCallCount += 1;
8841
- throw new RuntimePersistenceCircuitOpenError(
8842
- this.persistenceLatch,
9006
+ const dispatchResults = await dispatchBoundedSettled(
9007
+ pendingRequests,
9008
+ dispatchWidth,
9009
+ async (request) => {
9010
+ // Circuit breaker: do not dispatch this provider call once a
9011
+ // persistence failure has occurred in this run.
9012
+ if (this.persistenceLatch.tripped) {
9013
+ this.persistenceLatch.preventedCallCount += 1;
9014
+ throw new RuntimePersistenceCircuitOpenError(
9015
+ this.persistenceLatch,
9016
+ );
9017
+ }
9018
+ const execution = await this.callToolExecutionAPI(
9019
+ toolId,
9020
+ request.input,
9021
+ {
9022
+ beforeProviderCall: () =>
9023
+ this.assertRuntimeToolReceiptOwnership([request]),
9024
+ ...(request.receiptKey
9025
+ ? {
9026
+ durableCallReceiptKey: request.receiptKey,
9027
+ executionAuthScopeDigest:
9028
+ request.executionAuthScopeDigest,
9029
+ providerIdempotencyKey:
9030
+ this.providerIdempotencyKeyForToolCall({
9031
+ cacheKey: request.receiptKey,
9032
+ force: request.force === true,
9033
+ leaseId: request.receiptLeaseId,
9034
+ }),
9035
+ receiptLeaseExpiresAt:
9036
+ request.receiptLeaseExpiresAt,
9037
+ heartbeatReceipt: () =>
9038
+ this.renewRuntimeToolReceiptOwnership([request]),
9039
+ }
9040
+ : {}),
9041
+ timeoutMs: resolveRuntimeTimeoutMsForClaimedOwners([
9042
+ request,
9043
+ ]),
9044
+ },
8843
9045
  );
8844
- }
8845
- const execution = await this.callToolExecutionAPI(
8846
- toolId,
8847
- request.input,
8848
- {
8849
- beforeProviderCall: () =>
8850
- this.assertRuntimeToolReceiptOwnership([request]),
8851
- ...(request.receiptKey
8852
- ? {
8853
- durableCallReceiptKey: request.receiptKey,
8854
- executionAuthScopeDigest:
8855
- request.executionAuthScopeDigest,
8856
- providerIdempotencyKey:
8857
- this.providerIdempotencyKeyForToolCall({
8858
- cacheKey: request.receiptKey,
8859
- force: request.force === true,
8860
- leaseId: request.receiptLeaseId,
8861
- }),
8862
- receiptLeaseExpiresAt: request.receiptLeaseExpiresAt,
8863
- heartbeatReceipt: () =>
8864
- this.renewRuntimeToolReceiptOwnership([request]),
8865
- }
8866
- : {}),
8867
- timeoutMs: resolveRuntimeTimeoutMsForClaimedOwners([
8868
- request,
8869
- ]),
8870
- },
8871
- );
8872
- return await enqueueCompletion(request, execution);
8873
- },
8874
- );
8875
- const failedEntries = dispatchResults.filter(
8876
- (entry) =>
8877
- entry.error !== undefined &&
8878
- !completionFailedCallIds.has(entry.job.callId),
8879
- );
8880
- await Promise.allSettled(
8881
- failedEntries.map((entry) =>
8882
- rejectWithLiveFollowers(entry.job, entry.error),
8883
- ),
8884
- );
8885
- recordToolStep(pendingRequests);
8886
- this.#options.onBatchComplete?.(this.checkpoint);
9046
+ return await enqueueCompletion(request, execution);
9047
+ },
9048
+ );
9049
+ const failedEntries = dispatchResults.filter(
9050
+ (entry) =>
9051
+ entry.error !== undefined &&
9052
+ !completionFailedCallIds.has(entry.job.callId),
9053
+ );
9054
+ await Promise.allSettled(
9055
+ failedEntries.map((entry) =>
9056
+ rejectWithLiveFollowers(entry.job, entry.error),
9057
+ ),
9058
+ );
9059
+ recordToolStep(pendingRequests);
9060
+ this.#options.onBatchComplete?.(this.checkpoint);
9061
+ }
8887
9062
  }
9063
+ } finally {
9064
+ pendingReceiptHeartbeat?.stop();
8888
9065
  }
9066
+ const pendingReceiptLeaseLost = pendingReceiptHeartbeat?.leaseLost();
9067
+ if (pendingReceiptLeaseLost) throw pendingReceiptLeaseLost;
8889
9068
  if (durableExistingRunningHandler) {
8890
9069
  await durableExistingRunningHandler;
8891
9070
  }
@@ -9321,11 +9500,44 @@ export class PlayContextImpl {
9321
9500
  // is the fetch itself, so independently delayed runners cannot
9322
9501
  // compress real provider arrivals after their tickets.
9323
9502
  const protectionHeaders = await this.vercelProtectionHeaders();
9324
- const providerPermit =
9325
- await this.resourceGovernor.acquireProviderPermit({
9326
- toolId,
9327
- signal: abortController?.signal,
9328
- });
9503
+ // Use the mode carried by this physical execution request.
9504
+ // A restored execution scope may predate integration-mode
9505
+ // authority metadata, while the signed launch and every tool
9506
+ // request still carry options.integrationMode. Pacing the
9507
+ // latter as live would make fixture runs both slow and
9508
+ // unauditable even though the app never dispatches a provider
9509
+ // request.
9510
+ const fixtureExecution =
9511
+ this.#options.integrationMode === 'fixture';
9512
+ const enforceFixtureProviderPacing =
9513
+ fixtureExecution &&
9514
+ this.#options.enforceFixtureProviderPacing === true;
9515
+ const fixtureOnlyExecution =
9516
+ fixtureExecution && !enforceFixtureProviderPacing;
9517
+ if (
9518
+ fixtureOnlyExecution &&
9519
+ !this.fixtureProviderPacingBypassLogged
9520
+ ) {
9521
+ this.fixtureProviderPacingBypassLogged = true;
9522
+ this.log(
9523
+ 'Fixture mode: provider pacing bypassed because no provider request is dispatched.',
9524
+ );
9525
+ }
9526
+ if (
9527
+ enforceFixtureProviderPacing &&
9528
+ !this.fixtureProviderPacingEnforcementLogged
9529
+ ) {
9530
+ this.fixtureProviderPacingEnforcementLogged = true;
9531
+ this.log(
9532
+ 'Fixture mode: provider pacing explicitly enforced for production-parity testing.',
9533
+ );
9534
+ }
9535
+ const providerPermit = fixtureOnlyExecution
9536
+ ? { release() {} }
9537
+ : await this.resourceGovernor.acquireProviderPermit({
9538
+ toolId,
9539
+ signal: abortController?.signal,
9540
+ });
9329
9541
  const integrationFetchStartedAt = Date.now();
9330
9542
  providerCallStartedAt = integrationFetchStartedAt;
9331
9543
  try {