deepline 0.3.392 → 0.3.394

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 (153) hide show
  1. package/dist/bundling-sources/sdk/src/client.ts +156 -24
  2. package/dist/bundling-sources/sdk/src/compat.ts +9 -0
  3. package/dist/bundling-sources/sdk/src/http.ts +5 -1
  4. package/dist/bundling-sources/sdk/src/index.ts +5 -0
  5. package/dist/bundling-sources/sdk/src/plays/run-request.ts +182 -6
  6. package/dist/bundling-sources/sdk/src/release.ts +4 -1
  7. package/dist/bundling-sources/sdk/src/types.ts +13 -0
  8. package/dist/bundling-sources/shared_libs/integration-contracts/request-warnings.ts +64 -0
  9. package/dist/bundling-sources/shared_libs/play-data-plane/runtime-sheet-storage-contract.ts +315 -0
  10. package/dist/bundling-sources/shared_libs/play-runtime/async-operation.ts +41 -8
  11. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +895 -54
  12. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +41 -4
  13. package/dist/bundling-sources/shared_libs/play-runtime/durable-call-policy.ts +28 -0
  14. package/dist/bundling-sources/shared_libs/play-runtime/durable-invocation-fence.ts +36 -0
  15. package/dist/bundling-sources/shared_libs/play-runtime/freshness-receipt-execution.ts +251 -0
  16. package/dist/bundling-sources/shared_libs/play-runtime/map-execution-frame.ts +11 -3
  17. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/projection/gateway-adapter.ts +1 -0
  18. package/dist/bundling-sources/shared_libs/play-runtime/play-runtime-semantics.ts +30 -1
  19. package/dist/bundling-sources/shared_libs/play-runtime/run-identity.ts +26 -0
  20. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +168 -10
  21. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +252 -10
  22. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +92 -58
  23. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-pool.ts +7 -1
  24. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-progress.ts +30 -7
  25. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +1161 -316
  26. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-event-search.ts +28 -15
  27. package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +24 -3
  28. package/dist/bundling-sources/shared_libs/play-runtime/tool-result.ts +32 -0
  29. package/dist/bundling-sources/shared_libs/plays/authoring-contract.ts +103 -23
  30. package/dist/bundling-sources/shared_libs/plays/core.ts +1 -1
  31. package/dist/bundling-sources/shared_libs/plays/dataset.ts +172 -79
  32. package/dist/bundling-sources/shared_libs/plays/static-pipeline.ts +173 -1
  33. package/dist/bundling-sources/shared_libs/plays/tool-result-types.ts +18 -0
  34. package/dist/cli/auth-IHKISLJQ.js +39 -0
  35. package/dist/cli/{auth-6JSA2ZCW.mjs → auth-OSXDAVCF.mjs} +4 -4
  36. package/dist/cli/{billing-Q5BLQHIU.js → billing-EKLYOXUY.js} +101 -74
  37. package/dist/cli/{billing-MRRO643H.mjs → billing-ZNNKX3UR.mjs} +32 -5
  38. package/dist/cli/{chunk-4SFPVW7A.js → chunk-2MDDA7A4.js} +26 -26
  39. package/dist/cli/{chunk-JOFPQGXW.js → chunk-354JCXA2.js} +50 -50
  40. package/dist/cli/{chunk-7E66A24V.mjs → chunk-3HU47EGZ.mjs} +71 -9
  41. package/dist/cli/{chunk-DSWSEHRX.mjs → chunk-3HWALOL2.mjs} +1 -1
  42. package/dist/cli/{chunk-T7OKJ2T6.js → chunk-4P27AXEA.js} +73 -63
  43. package/dist/cli/{chunk-24VEN5PQ.mjs → chunk-5BB2RGAX.mjs} +190 -18
  44. package/dist/cli/{chunk-JSLPHFP7.js → chunk-6YAUMG27.js} +3 -2
  45. package/dist/cli/{chunk-MU6M3URX.mjs → chunk-BP33SIZF.mjs} +1 -1
  46. package/dist/cli/{chunk-WMS5U6YB.js → chunk-CS6RAIVY.js} +20 -19
  47. package/dist/cli/{chunk-CVLM3WMJ.js → chunk-E23BQ5SC.js} +242 -183
  48. package/dist/cli/{chunk-2GAHHSLB.mjs → chunk-FJGW5GXU.mjs} +3 -2
  49. package/dist/cli/{chunk-YN6YA7ZZ.mjs → chunk-JHIJIHFB.mjs} +79 -20
  50. package/dist/cli/{chunk-Q6EOHNRH.mjs → chunk-JI2B4RZP.mjs} +119 -6
  51. package/dist/cli/{chunk-5O4B6AUN.js → chunk-KB4SDX5S.js} +71 -9
  52. package/dist/cli/{chunk-RV6ALZIX.js → chunk-KGNTKMRG.js} +32 -7
  53. package/dist/cli/{chunk-6DBOU7UH.js → chunk-KI635JN4.js} +176 -87
  54. package/dist/cli/{chunk-2FQG2HU4.js → chunk-KIUMEOM5.js} +8 -8
  55. package/dist/cli/{chunk-Y6VOGHF2.mjs → chunk-KZZC33PY.mjs} +0 -181
  56. package/dist/cli/{chunk-2Q3GEFL4.mjs → chunk-M57J2SK4.mjs} +2 -2
  57. package/dist/cli/{chunk-R3O2CZ4H.mjs → chunk-MFB2VKBO.mjs} +2 -2
  58. package/dist/cli/{chunk-QGKPTBRR.js → chunk-MH73HMPV.js} +3 -3
  59. package/dist/cli/{chunk-BB3FNYFY.js → chunk-MLPU5SMZ.js} +2 -2
  60. package/dist/cli/{chunk-2G7DXFHI.mjs → chunk-NR3DFKNF.mjs} +27 -2
  61. package/dist/cli/{chunk-EY7RFC6A.js → chunk-QIQBYKLB.js} +15 -15
  62. package/dist/cli/{chunk-RIV2GS5E.js → chunk-RBJYWRCF.js} +638 -466
  63. package/dist/cli/{chunk-3VN4FTE3.js → chunk-RDXFGPJU.js} +1 -182
  64. package/dist/cli/{chunk-YAZCDBXX.mjs → chunk-TJI2EN6K.mjs} +3 -2
  65. package/dist/cli/chunk-UBKA33KY.mjs +310 -0
  66. package/dist/cli/{chunk-PAIMOGCM.js → chunk-UHNDVD3P.js} +125 -12
  67. package/dist/cli/{chunk-4TCT77UF.js → chunk-URWF7NGQ.js} +6 -1
  68. package/dist/cli/{chunk-2QLML6G4.mjs → chunk-UXFSWG3Q.mjs} +4 -4
  69. package/dist/cli/{chunk-UCKJQG25.js → chunk-VK6FYZLP.js} +8 -8
  70. package/dist/cli/{chunk-ZTACINO5.mjs → chunk-X63FDZJZ.mjs} +13 -3
  71. package/dist/cli/chunk-XDVT3GOM.js +310 -0
  72. package/dist/cli/{chunk-A3APGZFP.mjs → chunk-XRQ2AQL7.mjs} +2 -2
  73. package/dist/cli/{chunk-3FV4GBRK.mjs → chunk-XXUDVEXK.mjs} +97 -8
  74. package/dist/cli/{chunk-PQSA443V.mjs → chunk-ZI2ZNDJK.mjs} +6 -1
  75. package/dist/cli/{chunk-HGYBEAQP.mjs → chunk-ZYBGA4AR.mjs} +3 -3
  76. package/dist/cli/command-docs.js +2 -2
  77. package/dist/cli/command-docs.mjs +1 -1
  78. package/dist/cli/{csv-E4QGUIGI.mjs → csv-5AXRB35H.mjs} +3 -3
  79. package/dist/cli/{csv-26U5T24Z.js → csv-LU7O75WE.js} +12 -12
  80. package/dist/cli/{db-JTRJIAQW.mjs → db-7XP5UWEW.mjs} +6 -5
  81. package/dist/cli/{db-QHBFLMVL.js → db-F7YTFKVM.js} +31 -30
  82. package/dist/cli/{describe-FDV2V74F.js → describe-FRH32RDV.js} +24 -23
  83. package/dist/cli/{describe-E3DE3ZK3.mjs → describe-K4ES5LIL.mjs} +17 -16
  84. package/dist/cli/{executions-B3GTNPRE.mjs → executions-IEHEHJUO.mjs} +6 -5
  85. package/dist/cli/{executions-VZYOGVQY.js → executions-RZV5XPT2.js} +8 -7
  86. package/dist/cli/{feedback-CR55FJA2.js → feedback-3P65QTX3.js} +7 -7
  87. package/dist/cli/{feedback-4CEBR3OV.mjs → feedback-FPUVZBAN.mjs} +3 -3
  88. package/dist/cli/{git-NM2PHHZ4.mjs → git-GLVVP7TY.mjs} +4 -4
  89. package/dist/cli/git-MFFAWVVX.js +23 -0
  90. package/dist/cli/index.js +100 -96
  91. package/dist/cli/index.mjs +47 -43
  92. package/dist/cli/{legacy-noops-A7MSW5ND.mjs → legacy-noops-3MOI6NBM.mjs} +4 -4
  93. package/dist/cli/{legacy-noops-ZUA4YFIH.js → legacy-noops-XKSGZR7Q.js} +7 -7
  94. package/dist/cli/{monitors-NVPRQYP3.mjs → monitors-FTBGNEFG.mjs} +6 -5
  95. package/dist/cli/{monitors-ZE3TLZ4H.js → monitors-NZ2DYNST.js} +87 -86
  96. package/dist/cli/{org-U7A6DV5D.js → org-MCKDQXIV.js} +66 -65
  97. package/dist/cli/{org-KUBILNQV.mjs → org-V2J5RBCN.mjs} +6 -5
  98. package/dist/cli/{play-HRDXQ4QE.mjs → play-AJFGME3B.mjs} +11 -10
  99. package/dist/cli/play-WLSN2EO2.js +158 -0
  100. package/dist/cli/{providers-VD6YTVQZ.js → providers-2JEZ2264.js} +9 -8
  101. package/dist/cli/{providers-OHIWI453.mjs → providers-I6RQJZFZ.mjs} +6 -5
  102. package/dist/cli/{quickstart-UBV2FONF.mjs → quickstart-BIDQDMO4.mjs} +3 -3
  103. package/dist/cli/{quickstart-ETTCWPA4.js → quickstart-GJ2RPDU3.js} +5 -5
  104. package/dist/cli/run-identity-G257GQED.js +25 -0
  105. package/dist/cli/run-identity-MRNCEGV2.mjs +25 -0
  106. package/dist/cli/search-5VN4YD4I.js +27 -0
  107. package/dist/cli/{search-F44IZRT7.mjs → search-ELRXFWG5.mjs} +7 -6
  108. package/dist/cli/{secrets-CAEMRORF.js → secrets-53IT6VXD.js} +15 -14
  109. package/dist/cli/{secrets-R6XOJZKU.mjs → secrets-SQXGZXIC.mjs} +6 -5
  110. package/dist/cli/{sessions-RLN6UQS3.mjs → sessions-2X5QF7SL.mjs} +4 -4
  111. package/dist/cli/{sessions-6GC7OFTT.js → sessions-CAV5M77Y.js} +5 -5
  112. package/dist/cli/{settings-DPNVUAV2.js → settings-532WNAZO.js} +76 -75
  113. package/dist/cli/{settings-KIPAMRPP.mjs → settings-7P67VGCJ.mjs} +9 -8
  114. package/dist/cli/{setup-DTL3L5SG.js → setup-6L2VSUWJ.js} +41 -41
  115. package/dist/cli/{setup-TU2F7PU5.mjs → setup-QGGBV2LE.mjs} +6 -6
  116. package/dist/cli/{skills-UV5MYJ46.mjs → skills-3FE62WBG.mjs} +4 -4
  117. package/dist/cli/skills-S6NPZAN7.js +28 -0
  118. package/dist/cli/tools-647RLXQS.js +41 -0
  119. package/dist/cli/{tools-QRRNDCQ7.mjs → tools-DMR25DEF.mjs} +10 -9
  120. package/dist/cli/{update-4426TF35.mjs → update-6RZMHJZP.mjs} +6 -6
  121. package/dist/cli/update-WVTSC3EM.js +28 -0
  122. package/dist/cli/{workflow-N4U5VLZ3.mjs → workflow-L2DQRE6R.mjs} +7 -6
  123. package/dist/cli/{workflow-HNLOVISB.js → workflow-TX4F2PIX.js} +17 -16
  124. package/dist/{client-e6Qh5ZIg.d.mts → client-BktIPdIj.d.mts} +153 -4
  125. package/dist/{client-Uxuy-xQ2.d.ts → client-C3O1Ipf3.d.ts} +153 -4
  126. package/dist/{compiler-manifest-CY2v8ab0.d.ts → compiler-manifest-DLxUY5kI.d.mts} +124 -10
  127. package/dist/{compiler-manifest-CY2v8ab0.d.mts → compiler-manifest-DLxUY5kI.d.ts} +124 -10
  128. package/dist/{index-BZ6uzZJl.d.ts → index-B_GM9E3q.d.ts} +1 -1
  129. package/dist/{index-B9fBZH1g.d.mts → index-SCb0NvDb.d.mts} +1 -1
  130. package/dist/index.d.mts +5 -5
  131. package/dist/index.d.ts +5 -5
  132. package/dist/index.js +120 -29
  133. package/dist/index.mjs +111 -20
  134. package/dist/install-integrity.json +94 -88
  135. package/dist/plays/bundle-play-file.d.mts +3 -3
  136. package/dist/plays/bundle-play-file.d.ts +3 -3
  137. package/dist/plays/bundle-play-file.mjs +2 -2
  138. package/dist/plays/run-request.d.mts +2 -2
  139. package/dist/plays/run-request.d.ts +2 -2
  140. package/dist/plays/run-request.js +5 -2
  141. package/dist/plays/run-request.mjs +4 -1
  142. package/dist/release.d.mts +4 -1
  143. package/dist/release.d.ts +4 -1
  144. package/dist/release.js +2 -2
  145. package/dist/release.mjs +1 -1
  146. package/package.json +1 -1
  147. package/dist/cli/auth-67TAELQL.js +0 -39
  148. package/dist/cli/git-I5JTK6YU.js +0 -23
  149. package/dist/cli/play-A2V7YDU7.js +0 -157
  150. package/dist/cli/search-WWXMBVPH.js +0 -26
  151. package/dist/cli/skills-SWOSEZLB.js +0 -28
  152. package/dist/cli/tools-IL2YCDQ6.js +0 -40
  153. package/dist/cli/update-BERDPEYR.js +0 -28
