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
@@ -338,17 +338,19 @@ export interface RuntimeDatasetOptions<TItem = Record<string, unknown>> {
338
338
  * Set staleAfterSeconds to rerun after that many seconds while retries/replays
339
339
  * inside the same window still hit the cache. Daily refreshes use 86400.
340
340
  */
341
- staleAfterSeconds?: number;
341
+ staleAfterSeconds?: number | null;
342
342
  /**
343
343
  * Optional stable key per row. When provided, row identity is derived from
344
344
  * the selected input field(s) or returned value instead of hashing the full
345
345
  * row. Use this to pin identity to primary columns (e.g. `{ key: "email" }`
346
346
  * or `{ key: ["first_name", "last_name", "domain"] }`) so harmless
347
347
  * mutations to other columns don't invalidate the cache.
348
+ * Providing a key selects key identity for non-CSV sources and retained
349
+ * authoring editions.
348
350
  *
349
351
  * Must return a non-empty string (numbers are coerced). Empty keys throw
350
352
  * loudly before any provider work runs. Rows whose keys duplicate an earlier
351
- * row are silently deduped (first occurrence wins) and logged, never failing
353
+ * row are deduped (first occurrence wins) and counted, never failing
352
354
  * the run. The key function should reference *input*
353
355
  * columns only — values produced by this dataset's field resolvers are stripped
354
356
  * before identity is computed.
@@ -357,6 +359,14 @@ export interface RuntimeDatasetOptions<TItem = Record<string, unknown>> {
357
359
  | (keyof TItem & string)
358
360
  | readonly (keyof TItem & string)[]
359
361
  | ((row: TItem, index: number) => string | number | readonly unknown[]);
362
+ /**
363
+ * Edition 11 CSV inputs always use source_row: the original CSV record
364
+ * ordinal distinguishes equal records and every occurrence is processed,
365
+ * even when key or rowIdentity: 'key' is supplied. For non-CSV sources and
366
+ * retained editions, rowIdentity: 'key' keeps the existing first-wins
367
+ * dedupe behavior.
368
+ */
369
+ rowIdentity?: 'source_row' | 'key';
360
370
  /**
361
371
  * Row failure policy for this dataset run.
362
372
  *
@@ -400,7 +410,12 @@ export type RuntimeDatasetDefinitionOptions<TItem = Record<string, unknown>> =
400
410
 
401
411
  export type RuntimeDatasetRunOptions<TItem = Record<string, unknown>> = Pick<
402
412
  RuntimeDatasetOptions<TItem>,
403
- 'description' | 'key' | 'onRowError' | 'mode' | 'undrawnColumns'
413
+ | 'description'
414
+ | 'key'
415
+ | 'rowIdentity'
416
+ | 'onRowError'
417
+ | 'mode'
418
+ | 'undrawnColumns'
404
419
  >;
405
420
 
406
421
  /** Runtime-only receipt recovery bit; never part of the customer authoring API. */
@@ -500,6 +515,11 @@ export interface MapExecutionFrame {
500
515
  suspendedInputRows?: Array<{
501
516
  inputIndex: number;
502
517
  data: Record<string, unknown>;
518
+ /** Original admission input, separate from restored execution/output cells. */
519
+ authoredInput?: {
520
+ data: Record<string, unknown>;
521
+ source?: Readonly<{ datasetId: string; rowIndex: number }>;
522
+ };
503
523
  /** Digest of the authored input before Runtime Sheet outputs were merged. */
504
524
  authoredInputDigest?: string;
505
525
  }>;
@@ -507,6 +527,13 @@ export interface MapExecutionFrame {
507
527
  completedRowsCount?: number;
508
528
  pendingRowsCount?: number;
509
529
  failedRowsCount?: number;
530
+ /** Original source cardinality retained across pending-only checkpoint replay. */
531
+ inputCardinality?: {
532
+ sourceRows: number;
533
+ uniqueInputRows: number;
534
+ duplicateOccurrences: number;
535
+ rowIdentity: 'source_row' | 'key';
536
+ };
510
537
  /** Bounded final-row causes, additive for checkpoint compatibility. */
511
538
  failureCounts?: import('./row-failure-summary').RowFailureCounts;
512
539
  /** Terminal rows omitted from this run's latest view because a newer write won. */
@@ -988,7 +1015,9 @@ export interface ContextOptions {
988
1015
  tableNamespace: string;
989
1016
  succeededRows: number;
990
1017
  failedRows: number;
991
- }) => void;
1018
+ expectedRows?: number;
1019
+ staticPipeline?: PlayStaticPipeline | null;
1020
+ }) => void | Promise<void>;
992
1021
  /**
993
1022
  * Persists coalesced, non-terminal row patches before a map suspends.
994
1023
  * Terminal rows use onMapRowsCompleted; this narrow seam only preserves
@@ -1054,6 +1083,9 @@ export interface ContextOptions {
1054
1083
  /** Identity policy is required before reading receipts; lookup errors are
1055
1084
  * fatal, unlike the legacy best-effort retry advice below. */
