deepline 0.1.182 → 0.1.184

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/dist/bundling-sources/apps/play-runner-workers/src/child-play-await.ts +203 -6
  2. package/dist/bundling-sources/apps/play-runner-workers/src/child-play-submit.ts +8 -1
  3. package/dist/bundling-sources/apps/play-runner-workers/src/coordinator-entry.ts +639 -109
  4. package/dist/bundling-sources/apps/play-runner-workers/src/dedup-do.ts +3 -2
  5. package/dist/bundling-sources/apps/play-runner-workers/src/entry.ts +1752 -1899
  6. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/harness-receipt-store.ts +116 -0
  7. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/map-chunk-plan.ts +542 -78
  8. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/receipts.ts +292 -11
  9. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/run-work-dispatcher.ts +519 -0
  10. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/tool-dispatch.ts +2381 -0
  11. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/tool-receipts.ts +18 -5
  12. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/work-budget.ts +130 -0
  13. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/worker-platform-budget.ts +39 -0
  14. package/dist/bundling-sources/apps/play-runner-workers/src/workflow-retry-state.ts +2 -0
  15. package/dist/bundling-sources/sdk/src/client.ts +41 -0
  16. package/dist/bundling-sources/sdk/src/http.ts +23 -8
  17. package/dist/bundling-sources/sdk/src/plays/harness-stub.ts +56 -3
  18. package/dist/bundling-sources/sdk/src/release.ts +2 -2
  19. package/dist/bundling-sources/sdk/src/types.ts +2 -0
  20. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +133 -10
  21. package/dist/bundling-sources/shared_libs/play-runtime/auth-scope-resolver.ts +92 -0
  22. package/dist/bundling-sources/shared_libs/play-runtime/batch-runtime.ts +4 -4
  23. package/dist/bundling-sources/shared_libs/play-runtime/batching-types.ts +8 -0
  24. package/dist/bundling-sources/shared_libs/play-runtime/child-run-id.ts +101 -0
  25. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +1612 -287
  26. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +105 -6
  27. package/dist/bundling-sources/shared_libs/play-runtime/dedup-backend.ts +0 -0
  28. package/dist/bundling-sources/shared_libs/play-runtime/default-batch-strategies.ts +1 -0
  29. package/dist/bundling-sources/shared_libs/play-runtime/durability-store.ts +4 -0
  30. package/dist/bundling-sources/shared_libs/play-runtime/durable-call-cache.ts +116 -16
  31. package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +186 -20
  32. package/dist/bundling-sources/shared_libs/play-runtime/dynamic-worker-version.ts +2 -0
  33. package/dist/bundling-sources/shared_libs/play-runtime/execution-ledger-store.ts +133 -0
  34. package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +245 -0
  35. package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +5 -5
  36. package/dist/bundling-sources/shared_libs/play-runtime/governor/policy.ts +14 -0
  37. package/dist/bundling-sources/shared_libs/play-runtime/lease-policy.ts +52 -0
  38. package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +41 -0
  39. package/dist/bundling-sources/shared_libs/play-runtime/providers.ts +3 -2
  40. package/dist/bundling-sources/shared_libs/play-runtime/receipt-sql.ts +124 -0
  41. package/dist/bundling-sources/shared_libs/play-runtime/receipt-status.ts +6 -2
  42. package/dist/bundling-sources/shared_libs/play-runtime/row-isolation.ts +48 -2
  43. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +158 -15
  44. package/dist/bundling-sources/shared_libs/play-runtime/run-terminal-source.ts +45 -0
  45. package/dist/bundling-sources/shared_libs/play-runtime/runtime-actions.ts +9 -0
  46. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api-paths.ts +21 -0
  47. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +2038 -262
  48. package/dist/bundling-sources/shared_libs/play-runtime/runtime-contract.ts +42 -0
  49. package/dist/bundling-sources/shared_libs/play-runtime/runtime-sheet-attempt-state-machine.ts +183 -0
  50. package/dist/bundling-sources/shared_libs/play-runtime/runtime-sheet-errors.ts +88 -0
  51. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +8 -1
  52. package/dist/bundling-sources/shared_libs/play-runtime/sheet-attempt-sql.ts +103 -0
  53. package/dist/bundling-sources/shared_libs/play-runtime/suspension.ts +19 -1
  54. package/dist/bundling-sources/shared_libs/play-runtime/test-runtime-seams.ts +343 -0
  55. package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +43 -0
  56. package/dist/bundling-sources/shared_libs/play-runtime/tool-http-errors.ts +11 -0
  57. package/dist/bundling-sources/shared_libs/play-runtime/work-receipt-state-machine.ts +437 -0
  58. package/dist/bundling-sources/shared_libs/play-runtime/work-receipts.ts +83 -6
  59. package/dist/bundling-sources/shared_libs/plays/bundling/index.ts +28 -11
  60. package/dist/bundling-sources/shared_libs/plays/static-pipeline.ts +38 -6
  61. package/dist/bundling-sources/shared_libs/security/safe-outbound-fetch.ts +24 -17
  62. package/dist/cli/index.js +76 -17
  63. package/dist/cli/index.mjs +76 -17
  64. package/dist/index.d.mts +2 -0
  65. package/dist/index.d.ts +2 -0
  66. package/dist/index.js +63 -9
  67. package/dist/index.mjs +63 -9
  68. package/dist/plays/bundle-play-file.mjs +20 -6
  69. package/package.json +1 -1
@@ -23,12 +23,28 @@ import {
23
23
  } from './batch-runtime';
24
24
  import type { PlayQueueHint } from './governor/rate-state-backend';
25
25
  import type { MapRowOutcome } from './durability-store';
26
+ import type { WorkReceiptFailureKind } from './work-receipts';
26
27
  import {
27
28
  completedMapRowOutcome,
28
29
  failedMapRowOutcome,
29
30
  mapRowOutcomeRuntimeFields,
30
31
  resolveMapRowOutcomeKey,
31
32
  } from './map-row-outcome';
