deepline 0.3.72 → 0.3.73

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.
@@ -175,6 +175,7 @@ import {
175
175
  buildDurableToolAggregateReceiptKey,
176
176
  buildDurableToolCallAuthScopeDigest,
177
177
  buildDurableToolCallCacheKey,
178
+ buildLegacyRevisionedDurableToolCallCacheKey,
178
179
  buildDurableToolPayloadProviderIdempotencyKey,
179
180
  buildDurableToolProviderIdempotencyKey,
180
181
  buildDurableToolReceiptPrefix,
@@ -1395,6 +1396,8 @@ function playNodeScopeForToolCallRequest(
1395
1396
  type ToolExecutionApiOptions = {
1396
1397
  timeoutMs?: number;
1397
1398
  durableCallReceiptKey?: string | null;
1399
+ /** The receipt key is a completed-result cache for this read-only call. */
1400
+ completedResultCache?: boolean;
1398
1401
  executionAuthScopeDigest?: string | null;
1399
1402
  /**
1400
1403
  * Historical receipt identity used for the API's provider/billing
@@ -2763,9 +2766,22 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
2763
2766
  }
2764
2767
 
2765
2768
  /**
2766
- * Only explicitly declared response transformations affect receipt reuse.
2767
- * Missing preserves historical artifact/receipt identity, while the public
2768
- * response header still normalizes it to the legacy V2 behavior above.
2769
+ * A real published artifact pins its response presentation in its launch
2770
+ * contract. In-process callers that omit the option are runtime consumers,
2771
+ * not historical Play artifacts, and retain the canonical result they pass
2772
+ * in. This keeps the context API backward-compatible while letting a pinned
2773
+ * legacy artifact request its old projection after a cache read.
2774
+ */
2775
+ private get hasPinnedToolResponseContract(): boolean {
2776
+ return (
2777
+ this.activeInlineComposition !== null ||
2778
+ this.#options.toolResponseContract !== undefined
2779
+ );
2780
+ }
2781
+
2782
+ /**
2783
+ * Compatibility metadata retained for already-published artifacts. It never
2784
+ * changes the completed-result cache identity.
2769
2785
  */
2770
2786
  private get currentToolResponseReceiptRevision(): string | undefined {
2771
2787
  if (this.activeInlineComposition) {
@@ -4020,14 +4036,14 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
4020
4036
  );
4021
4037
  if (normalized) byKey.set(normalized.key, normalized);
4022
4038
  }
4023
- await this.reconcileMissingRuntimeStepReceiptCompletions(
4039
+ await this.reconcileIncompleteRuntimeStepReceiptCompletions(
4024
4040
  normalizedInputs,
4025
4041
  byKey,
4026
4042
  );
4027
4043
  return byKey;
4028
4044
  }
4029
4045
 
4030
- private async reconcileMissingRuntimeStepReceiptCompletions(
4046
+ private async reconcileIncompleteRuntimeStepReceiptCompletions(
4031
4047
  receipts: Array<{
4032
4048
  key: string;
4033
4049
  runId: string;
@@ -4038,32 +4054,45 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
4038
4054
  completedByKey: Map<string, RuntimeStepReceipt>,
4039
4055
  ): Promise<void> {
4040
4056
  if (
4041
- receipts.length === completedByKey.size ||
4042
- (!this.#options.getRuntimeStepReceipt &&
4043
- !this.#options.getRuntimeStepReceipts)
4057
+ !this.#options.getRuntimeStepReceipt &&
4058
+ !this.#options.getRuntimeStepReceipts
4044
4059
  ) {
4045
4060
  return;
4046
4061
  }
4047
- const missingKeys = receipts
4062
+ // The receipt gateway is allowed to return a compact terminal
4063
+ // acknowledgement without `output`. That is sufficient for old callers
4064
+ // which keep the live result, but a completed-result cache must hydrate
4065
+ // the stored output before handing it to a cache cohort.
4066
+ const incompleteKeys = receipts
4048
4067
  .map((receipt) => receipt.key.trim())
4049
- .filter((key) => key && !completedByKey.has(key));
4050
- if (missingKeys.length === 0) return;
4051
- const latest = await this.getRuntimeStepReceipts(missingKeys);
4052
- for (const key of missingKeys) {
4068
+ .filter((key) => {
4069
+ if (!key) return false;
4070
+ const completed = completedByKey.get(key);
4071
+ return (
4072
+ !completed ||
4073
+ ((completed.status === 'completed' ||
4074
+ completed.status === 'skipped') &&
4075
+ completed.output === undefined)
4076
+ );
4077
+ });
4078
+ if (incompleteKeys.length === 0) return;
4079
+ const latest = await this.getRuntimeStepReceipts(incompleteKeys);
4080
+ for (const key of incompleteKeys) {
4053
4081
  const recovered = latest.get(key);
4054
4082
  if (
4055
- recovered?.status === 'completed' ||
4056
- recovered?.status === 'skipped'
4083
+ (recovered?.status === 'completed' ||
4084
+ recovered?.status === 'skipped') &&
4085
+ recovered.output !== undefined
4057
4086
  ) {
4058
4087
  completedByKey.set(key, recovered);
4059
4088
  }
4060
4089
  }
4061
- const recoveredCount = missingKeys.filter((key) =>
4062
- completedByKey.has(key),
4090
+ const recoveredCount = incompleteKeys.filter(
4091
+ (key) => completedByKey.get(key)?.output !== undefined,
4063
4092
  ).length;
4064
4093
  if (recoveredCount > 0) {
4065
4094
  this.runtimeLog(
4066
- `Runtime tool receipt completion response reconciled ${recoveredCount}/${missingKeys.length} missing receipt(s) from durable store read-back.`,
4095
+ `Runtime tool receipt completion response hydrated ${recoveredCount}/${incompleteKeys.length} incomplete receipt(s) from durable store read-back.`,
4067
4096
  );
4068
4097
  }
4069
4098
  }
@@ -4178,17 +4207,39 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
4178
4207
  );
4179
4208
  }
4180
4209
 
4181
- private async durableToolCallCacheKeyForScope(
4182
- input: {
4183
- toolId: string;
4184
- requestInput: Record<string, unknown>;
4185
- executionAuthScopeDigest?: string | null;
4186
- staleAfterSeconds?: number | null;
4187
- playLocalScope?: string | null;
4188
- },
4189
- toolResponseReceiptRevision: string | null | undefined = this
4190
- .currentToolResponseReceiptRevision,
4191
- ): Promise<string> {
4210
+ private async durableToolCallCacheKeyForScope(input: {
4211
+ toolId: string;
4212
+ requestInput: Record<string, unknown>;
4213
+ executionAuthScopeDigest?: string | null;
4214
+ staleAfterSeconds?: number | null;
4215
+ playLocalScope?: string | null;
4216
+ }): Promise<string> {
4217
+ const identity = await this.durableToolCallCacheIdentity(input);
4218
+ return buildDurableToolCallCacheKey(identity);
4219
+ }
4220
+
4221
+ private async legacyDurableToolCallCacheKeyForScope(input: {
4222
+ toolId: string;
4223
+ requestInput: Record<string, unknown>;
4224
+ executionAuthScopeDigest?: string | null;
4225
+ staleAfterSeconds?: number | null;
4226
+ playLocalScope?: string | null;
4227
+ }): Promise<string | null> {
4228
+ const revision = this.currentToolResponseReceiptRevision;
4229
+ if (!revision) return null;
4230
+ return buildLegacyRevisionedDurableToolCallCacheKey({
4231
+ ...(await this.durableToolCallCacheIdentity(input)),
4232
+ toolResponseReceiptRevision: revision,
4233
+ });
4234
+ }
4235
+
4236
+ private async durableToolCallCacheIdentity(input: {
4237
+ toolId: string;
4238
+ requestInput: Record<string, unknown>;
4239
+ executionAuthScopeDigest?: string | null;
4240
+ staleAfterSeconds?: number | null;
4241
+ playLocalScope?: string | null;
4242
+ }): Promise<Parameters<typeof buildDurableToolCallCacheKey>[0]> {
4192
4243
  const providerActionVersion =
4193
4244
  (await this.#options.getToolActionCacheVersion?.(input.toolId))?.trim() ??
4194
4245
  '';
@@ -4196,7 +4247,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
4196
4247
  input.executionAuthScopeDigest?.trim() ||
4197
4248
  (await this.resolveToolAuthScopeDigest(input.toolId))?.trim() ||
4198
4249
  null;
4199
- return buildDurableToolCallCacheKey({
4250
+ return {
4200
4251
  orgId: this.#options.orgId,
4201
4252
  playLocalScope: input.playLocalScope,
4202
4253
  toolId: input.toolId,
@@ -4207,29 +4258,33 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
4207
4258
  executionAuthScopeDigest,
4208
4259
  }),
4209
4260
  providerActionVersion,
4210
- toolResponseReceiptRevision,
4211
4261
  staleAfterSeconds: input.staleAfterSeconds,
4212
4262
  cacheEpochMs: this.durableCallCacheEpochMs,
4263
+ };
4264
+ }
4265
+
4266
+ private async durableToolCallCacheKey(input: {
4267
+ toolId: string;
4268
+ requestInput: Record<string, unknown>;
4269
+ executionAuthScopeDigest?: string | null;
4270
+ staleAfterSeconds?: number | null;
4271
+ }): Promise<string> {
4272
+ return await this.durableToolCallCacheKeyForScope({
4273
+ ...input,
4274
+ playLocalScope: this.currentGovernance.currentPlayId,
4213
4275
  });
4214
4276
  }
4215
4277
 
4216
- private async durableToolCallCacheKey(
4217
- input: {
4218
- toolId: string;
4219
- requestInput: Record<string, unknown>;
4220
- executionAuthScopeDigest?: string | null;
4221
- staleAfterSeconds?: number | null;
4222
- },
4223
- toolResponseReceiptRevision: string | null | undefined = this
4224
- .currentToolResponseReceiptRevision,
4225
- ): Promise<string> {
4226
- return await this.durableToolCallCacheKeyForScope(
4227
- {
4228
- ...input,
4229
- playLocalScope: this.currentGovernance.currentPlayId,
4230
- },
4231
- toolResponseReceiptRevision,
4232
- );
4278
+ private async legacyDurableToolCallCacheKey(input: {
4279
+ toolId: string;
4280
+ requestInput: Record<string, unknown>;
4281
+ executionAuthScopeDigest?: string | null;
4282
+ staleAfterSeconds?: number | null;
4283
+ }): Promise<string | null> {
4284
+ return await this.legacyDurableToolCallCacheKeyForScope({
4285
+ ...input,
4286
+ playLocalScope: this.currentGovernance.currentPlayId,
4287
+ });
4233
4288
  }
4234
4289
 
4235
4290
  private async resolveToolAuthScopeDigest(
@@ -5379,6 +5434,23 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
5379
5434
  return null;
5380
5435
  }
5381
5436
 
5437
+ private canReuseToolResultForCurrentResponseContract(
5438
+ result: unknown,
5439
+ ): boolean {
5440
+ if (this.currentToolResponseContract !== RAW_V2_TOOL_RESPONSE_CONTRACT) {
5441
+ return true;
5442
+ }
5443
+ const toolResult = isToolExecuteResult(result)
5444
+ ? result
5445
+ : isSerializedToolExecuteResult(result)
5446
+ ? deserializeToolExecuteResult(result)
5447
+ : null;
5448
+ return (
5449
+ toolResult !== null &&
5450
+ Object.prototype.hasOwnProperty.call(toolResult.toolResponse, 'rawV2')
5451
+ );
5452
+ }
5453
+
5382
5454
  private cacheToolResult(
5383
5455
  toolId: string,
5384
5456
  rowCacheKey: string,
@@ -5491,6 +5563,38 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
5491
5563
  );
5492
5564
  }
5493
5565
 
5566
+ /**
5567
+ * Receipts retain the canonical raw-v2 provider outcome. Older artifacts
5568
+ * receive their legacy projection only at the Play boundary, after cache
5569
+ * lookup and persistence have finished.
5570
+ */
5571
+ private materializeToolExecutionResult(
5572
+ result: ToolExecuteResult,
5573
+ ): ToolExecuteResult {
5574
+ if (
5575
+ this.currentToolResponseContract === RAW_V2_TOOL_RESPONSE_CONTRACT ||
5576
+ !this.hasPinnedToolResponseContract ||
5577
+ !Object.prototype.hasOwnProperty.call(result.toolResponse, 'rawV2')
5578
+ ) {
5579
+ return result;
5580
+ }
5581
+ const serialized = serializeToolExecuteResult(result);
5582
+ if (serialized.__kind !== 'deepline.tool_execute_result.v2') {
5583
+ return result;
5584
+ }
5585
+ return deserializeToolExecuteResult({
5586
+ ...serialized,
5587
+ __kind: 'deepline.tool_execute_result.v1',
5588
+ toolResponse: {
5589
+ raw: legacyRawFromToolResponseRawV2(
5590
+ serialized.toolResponse.rawV2,
5591
+ serialized.toolResponse.view,
5592
+ serialized.toolResponse.responseMeta,
5593
+ ),
5594
+ },
5595
+ });
5596
+ }
5597
+
5494
5598
  private attachCustomerDbDatasetResult(
5495
5599
  toolId: string,
5496
5600
  requestInput: Record<string, unknown> | undefined,
@@ -9272,16 +9376,26 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9272
9376
  executionAuthScopeDigest,
9273
9377
  staleAfterSeconds: toolCachePolicy.staleAfterSeconds,
9274
9378
  });
9275
- let providerIdempotencyKeyBase = await this.durableToolCallCacheKey(
9276
- {
9277
- toolId,
9278
- requestInput: input,
9279
- executionAuthScopeDigest,
9280
- staleAfterSeconds: toolCachePolicy.staleAfterSeconds,
9281
- },
9282
- null,
9283
- );
9284
- const checkpointCacheKeys = [durableCacheKey];
9379
+ let legacyDurableCacheKey = await this.legacyDurableToolCallCacheKey({
9380
+ toolId,
9381
+ requestInput: input,
9382
+ executionAuthScopeDigest,
9383
+ staleAfterSeconds: toolCachePolicy.staleAfterSeconds,
9384
+ });
9385
+ // Only explicitly fenced operations use this key as a provider identity.
9386
+ // Ordinary calls use it solely as their completed-result cache key.
9387
+ let providerIdempotencyKeyBase = durableCacheKey;
9388
+ let directResultReceiptKey = durableCacheKey;
9389
+ // A raw-v2 artifact must never read an older receipt that cannot render
9390
+ // raw-v2. If both the primary and its revisioned recovery receipt are
9391
+ // unrenderable, make a live call rather than returning either cache entry.
9392
+ // We cannot safely replace an immutable completed receipt, so that rare
9393
+ // incompatible state deliberately forgoes caching until it is repaired.
9394
+ let bypassDirectResultCache = false;
9395
+ const checkpointCacheKeys = [
9396
+ durableCacheKey,
9397
+ ...(legacyDurableCacheKey ? [legacyDurableCacheKey] : []),
9398
+ ];
9285
9399
 
9286
9400
  const executeTool = async (context?: {
9287
9401
  leaseId?: string | null;
@@ -9310,7 +9424,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9310
9424
  if (!store) {
9311
9425
  const directCacheKey = durableCacheKey;
9312
9426
  let initialDirectReceipt: RuntimeStepReceipt | null | undefined;
9313
- const cached = !cacheableToolResult
9427
+ const cachedCandidate = !cacheableToolResult
9314
9428
  ? null
9315
9429
  : toolCachePolicy.force
9316
9430
  ? null
@@ -9319,6 +9433,13 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9319
9433
  checkpointCacheKeys,
9320
9434
  'direct',
9321
9435
  );
9436
+ const cached =
9437
+ cachedCandidate &&
9438
+ this.canReuseToolResultForCurrentResponseContract(
9439
+ cachedCandidate.result.result,
9440
+ )
9441
+ ? cachedCandidate
9442
+ : null;
9322
9443
  if (cached) {
9323
9444
  this.runtimeLog(`Calling tool: ${toolId} recovered from checkpoint`);
9324
9445
  // Started and settled are paired in a finally: an uncovered throw
@@ -9342,7 +9463,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9342
9463
  outcome: 'cached',
9343
9464
  durationMs: 0,
9344
9465
  });
9345
- return recovered;
9466
+ return this.materializeToolExecutionResult(recovered);
9346
9467
  } finally {
9347
9468
  if (!settled) {
9348
9469
  this.emitToolCallLifecycle('settled', toolId, normalizedKey, {
@@ -9361,17 +9482,36 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9361
9482
  const historicalReceipt = await this.withRuntimeReceiptHydrationTurn(
9362
9483
  async () => await this.getRuntimeStepReceipt(directCacheKey),
9363
9484
  );
9364
- initialDirectReceipt = historicalReceipt;
9485
+ const historicalOutput = historicalReceipt
9486
+ ? this.runtimeReceiptOutput<unknown>(historicalReceipt)
9487
+ : undefined;
9488
+ const historicalIsTerminal =
9489
+ historicalReceipt?.status === 'completed' ||
9490
+ historicalReceipt?.status === 'skipped';
9491
+ const historicalCanServeCurrentContract =
9492
+ !historicalIsTerminal ||
9493
+ this.canReuseToolResultForCurrentResponseContract(historicalOutput);
9494
+ initialDirectReceipt = historicalCanServeCurrentContract
9495
+ ? historicalReceipt
9496
+ : null;
9497
+ if (!historicalCanServeCurrentContract) {
9498
+ if (!legacyDurableCacheKey) {
9499
+ throw new Error(
9500
+ `Tool ${toolId} has an unrenderable legacy result but no response-contract recovery cache key.`,
9501
+ );
9502
+ }
9503
+ directResultReceiptKey = legacyDurableCacheKey;
9504
+ }
9365
9505
  if (
9366
9506
  historicalReceipt &&
9367
- (historicalReceipt.status === 'completed' ||
9368
- historicalReceipt.status === 'skipped') &&
9507
+ historicalIsTerminal &&
9508
+ historicalCanServeCurrentContract &&
9369
9509
  !isSerializedToolExecuteResult(historicalReceipt.output) &&
9370
9510
  !isToolExecuteResult(historicalReceipt.output)
9371
9511
  ) {
9372
9512
  const historicalOutput =
9373
9513
  this.runtimeReceiptOutput<unknown>(historicalReceipt);
9374
- return markToolExecuteResultExecutionOutcome(
9514
+ const recoveredHistorical = markToolExecuteResultExecutionOutcome(
9375
9515
  historicalOutput,
9376
9516
  toolExecutionOutcomeForDurableReceipt({
9377
9517
  source: 'cache',
@@ -9379,6 +9519,43 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9379
9519
  cacheKey: directCacheKey,
9380
9520
  }),
9381
9521
  );
9522
+ return isToolExecuteResult(recoveredHistorical)
9523
+ ? this.materializeToolExecutionResult(recoveredHistorical)
9524
+ : recoveredHistorical;
9525
+ }
9526
+ if (
9527
+ (!historicalReceipt || !historicalCanServeCurrentContract) &&
9528
+ legacyDurableCacheKey
9529
+ ) {
9530
+ const legacyReceipt = await this.withRuntimeReceiptHydrationTurn(
9531
+ async () =>
9532
+ await this.getRuntimeStepReceipt(legacyDurableCacheKey!),
9533
+ );
9534
+ if (
9535
+ legacyReceipt &&
9536
+ (legacyReceipt.status === 'completed' ||
9537
+ legacyReceipt.status === 'skipped') &&
9538
+ legacyReceipt.output !== undefined
9539
+ ) {
9540
+ const recoveredLegacy = markToolExecuteResultExecutionOutcome(
9541
+ this.runtimeReceiptOutput(legacyReceipt),
9542
+ toolExecutionOutcomeForDurableReceipt({
9543
+ source: 'cache',
9544
+ receiptKey: legacyDurableCacheKey,
9545
+ cacheKey: directCacheKey,
9546
+ }),
9547
+ );
9548
+ if (
9549
+ this.canReuseToolResultForCurrentResponseContract(
9550
+ recoveredLegacy,
9551
+ )
9552
+ ) {
9553
+ return isToolExecuteResult(recoveredLegacy)
9554
+ ? this.materializeToolExecutionResult(recoveredLegacy)
9555
+ : recoveredLegacy;
9556
+ }
9557
+ bypassDirectResultCache = true;
9558
+ }
9382
9559
  }
9383
9560
  }
9384
9561
  this.runtimeLog(
@@ -9416,23 +9593,27 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9416
9593
  'Direct tool execution requires a stable logical call identity.',
9417
9594
  );
9418
9595
  }
9419
- const physicalDirectKey = cacheableToolResult
9596
+ const useDirectResultReceipts =
9597
+ cacheableToolResult && !bypassDirectResultCache;
9598
+ const physicalDirectKey = useDirectResultReceipts
9420
9599
  ? directCacheKey
9421
9600
  : `${buildDurableToolReceiptPrefix({
9422
9601
  orgId: this.#options.orgId ?? 'unknown-org',
9423
9602
  toolId,
9424
9603
  })}${stableDigest(`${this.currentRunId}:${logicalCallId}:always-fresh`)}`;
9604
+ const resultReceiptKey = directResultReceiptKey;
9425
9605
  const providerOperationKey = providerIdempotencyKeyBase;
9426
- const resultReceiptKey = directCacheKey;
9427
9606
  const toolRetryPolicy = await this.#options
9428
9607
  .getToolRetryPolicy?.(toolId, input)
9429
9608
  .catch(() => null);
9430
- // Direct cacheable calls now give ToolCall the real completed-result
9431
- // receipt Adapter. The provider-operation fence remains below the
9432
- // dispatcher: V2 and legacy presentation keys can therefore publish
9433
- // independent Tool Result Receipts without a second provider effect.
9434
- // Map members retain their cohort-owned receipt Adapter until their
9435
- // batched claim/heartbeat protocol moves behind the same Interface.
9609
+ if (
9610
+ bypassDirectResultCache &&
9611
+ toolRetryPolicy?.requiresExecutionFence === true
9612
+ ) {
9613
+ throw new Error(
9614
+ `Tool ${toolId} has an unrenderable completed result under an execution fence and cannot safely refresh it.`,
9615
+ );
9616
+ }
9436
9617
  const dispatcher = {
9437
9618
  dispatch: async ({
9438
9619
  providerOperationKey: operationKey,
@@ -9440,7 +9621,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9440
9621
  providerOperationKey: string | null;
9441
9622
  }) => {
9442
9623
  if (
9443
- cacheableToolResult &&
9624
+ useDirectResultReceipts &&
9444
9625
  operationKey !== providerOperationKey
9445
9626
  ) {
9446
9627
  throw new Error(
@@ -9453,18 +9634,11 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9453
9634
  startedAt: directCallStartedAt,
9454
9635
  input,
9455
9636
  options: {
9456
- ...(cacheableToolResult
9637
+ ...(useDirectResultReceipts
9457
9638
  ? {
9458
9639
  durableCallReceiptKey: physicalDirectKey,
9640
+ completedResultCache: true,
9459
9641
  executionAuthScopeDigest,
9460
- providerIdempotencyReceiptKey: operationKey,
9461
- providerIdempotencyKey:
9462
- this.providerIdempotencyKeyForToolCall({
9463
- cacheKey: providerIdempotencyKeyBase,
9464
- force: toolCachePolicy.force,
9465
- leaseId: directReceiptLeaseId,
9466
- logicalCallId,
9467
- }),
9468
9642
  ...(directReceiptLeaseId &&
9469
9643
  (this.#options.heartbeatRuntimeStepReceipts ||
9470
9644
  this.#options.getRuntimeStepReceipt ||
@@ -9487,6 +9661,24 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9487
9661
  : {}),
9488
9662
  }
9489
9663
  : {}),
9664
+ ...(toolRetryPolicy?.requiresExecutionFence === true
9665
+ ? {
9666
+ providerIdempotencyReceiptKey:
9667
+ cacheableToolResult && operationKey
9668
+ ? operationKey
9669
+ : physicalDirectKey,
9670
+ providerIdempotencyKey:
9671
+ this.providerIdempotencyKeyForToolCall({
9672
+ cacheKey:
9673
+ useDirectResultReceipts && operationKey
9674
+ ? providerIdempotencyKeyBase
9675
+ : physicalDirectKey,
9676
+ force: cacheableToolResult && toolCachePolicy.force,
9677
+ leaseId: directReceiptLeaseId,
9678
+ logicalCallId,
9679
+ }),
9680
+ }
9681
+ : {}),
9490
9682
  playNodeScope: buildPlayNodeScope({
9491
9683
  toolId,
9492
9684
  callKey: normalizedKey,
@@ -9511,14 +9703,14 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9511
9703
  execution: toolExecutionMetadataForOutcome({
9512
9704
  kind: 'live',
9513
9705
  cacheKey: directCacheKey,
9514
- ...(cacheableToolResult
9706
+ ...(useDirectResultReceipts
9515
9707
  ? { receiptKey: resultReceiptKey }
9516
9708
  : {}),
9517
9709
  }),
9518
9710
  });
9519
9711
  },
9520
9712
  };
9521
- const toolCall = cacheableToolResult
9713
+ const toolCall = useDirectResultReceipts
9522
9714
  ? createToolCallJob({
9523
9715
  resultReceipts: createRuntimeStepToolResultReceipts({
9524
9716
  store: this.durableReceiptExecutionStore(),
@@ -9526,7 +9718,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9526
9718
  ...(initialDirectReceipt !== undefined
9527
9719
  ? {
9528
9720
  initialReceipt: {
9529
- key: directCacheKey,
9721
+ key: directResultReceiptKey,
9530
9722
  receipt: initialDirectReceipt,
9531
9723
  },
9532
9724
  }
@@ -9537,6 +9729,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9537
9729
  ? {
9538
9730
  executionLock: {
9539
9731
  required: true,
9732
+ key: providerOperationKey,
9540
9733
  ttlMs: Math.min(
9541
9734
  600_000,
9542
9735
  resolveToolRuntimeTimeoutMs(
@@ -9599,11 +9792,16 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9599
9792
  })
9600
9793
  : await createAlwaysFreshToolCallAdapter({ dispatcher }).call(call);
9601
9794
  settleDirectCall(toolAttemptOutcomeForResult(wrapped));
9602
- if (cacheableToolResult) {
9603
- this.cacheToolResult(toolId, directCacheKey, wrapped, 'direct');
9795
+ if (useDirectResultReceipts) {
9796
+ this.cacheToolResult(
9797
+ toolId,
9798
+ directResultReceiptKey,
9799
+ wrapped,
9800
+ 'direct',
9801
+ );
9604
9802
  this.#options.onBatchComplete?.(this.checkpoint);
9605
9803
  }
9606
- return wrapped;
9804
+ return this.materializeToolExecutionResult(wrapped);
9607
9805
  } catch (error) {
9608
9806
  settleDirectCall('failed', error);
9609
9807
  throw error;
@@ -9637,17 +9835,22 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9637
9835
  : this.getCachedToolResultCandidate(toolId, checkpointCacheKeys);
9638
9836
  if (cached) {
9639
9837
  this.runtimeLog(` Row ${rowId} ${toolId}: recovered from checkpoint`);
9640
- return await this.wrapToolExecutionResult({
9641
- toolId,
9642
- status: cached.result.result == null ? 'no_result' : 'completed',
9643
- result: cached.result.result,
9644
- requestInput: input,
9645
- execution: toolExecutionMetadataForOutcome({
9646
- kind: 'checkpoint',
9647
- cacheKey: cached.cacheKey,
9838
+ return this.materializeToolExecutionResult(
9839
+ await this.wrapToolExecutionResult({
9840
+ toolId,
9841
+ status: cached.result.result == null ? 'no_result' : 'completed',
9842
+ result: cached.result.result,
9843
+ requestInput: input,
9844
+ execution: toolExecutionMetadataForOutcome({
9845
+ kind: 'checkpoint',
9846
+ cacheKey: cached.cacheKey,
9847
+ }),
9648
9848
  }),
9649
- });
9849
+ );
9650
9850
  }
9851
+ const rowToolRetryPolicy = await this.#options
9852
+ .getToolRetryPolicy?.(toolId, input)
9853
+ .catch(() => null);
9651
9854
 
9652
9855
  // ADR 0018: record this leg before it runs. `producer` still names the
9653
9856
  // last leg (the usual cascade winner); `producers` keeps every leg, which
@@ -9702,7 +9905,12 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9702
9905
  providerIdempotencyKeyBase,
9703
9906
  cacheable: cacheableToolResult,
9704
9907
  receiptKey: cacheableToolResult ? durableCacheKey : null,
9908
+ legacyReceiptKeys: legacyDurableCacheKey
9909
+ ? [legacyDurableCacheKey]
9910
+ : [],
9705
9911
  executionAuthScopeDigest,
9912
+ requiresExecutionFence:
9913
+ rowToolRetryPolicy?.requiresExecutionFence === true,
9706
9914
  force: toolCachePolicy.force,
9707
9915
  forceFailedRefresh: toolCachePolicy.forceFailedRefresh,
9708
9916
  rowId,
@@ -9722,7 +9930,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9722
9930
  return await pendingToolCall.then(
9723
9931
  (value) => {
9724
9932
  settleProducerAttempt(toolAttemptOutcomeForResult(value));
9725
- return value;
9933
+ return this.materializeToolExecutionResult(
9934
+ value as ToolExecuteResult,
9935
+ );
9726
9936
  },
9727
9937
  (error: unknown) => {
9728
9938
  settleProducerAttempt('failed');
@@ -9771,16 +9981,21 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9771
9981
  executionAuthScopeDigest,
9772
9982
  staleAfterSeconds: toolCachePolicy.staleAfterSeconds,
9773
9983
  });
9774
- providerIdempotencyKeyBase = await this.durableToolCallCacheKey(
9775
- {
9776
- toolId,
9777
- requestInput: input,
9778
- executionAuthScopeDigest,
9779
- staleAfterSeconds: toolCachePolicy.staleAfterSeconds,
9780
- },
9781
- null,
9984
+ legacyDurableCacheKey = await this.legacyDurableToolCallCacheKey({
9985
+ toolId,
9986
+ requestInput: input,
9987
+ executionAuthScopeDigest,
9988
+ staleAfterSeconds: toolCachePolicy.staleAfterSeconds,
9989
+ });
9990
+ providerIdempotencyKeyBase = durableCacheKey;
9991
+ directResultReceiptKey = durableCacheKey;
9992
+ bypassDirectResultCache = false;
9993
+ checkpointCacheKeys.splice(
9994
+ 0,
9995
+ checkpointCacheKeys.length,
9996
+ durableCacheKey,
9997
+ ...(legacyDurableCacheKey ? [legacyDurableCacheKey] : []),
9782
9998
  );
9783
- checkpointCacheKeys[0] = durableCacheKey;
9784
9999
  }
9785
10000
  }
9786
10001
 
@@ -11009,75 +11224,59 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
11009
11224
  /**
11010
11225
  * Concrete RuntimeStepReceipt adapter for the map receipt-cohort Module.
11011
11226
  *
11012
- * It is intentionally shared by scalar and provider-native map calls. The
11013
- * two call shapes differ at provider dispatch, not at the member receipt
11014
- * protocol: both need an atomic claim/reclaim, a single cohort heartbeat,
11015
- * and lease-fenced terminal fanout. Keeping that at one seam prevents a
11016
- * native batch from becoming a second, subtly different receipt runtime.
11227
+ * Scalar and provider-native map calls share completed-result cache reads
11228
+ * and atomic successful inserts. A miss has no durable execution owner.
11017
11229
  */
11018
11230
  private createRuntimeStepToolCallReceiptCohortStore(input: {
11019
11231
  toolId: string;
11020
11232
  requestForReceipt: ReadonlyMap<string, ToolCallRequest>;
11021
11233
  }): ToolCallReceiptCohortStore {
11022
11234
  const asRecord = async (
11023
- receiptKey: string,
11235
+ resultReceiptKey: string,
11236
+ storedReceiptKey: string,
11024
11237
  receipt: RuntimeStepReceipt | null | undefined,
11025
11238
  ): Promise<ToolCallReceiptRecord | null> => {
11026
11239
  if (!receipt) return null;
11027
- const normalized = this.normalizeRuntimeStepReceipt(receiptKey, receipt);
11240
+ const normalized = this.normalizeRuntimeStepReceipt(
11241
+ storedReceiptKey,
11242
+ receipt,
11243
+ );
11028
11244
  if (!normalized) return null;
11029
- const request = input.requestForReceipt.get(receiptKey);
11245
+ if (
11246
+ normalized.status !== 'completed' &&
11247
+ normalized.status !== 'skipped'
11248
+ ) {
11249
+ return null;
11250
+ }
11251
+ // A terminal acknowledgement without output is not a cache hit. The
11252
+ // completion path hydrates it first; if that read cannot provide an
11253
+ // output, retain the live result rather than resolving `undefined`.
11254
+ if (normalized.output === undefined) return null;
11255
+ const request = input.requestForReceipt.get(resultReceiptKey);
11030
11256
  if (!request) {
11031
11257
  throw new Error(
11032
- `Unknown Tool Call Receipt Cohort receipt ${receiptKey}.`,
11258
+ `Unknown Tool Call Receipt Cohort receipt ${resultReceiptKey}.`,
11033
11259
  );
11034
11260
  }
11035
- const terminal =
11036
- normalized.status === 'completed' || normalized.status === 'skipped';
11037
- const result = terminal
11038
- ? await this.wrapToolExecutionResult({
11039
- toolId: input.toolId,
11040
- status:
11041
- normalized.output === null || normalized.output === undefined
11042
- ? 'no_result'
11043
- : 'completed',
11044
- result: this.runtimeReceiptOutput(normalized),
11045
- requestInput: request.input,
11046
- execution: toolExecutionMetadataForOutcome({
11047
- kind: 'cache',
11048
- cacheKey: request.cacheKey,
11049
- receiptKey,
11050
- attachedToReceiptKey: receiptKey,
11051
- }),
11052
- })
11053
- : undefined;
11261
+ const result = await this.wrapToolExecutionResult({
11262
+ toolId: input.toolId,
11263
+ status:
11264
+ normalized.output === null || normalized.output === undefined
11265
+ ? 'no_result'
11266
+ : 'completed',
11267
+ result: this.runtimeReceiptOutput(normalized),
11268
+ requestInput: request.input,
11269
+ execution: toolExecutionMetadataForOutcome({
11270
+ kind: 'cache',
11271
+ cacheKey: request.cacheKey,
11272
+ receiptKey: resultReceiptKey,
11273
+ attachedToReceiptKey: storedReceiptKey,
11274
+ }),
11275
+ });
11054
11276
  return {
11055
- resultReceiptKey: receiptKey,
11277
+ resultReceiptKey,
11056
11278
  status: normalized.status,
11057
- ...(result !== undefined ? { result } : {}),
11058
- ...(normalized.status === 'failed'
11059
- ? {
11060
- error: runtimeReceiptFailureError(
11061
- normalized,
11062
- 'Durable tool call failed',
11063
- this.currentToolErrorSchemaVersion,
11064
- ),
11065
- }
11066
- : {}),
11067
- ...(normalized.leaseId &&
11068
- (normalized.leaseOwnerRunId ?? normalized.runId)?.trim()
11069
- ? {
11070
- lease: {
11071
- leaseId: normalized.leaseId,
11072
- ownerRunId: (normalized.leaseOwnerRunId ??
11073
- normalized.runId)!.trim(),
11074
- },
11075
- }
11076
- : {}),
11077
- ...(normalized.leaseExpiresAt
11078
- ? { leaseExpiresAt: normalized.leaseExpiresAt }
11079
- : {}),
11080
- ...(normalized.claimState ? { claimState: normalized.claimState } : {}),
11279
+ result,
11081
11280
  };
11082
11281
  };
11083
11282
  const recordsFor = async (
@@ -11086,10 +11285,22 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
11086
11285
  ): Promise<ToolCallReceiptRecord[]> =>
11087
11286
  (
11088
11287
  await Promise.all(
11089
- receiptKeys.map(
11090
- async (receiptKey) =>
11091
- await asRecord(receiptKey, receipts.get(receiptKey)),
11092
- ),
11288
+ receiptKeys.map(async (receiptKey) => {
11289
+ const request = input.requestForReceipt.get(receiptKey);
11290
+ const candidates = [
11291
+ receiptKey,
11292
+ ...(request?.legacyReceiptKeys ?? []),
11293
+ ];
11294
+ for (const candidate of candidates) {
11295
+ const record = await asRecord(
11296
+ receiptKey,
11297
+ candidate,
11298
+ receipts.get(candidate),
11299
+ );
11300
+ if (record) return record;
11301
+ }
11302
+ return null;
11303
+ }),
11093
11304
  )
11094
11305
  ).filter((record): record is ToolCallReceiptRecord => record !== null);
11095
11306
 
@@ -11099,83 +11310,17 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
11099
11310
  async () =>
11100
11311
  await recordsFor(
11101
11312
  resultReceiptKeys,
11102
- await this.getRuntimeStepReceipts(resultReceiptKeys),
11313
+ await this.getRuntimeStepReceipts(
11314
+ [...new Set(
11315
+ resultReceiptKeys.flatMap((receiptKey) => [
11316
+ receiptKey,
11317
+ ...(input.requestForReceipt.get(receiptKey)
11318
+ ?.legacyReceiptKeys ?? []),
11319
+ ]),
11320
+ )],
11321
+ ),
11103
11322
  ),
11104
11323
  ),
11105
- claim: async ({ receipts }) => {
11106
- const claims = new Map<string, RuntimeStepReceipt>();
11107
- const groups = new Map<string, Array<(typeof receipts)[number]>>();
11108
- for (const receipt of receipts) {
11109
- const groupKey = [
11110
- receipt.reclaimRunning ? 'reclaim' : 'ordinary',
11111
- receipt.force ? 'force' : 'cached',
11112
- receipt.forceFailedRefresh ? 'failed-force' : 'failed-cached',
11113
- ].join(':');
11114
- const group = groups.get(groupKey) ?? [];
11115
- group.push(receipt);
11116
- groups.set(groupKey, group);
11117
- }
11118
- for (const group of groups.values()) {
11119
- const first = group[0]!;
11120
- const claimed = await this.claimRuntimeStepReceipts(
11121
- group.map((receipt) => receipt.resultReceiptKey),
11122
- this.currentReceiptOwnerRunId,
11123
- first.reclaimRunning,
11124
- first.force,
11125
- first.forceFailedRefresh,
11126
- );
11127
- for (const [key, receipt] of claimed) claims.set(key, receipt);
11128
- }
11129
- const safeFailedKeys = receipts.flatMap((receipt) => {
11130
- const claimed = claims.get(receipt.resultReceiptKey);
11131
- return claimed && this.canRetryWithoutProviderDispatch(claimed)
11132
- ? [receipt.resultReceiptKey]
11133
- : [];
11134
- });
11135
- if (safeFailedKeys.length > 0) {
11136
- const refreshed = await this.claimRuntimeStepReceipts(
11137
- safeFailedKeys,
11138
- this.currentReceiptOwnerRunId,
11139
- false,
11140
- false,
11141
- true,
11142
- );
11143
- for (const [key, receipt] of refreshed) claims.set(key, receipt);
11144
- }
11145
- return await recordsFor(
11146
- receipts.map((receipt) => receipt.resultReceiptKey),
11147
- claims,
11148
- );
11149
- },
11150
- heartbeat: async ({ receipts }) => {
11151
- if (!this.#options.heartbeatRuntimeStepReceipts) {
11152
- return await recordsFor(
11153
- receipts.map((receipt) => receipt.resultReceiptKey),
11154
- await this.getRuntimeStepReceipts(
11155
- receipts.map((receipt) => receipt.resultReceiptKey),
11156
- ),
11157
- );
11158
- }
11159
- const response = await this.#options.heartbeatRuntimeStepReceipts({
11160
- runId: this.currentReceiptOwnerRunId,
11161
- runAttempt: this.currentRunAttempt,
11162
- keys: receipts.map((receipt) => receipt.resultReceiptKey),
11163
- leaseIds: receipts.map((receipt) => receipt.lease.leaseId),
11164
- });
11165
- const byKey = new Map<string, RuntimeStepReceipt>();
11166
- for (let index = 0; index < receipts.length; index += 1) {
11167
- const request = receipts[index]!;
11168
- const normalized = this.normalizeRuntimeStepReceipt(
11169
- request.resultReceiptKey,
11170
- response[index],
11171
- );
11172
- if (normalized) byKey.set(request.resultReceiptKey, normalized);
11173
- }
11174
- return await recordsFor(
11175
- receipts.map((receipt) => receipt.resultReceiptKey),
11176
- byKey,
11177
- );
11178
- },
11179
11324
  complete: async ({ receipts }) => {
11180
11325
  const completed = await this.completeRuntimeStepReceipts(
11181
11326
  receipts.map((receipt) => ({
@@ -11190,33 +11335,13 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
11190
11335
  completed,
11191
11336
  );
11192
11337
  },
11193
- fail: async ({ receipts }) => {
11194
- const failed = await this.failRuntimeStepReceipts(
11195
- receipts.map((receipt) => ({
11196
- key: receipt.resultReceiptKey,
11197
- runId: this.currentReceiptOwnerRunId,
11198
- leaseId: receipt.lease.leaseId,
11199
- error: this.formatRuntimeError(receipt.error),
11200
- errorPayload: serializeToolExecutionFailure(receipt.error),
11201
- failureKind: runtimeReceiptFailureKindForError(receipt.error),
11202
- })),
11203
- );
11204
- return await recordsFor(
11205
- receipts.map((receipt) => receipt.resultReceiptKey),
11206
- failed,
11207
- );
11208
- },
11209
11338
  };
11210
11339
  }
11211
11340
 
11212
11341
  /**
11213
- * The scalar map path used to duplicate the receipt state machine in
11214
- * `executeBatchedToolCalls`: one loop claimed/reclaimed/followed rows and a
11215
- * later loop heartbeated and completed them. That made it too easy for the
11216
- * Tool Call boundary to be only cosmetic. This is the narrow replacement
11217
- * slice: cacheable members of a *non-native* map hand their whole receipt
11218
- * lifecycle to the cohort Module, while this Context remains only the
11219
- * transport/presentation Adapter.
11342
+ * Cacheable scalar map members share completed-result reads and successful
11343
+ * writes through the cohort Module. This Context only transports and
11344
+ * presents results.
11220
11345
  *
11221
11346
  * Do not widen this to physical native batches or always-fresh calls. A
11222
11347
  * native batch owns a different aggregate operation receipt, and
@@ -11318,7 +11443,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
11318
11443
  const resolve = async (
11319
11444
  request: ToolCallRequest,
11320
11445
  result: ToolExecuteResult,
11321
- source: 'cache' | 'in_flight' | 'owner',
11446
+ source: 'cache' | 'owner',
11322
11447
  ): Promise<void> => {
11323
11448
  const receiptKey = request.receiptKey!;
11324
11449
  const finalResult = cloneToolExecuteResultWithExecution(
@@ -11353,180 +11478,111 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
11353
11478
  };
11354
11479
  const reject = async (request: ToolCallRequest, error: unknown) => {
11355
11480
  await this.rejectToolCall(toolId, request, error, {
11356
- // ToolCallReceiptCohort is the one failure writer. Re-entering the
11357
- // legacy writer here would race its member terminal transition.
11481
+ // Failures are never cache entries, including failed local misses.
11358
11482
  persistReceiptFailure: false,
11359
11483
  });
11360
11484
  };
11361
11485
 
11362
- try {
11363
- const handled = new Set<string>();
11364
- const followOrRecover: Promise<void>[] = [];
11365
- const collectReadyMembers = (): Array<{
11486
+ const handled = new Set<string>();
11487
+ const followerResults: Promise<void>[] = [];
11488
+ const collectReadyMembers = (): Array<{
11489
+ request: ToolCallRequest;
11490
+ receipts: ToolResultReceipts;
11491
+ }> => {
11492
+ const owned: Array<{
11366
11493
  request: ToolCallRequest;
11367
11494
  receipts: ToolResultReceipts;
11368
- }> => {
11369
- const owned: Array<{
11370
- request: ToolCallRequest;
11371
- receipts: ToolResultReceipts;
11372
- }> = [];
11373
- for (const request of requests) {
11374
- if (handled.has(request.callId)) continue;
11375
- const member = cohort.member(request.callId);
11376
- if (member.kind === 'recovering') continue;
11377
- handled.add(request.callId);
11378
- if (member.kind === 'completed') {
11379
- followOrRecover.push(
11380
- resolve(request, member.result, member.source),
11381
- );
11382
- } else if (member.kind === 'failed') {
11383
- followOrRecover.push(reject(request, member.error));
11384
- } else if (member.kind === 'following') {
11385
- followOrRecover.push(
11386
- member.result.then(
11387
- async (result) => await resolve(request, result, 'in_flight'),
11388
- async (error: unknown) => await reject(request, error),
11389
- ),
11390
- );
11391
- } else {
11392
- request.receiptLeaseId = member.lease.leaseId;
11393
- request.receiptLeaseExpiresAt = member.leaseExpiresAt ?? null;
11394
- owned.push({ request, receipts: member.receipts });
11395
- }
11495
+ }> = [];
11496
+ for (const request of requests) {
11497
+ if (handled.has(request.callId)) continue;
11498
+ const member = cohort.member(request.callId);
11499
+ handled.add(request.callId);
11500
+ if (member.kind === 'completed') {
11501
+ followerResults.push(resolve(request, member.result, member.source));
11502
+ } else if (member.kind === 'following') {
11503
+ followerResults.push(
11504
+ member.result.then(
11505
+ async (result) => await resolve(request, result, 'owner'),
11506
+ async (error: unknown) => await reject(request, error),
11507
+ ),
11508
+ );
11509
+ } else {
11510
+ owned.push({ request, receipts: member.receipts });
11396
11511
  }
11397
- return owned;
11398
- };
11512
+ }
11513
+ return owned;
11514
+ };
11399
11515
 
11400
- const width = Math.min(
11401
- this.governor.policy.concurrency.toolCalls,
11402
- Math.max(
11403
- 1,
11404
- await this.resourceGovernor.suggestedToolParallelism(
11405
- toolId,
11406
- this.governor.policy.concurrency.toolCalls,
11407
- ),
11516
+ const width = Math.min(
11517
+ this.governor.policy.concurrency.toolCalls,
11518
+ Math.max(
11519
+ 1,
11520
+ await this.resourceGovernor.suggestedToolParallelism(
11521
+ toolId,
11522
+ this.governor.policy.concurrency.toolCalls,
11408
11523
  ),
11409
- );
11410
- const dispatchOwned = async (
11411
- owned: Array<{
11412
- request: ToolCallRequest;
11413
- receipts: ToolResultReceipts;
11414
- }>,
11415
- ) =>
11416
- await dispatchBoundedSettled(
11417
- owned,
11418
- width,
11419
- async ({ request, receipts }) => {
11420
- if (this.persistenceLatch.tripped) {
11421
- const error = new RuntimePersistenceCircuitOpenError(
11422
- this.persistenceLatch,
11423
- );
11424
- await cohort.fail([{ memberId: request.callId, error }]);
11425
- throw error;
11426
- }
11427
- let releaseToolSlot: () => void = () => undefined;
11428
- try {
11429
- const result = await this.executeMappedToolCallThroughModule({
11430
- request,
11431
- dispatchToolId: toolId,
11432
- dispatchPayload: request.input,
11433
- resultReceiptKey: request.receiptKey!,
11434
- providerOperationKey:
11435
- request.providerIdempotencyKeyBase ?? request.receiptKey!,
11436
- resultReceipts: receipts,
11437
- apiOptions: {
11438
- beforeProviderCall: () => {
11439
- if (cohortLeaseLost) throw cohortLeaseLost;
11440
- heartbeat.assertOwned();
11441
- },
11442
- heartbeatReceipt: async () => {
11443
- try {
11444
- const outcome = await cohort.heartbeat();
11445
- if (outcome === 'terminal') {
11446
- throw new ToolCallReceiptLeaseLostError(
11447
- request.receiptKey!,
11448
- );
11449
- }
11450
- } catch (error) {
11451
- if (error instanceof ToolCallReceiptLeaseLostError) {
11452
- throw new RuntimeReceiptLeaseLostError({
11453
- receiptKey: request.receiptKey!,
11454
- runId: this.currentReceiptOwnerRunId,
11455
- leaseId:
11456
- request.receiptLeaseId ?? 'missing-cohort-lease',
11457
- });
11458
- }
11459
- throw error;
11460
- }
11461
- },
11462
- playNodeScope: playNodeScopeForToolCallRequest(request),
11463
- durableCallReceiptKey: request.receiptKey,
11464
- executionAuthScopeDigest: request.executionAuthScopeDigest,
11465
- providerIdempotencyReceiptKey:
11466
- request.providerIdempotencyKeyBase ?? request.receiptKey,
11467
- providerIdempotencyKey:
11468
- this.providerIdempotencyKeyForToolCall({
11469
- cacheKey:
11470
- request.providerIdempotencyKeyBase ??
11471
- request.receiptKey!,
11472
- force: request.force === true,
11473
- leaseId: request.receiptLeaseId,
11474
- logicalCallId: request.logicalCallId,
11475
- }),
11476
- receiptLeaseExpiresAt: request.receiptLeaseExpiresAt,
11477
- timeoutMs:
11478
- typeof request.timeoutMs === 'number' &&
11479
- request.timeoutMs > 0
11480
- ? request.timeoutMs
11481
- : undefined,
11482
- retainToolSlot: (release) => {
11483
- releaseToolSlot = release;
11484
- },
11524
+ ),
11525
+ );
11526
+ const dispatchOwned = async (
11527
+ owned: Array<{
11528
+ request: ToolCallRequest;
11529
+ receipts: ToolResultReceipts;
11530
+ }>,
11531
+ ) =>
11532
+ await dispatchBoundedSettled(
11533
+ owned,
11534
+ width,
11535
+ async ({ request, receipts }) => {
11536
+ if (this.persistenceLatch.tripped) {
11537
+ const error = new RuntimePersistenceCircuitOpenError(
11538
+ this.persistenceLatch,
11539
+ );
11540
+ throw error;
11541
+ }
11542
+ let releaseToolSlot: () => void = () => undefined;
11543
+ try {
11544
+ const result = await this.executeMappedToolCallThroughModule({
11545
+ request,
11546
+ dispatchToolId: toolId,
11547
+ dispatchPayload: request.input,
11548
+ resultReceiptKey: request.receiptKey!,
11549
+ providerOperationKey:
11550
+ request.providerIdempotencyKeyBase ?? request.receiptKey!,
11551
+ resultReceipts: receipts,
11552
+ apiOptions: {
11553
+ playNodeScope: playNodeScopeForToolCallRequest(request),
11554
+ durableCallReceiptKey: request.receiptKey,
11555
+ completedResultCache: true,
11556
+ executionAuthScopeDigest: request.executionAuthScopeDigest,
11557
+ timeoutMs:
11558
+ typeof request.timeoutMs === 'number' && request.timeoutMs > 0
11559
+ ? request.timeoutMs
11560
+ : undefined,
11561
+ retainToolSlot: (release) => {
11562
+ releaseToolSlot = release;
11485
11563
  },
11486
- });
11487
- await resolve(request, result, 'owner');
11488
- } catch (error) {
11489
- await reject(request, error);
11490
- } finally {
11491
- releaseToolSlot();
11492
- }
11493
- },
11494
- );
11564
+ },
11565
+ });
11566
+ await resolve(request, result, 'owner');
11567
+ } catch (error) {
11568
+ await reject(request, error);
11569
+ } finally {
11570
+ releaseToolSlot();
11571
+ }
11572
+ },
11573
+ );
11495
11574
 
11496
- // Eager cohort acquisition makes the initial claim available before a
11497
- // foreign receipt's read/reclaim path completes. This preserves map
11498
- // overlap: a broken foreign read must not strand an already-owned
11499
- // provider operation behind it.
11500
- const initialDispatch = dispatchOwned(collectReadyMembers());
11501
- const settlement = cohort.settle();
11502
- const [initialResult, settlementResult] = await Promise.allSettled([
11503
- initialDispatch,
11504
- settlement,
11505
- ]);
11506
- // A recovery can reclaim an expired receipt and turn it into another
11507
- // owned member. Collect its final state after the initial work settles.
11508
- const recoveredDispatch = await dispatchOwned(collectReadyMembers());
11509
- await Promise.allSettled(followOrRecover);
11510
- // dispatchBoundedSettled intentionally captures provider failures; all
11511
- // affected row resolvers have already been rejected above.
11512
- void initialResult;
11513
- void recoveredDispatch;
11514
- if (settlementResult.status === 'rejected') {
11515
- throw settlementResult.reason;
11516
- }
11517
- } finally {
11518
- heartbeat.stop();
11519
- }
11575
+ await dispatchOwned(collectReadyMembers());
11576
+ await Promise.allSettled(followerResults);
11520
11577
  return successful;
11521
11578
  }
11522
11579
 
11523
11580
  /**
11524
11581
  * Executes provider-native batches through the Native Batch Tool Call
11525
- * Module. Unlike the compatibility path below, the Module owns the member
11526
- * receipt cohort from claim through recovery, heartbeat and terminal split
11527
- * fanout. This method is only the runtime Adapter: it compiles provider
11528
- * payloads, invokes transport, and presents already-terminal member values
11529
- * to map rows.
11582
+ * Module. The Module reads exact completed-result cache entries, batches
11583
+ * misses, then publishes each successful split result. This Context only
11584
+ * compiles provider payloads, invokes transport, and presents terminal
11585
+ * member values to map rows.
11530
11586
  */
11531
11587
  private async executeCacheableNativeMapToolCallCohort(input: {
11532
11588
  toolId: string;
@@ -11581,9 +11637,6 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
11581
11637
  batchParallelismCeiling,
11582
11638
  )
11583
11639
  : batchParallelismCeiling;
11584
- const batchProviderActionVersion =
11585
- (await this.#options.getToolActionCacheVersion?.(toolId))?.trim() ??
11586
- 'unversioned';
11587
11640
  const successful: ToolCallRequest[] = [];
11588
11641
 
11589
11642
  const deliver = (request: ToolCallRequest, result: ToolExecuteResult) => {
@@ -11613,7 +11666,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
11613
11666
  };
11614
11667
  const reject = async (request: ToolCallRequest, error: unknown) => {
11615
11668
  await this.rejectToolCall(toolId, request, error, {
11616
- // NativeBatchToolCall owns the durable member failure transition.
11669
+ // Failed provider calls are never cache entries.
11617
11670
  persistReceiptFailure: false,
11618
11671
  });
11619
11672
  };
@@ -11675,15 +11728,13 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
11675
11728
  call.payload,
11676
11729
  {
11677
11730
  durableCallReceiptKey: compatibility.resultReceiptKey,
11731
+ completedResultCache: true,
11678
11732
  playNodeScope: playNodeScopeForToolCallRequest(
11679
11733
  detail.members[0]!,
11680
11734
  call.tool.id,
11681
11735
  ),
11682
11736
  executionAuthScopeDigest:
11683
11737
  detail.members[0]?.executionAuthScopeDigest ?? null,
11684
- providerIdempotencyReceiptKey:
11685
- detail.providerOperationKey,
11686
- providerIdempotencyKey: detail.providerOperationKey,
11687
11738
  receiptLeaseExpiresAt: detail.receiptLeaseExpiresAt,
11688
11739
  timeoutMs: detail.timeoutMs,
11689
11740
  },
@@ -11751,8 +11802,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
11751
11802
  },
11752
11803
  compatibility: {
11753
11804
  resultReceiptKey: request.receiptKey!,
11754
- providerOperationKey:
11755
- request.providerIdempotencyKeyBase ?? request.receiptKey!,
11805
+ providerOperationKey: request.receiptKey!,
11756
11806
  },
11757
11807
  },
11758
11808
  })),
@@ -11775,27 +11825,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
11775
11825
  'Native batch owned members changed provider operation during compilation.',
11776
11826
  );
11777
11827
  }
11778
- const providerOperationKey =
11779
- buildDurableToolPayloadProviderIdempotencyKey({
11780
- orgId: this.#options.orgId,
11781
- toolId: batch.batchOperation,
11782
- executionAuthScopeDigest:
11783
- owned[0]?.executionAuthScopeDigest ?? null,
11784
- providerActionVersion: batchProviderActionVersion,
11785
- payload: compiled.batchPayload,
11786
- forceRefreshIdentities: owned
11787
- .filter((request) => request.force === true)
11788
- .map((request) =>
11789
- this.providerIdempotencyKeyForToolCall({
11790
- cacheKey:
11791
- request.providerIdempotencyKeyBase ??
11792
- request.receiptKey!,
11793
- force: true,
11794
- leaseId: request.receiptLeaseId,
11795
- logicalCallId: request.logicalCallId,
11796
- }),
11797
- ),
11798
- });
11828
+ const providerOperationKey = aggregateReceiptKey;
11799
11829
  dispatchDetails.set(aggregateReceiptKey, {
11800
11830
  members: owned,
11801
11831
  compiled,
@@ -11885,8 +11915,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
11885
11915
  toolResponse: publicToolResponseForBatchedItem(
11886
11916
  aggregate,
11887
11917
  item,
11888
- this.currentToolResponseContract ===
11889
- RAW_V2_TOOL_RESPONSE_CONTRACT,
11918
+ true,
11890
11919
  ),
11891
11920
  requestInput: request.input,
11892
11921
  execution: toolExecutionMetadataForOutcome({
@@ -11899,60 +11928,22 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
11899
11928
  }),
11900
11929
  );
11901
11930
  },
11902
- heartbeat: {
11903
- // Claim replies carry the real expiry. A fixed fallback interval
11904
- // can be too slow for a short-lived gateway lease, so derive the
11905
- // one cohort supervisor from the earliest owned member instead.
11906
- intervalMs: ({ members }) =>
11907
- Math.max(
11908
- 1,
11909
- Math.floor(
11910
- runtimeLeaseHeartbeatIntervalFromExpiry({
11911
- leaseExpiresAt: members.reduce<string | null>(
11912
- (earliest, member) => {
11913
- const expiresAt = member.leaseExpiresAt ?? null;
11914
- if (!expiresAt) return earliest;
11915
- if (!earliest) return expiresAt;
11916
- return Date.parse(expiresAt) < Date.parse(earliest)
11917
- ? expiresAt
11918
- : earliest;
11919
- },
11920
- null,
11921
- ),
11922
- fallbackTtlMs: PLAY_RUNTIME_WORK_RECEIPT_LEASE_TTL_MS,
11923
- }) / 2,
11924
- ),
11925
- ),
11926
- onTransientFailure: (error) => {
11927
- this.runtimeLog(
11928
- `Native Tool Call Receipt Cohort heartbeat transport failed; retrying: ${
11929
- error instanceof Error ? error.message : String(error)
11930
- }`,
11931
- );
11932
- },
11933
- },
11934
11931
  });
11935
11932
  return { batch, result };
11936
11933
  },
11937
11934
  onChunkComplete: async (entries) => {
11938
11935
  for (const entry of entries) {
11939
11936
  if (entry.error !== undefined) {
11940
- // The cohort may have recovered each member through its
11941
- // lease-fenced single-receipt fallback, but that only prevents
11942
- // duplicate provider work. It must not convert a failed bulk
11943
- // persistence boundary into a successful map result: callers need
11944
- // the loud failure so this run can be retried from the durable
11945
- // receipts instead of continuing after an ambiguous bulk write.
11937
+ // A failed batch is not a cache entry. Surface it to every row so
11938
+ // a later run can make a fresh provider attempt.
11946
11939
  for (const request of entry.request.memberRequests) {
11947
11940
  await reject(request, entry.error);
11948
11941
  }
11949
11942
  throw entry.error;
11950
11943
  }
11951
11944
  if (!entry.result) {
11952
- // The NativeBatchToolCall already attempted a lease-fenced failure
11953
- // fanout. Read the actual durable terminal facts so cache hits in
11954
- // the same cohort still reach their rows and a stale failure never
11955
- // causes this Context to write a second terminal transition.
11945
+ // Successful split results must be present in the completed cache
11946
+ // before they are delivered to rows.
11956
11947
  const durable = await store.read({
11957
11948
  resultReceiptKeys: entry.request.memberRequests.map(
11958
11949
  (request) => request.receiptKey!,
@@ -11973,8 +11964,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
11973
11964
  } else {
11974
11965
  await reject(
11975
11966
  request,
11976
- record?.error ??
11977
- entry.error ??
11967
+ entry.error ??
11978
11968
  new Error(
11979
11969
  'Native Tool Call did not reach a terminal receipt.',
11980
11970
  ),
@@ -12198,6 +12188,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
12198
12188
  const cohortRequests = pendingRequests.filter(
12199
12189
  (request) =>
12200
12190
  request.cacheable !== false &&
12191
+ request.requiresExecutionFence !== true &&
12201
12192
  Boolean(request.receiptKey?.trim()),
12202
12193
  );
12203
12194
  if (cohortRequests.length > 0) {
@@ -12230,6 +12221,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
12230
12221
  const cohortRequests = pendingRequests.filter(
12231
12222
  (request) =>
12232
12223
  request.cacheable !== false &&
12224
+ request.requiresExecutionFence !== true &&
12233
12225
  Boolean(request.receiptKey?.trim()),
12234
12226
  );
12235
12227
  if (cohortRequests.length > 0) {
@@ -13056,8 +13048,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
13056
13048
  : publicToolResponseForBatchedItem(
13057
13049
  batchExecution.execution,
13058
13050
  splitResults[index] ?? null,
13059
- this.currentToolResponseContract ===
13060
- RAW_V2_TOOL_RESPONSE_CONTRACT,
13051
+ true,
13061
13052
  ),
13062
13053
  }),
13063
13054
  ),
@@ -13213,6 +13204,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
13213
13204
  ...(request.receiptKey
13214
13205
  ? {
13215
13206
  durableCallReceiptKey: request.receiptKey,
13207
+ completedResultCache: true,
13216
13208
  executionAuthScopeDigest:
13217
13209
  request.executionAuthScopeDigest,
13218
13210
  providerIdempotencyReceiptKey:
@@ -13675,6 +13667,19 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
13675
13667
  .catch(() => null);
13676
13668
  const retrySafeTransientHttp =
13677
13669
  retryPolicy?.retrySafeTransientHttp === true;
13670
+ const requiresExecutionFence =
13671
+ retryPolicy?.requiresExecutionFence === true;
13672
+ const fixtureReplaySafe = this.#options.integrationMode === 'fixture';
13673
+ const readOnlyQuery = isQueryResultDatasetReadRequest(
13674
+ toolId,
13675
+ toolInputSnapshot,
13676
+ );
13677
+ // A completed result cache eliminates repeated successful work; it
13678
+ // does not prove an ambiguous provider write can be replayed. Only
13679
+ // a read-only query or an action which explicitly declares transient
13680
+ // retries safe may issue another physical request after dispatch.
13681
+ const ambiguousTransportReplayAllowed =
13682
+ readOnlyQuery || retrySafeTransientHttp;
13678
13683
  const invocationOrgId = this.#options.orgId?.trim() ?? '';
13679
13684
  const durableCallReceiptKey =
13680
13685
  options?.durableCallReceiptKey?.trim() ||
@@ -13689,26 +13694,25 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
13689
13694
  const executionAuthScopeDigest = options?.executionAuthScopeDigest
13690
13695
  ? options.executionAuthScopeDigest.trim() || null
13691
13696
  : ((await this.resolveToolAuthScopeDigest(toolId))?.trim() ?? null);
13692
- const canonicalOperation = {
13693
- version: 1 as const,
13694
- tool_id: toolId,
13695
- action_version:
13696
- (
13697
- await this.#options.getToolActionCacheVersion?.(toolId)
13698
- )?.trim() || 'unversioned',
13699
- play_scope: this.currentGovernance.currentPlayId,
13700
- output_intent: 'dataset',
13701
- };
13702
- const providerIdempotencyKey =
13703
- options?.providerIdempotencyKey?.trim() || durableCallReceiptKey;
13697
+ const executionRequestNonce = crypto.randomUUID();
13698
+ // This identifies one logical tool call to billing. Physical retry
13699
+ // count remains in request metadata, so a transport retry cannot
13700
+ // reserve or charge the same call twice.
13701
+ const deeplineRequestId = `ctx-tool-${stableDigest(executionRequestNonce).slice(0, 32)}`;
13702
+ // The provider operation has a semantic identity independent of
13703
+ // the response receipt. Its key is supplied by the caller that owns
13704
+ // that compatibility contract; never substitute request/billing
13705
+ // attribution for it.
13704
13706
  const providerIdempotencyReceiptKey =
13705
- options?.providerIdempotencyReceiptKey?.trim() ||
13706
- durableCallReceiptKey;
13707
- // Correlation identity is stable across every transport retry,
13708
- // including calls without a durable receipt.
13709
- const deeplineRequestId = providerIdempotencyKey
13710
- ? `ctx-tool-${stableDigest(providerIdempotencyKey).slice(0, 32)}`
13711
- : `ctx-tool-${crypto.randomUUID()}`;
13707
+ options?.providerIdempotencyReceiptKey?.trim() || null;
13708
+ const providerExecutionFenceKey = requiresExecutionFence
13709
+ ? options?.providerIdempotencyKey?.trim() || null
13710
+ : null;
13711
+ if (requiresExecutionFence && !providerExecutionFenceKey) {
13712
+ throw new Error(
13713
+ `Tool ${toolId} requires a provider execution fence but no provider operation identity was supplied.`,
13714
+ );
13715
+ }
13712
13716
  // Every runtime provider call carries at least a tool-scoped
13713
13717
  // attribution. Callers that know the execution location (row-scoped
13714
13718
  // map cells, batched owners) supply the fuller scope.
@@ -13723,16 +13727,6 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
13723
13727
  ...(requestsDurableInvocationFence
13724
13728
  ? { invocation_fence_version: 1 }
13725
13729
  : {}),
13726
- // Canonical provider-operation receipts validate this semantic
13727
- // plan before replay. It is intentionally removed from the
13728
- // legacy byte-receipt fingerprint so mixed runner artifacts
13729
- // retain their existing provider-once behavior.
13730
- ...(requestsDurableInvocationFence
13731
- ? { canonical_operation: canonicalOperation }
13732
- : {}),
13733
- // The worker-owned receipt coordinates this physical durable
13734
- // call. Provider idempotency is carried separately below so
13735
- // exact-payload reuse never replaces batch receipt ownership.
13736
13730
  ...(durableCallReceiptKey
13737
13731
  ? {
13738
13732
  durable_call_receipt_key: durableCallReceiptKey,
@@ -13744,10 +13738,15 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
13744
13738
  : {}),
13745
13739
  }
13746
13740
  : {}),
13747
- ...(providerIdempotencyReceiptKey
13741
+ ...(providerExecutionFenceKey
13748
13742
  ? {
13749
- provider_idempotency_receipt_key:
13750
- providerIdempotencyReceiptKey,
13743
+ provider_idempotency_key: providerExecutionFenceKey,
13744
+ ...(providerIdempotencyReceiptKey
13745
+ ? {
13746
+ provider_idempotency_receipt_key:
13747
+ providerIdempotencyReceiptKey,
13748
+ }
13749
+ : {}),
13751
13750
  }
13752
13751
  : {}),
13753
13752
  ...(options?.customerDbDataset
@@ -13770,9 +13769,6 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
13770
13769
  : {}),
13771
13770
  }
13772
13771
  : {}),
13773
- ...(providerIdempotencyKey
13774
- ? { provider_idempotency_key: providerIdempotencyKey }
13775
- : {}),
13776
13772
  ...(playNodeScope
13777
13773
  ? { play_node_scope: playNodeScopeToWire(playNodeScope) }
13778
13774
  : {}),
@@ -13789,6 +13785,8 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
13789
13785
  elapsedMs: number;
13790
13786
  requestId: string | null;
13791
13787
  aborted: boolean;
13788
+ /** The ingress rejected the request before any provider dispatch. */
13789
+ knownNotDispatched?: boolean;
13792
13790
  }): Promise<void> => {
13793
13791
  transportAttempt += 1;
13794
13792
  const diagnostic = describeTransportError(input.error);
@@ -13804,7 +13802,13 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
13804
13802
  error: diagnostic,
13805
13803
  })}`,
13806
13804
  );
13807
- if (transportAttempt < TOOL_EXECUTE_TRANSPORT_MAX_ATTEMPTS) {
13805
+ if (
13806
+ transportAttempt < TOOL_EXECUTE_TRANSPORT_MAX_ATTEMPTS &&
13807
+ (input.knownNotDispatched === true ||
13808
+ ambiguousTransportReplayAllowed ||
13809
+ fixtureReplaySafe)
13810
+ ) {
13811
+ invocationAttempt += 1;
13808
13812
  await this.governor.chargeBudget('retry');
13809
13813
  const retryAfterMs =
13810
13814
  TOOL_RETRY_AFTER_FALLBACK_MS * transportAttempt;
@@ -14033,17 +14037,17 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
14033
14037
  'Content-Type': 'application/json',
14034
14038
  Authorization: `Bearer ${this.#options.executorToken}`,
14035
14039
  [EXECUTE_RESPONSE_CONTRACT_HEADER]:
14036
- this.currentToolResponseContract,
14040
+ RAW_V2_TOOL_RESPONSE_CONTRACT,
14037
14041
  [EXECUTE_RESPONSE_INTENT_HEADER]: 'dataset',
14038
14042
  [EXECUTE_TOOL_METADATA_HEADER]: 'true',
14039
14043
  [TOOL_EXECUTION_ERROR_SCHEMA_HEADER]: String(
14040
14044
  toolErrorSchemaVersion,
14041
14045
  ),
14042
14046
  'x-deepline-request-id': deeplineRequestId,
14043
- ...(providerIdempotencyKey
14047
+ ...(providerExecutionFenceKey
14044
14048
  ? {
14045
14049
  'x-deepline-idempotency-key':
14046
- providerIdempotencyKey,
14050
+ providerExecutionFenceKey,
14047
14051
  }
14048
14052
  : {}),
14049
14053
  ...protectionHeaders,
@@ -14145,40 +14149,15 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
14145
14149
  (response === null ||
14146
14150
  error instanceof ToolExecuteResponseBodyTransportError ||
14147
14151
  error instanceof ToolExecuteInvalidJsonError);
14148
- const hasDurableInvocationIdentity = Boolean(
14149
- durableCallReceiptKey &&
14150
- providerIdempotencyKey &&
14151
- executionAuthScopeDigest,
14152
- );
14153
14152
  // A post-header body deadline aborts the fetch controller solely
14154
14153
  // to stop consuming that response. It must not also cancel the
14155
14154
  // independent gateway health check that establishes whether the
14156
14155
  // committed response can be replayed safely. Preserve genuine
14157
14156
  // caller/runtime cancellation by dropping the signal only when
14158
14157
  // this exact body deadline caused the abort.
14159
- const durableFenceVerificationSignal =
14160
- abortController?.signal.aborted === true &&
14161
- abortController.signal.reason instanceof
14162
- ToolExecuteResponseBodyTimeoutError
14163
- ? undefined
14164
- : abortController?.signal;
14165
- const durableInvocationFenceVerified =
14166
- hasDurableInvocationIdentity &&
14167
- requestsDurableInvocationFence &&
14168
- (this.#options.durableInvocationFence === true ||
14169
- (await this.#options
14170
- .verifyDurableInvocationFence?.(
14171
- durableFenceVerificationSignal,
14172
- )
14173
- .catch(() => false)) === true);
14174
- // Fixture execution has no provider-side effect. Replaying the
14175
- // same stable request after our own transport fails is always
14176
- // safe, even when this runner lacks a live-provider fence.
14177
- const fixtureReplaySafe =
14178
- this.#options.integrationMode === 'fixture';
14179
14158
  const transportReplaySafe =
14180
14159
  !ambiguousDispatchedFailure ||
14181
- durableInvocationFenceVerified ||
14160
+ ambiguousTransportReplayAllowed ||
14182
14161
  fixtureReplaySafe;
14183
14162
  if (!transportReplaySafe) {
14184
14163
  const diagnostic = describeTransportError(transportError);
@@ -14271,6 +14250,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
14271
14250
  elapsedMs: providerCallElapsedMs ?? 0,
14272
14251
  requestId: deeplineRequestId,
14273
14252
  aborted: false,
14253
+ knownNotDispatched: true,
14274
14254
  });
14275
14255
  continue;
14276
14256
  }
@@ -14308,13 +14288,13 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
14308
14288
  failure.backpressureDelayMs,
14309
14289
  );
14310
14290
  }
14311
- if (failure.shouldRetry) {
14312
- if (
14313
- failure.reason !== 'gateway_invocation_in_progress' &&
14314
- failure.reason !== 'concurrency_backpressure'
14315
- ) {
14316
- invocationAttempt += 1;
14317
- }
14291
+ if (
14292
+ failure.shouldRetry &&
14293
+ (ambiguousTransportReplayAllowed ||
14294
+ retrySafeTransientHttp ||
14295
+ fixtureReplaySafe)
14296
+ ) {
14297
+ invocationAttempt += 1;
14318
14298
  if (failure.chargeRetryBudget) {
14319
14299
  await this.governor.chargeBudget('retry');
14320
14300
  }