1056
1085
  getToolIdempotencyScope?: (toolId: string) => Promise<'invocation' | null>;
1086
+ getToolReusableResultSupport?: (
1087
+ toolId: string,
1088
+ ) => Promise<{ supported: boolean; unavailableReason: string | null }>;
1057
1089
  getToolRetryPolicy?: (
1058
1090
  toolId: string,
1059
1091
  input: Record<string, unknown>,
@@ -1071,6 +1103,10 @@ export interface ContextOptions {
1071
1103
  toolId: string,
1072
1104
  ) => Promise<string[]> | string[];
1073
1105
  getToolAuthScopeDigest?: (toolId: string) => Promise<string> | string;
1106
+ /** Absent on older app readers; only the declared async boundary may opt in. */
1107
+ getToolAsyncExecutionRecovery?: (
1108
+ toolId: string,
1109
+ ) => Promise<{ origin: string } | null>;
1074
1110
  /** Original run for an explicit customer rerun, never an automatic retry. */
1075
1111
  replayedFromRunId?: string | null;
1076
1112
  getToolRateScope?: (
@@ -1332,6 +1368,7 @@ export type PlayFetchResponse = {
1332
1368
  headers: Record<string, string>;
1333
1369
  bodyText: string;
1334
1370
  json: unknown | null;
1371
+ freshness?: import('../plays/tool-result-types').CallFreshnessMetadata;
1335
1372
  };
1336
1373
 
1337
1374
  export interface BatchResult {
@@ -13,6 +13,34 @@ export type DurableCallCachePolicy = {
13
13
  staleAfterSeconds: number | null;
14
14
  };
15
15
 
16
+ /** Future artifact semantics; legacy optional callers retain the resolver below. */
17
+ export function resolveExplicitDurableCallPolicy(input: {
18
+ staleAfterSeconds?: number | null;
19
+ force?: boolean;
20
+ }): { requested: number | null; effective: number | null } {
21
+ if (input.staleAfterSeconds === undefined) {
22
+ throw new Error(
23
+ 'Choose staleAfterSeconds explicitly: 0, a positive whole number of seconds, or null.',
24
+ );
25
+ }
26
+ resolveDurableCallCachePolicy(input.staleAfterSeconds);
27
+ if (
28
+ input.staleAfterSeconds !== null &&
29
+ !Number.isSafeInteger(input.staleAfterSeconds)
30
+ ) {
31
+ throw new Error(DURABLE_CALL_STALE_AFTER_SECONDS_ERROR);
32
+ }
33
+ if (input.force === true && input.staleAfterSeconds !== 0) {
34
+ throw new Error(
35
+ 'force: true contradicts reusable freshness. Use staleAfterSeconds: 0.',
36
+ );
37
+ }
38
+ return {
39
+ requested: input.staleAfterSeconds,
40
+ effective: input.staleAfterSeconds,
41
+ };
42
+ }
43
+
16
44
  /** Normalize the public freshness contract shared by durable calls. */
17
45
  export function resolveDurableCallCachePolicy(
18
46
  staleAfterSeconds?: number | null,
@@ -1,3 +1,39 @@
1
+ import { sha256Hex } from '../plays/row-identity';
2
+
3
+ export const RUNTIME_EXECUTION_RECOVERY_KEY_PREFIX = 'runtime-gateway:';
4
+ export const RUNTIME_EXECUTION_RECOVERY_SUPPORTED_HEADER =
5
+ 'x-deepline-idempotency-supported';
6
+ export const RUNTIME_EXECUTION_RECOVERY_ONLY_HEADER =
7
+ 'x-deepline-execution-recovery-only';
8
+ /** Gateway-owned support for forwarding a keyed async call and its app ACK. */
9
+ export const RUNTIME_ASYNC_EXECUTION_RECOVERY_TRANSPORT_HEADER =
10
+ 'x-deepline-async-execution-recovery-transport';
11
+
12
+ /** Same-run delivery recovery must not coalesce separate Play runs. */
13
+ export function buildRuntimeExecutionRecoveryKey(input: {
14
+ parentRunId: string;
15
+ invocationKey: string;
16
+ }): string {
17
+ if (!input.parentRunId.trim() || !input.invocationKey.trim()) {
18
+ throw new Error(
19
+ 'Runtime execution recovery requires run and invocation identities.',
20
+ );
21
+ }
22
+ return `${RUNTIME_EXECUTION_RECOVERY_KEY_PREFIX}${sha256Hex(input.parentRunId)}:${sha256Hex(input.invocationKey)}`;
23
+ }
24
+
25
+ /** A known key is not authority to read another executor run's response. */
26
+ export function isRuntimeExecutionRecoveryKeyForRun(
27
+ key: string,
28
+ runId: string,
29
+ ): boolean {
30
+ if (!runId.trim()) return false;
31
+ const prefix = `${RUNTIME_EXECUTION_RECOVERY_KEY_PREFIX}${sha256Hex(runId)}:`;
32
+ return (
33
+ key.startsWith(prefix) && /^[a-f0-9]{64}$/.test(key.slice(prefix.length))
34
+ );
35
+ }
36
+
1
37
  /**
2
38
  * `execute-fenced-v2` is the first operation that requires semantic provider
3
39
  * identity. It is intentionally a named operation rather than a runtime-wide
@@ -0,0 +1,251 @@
1
+ import { sha256Hex, stableStringify } from '../plays/row-identity';
2
+ import type { CallFreshnessMetadata } from '../plays/tool-result-types';
3
+ import type { RuntimeStepReceipt } from './ctx-types';
4
+ import {
5
+ COMPLETED_RECEIPT_CACHE_INSERT_LEASE_ID,
6
+ type DurableReceiptExecutionStore,
7
+ } from './durable-receipt-execution';
8
+ import { assertNoSecretTaint } from './secret-capability';
9
+ import {
10
+ deserializeToolExecuteResult,
11
+ isSerializedToolExecuteResult,
12
+ isToolExecuteResult,
13
+ relayPublishedToolResultReference,
14
+ serializeToolExecuteResult,
15
+ } from './tool-result';
16
+
17
+ type CompletedCall = {
18
+ completedAt: number;
19
+ value?: unknown;
20
+ /**
21
+ * Large tool results already live in their canonical result receipt. Keep a
22
+ * receipt-key indirection here instead of copying the full result into each
23
+ * operation and reusable freshness receipt.
24
+ */
25
+ valueReceiptKey?: string;
26
+ undefinedValue?: true;
27
+ };
28
+
29
+ type FreshnessStore = Pick<
30
+ DurableReceiptExecutionStore,
31
+ 'enabled' | 'get' | 'complete'
32
+ >;
33
+
34
+ function completedCall(
35
+ receipt: RuntimeStepReceipt | null,
36
+ ): CompletedCall | null {
37
+ if (!receipt || !['completed', 'skipped'].includes(receipt.status)) {
38
+ return null;
39
+ }
40
+ const output = receipt.output;
41
+ if (!output || typeof output !== 'object') {
42
+ throw new Error('Invalid completed freshness receipt.');
43
+ }
44
+ const candidate = output as Partial<CompletedCall>;
45
+ const hasInlineValue = 'value' in candidate;
46
+ const hasReceiptReference =
47
+ typeof candidate.valueReceiptKey === 'string' &&
48
+ candidate.valueReceiptKey.trim().length > 0;
49
+ if (
50
+ !Number.isSafeInteger(candidate.completedAt) ||
51
+ candidate.completedAt! < 0 ||
52
+ hasInlineValue === hasReceiptReference ||
53
+ (candidate.valueReceiptKey !== undefined && !hasReceiptReference) ||
54
+ (candidate.undefinedValue !== undefined &&
55
+ candidate.undefinedValue !== true) ||
56
+ (candidate.undefinedValue === true &&
57
+ (hasReceiptReference || candidate.value !== null))
58
+ ) {
59
+ throw new Error('Invalid completed freshness receipt.');
60
+ }
61
+ return candidate as CompletedCall;
62
+ }
63
+
64
+ /**
65
+ * Operation recovery is run-local. Reusable results are a separate completed
66
+ * cache. Adjacent partitions bound lookup, while admission checks actual age.
67
+ * A concurrent miss executes independently and publishes its own operation.
68
+ */
69
+ export async function executeWithFreshnessReceipt<T>(input: {
70
+ semanticKey: string;
71
+ orgId: string;
72
+ invocationId: string;
73
+ runId: string;
74
+ requestedStaleAfterSeconds: number | null;
75
+ effectiveStaleAfterSeconds: number | null;
76
+ store: FreshnessStore;
77
+ execute: () => Promise<T>;
78
+ completedAt?: (value: T) => number | undefined;
79
+ onOutcome: (value: T, metadata: CallFreshnessMetadata) => T;
80
+ withHydration: <Result>(
81
+ read: (
82
+ measure?: (receipt: RuntimeStepReceipt | null) => void,
83
+ ) => Promise<Result>,
84
+ ) => Promise<Result>;
85
+ now?: () => number;
86
+ }): Promise<T> {
87
+ if (!input.store.enabled) {
88
+ throw new Error(
89
+ 'Durable operation recovery is unavailable in this runtime.',
90
+ );
91
+ }
92
+ const now = input.now ?? Date.now;
93
+ const cachePrefix = `ctx:${input.orgId}:freshness:${sha256Hex(input.semanticKey)}`;
94
+ const operationKey = `ctx:${input.orgId}:freshness:operation:${sha256Hex(
95
+ stableStringify({ runId: input.runId, invocationId: input.invocationId }),
96
+ )}`;
97
+ const read = (key: string) =>
98
+ input.withHydration(async (measure) => {
99
+ const receipt = await input.store.get(key);
100
+ measure?.(receipt);
101
+ return completedCall(receipt);
102
+ });
103
+ const readReferencedResult = (key: string) =>
104
+ input.withHydration(async (measure) => {
105
+ const receipt = await input.store.get(key);
106
+ measure?.(receipt);
107
+ if (
108
+ !receipt ||
109
+ !['completed', 'skipped'].includes(receipt.status) ||
110
+ receipt.output === undefined
111
+ ) {
112
+ throw new Error(
113
+ 'Completed freshness result receipt could not be recovered.',
114
+ );
115
+ }
116
+ const value = receipt.output;
117
+ if (
118
+ !isSerializedToolExecuteResult(value) &&
119
+ !isToolExecuteResult(value)
120
+ ) {
121
+ throw new Error(
122
+ 'Completed freshness result receipt has an invalid tool result.',
123
+ );
124
+ }
125
+ return value;
126
+ });
127
+ const outcome = async (
128
+ call: CompletedCall,
129
+ source: CallFreshnessMetadata['source'],
130
+ liveResult?: { value: T },
131
+ ): Promise<T> => {
132
+ let value: unknown;
133
+ if (liveResult !== undefined) {
134
+ value = liveResult.value;
135
+ } else if (call.undefinedValue) {
136
+ value = undefined;
137
+ } else if (call.valueReceiptKey) {
138
+ value = await readReferencedResult(call.valueReceiptKey);
139
+ } else {
140
+ value = call.value;
141
+ }
142
+ value = isSerializedToolExecuteResult(value)
143
+ ? deserializeToolExecuteResult(value)
144
+ : value;
145
+ return input.onOutcome(value as T, {
146
+ requestedStaleAfterSeconds: input.requestedStaleAfterSeconds,
147
+ effectiveStaleAfterSeconds: input.effectiveStaleAfterSeconds,
148
+ source,
149
+ resultAgeMs: Math.max(0, now() - call.completedAt),
150
+ completedAt: call.completedAt,
151
+ operationReceiptKey: operationKey,
152
+ });
153
+ };
154
+ const publish = async (
155
+ key: string,
156
+ call: CompletedCall,
157
+ ): Promise<CompletedCall> => {
158
+ const receipt = await input.store.complete(
159
+ key,
160
+ input.runId,
161
+ call,
162
+ COMPLETED_RECEIPT_CACHE_INSERT_LEASE_ID,
163
+ );
164
+ const stored =
165
+ receipt?.output != null ? completedCall(receipt) : await read(key);
166
+ if (!stored) {
167
+ throw new Error('Completed freshness receipt publication failed.');
168
+ }
169
+ return stored;
170
+ };
171
+
172
+ const recovered = await read(operationKey);
173
+ if (recovered) return outcome(recovered, 'operation_recovery');
174
+
175
+ const ttl = input.effectiveStaleAfterSeconds;
176
+ const admissionTime = now();
177
+ const currentPartition =
178
+ ttl === null ? null : Math.floor(admissionTime / (ttl * 1000));
179
+ const reusableKey =
180
+ ttl === null
181
+ ? `${cachePrefix}:reusable:indefinite`
182
+ : `${cachePrefix}:reusable:${ttl}:${currentPartition}`;
183
+
184
+ if (ttl !== 0) {
185
+ const candidates =
186
+ ttl === null
187
+ ? [reusableKey]
188
+ : [
189
+ reusableKey,
190
+ `${cachePrefix}:reusable:${ttl}:${currentPartition! - 1}`,
191
+ ];
192
+ for (const key of candidates) {
193
+ const cached = await read(key);
194
+ if (!cached) continue;
195
+ const age = now() - cached.completedAt;
196
+ if (age < 0 || (ttl !== null && age >= ttl * 1000)) continue;
197
+ const own = await publish(operationKey, cached);
198
+ return outcome(own, 'reusable_result');
199
+ }
200
+ }
201
+
202
+ const value = await input.execute();
203
+ assertNoSecretTaint(value, 'Freshness call result');
204
+ const completedAt = input.completedAt?.(value) ?? now();
205
+ if (!Number.isSafeInteger(completedAt) || completedAt < 0) {
206
+ throw new Error('Freshness call returned an invalid completion time.');
207
+ }
208
+ const relayReference = isToolExecuteResult(value)
209
+ ? relayPublishedToolResultReference(value)
210
+ : null;
211
+ const resultReceiptKey =
212
+ isToolExecuteResult(value) && relayReference
213
+ ? value._metadata.execution.resultReceiptKey?.trim() ||
214
+ value._metadata.execution.receiptKey?.trim() ||
215
+ ''
216
+ : '';
217
+ if (relayReference && !resultReceiptKey) {
218
+ throw new Error(
219
+ 'Referenced freshness result has no canonical result receipt identity.',
220
+ );
221
+ }
222
+ const completed: CompletedCall = {
223
+ completedAt,
224
+ ...(value === undefined
225
+ ? { value: null, undefinedValue: true as const }
226
+ : resultReceiptKey
227
+ ? { valueReceiptKey: resultReceiptKey }
228
+ : {
229
+ value: isToolExecuteResult(value)
230
+ ? serializeToolExecuteResult(value)
231
+ : value,
232
+ }),
233
+ };
234
+
235
+ // Recover this exact operation even if reusable publication fails afterwards.
236
+ await publish(operationKey, completed);
237
+ if (ttl !== 0) {
238
+ const completionPartition =
239
+ ttl === null
240
+ ? 'indefinite'
241
+ : Math.floor(completed.completedAt / (ttl * 1000));
242
+ const resultKey =
243
+ ttl === null
244
+ ? `${cachePrefix}:reusable:indefinite`
245
+ : `${cachePrefix}:reusable:${ttl}:${completionPartition}`;
246
+ await publish(resultKey, completed);
247
+ }
248
+ // Each overlapping miss owns its live result even if the immutable receipt
249
+ // insert loses to a sibling. The stored winner is for later recovery.
250
+ return outcome(completed, 'executed', { value });
251
+ }
@@ -24,10 +24,18 @@ export function cloneMapFrame(frame: MapExecutionFrame): MapExecutionFrame {
24
24
  ...(frame.suspendedInputRows
25
25
  ? {
26
26
  suspendedInputRows: frame.suspendedInputRows.map((row) => ({
27
- inputIndex: row.inputIndex,
27
+ ...row,
28
28
  data: cloneCsvAliasedRow(row.data),
29
- ...(row.authoredInputDigest
30
- ? { authoredInputDigest: row.authoredInputDigest }
29
+ ...(row.authoredInput
30
+ ? {
31
+ authoredInput: {
32
+ ...row.authoredInput,
33
+ data: cloneCsvAliasedRow(row.authoredInput.data),
34
+ ...(row.authoredInput.source
35
+ ? { source: { ...row.authoredInput.source } }
36
+ : {}),
37
+ },
38
+ }
31
39
  : {}),
32
40
  })),
33
41
  }
@@ -35,6 +35,7 @@ export function createGatewayRunLedgerProjectionAdapter(
35
35
  runId: input.runId,
36
36
  runtimeApi: input.runtimeApi,
37
37
  tokenAuthority: 'gateway' as const,
38
+ ...(input.runtimeApi.signal ? { signal: input.runtimeApi.signal } : {}),
38
39
  };
39
40
  return {
40
41
  async deliver(request) {
@@ -1,6 +1,8 @@
1
1
  import {
2
+ EXPLICIT_FRESHNESS_PLAY_AUTHORING_THRESHOLD,
2
3
  normalizePlayAuthoringContractEdition,
3
4
  playToolListGetterMode,
5
+ playCsvDatasetRowIdentity,
4
6
  type PlayAuthoringContractEdition,
5
7
  } from '../plays/authoring-contract';
6
8
 
@@ -11,6 +13,9 @@ export type PlayRuntimeSecretValueSemantics =
11
13
  export type PlayRuntimeFetchHttpFailureSemantics =
12
14
  | 'return_response'
13
15
  | 'throw_typed_error';
16
+ export type PlayRuntimeFreshnessSemantics =
17
+ | 'legacy_bucket'
18
+ | 'actual_age_with_operation_recovery';
14
19
 
15
20
  /**
16
21
  * Immutable worker-side projection of the semantics pinned by a Play artifact.
@@ -35,6 +40,7 @@ export type PlayRuntimeSemantics = Readonly<{
35
40
  */
36
41
  receiptIdentityEdition: PlayAuthoringContractEdition | null;
37
42
  }>;
43
+ freshness: PlayRuntimeFreshnessSemantics;
38
44
  inlineChild: Readonly<{
39
45
  /**
40
46
  * Whether a scalar child may rely solely on its execution-time
@@ -50,11 +56,12 @@ export type PlayRuntimeSemantics = Readonly<{
50
56
  slackHitl: Readonly<{
51
57
  defaultTimeoutMs: number;
52
58
  }>;
59
+ datasets: Readonly<{ csvRowIdentity: 'source_row' | 'key' }>;
53
60
  }>;
54
61
 
55
62
  type PlayRuntimeSemanticsTemplate = Omit<
56
63
  PlayRuntimeSemantics,
57
- 'authoringContractEdition' | 'tools'
64
+ 'authoringContractEdition' | 'freshness' | 'tools' | 'datasets'
58
65
  > & { tools: Omit<PlayRuntimeSemantics['tools'], 'listGetters'> };
59
66
 
60
67
  function definePlayRuntimeSemantics(
@@ -66,12 +73,22 @@ function definePlayRuntimeSemantics(
66
73
  ...semantics,
67
74
  secrets: Object.freeze({ ...semantics.secrets }),
68
75
  fetch: Object.freeze({ ...semantics.fetch }),
76
+ freshness:
77
+ semantics.authoringContractEdition >=
78
+ EXPLICIT_FRESHNESS_PLAY_AUTHORING_THRESHOLD
79
+ ? 'actual_age_with_operation_recovery'
80
+ : 'legacy_bucket',
69
81
  inlineChild: Object.freeze({ ...semantics.inlineChild }),
70
82
  tools: Object.freeze({
71
83
  ...semantics.tools,
72
84
  listGetters: playToolListGetterMode(semantics.authoringContractEdition),
73
85
  }),
74
86
  slackHitl: Object.freeze({ ...semantics.slackHitl }),
87
+ datasets: Object.freeze({
88
+ csvRowIdentity: playCsvDatasetRowIdentity(
89
+ semantics.authoringContractEdition,
90
+ ),
91
+ }),
75
92
  });
76
93
  }
77
94
 
@@ -198,6 +215,18 @@ const PLAY_RUNTIME_SEMANTICS_BY_AUTHORING = {
198
215
  tools: { nativeBatchResponse: 'logical_response' },
199
216
  slackHitl: WEEK_LONG_SLACK_HITL_SEMANTICS,
200
217
  },
218
+ 11: {
219
+ ...PLAINTEXT_SECRET_SEMANTICS,
220
+ fetch: {
221
+ httpFailure: 'throw_typed_error',
222
+ persistHttpFailure: false,
223
+ // Freshness policy does not create another fetch receipt identity.
224
+ receiptIdentityEdition: FETCH_RECEIPT_IDENTITIES.typedHttpFailure,
225
+ },
226
+ inlineChild: { runtimeOnly: true },
227
+ tools: { nativeBatchResponse: 'logical_response' },
228
+ slackHitl: WEEK_LONG_SLACK_HITL_SEMANTICS,
229
+ },
201
230
  } satisfies Record<PlayAuthoringContractEdition, PlayRuntimeSemanticsTemplate>;
202
231
 
203
232
  const PLAY_RUNTIME_SEMANTICS = Object.fromEntries(
@@ -90,6 +90,32 @@ export function buildPlayWorkflowId(
90
90
  return `play/${familyKey}/run/${buildPlayWorkflowRunSuffix(timestamp)}${entropy}`;
91
91
  }
92
92
 
93
+ /** Caller-owned identity binds the complete submitted JSON before admission. */
94
+ export function buildCallerPlayWorkflowId(
95
+ playName: string,
96
+ request: Record<string, unknown>,
97
+ now?: Date,
98
+ ): string {
99
+ const { workflowId: _ignored, ...intent } = request;
100
+ const wireIntent = JSON.parse(JSON.stringify(intent));
101
+ return `${buildPlayWorkflowId(playName, now, { unique: true })}-caller-${sha256Hex(stableStringify(wireIntent))}`;
102
+ }
103
+
104
+ export function callerPlayWorkflowIdMatchesRequest(
105
+ workflowId: string,
106
+ request: Record<string, unknown>,
107
+ ): boolean {
108
+ const match =
109
+ /^play\/[a-z0-9/-]+\/run\/\d{8}t\d{6}-\d{3}-[a-z0-9]{8}-caller-([a-f0-9]{64})$/.exec(
110
+ workflowId,
111
+ );
112
+ if (!match || canonicalPlayRunStartedAtMs(workflowId) === null) return false;
113
+ const { workflowId: _ignored, ...intent } = request;
114
+ return (
115
+ match[1] === sha256Hex(stableStringify(JSON.parse(JSON.stringify(intent))))
116
+ );
117
+ }
118
+
93
119
  export type PlayRunIdentity = {
94
120
  workflowId: string;
95
121
  runId: string;