@@ -25,6 +25,10 @@ import { AsyncLocalStorage } from 'async_hooks';
25
25
  import { isDatasetPayloadField } from '../play-data-plane/sheet-contract';
26
26
  import {
27
27
  createDeferredPlayDataset,
28
+ copyPlayDatasetSourceRowIdentity,
29
+ getPlayDatasetSourceRowIdentity,
30
+ getPlayDatasetSourceRowIdentityAt,
31
+ setPlayDatasetSourceRowIdentity,
28
32
  deserializeLegacyPlayDataset,
29
33
  deserializePlayDatasetCell,
30
34
  isPlayDataset,
@@ -32,6 +36,7 @@ import {
32
36
  isSerializedPlayDatasetCell,
33
37
  iteratePlayDatasetInputPages,
34
38
  materializePlayDatasetInput,
39
+ residentPlayDatasetRows,
35
40
  resolveMaterializeLimitCap,
36
41
  serializePlayDatasetCell,
37
42
  } from '../plays/dataset';
@@ -69,7 +74,10 @@ import type {
69
74
  ProviderPacingGrant,
70
75
  } from './governor/rate-state-backend';
71
76
  import type { MapRowOutcome } from './durability-store';
72
- import { stringifyPostgresJson } from './postgres-json';
77
+ import {
78
+ sanitizePostgresJsonValue,
79
+ stringifyPostgresJson,
80
+ } from './postgres-json';
73
81
  import { providerCapacityLabel } from './activity-observation';
74
82
  import { RUNTIME_ROW_OUTPUT_NOT_JSON_SERIALIZABLE } from './runtime-sheet-row-transition';
75
83
  import {
@@ -91,7 +99,10 @@ import {
91
99
  import {
92
100
  DURABLE_INVOCATION_FENCE_VERSION,
93
101
  durableInvocationFenceExecutePath,
102
+ buildRuntimeExecutionRecoveryKey,
103
+ RUNTIME_EXECUTION_RECOVERY_SUPPORTED_HEADER,
94
104
  } from './durable-invocation-fence';
105
+ import { isImmutableRuntimeExecutionRecoveryOrigin } from './scheduler-backends/runtime-api-origin';
95
106
  import {
96
107
  isRetryableSecretResolutionStatus,
97
108
  secretResolutionRetryDecision,
@@ -184,6 +195,7 @@ import {
184
195
  type ToolExecuteResult,
185
196
  type ToolResultExecutionMetadata,
186
197
  type ToolResultMetadataInput,
198
+ type CallFreshnessMetadata,
187
199
  } from './tool-result';
188
200
  import {
189
201
  EXECUTION_GATEWAY_RECEIPT_OUTPUT_REFERENCE_HEADER,
@@ -258,9 +270,12 @@ import {
258
270
  scopeToolReceiptToInvocation,
259
271
  resolveDurableCallCachePolicy,
260
272
  } from './durable-call-cache';
273
+ import { resolveExplicitDurableCallPolicy } from './durable-call-policy';
274
+ import { executeWithFreshnessReceipt } from './freshness-receipt-execution';
261
275
  import {
262
276
  CtxFetchHttpError,
263
277
  PLAY_AUTHORING_CONTRACT_EDITION,
278
+ PlayAuthoringFieldValidationError,
264
279
  normalizePlayAuthoringCustomerDbStatement,
265
280
  validateOptionalPlayAuthoringField,
266
281
  validatePlayAuthoringField,
@@ -304,9 +319,11 @@ import {
304
319
  classifyAsyncOperationResult,
305
320
  executeAsyncOperationLifecycle,
306
321
  extractAsyncOperationJobId,
322
+ formatAsyncOperationTimeout,
307
323
  readAsyncOperationPath,
308
324
  asyncOperationShouldWait,
309
325
  hasExplicitAsyncOperationTerminalMatch,
326
+ resolveAsyncOperationPolling,
310
327
  type AsyncOperationContractWire,
311
328
  } from './async-operation';
312
329
  import {
@@ -626,6 +643,7 @@ const inlineCompositionContext =
626
643
  const authoredStepContext = new AsyncLocalStorage<{
627
644
  context: PlayContextImpl;
628
645
  stepId: string;
646
+ invocationId?: string;
629
647
  }>();
630
648
  const toolExecutionOverrides = new AsyncLocalStorage<{ force: boolean }>();
631
649
  const PROGRESS_HEARTBEAT_INTERVAL_MS = 1_000;
@@ -2918,6 +2936,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
2918
2936
  private mapInvocationIndex = 0;
2919
2937
  private readonly stepCallIndexByKey = new Map<string, number>();
2920
2938
  private readonly toolCallIndexByKey = new Map<string, number>();
2939
+ private readonly freshnessCallIndexByKey = new Map<string, number>();
2921
2940
  /**
2922
2941
  * Parent-level inline-child aggregates. Maintained only by the single-writer
2923
2942
  * progress path (never per child) so concurrent fan-out cannot contend. See
@@ -3195,7 +3214,10 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
3195
3214
  key: string,
3196
3215
  toolId: string,
3197
3216
  input: Record<string, unknown>,
3198
- options?: { description?: string },
3217
+ options?: {
3218
+ description?: string;
3219
+ staleAfterSeconds?: number | null;
3220
+ },
3199
3221
  ): Promise<ToolExecuteResult<TOutput>> {
3200
3222
  validatePlayAuthoringField('ctx.tool.key', key);
3201
3223
  validatePlayAuthoringField('ctx.tool.tool', toolId);
@@ -3211,6 +3233,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
3211
3233
  tool: toolId,
3212
3234
  input,
3213
3235
  ...(options?.description ? { description: options.description } : {}),
3236
+ ...(options?.staleAfterSeconds !== undefined
3237
+ ? { staleAfterSeconds: options.staleAfterSeconds }
3238
+ : {}),
3214
3239
  })) as ToolExecuteResult<TOutput>;
3215
3240
  }
3216
3241
 
@@ -3220,9 +3245,41 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
3220
3245
  readonly customerDb = {
3221
3246
  query: async <TRow extends object = Record<string, unknown>>(
3222
3247
  statement: PlaySqlQuery | string,
3223
- options?: { maxRows?: number; timeoutMs?: number },
3248
+ options?: {
3249
+ id?: string;
3250
+ staleAfterSeconds?: number | null;
3251
+ maxRows?: number;
3252
+ timeoutMs?: number;
3253
+ },
3224
3254
  ): Promise<TRow[]> => {
3225
3255
  const sql = normalizePlayAuthoringCustomerDbStatement(statement);
3256
+ if (this.usesExplicitFreshness) {
3257
+ validatePlayAuthoringField(
3258
+ 'ctx.customerDb.query.options.id',
3259
+ options?.id,
3260
+ );
3261
+ } else if (options?.id !== undefined) {
3262
+ validatePlayAuthoringField(
3263
+ 'ctx.customerDb.query.options.id',
3264
+ options.id,
3265
+ );
3266
+ }
3267
+ if (options?.staleAfterSeconds !== undefined) {
3268
+ validatePlayAuthoringField(
3269
+ 'ctx.customerDb.query.options.staleAfterSeconds',
3270
+ options.staleAfterSeconds,
3271
+ );
3272
+ }
3273
+ if (
3274
+ this.usesExplicitFreshness &&
3275
+ options?.staleAfterSeconds === undefined
3276
+ ) {
3277
+ throw new PlayAuthoringFieldValidationError(
3278
+ 'ctx.customerDb.query.options.staleAfterSeconds',
3279
+ 'play_authoring_durable_policy_invalid',
3280
+ 'Edition 11 Customer DB queries require staleAfterSeconds: 0.',
3281
+ );
3282
+ }
3226
3283
  if (options?.maxRows !== undefined) {
3227
3284
  validatePlayAuthoringField(
3228
3285
  'ctx.customerDb.query.options.maxRows',
@@ -3236,7 +3293,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
3236
3293
  );
3237
3294
  }
3238
3295
  const result = (await this.tools.execute({
3239
- id: `customer_db_query_${this.customerDbQueryIndex++}`,
3296
+ id: options?.id ?? `customer_db_query_${this.customerDbQueryIndex++}`,
3240
3297
  tool: 'query_customer_db',
3241
3298
  input: {
3242
3299
  sql,
@@ -3247,7 +3304,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
3247
3304
  description: 'Customer DB query',
3248
3305
  // The direct executor never reused a prior SQL result. Preserve that
3249
3306
  // imperative behavior for reads and mutations alike.
3250
- force: true,
3307
+ ...(this.usesExplicitFreshness
3308
+ ? { staleAfterSeconds: options?.staleAfterSeconds }
3309
+ : { force: true }),
3251
3310
  ...(options?.timeoutMs !== undefined
3252
3311
  ? { timeoutMs: options.timeoutMs }
3253
3312
  : {}),
@@ -3853,6 +3912,95 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
3853
3912
  return this.currentRuntimeContract.semantics;
3854
3913
  }
3855
3914
 
3915
+ private get usesExplicitFreshness(): boolean {
3916
+ return (
3917
+ this.currentRuntimeSemantics.freshness ===
3918
+ 'actual_age_with_operation_recovery'
3919
+ );
3920
+ }
3921
+
3922
+ private freshnessInvocationId(operation: string, id: string): string {
3923
+ const row = rowContext.getStore();
3924
+ const step = authoredStepContext.getStore();
3925
+ const scope = stableStringify({
3926
+ namespace: this.currentExecutionScope.receipt.namespace,
3927
+ operation,
3928
+ id,
3929
+ step: step?.context === this ? (step.invocationId ?? step.stepId) : null,
3930
+ row: row
3931
+ ? {
3932
+ table: row.tableNamespace ?? null,
3933
+ key: row.rowKey ?? row.rowId,
3934
+ field: row.fieldName ?? null,
3935
+ }
3936
+ : null,
3937
+ });
3938
+ const freshIndex = this.freshnessCallIndexByKey.get(scope) ?? 0;
3939
+ this.freshnessCallIndexByKey.set(scope, freshIndex + 1);
3940
+ // Invocation identity follows authored scope and call order. Keep it
3941
+ // separate from the request/auth/action fingerprint used by reusable
3942
+ // result caching, so credential or response-contract changes cannot mint
3943
+ // a second provider operation key for the same authored effect.
3944
+ // A mapped row can be re-entered after provider backpressure; its replay
3945
+ // cursor restores the original call position.
3946
+ const index = nextReplayableCallIndex(`freshness:${scope}`, freshIndex);
3947
+ return sha256Hex(`${scope}:${index}`);
3948
+ }
3949
+
3950
+ private async executeFreshnessControlledCall<T>(input: {
3951
+ operation: DurableCtxOperation;
3952
+ id: string;
3953
+ semanticKey: string;
3954
+ invocationId: string;
3955
+ policy: { requested: number | null; effective: number | null };
3956
+ execute: () => Promise<T>;
3957
+ completedAt?: (value: T) => number | undefined;
3958
+ onOutcome?: (value: T, metadata: CallFreshnessMetadata) => T;
3959
+ }): Promise<T> {
3960
+ const orgId = this.#options.orgId?.trim();
3961
+ if (!orgId) {
3962
+ throw new Error(
3963
+ 'Freshness-controlled calls require a resolved organization identity.',
3964
+ );
3965
+ }
3966
+ return executeWithFreshnessReceipt({
3967
+ orgId,
3968
+ semanticKey: input.semanticKey,
3969
+ invocationId: input.invocationId,
3970
+ runId: this.currentRunId,
3971
+ requestedStaleAfterSeconds: input.policy.requested,
3972
+ effectiveStaleAfterSeconds: input.policy.effective,
3973
+ store: this.durableReceiptExecutionStore(),
3974
+ completedAt: input.completedAt,
3975
+ withHydration: (read) =>
3976
+ this.withRuntimeReceiptHydrationTurn(
3977
+ (turn) =>
3978
+ read((receipt) =>
3979
+ turn.settleBytes(
3980
+ runtimeStepReceiptPayloadBytes(receipt ? [receipt] : []),
3981
+ ),
3982
+ ),
3983
+ { source: 'durable_operation' },
3984
+ ),
3985
+ execute: async () => {
3986
+ const value = await input.execute();
3987
+ this.assertNoPendingDurableSuspension();
3988
+ return value;
3989
+ },
3990
+ onOutcome: (value, metadata) => {
3991
+ this.runtimeLog(
3992
+ JSON.stringify({
3993
+ event: 'call_freshness',
3994
+ operation: input.operation,
3995
+ call_key: input.id,
3996
+ ...metadata,
3997
+ }),
3998
+ );
3999
+ return input.onOutcome ? input.onOutcome(value, metadata) : value;
4000
+ },
4001
+ });
4002
+ }
4003
+
3856
4004
  private recordPlayCallStep(input: {
3857
4005
  playId: string;
3858
4006
  execution?: 'inline';
@@ -6284,6 +6432,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
6284
6432
  force?: boolean;
6285
6433
  receiptKey?: string | null;
6286
6434
  semanticKey?: string | null;
6435
+ invocationId?: string;
6287
6436
  staleAfterSeconds?: number | null;
6288
6437
  repairRunningReceiptForSameRun?: boolean;
6289
6438
  repairRunningReceiptForSameRunAfterWaitTimeout?: boolean;
@@ -6327,6 +6476,15 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
6327
6476
  },
6328
6477
  });
6329
6478
  if (opts.transient === true) {
6479
+ if (
6480
+ this.usesExplicitFreshness &&
6481
+ opts.staleAfterSeconds !== undefined &&
6482
+ opts.staleAfterSeconds !== 0
6483
+ ) {
6484
+ throw new Error(
6485
+ 'Transient fetch does not support retained result reuse; omit freshness or choose staleAfterSeconds: 0.',
6486
+ );
6487
+ }
6330
6488
  try {
6331
6489
  return await execute(null);
6332
6490
  } finally {
@@ -6334,6 +6492,44 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
6334
6492
  release?.();
6335
6493
  }
6336
6494
  }
6495
+ if (this.usesExplicitFreshness && operation === 'fetch') {
6496
+ const policy = resolveExplicitDurableCallPolicy(opts);
6497
+ const semanticKey = stableStringify({
6498
+ orgId: this.#options.orgId,
6499
+ namespace: this.currentExecutionScope.receipt.namespace,
6500
+ operation,
6501
+ id,
6502
+ semanticKey: opts.semanticKey,
6503
+ });
6504
+ try {
6505
+ return await this.executeFreshnessControlledCall({
6506
+ operation,
6507
+ id,
6508
+ semanticKey,
6509
+ invocationId:
6510
+ opts.invocationId ?? this.freshnessInvocationId(operation, id),
6511
+ policy,
6512
+ execute: () => execute(null),
6513
+ onOutcome: (value, freshness) => {
6514
+ const output =
6515
+ opts.onRecovered && freshness.source !== 'executed'
6516
+ ? opts.onRecovered(
6517
+ value,
6518
+ {
6519
+ key: freshness.operationReceiptKey!,
6520
+ status: 'completed',
6521
+ },
6522
+ 'cache',
6523
+ )
6524
+ : value;
6525
+ return { ...output, freshness };
6526
+ },
6527
+ });
6528
+ } finally {
6529
+ const release = releaseExternalCallSlot as (() => void) | null;
6530
+ release?.();
6531
+ }
6532
+ }
6337
6533
  const stalePolicy = resolveDurableCallCachePolicy(
6338
6534
  opts.staleAfterSeconds,
6339
6535
  operation === 'step'
@@ -8206,6 +8402,10 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
8206
8402
  options.onRowError,
8207
8403
  );
8208
8404
  validateOptionalPlayAuthoringField('ctx.dataset.run.mode', options.mode);
8405
+ validateOptionalPlayAuthoringField(
8406
+ 'ctx.dataset.run.rowIdentity',
8407
+ options.rowIdentity,
8408
+ );
8209
8409
  validateOptionalPlayAuthoringField(
8210
8410
  'ctx.dataset.run.undrawnColumns',
8211
8411
  options.undrawnColumns,
@@ -8260,11 +8460,53 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
8260
8460
  ),
8261
8461
  );
8262
8462
 
8463
+ const residentSourceRows = isPlayDataset(items)
8464
+ ? residentPlayDatasetRows(items)
8465
+ : Array.isArray(items)
8466
+ ? items
8467
+ : null;
8468
+ const csvSource =
8469
+ (isPlayDataset(items) && items.datasetKind === 'csv') ||
8470
+ residentSourceRows?.some(
8471
+ (_row, index) =>
8472
+ getPlayDatasetSourceRowIdentityAt(residentSourceRows, index) !==
8473
+ undefined,
8474
+ ) === true;
8475
+ const csvOccurrenceIdentityRequired =
8476
+ csvSource &&
8477
+ this.currentRuntimeSemantics.datasets.csvRowIdentity === 'source_row';
8478
+ const rowIdentityMode = csvOccurrenceIdentityRequired
8479
+ ? 'source_row'
8480
+ : (options?.rowIdentity ??
8481
+ (options?.key !== undefined
8482
+ ? 'key'
8483
+ : csvSource
8484
+ ? this.currentRuntimeSemantics.datasets.csvRowIdentity
8485
+ : 'key'));
8486
+ const useSourceRowIdentity = rowIdentityMode === 'source_row';
8263
8487
  const userKeyOption = options?.key;
8264
8488
  let explicitKeyResolver:
8265
8489
  | ((row: Record<string, unknown>, index: number) => string)
8266
8490
  | null = null;
8267
- if (userKeyOption !== undefined) {
8491
+ if (useSourceRowIdentity) {
8492
+ explicitKeyResolver = (row, index) => {
8493
+ const source = getPlayDatasetSourceRowIdentity(row);
8494
+ const inputRow = Object.fromEntries(
8495
+ Object.entries(toSerializableCsvAliasedRow(row)).filter(
8496
+ ([field]) => !field.startsWith('__deepline'),
8497
+ ),
8498
+ );
8499
+ // Position distinguishes equal records; the input digest prevents a
8500
+ // changed file at the same path from replaying an earlier row's output.
8501
+ return JSON.stringify([
8502
+ 'source-row-v1',
8503
+ source?.datasetId ??
8504
+ (isPlayDataset(items) ? items.datasetId : normalizedMapNamespace),
8505
+ source?.rowIndex ?? index,
8506
+ stableDigest(stableStringify(inputRow)),
8507
+ ]);
8508
+ };
8509
+ } else if (userKeyOption !== undefined) {
8268
8510
  explicitKeyResolver = (row, index) => {
8269
8511
  const inputRow = stripFieldOutputs(row);
8270
8512
  const raw =
@@ -8469,6 +8711,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
8469
8711
  let failedCount = 0;
8470
8712
  let duplicateReuseCount = 0;
8471
8713
  let executedCount = 0;
8714
+ let sourceRowsCount = 0;
8472
8715
  const failureSamples: Array<{ rowLabel: string; error: string }> = [];
8473
8716
  const failureSampleErrors = new Set<string>();
8474
8717
  const previewRows: Record<string, unknown>[] = [];
@@ -8572,9 +8815,17 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
8572
8815
  offset: number;
8573
8816
  }): Promise<PreparedPage> => {
8574
8817
  this.assertRowAdmissionOpen();
8575
- const pageRawItems = page.rows.map((item) =>
8576
- this.toOutputRow(item as Record<string, unknown>),
8577
- );
8818
+ sourceRowsCount += page.rows.length;
8819
+ const pageRawItems = page.rows.map((item, index) => {
8820
+ const row = this.toOutputRow(item as Record<string, unknown>);
8821
+ const sourceIdentity = getPlayDatasetSourceRowIdentityAt(
8822
+ page.rows,
8823
+ index,
8824
+ );
8825
+ return sourceIdentity
8826
+ ? setPlayDatasetSourceRowIdentity(row, sourceIdentity)
8827
+ : row;
8828
+ });
8578
8829
  const pageMemoryEstimate = estimateRuntimeMapRowsMemory(pageRawItems);
8579
8830
  if (pageMemoryEstimate.estimatedResidentBytes > pageBudgetBytes) {
8580
8831
  throw new RuntimeMapMemoryLimitError({
@@ -8588,6 +8839,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
8588
8839
  const originalIndex = page.offset + index;
8589
8840
  return {
8590
8841
  ...toSerializableCsvAliasedRow(row),
8842
+ ...(getPlayDatasetSourceRowIdentity(row)
8843
+ ? { __deeplineSourceRow: getPlayDatasetSourceRowIdentity(row) }
8844
+ : {}),
8591
8845
  ...mapRowOutcomeRuntimeFields({
8592
8846
  key: seedRowIdentity(row, originalIndex),
8593
8847
  inputIndex: originalIndex,
@@ -8630,9 +8884,8 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
8630
8884
  completedRowsByKey.set(rowKey, row);
8631
8885
  }
8632
8886
 
8633
- const seededItems = page.rows.map((item, index) => {
8887
+ const seededItems = pageRawItems.map((row, index) => {
8634
8888
  const originalIndex = page.offset + index;
8635
- const row = this.toOutputRow(item as Record<string, unknown>);
8636
8889
  const rowKey = seedRowIdentity(row, originalIndex);
8637
8890
  const persisted =
8638
8891
  pendingRowsByKey.get(rowKey) ?? completedRowsByKey.get(rowKey);
@@ -8661,7 +8914,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
8661
8914
  { row: Record<string, unknown>; originalIndex: number }
8662
8915
  >();
8663
8916
  for (const { row, index } of admittedSeededItems) {
8664
- if (explicitKeyResolver) {
8917
+ if (explicitKeyResolver && !useSourceRowIdentity) {
8665
8918
  const explicitKey = explicitKeyResolver(row, index);
8666
8919
  if (seenExplicitKeys.has(explicitKey)) {
8667
8920
  duplicateReuseCount += 1;
@@ -8990,12 +9243,14 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
8990
9243
  );
8991
9244
  successfulCount = visibleResults.completedRows;
8992
9245
  failedCount = visibleResults.failedRows;
8993
- this.#options.onMapRowsSettled?.({
9246
+ await this.#options.onMapRowsSettled?.({
8994
9247
  playName: this.#options.playName,
8995
9248
  runId: this.#options.runId,
8996
9249
  tableNamespace: resolvedTableNamespace,
8997
9250
  succeededRows: successfulCount,
8998
9251
  failedRows: failedCount,
9252
+ expectedRows: totalInputCount - duplicateReuseCount,
9253
+ staticPipeline: this.currentStaticPipeline,
8999
9254
  });
9000
9255
 
9001
9256
  // Isolated failures are a partial result only when at least one admitted
@@ -9181,6 +9436,13 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9181
9436
  skipped: duplicateReuseCount,
9182
9437
  pending: 0,
9183
9438
  failed: failedCount,
9439
+ cardinality: {
9440
+ sourceRows: sourceRowsCount,
9441
+ uniqueInputRows: processedCount - duplicateReuseCount,
9442
+ duplicateOccurrences: duplicateReuseCount,
9443
+ outputRows: successfulCount,
9444
+ rowIdentity: rowIdentityMode,
9445
+ },
9184
9446
  ...(duplicateReuseCount > 0
9185
9447
  ? { duplicates: { exact: duplicateReuseCount } }
9186
9448
  : {}),
@@ -9228,24 +9490,66 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9228
9490
  },
9229
9491
  });
9230
9492
  }
9231
- const rawMaterializedEntries =
9493
+ const rawMaterializedEntries: {
9494
+ item: T;
9495
+ inputIndex: number;
9496
+ sourceIdentity?: ReturnType<typeof getPlayDatasetSourceRowIdentityAt>;
9497
+ }[] =
9232
9498
  resumedPendingRowsOnly && suspendedInputRows
9233
- ? suspendedInputRows.map((row) => ({
9234
- item: cloneCsvAliasedRow(row.data) as T,
9235
- inputIndex: row.inputIndex,
9236
- }))
9237
- : (
9238
- await materializePlayDatasetInput(items, {
9499
+ ? suspendedInputRows.map((row) => {
9500
+ const retainedKey = resolveMapRowOutcomeKey(row.data);
9501
+ return {
9502
+ // Older checkpoints contain only execution data. New checkpoints
9503
+ // also retain the original admission input before sheet outputs
9504
+ // were restored; keep the already-admitted key across either path.
9505
+ item: cloneCsvAliasedRow(
9506
+ row.authoredInput?.data ?? row.data,
9507
+ retainedKey
9508
+ ? mapRowOutcomeRuntimeFields({ key: retainedKey })
9509
+ : {},
9510
+ ) as T,
9511
+ inputIndex: row.inputIndex,
9512
+ sourceIdentity: row.authoredInput?.source,
9513
+ };
9514
+ })
9515
+ : await (async () => {
9516
+ const materialized = await materializePlayDatasetInput(items, {
9239
9517
  onRow: (row) => {
9240
9518
  materializationMemoryTracker.track(row);
9241
9519
  },
9242
- })
9243
- ).map((item, inputIndex) => ({ item, inputIndex }));
9520
+ });
9521
+ return materialized.map((item, inputIndex) => ({
9522
+ item,
9523
+ inputIndex,
9524
+ sourceIdentity: getPlayDatasetSourceRowIdentityAt(
9525
+ materialized,
9526
+ inputIndex,
9527
+ ),
9528
+ }));
9529
+ })();
9244
9530
  const rawMaterializedItems = rawMaterializedEntries.map(
9245
- (entry) => entry.item,
9531
+ // Occurrence identity also applies to an explicitly selected array whose
9532
+ // author reused the same object reference. Give each occurrence its own
9533
+ // execution object so the original-index lookup cannot alias the last row.
9534
+ (entry) => {
9535
+ const sourceIdentity =
9536
+ 'sourceIdentity' in entry
9537
+ ? entry.sourceIdentity
9538
+ : getPlayDatasetSourceRowIdentity(entry.item);
9539
+ if (!useSourceRowIdentity && !sourceIdentity) return entry.item;
9540
+ const row = this.toOutputRow(entry.item);
9541
+ return (
9542
+ sourceIdentity
9543
+ ? setPlayDatasetSourceRowIdentity(row, sourceIdentity)
9544
+ : row
9545
+ ) as T;
9546
+ },
9246
9547
  );
9247
9548
  const originalInputIndexByItem = new Map<T, number>(
9248
- rawMaterializedEntries.map(({ item, inputIndex }) => [item, inputIndex]),
9549
+ rawMaterializedItems.map((item, index) => [
9550
+ item,
9551
+ rawMaterializedEntries[index]!.inputIndex,
9552
+ ]),
9249
9553
  );
9250
9554
  // Silent dedupe of duplicate explicit map keys: keep the first row per
9251
9555
  // canonical key and drop subsequent duplicates. Deduping the materialized
@@ -9253,24 +9557,75 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9253
9557
  // counts, sheet writes, resumable keys) consistent — the resolver derives
9254
9558
  // keys from row content, so dropped rows are excluded everywhere with no
9255
9559
  // index mismatch. Replaces the prior throw-on-duplicate behavior.
9256
- const dedupedMaterialized = explicitKeyResolver
9257
- ? dedupeExplicitMapKeyRows({
9258
- rows: rawMaterializedItems,
9259
- resolver: (item, index) =>
9260
- explicitKeyResolver(
9261
- this.toOutputRow(item as Record<string, unknown>),
9262
- originalInputIndexByItem.get(item) ?? index,
9263
- ),
9264
- })
9265
- : { rows: rawMaterializedItems, droppedCount: 0, duplicateKeys: [] };
9560
+ const dedupedMaterialized =
9561
+ explicitKeyResolver && !useSourceRowIdentity
9562
+ ? dedupeExplicitMapKeyRows({
9563
+ rows: rawMaterializedItems,
9564
+ resolver: (item, index) =>
9565
+ explicitKeyResolver(
9566
+ this.toOutputRow(item as Record<string, unknown>),
9567
+ originalInputIndexByItem.get(item) ?? index,
9568
+ ),
9569
+ })
9570
+ : { rows: rawMaterializedItems, droppedCount: 0, duplicateKeys: [] };
9266
9571
  if (dedupedMaterialized.droppedCount > 0) {
9267
- const keySample = dedupedMaterialized.duplicateKeys.join(', ');
9268
9572
  this.runtimeLog(
9269
- `deduped ${dedupedMaterialized.droppedCount} duplicate dataset key(s) for ctx.dataset("${normalizedMapNamespace}"); keeping first occurrence` +
9270
- (keySample ? ` (e.g. ${keySample})` : ''),
9573
+ `[runtime.dataset_input_cardinality] ${JSON.stringify({
9574
+ dataset: normalizedMapNamespace,
9575
+ rowIdentity: rowIdentityMode,
9576
+ sourceRows: rawMaterializedEntries.length,
9577
+ uniqueInputRows: dedupedMaterialized.rows.length,
9578
+ duplicateOccurrences: dedupedMaterialized.droppedCount,
9579
+ duplicatePolicy: 'first_wins',
9580
+ })}`,
9271
9581
  );
9272
9582
  }
9273
9583
  const materializedItems = dedupedMaterialized.rows;
9584
+ // A legacy checkpoint does not contain its pre-dedupe source cardinality.
9585
+ // Leave the optional summary absent rather than report its pending subset
9586
+ // as the original source. New checkpoints retain only fixed-size counts.
9587
+ const inputCardinality = resumedPendingRowsOnly
9588
+ ? replayMapFrame?.inputCardinality
9589
+ : {
9590
+ sourceRows: rawMaterializedEntries.length,
9591
+ uniqueInputRows: materializedItems.length,
9592
+ duplicateOccurrences: dedupedMaterialized.droppedCount,
9593
+ rowIdentity: rowIdentityMode,
9594
+ };
9595
+ // Admission provenance is captured before onMapStart restores output
9596
+ // cells. The execution row stays intact; only pending inputs enter a
9597
+ // checkpoint, and their sheet outputs are restored by the existing owner
9598
+ // after the next admission. Never infer original input from enriched data.
9599
+ const retainedAuthoredInputsByIndex = new Map(
9600
+ (suspendedInputRows ?? []).flatMap((row) =>
9601
+ row.authoredInput ? [[row.inputIndex, row.authoredInput] as const] : [],
9602
+ ),
9603
+ );
9604
+ const authoredInputsByIndex = new Map(
9605
+ canReadPersistedMapResults
9606
+ ? materializedItems.flatMap((item, index) => {
9607
+ const inputIndex = originalInputIndexByItem.get(item) ?? index;
9608
+ if (resumedPendingRowsOnly) {
9609
+ const retained = retainedAuthoredInputsByIndex.get(inputIndex);
9610
+ // An older pending checkpoint has execution data only. Keep its
9611
+ // optional provenance absent rather than relabel enriched cells.
9612
+ return retained ? [[inputIndex, retained] as const] : [];
9613
+ }
9614
+ const row = this.toOutputRow(item as Record<string, unknown>);
9615
+ const source = getPlayDatasetSourceRowIdentity(row);
9616
+ const data = sanitizePostgresJsonValue(
9617
+ toSerializableCsvAliasedRow(row),
9618
+ );
9619
+ materializationMemoryTracker.track(data);
9620
+ return [
9621
+ [
9622
+ inputIndex,
9623
+ { data, ...(source ? { source: { ...source } } : {}) },
9624
+ ] as const,
9625
+ ];
9626
+ })
9627
+ : [],
9628
+ );
9274
9629
  const priorAuthoredDigestsByIndex = new Map(
9275
9630
  (suspendedInputRows ?? []).flatMap((row) =>
9276
9631
  row.authoredInputDigest
@@ -9405,6 +9760,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9405
9760
  const inputIndex = itemOriginalIndexes[index] ?? index;
9406
9761
  return {
9407
9762
  ...toSerializableCsvAliasedRow(row),
9763
+ ...(getPlayDatasetSourceRowIdentity(row)
9764
+ ? { __deeplineSourceRow: getPlayDatasetSourceRowIdentity(row) }
9765
+ : {}),
9408
9766
  ...mapRowOutcomeRuntimeFields({
9409
9767
  key: seedRowIdentity(row, inputIndex),
9410
9768
  inputIndex,
@@ -9513,6 +9871,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9513
9871
  artifactTableNamespace: mapScope.artifactTableNamespace,
9514
9872
  status: 'running',
9515
9873
  totalRows: totalInputCount,
9874
+ inputCardinality,
9516
9875
  completedRowKeys,
9517
9876
  pendingRowKeys,
9518
9877
  completedRowsCount: previouslyCompletedRowsCount,
@@ -9663,13 +10022,17 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9663
10022
  executionRowIndexes: rowsToExecuteEntries.map(
9664
10023
  (entry) => entry.originalIndex,
9665
10024
  ),
9666
- ...(!canReadPersistedMapResults
10025
+ ...(canReadPersistedMapResults
9667
10026
  ? {
10027
+ authoredInputs: rowsToExecuteEntries.map((entry) =>
10028
+ authoredInputsByIndex.get(entry.originalIndex),
10029
+ ),
10030
+ }
10031
+ : {
9668
10032
  authoredInputDigests: rowsToExecuteEntries.map((entry) =>
9669
10033
  authoredInputDigestsByIndex.get(entry.originalIndex),
9670
10034
  ),
9671
- }
9672
- : {}),
10035
+ }),
9673
10036
  retainedRowsMemoryBudgetBytes:
9674
10037
  mapMemoryLimits.materializedBudgetBytes,
9675
10038
  progressCompletedOffset:
@@ -9854,13 +10217,15 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9854
10217
  });
9855
10218
  }
9856
10219
 
9857
- this.#options.onMapRowsSettled?.({
10220
+ await this.#options.onMapRowsSettled?.({
9858
10221
  playName: this.#options.playName,
9859
10222
  runId: this.#options.runId,
9860
10223
  tableNamespace: resolvedTableNamespace,
9861
10224
  succeededRows:
9862
10225
  previouslyCompletedRowKeys.length + visibleCompletedRows.length,
9863
10226
  failedRows: durableFailedRows,
10227
+ expectedRows: totalInputCount - duplicateReuseCount,
10228
+ staticPipeline: this.currentStaticPipeline,
9864
10229
  });
9865
10230
 
9866
10231
  // This map's terminal rows have already crossed the persistence barrier
@@ -9950,8 +10315,24 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9950
10315
  skipped: reusedCount,
9951
10316
  pending: 0,
9952
10317
  failed: durableFailedRows,
9953
- ...(duplicateReuseCount > 0
9954
- ? { duplicates: { exact: duplicateReuseCount } }
10318
+ ...(inputCardinality
10319
+ ? {
10320
+ cardinality: {
10321
+ ...inputCardinality,
10322
+ uniqueInputRows:
10323
+ inputCardinality.uniqueInputRows - duplicateReuseCount,
10324
+ duplicateOccurrences:
10325
+ inputCardinality.duplicateOccurrences + duplicateReuseCount,
10326
+ outputRows: results.length,
10327
+ },
10328
+ }
10329
+ : {}),
10330
+ ...(dedupedMaterialized.droppedCount + duplicateReuseCount > 0
10331
+ ? {
10332
+ duplicates: {
10333
+ exact: dedupedMaterialized.droppedCount + duplicateReuseCount,
10334
+ },
10335
+ }
9955
10336
  : {}),
9956
10337
  },
9957
10338
  resolvers: {
@@ -9989,6 +10370,11 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9989
10370
  onRowError?: 'isolate' | 'fail';
9990
10371
  executionRowKeys?: string[];
9991
10372
  executionRowIndexes?: number[];
10373
+ authoredInputs?: Array<
10374
+ NonNullable<
10375
+ MapExecutionFrame['suspendedInputRows']
10376
+ >[number]['authoredInput']
10377
+ >;
9992
10378
  authoredInputDigests?: Array<string | undefined>;
9993
10379
  /**
9994
10380
  * Opt-in cap on how many row resolvers run their body concurrently. When
@@ -10796,6 +11182,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
10796
11182
  ),
10797
11183
  ...mapRowOutcomeRuntimeFields({ key: rowKey }),
10798
11184
  },
11185
+ ...(runtimeOptions?.authoredInputs?.[index]
11186
+ ? { authoredInput: runtimeOptions.authoredInputs[index] }
11187
+ : {}),
10799
11188
  ...(runtimeOptions?.authoredInputDigests?.[index]
10800
11189
  ? {
10801
11190
  authoredInputDigest:
@@ -11204,6 +11593,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
11204
11593
  key: keyForIndex(index),
11205
11594
  }),
11206
11595
  },
11596
+ ...(runtimeOptions?.authoredInputs?.[index]
11597
+ ? { authoredInput: runtimeOptions.authoredInputs[index] }
11598
+ : {}),
11207
11599
  ...(runtimeOptions?.authoredInputDigests?.[index]
11208
11600
  ? {
11209
11601
  authoredInputDigest:
@@ -11453,6 +11845,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
11453
11845
  stepPath,
11454
11846
  }),
11455
11847
  ),
11848
+ ...(step.staleAfterSeconds !== undefined
11849
+ ? { staleAfterSeconds: step.staleAfterSeconds }
11850
+ : {}),
11456
11851
  },
11457
11852
  )
11458
11853
  : await this.executeStepProgramStep(
@@ -12530,7 +12925,10 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
12530
12925
 
12531
12926
  private toOutputRow<T>(item: T): Record<string, unknown> {
12532
12927
  if (item != null && typeof item === 'object' && !Array.isArray(item)) {
12533
- return stripCsvProjectionMetadata(item as Record<string, unknown>);
12928
+ return copyPlayDatasetSourceRowIdentity(
12929
+ item,
12930
+ stripCsvProjectionMetadata(item as Record<string, unknown>),
12931
+ );
12534
12932
  }
12535
12933
 
12536
12934
  return { value: item };
@@ -12739,6 +13137,165 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
12739
13137
  toolId: string,
12740
13138
  input: Record<string, unknown>,
12741
13139
  options?: ToolCallOptions,
13140
+ ): Promise<unknown> {
13141
+ if (!this.usesExplicitFreshness) {
13142
+ return this.executeToolLegacyInternal(key, toolId, input, options);
13143
+ }
13144
+ const policy = resolveExplicitDurableCallPolicy(options ?? {});
13145
+ if (
13146
+ this.#options.cachePolicy?.forceToolRefresh === true ||
13147
+ toolExecutionOverrides.getStore()?.force === true
13148
+ ) {
13149
+ policy.effective = 0;
13150
+ }
13151
+ const normalizedKey = this.normalizeContextKey(key, 'tool');
13152
+ // Reserve operation identity before asynchronous metadata lookup. Two
13153
+ // concurrent calls may share an authored id; assigning their identity
13154
+ // after auth/action metadata resolves would make recovery depend on lookup
13155
+ // completion order instead of authored call order.
13156
+ const invocationId = this.freshnessInvocationId('tool', normalizedKey);
13157
+ const sqlTool = isCustomerDbDatasetTool(toolId);
13158
+ const scalarCustomerDbQuery = sqlTool && input.result_mode === 'rows';
13159
+ const liveDataset =
13160
+ isQueryResultDatasetReadRequest(toolId, input) && !scalarCustomerDbQuery;
13161
+ const sqlMutation =
13162
+ sqlTool &&
13163
+ !isQueryResultDatasetReadRequest(toolId, {
13164
+ ...input,
13165
+ result_mode: undefined,
13166
+ });
13167
+ const [eventWait, invocationScoped, retryPolicy, reusableSupport] =
13168
+ policy.effective === 0
13169
+ ? ([null, false, null, null] as const)
13170
+ : await Promise.all([
13171
+ this.#options.getIntegrationEventWaitHandler?.(toolId) ?? null,
13172
+ this.#options
13173
+ .getToolIdempotencyScope?.(toolId)
13174
+ .then((scope) => scope === 'invocation') ?? false,
13175
+ this.#options.getToolRetryPolicy?.(toolId, input) ?? null,
13176
+ this.#options.getToolReusableResultSupport?.(toolId) ?? null,
13177
+ ]);
13178
+ const requiresExecutionFence = retryPolicy?.requiresExecutionFence === true;
13179
+ if (policy.effective !== 0 && reusableSupport?.supported !== true) {
13180
+ throw new Error(
13181
+ `Tool ${toolId} does not support reusable freshness; use staleAfterSeconds: 0. ${reusableSupport?.unavailableReason ?? 'No read-result reuse capability was supplied by the action owner.'}`,
13182
+ );
13183
+ }
13184
+ if (
13185
+ policy.effective !== 0 &&
13186
+ (eventWait ||
13187
+ invocationScoped ||
13188
+ requiresExecutionFence ||
13189
+ liveDataset ||
13190
+ sqlMutation ||
13191
+ isAlwaysFreshIntegrationTool(toolId))
13192
+ ) {
13193
+ throw new Error(
13194
+ `Tool ${toolId} does not support reusable freshness; use staleAfterSeconds: 0. Live datasets, event waits, invocation-scoped actions, writes, and Workspace operations cannot reuse an older result.`,
13195
+ );
13196
+ }
13197
+
13198
+ const authScope = await this.resolveToolAuthScopeDigest(toolId);
13199
+ const actionVersion =
13200
+ (await this.#options.getToolActionCacheVersion?.(toolId))?.trim() ?? '';
13201
+ if (!actionVersion) {
13202
+ throw new Error(`Tool ${toolId} has no action identity for freshness.`);
13203
+ }
13204
+ const semanticKey = stableStringify({
13205
+ kind: 'tool',
13206
+ orgId: this.#options.orgId,
13207
+ toolId,
13208
+ input,
13209
+ authScope,
13210
+ actionVersion,
13211
+ responseContract: this.currentToolResponseContract,
13212
+ responseRevision: this.currentToolResponseReceiptRevision,
13213
+ });
13214
+ if (sqlTool && input.operation_key !== undefined) {
13215
+ throw new Error(
13216
+ 'Play SQL operation identity is assigned by the runtime. Use the authored call id to identify the operation.',
13217
+ );
13218
+ }
13219
+ const requestInput =
13220
+ sqlTool && (sqlMutation || scalarCustomerDbQuery)
13221
+ ? { ...input, operation_key: `${this.currentRunId}:${invocationId}` }
13222
+ : input;
13223
+
13224
+ if (liveDataset || isAlwaysFreshIntegrationTool(toolId)) {
13225
+ const value = await this.executeToolLegacyInternal(
13226
+ key,
13227
+ toolId,
13228
+ requestInput,
13229
+ { ...options, force: true, staleAfterSeconds: 0 },
13230
+ );
13231
+ const freshness: CallFreshnessMetadata = {
13232
+ requestedStaleAfterSeconds: policy.requested,
13233
+ effectiveStaleAfterSeconds: 0,
13234
+ source: 'live_observation',
13235
+ resultAgeMs: 0,
13236
+ completedAt: Date.now(),
13237
+ unavailableReason: liveDataset
13238
+ ? 'Dataset rows are live observations; the dataset owner controls pagination and recovery.'
13239
+ : 'Workspace reviewer edits and conflict checks must be observed on every invocation.',
13240
+ };
13241
+ return isToolExecuteResult(value)
13242
+ ? cloneToolExecuteResultWithExecution(value, {
13243
+ ...value._metadata.execution,
13244
+ freshness,
13245
+ })
13246
+ : value;
13247
+ }
13248
+
13249
+ return this.executeFreshnessControlledCall({
13250
+ operation: 'tool',
13251
+ id: normalizedKey,
13252
+ semanticKey,
13253
+ invocationId,
13254
+ policy,
13255
+ completedAt: (value) => {
13256
+ if (!isToolExecuteResult(value)) return undefined;
13257
+ const raw = value.toolResponse.raw as {
13258
+ operation?: { completed_at_ms?: unknown };
13259
+ } | null;
13260
+ const completedAt = raw?.operation?.completed_at_ms;
13261
+ return typeof completedAt === 'number' &&
13262
+ Number.isSafeInteger(completedAt)
13263
+ ? completedAt
13264
+ : value._metadata.execution.completedAt;
13265
+ },
13266
+ execute: async () => {
13267
+ const value = await this.executeToolLegacyInternal(
13268
+ key,
13269
+ toolId,
13270
+ requestInput,
13271
+ { ...options, force: true, staleAfterSeconds: 0 },
13272
+ );
13273
+ if ((await this.resolveToolAuthScopeDigest(toolId)) !== authScope) {
13274
+ throw new Error(
13275
+ 'Tool credential identity changed during execution. Resume under the current credential identity before retaining this result.',
13276
+ );
13277
+ }
13278
+ return value;
13279
+ },
13280
+ onOutcome: (value, freshness) =>
13281
+ isToolExecuteResult(value)
13282
+ ? cloneToolExecuteResultWithExecution(value, {
13283
+ ...value._metadata.execution,
13284
+ cached: freshness.source !== 'executed',
13285
+ source: freshness.source === 'executed' ? 'live' : 'cache',
13286
+ resultReceiptKey: freshness.operationReceiptKey,
13287
+ freshness,
13288
+ completedAt: freshness.completedAt,
13289
+ })
13290
+ : value,
13291
+ });
13292
+ }
13293
+
13294
+ private async executeToolLegacyInternal(
13295
+ key: string,
13296
+ toolId: string,
13297
+ input: Record<string, unknown>,
13298
+ options?: ToolCallOptions,
12742
13299
  ): Promise<unknown> {
12743
13300
  if (!rowContext.getStore()) this.assertRowAdmissionOpen();
12744
13301
  this.assertNoPendingDurableSuspension();
@@ -14873,6 +15430,10 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
14873
15430
  if (!rowContext.getStore()) this.assertRowAdmissionOpen();
14874
15431
  validatePlayAuthoringField('ctx.fetch.key', key);
14875
15432
  const normalizedKey = this.normalizeContextKey(key, 'fetch');
15433
+ const invocationId =
15434
+ this.usesExplicitFreshness && options?.transient !== true
15435
+ ? this.freshnessInvocationId('fetch', normalizedKey)
15436
+ : undefined;
14876
15437
  const rowStore = rowContext.getStore();
14877
15438
  const rowFetchScope = rowStore
14878
15439
  ? {
@@ -14886,6 +15447,30 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
14886
15447
  const url = input.toString();
14887
15448
  const parsedUrl = new URL(url);
14888
15449
  const method = (init.method ?? 'GET').toUpperCase();
15450
+ const reusableReadMethod = ['GET', 'HEAD', 'OPTIONS'].includes(method);
15451
+ if (this.usesExplicitFreshness && options?.transient === true) {
15452
+ if (!reusableReadMethod) {
15453
+ throw new Error(
15454
+ 'Edition 11 transient fetches are limited to read-only HTTP methods.',
15455
+ );
15456
+ }
15457
+ if (
15458
+ options.staleAfterSeconds !== undefined &&
15459
+ options.staleAfterSeconds !== 0
15460
+ ) {
15461
+ throw new Error(
15462
+ 'Transient fetch does not support retained result reuse; omit freshness or choose staleAfterSeconds: 0.',
15463
+ );
15464
+ }
15465
+ } else if (
15466
+ this.usesExplicitFreshness &&
15467
+ !reusableReadMethod &&
15468
+ options?.staleAfterSeconds !== 0
15469
+ ) {
15470
+ throw new Error(
15471
+ 'Edition 11 mutating or non-read-only fetches require staleAfterSeconds: 0.',
15472
+ );
15473
+ }
14889
15474
  const urlContainsResolvedSecret =
14890
15475
  this.secretRedactor.containsRegisteredSecret(url, {
14891
15476
  includeEncoded: true,
@@ -14975,6 +15560,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
14975
15560
  : {}),
14976
15561
  }),
14977
15562
  ),
15563
+ invocationId,
14978
15564
  staleAfterSeconds: options?.staleAfterSeconds,
14979
15565
  transient: options?.transient === true,
14980
15566
  onRecovered: (output) => {
@@ -15026,6 +15612,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
15026
15612
  },
15027
15613
  body: receiptBody,
15028
15614
  row: rowFetchScope,
15615
+ ...(invocationId ? { invocationId } : {}),
15029
15616
  }),
15030
15617
  )}`,
15031
15618
  );
@@ -15174,6 +15761,24 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
15174
15761
  'ctx.step.semanticKey',
15175
15762
  options?.semanticKey,
15176
15763
  );
15764
+ const freshnessPolicy = this.usesExplicitFreshness
15765
+ ? resolveExplicitDurableCallPolicy(options ?? {})
15766
+ : null;
15767
+ if (
15768
+ freshnessPolicy &&
15769
+ this.#options.cachePolicy?.forceStepRefresh === true
15770
+ ) {
15771
+ freshnessPolicy.effective = 0;
15772
+ }
15773
+ if (
15774
+ freshnessPolicy &&
15775
+ freshnessPolicy.requested !== 0 &&
15776
+ !options?.semanticKey?.trim()
15777
+ ) {
15778
+ throw new Error(
15779
+ 'Reusable ctx.step results require a stable semanticKey.',
15780
+ );
15781
+ }
15177
15782
  const normalizedKey = this.normalizeContextKey(key, 'step');
15178
15783
  if (!normalizedKey.trim()) {
15179
15784
  throw new Error('ctx.step(key, fn) requires a non-empty stable step id.');
@@ -15181,7 +15786,26 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
15181
15786
 
15182
15787
  const rowStore = rowContext.getStore();
15183
15788
  const scope = rowStore ? `row-${rowStore.rowId}` : 'workflow';
15184
- const callIndexKey = `${this.currentExecutionScope.receipt.namespace}:${scope}:${normalizedKey}`;
15789
+ const parentStep = authoredStepContext.getStore();
15790
+ const explicitStepScope = freshnessPolicy
15791
+ ? stableDigest(
15792
+ stableStringify({
15793
+ semanticKey: options?.semanticKey ?? null,
15794
+ parent:
15795
+ parentStep?.context === this
15796
+ ? (parentStep.invocationId ?? parentStep.stepId)
15797
+ : null,
15798
+ row: rowStore
15799
+ ? {
15800
+ table: rowStore.tableNamespace,
15801
+ key: rowStore.rowKey ?? rowStore.rowId,
15802
+ field: rowStore.fieldName,
15803
+ }
15804
+ : null,
15805
+ }),
15806
+ )
15807
+ : null;
15808
+ const callIndexKey = `${this.currentExecutionScope.receipt.namespace}:${scope}:${normalizedKey}${explicitStepScope ? `:${explicitStepScope}` : ''}`;
15185
15809
  const freshCallIndex = this.stepCallIndexByKey.get(callIndexKey) ?? 0;
15186
15810
  const callIndex = nextReplayableCallIndex(
15187
15811
  `step:${callIndexKey}`,
@@ -15193,7 +15817,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
15193
15817
  );
15194
15818
  const boundarySuffix = callIndex === 0 ? '' : `:${callIndex}`;
15195
15819
  const boundaryId = this.durableBoundaryId(
15196
- `step-${scope}:${normalizedKey}${boundarySuffix}`,
15820
+ `step-${scope}:${normalizedKey}${explicitStepScope ? `:${explicitStepScope}` : ''}${boundarySuffix}`,
15197
15821
  );
15198
15822
  const advanceRecoveredSleepCursor = async (
15199
15823
  receipt?: RuntimeStepReceipt,
@@ -15276,9 +15900,63 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
15276
15900
  });
15277
15901
  try {
15278
15902
  const output = await authoredStepContext.run(
15279
- { context: this, stepId: normalizedKey },
15280
- () =>
15281
- this.executeWithRuntimeReceipt<T>(
15903
+ {
15904
+ context: this,
15905
+ stepId: normalizedKey,
15906
+ ...(freshnessPolicy ? { invocationId: boundaryId } : {}),
15907
+ },
15908
+ async () => {
15909
+ if (freshnessPolicy) {
15910
+ const semanticKey = stableStringify({
15911
+ operation: 'step',
15912
+ orgId: this.#options.orgId,
15913
+ namespace: this.currentExecutionScope.receipt.namespace,
15914
+ id: normalizedKey,
15915
+ semanticKey: options?.semanticKey,
15916
+ row: rowStore
15917
+ ? {
15918
+ table: rowStore.tableNamespace,
15919
+ key: rowStore.rowKey ?? rowStore.rowId,
15920
+ field: rowStore.fieldName,
15921
+ }
15922
+ : null,
15923
+ });
15924
+ const completed = await this.executeFreshnessControlledCall({
15925
+ operation: 'step',
15926
+ id: normalizedKey,
15927
+ semanticKey,
15928
+ invocationId: boundaryId,
15929
+ policy: freshnessPolicy,
15930
+ completedAt: (value) => value.completedAt,
15931
+ execute: async () => {
15932
+ const before = this.sleepBoundaryIndex;
15933
+ const value = await executeStep();
15934
+ const boundary =
15935
+ this.checkpoint.resolvedBoundaries?.[boundaryId];
15936
+ return {
15937
+ output: value === undefined ? null : value,
15938
+ undefinedOutput: value === undefined,
15939
+ sleepCount: this.sleepBoundaryIndex - before,
15940
+ completedAt:
15941
+ (boundary?.kind === 'step'
15942
+ ? boundary.completedAt
15943
+ : undefined) ?? Date.now(),
15944
+ };
15945
+ },
15946
+ onOutcome: (value, freshness) => {
15947
+ if (freshness.source !== 'executed') {
15948
+ this.sleepBoundaryIndex += value.sleepCount;
15949
+ }
15950
+ return value;
15951
+ },
15952
+ });
15953
+ const result = completed.undefinedOutput
15954
+ ? undefined
15955
+ : completed.output;
15956
+ assertJsonSerializableStepOutput(normalizedKey, result);
15957
+ return result as T;
15958
+ }
15959
+ return await this.executeWithRuntimeReceipt<T>(
15282
15960
  'step',
15283
15961
  normalizedKey,
15284
15962
  this.currentRunId,
@@ -15308,7 +15986,8 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
15308
15986
  },
15309
15987
  execute: executeStep,
15310
15988
  },
15311
- ),
15989
+ );
15990
+ },
15312
15991
  );
15313
15992
  if (observe)
15314
15993
  this.emitExecutionEvent({
@@ -17746,6 +18425,17 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
17746
18425
  );
17747
18426
  const shouldWaitAtExecutionBoundary =
17748
18427
  shouldWaitForAsyncOperation && !shouldResolveAsyncOperationInRuntime;
18428
+ const asyncExecutionRecovery = shouldWaitAtExecutionBoundary
18429
+ ? await this.#options.getToolAsyncExecutionRecovery?.(toolId)
18430
+ : null;
18431
+ if (
18432
+ asyncExecutionRecovery &&
18433
+ !isImmutableRuntimeExecutionRecoveryOrigin(asyncExecutionRecovery.origin)
18434
+ ) {
18435
+ throw new Error(
18436
+ 'Async execution recovery requires the serving app immutable origin. No provider request was sent.',
18437
+ );
18438
+ }
17749
18439
  // An explicitly opted-in caller-managed launch gives the Play author the
17750
18440
  // provider handle and leaves polling to the authored graph. Do not let a
17751
18441
  // provider's legacy synchronous helper turn a wide start wave into
@@ -18106,6 +18796,70 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
18106
18796
  // map cells, batched owners) supply the fuller scope.
18107
18797
  const playNodeScope =
18108
18798
  options?.playNodeScope ?? buildPlayNodeScope({ toolId });
18799
+ const asyncRecoveryInvocationKey = asyncExecutionRecovery
18800
+ ? stableDigest(
18801
+ stableStringify({
18802
+ toolId,
18803
+ // A queued call/chunk supplies its exact durable transport
18804
+ // identity. A direct invocation owns a fresh call identity.
18805
+ callIdentity:
18806
+ options?.durableCallReceiptKey ??
18807
+ (options?.providerOperationForceRefreshIdentities?.length
18808
+ ? options.providerOperationForceRefreshIdentities
18809
+ : (options?.providerIdempotencyKey ??
18810
+ executionRequestNonce)),
18811
+ payload: toolInputSnapshot,
18812
+ authScope: executionAuthScopeDigest,
18813
+ playNodeScope,
18814
+ forceRefreshIdentities:
18815
+ options?.providerOperationForceRefreshIdentities ?? [],
18816
+ }),
18817
+ )
18818
+ : null;
18819
+ const asyncRecoveryPolling = asyncExecutionRecovery
18820
+ ? resolveAsyncOperationPolling({
18821
+ contract: asyncContract!,
18822
+ startInput: toolInputSnapshot,
18823
+ playRuntimeWait: true,
18824
+ })
18825
+ : null;
18826
+ const asyncRecoveryDeadlineAtMs = asyncRecoveryPolling
18827
+ ? Date.now() + asyncRecoveryPolling.timeoutMs
18828
+ : null;
18829
+ let confirmedAsyncRecoveryPending = false;
18830
+ let asyncRecoveryWaitTimeout: ReturnType<
18831
+ typeof createToolHttpError
18832
+ > | null = null;
18833
+ const asyncRecoveryWaitTimeoutError = () =>
18834
+ (asyncRecoveryWaitTimeout ??= createToolHttpError(
18835
+ toolErrorSchemaVersion,
18836
+ formatAsyncOperationTimeout({
18837
+ startAction: asyncContract!.lifecycle.startAction,
18838
+ jobId: null,
18839
+ timeoutDescription: `${asyncRecoveryPolling!.timeoutMs}ms`,
18840
+ }) +
18841
+ ' The execution remains recoverable under its original idempotency key; it will not be relaunched.',
18842
+ null,
18843
+ 504,
18844
+ 'repairable',
18845
+ {
18846
+ toolId,
18847
+ provider,
18848
+ operation: toolId,
18849
+ code: 'ASYNC_OPERATION_TIMEOUT',
18850
+ origin: 'provider',
18851
+ category: 'upstream',
18852
+ retryable: false,
18853
+ statusCode: 504,
18854
+ },
18855
+ ));
18856
+ const asyncRecoveryKey = (attempt: number) =>
18857
+ asyncRecoveryInvocationKey
18858
+ ? buildRuntimeExecutionRecoveryKey({
18859
+ parentRunId: this.currentRunId,
18860
+ invocationKey: `${asyncRecoveryInvocationKey}:attempt:${attempt}`,
18861
+ })
18862
+ : null;
18109
18863
  // This is deliberately a claim about only the immediately preceding
18110
18864
  // *durable* provider attempt. The execution gateway validates and
18111
18865
  // attests it before a retried call can use its fresh charge
@@ -18361,6 +19115,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
18361
19115
  transportAttempt < TOOL_EXECUTE_TRANSPORT_MAX_ATTEMPTS &&
18362
19116
  (input.knownNotDispatched === true ||
18363
19117
  usesDurableInvocationFence ||
19118
+ asyncExecutionRecovery ||
18364
19119
  ambiguousTransportReplayAllowed ||
18365
19120
  fixtureReplaySafe)
18366
19121
  ) {
@@ -18368,7 +19123,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
18368
19123
  // attempt and any prior provider-429 proof belong to the logical
18369
19124
  // invocation; only a definite retryable HTTP outcome advances it.
18370
19125
  // Ordinary transport retries retain their physical attempt count.
18371
- if (!usesDurableInvocationFence) {
19126
+ if (!usesDurableInvocationFence && !asyncExecutionRecovery) {
18372
19127
  providerRateLimitRetryFromInvocationAttempt = null;
18373
19128
  invocationAttempt += 1;
18374
19129
  }
@@ -18454,6 +19209,12 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
18454
19209
 
18455
19210
  while (true) {
18456
19211
  toolAbortSignal?.throwIfAborted();
19212
+ if (
19213
+ confirmedAsyncRecoveryPending &&
19214
+ Date.now() >= asyncRecoveryDeadlineAtMs!
19215
+ ) {
19216
+ throw asyncRecoveryWaitTimeoutError();
19217
+ }
18457
19218
  if (
18458
19219
  providerNoProgressExpired({
18459
19220
  startedAtMsByScope:
@@ -18806,6 +19567,12 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
18806
19567
  // A stale permit can take arbitrarily long to
18807
19568
  // replace. Only fixture residence and the physical
18808
19569
  // HTTP exchange spend the tool execution deadline.
19570
+ if (
19571
+ confirmedAsyncRecoveryPending &&
19572
+ Date.now() >= asyncRecoveryDeadlineAtMs!
19573
+ ) {
19574
+ throw asyncRecoveryWaitTimeoutError();
19575
+ }
18809
19576
  armProviderTimeout();
18810
19577
  beginProviderExecution();
18811
19578
  const fetchedAt = Date.now();
@@ -18865,6 +19632,12 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
18865
19632
  }
18866
19633
  : {}),
18867
19634
  'x-deepline-request-id': deeplineRequestId,
19635
+ ...(asyncRecoveryKey(invocationAttempt)
19636
+ ? {
19637
+ 'idempotency-key':
19638
+ asyncRecoveryKey(invocationAttempt)!,
19639
+ }
19640
+ : {}),
18868
19641
  ...(providerExecutionFenceKey
18869
19642
  ? {
18870
19643
  'x-deepline-idempotency-key':
@@ -19247,6 +20020,12 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
19247
20020
  receiptHeartbeatSupervisor?.stop();
19248
20021
  }
19249
20022
  } catch (error) {
20023
+ if (
20024
+ asyncRecoveryWaitTimeout !== null &&
20025
+ error === asyncRecoveryWaitTimeout
20026
+ ) {
20027
+ throw error;
20028
+ }
19250
20029
  if (heartbeatFailure) {
19251
20030
  throw heartbeatFailure;
19252
20031
  }
@@ -19366,6 +20145,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
19366
20145
  const transportReplaySafe =
19367
20146
  !ambiguousDispatchedFailure ||
19368
20147
  usesDurableInvocationFence ||
20148
+ Boolean(asyncExecutionRecovery) ||
19369
20149
  ambiguousTransportReplayAllowed ||
19370
20150
  fixtureReplaySafe;
19371
20151
  if (!transportReplaySafe) {
@@ -19444,8 +20224,62 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
19444
20224
  continue;
19445
20225
  }
19446
20226
 
20227
+ if (
20228
+ asyncExecutionRecovery &&
20229
+ response.ok &&
20230
+ response.headers.get(
20231
+ RUNTIME_EXECUTION_RECOVERY_SUPPORTED_HEADER,
20232
+ ) !== 'true'
20233
+ ) {
20234
+ throw createToolHttpError(
20235
+ toolErrorSchemaVersion,
20236
+ `Tool ${toolId} did not confirm the declared async recovery reader. The execution will not be relaunched.`,
20237
+ null,
20238
+ response.status,
20239
+ 'repairable',
20240
+ );
20241
+ }
20242
+
19447
20243
  if (!response.ok) {
19448
20244
  const text = responseErrorText ?? '';
20245
+ const recoveryKey = asyncRecoveryKey(invocationAttempt);
20246
+ if (
20247
+ recoveryKey &&
20248
+ response.status === 409 &&
20249
+ response.headers.get(
20250
+ RUNTIME_EXECUTION_RECOVERY_SUPPORTED_HEADER,
20251
+ ) === 'true'
20252
+ ) {
20253
+ const pending = (() => {
20254
+ try {
20255
+ return JSON.parse(text) as Record<string, unknown>;
20256
+ } catch {
20257
+ return null;
20258
+ }
20259
+ })();
20260
+ const recovery = pending?.executionRecovery as
20261
+ | Record<string, unknown>
20262
+ | undefined;
20263
+ if (
20264
+ pending?.code === 'EXECUTION_IN_PROGRESS' &&
20265
+ pending.retryable === true &&
20266
+ recovery?.idempotencyKey === recoveryKey &&
20267
+ (recovery.state === 'running' ||
20268
+ recovery.state === 'completed')
20269
+ ) {
20270
+ confirmedAsyncRecoveryPending = true;
20271
+ if (Date.now() >= asyncRecoveryDeadlineAtMs!) {
20272
+ throw asyncRecoveryWaitTimeoutError();
20273
+ }
20274
+ // This is continuation of already accepted work. Keep the
20275
+ // original body/key and let the route attach its saved job.
20276
+ await waitForToolRetry(
20277
+ Math.min(1_000, asyncRecoveryDeadlineAtMs! - Date.now()),
20278
+ false,
20279
+ );
20280
+ continue;
20281
+ }
20282
+ }
19449
20283
  const developerTunnelOrigin502 =
19450
20284
  isDeeplineDeveloperTunnelOrigin502({
19451
20285
  url,
@@ -19487,7 +20321,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
19487
20321
  status: response.status,
19488
20322
  bodyText: text,
19489
20323
  transientHttpRetrySafe:
19490
- retrySafeTransientHttp || fixtureReplaySafe,
20324
+ retrySafeTransientHttp ||
20325
+ fixtureReplaySafe ||
20326
+ Boolean(asyncExecutionRecovery),
19491
20327
  executionFenced: usesDurableInvocationFence,
19492
20328
  });
19493
20329
  const failure =
@@ -19501,7 +20337,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
19501
20337
  retryAfterHeader: response.headers.get('retry-after'),
19502
20338
  retryAtMs: parseToolExecuteExactProviderRetryAtMs(response),
19503
20339
  transientHttpRetrySafe:
19504
- retrySafeTransientHttp || fixtureReplaySafe,
20340
+ retrySafeTransientHttp ||
20341
+ fixtureReplaySafe ||
20342
+ Boolean(asyncExecutionRecovery),
19505
20343
  executionFenced: usesDurableInvocationFence,
19506
20344
  ...(providerCallElapsedMs !== null
19507
20345
  ? { providerLatencyMs: providerCallElapsedMs }
@@ -19643,6 +20481,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
19643
20481
  (failure.knownNotDispatched ||
19644
20482
  failure.reason === 'gateway_invocation_in_progress' ||
19645
20483
  ambiguousTransportReplayAllowed ||
20484
+ Boolean(asyncExecutionRecovery) ||
19646
20485
  retrySafeTransientHttp ||
19647
20486
  fixtureReplaySafe ||
19648
20487
  // An explicit provider-owned 429 is a rejected operation,
@@ -19659,7 +20498,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
19659
20498
  // prior-attempt sweep see an intentionally absent receipt.
19660
20499
  if (
19661
20500
  failure.reason !== 'gateway_invocation_in_progress' &&
19662
- !shedKnownNotDispatched
20501
+ !shedKnownNotDispatched &&
20502
+ (!asyncExecutionRecovery ||
20503
+ failure.backpressureDelayMs !== null)
19663
20504
  ) {
19664
20505
  const previousInvocationAttempt = invocationAttempt;
19665
20506
  if (failure.backpressureDelayMs !== null) {