33
+ import { RuntimeSheetRowsBlockedError } from './runtime-sheet-errors';
34
+ import { buildChildRunId } from './child-run-id';
35
+ import type { PlayCallGovernanceSnapshot } from './scheduler-backend';
36
+ import {
37
+ RUNTIME_SCHEDULER_SCHEMA_OVERRIDE_HEADER,
38
+ SYNTHETIC_RUN_HEADER,
39
+ } from './coordinator-headers';
40
+ import {
41
+ PLAY_RUNTIME_API_COMPAT_PATH,
42
+ PLAY_RUNTIME_CHILD_EXECUTOR_TOKEN_COMPAT_PATH,
43
+ } from './runtime-api-paths';
44
+ export {
45
+ RuntimeSheetRowsBlockedError,
46
+ type RuntimeSheetBlockedRowDetail,
47
+ } from './runtime-sheet-errors';
32
48
  import {
33
49
  createDefaultGovernanceSnapshot,
34
50
  createPlayExecutionGovernor,
@@ -64,30 +80,38 @@ import {
64
80
  TOOL_EXECUTE_TRANSPORT_MAX_ATTEMPTS,
65
81
  classifyToolExecuteHttpFailure,
66
82
  createToolExecuteHttpFailureAttemptTracker,
83
+ parseToolExecuteAuthScopeChangedError,
84
+ ToolExecuteAuthScopeChangedError,
67
85
  } from './tool-execute-retry-policy';
68
86
  import {
69
87
  buildDurableCtxCallCacheKey,
88
+ buildDurableRunPlaySemanticKey,
89
+ buildDurableToolAggregateProviderIdempotencyKey,
90
+ buildDurableToolAggregateReceiptKey,
70
91
  buildDurableToolCallAuthScopeDigest,
71
92
  buildDurableToolCallCacheKey,
93
+ buildDurableToolProviderIdempotencyKey,
94
+ buildDurableToolReceiptPrefix,
72
95
  } from './durable-call-cache';
73
96
  import {
74
- QUERY_RESULT_DATASET_PAGE_SIZE,
75
- isCustomerDbDatasetTool,
76
- isQueryResultDatasetReadRequest,
77
- isQueryResultDatasetTool,
78
- } from './query-result-dataset';
79
- import {
97
+ RuntimeReceiptLeaseLostError,
80
98
  RuntimeReceiptWaitTimeoutError,
81
99
  executeWithDurableRuntimeReceipt,
82
100
  resolveRuntimeToolReceiptWaitMaxAttempts,
101
+ runtimeReceiptFailureKindForError,
83
102
  runtimeReceiptOutput as durableRuntimeReceiptOutput,
84
103
  waitForCompletedRuntimeReceipt,
85
104
  type DurableReceiptExecutionStore,
86
105
  } from './durable-receipt-execution';
106
+ import {
107
+ QUERY_RESULT_DATASET_PAGE_SIZE,
108
+ isCustomerDbDatasetTool,
109
+ isQueryResultDatasetReadRequest,
110
+ isQueryResultDatasetTool,
111
+ } from './query-result-dataset';
87
112
  import { isRowIsolationExemptError } from './row-isolation';
88
113
  import {
89
114
  createRuntimePersistenceLatch,
90
- PROVIDER_DISPATCH_WAVE_SIZE,
91
115
  RuntimePersistenceCircuitOpenError,
92
116
  tripRuntimePersistenceLatch,
93
117
  type RuntimePersistenceLatch,
@@ -119,7 +143,9 @@ import { DISALLOWED_RUN_JAVASCRIPT_TOOL_MESSAGE } from './runtime-constraints';
119
143
  import {
120
144
  PlayExecutionSuspendedError,
121
145
  PlayRowExecutionSuspendedError,
146
+ isPlayExecutionSuspendedError,
122
147
  isPlayRowExecutionSuspendedError,
148
+ type PlayExecutionSuspension,
123
149
  } from './suspension';
124
150
  import {
125
151
  createSecretRedactionContext,
@@ -171,6 +197,7 @@ import {
171
197
  type StepProgramDatasetColumnInput,
172
198
  type StepProgramDatasetOptions,
173
199
  } from './step-program-dataset-builder';
200
+ import { readRuntimeSheetDatasetRows } from './runtime-api';
174
201
 
175
202
  /**
176
203
  * SECURITY: AsyncLocalStorage is per async execution, not a cross-run cache.
@@ -207,9 +234,118 @@ const TOOL_RETRY_HEARTBEAT_INTERVAL_MS = 30_000;
207
234
  const DEEPLINEAGENT_TOOL_RUNTIME_TIMEOUT_MS = 15 * 60 * 1000;
208
235
  const FETCH_TRANSPORT_MAX_ATTEMPTS = 3;
209
236
  const FETCH_TRANSPORT_RETRY_DELAY_MS = 100;
237
+ const NODE_RUNTIME_MAP_VISIBILITY_MAX_ATTEMPTS = 100;
238
+ const NODE_RUNTIME_MAP_VISIBILITY_RETRY_MS = 25;
210
239
  type SafeFetchModule = typeof import('@shared_libs/security/safe-fetch');
211
240
  let safeFetchModule: Promise<SafeFetchModule> | null = null;
212
241
 
242
+ export async function waitForNodeRuntimeMapRowsVisible(input: {
243
+ mapName: string;
244
+ tableNamespace: string;
245
+ runId?: string | null;
246
+ expectedRows: number;
247
+ updatedRows: number;
248
+ readVisibleRowCount: () => Promise<number>;
249
+ sleep?: (ms: number) => Promise<void>;
250
+ log?: (line: string) => void;
251
+ }): Promise<number> {
252
+ if (input.expectedRows <= 0) return 0;
253
+
254
+ const sleep =
255
+ input.sleep ??
256
+ ((ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)));
257
+ let lastVisibleRows = -1;
258
+ for (
259
+ let attempt = 1;
260
+ attempt <= NODE_RUNTIME_MAP_VISIBILITY_MAX_ATTEMPTS;
261
+ attempt += 1
262
+ ) {
263
+ lastVisibleRows = await input.readVisibleRowCount();
264
+ if (lastVisibleRows >= input.expectedRows) {
265
+ if (attempt > 1) {
266
+ input.log?.(
267
+ `Runtime sheet visibility barrier satisfied ctx.dataset("${input.mapName}") ` +
268
+ `after ${attempt} read(s): visible ${lastVisibleRows}/${input.expectedRows}; ` +
269
+ `wrote ${input.updatedRows}/${input.expectedRows}; run ${input.runId ?? 'unknown'}.`,
270
+ );
271
+ }
272
+ return lastVisibleRows;
273
+ }
274
+
275
+ if (attempt === 1) {
276
+ input.log?.(
277
+ `Runtime sheet visibility barrier waiting ctx.dataset("${input.mapName}"): ` +
278
+ `visible ${lastVisibleRows}/${input.expectedRows}; ` +
279
+ `wrote ${input.updatedRows}/${input.expectedRows}; run ${input.runId ?? 'unknown'}.`,
280
+ );
281
+ }
282
+ if (attempt < NODE_RUNTIME_MAP_VISIBILITY_MAX_ATTEMPTS) {
283
+ await sleep(NODE_RUNTIME_MAP_VISIBILITY_RETRY_MS);
284
+ }
285
+ }
286
+
287
+ throw new Error(
288
+ `Runtime sheet visibility mismatch for ctx.dataset("${input.mapName}"): ` +
289
+ `expected ${input.expectedRows} terminal visible row(s), saw ${lastVisibleRows}; ` +
290
+ `write reported ${input.updatedRows}; run ${input.runId ?? 'unknown'}.`,
291
+ );
292
+ }
293
+
294
+ export async function reconcileNodeRuntimeMapResultsWithPersistedSheet(input: {
295
+ mapName: string;
296
+ tableNamespace: string;
297
+ runId?: string | null;
298
+ expectedRows: number;
299
+ currentRows: Record<string, unknown>[];
300
+ failedRowCount: number;
301
+ readPersistedRows: (input: {
302
+ limit: number;
303
+ offset: number;
304
+ rowMode: 'all';
305
+ }) => Promise<Record<string, unknown>[]>;
306
+ log?: (line: string) => void;
307
+ }): Promise<Record<string, unknown>[]> {
308
+ if (
309
+ input.failedRowCount > 0 ||
310
+ input.expectedRows <= 0 ||
311
+ input.currentRows.length >= input.expectedRows
312
+ ) {
313
+ return input.currentRows;
314
+ }
315
+
316
+ const persistedRows: Record<string, unknown>[] = [];
317
+ let offset = 0;
318
+ while (persistedRows.length < input.expectedRows) {
319
+ const page = await input.readPersistedRows({
320
+ limit: Math.min(10_000, input.expectedRows - persistedRows.length),
321
+ offset,
322
+ rowMode: 'all',
323
+ });
324
+ if (page.length === 0) break;
325
+ persistedRows.push(...page);
326
+ offset += page.length;
327
+ }
328
+
329
+ if (persistedRows.length < input.expectedRows) {
330
+ throw new Error(
331
+ `Runtime sheet finalization mismatch for ctx.dataset("${input.mapName}"): ` +
332
+ `expected ${input.expectedRows} terminal persisted row(s), saw ${persistedRows.length}; ` +
333
+ `in-memory Node map results reported ${input.currentRows.length}; run ${input.runId ?? 'unknown'}.`,
334
+ );
335
+ }
336
+
337
+ if (persistedRows.length > input.currentRows.length) {
338
+ input.log?.(
339
+ `Runtime sheet finalization reconciled ctx.dataset("${input.mapName}") ` +
340
+ `from ${input.currentRows.length}/${input.expectedRows} in-memory Node row(s) ` +
341
+ `to ${persistedRows.length} terminal persisted row(s).`,
342
+ );
343
+ return persistedRows;
344
+ }
345
+
346
+ return input.currentRows;
347
+ }
348
+
213
349
  export function resolveToolRuntimeTimeoutMs(
214
350
  toolId: string,
215
351
  requestedTimeoutMs?: number,
@@ -304,6 +440,9 @@ function finitePositiveInteger(value: unknown): number | null {
304
440
 
305
441
  type ToolExecutionApiOptions = {
306
442
  timeoutMs?: number;
443
+ durableCallReceiptKey?: string | null;
444
+ executionAuthScopeDigest?: string | null;
445
+ providerIdempotencyKey?: string | null;
307
446
  customerDbDataset?: {
308
447
  limit: number;
309
448
  offset: number;
@@ -347,6 +486,16 @@ function persistableMapRowBytes(row: PersistableMapRow): number {
347
486
  return JSON.stringify(row).length;
348
487
  }
349
488
 
489
+ function failedMapRowLogLabel(row: PersistableMapRow): string {
490
+ const rowId = row.data?.row_id;
491
+ if (typeof rowId === 'string' && rowId.trim()) return rowId.trim();
492
+ if (row.key?.trim()) return row.key.trim();
493
+ if (typeof row.inputIndex === 'number' && Number.isFinite(row.inputIndex)) {
494
+ return `input:${Math.floor(row.inputIndex)}`;
495
+ }
496
+ return 'unknown';
497
+ }
498
+
350
499
  type FieldMapRunResult = {
351
500
  completedRows: PersistableMapRow[];
352
501
  failedRows: PersistableMapRow[];
@@ -508,9 +657,7 @@ function durableCtxKey(input: {
508
657
  staleAfterSeconds?: number | null;
509
658
  }): string {
510
659
  if (input.operation === 'tool') {
511
- throw new Error(
512
- 'Durable ctx key is only for non-tool call boundaries; tools use buildDurableToolCallCacheKey.',
513
- );
660
+ throw new Error('Tool calls use tool receipt keys.');
514
661
  }
515
662
  return buildDurableCtxCallCacheKey({
516
663
  orgId: input.orgId,
@@ -756,6 +903,13 @@ export class PlayContextImpl {
756
903
  eventKey: string;
757
904
  timeoutMs: number;
758
905
  }> = [];
906
+ private pendingChildPlayBoundaries: Array<{
907
+ boundaryId: string;
908
+ childRunId: string;
909
+ childPlayName: string;
910
+ }> = [];
911
+ private activeScheduledChildPlayCalls = 0;
912
+ private scheduledChildPlayQuiescenceWaiters: Array<() => void> = [];
759
913
  private processedRowCount = 0;
760
914
  private sleepBoundaryIndex = 0;
761
915
  private fetchCallIndex = 0;
@@ -891,13 +1045,18 @@ export class PlayContextImpl {
891
1045
  options.workflowId ||
892
1046
  'anonymous-play';
893
1047
  const rootRunId = options.runId ?? options.workflowId ?? 'anonymous-run';
1048
+ if (options.requireSharedRateState && !options.rateState) {
1049
+ throw new Error(
1050
+ 'Shared rate-state backend is required for this Node runtime substrate.',
1051
+ );
1052
+ }
894
1053
  this.governor = createPlayExecutionGovernor({
895
1054
  adapter: 'cjs_node20',
896
1055
  scope: {
897
1056
  orgId: options.orgId ?? 'org',
898
1057
  rootRunId: options.runId ?? options.workflowId ?? 'run',
899
1058
  },
900
- rateState: new InMemoryRateStateBackend(),
1059
+ rateState: options.rateState ?? new InMemoryRateStateBackend(),
901
1060
  resolvePacing: createPacingResolver(options.getToolQueueHints),
902
1061
  // A root run keeps depth 0 (its first child play is depth 1) but still
903
1062
  // seeds its own play id into the ancestry/currentPlayId so the cycle guard
@@ -923,6 +1082,263 @@ export class PlayContextImpl {
923
1082
  return `${this.governance.currentRunId}:${localId}`;
924
1083
  }
925
1084
 
1085
+ private shouldLaunchChildPlaysThroughScheduler(): boolean {
1086
+ return (
1087
+ this.#options.requireSharedRateState === true &&
1088
+ this.#options.durableBoundaries === true
1089
+ );
1090
+ }
1091
+
1092
+ private childPlayBoundaryId(childRunId: string): string {
1093
+ return this.durableBoundaryId(`runPlay:${childRunId}`);
1094
+ }
1095
+
1096
+ private recordPendingChildPlayBoundary(input: {
1097
+ boundaryId: string;
1098
+ childRunId: string;
1099
+ childPlayName: string;
1100
+ }): void {
1101
+ this.pendingChildPlayBoundaries.push(input);
1102
+ }
1103
+
1104
+ private beginScheduledChildPlayCall(): { release: () => void } {
1105
+ this.activeScheduledChildPlayCalls += 1;
1106
+ let released = false;
1107
+ return {
1108
+ release: () => {
1109
+ if (released) return;
1110
+ released = true;
1111
+ this.activeScheduledChildPlayCalls = Math.max(
1112
+ 0,
1113
+ this.activeScheduledChildPlayCalls - 1,
1114
+ );
1115
+ if (this.activeScheduledChildPlayCalls !== 0) return;
1116
+ for (const resolve of this.scheduledChildPlayQuiescenceWaiters.splice(
1117
+ 0,
1118
+ )) {
1119
+ resolve();
1120
+ }
1121
+ },
1122
+ };
1123
+ }
1124
+
1125
+ private async waitForScheduledChildPlayQuiescence(): Promise<void> {
1126
+ if (this.activeScheduledChildPlayCalls === 0) return;
1127
+ await new Promise<void>((resolve) => {
1128
+ this.scheduledChildPlayQuiescenceWaiters.push(resolve);
1129
+ });
1130
+ }
1131
+
1132
+ private consumePendingChildPlaySuspension(): PlayExecutionSuspension | null {
1133
+ if (this.pendingChildPlayBoundaries.length === 0) return null;
1134
+ const boundaries = [
1135
+ ...new Map(
1136
+ this.pendingChildPlayBoundaries.map((boundary) => [
1137
+ boundary.boundaryId,
1138
+ boundary,
1139
+ ]),
1140
+ ).values(),
1141
+ ];
1142
+ this.pendingChildPlayBoundaries = [];
1143
+ if (boundaries.length === 1) {
1144
+ const boundary = boundaries[0]!;
1145
+ return {
1146
+ kind: 'child_play',
1147
+ boundaryId: boundary.boundaryId,
1148
+ childRunId: boundary.childRunId,
1149
+ childPlayName: boundary.childPlayName,
1150
+ };
1151
+ }
1152
+ return {
1153
+ kind: 'child_play_batch',
1154
+ boundaries,
1155
+ };
1156
+ }
1157
+
1158
+ private childPlayBoundaryOutput<TOutput>(input: {
1159
+ boundaryId: string;
1160
+ childRunId: string;
1161
+ childPlayName: string;
1162
+ }): { found: true; output: TOutput } | { found: false } {
1163
+ const existing = this.checkpoint.resolvedBoundaries?.[input.boundaryId];
1164
+ if (!existing || existing.kind !== 'child_play') return { found: false };
1165
+ if (existing.childRunId !== input.childRunId) return { found: false };
1166
+ if (existing.status === 'completed') {
1167
+ return { found: true, output: existing.output as TOutput };
1168
+ }
1169
+ const message =
1170
+ typeof existing.error === 'string' && existing.error.trim()
1171
+ ? existing.error.trim()
1172
+ : `Child play ${input.childPlayName} (${input.childRunId}) ${existing.status}.`;
1173
+ throw new Error(message);
1174
+ }
1175
+
1176
+ private recordPlayCallStep(input: {
1177
+ playId: string;
1178
+ description?: string | null;
1179
+ nestedSteps?: PlayStep[];
1180
+ }): void {
1181
+ const step = {
1182
+ type: 'play_call' as const,
1183
+ playId: input.playId,
1184
+ nestedSteps: input.nestedSteps ?? [],
1185
+ description: normalizeStepDescription(input.description ?? undefined),
1186
+ };
1187
+ if (this.activeDatasetStep) {
1188
+ this.activeDatasetStep.substeps.push(step);
1189
+ } else {
1190
+ this.steps.push(step);
1191
+ }
1192
+ }
1193
+
1194
+ private runtimeApiHeaders(): Record<string, string> {
1195
+ if (!this.#options.executorToken?.trim()) {
1196
+ throw new Error(
1197
+ 'ctx.runPlay scheduled child launch requires an executor token.',
1198
+ );
1199
+ }
1200
+ return {
1201
+ Authorization: `Bearer ${this.#options.executorToken.trim()}`,
1202
+ 'Content-Type': 'application/json',
1203
+ ...(this.#options.runtimeSchedulerSchema?.trim()
1204
+ ? {
1205
+ [RUNTIME_SCHEDULER_SCHEMA_OVERRIDE_HEADER]:
1206
+ this.#options.runtimeSchedulerSchema.trim(),
1207
+ [SYNTHETIC_RUN_HEADER]: '1',
1208
+ }
1209
+ : {}),
1210
+ ...(this.#options.vercelProtectionBypassToken?.trim()
1211
+ ? {
1212
+ 'x-vercel-protection-bypass':
1213
+ this.#options.vercelProtectionBypassToken.trim(),
1214
+ }
1215
+ : {}),
1216
+ };
1217
+ }
1218
+
1219
+ private runtimeApiBaseUrl(): string {
1220
+ const baseUrl = this.#options.baseUrl?.trim();
1221
+ if (!baseUrl) {
1222
+ throw new Error('ctx.runPlay scheduled child launch requires baseUrl.');
1223
+ }
1224
+ return baseUrl.replace(/\/$/, '');
1225
+ }
1226
+
1227
+ private async launchScheduledChildPlay(input: {
1228
+ key: string;
1229
+ childRunId: string;
1230
+ childPlayName: string;
1231
+ childGovernance: GovernanceSnapshot;
1232
+ childInput: Record<string, unknown>;
1233
+ description?: string | null;
1234
+ }): Promise<void> {
1235
+ const parentPlayName =
1236
+ this.governance.currentPlayId ||
1237
+ this.#options.playName ||
1238
+ this.#options.playId ||
1239
+ 'anonymous-play';
1240
+ const governance: PlayCallGovernanceSnapshot = {
1241
+ rootRunId: this.governance.rootRunId,
1242
+ parentRunId: this.governance.currentRunId,
1243
+ parentPlayName,
1244
+ key: input.key,
1245
+ ancestryPlayIds: [...this.governance.ancestryPlayIds],
1246
+ callDepth: input.childGovernance.callDepth,
1247
+ playCallCount: input.childGovernance.playCallCount,
1248
+ toolCallCount: input.childGovernance.toolCallCount,
1249
+ retryCount: input.childGovernance.retryCount,
1250
+ descendantCount: input.childGovernance.descendantCount,
1251
+ waterfallStepExecutions: input.childGovernance.waterfallStepExecutions,
1252
+ };
1253
+ const response = await fetch(
1254
+ `${this.runtimeApiBaseUrl()}/api/v2/plays/run`,
1255
+ {
1256
+ method: 'POST',
1257
+ headers: this.runtimeApiHeaders(),
1258
+ body: JSON.stringify({
1259
+ name: input.childPlayName,
1260
+ workflowId: input.childRunId,
1261
+ input: input.childInput,
1262
+ profile: 'hatchet',
1263
+ waitForCompletionMs: 0,
1264
+ internalRunPlay: {
1265
+ ...governance,
1266
+ description: input.description ?? null,
1267
+ },
1268
+ }),
1269
+ },
1270
+ );
1271
+ if (!response.ok) {
1272
+ const body = (await response.json().catch(() => null)) as {
1273
+ error?: unknown;
1274
+ } | null;
1275
+ const message =
1276
+ typeof body?.error === 'string' && body.error.trim()
1277
+ ? body.error.trim()
1278
+ : response.statusText;
1279
+ throw new Error(
1280
+ `ctx.runPlay(${input.key}) failed to launch child play "${input.childPlayName}": ${message} (status ${response.status}).`,
1281
+ );
1282
+ }
1283
+ }
1284
+
1285
+ private async readScheduledChildTerminal<TOutput>(input: {
1286
+ childRunId: string;
1287
+ childPlayName: string;
1288
+ }): Promise<
1289
+ | { status: 'completed'; output: TOutput }
1290
+ | { status: 'failed' | 'cancelled'; error: string }
1291
+ | null
1292
+ > {
1293
+ const response = await fetch(
1294
+ `${this.runtimeApiBaseUrl()}${PLAY_RUNTIME_API_COMPAT_PATH}`,
1295
+ {
1296
+ method: 'POST',
1297
+ headers: this.runtimeApiHeaders(),
1298
+ body: JSON.stringify({
1299
+ action: 'read_child_run_terminal_snapshot',
1300
+ parentRunId: this.governance.currentRunId,
1301
+ childRunId: input.childRunId,
1302
+ childPlayName: input.childPlayName,
1303
+ }),
1304
+ },
1305
+ );
1306
+ const body = (await response.json().catch(() => null)) as {
1307
+ state?: {
1308
+ data?: {
1309
+ status?: unknown;
1310
+ result?: unknown;
1311
+ error?: unknown;
1312
+ };
1313
+ };
1314
+ error?: unknown;
1315
+ } | null;
1316
+ if (!response.ok) {
1317
+ const message =
1318
+ typeof body?.error === 'string' && body.error.trim()
1319
+ ? body.error.trim()
1320
+ : response.statusText;
1321
+ throw new Error(
1322
+ `ctx.runPlay failed to read child terminal snapshot for "${input.childPlayName}": ${message} (status ${response.status}).`,
1323
+ );
1324
+ }
1325
+ const data = body?.state?.data;
1326
+ const status = String(data?.status ?? '').toLowerCase();
1327
+ if (status === 'completed') {
1328
+ return { status, output: data?.result as TOutput };
1329
+ }
1330
+ if (status === 'failed' || status === 'cancelled') {
1331
+ return {
1332
+ status,
1333
+ error:
1334
+ typeof data?.error === 'string' && data.error.trim()
1335
+ ? data.error.trim()
1336
+ : `Child play ${input.childPlayName} (${input.childRunId}) ${status}.`,
1337
+ };
1338
+ }
1339
+ return null;
1340
+ }
1341
+
926
1342
  private emitScopedRowUpdate(
927
1343
  key: string | null,
928
1344
  tableNamespace: string | null,
@@ -990,7 +1406,7 @@ export class PlayContextImpl {
990
1406
  this.#options.runId
991
1407
  ) {
992
1408
  const response = await fetch(
993
- `${this.#options.baseUrl.replace(/\/$/, '')}/api/v2/plays/internal/runtime`,
1409
+ `${this.#options.baseUrl.replace(/\/$/, '')}${PLAY_RUNTIME_API_COMPAT_PATH}`,
994
1410
  {
995
1411
  method: 'POST',
996
1412
  headers: {
@@ -1043,8 +1459,7 @@ export class PlayContextImpl {
1043
1459
  artifactTableNamespace: string;
1044
1460
  mapNodeId?: string | null;
1045
1461
  explicitKey?:
1046
- | ((row: Record<string, unknown>, index: number) => string)
1047
- | null;
1462
+ ((row: Record<string, unknown>, index: number) => string) | null;
1048
1463
  }): MapExecutionScope {
1049
1464
  const mapInvocationId = `${input.logicalNamespace}:${this.mapInvocationIndex}`;
1050
1465
  this.mapInvocationIndex += 1;
@@ -1102,6 +1517,7 @@ export class PlayContextImpl {
1102
1517
  runId: string,
1103
1518
  reclaimRunning = false,
1104
1519
  forceRefresh = false,
1520
+ forceFailedRefresh = false,
1105
1521
  ): Promise<RuntimeStepReceipt | null> {
1106
1522
  if (!this.#options.claimRuntimeStepReceipt) {
1107
1523
  return null;
@@ -1109,8 +1525,11 @@ export class PlayContextImpl {
1109
1525
  const claimed = await this.#options.claimRuntimeStepReceipt({
1110
1526
  key,
1111
1527
  runId,
1528
+ runAttempt: this.currentRunAttempt,
1529
+ leaseAware: true,
1112
1530
  ...(reclaimRunning ? { reclaimRunning: true } : {}),
1113
1531
  ...(forceRefresh ? { forceRefresh: true } : {}),
1532
+ ...(forceFailedRefresh ? { forceFailedRefresh: true } : {}),
1114
1533
  });
1115
1534
  if (!claimed || typeof claimed.key !== 'string' || !claimed.key.trim()) {
1116
1535
  return null;
@@ -1126,6 +1545,7 @@ export class PlayContextImpl {
1126
1545
  key: string,
1127
1546
  runId: string,
1128
1547
  output: unknown | null,
1548
+ leaseId?: string | null,
1129
1549
  ): Promise<RuntimeStepReceipt | null> {
1130
1550
  assertNoSecretTaint(output, 'ctx.step receipt output');
1131
1551
  if (!this.#options.completeRuntimeStepReceipt) {
@@ -1134,6 +1554,8 @@ export class PlayContextImpl {
1134
1554
  const completed = await this.#options.completeRuntimeStepReceipt({
1135
1555
  key,
1136
1556
  runId,
1557
+ runAttempt: this.currentRunAttempt,
1558
+ ...(leaseId ? { leaseId } : {}),
1137
1559
  output,
1138
1560
  });
1139
1561
  if (
@@ -1150,10 +1572,167 @@ export class PlayContextImpl {
1150
1572
  };
1151
1573
  }
1152
1574
 
1575
+ private async releaseRuntimeStepReceipt(
1576
+ key: string,
1577
+ runId: string,
1578
+ leaseId?: string | null,
1579
+ ): Promise<RuntimeStepReceipt | null> {
1580
+ if (!this.#options.releaseRuntimeStepReceipt) {
1581
+ return null;
1582
+ }
1583
+ const released = await this.#options.releaseRuntimeStepReceipt({
1584
+ key,
1585
+ runId,
1586
+ runAttempt: this.currentRunAttempt,
1587
+ ...(leaseId ? { leaseId } : {}),
1588
+ });
1589
+ if (!released || typeof released.key !== 'string' || !released.key.trim()) {
1590
+ return null;
1591
+ }
1592
+ return {
1593
+ ...released,
1594
+ key: released.key.trim(),
1595
+ runId: released.runId ?? null,
1596
+ };
1597
+ }
1598
+
1599
+ private async heartbeatRuntimeStepReceipt(
1600
+ key: string,
1601
+ runId: string,
1602
+ leaseId: string,
1603
+ ): Promise<RuntimeStepReceipt | null> {
1604
+ if (!this.#options.heartbeatRuntimeStepReceipts) {
1605
+ return null;
1606
+ }
1607
+ const [receipt] = await this.#options.heartbeatRuntimeStepReceipts({
1608
+ runId,
1609
+ runAttempt: this.currentRunAttempt,
1610
+ leaseId,
1611
+ keys: [key],
1612
+ });
1613
+ if (!receipt || typeof receipt.key !== 'string' || !receipt.key.trim()) {
1614
+ return null;
1615
+ }
1616
+ return {
1617
+ ...receipt,
1618
+ key: receipt.key.trim(),
1619
+ runId: receipt.runId ?? null,
1620
+ };
1621
+ }
1622
+
1623
+ private async assertRuntimeToolReceiptOwnership(
1624
+ requests: ToolCallRequest[],
1625
+ ): Promise<void> {
1626
+ const targets = requests.flatMap((request) => {
1627
+ const receiptKey = request.receiptKey?.trim() || null;
1628
+ if (!receiptKey) return [];
1629
+ const leaseId = request.receiptLeaseId?.trim() || null;
1630
+ if (!leaseId) return [];
1631
+ return [{ receiptKey, leaseId }];
1632
+ });
1633
+ if (targets.length === 0) return;
1634
+
1635
+ const byLeaseId = new Map<string, string[]>();
1636
+ for (const target of targets) {
1637
+ const keys = byLeaseId.get(target.leaseId) ?? [];
1638
+ keys.push(target.receiptKey);
1639
+ byLeaseId.set(target.leaseId, keys);
1640
+ }
1641
+
1642
+ if (this.#options.heartbeatRuntimeStepReceipts) {
1643
+ for (const [leaseId, keys] of byLeaseId) {
1644
+ const receipts = await this.#options.heartbeatRuntimeStepReceipts({
1645
+ runId: this.currentRunId,
1646
+ runAttempt: this.currentRunAttempt,
1647
+ leaseId,
1648
+ keys,
1649
+ });
1650
+ this.assertRuntimeToolReceiptHeartbeatResult(keys, leaseId, receipts);
1651
+ }
1652
+ return;
1653
+ }
1654
+
1655
+ if (
1656
+ !this.#options.getRuntimeStepReceipt &&
1657
+ !this.#options.getRuntimeStepReceipts
1658
+ ) {
1659
+ throw new RuntimeReceiptLeaseLostError({
1660
+ receiptKey: targets[0]?.receiptKey ?? 'unknown',
1661
+ runId: this.currentRunId,
1662
+ leaseId: targets[0]?.leaseId ?? 'unknown',
1663
+ });
1664
+ }
1665
+
1666
+ const latest = await this.getRuntimeStepReceipts(
1667
+ targets.map((target) => target.receiptKey),
1668
+ );
1669
+ for (const target of targets) {
1670
+ const receipt = latest.get(target.receiptKey);
1671
+ if (!this.runtimeToolReceiptStillOwned(receipt, target.leaseId)) {
1672
+ throw new RuntimeReceiptLeaseLostError({
1673
+ receiptKey: target.receiptKey,
1674
+ runId: this.currentRunId,
1675
+ leaseId: target.leaseId,
1676
+ });
1677
+ }
1678
+ }
1679
+ }
1680
+
1681
+ private assertRuntimeToolReceiptHeartbeatResult(
1682
+ keys: string[],
1683
+ leaseId: string,
1684
+ receipts: Array<RuntimeStepReceipt | null>,
1685
+ ): void {
1686
+ for (let index = 0; index < keys.length; index += 1) {
1687
+ const receipt = this.normalizeRuntimeStepReceipt(
1688
+ keys[index] ?? '',
1689
+ receipts[index],
1690
+ );
1691
+ if (!this.runtimeToolReceiptStillOwned(receipt, leaseId)) {
1692
+ throw new RuntimeReceiptLeaseLostError({
1693
+ receiptKey: keys[index] ?? 'unknown',
1694
+ runId: this.currentRunId,
1695
+ leaseId,
1696
+ });
1697
+ }
1698
+ }
1699
+ }
1700
+
1701
+ private runtimeToolReceiptStillOwned(
1702
+ receipt: RuntimeStepReceipt | null | undefined,
1703
+ leaseId: string,
1704
+ ): boolean {
1705
+ if (!receipt || receipt.status !== 'running') return false;
1706
+ if (receipt.leaseId !== leaseId) return false;
1707
+ const ownerRunId = receipt.leaseOwnerRunId ?? receipt.runId ?? null;
1708
+ return !ownerRunId || ownerRunId === this.currentRunId;
1709
+ }
1710
+
1711
+ private runtimeToolReceiptStillOwnedByCurrentAttempt(
1712
+ receipt: RuntimeStepReceipt | null | undefined,
1713
+ leaseId: string | null | undefined,
1714
+ ): boolean {
1715
+ if (!receipt || receipt.status !== 'running') return false;
1716
+ if (leaseId?.trim()) {
1717
+ if (receipt.leaseId !== leaseId.trim()) return false;
1718
+ } else if (receipt.leaseId != null) {
1719
+ return false;
1720
+ }
1721
+ const ownerRunId = receipt.leaseOwnerRunId ?? receipt.runId ?? null;
1722
+ if (ownerRunId !== this.currentRunId) return false;
1723
+ const ownerAttempt =
1724
+ typeof receipt.leaseOwnerAttempt === 'number'
1725
+ ? receipt.leaseOwnerAttempt
1726
+ : 0;
1727
+ return ownerAttempt === this.currentRunAttempt;
1728
+ }
1729
+
1153
1730
  private async failRuntimeStepReceipt(
1154
1731
  key: string,
1155
1732
  runId: string,
1156
1733
  error: string,
1734
+ leaseId?: string | null,
1735
+ failureKind?: WorkReceiptFailureKind | null,
1157
1736
  ): Promise<RuntimeStepReceipt | null> {
1158
1737
  if (!this.#options.failRuntimeStepReceipt) {
1159
1738
  return null;
@@ -1161,6 +1740,9 @@ export class PlayContextImpl {
1161
1740
  const failed = await this.#options.failRuntimeStepReceipt({
1162
1741
  key,
1163
1742
  runId,
1743
+ runAttempt: this.currentRunAttempt,
1744
+ ...(leaseId ? { leaseId } : {}),
1745
+ ...(failureKind ? { failureKind } : {}),
1164
1746
  error,
1165
1747
  });
1166
1748
  if (!failed || typeof failed.key !== 'string' || !failed.key.trim()) {
@@ -1220,6 +1802,7 @@ export class PlayContextImpl {
1220
1802
  runId: string,
1221
1803
  reclaimRunning = false,
1222
1804
  forceRefresh = false,
1805
+ forceFailedRefresh = false,
1223
1806
  ): Promise<Map<string, RuntimeStepReceipt>> {
1224
1807
  const uniqueKeys = [
1225
1808
  ...new Set(keys.map((key) => key.trim()).filter(Boolean)),
@@ -1229,8 +1812,11 @@ export class PlayContextImpl {
1229
1812
  ? await this.#options.claimRuntimeStepReceipts({
1230
1813
  keys: uniqueKeys,
1231
1814
  runId,
1815
+ runAttempt: this.currentRunAttempt,
1816
+ leaseAware: true,
1232
1817
  ...(reclaimRunning ? { reclaimRunning: true } : {}),
1233
1818
  ...(forceRefresh ? { forceRefresh: true } : {}),
1819
+ ...(forceFailedRefresh ? { forceFailedRefresh: true } : {}),
1234
1820
  })
1235
1821
  : await Promise.all(
1236
1822
  uniqueKeys.map((key) =>
@@ -1239,6 +1825,7 @@ export class PlayContextImpl {
1239
1825
  runId,
1240
1826
  reclaimRunning,
1241
1827
  forceRefresh,
1828
+ forceFailedRefresh,
1242
1829
  ),
1243
1830
  ),
1244
1831
  );
@@ -1254,26 +1841,44 @@ export class PlayContextImpl {
1254
1841
  }
1255
1842
 
1256
1843
  private async completeRuntimeStepReceipts(
1257
- receipts: Array<{ key: string; runId: string; output: unknown | null }>,
1844
+ receipts: Array<{
1845
+ key: string;
1846
+ runId: string;
1847
+ runAttempt?: number | null;
1848
+ output: unknown | null;
1849
+ leaseId?: string | null;
1850
+ }>,
1258
1851
  ): Promise<Map<string, RuntimeStepReceipt>> {
1259
1852
  const normalizedInputs = receipts.filter((receipt) => receipt.key.trim());
1260
1853
  if (normalizedInputs.length === 0) return new Map();
1261
1854
  for (const receipt of normalizedInputs) {
1262
1855
  assertNoSecretTaint(receipt.output, 'ctx.tool receipt output');
1263
1856
  }
1264
- const completed = this.#options.completeRuntimeStepReceipts
1265
- ? await this.#options.completeRuntimeStepReceipts({
1266
- receipts: normalizedInputs,
1267
- })
1268
- : await Promise.all(
1269
- normalizedInputs.map((receipt) =>
1270
- this.completeRuntimeStepReceipt(
1271
- receipt.key,
1272
- receipt.runId,
1273
- receipt.output,
1857
+ let completed: Array<RuntimeStepReceipt | null>;
1858
+ try {
1859
+ completed = this.#options.completeRuntimeStepReceipts
1860
+ ? await this.#options.completeRuntimeStepReceipts({
1861
+ receipts: normalizedInputs.map((receipt) => ({
1862
+ ...receipt,
1863
+ runAttempt: receipt.runAttempt ?? this.currentRunAttempt,
1864
+ })),
1865
+ })
1866
+ : await Promise.all(
1867
+ normalizedInputs.map((receipt) =>
1868
+ this.completeRuntimeStepReceipt(
1869
+ receipt.key,
1870
+ receipt.runId,
1871
+ receipt.output,
1872
+ receipt.leaseId,
1873
+ ),
1274
1874
  ),
1275
- ),
1276
- );
1875
+ );
1876
+ } catch (error) {
1877
+ await this.recoverRuntimeStepReceiptCompletionsAfterBulkError(
1878
+ normalizedInputs,
1879
+ );
1880
+ throw error;
1881
+ }
1277
1882
  const byKey = new Map<string, RuntimeStepReceipt>();
1278
1883
  for (let index = 0; index < completed.length; index += 1) {
1279
1884
  const normalized = this.normalizeRuntimeStepReceipt(
@@ -1282,17 +1887,123 @@ export class PlayContextImpl {
1282
1887
  );
1283
1888
  if (normalized) byKey.set(normalized.key, normalized);
1284
1889
  }
1890
+ await this.reconcileMissingRuntimeStepReceiptCompletions(
1891
+ normalizedInputs,
1892
+ byKey,
1893
+ );
1285
1894
  return byKey;
1286
1895
  }
1287
1896
 
1897
+ private async reconcileMissingRuntimeStepReceiptCompletions(
1898
+ receipts: Array<{
1899
+ key: string;
1900
+ runId: string;
1901
+ runAttempt?: number | null;
1902
+ output: unknown | null;
1903
+ leaseId?: string | null;
1904
+ }>,
1905
+ completedByKey: Map<string, RuntimeStepReceipt>,
1906
+ ): Promise<void> {
1907
+ if (
1908
+ receipts.length === completedByKey.size ||
1909
+ (!this.#options.getRuntimeStepReceipt &&
1910
+ !this.#options.getRuntimeStepReceipts)
1911
+ ) {
1912
+ return;
1913
+ }
1914
+ const missingKeys = receipts
1915
+ .map((receipt) => receipt.key.trim())
1916
+ .filter((key) => key && !completedByKey.has(key));
1917
+ if (missingKeys.length === 0) return;
1918
+ const latest = await this.getRuntimeStepReceipts(missingKeys);
1919
+ for (const key of missingKeys) {
1920
+ const recovered = latest.get(key);
1921
+ if (
1922
+ recovered?.status === 'completed' ||
1923
+ recovered?.status === 'skipped'
1924
+ ) {
1925
+ completedByKey.set(key, recovered);
1926
+ }
1927
+ }
1928
+ const recoveredCount = missingKeys.filter((key) =>
1929
+ completedByKey.has(key),
1930
+ ).length;
1931
+ if (recoveredCount > 0) {
1932
+ this.log(
1933
+ `Runtime tool receipt completion response reconciled ${recoveredCount}/${missingKeys.length} missing receipt(s) from durable store read-back.`,
1934
+ );
1935
+ }
1936
+ }
1937
+
1938
+ private async recoverRuntimeStepReceiptCompletionsAfterBulkError(
1939
+ receipts: Array<{
1940
+ key: string;
1941
+ runId: string;
1942
+ runAttempt?: number | null;
1943
+ output: unknown | null;
1944
+ leaseId?: string | null;
1945
+ }>,
1946
+ ): Promise<void> {
1947
+ if (
1948
+ receipts.length === 0 ||
1949
+ (!this.#options.getRuntimeStepReceipt &&
1950
+ !this.#options.getRuntimeStepReceipts)
1951
+ ) {
1952
+ return;
1953
+ }
1954
+ let latest: Map<string, RuntimeStepReceipt>;
1955
+ try {
1956
+ latest = await this.getRuntimeStepReceipts(
1957
+ receipts.map((receipt) => receipt.key),
1958
+ );
1959
+ } catch {
1960
+ return;
1961
+ }
1962
+ await Promise.allSettled(
1963
+ receipts.map(async (receipt) => {
1964
+ const recovered = latest.get(receipt.key);
1965
+ if (
1966
+ recovered?.status === 'completed' ||
1967
+ recovered?.status === 'skipped'
1968
+ ) {
1969
+ return;
1970
+ }
1971
+ if (
1972
+ !this.runtimeToolReceiptStillOwnedByCurrentAttempt(
1973
+ recovered,
1974
+ receipt.leaseId,
1975
+ )
1976
+ ) {
1977
+ return;
1978
+ }
1979
+ await this.completeRuntimeStepReceipt(
1980
+ receipt.key,
1981
+ receipt.runId,
1982
+ receipt.output,
1983
+ receipt.leaseId,
1984
+ );
1985
+ }),
1986
+ );
1987
+ }
1988
+
1288
1989
  private async failRuntimeStepReceipts(
1289
- receipts: Array<{ key: string; runId: string; error: string }>,
1990
+ receipts: Array<{
1991
+ key: string;
1992
+ runId: string;
1993
+ runAttempt?: number | null;
1994
+ error: string;
1995
+ leaseId?: string | null;
1996
+ failureKind?: WorkReceiptFailureKind | null;
1997
+ }>,
1290
1998
  ): Promise<Map<string, RuntimeStepReceipt>> {
1291
1999
  const normalizedInputs = receipts.filter((receipt) => receipt.key.trim());
1292
2000
  if (normalizedInputs.length === 0) return new Map();
1293
2001
  const failed = this.#options.failRuntimeStepReceipts
1294
2002
  ? await this.#options.failRuntimeStepReceipts({
1295
- receipts: normalizedInputs,
2003
+ receipts: normalizedInputs.map((receipt) => ({
2004
+ ...receipt,
2005
+ runAttempt: receipt.runAttempt ?? this.currentRunAttempt,
2006
+ })),
1296
2007
  })
1297
2008
  : await Promise.all(
1298
2009
  normalizedInputs.map((receipt) =>
@@ -1300,6 +2011,8 @@ export class PlayContextImpl {
1300
2011
  receipt.key,
1301
2012
  receipt.runId,
1302
2013
  receipt.error,
2014
+ receipt.leaseId,
2015
+ receipt.failureKind,
1303
2016
  ),
1304
2017
  ),
1305
2018
  );
@@ -1327,45 +2040,98 @@ export class PlayContextImpl {
1327
2040
  );
1328
2041
  }
1329
2042
 
1330
- private async durableToolCallCacheKey(input: {
2043
+ private async durableToolCallCacheKeyForScope(input: {
1331
2044
  toolId: string;
1332
2045
  requestInput: Record<string, unknown>;
2046
+ executionAuthScopeDigest?: string | null;
1333
2047
  staleAfterSeconds?: number | null;
2048
+ playLocalScope?: string | null;
1334
2049
  }): Promise<string> {
1335
2050
  const providerActionVersion =
1336
2051
  (await this.#options.getToolActionCacheVersion?.(input.toolId))?.trim() ??
1337
2052
  '';
2053
+ const executionAuthScopeDigest =
2054
+ input.executionAuthScopeDigest?.trim() ||
2055
+ (await this.resolveToolAuthScopeDigest(input.toolId))?.trim() ||
2056
+ null;
1338
2057
  return buildDurableToolCallCacheKey({
1339
2058
  orgId: this.#options.orgId,
1340
- playId: this.governance.currentPlayId,
2059
+ playLocalScope: input.playLocalScope,
1341
2060
  toolId: input.toolId,
1342
2061
  requestInput: input.requestInput,
1343
2062
  authScopeDigest: buildDurableToolCallAuthScopeDigest({
1344
2063
  orgId: this.#options.orgId,
1345
- userEmail: this.#options.userEmail,
1346
2064
  toolId: input.toolId,
2065
+ executionAuthScopeDigest,
1347
2066
  }),
1348
2067
  providerActionVersion,
1349
2068
  staleAfterSeconds: input.staleAfterSeconds,
1350
2069
  });
1351
2070
  }
1352
2071
 
2072
+ private async durableToolCallCacheKey(input: {
2073
+ toolId: string;
2074
+ requestInput: Record<string, unknown>;
2075
+ executionAuthScopeDigest?: string | null;
2076
+ staleAfterSeconds?: number | null;
2077
+ }): Promise<string> {
2078
+ return await this.durableToolCallCacheKeyForScope({
2079
+ ...input,
2080
+ playLocalScope: this.governance.currentPlayId,
2081
+ });
2082
+ }
2083
+
2084
+ private async resolveToolAuthScopeDigest(
2085
+ toolId: string,
2086
+ ): Promise<string | null> {
2087
+ const configured = await this.#options.getToolAuthScopeDigest?.(toolId);
2088
+ if (typeof configured === 'string' && configured.trim()) {
2089
+ return configured.trim();
2090
+ }
2091
+ return null;
2092
+ }
2093
+
2094
+ private invalidateToolAuthScopeDigest(toolId: string): void {
2095
+ this.#options.invalidateToolAuthScopeDigest?.(toolId);
2096
+ }
2097
+
1353
2098
  private durableReceiptExecutionStore(): DurableReceiptExecutionStore {
1354
2099
  return {
1355
2100
  enabled: Boolean(this.#options.claimRuntimeStepReceipt),
1356
2101
  get: (receiptKey) => this.getRuntimeStepReceipt(receiptKey),
1357
2102
  getMany: (receiptKeys) => this.getRuntimeStepReceipts(receiptKeys),
1358
- claim: (receiptKey, runId, reclaimRunning, forceRefresh) =>
2103
+ claim: (
2104
+ receiptKey,
2105
+ runId,
2106
+ reclaimRunning,
2107
+ forceRefresh,
2108
+ forceFailedRefresh,
2109
+ ) =>
1359
2110
  this.claimRuntimeStepReceipt(
1360
2111
  receiptKey,
1361
2112
  runId,
1362
2113
  reclaimRunning,
1363
2114
  forceRefresh,
2115
+ forceFailedRefresh,
2116
+ ),
2117
+ complete: (receiptKey, runId, output, leaseId) =>
2118
+ this.completeRuntimeStepReceipt(receiptKey, runId, output, leaseId),
2119
+ release: (receiptKey, runId, leaseId) =>
2120
+ this.releaseRuntimeStepReceipt(receiptKey, runId, leaseId),
2121
+ ...(this.#options.heartbeatRuntimeStepReceipts
2122
+ ? {
2123
+ heartbeat: (receiptKey, runId, leaseId) =>
2124
+ this.heartbeatRuntimeStepReceipt(receiptKey, runId, leaseId),
2125
+ }
2126
+ : {}),
2127
+ fail: (receiptKey, runId, error, leaseId, failureKind) =>
2128
+ this.failRuntimeStepReceipt(
2129
+ receiptKey,
2130
+ runId,
2131
+ error,
2132
+ leaseId,
2133
+ failureKind,
1364
2134
  ),
1365
- complete: (receiptKey, runId, output) =>
1366
- this.completeRuntimeStepReceipt(receiptKey, runId, output),
1367
- fail: (receiptKey, runId, error) =>
1368
- this.failRuntimeStepReceipt(receiptKey, runId, error),
1369
2135
  canPersistFailure: Boolean(this.#options.failRuntimeStepReceipt),
1370
2136
  canPersistCompletion: Boolean(this.#options.completeRuntimeStepReceipt),
1371
2137
  };
@@ -1407,7 +2173,8 @@ export class PlayContextImpl {
1407
2173
  source: DurableReceiptRecoverySource,
1408
2174
  ) => T;
1409
2175
  onClaimedResult?: (output: T, receiptKey: string) => T;
1410
- execute: () => Promise<T>;
2176
+ shouldPersistFailure?: (error: unknown) => boolean;
2177
+ execute: (context: { leaseId: string | null }) => Promise<T>;
1411
2178
  },
1412
2179
  ): Promise<T> {
1413
2180
  const receiptKey =
@@ -1436,6 +2203,7 @@ export class PlayContextImpl {
1436
2203
  markSkipped: opts.markSkipped,
1437
2204
  onRecovered: opts.onRecovered,
1438
2205
  onClaimedResult: opts.onClaimedResult,
2206
+ shouldPersistFailure: opts.shouldPersistFailure,
1439
2207
  formatError: (error) => this.formatRuntimeError(error),
1440
2208
  log: (message) => this.log(message),
1441
2209
  execute: opts.execute,
@@ -1446,6 +2214,13 @@ export class PlayContextImpl {
1446
2214
  return this.#options.runId ?? this.governance.currentRunId ?? 'unknown';
1447
2215
  }
1448
2216
 
2217
+ private get currentRunAttempt(): number {
2218
+ const attempt = this.#options.runAttempt;
2219
+ return typeof attempt === 'number' && Number.isFinite(attempt)
2220
+ ? Math.max(0, Math.floor(attempt))
2221
+ : 0;
2222
+ }
2223
+
1449
2224
  private emitScopedFieldMetaUpdate(input: {
1450
2225
  rowId: number;
1451
2226
  key: string | null;
@@ -1659,12 +2434,17 @@ export class PlayContextImpl {
1659
2434
 
1660
2435
  private effectiveToolCallCachePolicy(options?: ToolCallOptions): {
1661
2436
  force: boolean;
2437
+ forceFailedRefresh: boolean;
1662
2438
  staleAfterSeconds?: number | null;
1663
2439
  } {
1664
2440
  return {
1665
2441
  force:
1666
2442
  options?.force === true ||
1667
2443
  this.#options.cachePolicy?.forceToolRefresh === true,
2444
+ forceFailedRefresh:
2445
+ options?.force === true ||
2446
+ this.#options.cachePolicy?.forceToolRefresh === true ||
2447
+ this.#options.cachePolicy?.forceFailedToolRefresh === true,
1668
2448
  staleAfterSeconds: options?.staleAfterSeconds ?? null,
1669
2449
  };
1670
2450
  }
@@ -1685,6 +2465,19 @@ export class PlayContextImpl {
1685
2465
  return this.checkpoint.completedToolBatches[toolId]?.[rowCacheKey];
1686
2466
  }
1687
2467
 
2468
+ private getCachedToolResultCandidate(
2469
+ toolId: string,
2470
+ rowCacheKeys: readonly string[],
2471
+ ): { cacheKey: string; result: ToolBatchResult } | null {
2472
+ for (const rowCacheKey of rowCacheKeys) {
2473
+ const cached = this.getCachedToolResult(toolId, rowCacheKey);
2474
+ if (cached?.done) {
2475
+ return { cacheKey: rowCacheKey, result: cached };
2476
+ }
2477
+ }
2478
+ return null;
2479
+ }
2480
+
1688
2481
  private cacheToolResult(
1689
2482
  toolId: string,
1690
2483
  rowCacheKey: string,
@@ -1914,6 +2707,7 @@ export class PlayContextImpl {
1914
2707
  {
1915
2708
  key: receiptKey,
1916
2709
  runId: this.currentRunId,
2710
+ leaseId: request.receiptLeaseId ?? null,
1917
2711
  output: serializeToolExecuteResult(wrapped),
1918
2712
  },
1919
2713
  ])
@@ -1969,12 +2763,13 @@ export class PlayContextImpl {
1969
2763
  {
1970
2764
  key: receiptKey,
1971
2765
  runId: this.currentRunId,
2766
+ leaseId: entry.request.receiptLeaseId ?? null,
1972
2767
  output: serializeToolExecuteResult(entry.wrapped),
1973
2768
  },
1974
2769
  ]
1975
2770
  : [];
1976
2771
  });
1977
- let completedByKey: Map<string, RuntimeStepReceipt>;
2772
+ let completedByKey = new Map<string, RuntimeStepReceipt>();
1978
2773
  if (receiptInputs.length > 0) {
1979
2774
  try {
1980
2775
  completedByKey = await this.completeRuntimeStepReceipts(receiptInputs);
@@ -1982,10 +2777,15 @@ export class PlayContextImpl {
1982
2777
  // Completion failed AFTER successful (billed) tool calls: trip the
1983
2778
  // breaker so remaining rows stop dispatching new provider calls.
1984
2779
  tripRuntimePersistenceLatch(this.persistenceLatch, receiptError);
2780
+ await Promise.all(
2781
+ wrappedEntries.map((entry) =>
2782
+ this.rejectToolCall(toolId, entry.request, receiptError, {
2783
+ persistReceiptFailure: false,
2784
+ }),
2785
+ ),
2786
+ );
1985
2787
  throw receiptError;
1986
2788
  }
1987
- } else {
1988
- completedByKey = new Map<string, RuntimeStepReceipt>();
1989
2789
  }
1990
2790
  const results: unknown[] = [];
1991
2791
  for (const entry of wrappedEntries) {
@@ -2009,9 +2809,23 @@ export class PlayContextImpl {
2009
2809
  completed: RuntimeStepReceipt | null | undefined,
2010
2810
  ): Promise<unknown> {
2011
2811
  const cacheKey = request.cacheKey;
2812
+ const receiptKey = request.receiptKey?.trim() || null;
2813
+ const canPersistReceiptCompletion = Boolean(
2814
+ this.#options.completeRuntimeStepReceipt ||
2815
+ this.#options.completeRuntimeStepReceipts,
2816
+ );
2817
+ if (
2818
+ receiptKey &&
2819
+ canPersistReceiptCompletion &&
2820
+ completed?.status !== 'completed' &&
2821
+ completed?.status !== 'skipped'
2822
+ ) {
2823
+ throw new Error(
2824
+ `Durable tool call ${receiptKey} lost receipt ownership before completion.`,
2825
+ );
2826
+ }
2012
2827
  const finalWrapped =
2013
- (completed?.status === 'completed' || completed?.status === 'skipped') &&
2014
- completed.runId !== this.currentRunId
2828
+ completed?.status === 'completed' || completed?.status === 'skipped'
2015
2829
  ? await this.wrapToolExecutionResult({
2016
2830
  toolId,
2017
2831
  status:
@@ -2030,8 +2844,8 @@ export class PlayContextImpl {
2030
2844
  : {
2031
2845
  kind: 'cache',
2032
2846
  cacheKey,
2033
- receiptKey: request.receiptKey ?? '',
2034
- attachedToReceiptKey: request.receiptKey,
2847
+ receiptKey: receiptKey ?? '',
2848
+ attachedToReceiptKey: receiptKey,
2035
2849
  },
2036
2850
  ),
2037
2851
  })
@@ -2076,7 +2890,9 @@ export class PlayContextImpl {
2076
2890
  {
2077
2891
  key: receiptKey,
2078
2892
  runId: this.currentRunId,
2893
+ leaseId: request.receiptLeaseId ?? null,
2079
2894
  error: message,
2895
+ failureKind: runtimeReceiptFailureKindForError(error),
2080
2896
  },
2081
2897
  ]);
2082
2898
  } catch (receiptError) {
@@ -2190,8 +3006,7 @@ export class PlayContextImpl {
2190
3006
  _key: string,
2191
3007
  _items: PlayDatasetInput<T>,
2192
3008
  _input?:
2193
- | MapFieldDefinition<T, Record<string, unknown>>
2194
- | RuntimeStepProgram,
3009
+ MapFieldDefinition<T, Record<string, unknown>> | RuntimeStepProgram,
2195
3010
  _options?: DatasetOptions<T>,
2196
3011
  ): never {
2197
3012
  void _key;
@@ -2277,8 +3092,7 @@ export class PlayContextImpl {
2277
3092
 
2278
3093
  const userKeyOption = options?.key;
2279
3094
  let explicitKeyResolver:
2280
- | ((row: Record<string, unknown>, index: number) => string)
2281
- | null = null;
3095
+ ((row: Record<string, unknown>, index: number) => string) | null = null;
2282
3096
  if (userKeyOption !== undefined) {
2283
3097
  explicitKeyResolver = (row, index) => {
2284
3098
  const inputRow = stripFieldOutputs(row);
@@ -2360,6 +3174,7 @@ export class PlayContextImpl {
2360
3174
  this.toOutputRow(item as Record<string, unknown>),
2361
3175
  );
2362
3176
  itemOriginalIndexes = materializedItems.map((_item, index) => index);
3177
+ const mapStartSeedRowsByKey = new Map<string, Record<string, unknown>>();
2363
3178
  if (this.#options.onMapStart) {
2364
3179
  const shouldPassRowKey = explicitKeyResolver != null;
2365
3180
  // toSerializableCsvAliasedRow (not a plain spread): projected CSV
@@ -2381,6 +3196,7 @@ export class PlayContextImpl {
2381
3196
  playName: this.#options.playName,
2382
3197
  playId: this.#options.playId,
2383
3198
  runId: this.#options.runId,
3199
+ executorToken: this.#options.executorToken,
2384
3200
  staticPipeline: this.#options.staticPipeline,
2385
3201
  forceRefresh: this.#options.cachePolicy?.forceToolRefresh === true,
2386
3202
  },
@@ -2388,13 +3204,14 @@ export class PlayContextImpl {
2388
3204
  resolvedTableNamespace = normalizeTableNamespace(
2389
3205
  mapStartResult.tableNamespace,
2390
3206
  );
3207
+ if ((mapStartResult.blockedRows?.length ?? 0) > 0) {
3208
+ throw new RuntimeSheetRowsBlockedError({
3209
+ tableNamespace: resolvedTableNamespace,
3210
+ blockedRows: mapStartResult.blockedRows ?? [],
3211
+ });
3212
+ }
2391
3213
  const persistedRowIdentity = (row: Record<string, unknown>, index = 0) =>
2392
3214
  resolveMapRowOutcomeKey(row) ?? rowIdentity(row, index);
2393
- const pendingKeys = new Set(
2394
- mapStartResult.pendingRows.map((row, index) =>
2395
- persistedRowIdentity(row, index),
2396
- ),
2397
- );
2398
3215
  const pendingRowsByKey = new Map<string, Record<string, unknown>>();
2399
3216
  for (
2400
3217
  let index = 0;
@@ -2405,15 +3222,20 @@ export class PlayContextImpl {
2405
3222
  const rowKey = persistedRowIdentity(row, index);
2406
3223
  if (rowKey) pendingRowsByKey.set(rowKey, row);
2407
3224
  }
2408
- const pendingItems = materializedItems
3225
+ for (const row of mapStartResult.completedRows ?? []) {
3226
+ const rowKey = persistedRowIdentity(row);
3227
+ if (!rowKey) continue;
3228
+ mapStartSeedRowsByKey.set(rowKey, row);
3229
+ }
3230
+ const seededItems = materializedItems
2409
3231
  .map((item, index) => ({
2410
3232
  row: this.toOutputRow(item as Record<string, unknown>),
2411
3233
  index,
2412
3234
  }))
2413
- .filter(({ row, index }) => pendingKeys.has(rowIdentity(row, index)))
2414
3235
  .map(({ row, index }) => {
2415
3236
  const rowKey = rowIdentity(row, index);
2416
- const persisted = pendingRowsByKey.get(rowKey);
3237
+ const persisted =
3238
+ pendingRowsByKey.get(rowKey) ?? mapStartSeedRowsByKey.get(rowKey);
2417
3239
  // Persisted rows lose the non-enumerable CSV alias projections on
2418
3240
  // the sheet round-trip. Merge the persisted data/meta over the
2419
3241
  // in-memory row so key functions and column resolvers keep seeing
@@ -2423,8 +3245,8 @@ export class PlayContextImpl {
2423
3245
  index,
2424
3246
  };
2425
3247
  });
2426
- itemsToProcess = pendingItems.map((item) => item.row);
2427
- itemOriginalIndexes = pendingItems.map((item) => item.index);
3248
+ itemsToProcess = seededItems.map((item) => item.row);
3249
+ itemOriginalIndexes = seededItems.map((item) => item.index);
2428
3250
  }
2429
3251
 
2430
3252
  const mapScope = this.createMapExecutionScope({
@@ -2495,6 +3317,7 @@ export class PlayContextImpl {
2495
3317
  playName: this.#options.playName,
2496
3318
  playId: this.#options.playId,
2497
3319
  runId: this.#options.runId,
3320
+ executorToken: this.#options.executorToken,
2498
3321
  tableNamespace: resolvedTableNamespace,
2499
3322
  rows: chunk,
2500
3323
  outputFields: datasetColumnNames.filter((field) =>
@@ -2580,7 +3403,7 @@ export class PlayContextImpl {
2580
3403
  const directCompletedResults = mapResult.completedRows.map((row) =>
2581
3404
  this.toPublicOutputRow(row.data),
2582
3405
  );
2583
- const results =
3406
+ let results =
2584
3407
  mapResult.failedRows.length === 0 &&
2585
3408
  directCompletedResults.length === rawItems.length
2586
3409
  ? directCompletedResults
@@ -2633,6 +3456,49 @@ export class PlayContextImpl {
2633
3456
  }
2634
3457
  this.activeMapCellMeta = null;
2635
3458
 
3459
+ if (this.#options.onMapRowsCompleted) {
3460
+ results = await reconcileNodeRuntimeMapResultsWithPersistedSheet({
3461
+ mapName: normalizedMapNamespace,
3462
+ tableNamespace: resolvedTableNamespace,
3463
+ runId: this.#options.runId,
3464
+ expectedRows: totalInputCount,
3465
+ currentRows: results,
3466
+ failedRowCount: mapResult.failedRows.length,
3467
+ log: (line) => this.log(line),
3468
+ readPersistedRows: async (readInput) => {
3469
+ if (
3470
+ !this.#options.baseUrl ||
3471
+ !this.#options.executorToken ||
3472
+ !this.#options.playName ||
3473
+ !this.#options.runId
3474
+ ) {
3475
+ throw new Error(
3476
+ `Runtime sheet finalization mismatch for ctx.dataset("${normalizedMapNamespace}"): ` +
3477
+ 'cannot verify terminal persisted rows because the Node runtime context is missing ' +
3478
+ 'baseUrl, executorToken, playName, or runId.',
3479
+ );
3480
+ }
3481
+ const result = await readRuntimeSheetDatasetRows(
3482
+ {
3483
+ baseUrl: this.#options.baseUrl,
3484
+ executorToken: this.#options.executorToken,
3485
+ playName: this.#options.playName,
3486
+ userEmail: this.#options.userEmail,
3487
+ runId: this.#options.runId,
3488
+ },
3489
+ {
3490
+ tableNamespace: resolvedTableNamespace,
3491
+ runId: this.#options.runId,
3492
+ rowMode: readInput.rowMode,
3493
+ limit: readInput.limit,
3494
+ offset: readInput.offset,
3495
+ },
3496
+ );
3497
+ return result.rows;
3498
+ },
3499
+ });
3500
+ }
3501
+
2636
3502
  return createDeferredPlayDataset({
2637
3503
  datasetKind: 'map',
2638
3504
  datasetId: createRuntimeDatasetId(
@@ -2909,7 +3775,7 @@ export class PlayContextImpl {
2909
3775
  this.lastDatasetStep = datasetStep;
2910
3776
  this.activeDatasetStep = null;
2911
3777
  this.log(
2912
- `Map completed: ${results.length + completedRows} results (${results.length} executed, ${completedRows} duplicate keys skipped)`,
3778
+ `Map completed: ${results.length + completedRows} results (${results.length} succeeded, 0 failed, ${completedRows} duplicate keys skipped)`,
2913
3779
  );
2914
3780
  return {
2915
3781
  completedRows: results.map((row, index) =>
@@ -3183,6 +4049,17 @@ export class PlayContextImpl {
3183
4049
  }
3184
4050
  return result.value;
3185
4051
  });
4052
+ const accountedRowIndexes = new Set<number>();
4053
+ for (const row of completedRowsToPersist) {
4054
+ if (typeof row.inputIndex === 'number') {
4055
+ accountedRowIndexes.add(row.inputIndex);
4056
+ }
4057
+ }
4058
+ for (const row of failedRowsToPersist) {
4059
+ if (typeof row.inputIndex === 'number') {
4060
+ accountedRowIndexes.add(row.inputIndex);
4061
+ }
4062
+ }
3186
4063
  if (this.pendingRowEventBoundaries.length > 0) {
3187
4064
  const completedRowKeys = new Set<string>();
3188
4065
  for (let index = 0; index < settledResults.length; index += 1) {
@@ -3195,6 +4072,7 @@ export class PlayContextImpl {
3195
4072
  if (key) {
3196
4073
  completedRowKeys.add(key);
3197
4074
  }
4075
+ accountedRowIndexes.add(executionRowIndex(index));
3198
4076
  continue;
3199
4077
  }
3200
4078
  const row = result as Record<string, unknown>;
@@ -3238,6 +4116,19 @@ export class PlayContextImpl {
3238
4116
  boundaries: uniqueBoundaries,
3239
4117
  });
3240
4118
  }
4119
+ if (accountedRowIndexes.size !== items.length) {
4120
+ const expectedRowIndexes = items.map((_item, index) =>
4121
+ executionRowIndex(index),
4122
+ );
4123
+ const missingRowIndexes = expectedRowIndexes.filter(
4124
+ (index) => !accountedRowIndexes.has(index),
4125
+ );
4126
+ throw new Error(
4127
+ `Runtime map execution accounting mismatch for ctx.dataset("${normalizedTableNamespace}"): ` +
4128
+ `expected ${items.length} row(s), accounted for ${accountedRowIndexes.size}; ` +
4129
+ `missing input index(es): ${missingRowIndexes.slice(0, 10).join(', ') || 'unknown'}.`,
4130
+ );
4131
+ }
3241
4132
  const results = settledResults.filter(
3242
4133
  (result): result is Record<string, unknown> =>
3243
4134
  result !== WAITING_ROW && result !== FAILED_ROW,
@@ -3249,8 +4140,18 @@ export class PlayContextImpl {
3249
4140
  emitEventType: 'map.completed',
3250
4141
  });
3251
4142
  }
4143
+ for (const failedRow of failedRowsToPersist.slice(0, 3)) {
4144
+ this.log(
4145
+ `row ${failedMapRowLogLabel(failedRow)} failed: ${failedRow.error ?? 'unknown error'}`,
4146
+ );
4147
+ }
4148
+ if (failedRowsToPersist.length > 3) {
4149
+ this.log(
4150
+ `${failedRowsToPersist.length - 3} additional row failure(s) omitted from map log`,
4151
+ );
4152
+ }
3252
4153
  this.log(
3253
- `Map completed: ${results.length + completedRows} results (${results.length} executed, ${completedRows} duplicate keys skipped)`,
4154
+ `Map completed: ${results.length + completedRows} results (${results.length} succeeded, ${failedRowsToPersist.length} failed, ${completedRows} duplicate keys skipped)`,
3254
4155
  );
3255
4156
  return {
3256
4157
  completedRows: [...completedRowsToPersist].sort(
@@ -3560,6 +4461,13 @@ export class PlayContextImpl {
3560
4461
  if (!hasPendingWork) {
3561
4462
  const status = await raceSettled();
3562
4463
  if (status === 'done' && this.toolCallQueue.length === 0) {
4464
+ if (!this.activeDatasetStep) {
4465
+ const childPlaySuspension =
4466
+ this.consumePendingChildPlaySuspension();
4467
+ if (childPlaySuspension) {
4468
+ throw new PlayExecutionSuspendedError(childPlaySuspension);
4469
+ }
4470
+ }
3563
4471
  break;
3564
4472
  }
3565
4473
 
@@ -3656,18 +4564,23 @@ export class PlayContextImpl {
3656
4564
  toolId,
3657
4565
  requestInput: input,
3658
4566
  });
4567
+ let executionAuthScopeDigest =
4568
+ (await this.resolveToolAuthScopeDigest(toolId))?.trim() ?? null;
3659
4569
  const cacheableToolResult = !isQueryResultDatasetReadRequest(toolId, input);
3660
- const durableCacheKey = await this.durableToolCallCacheKey({
4570
+ let durableCacheKey = await this.durableToolCallCacheKey({
3661
4571
  toolId,
3662
4572
  requestInput: input,
4573
+ executionAuthScopeDigest,
3663
4574
  staleAfterSeconds: toolCachePolicy.staleAfterSeconds,
3664
4575
  });
3665
- const durableReceiptKey = cacheableToolResult ? durableCacheKey : null;
4576
+ const checkpointCacheKeys = [durableCacheKey];
3666
4577
  const eventWaitHandler =
3667
4578
  (await this.#options.getIntegrationEventWaitHandler?.(toolId)) ?? null;
3668
4579
  const store = rowContext.getStore();
3669
4580
 
3670
- const executeTool = async (): Promise<unknown> => {
4581
+ const executeTool = async (context?: {
4582
+ leaseId?: string | null;
4583
+ }): Promise<unknown> => {
3671
4584
  if (eventWaitHandler) {
3672
4585
  return this.executeIntegrationEventWaitTool(
3673
4586
  toolId,
@@ -3694,17 +4607,17 @@ export class PlayContextImpl {
3694
4607
  ? null
3695
4608
  : toolCachePolicy.force
3696
4609
  ? null
3697
- : this.getCachedToolResult(toolId, directCacheKey);
3698
- if (cached?.done) {
3699
- this.log(`Tool cache hit: ${toolId} exact payload`);
4610
+ : this.getCachedToolResultCandidate(toolId, checkpointCacheKeys);
4611
+ if (cached) {
4612
+ this.log(`Calling tool: ${toolId} recovered from checkpoint`);
3700
4613
  return await this.wrapToolExecutionResult({
3701
4614
  toolId,
3702
- status: cached.result == null ? 'no_result' : 'completed',
3703
- result: cached.result,
4615
+ status: cached.result.result == null ? 'no_result' : 'completed',
4616
+ result: cached.result.result,
3704
4617
  requestInput: input,
3705
4618
  execution: toolExecutionMetadataForOutcome({
3706
4619
  kind: 'checkpoint',
3707
- cacheKey: directCacheKey,
4620
+ cacheKey: cached.cacheKey,
3708
4621
  }),
3709
4622
  });
3710
4623
  }
@@ -3714,6 +4627,17 @@ export class PlayContextImpl {
3714
4627
  : `Calling tool: ${toolId}`,
3715
4628
  );
3716
4629
  const execution = await this.callToolExecutionAPI(toolId, input, {
4630
+ ...(cacheableToolResult
4631
+ ? {
4632
+ durableCallReceiptKey: directCacheKey,
4633
+ executionAuthScopeDigest,
4634
+ providerIdempotencyKey: this.providerIdempotencyKeyForToolCall({
4635
+ cacheKey: directCacheKey,
4636
+ force: toolCachePolicy.force,
4637
+ leaseId: context?.leaseId ?? null,
4638
+ }),
4639
+ }
4640
+ : {}),
3717
4641
  timeoutMs: resolveToolRuntimeTimeoutMs(toolId, options?.timeoutMs),
3718
4642
  });
3719
4643
  const wrapped = await this.wrapToolExecutionResult({
@@ -3763,17 +4687,17 @@ export class PlayContextImpl {
3763
4687
  ? null
3764
4688
  : toolCachePolicy.force
3765
4689
  ? null
3766
- : this.getCachedToolResult(toolId, toolResultCacheKey);
3767
- if (cached?.done) {
3768
- this.log(` Row ${rowId} ${toolId}: cache hit exact payload`);
4690
+ : this.getCachedToolResultCandidate(toolId, checkpointCacheKeys);
4691
+ if (cached) {
4692
+ this.log(` Row ${rowId} ${toolId}: recovered from checkpoint`);
3769
4693
  return await this.wrapToolExecutionResult({
3770
4694
  toolId,
3771
- status: cached.result == null ? 'no_result' : 'completed',
3772
- result: cached.result,
4695
+ status: cached.result.result == null ? 'no_result' : 'completed',
4696
+ result: cached.result.result,
3773
4697
  requestInput: input,
3774
4698
  execution: toolExecutionMetadataForOutcome({
3775
4699
  kind: 'checkpoint',
3776
- cacheKey: toolResultCacheKey,
4700
+ cacheKey: cached.cacheKey,
3777
4701
  }),
3778
4702
  });
3779
4703
  }
@@ -3806,8 +4730,10 @@ export class PlayContextImpl {
3806
4730
  callId,
3807
4731
  cacheKey: toolResultCacheKey,
3808
4732
  cacheable: cacheableToolResult,
3809
- receiptKey: durableReceiptKey,
4733
+ receiptKey: cacheableToolResult ? durableCacheKey : null,
4734
+ executionAuthScopeDigest,
3810
4735
  force: toolCachePolicy.force,
4736
+ forceFailedRefresh: toolCachePolicy.forceFailedRefresh,
3811
4737
  rowId,
3812
4738
  fieldName,
3813
4739
  toolId,
@@ -3827,41 +4753,81 @@ export class PlayContextImpl {
3827
4753
  return await executeTool();
3828
4754
  }
3829
4755
 
3830
- return this.executeWithRuntimeReceipt(
3831
- 'tool',
3832
- normalizedKey,
3833
- this.currentRunId,
3834
- {
3835
- receiptKey: durableReceiptKey,
3836
- semanticKey: toolRequestIdentity,
3837
- force: toolCachePolicy.force,
3838
- staleAfterSeconds: toolCachePolicy.staleAfterSeconds,
3839
- markSkipped: () => {
3840
- this.log(
3841
- `Tool cache hit: ${toolId} exact payload (${normalizedKey}:${toolRequestIdentity})`,
3842
- );
3843
- },
3844
- onClaimedResult: (output, receiptKey) =>
3845
- markToolExecuteResultExecutionOutcome(output, {
3846
- kind: 'live',
3847
- receiptKey,
3848
- }),
3849
- onRecovered: (output, _receipt, source) =>
3850
- markToolExecuteResultExecutionOutcome(
3851
- output,
3852
- toolExecutionOutcomeForDurableReceipt({
3853
- source,
3854
- receiptKey: durableCacheKey,
3855
- }),
3856
- ),
3857
- runningReceiptWaitMaxAttempts: resolveRuntimeToolReceiptWaitMaxAttempts(
3858
- typeof options?.receiptWaitMs === 'number'
3859
- ? { max_wait_ms: options.receiptWaitMs }
3860
- : input,
3861
- ),
3862
- execute: executeTool,
3863
- },
3864
- );
4756
+ for (
4757
+ let authScopeAttempt = 0;
4758
+ authScopeAttempt < 2;
4759
+ authScopeAttempt += 1
4760
+ ) {
4761
+ try {
4762
+ return await this.executeWithRuntimeReceipt(
4763
+ 'tool',
4764
+ normalizedKey,
4765
+ this.currentRunId,
4766
+ {
4767
+ receiptKey: durableCacheKey,
4768
+ semanticKey: toolRequestIdentity,
4769
+ force: toolCachePolicy.force,
4770
+ staleAfterSeconds: toolCachePolicy.staleAfterSeconds,
4771
+ markSkipped: () => {
4772
+ this.log(
4773
+ `ctx.tools.execute(${toolId}): no-op due completed receipt ${toolRequestIdentity} (label: ${normalizedKey})`,
4774
+ );
4775
+ },
4776
+ onClaimedResult: (output, receiptKey) =>
4777
+ markToolExecuteResultExecutionOutcome(output, {
4778
+ kind: 'live',
4779
+ receiptKey,
4780
+ }),
4781
+ onRecovered: (output, _receipt, source) =>
4782
+ markToolExecuteResultExecutionOutcome(
4783
+ output,
4784
+ toolExecutionOutcomeForDurableReceipt({
4785
+ source,
4786
+ receiptKey: durableCacheKey,
4787
+ }),
4788
+ ),
4789
+ shouldPersistFailure: (error) =>
4790
+ !(error instanceof ToolExecuteAuthScopeChangedError),
4791
+ execute: ({ leaseId }) => executeTool({ leaseId }),
4792
+ runningReceiptWaitMaxAttempts:
4793
+ resolveRuntimeToolReceiptWaitMaxAttempts(
4794
+ typeof options?.receiptWaitMs === 'number'
4795
+ ? { max_wait_ms: options.receiptWaitMs }
4796
+ : input,
4797
+ ),
4798
+ },
4799
+ );
4800
+ } catch (error) {
4801
+ if (
4802
+ !(error instanceof ToolExecuteAuthScopeChangedError) ||
4803
+ authScopeAttempt > 0
4804
+ ) {
4805
+ throw error;
4806
+ }
4807
+ // Make the bounded auth-scope re-claim observable in run logs instead
4808
+ // of a silent continuation: the credential identity changed after the
4809
+ // receipt key was prepared, so we evict the cached digest, re-resolve,
4810
+ // and re-claim a fresh receipt under the new scope before retrying once.
4811
+ this.log(
4812
+ `ctx.tools.execute(${toolId}): auth_scope_changed_reclaim ` +
4813
+ `(label: ${normalizedKey}); credential identity changed mid-run, ` +
4814
+ `re-resolving auth scope and re-claiming a fresh receipt under the ` +
4815
+ `new scope (attempt ${authScopeAttempt + 1}/2).`,
4816
+ );
4817
+ this.invalidateToolAuthScopeDigest(toolId);
4818
+ executionAuthScopeDigest =
4819
+ (await this.resolveToolAuthScopeDigest(toolId))?.trim() ?? null;
4820
+ durableCacheKey = await this.durableToolCallCacheKey({
4821
+ toolId,
4822
+ requestInput: input,
4823
+ executionAuthScopeDigest,
4824
+ staleAfterSeconds: toolCachePolicy.staleAfterSeconds,
4825
+ });
4826
+ checkpointCacheKeys[0] = durableCacheKey;
4827
+ }
4828
+ }
4829
+
4830
+ throw new ToolExecuteAuthScopeChangedError();
3865
4831
  }
3866
4832
 
3867
4833
  private async executeIntegrationEventWaitTool(
@@ -3987,163 +4953,349 @@ export class PlayContextImpl {
3987
4953
  if (!resolvedName.trim()) {
3988
4954
  throw new Error('ctx.runPlay(...) requires a resolvable play name.');
3989
4955
  }
4956
+ const scheduledChildPlayCall = this.shouldLaunchChildPlaysThroughScheduler()
4957
+ ? this.beginScheduledChildPlayCall()
4958
+ : null;
3990
4959
 
3991
- if (!this.#options.resolvePlay) {
3992
- throw new Error(
3993
- 'ctx.runPlay(...) is unavailable because no play resolver was configured.',
3994
- );
3995
- }
3996
- const resolvePlay = this.#options.resolvePlay;
3997
- const resolvedPlay = await resolvePlay(resolvedName);
3998
- if (!resolvedPlay) {
3999
- throw new Error(
4000
- `Unable to resolve play "${resolvedName}" for ctx.runPlay(...).`,
4001
- );
4002
- }
4003
- const childRevisionFingerprint =
4004
- resolvedPlayRevisionFingerprint(resolvedPlay);
4005
-
4006
- const store = rowContext.getStore();
4007
- const executePlayCall = async (): Promise<TOutput> => {
4008
- // Reserve depth + per-parent + descendant/play-call budget on the parent
4009
- // and fork the lineage-global snapshot for the child. forkChild owns the
4010
- // cycle guard, depth cap, per-parent cap, and play/descendant charges, so
4011
- // a recovered receipt (which skips this body) never consumes budget. The
4012
- // child run id stays deterministic — it matches the prior post-increment
4013
- // playCallCount — so receipt keys and replay are stable.
4014
- const childRunId = `${this.governance.rootRunId}:${resolvedName}:${this.governor.snapshot().playCallCount + 1}`;
4015
- const childGovernance = this.governor.forkChild({
4960
+ try {
4961
+ if (!this.#options.resolvePlay) {
4962
+ throw new Error(
4963
+ 'ctx.runPlay(...) is unavailable because no play resolver was configured.',
4964
+ );
4965
+ }
4966
+ const resolvePlay = this.#options.resolvePlay;
4967
+ const resolvedPlay = await resolvePlay(resolvedName);
4968
+ if (!resolvedPlay) {
4969
+ throw new Error(
4970
+ `Unable to resolve play "${resolvedName}" for ctx.runPlay(...).`,
4971
+ );
4972
+ }
4973
+ const childRevisionFingerprint =
4974
+ resolvedPlayRevisionFingerprint(resolvedPlay);
4975
+ const runPlayRowScope = rowContext.getStore();
4976
+ const runPlaySemanticKey = buildDurableRunPlaySemanticKey({
4016
4977
  childPlayName: resolvedName,
4017
- childRunId,
4978
+ childRevisionFingerprint,
4979
+ input,
4980
+ rowScope: runPlayRowScope
4981
+ ? {
4982
+ fieldName: runPlayRowScope.fieldName,
4983
+ rowId: runPlayRowScope.rowId,
4984
+ rowKey: runPlayRowScope.rowKey ?? null,
4985
+ tableNamespace: runPlayRowScope.tableNamespace ?? null,
4986
+ }
4987
+ : null,
4018
4988
  });
4019
- const childPlaySlot = await this.governor.acquireChildPlaySlot();
4020
- const rowStore = rowContext.getStore();
4021
- const producer = {
4022
- kind: 'play' as const,
4023
- id: normalizedKey,
4024
- playId: resolvedName,
4025
- displayName: displayNameFromProducerId(resolvedName),
4026
- runId: childRunId,
4027
- };
4028
4989
 
4029
- try {
4030
- if (rowStore) {
4031
- this.emitScopedFieldMetaUpdate({
4032
- rowId: rowStore.rowId,
4033
- key: rowStore.rowKey ?? null,
4034
- tableNamespace: rowStore.tableNamespace ?? null,
4035
- fieldName: rowStore.fieldName,
4036
- status: 'running',
4037
- rowStatus: 'running',
4038
- stage: resolvedName,
4039
- provider: 'deepline_native',
4040
- error: null,
4041
- producer,
4042
- dataPatch: {},
4043
- });
4044
- }
4045
- const childContext = createPlayContext({
4046
- ...this.#options,
4047
- playId: resolvedName,
4048
- playName: resolvedName,
4049
- runId: childGovernance.currentRunId,
4050
- staticPipeline: resolvedPlay.staticPipeline ?? null,
4051
- checkpoint: this.checkpoint,
4052
- governance: childGovernance,
4053
- getRuntimeStepReceipt: undefined,
4054
- getRuntimeStepReceipts: undefined,
4055
- claimRuntimeStepReceipt: undefined,
4056
- claimRuntimeStepReceipts: undefined,
4057
- completeRuntimeStepReceipt: undefined,
4058
- completeRuntimeStepReceipts: undefined,
4059
- failRuntimeStepReceipt: undefined,
4060
- failRuntimeStepReceipts: undefined,
4061
- skipRuntimeStepReceipt: undefined,
4062
- });
4063
- const childExecution = this.executeResolvedPlay(
4064
- resolvedPlay,
4065
- childContext,
4990
+ const executePlayCall = async (): Promise<TOutput> => {
4991
+ // Reserve depth + per-parent + descendant/play-call budget on the parent
4992
+ // and fork the lineage-global snapshot for the child. forkChild owns the
4993
+ // cycle guard, depth cap, per-parent cap, and play/descendant charges, so
4994
+ // a recovered receipt (which skips this body) never consumes budget.
4995
+ //
4996
+ // The child run id is derived through the shared canonical builder so the
4997
+ // in-process runtime and the workers_edge coordinator produce the same
4998
+ // FORMAT (`play/<slug>/run/child-<digest>`) via the same code. The digest
4999
+ // is deterministic across replay because every input (parent lineage,
5000
+ // call key, input) is stable. Per-substrate difference: in-process has no
5001
+ // coordinator idempotency key and no compiled child graphHash, so it uses
5002
+ // the semantic fallback with graphHash omitted; workers_edge feeds the
5003
+ // durable child idempotency key. See child-run-id.ts.
5004
+ const childRunId = buildChildRunId({
5005
+ childPlayName: resolvedName,
5006
+ parentRunId: this.currentRunId,
5007
+ parentPlayName: this.#options.playName,
5008
+ key: normalizedKey,
5009
+ runPlaySemanticKey,
4066
5010
  input,
4067
- );
4068
- await childContext.drainQueuedWork([childExecution]);
4069
- const result = await childExecution;
4070
- if (rowStore) {
4071
- this.emitScopedFieldMetaUpdate({
4072
- rowId: rowStore.rowId,
4073
- key: rowStore.rowKey ?? null,
4074
- tableNamespace: rowStore.tableNamespace ?? null,
4075
- fieldName: rowStore.fieldName,
4076
- status: 'completed',
4077
- rowStatus: 'running',
4078
- stage: resolvedName,
4079
- provider: 'deepline_native',
4080
- error: null,
4081
- producer,
4082
- dataPatch: {},
4083
- });
4084
- }
4085
- const step = {
4086
- type: 'play_call' as const,
5011
+ graphHash: null,
5012
+ });
5013
+ const childGovernance = this.governor.forkChild({
5014
+ childPlayName: resolvedName,
5015
+ childRunId,
5016
+ });
5017
+ const childPlaySlot = await this.governor.acquireChildSubmitSlot();
5018
+ const rowStore = rowContext.getStore();
5019
+ const producer = {
5020
+ kind: 'play' as const,
5021
+ id: normalizedKey,
4087
5022
  playId: resolvedName,
4088
- nestedSteps: childContext.getSteps(),
4089
- description: normalizeStepDescription(options?.description),
5023
+ displayName: displayNameFromProducerId(resolvedName),
5024
+ runId: childRunId,
4090
5025
  };
4091
- if (this.activeDatasetStep) {
4092
- this.activeDatasetStep.substeps.push(step);
4093
- } else {
4094
- this.steps.push(step);
4095
- }
4096
- return result as TOutput;
4097
- } catch (error) {
4098
- if (rowStore) {
4099
- this.emitScopedFieldMetaUpdate({
4100
- rowId: rowStore.rowId,
4101
- key: rowStore.rowKey ?? null,
4102
- tableNamespace: rowStore.tableNamespace ?? null,
4103
- fieldName: rowStore.fieldName,
4104
- status: 'failed',
4105
- rowStatus: 'running',
4106
- stage: resolvedName,
4107
- provider: 'deepline_native',
4108
- error: this.formatRuntimeError(error),
4109
- producer,
4110
- dataPatch: {},
5026
+
5027
+ try {
5028
+ if (rowStore) {
5029
+ this.emitScopedFieldMetaUpdate({
5030
+ rowId: rowStore.rowId,
5031
+ key: rowStore.rowKey ?? null,
5032
+ tableNamespace: rowStore.tableNamespace ?? null,
5033
+ fieldName: rowStore.fieldName,
5034
+ status: 'running',
5035
+ rowStatus: 'running',
5036
+ stage: resolvedName,
5037
+ provider: 'deepline_native',
5038
+ error: null,
5039
+ producer,
5040
+ dataPatch: {},
5041
+ });
5042
+ }
5043
+ if (this.shouldLaunchChildPlaysThroughScheduler()) {
5044
+ const boundaryId = this.childPlayBoundaryId(
5045
+ childGovernance.currentRunId,
5046
+ );
5047
+ const checkpointResult = this.childPlayBoundaryOutput<TOutput>({
5048
+ boundaryId,
5049
+ childRunId: childGovernance.currentRunId,
5050
+ childPlayName: resolvedName,
5051
+ });
5052
+ if (checkpointResult.found) {
5053
+ if (rowStore) {
5054
+ this.emitScopedFieldMetaUpdate({
5055
+ rowId: rowStore.rowId,
5056
+ key: rowStore.rowKey ?? null,
5057
+ tableNamespace: rowStore.tableNamespace ?? null,
5058
+ fieldName: rowStore.fieldName,
5059
+ status: 'completed',
5060
+ rowStatus: 'running',
5061
+ stage: resolvedName,
5062
+ provider: 'deepline_native',
5063
+ error: null,
5064
+ producer,
5065
+ dataPatch: {},
5066
+ });
5067
+ }
5068
+ this.recordPlayCallStep({
5069
+ playId: resolvedName,
5070
+ description: options?.description,
5071
+ });
5072
+ return checkpointResult.output;
5073
+ }
5074
+
5075
+ await this.launchScheduledChildPlay({
5076
+ key: normalizedKey,
5077
+ childRunId: childGovernance.currentRunId,
5078
+ childPlayName: resolvedName,
5079
+ childGovernance,
5080
+ childInput: input,
5081
+ description: options?.description ?? null,
5082
+ });
5083
+ childPlaySlot.release();
5084
+ const terminal = await this.readScheduledChildTerminal<TOutput>({
5085
+ childRunId: childGovernance.currentRunId,
5086
+ childPlayName: resolvedName,
5087
+ });
5088
+ if (terminal?.status === 'completed') {
5089
+ this.checkpoint.resolvedBoundaries = {
5090
+ ...(this.checkpoint.resolvedBoundaries ?? {}),
5091
+ [boundaryId]: {
5092
+ kind: 'child_play',
5093
+ childRunId: childGovernance.currentRunId,
5094
+ childPlayName: resolvedName,
5095
+ status: 'completed',
5096
+ output: terminal.output,
5097
+ completedAt: Date.now(),
5098
+ },
5099
+ };
5100
+ if (rowStore) {
5101
+ this.emitScopedFieldMetaUpdate({
5102
+ rowId: rowStore.rowId,
5103
+ key: rowStore.rowKey ?? null,
5104
+ tableNamespace: rowStore.tableNamespace ?? null,
5105
+ fieldName: rowStore.fieldName,
5106
+ status: 'completed',
5107
+ rowStatus: 'running',
5108
+ stage: resolvedName,
5109
+ provider: 'deepline_native',
5110
+ error: null,
5111
+ producer,
5112
+ dataPatch: {},
5113
+ });
5114
+ }
5115
+ this.recordPlayCallStep({
5116
+ playId: resolvedName,
5117
+ description: options?.description,
5118
+ });
5119
+ return terminal.output;
5120
+ }
5121
+ if (terminal) {
5122
+ throw new Error(terminal.error);
5123
+ }
5124
+
5125
+ const suspension = {
5126
+ kind: 'child_play',
5127
+ boundaryId,
5128
+ childRunId: childGovernance.currentRunId,
5129
+ childPlayName: resolvedName,
5130
+ } as const;
5131
+ this.recordPendingChildPlayBoundary(suspension);
5132
+ scheduledChildPlayCall?.release();
5133
+ await this.waitForScheduledChildPlayQuiescence();
5134
+ throw new PlayExecutionSuspendedError(
5135
+ this.consumePendingChildPlaySuspension() ?? suspension,
5136
+ );
5137
+ }
5138
+ const childContext = createPlayContext({
5139
+ ...this.#options,
5140
+ playId: resolvedName,
5141
+ playName: resolvedName,
5142
+ runId: childGovernance.currentRunId,
5143
+ executorToken: await this.mintChildExecutorToken({
5144
+ parentRunId: this.currentRunId,
5145
+ parentPlayName: this.#options.playName,
5146
+ childRunId: childGovernance.currentRunId,
5147
+ childPlayName: resolvedName,
5148
+ }),
5149
+ staticPipeline: resolvedPlay.staticPipeline ?? null,
5150
+ checkpoint: this.checkpoint,
5151
+ governance: childGovernance,
5152
+ getRuntimeStepReceipt: undefined,
5153
+ getRuntimeStepReceipts: undefined,
5154
+ claimRuntimeStepReceipt: undefined,
5155
+ claimRuntimeStepReceipts: undefined,
5156
+ completeRuntimeStepReceipt: undefined,
5157
+ completeRuntimeStepReceipts: undefined,
5158
+ failRuntimeStepReceipt: undefined,
5159
+ failRuntimeStepReceipts: undefined,
5160
+ heartbeatRuntimeStepReceipts: undefined,
5161
+ skipRuntimeStepReceipt: undefined,
4111
5162
  });
5163
+ const childExecution = this.executeResolvedPlay(
5164
+ resolvedPlay,
5165
+ childContext,
5166
+ input,
5167
+ );
5168
+ await childContext.drainQueuedWork([childExecution]);
5169
+ const result = await childExecution;
5170
+ if (rowStore) {
5171
+ this.emitScopedFieldMetaUpdate({
5172
+ rowId: rowStore.rowId,
5173
+ key: rowStore.rowKey ?? null,
5174
+ tableNamespace: rowStore.tableNamespace ?? null,
5175
+ fieldName: rowStore.fieldName,
5176
+ status: 'completed',
5177
+ rowStatus: 'running',
5178
+ stage: resolvedName,
5179
+ provider: 'deepline_native',
5180
+ error: null,
5181
+ producer,
5182
+ dataPatch: {},
5183
+ });
5184
+ }
5185
+ const step = {
5186
+ type: 'play_call' as const,
5187
+ playId: resolvedName,
5188
+ nestedSteps: childContext.getSteps(),
5189
+ description: normalizeStepDescription(options?.description),
5190
+ };
5191
+ if (this.activeDatasetStep) {
5192
+ this.activeDatasetStep.substeps.push(step);
5193
+ } else {
5194
+ this.steps.push(step);
5195
+ }
5196
+ return result as TOutput;
5197
+ } catch (error) {
5198
+ if (isPlayExecutionSuspendedError(error)) {
5199
+ throw error;
5200
+ }
5201
+ if (rowStore) {
5202
+ this.emitScopedFieldMetaUpdate({
5203
+ rowId: rowStore.rowId,
5204
+ key: rowStore.rowKey ?? null,
5205
+ tableNamespace: rowStore.tableNamespace ?? null,
5206
+ fieldName: rowStore.fieldName,
5207
+ status: 'failed',
5208
+ rowStatus: 'running',
5209
+ stage: resolvedName,
5210
+ provider: 'deepline_native',
5211
+ error: this.formatRuntimeError(error),
5212
+ producer,
5213
+ dataPatch: {},
5214
+ });
5215
+ }
5216
+ throw error;
5217
+ } finally {
5218
+ childPlaySlot.release();
4112
5219
  }
4113
- throw error;
4114
- } finally {
4115
- childPlaySlot.release();
4116
- }
4117
- };
5220
+ };
4118
5221
 
4119
- if (store) {
4120
- return await executePlayCall();
5222
+ return await this.executeWithRuntimeReceipt<TOutput>(
5223
+ 'runPlay',
5224
+ normalizedKey,
5225
+ this.currentRunId,
5226
+ {
5227
+ semanticKey: runPlaySemanticKey,
5228
+ staleAfterSeconds: options?.staleAfterSeconds,
5229
+ markSkipped: () => {
5230
+ this.log(
5231
+ `ctx.runPlay(${normalizedKey}): no-op due completed receipt`,
5232
+ );
5233
+ },
5234
+ repairRunningReceiptForSameRunAfterWaitTimeout: true,
5235
+ runningReceiptWaitMaxAttempts:
5236
+ resolveRuntimeToolReceiptWaitMaxAttempts(input),
5237
+ execute: executePlayCall,
5238
+ },
5239
+ );
5240
+ } finally {
5241
+ scheduledChildPlayCall?.release();
4121
5242
  }
5243
+ }
4122
5244
 
4123
- return this.executeWithRuntimeReceipt<TOutput>(
4124
- 'runPlay',
4125
- normalizedKey,
4126
- this.currentRunId,
5245
+ private async mintChildExecutorToken(input: {
5246
+ parentRunId: string;
5247
+ parentPlayName?: string | null;
5248
+ childRunId: string;
5249
+ childPlayName: string;
5250
+ }): Promise<string | undefined> {
5251
+ if (!this.#options.executorToken || !this.#options.baseUrl) {
5252
+ return this.#options.executorToken;
5253
+ }
5254
+ const parentPlayName = input.parentPlayName?.trim();
5255
+ if (!parentPlayName) {
5256
+ return this.#options.executorToken;
5257
+ }
5258
+ const response = await fetch(
5259
+ `${this.#options.baseUrl.replace(/\/$/, '')}${PLAY_RUNTIME_CHILD_EXECUTOR_TOKEN_COMPAT_PATH}`,
4127
5260
  {
4128
- semanticKey: stableDigest(
4129
- stableStringify({
4130
- childPlayName: resolvedName,
4131
- childRevisionFingerprint,
4132
- input,
4133
- }),
4134
- ),
4135
- staleAfterSeconds: options?.staleAfterSeconds,
4136
- markSkipped: () => {
4137
- this.log(
4138
- `ctx.runPlay(${normalizedKey}): no-op due completed receipt`,
4139
- );
5261
+ method: 'POST',
5262
+ headers: {
5263
+ Authorization: `Bearer ${this.#options.executorToken}`,
5264
+ 'Content-Type': 'application/json',
5265
+ ...(this.#options.vercelProtectionBypassToken?.trim()
5266
+ ? {
5267
+ 'x-vercel-protection-bypass':
5268
+ this.#options.vercelProtectionBypassToken.trim(),
5269
+ }
5270
+ : {}),
4140
5271
  },
4141
- repairRunningReceiptForSameRunAfterWaitTimeout: true,
4142
- runningReceiptWaitMaxAttempts:
4143
- resolveRuntimeToolReceiptWaitMaxAttempts(input),
4144
- execute: executePlayCall,
5272
+ body: JSON.stringify({
5273
+ parentRunId: input.parentRunId,
5274
+ parentPlayName,
5275
+ childRunId: input.childRunId,
5276
+ childPlayName: input.childPlayName,
5277
+ }),
4145
5278
  },
4146
5279
  );
5280
+ const body = (await response.json().catch(() => null)) as {
5281
+ executorToken?: unknown;
5282
+ error?: unknown;
5283
+ } | null;
5284
+ if (!response.ok) {
5285
+ const message =
5286
+ typeof body?.error === 'string' && body.error.trim()
5287
+ ? body.error.trim()
5288
+ : response.statusText;
5289
+ throw new Error(
5290
+ `ctx.runPlay failed to mint child executor token for "${input.childPlayName}": ${message} (status ${response.status}).`,
5291
+ );
5292
+ }
5293
+ if (typeof body?.executorToken !== 'string' || !body.executorToken.trim()) {
5294
+ throw new Error(
5295
+ `ctx.runPlay failed to mint child executor token for "${input.childPlayName}": response did not include executorToken.`,
5296
+ );
5297
+ }
5298
+ return body.executorToken.trim();
4147
5299
  }
4148
5300
 
4149
5301
  /**
@@ -4630,7 +5782,11 @@ export class PlayContextImpl {
4630
5782
  const followers = liveFollowersByOwnerCallId.get(owner.callId) ?? [];
4631
5783
  liveFollowersByOwnerCallId.delete(owner.callId);
4632
5784
  for (const request of [owner, ...followers]) {
4633
- await this.rejectToolCall(toolId, request, error);
5785
+ await this.rejectToolCall(toolId, request, error, {
5786
+ persistReceiptFailure: !(
5787
+ error instanceof ToolExecuteAuthScopeChangedError
5788
+ ),
5789
+ });
4634
5790
  }
4635
5791
  };
4636
5792
  const requestsWithLiveFollowers = (
@@ -4685,7 +5841,11 @@ export class PlayContextImpl {
4685
5841
  ) {
4686
5842
  const requestsByReceiptKey = new Map<
4687
5843
  string,
4688
- { requests: ToolCallRequest[]; forceRefresh: boolean }
5844
+ {
5845
+ requests: ToolCallRequest[];
5846
+ forceRefresh: boolean;
5847
+ forceFailedRefresh: boolean;
5848
+ }
4689
5849
  >();
4690
5850
  const localOnlyRequests: ToolCallRequest[] = [];
4691
5851
  for (const request of pendingRequests) {
@@ -4697,17 +5857,37 @@ export class PlayContextImpl {
4697
5857
  const group = requestsByReceiptKey.get(receiptKey) ?? {
4698
5858
  requests: [],
4699
5859
  forceRefresh: false,
5860
+ forceFailedRefresh: false,
4700
5861
  };
4701
5862
  group.requests.push(request);
4702
5863
  group.forceRefresh ||= request.force === true;
5864
+ group.forceFailedRefresh ||= request.forceFailedRefresh === true;
4703
5865
  requestsByReceiptKey.set(receiptKey, group);
4704
5866
  }
4705
5867
  const normalReceiptKeys = [...requestsByReceiptKey]
4706
- .filter(([, group]) => !group.forceRefresh)
5868
+ .filter(
5869
+ ([, group]) => !group.forceRefresh && !group.forceFailedRefresh,
5870
+ )
4707
5871
  .map(([receiptKey]) => receiptKey);
4708
5872
  const forcedReceiptKeys = [...requestsByReceiptKey]
4709
5873
  .filter(([, group]) => group.forceRefresh)
4710
5874
  .map(([receiptKey]) => receiptKey);
5875
+ const failedRefreshReceiptKeys = [...requestsByReceiptKey]
5876
+ .filter(
5877
+ ([, group]) => !group.forceRefresh && group.forceFailedRefresh,
5878
+ )
5879
+ .map(([receiptKey]) => receiptKey);
5880
+ const forcedExisting = forcedReceiptKeys.length
5881
+ ? await this.getRuntimeStepReceipts(forcedReceiptKeys)
5882
+ : new Map<string, RuntimeStepReceipt>();
5883
+ const forcedRunningReceiptKeys = new Set(
5884
+ [...forcedExisting]
5885
+ .filter(([, receipt]) => receipt.status === 'running')
5886
+ .map(([receiptKey]) => receiptKey),
5887
+ );
5888
+ const claimableForcedReceiptKeys = forcedReceiptKeys.filter(
5889
+ (receiptKey) => !forcedRunningReceiptKeys.has(receiptKey),
5890
+ );
4711
5891
  const claims =
4712
5892
  normalReceiptKeys.length > 0
4713
5893
  ? await this.claimRuntimeStepReceipts(
@@ -4716,17 +5896,30 @@ export class PlayContextImpl {
4716
5896
  )
4717
5897
  : new Map<string, RuntimeStepReceipt>();
4718
5898
  const forcedClaims =
4719
- forcedReceiptKeys.length > 0
5899
+ claimableForcedReceiptKeys.length > 0
4720
5900
  ? await this.claimRuntimeStepReceipts(
4721
- forcedReceiptKeys,
5901
+ claimableForcedReceiptKeys,
4722
5902
  this.currentRunId,
5903
+ false,
4723
5904
  true,
5905
+ )
5906
+ : new Map<string, RuntimeStepReceipt>();
5907
+ const failedRefreshClaims =
5908
+ failedRefreshReceiptKeys.length > 0
5909
+ ? await this.claimRuntimeStepReceipts(
5910
+ failedRefreshReceiptKeys,
5911
+ this.currentRunId,
5912
+ false,
5913
+ false,
4724
5914
  true,
4725
5915
  )
4726
5916
  : new Map<string, RuntimeStepReceipt>();
4727
5917
  for (const [receiptKey, claim] of forcedClaims) {
4728
5918
  claims.set(receiptKey, claim);
4729
5919
  }
5920
+ for (const [receiptKey, claim] of failedRefreshClaims) {
5921
+ claims.set(receiptKey, claim);
5922
+ }
4730
5923
  const claimedRequests: ToolCallRequest[] = [...localOnlyRequests];
4731
5924
  const durableRecoveredRequests: ToolCallRequest[] = [];
4732
5925
  const resolveRequestsFromReceipt = async (
@@ -4738,9 +5931,7 @@ export class PlayContextImpl {
4738
5931
  if (!request) return;
4739
5932
  const receiptKey = request.receiptKey?.trim() || null;
4740
5933
  if (!receiptKey) {
4741
- throw new Error(
4742
- `Durable tool receipt ${source} recovery requires a receipt key.`,
4743
- );
5934
+ throw new Error(`${source} tool recovery needs receipt key.`);
4744
5935
  }
4745
5936
  const wrapped = await this.wrapToolExecutionResult({
4746
5937
  toolId,
@@ -4794,6 +5985,7 @@ export class PlayContextImpl {
4794
5985
  const reclaimExistingRunningReceiptAfterWait = async (
4795
5986
  receiptKey: string,
4796
5987
  requestsForKey: ToolCallRequest[],
5988
+ forceAfterWait = false,
4797
5989
  ): Promise<void> => {
4798
5990
  const waitMaxAttempts =
4799
5991
  requestsForKey.length > 0
@@ -4808,15 +6000,18 @@ export class PlayContextImpl {
4808
6000
  )
4809
6001
  : undefined;
4810
6002
  try {
4811
- await resolveRequestsFromReceipt(
4812
- requestsForKey,
4813
- await this.waitForCompletedRuntimeToolReceipt(
4814
- receiptKey,
4815
- waitMaxAttempts,
4816
- ),
4817
- 'in_flight',
6003
+ const completed = await this.waitForCompletedRuntimeToolReceipt(
6004
+ receiptKey,
6005
+ waitMaxAttempts,
4818
6006
  );
4819
- return;
6007
+ if (!forceAfterWait) {
6008
+ await resolveRequestsFromReceipt(
6009
+ requestsForKey,
6010
+ completed,
6011
+ 'in_flight',
6012
+ );
6013
+ return;
6014
+ }
4820
6015
  } catch (error) {
4821
6016
  if (!(error instanceof RuntimeReceiptWaitTimeoutError)) {
4822
6017
  for (const request of requestsForKey) {
@@ -4832,6 +6027,7 @@ export class PlayContextImpl {
4832
6027
  [receiptKey],
4833
6028
  this.currentRunId,
4834
6029
  true,
6030
+ forceAfterWait,
4835
6031
  )
4836
6032
  ).get(receiptKey);
4837
6033
  if (
@@ -4862,14 +6058,31 @@ export class PlayContextImpl {
4862
6058
  ) {
4863
6059
  const [owner, ...waiters] = requestsForKey;
4864
6060
  if (!owner) return;
6061
+ if (!reclaimed.leaseId) {
6062
+ throw new RuntimeReceiptLeaseLostError({
6063
+ receiptKey,
6064
+ runId: this.currentRunId,
6065
+ leaseId: 'missing',
6066
+ });
6067
+ }
6068
+ owner.receiptLeaseId = reclaimed.leaseId ?? null;
4865
6069
  if (waiters.length > 0) {
4866
6070
  liveFollowersByOwnerCallId.set(owner.callId, waiters);
4867
6071
  }
4868
6072
  try {
6073
+ await this.assertRuntimeToolReceiptOwnership([owner]);
4869
6074
  const execution = await this.callToolExecutionAPI(
4870
6075
  toolId,
4871
6076
  owner.input,
4872
6077
  {
6078
+ durableCallReceiptKey: receiptKey,
6079
+ executionAuthScopeDigest: owner.executionAuthScopeDigest,
6080
+ providerIdempotencyKey:
6081
+ this.providerIdempotencyKeyForToolCall({
6082
+ cacheKey: owner.cacheKey,
6083
+ force: owner.force === true,
6084
+ leaseId: owner.receiptLeaseId,
6085
+ }),
4873
6086
  timeoutMs: resolveRuntimeTimeoutMsForClaimedOwners([owner]),
4874
6087
  },
4875
6088
  );
@@ -4904,8 +6117,13 @@ export class PlayContextImpl {
4904
6117
  const requestsForKey = group.requests;
4905
6118
  const claim = claims.get(receiptKey);
4906
6119
  if (!claim) {
4907
- const [owner, ...waiters] = requestsForKey;
4908
- claimOwnerWithLiveFollowers(owner, waiters);
6120
+ durableExistingRunningHandlers.push(
6121
+ reclaimExistingRunningReceiptAfterWait(
6122
+ receiptKey,
6123
+ requestsForKey,
6124
+ group.forceRefresh,
6125
+ ),
6126
+ );
4909
6127
  continue;
4910
6128
  }
4911
6129
  if (claim?.status === 'completed' || claim?.status === 'skipped') {
@@ -4928,6 +6146,16 @@ export class PlayContextImpl {
4928
6146
  claim.claimState !== 'existing'
4929
6147
  ) {
4930
6148
  const [owner, ...waiters] = requestsForKey;
6149
+ if (!claim.leaseId) {
6150
+ throw new RuntimeReceiptLeaseLostError({
6151
+ receiptKey,
6152
+ runId: this.currentRunId,
6153
+ leaseId: 'missing',
6154
+ });
6155
+ }
6156
+ if (owner) {
6157
+ owner.receiptLeaseId = claim.leaseId ?? null;
6158
+ }
4931
6159
  claimOwnerWithLiveFollowers(owner, waiters);
4932
6160
  continue;
4933
6161
  }
@@ -4939,6 +6167,7 @@ export class PlayContextImpl {
4939
6167
  reclaimExistingRunningReceiptAfterWait(
4940
6168
  receiptKey,
4941
6169
  requestsForKey,
6170
+ group.forceRefresh,
4942
6171
  ),
4943
6172
  );
4944
6173
  continue;
@@ -4947,6 +6176,7 @@ export class PlayContextImpl {
4947
6176
  reclaimExistingRunningReceiptAfterWait(
4948
6177
  receiptKey,
4949
6178
  requestsForKey,
6179
+ group.forceRefresh,
4950
6180
  ),
4951
6181
  );
4952
6182
  }
@@ -4971,18 +6201,13 @@ export class PlayContextImpl {
4971
6201
  compiledBatches.length > 0
4972
6202
  ? await this.governor.suggestedParallelism(
4973
6203
  compiledBatches[0]!.batchOperation,
4974
- 4,
6204
+ this.governor.policy.pacing
6205
+ .workerToolBatchDefaultParallelism,
4975
6206
  )
4976
- : 4;
6207
+ : this.governor.policy.pacing.workerToolBatchDefaultParallelism;
4977
6208
  await executeChunkedRequests({
4978
6209
  requests: compiledBatches,
4979
6210
  batchSize,
4980
- // Bounded dispatch wave (postgres_fast parity with workers_edge):
4981
- // cap the provider calls (counted by batch member size) a single
4982
- // chunk dispatches before the latch is read again, so a
4983
- // persistence failure prevents the rest of a large batch group.
4984
- maxChunkWeight: PROVIDER_DISPATCH_WAVE_SIZE,
4985
- weightOf: (batch) => batch.memberRequests.length,
4986
6211
  execute: async (batch) => {
4987
6212
  // Circuit breaker: skip dispatching this batch's provider call
4988
6213
  // once a persistence failure has occurred in this run.
@@ -4993,10 +6218,42 @@ export class PlayContextImpl {
4993
6218
  this.persistenceLatch,
4994
6219
  );
4995
6220
  }
6221
+ await this.assertRuntimeToolReceiptOwnership(
6222
+ batch.memberRequests,
6223
+ );
6224
+ const receiptKeys = batch.memberRequests.map(
6225
+ (request) => request.cacheKey,
6226
+ );
6227
+ const aggregateReceiptKey = buildDurableToolAggregateReceiptKey(
6228
+ {
6229
+ receiptKeys,
6230
+ prefix: 'batch',
6231
+ aggregateReceiptPrefix: buildDurableToolReceiptPrefix({
6232
+ orgId: this.#options.orgId,
6233
+ toolId: batch.batchOperation,
6234
+ }),
6235
+ },
6236
+ );
4996
6237
  return await this.callToolAPI(
4997
6238
  batch.batchOperation,
4998
6239
  batch.batchPayload,
4999
6240
  {
6241
+ durableCallReceiptKey: aggregateReceiptKey,
6242
+ executionAuthScopeDigest:
6243
+ batch.memberRequests[0]?.executionAuthScopeDigest ?? null,
6244
+ providerIdempotencyKey:
6245
+ buildDurableToolAggregateProviderIdempotencyKey({
6246
+ aggregateReceiptKey,
6247
+ receiptKeys,
6248
+ providerIdempotencyKeys: batch.memberRequests.map(
6249
+ (request) =>
6250
+ this.providerIdempotencyKeyForToolCall({
6251
+ cacheKey: request.cacheKey,
6252
+ force: request.force === true,
6253
+ leaseId: request.receiptLeaseId,
6254
+ }),
6255
+ ),
6256
+ }),
5000
6257
  timeoutMs: resolveRuntimeTimeoutMsForClaimedOwners(
5001
6258
  batch.memberRequests,
5002
6259
  ),
@@ -5042,13 +6299,9 @@ export class PlayContextImpl {
5042
6299
  },
5043
6300
  });
5044
6301
  } else {
5045
- // Bounded dispatch wave (postgres_fast parity with workers_edge):
5046
- // an unbatched tool group can carry a whole chunk's rows, so cap the
5047
- // provider calls dispatched before the latch is read again to the
5048
- // wave size. Respect a lower provider-pacing suggestion when present.
5049
- const batchSize = Math.min(
5050
- await this.governor.suggestedParallelism(toolId, 50),
5051
- PROVIDER_DISPATCH_WAVE_SIZE,
6302
+ const batchSize = await this.governor.suggestedParallelism(
6303
+ toolId,
6304
+ this.governor.policy.pacing.suggestedMaxParallelism,
5052
6305
  );
5053
6306
  for (
5054
6307
  let start = 0;
@@ -5071,10 +6324,24 @@ export class PlayContextImpl {
5071
6324
  return;
5072
6325
  }
5073
6326
  try {
6327
+ await this.assertRuntimeToolReceiptOwnership([request]);
5074
6328
  const execution = await this.callToolExecutionAPI(
5075
6329
  toolId,
5076
6330
  request.input,
5077
6331
  {
6332
+ ...(request.receiptKey
6333
+ ? {
6334
+ durableCallReceiptKey: request.receiptKey,
6335
+ executionAuthScopeDigest:
6336
+ request.executionAuthScopeDigest,
6337
+ providerIdempotencyKey:
6338
+ this.providerIdempotencyKeyForToolCall({
6339
+ cacheKey: request.receiptKey,
6340
+ force: request.force === true,
6341
+ leaseId: request.receiptLeaseId,
6342
+ }),
6343
+ }
6344
+ : {}),
5078
6345
  timeoutMs: resolveRuntimeTimeoutMsForClaimedOwners([
5079
6346
  request,
5080
6347
  ]),
@@ -5198,6 +6465,23 @@ export class PlayContextImpl {
5198
6465
  : execution.result;
5199
6466
  }
5200
6467
 
6468
+ private providerIdempotencyKeyForToolCall(input: {
6469
+ cacheKey: string;
6470
+ force?: boolean;
6471
+ leaseId?: string | null;
6472
+ }): string {
6473
+ const fallbackAttemptId =
6474
+ input.force === true && !input.leaseId
6475
+ ? `${this.currentRunId}:${crypto.randomUUID()}`
6476
+ : null;
6477
+ return buildDurableToolProviderIdempotencyKey({
6478
+ receiptKey: input.cacheKey,
6479
+ force: input.force,
6480
+ receiptLeaseId: input.leaseId,
6481
+ fallbackAttemptId,
6482
+ });
6483
+ }
6484
+
5201
6485
  private async callToolExecutionAPI(
5202
6486
  toolId: string,
5203
6487
  input: Record<string, unknown>,
@@ -5240,6 +6524,20 @@ export class PlayContextImpl {
5240
6524
 
5241
6525
  while (true) {
5242
6526
  let response: Response;
6527
+ const durableCallReceiptKey =
6528
+ options?.durableCallReceiptKey?.trim() || null;
6529
+ const executionAuthScopeDigest =
6530
+ durableCallReceiptKey && options?.executionAuthScopeDigest
6531
+ ? options.executionAuthScopeDigest.trim() || null
6532
+ : durableCallReceiptKey
6533
+ ? ((await this.resolveToolAuthScopeDigest(toolId))?.trim() ??
6534
+ null)
6535
+ : null;
6536
+ const providerIdempotencyKey =
6537
+ options?.providerIdempotencyKey?.trim() || durableCallReceiptKey;
6538
+ const deeplineRequestId = providerIdempotencyKey
6539
+ ? `ctx-tool-${stableDigest(providerIdempotencyKey).slice(0, 32)}`
6540
+ : null;
5243
6541
  const abortController = timeoutMs ? new AbortController() : null;
5244
6542
  const timeoutHandle = timeoutMs
5245
6543
  ? setTimeout(() => {
@@ -5261,6 +6559,12 @@ export class PlayContextImpl {
5261
6559
  V2_EXECUTE_RESPONSE_CONTRACT,
5262
6560
  [EXECUTE_RESPONSE_INTENT_HEADER]: 'dataset',
5263
6561
  [EXECUTE_TOOL_METADATA_HEADER]: 'true',
6562
+ ...(deeplineRequestId
6563
+ ? {
6564
+ 'x-deepline-request-id': deeplineRequestId,
6565
+ 'x-deepline-idempotency-key': providerIdempotencyKey!,
6566
+ }
6567
+ : {}),
5264
6568
  ...(this.#options.vercelProtectionBypassToken?.trim()
5265
6569
  ? {
5266
6570
  'x-vercel-protection-bypass':
@@ -5272,6 +6576,17 @@ export class PlayContextImpl {
5272
6576
  payload: input,
5273
6577
  metadata: {
5274
6578
  parent_run_id: this.#options.runId,
6579
+ ...(durableCallReceiptKey
6580
+ ? {
6581
+ durable_call_receipt_key: durableCallReceiptKey,
6582
+ ...(executionAuthScopeDigest
6583
+ ? {
6584
+ execution_auth_scope_digest:
6585
+ executionAuthScopeDigest,
6586
+ }
6587
+ : {}),
6588
+ }
6589
+ : {}),
5275
6590
  ...(options?.customerDbDataset
5276
6591
  ? {
5277
6592
  query_result_dataset: {
@@ -5293,6 +6608,9 @@ export class PlayContextImpl {
5293
6608
  : {}),
5294
6609
  }
5295
6610
  : {}),
6611
+ ...(providerIdempotencyKey
6612
+ ? { provider_idempotency_key: providerIdempotencyKey }
6613
+ : {}),
5296
6614
  },
5297
6615
  ...(this.#options.integrationMode
5298
6616
  ? { integration_mode: this.#options.integrationMode }
@@ -5337,6 +6655,13 @@ export class PlayContextImpl {
5337
6655
 
5338
6656
  if (!response.ok) {
5339
6657
  const text = await response.text();
6658
+ const authScopeChanged = parseToolExecuteAuthScopeChangedError({
6659
+ status: response.status,
6660
+ bodyText: text,
6661
+ });
6662
+ if (authScopeChanged) {
6663
+ throw authScopeChanged;
6664
+ }
5340
6665
  const httpFailureAttempt = httpFailureAttempts.next({
5341
6666
  toolId,
5342
6667
  status: response.status,