deepline 0.3.460 → 0.3.462

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 (297) hide show
  1. package/dist/bundling-sources/sdk/src/client-core.ts +7593 -0
  2. package/dist/bundling-sources/sdk/src/client.ts +8 -7366
  3. package/dist/bundling-sources/sdk/src/http-core.ts +1239 -0
  4. package/dist/bundling-sources/sdk/src/http.ts +6 -1231
  5. package/dist/bundling-sources/sdk/src/index.ts +4 -1
  6. package/dist/bundling-sources/sdk/src/play.ts +5 -5
  7. package/dist/bundling-sources/sdk/src/plays/client-core.ts +437 -0
  8. package/dist/bundling-sources/sdk/src/plays/client.ts +4 -421
  9. package/dist/bundling-sources/sdk/src/plays/input-example.ts +3 -4
  10. package/dist/bundling-sources/sdk/src/plays/play-description.ts +1 -1
  11. package/dist/bundling-sources/sdk/src/plays/run-request.ts +1 -1
  12. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  13. package/dist/bundling-sources/sdk/src/server.ts +43 -0
  14. package/dist/bundling-sources/sdk/src/types.ts +15 -5
  15. package/dist/bundling-sources/shared_libs/integration-contracts/billing-entitlements.ts +15 -0
  16. package/dist/bundling-sources/shared_libs/monitors/billing/state.ts +28 -0
  17. package/dist/bundling-sources/shared_libs/monitors/callback-security.ts +0 -307
  18. package/dist/bundling-sources/shared_libs/monitors/http-error-response.ts +3 -0
  19. package/dist/bundling-sources/shared_libs/monitors/lifecycle-notifications.ts +3 -2
  20. package/dist/bundling-sources/shared_libs/monitors/package.json +2 -0
  21. package/dist/bundling-sources/shared_libs/monitors/provider-callback-verification.ts +34 -0
  22. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-batch-operation.ts +59 -0
  23. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-upstream-identity.ts +94 -1
  24. package/dist/bundling-sources/shared_libs/monitors/signal-radars/inbound-dead-letter.ts +35 -1
  25. package/dist/bundling-sources/shared_libs/monitors/signal-radars/inbound-failure-codes.ts +24 -0
  26. package/dist/bundling-sources/shared_libs/observability/dlq.ts +32 -0
  27. package/dist/bundling-sources/shared_libs/observability/scheduled-jobs.json +57 -3
  28. package/dist/bundling-sources/shared_libs/observability/scheduled-work.ts +1 -0
  29. package/dist/bundling-sources/shared_libs/play-data-plane/r2.ts +66 -1
  30. package/dist/bundling-sources/shared_libs/play-data-plane/sheet-column-storage.ts +36 -7
  31. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +250 -11
  32. package/dist/bundling-sources/shared_libs/play-runtime/array-input-store.ts +319 -0
  33. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +991 -359
  34. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +17 -0
  35. package/dist/bundling-sources/shared_libs/play-runtime/daytona-sandbox-janitor.ts +13 -0
  36. package/dist/bundling-sources/shared_libs/play-runtime/daytona-sandbox-reaper.ts +110 -0
  37. package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-machines.ts +155 -6
  38. package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-registry.ts +110 -6
  39. package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +127 -13
  40. package/dist/bundling-sources/shared_libs/play-runtime/execution-capabilities.ts +14 -1
  41. package/dist/bundling-sources/shared_libs/play-runtime/fixture-behavior.ts +57 -1
  42. package/dist/bundling-sources/shared_libs/play-runtime/free-company-lookup-batching.ts +218 -0
  43. package/dist/bundling-sources/shared_libs/play-runtime/gateway-postgres-admission.ts +42 -5
  44. package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-budget-state-backend.ts +6 -1
  45. package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +92 -31
  46. package/dist/bundling-sources/shared_libs/play-runtime/governor/block-reserving-budget-state-backend.ts +69 -14
  47. package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +93 -23
  48. package/dist/bundling-sources/shared_libs/play-runtime/governor/in-memory-rate-state-backend.ts +15 -4
  49. package/dist/bundling-sources/shared_libs/play-runtime/governor/policy.ts +22 -18
  50. package/dist/bundling-sources/shared_libs/play-runtime/governor/rate-state-backend.ts +5 -0
  51. package/dist/bundling-sources/shared_libs/play-runtime/launch-identity.ts +2 -0
  52. package/dist/bundling-sources/shared_libs/play-runtime/map-memory-limits.ts +190 -14
  53. package/dist/bundling-sources/shared_libs/play-runtime/memory-ledger.ts +131 -0
  54. package/dist/bundling-sources/shared_libs/play-runtime/modal-runtime-config.ts +77 -2
  55. package/dist/bundling-sources/shared_libs/play-runtime/output-size-limits.ts +271 -2
  56. package/dist/bundling-sources/shared_libs/play-runtime/play-run-recovery-policy.ts +51 -0
  57. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/sandbox-cleanup/contracts.ts +2 -0
  58. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/sandbox-cleanup/job.ts +27 -1
  59. package/dist/bundling-sources/shared_libs/play-runtime/play-runtime-batching-registry.ts +2 -0
  60. package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +3 -0
  61. package/dist/bundling-sources/shared_libs/play-runtime/provider-text-for-log.ts +63 -0
  62. package/dist/bundling-sources/shared_libs/play-runtime/resource-governor.ts +19 -0
  63. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger-event-transport.ts +112 -16
  64. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +104 -2
  65. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +265 -151
  66. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-gateway-transport.ts +17 -0
  67. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-progress-push.ts +35 -14
  68. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-sheet-persistence-retry.ts +12 -1
  69. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-control-transport.ts +4 -0
  70. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-labels.ts +36 -0
  71. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-launch-transport.ts +136 -70
  72. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts +237 -36
  73. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-runtime-watchdog.ts +148 -4
  74. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-session-execution.ts +6 -0
  75. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +285 -32
  76. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/modal.ts +251 -56
  77. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/runtime-sandbox-reconciliation.ts +29 -4
  78. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/sandbox-create-outcome.ts +27 -0
  79. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/types.ts +22 -1
  80. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +1397 -382
  81. package/dist/bundling-sources/shared_libs/play-runtime/runtime-array-input-receipts.ts +920 -0
  82. package/dist/bundling-sources/shared_libs/play-runtime/runtime-constants.ts +16 -0
  83. package/dist/bundling-sources/shared_libs/play-runtime/runtime-environment.ts +38 -0
  84. package/dist/bundling-sources/shared_libs/play-runtime/runtime-flow.ts +6 -0
  85. package/dist/bundling-sources/shared_libs/play-runtime/runtime-incident-drills.ts +26 -1
  86. package/dist/bundling-sources/shared_libs/play-runtime/runtime-operation-contract.ts +30 -0
  87. package/dist/bundling-sources/shared_libs/play-runtime/runtime-receipt-store-adapter.ts +14 -3
  88. package/dist/bundling-sources/shared_libs/play-runtime/runtime-receipt-writer.ts +31 -4
  89. package/dist/bundling-sources/shared_libs/play-runtime/runtime-sheet-row-transition.ts +47 -0
  90. package/dist/bundling-sources/shared_libs/play-runtime/sandbox-compute-usage.ts +20 -7
  91. package/dist/bundling-sources/shared_libs/play-runtime/sandbox-runs/contract.ts +12 -0
  92. package/dist/bundling-sources/shared_libs/play-runtime/sandbox-runs/index.ts +9 -0
  93. package/dist/bundling-sources/shared_libs/play-runtime/sandbox-runs/runner-backend-adapter.ts +10 -0
  94. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +4 -0
  95. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +1496 -320
  96. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-worker-required-env.ts +31 -1
  97. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +1389 -129
  98. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-gateway.ts +36 -0
  99. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-ledger-dispatch-authority.ts +4 -0
  100. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-ledger-outbox-recovery.ts +162 -23
  101. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-event-search.ts +499 -115
  102. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-sandbox-capacity.ts +377 -9
  103. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-sandbox-ledger-reconciliation.ts +117 -8
  104. package/dist/bundling-sources/shared_libs/play-runtime/secret-resolution-retry-policy.ts +25 -0
  105. package/dist/bundling-sources/shared_libs/play-runtime/terminal-runtime-performance.ts +391 -0
  106. package/dist/bundling-sources/shared_libs/play-runtime/test-runtime-seams.ts +40 -0
  107. package/dist/bundling-sources/shared_libs/play-runtime/tool-call/index.ts +6 -0
  108. package/dist/bundling-sources/shared_libs/play-runtime/tool-call/mapped-calls.ts +171 -0
  109. package/dist/bundling-sources/shared_libs/play-runtime/tool-call/receipt-cohort.ts +16 -1
  110. package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +26 -3
  111. package/dist/bundling-sources/shared_libs/play-runtime/tool-result.ts +16 -2
  112. package/dist/bundling-sources/shared_libs/play-runtime/transport-error-diagnostics.ts +28 -0
  113. package/dist/bundling-sources/shared_libs/play-runtime/work-receipt-storage-contract.ts +55 -5
  114. package/dist/bundling-sources/shared_libs/plays/authoring-contract.ts +41 -1
  115. package/dist/bundling-sources/shared_libs/plays/contracts.ts +4 -0
  116. package/dist/bundling-sources/shared_libs/plays/dataset.ts +9 -3
  117. package/dist/bundling-sources/shared_libs/plays/deploy/manifest-contract.ts +2 -0
  118. package/dist/bundling-sources/shared_libs/plays/deploy/manifest.ts +3 -2
  119. package/dist/bundling-sources/shared_libs/plays/deploy/project-serializer.ts +2 -2
  120. package/dist/bundling-sources/shared_libs/plays/run-output-summary.ts +66 -0
  121. package/dist/bundling-sources/shared_libs/plays/script-entry.ts +1 -1
  122. package/dist/bundling-sources/shared_libs/product-notifications/catalog.ts +43 -1
  123. package/dist/bundling-sources/shared_libs/product-notifications/content.ts +9 -0
  124. package/dist/bundling-sources/shared_libs/product-notifications/email.ts +91 -13
  125. package/dist/bundling-sources/shared_libs/product-notifications/events.ts +3 -0
  126. package/dist/bundling-sources/shared_libs/product-notifications/slack-delivery.ts +41 -24
  127. package/dist/bundling-sources/shared_libs/security/attested-client-ip.ts +91 -0
  128. package/dist/bundling-sources/shared_libs/security/internal-actor.ts +0 -11
  129. package/dist/cli/{auth-HKA6ZVHQ.mjs → auth-JDLEY7VU.mjs} +5 -4
  130. package/dist/cli/auth-UXKY6TCC.js +41 -0
  131. package/dist/cli/{billing-MTO2ZZ3G.js → billing-HXAXB3XZ.js} +152 -78
  132. package/dist/cli/{billing-26G44S5N.mjs → billing-SDHLBLMV.mjs} +84 -10
  133. package/dist/cli/{chunk-BHGII2D6.js → chunk-3ACMRZRV.js} +8 -6
  134. package/dist/cli/{chunk-S4Q2XCGM.mjs → chunk-3KZCFOZ6.mjs} +1 -1
  135. package/dist/cli/chunk-4KQ6SIBC.mjs +1038 -0
  136. package/dist/cli/{chunk-Z2GUFYT6.js → chunk-5LEAXRHV.js} +11 -11
  137. package/dist/cli/{chunk-XAYO3Q2X.mjs → chunk-6NNVJDVR.mjs} +7 -5
  138. package/dist/cli/{chunk-NMJ4D2WO.js → chunk-6PMCRYDA.js} +216 -91
  139. package/dist/cli/{chunk-OMM54B3X.mjs → chunk-6RNQGWFD.mjs} +16 -806
  140. package/dist/cli/{chunk-EBU7LQ64.js → chunk-6V7KVRR5.js} +3 -3
  141. package/dist/cli/{chunk-RDPTDDKX.js → chunk-7LFIMWMW.js} +2 -2
  142. package/dist/cli/chunk-7MJ6VI2T.mjs +19 -0
  143. package/dist/cli/chunk-7SWNPNML.js +1039 -0
  144. package/dist/cli/{chunk-CTRPCARW.mjs → chunk-7U3XNPOA.mjs} +267 -38
  145. package/dist/cli/{chunk-RBTTUYZZ.js → chunk-7VZHXVOQ.js} +1 -1
  146. package/dist/cli/{chunk-YVBWO647.mjs → chunk-AT636LLU.mjs} +2 -2
  147. package/dist/cli/{chunk-VTODQWQA.js → chunk-BGTPIMSM.js} +392 -261
  148. package/dist/cli/{chunk-KADIT5NV.mjs → chunk-CKSOY47X.mjs} +2 -2
  149. package/dist/cli/{chunk-7DH2FD7Y.mjs → chunk-D464JW3N.mjs} +5 -85
  150. package/dist/cli/chunk-D6DXOUTO.mjs +827 -0
  151. package/dist/cli/{describe-QW54QQNB.js → chunk-EM4TXCCI.js} +27 -44
  152. package/dist/cli/chunk-F6QV42MC.mjs +91 -0
  153. package/dist/cli/chunk-FKVYR3QA.mjs +1039 -0
  154. package/dist/cli/{chunk-LMAULZLF.js → chunk-GEGW5HPZ.js} +29 -27
  155. package/dist/cli/{chunk-M52JQSLK.js → chunk-HUB3CCM2.js} +13 -4
  156. package/dist/cli/{chunk-YHTQM5XI.js → chunk-I6VHOU5O.js} +27 -27
  157. package/dist/cli/{chunk-Z2XUZC34.js → chunk-IAYSWZDJ.js} +188 -76
  158. package/dist/cli/chunk-JF3TLIMQ.js +19 -0
  159. package/dist/cli/{chunk-HDFRJ4IT.mjs → chunk-JVHOS3K3.mjs} +177 -65
  160. package/dist/cli/{chunk-LAWEURCT.mjs → chunk-LFXLQEZN.mjs} +1 -1
  161. package/dist/cli/{chunk-2PBN4SZE.js → chunk-LROWFPGN.js} +16 -16
  162. package/dist/cli/{chunk-SLEUWLJE.mjs → chunk-MSY22NQW.mjs} +5 -5
  163. package/dist/cli/{chunk-LVJOFUU7.mjs → chunk-MUUQZIOF.mjs} +23 -49
  164. package/dist/cli/{chunk-2XHWOQF4.js → chunk-NBLSCHKK.js} +52 -50
  165. package/dist/cli/{chunk-526R2HON.mjs → chunk-NHRWZPWZ.mjs} +4 -2
  166. package/dist/cli/{chunk-KXMNM4AQ.js → chunk-NMU3VR5U.js} +4 -5
  167. package/dist/cli/{chunk-W3BE3CHK.mjs → chunk-O2RX5A66.mjs} +14 -12
  168. package/dist/cli/{chunk-UKFZT7RF.js → chunk-OMQ23IXL.js} +2 -2
  169. package/dist/cli/{chunk-LJTFZOHU.js → chunk-OPJAJBD4.js} +28 -54
  170. package/dist/cli/{chunk-MLFR6DVF.mjs → chunk-PHSEINNH.mjs} +5 -5
  171. package/dist/cli/{chunk-3DNKFH2G.mjs → chunk-PKHVSIJG.mjs} +19 -1
  172. package/dist/cli/{chunk-WIYWSYDS.mjs → chunk-Q2EIZKVX.mjs} +3 -3
  173. package/dist/cli/{chunk-VJAEA4CS.js → chunk-QJZ6WNWD.js} +8 -8
  174. package/dist/cli/{chunk-EIMG75BT.mjs → chunk-S3A4IXBQ.mjs} +4 -5
  175. package/dist/cli/{chunk-RZSTT45F.mjs → chunk-S42YIQNE.mjs} +13 -8
  176. package/dist/cli/{chunk-YULBYKAK.mjs → chunk-S6UAJR6V.mjs} +3 -3
  177. package/dist/cli/{chunk-G7L27KZB.mjs → chunk-TKAQR3IB.mjs} +31 -21
  178. package/dist/cli/{chunk-QWRHGGUH.js → chunk-UAJFWWZE.js} +20 -2
  179. package/dist/cli/{chunk-ZR2RLI4O.mjs → chunk-UDRPEZ5F.mjs} +223 -92
  180. package/dist/cli/{chunk-QGAWTBIR.js → chunk-ULK7JGGI.js} +511 -282
  181. package/dist/cli/chunk-V335HGX5.js +1038 -0
  182. package/dist/cli/{chunk-NH6JSJW6.mjs → chunk-V3MEA7NB.mjs} +7 -5
  183. package/dist/cli/{chunk-NRGGFVVB.js → chunk-VFGVXXZP.js} +37 -35
  184. package/dist/cli/chunk-VLEVVDWG.js +91 -0
  185. package/dist/cli/{chunk-R2GLGLDR.mjs → chunk-VPHN2HAL.mjs} +216 -91
  186. package/dist/cli/{chunk-CFUTOWE4.js → chunk-WG3KGAII.js} +8 -88
  187. package/dist/cli/{describe-TOMIAT37.mjs → chunk-WIY6U6GD.mjs} +19 -36
  188. package/dist/cli/{chunk-YS2YBSJB.js → chunk-XANRM3QA.js} +6 -6
  189. package/dist/cli/{chunk-KGQNIA37.js → chunk-XQCTG5OO.js} +44 -34
  190. package/dist/cli/{chunk-EJGBLXDZ.mjs → chunk-XZAFIOUQ.mjs} +1 -1
  191. package/dist/cli/chunk-Y4DPU4OM.js +827 -0
  192. package/dist/cli/chunk-YT5EODJO.js +920 -0
  193. package/dist/cli/{chunk-2WRO2UJ5.js → chunk-ZGQSFYD2.js} +56 -51
  194. package/dist/cli/{chunk-OJ6RUKVT.mjs → chunk-ZUK6LIJB.mjs} +10 -1
  195. package/dist/cli/command-docs.d.mts +5 -0
  196. package/dist/cli/command-docs.d.ts +5 -0
  197. package/dist/cli/command-docs.js +2 -2
  198. package/dist/cli/command-docs.mjs +1 -1
  199. package/dist/cli/{csv-6FNFYJUC.js → csv-C56GSCEU.js} +13 -12
  200. package/dist/cli/{csv-MDHT4BKK.mjs → csv-L54JS7TF.mjs} +4 -3
  201. package/dist/cli/{db-D6AMVOEW.js → db-MOGRMZTA.js} +30 -27
  202. package/dist/cli/{db-74O3RFWY.mjs → db-YVBUW2DX.mjs} +10 -7
  203. package/dist/cli/describe-KYEULWPG.js +48 -0
  204. package/dist/cli/describe-NLZKKT7C.mjs +48 -0
  205. package/dist/cli/describe-core.d.mts +153 -5
  206. package/dist/cli/describe-core.d.ts +153 -5
  207. package/dist/cli/describe-core.js +9 -5
  208. package/dist/cli/describe-core.mjs +10 -6
  209. package/dist/cli/{executions-N4FMGVSC.js → executions-A7L4ULPP.js} +9 -7
  210. package/dist/cli/{executions-PVSF4HUF.mjs → executions-ZBGSTT37.mjs} +7 -5
  211. package/dist/cli/{feedback-OB7NANZW.mjs → feedback-G6GU37FD.mjs} +6 -5
  212. package/dist/cli/{feedback-WFSPCAI2.js → feedback-OHLY7RYD.js} +10 -9
  213. package/dist/cli/getting-started.d.mts +6 -6
  214. package/dist/cli/getting-started.d.ts +6 -6
  215. package/dist/cli/getting-started.js +2 -2
  216. package/dist/cli/getting-started.mjs +1 -1
  217. package/dist/cli/git-P76N4NDQ.js +25 -0
  218. package/dist/cli/{git-FHUSPAEU.mjs → git-PKZW3QD5.mjs} +5 -4
  219. package/dist/cli/index.js +152 -142
  220. package/dist/cli/index.mjs +56 -46
  221. package/dist/cli/{legacy-noops-ECHNA5HN.js → legacy-noops-5LM3D724.js} +8 -7
  222. package/dist/cli/{legacy-noops-HMK4BSCI.mjs → legacy-noops-LJJYG6YA.mjs} +5 -4
  223. package/dist/cli/{monitors-U7VQ3EZM.mjs → monitors-72QV4ALO.mjs} +19 -6
  224. package/dist/cli/{monitors-XLLFNENH.js → monitors-UX3H5DC2.js} +103 -90
  225. package/dist/cli/{org-6H3USO5M.mjs → org-6RRRKOCP.mjs} +9 -6
  226. package/dist/cli/{org-QX6ER2HN.js → org-IF7ISUVB.js} +70 -67
  227. package/dist/cli/pages-3SU3HKL7.js +252 -0
  228. package/dist/cli/pages-DXDYYKEK.mjs +252 -0
  229. package/dist/cli/{play-MOB2WYOR.mjs → play-AHASKRLA.mjs} +512 -1508
  230. package/dist/cli/{play-WNB7HBZK.js → play-EUIUVLUH.js} +1090 -2086
  231. package/dist/cli/{providers-CXEZ23E2.mjs → providers-GWGE3WY3.mjs} +7 -5
  232. package/dist/cli/{providers-7M3DISKX.js → providers-V5WU4VXJ.js} +10 -8
  233. package/dist/cli/{quickstart-ZFCHKHF6.mjs → quickstart-OXKQ5ZST.mjs} +5 -4
  234. package/dist/cli/{quickstart-CLJT4AZL.js → quickstart-QP65I5PF.js} +13 -12
  235. package/dist/cli/run-6MM72IGL.mjs +443 -0
  236. package/dist/cli/run-F3FTYS56.js +443 -0
  237. package/dist/cli/search-OYKOOI7A.js +32 -0
  238. package/dist/cli/{search-FVYWYIC3.mjs → search-ZMEBVJZB.mjs} +8 -6
  239. package/dist/cli/{secrets-YRLKZQS5.js → secrets-ACND5N6A.js} +16 -14
  240. package/dist/cli/{secrets-WAMPTQFC.mjs → secrets-XRCJFF3L.mjs} +7 -5
  241. package/dist/cli/{sessions-IQ332BJH.mjs → sessions-RTK3CEW6.mjs} +5 -4
  242. package/dist/cli/{sessions-44J6WIVU.js → sessions-ZGRJPCFF.js} +6 -5
  243. package/dist/cli/{settings-4IJR2I2T.js → settings-PD5KWQQZ.js} +80 -77
  244. package/dist/cli/{settings-22S6NIFQ.mjs → settings-XDRBPRDE.mjs} +14 -11
  245. package/dist/cli/{setup-OZUPKKPW.mjs → setup-3HPYAOCC.mjs} +12 -10
  246. package/dist/cli/{setup-3FWSRNOX.js → setup-E2Q6PXHC.js} +53 -51
  247. package/dist/cli/{skills-5CW3NFP3.mjs → skills-OOQFMPOV.mjs} +7 -6
  248. package/dist/cli/skills-XYWV52KB.js +31 -0
  249. package/dist/cli/tools-ASCDZJCK.js +55 -0
  250. package/dist/cli/tools-C5ZT3UIC.mjs +55 -0
  251. package/dist/cli/update-5RQSEIFM.js +33 -0
  252. package/dist/cli/{update-IBULOV7P.mjs → update-ZWJTADIW.mjs} +9 -8
  253. package/dist/cli/{workflow-MVZT7MWT.mjs → workflow-23NMPAJ7.mjs} +10 -8
  254. package/dist/cli/{workflow-Z7FW6GT2.js → workflow-TCXU5YJV.js} +20 -18
  255. package/dist/client-DS2IWYZF.d.ts +10 -0
  256. package/dist/client-DTwnz_AW.d.mts +10 -0
  257. package/dist/{client-CYqG23K1.d.ts → client-core-B_6HAIBy.d.ts} +385 -12
  258. package/dist/{client-BbU5_j7n.d.mts → client-core-DzBSlY1U.d.mts} +385 -12
  259. package/dist/{compiler-manifest-CWz_sNOQ.d.mts → compiler-manifest-Dcyd3-Zd.d.mts} +43 -1
  260. package/dist/{compiler-manifest-CWz_sNOQ.d.ts → compiler-manifest-Dcyd3-Zd.d.ts} +43 -1
  261. package/dist/errors-d-sTrZhe.d.mts +142 -0
  262. package/dist/errors-yQwfgrcX.d.ts +142 -0
  263. package/dist/{index-BF2JB_Ug.d.mts → index-C6oXchjN.d.mts} +1 -1
  264. package/dist/{index-4NPXDzlD.d.ts → index-P56zy_z5.d.ts} +1 -1
  265. package/dist/index.d.mts +11 -147
  266. package/dist/index.d.ts +11 -147
  267. package/dist/index.js +15 -11
  268. package/dist/index.mjs +14 -10
  269. package/dist/install-integrity.json +141 -104
  270. package/dist/plays/bundle-play-file.d.mts +3 -3
  271. package/dist/plays/bundle-play-file.d.ts +3 -3
  272. package/dist/plays/bundle-play-file.mjs +2 -1
  273. package/dist/plays/play-description.d.mts +2 -2
  274. package/dist/plays/play-description.d.ts +2 -2
  275. package/dist/plays/play-description.js +2 -2
  276. package/dist/plays/play-description.mjs +1 -1
  277. package/dist/plays/run-request.d.mts +2 -2
  278. package/dist/plays/run-request.d.ts +2 -2
  279. package/dist/release.d.mts +1 -1
  280. package/dist/release.d.ts +1 -1
  281. package/dist/release.js +2 -2
  282. package/dist/release.mjs +1 -1
  283. package/dist/server.d.mts +17 -0
  284. package/dist/server.d.ts +17 -0
  285. package/dist/server.js +76 -0
  286. package/dist/server.mjs +76 -0
  287. package/package.json +6 -1
  288. package/dist/cli/auth-VDOCNR7H.js +0 -40
  289. package/dist/cli/chunk-PQUUWFXY.js +0 -95
  290. package/dist/cli/chunk-QJLKYYPX.js +0 -1710
  291. package/dist/cli/chunk-YBWAG72P.mjs +0 -95
  292. package/dist/cli/git-4PMBJSH5.js +0 -24
  293. package/dist/cli/search-4CCQCTVD.js +0 -30
  294. package/dist/cli/skills-LMNM6B5F.js +0 -30
  295. package/dist/cli/tools-AGBBDJ7P.js +0 -45
  296. package/dist/cli/tools-NOPBDRGW.mjs +0 -45
  297. package/dist/cli/update-U3W6USVD.js +0 -32
@@ -34,6 +34,7 @@ import {
34
34
  } from './runtime-pg-driver';
35
35
  import {
36
36
  createDeferredPlayDataset,
37
+ iteratePlayDatasetInputPages,
37
38
  type PlayDataset,
38
39
  } from '../plays/dataset';
39
40
  import type {
@@ -50,6 +51,7 @@ import {
50
51
  } from '../play-data-plane/sheet-contract';
51
52
  import {
52
53
  RuntimeSheetColumns,
54
+ RuntimeSheetColumnSchemaChangeRequired,
53
55
  type RuntimeSheetColumnTypes,
54
56
  } from '../play-data-plane/sheet-column-storage';
55
57
  import {
@@ -116,9 +118,27 @@ import {
116
118
  } from './postgres-json';
117
119
  import { RECEIPT_STATUS_CODE, receiptStatusFromCode } from './receipt-status';
118
120
  import {
121
+ assertWorkReceiptKeyMutable,
119
122
  WORK_RECEIPT_EXECUTION_LOCK_TABLE,
120
123
  RUNTIME_SHEET_WRITE_INTENT_RECEIPT_KIND,
121
124
  } from './work-receipt-storage-contract';
125
+ import type {
126
+ RuntimeArrayInputScope,
127
+ RuntimeArrayInputOccurrence,
128
+ RuntimeArrayInputManifest,
129
+ } from './array-input-store';
130
+ import { RuntimeArrayInputStoreUnavailableError } from './array-input-store';
131
+ import {
132
+ getRuntimeArrayInputManifest as getRuntimeArrayInputManifestReceipt,
133
+ beginRuntimeArrayInput as beginRuntimeArrayInputReceipt,
134
+ putRuntimeArrayInputPage as putRuntimeArrayInputPageReceipt,
135
+ sealRuntimeArrayInput as sealRuntimeArrayInputReceipt,
136
+ readRuntimeArrayInputPage as readRuntimeArrayInputPageReceipt,
137
+ type RuntimeArrayInputBegin,
138
+ type RuntimeArrayInputPutPage,
139
+ type RuntimeArrayInputSeal,
140
+ type RuntimeArrayInputReceiptPort,
141
+ } from './runtime-array-input-receipts';
122
142
  import {
123
143
  workReceiptClaimableStatusCodes,
124
144
  workReceiptActiveOwnerPredicateSql,
@@ -135,6 +155,7 @@ import type { MapRowOutcome } from './durability-store';
135
155
  import { RUNTIME_CAPACITY_POLICY } from './runtime-capacity-policy';
136
156
  import type { RUNTIME_SHEET_ADMISSION_CAPABILITIES } from './runtime-operation-contract';
137
157
  import {
158
+ chunkPreparedRuntimeSheetRows,
138
159
  normalizeRuntimeMapInputIndex,
139
160
  prepareRuntimeSheetRowsForJsonTransport,
140
161
  prepareRuntimeSheetRowTransitions,
@@ -153,12 +174,17 @@ import {
153
174
  LEGACY_RUNTIME_RECEIPT_WIRE_HEADER,
154
175
  } from './runtime-contract';
155
176
  import { PLAY_RUNTIME_API_COMPAT_PATH } from './runtime-api-paths';
177
+ import { RUNNER_GATEWAY_CONNECTION_RETRY_BUDGET_MS } from './runtime-constants';
156
178
  import {
157
179
  PLAY_RUNTIME_SHEET_ATTEMPT_LEASE_TTL_MS,
158
180
  PLAY_RUNTIME_WORK_RECEIPT_LEASE_TTL_MS,
159
181
  } from './lease-policy';
160
182
  import { BoundedBatchFunction } from './bounded-batch-function';
161
- import { jsonByteLengthUpTo } from './output-size-limits';
183
+ import {
184
+ jsonByteLengthUpTo,
185
+ RUNNER_TERMINAL_PUSH_MAX_BODY_BYTES,
186
+ RUNTIME_SHEET_START_TARGET_BODY_BYTES,
187
+ } from './output-size-limits';
162
188
  import {
163
189
  parseRuntimeTestFaultCounts,
164
190
  PLAY_RUNTIME_TEST_FAULT_HEADER,
@@ -188,6 +214,9 @@ type RuntimeApiContext = {
188
214
  disablePostgresPoolCache?: boolean | null;
189
215
  postgresSessionUnwrapKey?: string | null;
190
216
  abortSignal?: AbortSignal | null;
217
+ onSheetMapCompletion?: (
218
+ diagnostics: RuntimeSheetMapCompletionDiagnostics,
219
+ ) => void | Promise<void>;
191
220
  };
192
221
 
193
222
  const RUNTIME_RUN_DATASET_CATALOG_TABLE = '_deepline_run_datasets';
@@ -357,6 +386,7 @@ const runtimeSheetEnsureCache = new Map<
357
386
  { expiresAt: number; promise: Promise<void> }
358
387
  >();
359
388
  const DIRECT_POSTGRES_BATCH_SIZE = 10_000;
389
+ const RUNTIME_MAP_SQL_BATCH_INPUT_BYTES = 2 * 1024 * 1024;
360
390
  const RUNTIME_DB_SESSION_ROW_LIMIT_FLOOR = DIRECT_POSTGRES_BATCH_SIZE;
361
391
  const APPEND_KEY_SUFFIX_LENGTH = 12;
362
392
  // On-demand (create_db_session) mint TTL. Deliberately NOT aligned with
@@ -427,6 +457,7 @@ export class RuntimeSheetDatasetReadBatchTooLargeError extends Error {
427
457
  // make progress instead of aborting a healthy queued start after 30 seconds.
428
458
  const RUNTIME_SHEET_ADMISSION_REQUEST_TIMEOUT_MS = 10 * 60_000;
429
459
  const RUNTIME_SHEET_COMPLETION_SERVER_OPERATION_TIMEOUT_MS = 75_000;
460
+ const RUNTIME_POSTGRES_ROLLBACK_CLEANUP_TIMEOUT_MS = 1_000;
430
461
  // Server admission is bounded to 30s and the single connection attempt to
431
462
  // 10s. Keep the transport outside that envelope so it never retries while the
432
463
  // first server-side write can still be active.
@@ -540,10 +571,13 @@ export type RuntimeSheetTiming = {
540
571
  type RuntimeMapCompletionPhase =
541
572
  | 'dbSessionAcquire'
542
573
  | 'poolAcquire'
574
+ | 'tenantAdmission'
575
+ | 'physicalConnect'
543
576
  | 'transactionBegin'
544
577
  | 'mapRowsUpdate'
545
578
  | 'missingRowsInsert'
546
579
  | 'failedRowsUpdate'
580
+ | 'membershipCapture'
547
581
  | 'transactionCommit'
548
582
  | 'queryClient';
549
583
 
@@ -555,12 +589,34 @@ type RuntimeMapCompletionDiagnostics = Record<
555
589
  provisioningRetryAttempts: number;
556
590
  };
557
591
 
592
+ /**
593
+ * Fixed, payload-free elapsed timings. Nested phases overlap: poolAcquire includes
594
+ * tenantAdmission and physicalConnect (the bounded driver connect attempt).
595
+ */
596
+ export type RuntimeSheetMapCompletionDiagnostics =
597
+ RuntimeMapCompletionDiagnostics & {
598
+ event: 'finished';
599
+ outcome: 'succeeded' | 'failed';
600
+ inputRows: number;
601
+ uniqueRows: number;
602
+ completedRows: number;
603
+ failedRows: number;
604
+ chunks: number;
605
+ updatedRows: number;
606
+ fencedRows: number;
607
+ totalMs: number;
608
+ };
609
+
558
610
  function createRuntimeMapCompletionDiagnostics(): RuntimeMapCompletionDiagnostics {
559
611
  return {
560
612
  dbSessionAcquireMs: 0,
561
613
  dbSessionAcquireFailures: 0,
562
614
  poolAcquireMs: 0,
563
615
  poolAcquireFailures: 0,
616
+ tenantAdmissionMs: 0,
617
+ tenantAdmissionFailures: 0,
618
+ physicalConnectMs: 0,
619
+ physicalConnectFailures: 0,
564
620
  transactionBeginMs: 0,
565
621
  transactionBeginFailures: 0,
566
622
  mapRowsUpdateMs: 0,
@@ -569,6 +625,8 @@ function createRuntimeMapCompletionDiagnostics(): RuntimeMapCompletionDiagnostic
569
625
  missingRowsInsertFailures: 0,
570
626
  failedRowsUpdateMs: 0,
571
627
  failedRowsUpdateFailures: 0,
628
+ membershipCaptureMs: 0,
629
+ membershipCaptureFailures: 0,
572
630
  transactionCommitMs: 0,
573
631
  transactionCommitFailures: 0,
574
632
  queryClientMs: 0,
@@ -689,6 +747,26 @@ type RuntimeApiActionRequest =
689
747
  | {
690
748
  action: 'runtime_receipts_release_attempt';
691
749
  input: Parameters<typeof releaseRuntimeWorkReceipts>[1];
750
+ }
751
+ | {
752
+ action: 'runtime_array_input_get_manifest';
753
+ input: Parameters<typeof getRuntimeArrayInputManifest>[1];
754
+ }
755
+ | {
756
+ action: 'runtime_array_input_begin';
757
+ input: Parameters<typeof beginRuntimeArrayInput>[1];
758
+ }
759
+ | {
760
+ action: 'runtime_array_input_put_page';
761
+ input: Parameters<typeof putRuntimeArrayInputPage>[1];
762
+ }
763
+ | {
764
+ action: 'runtime_array_input_seal';
765
+ input: Parameters<typeof sealRuntimeArrayInput>[1];
766
+ }
767
+ | {
768
+ action: 'runtime_array_input_read_page';
769
+ input: Parameters<typeof readRuntimeArrayInputPage>[1];
692
770
  };
693
771
 
694
772
  /**
@@ -869,23 +947,42 @@ async function readRuntimeApiResponseJson(input: {
869
947
  async function postRuntimeApi<TResponse>(
870
948
  context: RuntimeApiContext,
871
949
  body: RuntimeApiActionRequest,
950
+ preparedBodyJson?: string,
872
951
  ): Promise<TResponse> {
873
952
  const url = resolveRuntimeApiUrl(context);
874
953
  const vercelHeaders = vercelProtectionBypassHeader(
875
954
  context.vercelProtectionBypassToken,
876
955
  );
956
+ // Older receipt gateways validate their top-level scope before dispatching
957
+ // an action. Include the same authenticated input scope so an unsupported
958
+ // named array action reaches their explicit unsupported-action response.
959
+ const requestBody =
960
+ body.action === 'runtime_array_input_get_manifest' ||
961
+ body.action === 'runtime_array_input_begin' ||
962
+ body.action === 'runtime_array_input_put_page' ||
963
+ body.action === 'runtime_array_input_seal' ||
964
+ body.action === 'runtime_array_input_read_page'
965
+ ? { ...body, playName: body.input.playName, runId: body.input.runId }
966
+ : body;
877
967
  const maxAttempts = runtimeApiMaxAttempts(body.action);
878
- const operationStartedAt = Date.now();
879
- for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
968
+ const budget = startRuntimeApiRetryBudget(body.action);
969
+ let unreachableError: Error | null = null;
970
+ for (let attempt = 1; ; attempt += 1) {
880
971
  let response: Response | null = null;
881
972
  let parsed: Record<string, unknown> | null = null;
882
973
  const abortController = new AbortController();
883
- const operationRemainingMs = runtimeApiOperationRemainingMs(
884
- body.action,
885
- operationStartedAt,
886
- );
974
+ const previousAttemptUnreachable =
975
+ budget.unreachableAttemptStartedAt !== null;
976
+ const attemptStartedAt = startRuntimeApiAttempt(budget);
977
+ const operationRemainingMs =
978
+ runtimeApiRetryDeadlineAt(
979
+ budget,
980
+ previousAttemptUnreachable ? 'no_response' : 'response',
981
+ ) - attemptStartedAt;
887
982
  if (operationRemainingMs <= 0) {
888
- throw runtimeApiTotalDeadlineError(body.action);
983
+ throw previousAttemptUnreachable && unreachableError
984
+ ? unreachableError
985
+ : runtimeApiTotalDeadlineError(body.action);
889
986
  }
890
987
  const timeout = setTimeout(
891
988
  () => abortController.abort(),
@@ -895,7 +992,9 @@ async function postRuntimeApi<TResponse>(
895
992
  response = await fetch(url, {
896
993
  method: 'POST',
897
994
  headers: resolveRuntimeApiHeaders(context, vercelHeaders),
898
- body: JSON.stringify(prepareRetainedPlayPipelinesForTransport(body)),
995
+ body:
996
+ preparedBodyJson ??
997
+ JSON.stringify(prepareRetainedPlayPipelinesForTransport(requestBody)),
899
998
  signal: context.abortSignal
900
999
  ? AbortSignal.any([context.abortSignal, abortController.signal])
901
1000
  : abortController.signal,
@@ -929,28 +1028,39 @@ async function postRuntimeApi<TResponse>(
929
1028
  if (context.abortSignal?.aborted) {
930
1029
  throw context.abortSignal.reason ?? error;
931
1030
  }
932
- if (
933
- runtimeApiOperationRemainingMs(body.action, operationStartedAt) <= 0
934
- ) {
935
- throw runtimeApiTotalDeadlineError(body.action);
936
- }
937
- if (attempt < maxAttempts) {
1031
+ const message = error instanceof Error ? error.message : String(error);
1032
+ if (response) {
1033
+ // A status line arrived and then its body failed or stalled. The
1034
+ // gateway answered, so this spends the ordinary attempt ladder; only a
1035
+ // request that got no response at all retries through a network cut.
1036
+ const bodyError = new Error(
1037
+ `Runtime API request to ${url} failed while reading the response body (status ${response.status}): ${message}`,
1038
+ );
1039
+ budget.respondedAttempts += 1;
1040
+ if (budget.respondedAttempts >= maxAttempts) throw bodyError;
938
1041
  await sleepRuntimeApiRetry({
939
1042
  action: body.action,
940
1043
  attempt,
941
- operationStartedAt,
1044
+ deadlineAt: runtimeApiRetryDeadlineAt(budget, 'response'),
1045
+ deadlineError: bodyError,
942
1046
  abortSignal: context.abortSignal ?? undefined,
943
1047
  });
944
1048
  continue;
945
1049
  }
946
- const message = error instanceof Error ? error.message : String(error);
947
- throw new Error(
948
- `Runtime API request to ${url} failed ${
949
- response
950
- ? 'while reading the response body'
951
- : 'before receiving a response'
952
- }: ${message}`,
1050
+ // No response arrived: DNS, connect, reset, or the request timeout.
1051
+ // Keep trying through a sandbox network cut.
1052
+ budget.unreachableAttemptStartedAt = attemptStartedAt;
1053
+ unreachableError = new Error(
1054
+ `Runtime API request to ${url} failed before receiving a response: ${message}`,
953
1055
  );
1056
+ await sleepRuntimeApiRetry({
1057
+ action: body.action,
1058
+ attempt,
1059
+ deadlineAt: runtimeApiRetryDeadlineAt(budget, 'no_response'),
1060
+ deadlineError: unreachableError,
1061
+ abortSignal: context.abortSignal ?? undefined,
1062
+ });
1063
+ continue;
954
1064
  } finally {
955
1065
  clearTimeout(timeout);
956
1066
  }
@@ -963,11 +1073,13 @@ async function postRuntimeApi<TResponse>(
963
1073
  // response body. Treat that as a transport failure, not a typed success:
964
1074
  // every runtime action returns a JSON object and runtime writes are
965
1075
  // already retry-safe across the equivalent fetch-failed boundary.
966
- if (attempt < maxAttempts) {
1076
+ budget.respondedAttempts += 1;
1077
+ if (budget.respondedAttempts < maxAttempts) {
967
1078
  await sleepRuntimeApiRetry({
968
1079
  action: body.action,
969
1080
  attempt,
970
- operationStartedAt,
1081
+ deadlineAt: runtimeApiRetryDeadlineAt(budget, 'response'),
1082
+ deadlineError: runtimeApiTotalDeadlineError(body.action),
971
1083
  abortSignal: context.abortSignal ?? undefined,
972
1084
  });
973
1085
  continue;
@@ -1034,12 +1146,14 @@ async function postRuntimeApi<TResponse>(
1034
1146
  isTransientRuntimeApiErrorMessage(
1035
1147
  `${typeof parsed?.error === 'string' ? parsed.error : ''}\n${details ?? ''}`,
1036
1148
  ));
1037
- if (shouldRetryRuntimeResponse && attempt < maxAttempts) {
1149
+ budget.respondedAttempts += 1;
1150
+ if (shouldRetryRuntimeResponse && budget.respondedAttempts < maxAttempts) {
1038
1151
  await sleepRuntimeApiRetry({
1039
1152
  action: body.action,
1040
1153
  attempt,
1041
1154
  retryAfterMs,
1042
- operationStartedAt,
1155
+ deadlineAt: runtimeApiRetryDeadlineAt(budget, 'response'),
1156
+ deadlineError: runtimeApiTotalDeadlineError(body.action),
1043
1157
  abortSignal: context.abortSignal ?? undefined,
1044
1158
  });
1045
1159
  continue;
@@ -1072,14 +1186,28 @@ async function postRuntimeApi<TResponse>(
1072
1186
  new Error(`${errorMessage} (${diagnosticContext}).`),
1073
1187
  );
1074
1188
  }
1189
+ if (
1190
+ (body.action === 'runtime_array_input_get_manifest' ||
1191
+ body.action === 'runtime_array_input_begin') &&
1192
+ (((response.status === 403 || response.status === 409) &&
1193
+ errorCode === 'RUNTIME_ARRAY_INPUT_UNAVAILABLE' &&
1194
+ (parsed?.action === undefined || parsed.action === body.action)) ||
1195
+ (response.status === 400 &&
1196
+ errorMessage === 'Unsupported receipt gateway action.' &&
1197
+ parsed?.action === body.action) ||
1198
+ ((response.status === 400 || response.status === 403) &&
1199
+ (errorMessage === 'Unsupported runtime action capability scope.' ||
1200
+ errorMessage === 'Unsupported runtime data-plane action.' ||
1201
+ errorMessage === `Unsupported action: ${body.action}`)))
1202
+ ) {
1203
+ throw new RuntimeArrayInputStoreUnavailableError();
1204
+ }
1075
1205
  throw new Error(
1076
1206
  details
1077
1207
  ? `${errorMessage} (${diagnosticContext}): ${details}`
1078
1208
  : `${errorMessage} (${diagnosticContext}).`,
1079
1209
  );
1080
1210
  }
1081
-
1082
- throw new Error('Runtime API request failed after retries.');
1083
1211
  }
1084
1212
 
1085
1213
  function runtimeApiRetryDelayMs(
@@ -1087,8 +1215,11 @@ function runtimeApiRetryDelayMs(
1087
1215
  attempt: number,
1088
1216
  retryAfterMs?: number,
1089
1217
  ): number {
1218
+ // An operation runs past the ladder only after attempts that got no
1219
+ // response; it then waits the ladder's last step.
1220
+ const delays = runtimeApiRetryDelaysForAction(action);
1090
1221
  const configuredDelay =
1091
- runtimeApiRetryDelaysForAction(action)[attempt - 1] ??
1222
+ delays[Math.min(attempt, delays.length) - 1] ??
1092
1223
  RUNTIME_API_DEFAULT_RETRY_AFTER_MS;
1093
1224
  return retryAfterMs === undefined
1094
1225
  ? configuredDelay
@@ -1107,15 +1238,55 @@ function runtimeApiRequestTimeoutMs(
1107
1238
  : RUNTIME_API_REQUEST_TIMEOUT_MS;
1108
1239
  }
1109
1240
 
1110
- function runtimeApiOperationRemainingMs(
1241
+ /**
1242
+ * Retry bookkeeping for one runtime API operation. Attempts that got a
1243
+ * response spend the action's attempt ladder and, for map-row completion, its
1244
+ * total deadline. An attempt that got no response at all retries until
1245
+ * RUNNER_GATEWAY_CONNECTION_RETRY_BUDGET_MS after the operation began, and the
1246
+ * time spent unreachable is not charged to the response budget.
1247
+ */
1248
+ type RuntimeApiRetryBudget = {
1249
+ action: RuntimeApiActionRequest['action'];
1250
+ startedAt: number;
1251
+ respondedAttempts: number;
1252
+ unreachableMs: number;
1253
+ /** Start of the latest attempt, while that attempt got no response. */
1254
+ unreachableAttemptStartedAt: number | null;
1255
+ };
1256
+
1257
+ function startRuntimeApiRetryBudget(
1111
1258
  action: RuntimeApiActionRequest['action'],
1112
- startedAt: number,
1259
+ ): RuntimeApiRetryBudget {
1260
+ return {
1261
+ action,
1262
+ startedAt: Date.now(),
1263
+ respondedAttempts: 0,
1264
+ unreachableMs: 0,
1265
+ unreachableAttemptStartedAt: null,
1266
+ };
1267
+ }
1268
+
1269
+ /** Charges the previous attempt's time to the outage if it got no response. */
1270
+ function startRuntimeApiAttempt(budget: RuntimeApiRetryBudget): number {
1271
+ const now = Date.now();
1272
+ if (budget.unreachableAttemptStartedAt !== null) {
1273
+ budget.unreachableMs += now - budget.unreachableAttemptStartedAt;
1274
+ budget.unreachableAttemptStartedAt = null;
1275
+ }
1276
+ return now;
1277
+ }
1278
+
1279
+ function runtimeApiRetryDeadlineAt(
1280
+ budget: RuntimeApiRetryBudget,
1281
+ failure: 'response' | 'no_response',
1113
1282
  ): number {
1114
- return action === 'runtime_sheet_complete_map_rows'
1115
- ? Math.max(
1116
- 0,
1117
- RUNTIME_SHEET_COMPLETION_TOTAL_TIMEOUT_MS - (Date.now() - startedAt),
1118
- )
1283
+ if (failure === 'no_response') {
1284
+ return budget.startedAt + RUNNER_GATEWAY_CONNECTION_RETRY_BUDGET_MS;
1285
+ }
1286
+ return budget.action === 'runtime_sheet_complete_map_rows'
1287
+ ? budget.startedAt +
1288
+ budget.unreachableMs +
1289
+ RUNTIME_SHEET_COMPLETION_TOTAL_TIMEOUT_MS
1119
1290
  : Number.POSITIVE_INFINITY;
1120
1291
  }
1121
1292
 
@@ -1132,7 +1303,8 @@ async function sleepRuntimeApiRetry(input: {
1132
1303
  action: RuntimeApiActionRequest['action'];
1133
1304
  attempt: number;
1134
1305
  retryAfterMs?: number;
1135
- operationStartedAt: number;
1306
+ deadlineAt: number;
1307
+ deadlineError: Error;
1136
1308
  abortSignal?: AbortSignal;
1137
1309
  }): Promise<void> {
1138
1310
  const requestedDelayMs = runtimeApiRetryDelayMs(
@@ -1140,12 +1312,9 @@ async function sleepRuntimeApiRetry(input: {
1140
1312
  input.attempt,
1141
1313
  input.retryAfterMs,
1142
1314
  );
1143
- const operationRemainingMs = runtimeApiOperationRemainingMs(
1144
- input.action,
1145
- input.operationStartedAt,
1146
- );
1315
+ const operationRemainingMs = input.deadlineAt - Date.now();
1147
1316
  if (operationRemainingMs <= 0) {
1148
- throw runtimeApiTotalDeadlineError(input.action);
1317
+ throw input.deadlineError;
1149
1318
  }
1150
1319
  const delayMs = Math.min(requestedDelayMs, operationRemainingMs);
1151
1320
  await new Promise<void>((resolve, reject) => {
@@ -1164,7 +1333,7 @@ async function sleepRuntimeApiRetry(input: {
1164
1333
  input.abortSignal?.addEventListener('abort', onAbort, { once: true });
1165
1334
  });
1166
1335
  if (requestedDelayMs >= operationRemainingMs) {
1167
- throw runtimeApiTotalDeadlineError(input.action);
1336
+ throw input.deadlineError;
1168
1337
  }
1169
1338
  }
1170
1339
 
@@ -2119,11 +2288,14 @@ async function connectRuntimePostgresPool(
2119
2288
  lane: RuntimePostgresLane,
2120
2289
  postgresUrl: string,
2121
2290
  signal?: AbortSignal | null,
2291
+ completionDiagnostics?: RuntimeMapCompletionDiagnostics,
2122
2292
  ): Promise<RuntimePoolClient> {
2123
2293
  const admissionStartedAt = Date.now();
2124
- const releaseAdmission = await admission.acquire(lane, {
2125
- signal: signal ?? undefined,
2126
- });
2294
+ const releaseAdmission = await measureRuntimeSheetPhase(
2295
+ completionDiagnostics,
2296
+ 'tenantAdmission',
2297
+ () => admission.acquire(lane, { signal: signal ?? undefined }),
2298
+ );
2127
2299
  const admissionWaitMs = Math.max(0, Date.now() - admissionStartedAt);
2128
2300
  if (admissionWaitMs > 0) {
2129
2301
  const telemetry = admission.snapshot()[lane];
@@ -2142,6 +2314,8 @@ async function connectRuntimePostgresPool(
2142
2314
  }
2143
2315
  let timedOut = false;
2144
2316
  let timeout: ReturnType<typeof setTimeout> | null = null;
2317
+ const physicalConnectStartedAt = Date.now();
2318
+ let physicalConnectFailed = false;
2145
2319
  const connectPromise = pool.connect();
2146
2320
  connectPromise
2147
2321
  .then((client) => {
@@ -2185,11 +2359,19 @@ async function connectRuntimePostgresPool(
2185
2359
  },
2186
2360
  };
2187
2361
  } catch (error) {
2362
+ physicalConnectFailed = true;
2188
2363
  if (!timedOut) {
2189
2364
  releaseAdmission();
2190
2365
  }
2191
2366
  throw error;
2192
2367
  } finally {
2368
+ if (completionDiagnostics) {
2369
+ completionDiagnostics.physicalConnectMs +=
2370
+ Date.now() - physicalConnectStartedAt;
2371
+ completionDiagnostics.physicalConnectFailures += Number(
2372
+ physicalConnectFailed,
2373
+ );
2374
+ }
2193
2375
  if (timeout) {
2194
2376
  clearTimeout(timeout);
2195
2377
  }
@@ -2386,6 +2568,27 @@ async function awaitRuntimePostgresOperation<T>(input: {
2386
2568
  });
2387
2569
  }
2388
2570
 
2571
+ async function rollbackRuntimePostgresTransaction(
2572
+ client: RuntimePoolClient,
2573
+ ): Promise<void> {
2574
+ await awaitRuntimePostgresOperation({
2575
+ client,
2576
+ deadlineAt: Date.now() + RUNTIME_POSTGRES_ROLLBACK_CLEANUP_TIMEOUT_MS,
2577
+ operation: client.query('ROLLBACK'),
2578
+ });
2579
+ }
2580
+
2581
+ class RuntimeSheetTransactionRollbackError extends AggregateError {
2582
+ constructor(operationError: unknown, rollbackError: unknown) {
2583
+ super(
2584
+ [operationError, rollbackError],
2585
+ 'Runtime Sheet transaction rollback failed; transaction state is unknown',
2586
+ { cause: operationError },
2587
+ );
2588
+ this.name = 'RuntimeSheetTransactionRollbackError';
2589
+ }
2590
+ }
2591
+
2389
2592
  async function withRuntimePostgres<T>(
2390
2593
  session: RuntimePostgresSession,
2391
2594
  fn: (
@@ -2404,6 +2607,7 @@ async function withRuntimePostgres<T>(
2404
2607
  let requestLocalPool: RuntimePool | null = null;
2405
2608
  let poolLease: RuntimePostgresPoolLease | null = null;
2406
2609
  let roleTransactionStarted = false;
2610
+ let discardClient = false;
2407
2611
  let operationDeadlineAt: number | null = null;
2408
2612
  const cachePool =
2409
2613
  (options.cachePool ?? true) && canReuseRuntimePostgresPoolsAcrossRequests();
@@ -2430,6 +2634,7 @@ async function withRuntimePostgres<T>(
2430
2634
  lane,
2431
2635
  session.postgresUrl,
2432
2636
  options.signal,
2637
+ options.completionDiagnostics,
2433
2638
  ),
2434
2639
  );
2435
2640
  operationDeadlineAt =
@@ -2461,17 +2666,22 @@ async function withRuntimePostgres<T>(
2461
2666
  }
2462
2667
  break;
2463
2668
  } catch (error) {
2669
+ let failure = error;
2464
2670
  if (client) {
2465
2671
  if (roleTransactionStarted) {
2466
- await awaitRuntimePostgresOperation({
2467
- client,
2468
- deadlineAt: operationDeadlineAt,
2469
- operation: client.query('ROLLBACK'),
2470
- signal: options.signal,
2471
- }).catch(() => {});
2472
- roleTransactionStarted = false;
2672
+ try {
2673
+ await rollbackRuntimePostgresTransaction(client);
2674
+ } catch (rollbackError) {
2675
+ discardClient = true;
2676
+ failure = new RuntimeSheetTransactionRollbackError(
2677
+ error,
2678
+ rollbackError,
2679
+ );
2680
+ } finally {
2681
+ roleTransactionStarted = false;
2682
+ }
2473
2683
  }
2474
- client.release();
2684
+ client.release(discardClient);
2475
2685
  client = null;
2476
2686
  }
2477
2687
  // Queue overflow and caller cancellation happen before pool.connect().
@@ -2491,9 +2701,10 @@ async function withRuntimePostgres<T>(
2491
2701
  poolLease = null;
2492
2702
  if (
2493
2703
  attempt >= maxConnectAttempts ||
2494
- !isTransientRuntimePostgresConnectionError(error)
2704
+ failure instanceof RuntimeSheetTransactionRollbackError ||
2705
+ !isTransientRuntimePostgresConnectionError(failure)
2495
2706
  ) {
2496
- throw error;
2707
+ throw failure;
2497
2708
  }
2498
2709
  await sleep(
2499
2710
  RUNTIME_POSTGRES_CONNECT_RETRY_DELAYS_MS[attempt - 1] ??
@@ -2530,18 +2741,22 @@ async function withRuntimePostgres<T>(
2530
2741
  }
2531
2742
  return result;
2532
2743
  } catch (error) {
2744
+ if (error instanceof RuntimeSheetTransactionRollbackError) {
2745
+ discardClient = true;
2746
+ }
2533
2747
  if (roleTransactionStarted) {
2534
- await awaitRuntimePostgresOperation({
2535
- client,
2536
- deadlineAt: operationDeadlineAt,
2537
- operation: client.query('ROLLBACK'),
2538
- signal: options.signal,
2539
- }).catch(() => {});
2540
- roleTransactionStarted = false;
2748
+ try {
2749
+ await rollbackRuntimePostgresTransaction(client);
2750
+ } catch (rollbackError) {
2751
+ discardClient = true;
2752
+ throw new RuntimeSheetTransactionRollbackError(error, rollbackError);
2753
+ } finally {
2754
+ roleTransactionStarted = false;
2755
+ }
2541
2756
  }
2542
2757
  throw error;
2543
2758
  } finally {
2544
- client.release();
2759
+ client.release(discardClient);
2545
2760
  await poolLease?.release();
2546
2761
  if (requestLocalPool) {
2547
2762
  await Promise.resolve(requestLocalPool.end()).catch(() => {});
@@ -2673,6 +2888,7 @@ async function withRuntimeSheetProvisioningRetry<T>(
2673
2888
  retried: false,
2674
2889
  error: error instanceof Error ? error.message : String(error),
2675
2890
  });
2891
+ if (error instanceof RuntimeSheetTransactionRollbackError) throw error;
2676
2892
  if (isMissingRelationError(error)) {
2677
2893
  const ensureStartedAt = Date.now();
2678
2894
  await ensureRuntimeSheet(context, {
@@ -2741,77 +2957,109 @@ async function withRuntimeSheetQueryClient<T>(
2741
2957
  statementTimeoutMs?: number;
2742
2958
  timings?: RuntimeSheetTiming[];
2743
2959
  completionDiagnostics?: RuntimeMapCompletionDiagnostics;
2960
+ concurrentColumnPreparation?: boolean;
2744
2961
  },
2745
- operation: (client: RuntimeQueryClient) => Promise<T>,
2962
+ operation: (
2963
+ client: RuntimeQueryClient,
2964
+ columnLockMode: 'shared' | 'exclusive',
2965
+ ) => Promise<T>,
2746
2966
  ): Promise<T> {
2747
2967
  const resultStartedAt = Date.now();
2968
+ let columnLockMode: 'shared' | 'exclusive' = input.concurrentColumnPreparation
2969
+ ? 'shared'
2970
+ : 'exclusive';
2748
2971
  const resultPromise = withRuntimeSheetProvisioningRetry(
2749
2972
  context,
2750
2973
  { ...input, session },
2751
2974
  async () => {
2752
2975
  if (
2753
2976
  !input.transactional &&
2977
+ !input.concurrentColumnPreparation &&
2754
2978
  input.statementTimeoutMs === undefined &&
2755
2979
  isRuntimeOneShotQueryFactoryRegistered()
2756
2980
  ) {
2757
2981
  const operationStartedAt = Date.now();
2758
- return await withRuntimeOneShotPostgres(session, operation).finally(
2759
- () => {
2760
- input.timings?.push({
2761
- phase: 'one_shot_operation',
2762
- ms: Date.now() - operationStartedAt,
2763
- });
2764
- },
2765
- );
2982
+ return await withRuntimeOneShotPostgres(session, (client) =>
2983
+ operation(client, 'exclusive'),
2984
+ ).finally(() => {
2985
+ input.timings?.push({
2986
+ phase: 'one_shot_operation',
2987
+ ms: Date.now() - operationStartedAt,
2988
+ });
2989
+ });
2766
2990
  }
2767
2991
 
2768
- return await withRuntimePostgres(
2769
- session,
2770
- async (client, transaction) => {
2771
- const managesTransaction =
2772
- (input.transactional || input.statementTimeoutMs !== undefined) &&
2773
- !transaction.active;
2774
- if (managesTransaction) {
2775
- await measureRuntimeSheetPhase(
2776
- input.completionDiagnostics,
2777
- 'transactionBegin',
2778
- () => client.query('BEGIN'),
2779
- );
2780
- }
2781
- try {
2782
- if (input.statementTimeoutMs !== undefined) {
2783
- await client.query(
2784
- `SET LOCAL statement_timeout TO ${Math.max(1, Math.floor(input.statementTimeoutMs))}`,
2785
- );
2786
- }
2787
- const result = await operation(client);
2992
+ const runTransaction = () =>
2993
+ withRuntimePostgres(
2994
+ session,
2995
+ async (client, transaction) => {
2996
+ const managesTransaction =
2997
+ (input.concurrentColumnPreparation ||
2998
+ input.transactional ||
2999
+ input.statementTimeoutMs !== undefined) &&
3000
+ !transaction.active;
2788
3001
  if (managesTransaction) {
2789
3002
  await measureRuntimeSheetPhase(
2790
3003
  input.completionDiagnostics,
2791
- 'transactionCommit',
2792
- () => client.query('COMMIT'),
3004
+ 'transactionBegin',
3005
+ () => client.query('BEGIN'),
2793
3006
  );
2794
3007
  }
2795
- return result;
2796
- } catch (error) {
2797
- if (managesTransaction) {
2798
- await client.query('ROLLBACK').catch(() => {});
3008
+ try {
3009
+ if (input.statementTimeoutMs !== undefined) {
3010
+ await client.query(
3011
+ `SET LOCAL statement_timeout TO ${Math.max(1, Math.floor(input.statementTimeoutMs))}`,
3012
+ );
3013
+ }
3014
+ const result = await operation(client, columnLockMode);
3015
+ if (managesTransaction) {
3016
+ await measureRuntimeSheetPhase(
3017
+ input.completionDiagnostics,
3018
+ 'transactionCommit',
3019
+ () => client.query('COMMIT'),
3020
+ );
3021
+ }
3022
+ return result;
3023
+ } catch (error) {
3024
+ if (managesTransaction) {
3025
+ try {
3026
+ await rollbackRuntimePostgresTransaction(client);
3027
+ } catch (rollbackError) {
3028
+ throw new RuntimeSheetTransactionRollbackError(
3029
+ error,
3030
+ rollbackError,
3031
+ );
3032
+ }
3033
+ }
3034
+ throw error;
2799
3035
  }
2800
- throw error;
2801
- }
2802
- },
2803
- {
2804
- cachePool: !context.disablePostgresPoolCache,
2805
- maxConnectAttempts:
2806
- input.statementTimeoutMs === undefined ? undefined : 1,
2807
- operationTimeoutMs:
2808
- input.statementTimeoutMs === undefined
2809
- ? undefined
2810
- : RUNTIME_SHEET_COMPLETION_SERVER_OPERATION_TIMEOUT_MS,
2811
- signal: context.abortSignal,
2812
- completionDiagnostics: input.completionDiagnostics,
2813
- },
2814
- );
3036
+ },
3037
+ {
3038
+ cachePool: !context.disablePostgresPoolCache,
3039
+ maxConnectAttempts:
3040
+ input.statementTimeoutMs === undefined ? undefined : 1,
3041
+ operationTimeoutMs:
3042
+ input.statementTimeoutMs === undefined
3043
+ ? undefined
3044
+ : RUNTIME_SHEET_COMPLETION_SERVER_OPERATION_TIMEOUT_MS,
3045
+ signal: context.abortSignal,
3046
+ completionDiagnostics: input.completionDiagnostics,
3047
+ },
3048
+ );
3049
+ try {
3050
+ return await runTransaction();
3051
+ } catch (error) {
3052
+ if (
3053
+ columnLockMode !== 'shared' ||
3054
+ !(error instanceof RuntimeSheetColumnSchemaChangeRequired)
3055
+ )
3056
+ throw error;
3057
+ // The entire role/row transaction has confirmed rollback. Re-read
3058
+ // accepted rows, intent and schema under the existing exclusive fence;
3059
+ // never upgrade a shared lock or retain a pre-rollback SQL snapshot.
3060
+ columnLockMode = 'exclusive';
3061
+ return await runTransaction();
3062
+ }
2815
3063
  },
2816
3064
  );
2817
3065
  if (input.completionDiagnostics) {
@@ -3628,19 +3876,32 @@ async function readRuntimeRows(
3628
3876
  );
3629
3877
  }
3630
3878
  const { rows } = await client.query(
3879
+ // Select the preferred status once, using early-exit witnesses. A
3880
+ // window aggregate forces every fresh run row through WindowAgg even
3881
+ // for the first small page. Keep the same statement snapshot, status
3882
+ // preference, freshness predicate and public offset semantics.
3631
3883
  `WITH scoped AS (
3632
- SELECT *,
3633
- bool_or(_status = 'enriched') OVER () AS __has_enriched,
3634
- bool_or(_status = 'failed') OVER () AS __has_failed
3635
- FROM ${sheetTable(session)}
3636
- WHERE _run_id = $1::text
3637
- AND ${runtimeSheetResultFreshnessSql('_cell_meta', '$1::text')}
3884
+ SELECT CASE
3885
+ WHEN EXISTS (
3886
+ SELECT 1 FROM ${sheetTable(session)}
3887
+ WHERE _run_id = $1::text
3888
+ AND ${runtimeSheetResultFreshnessSql('_cell_meta', '$1::text')}
3889
+ AND _status = 'enriched'
3890
+ ) THEN 'enriched'
3891
+ WHEN EXISTS (
3892
+ SELECT 1 FROM ${sheetTable(session)}
3893
+ WHERE _run_id = $1::text
3894
+ AND ${runtimeSheetResultFreshnessSql('_cell_meta', '$1::text')}
3895
+ AND _status = 'failed'
3896
+ ) THEN 'failed'
3897
+ END AS preferred_status
3638
3898
  )
3639
3899
  SELECT *
3640
- FROM scoped
3641
- WHERE (__has_enriched AND _status = 'enriched')
3642
- OR (NOT __has_enriched AND __has_failed AND _status = 'failed')
3643
- OR (NOT __has_enriched AND NOT __has_failed)
3900
+ FROM ${sheetTable(session)}
3901
+ WHERE _run_id = $1::text
3902
+ AND ${runtimeSheetResultFreshnessSql('_cell_meta', '$1::text')}
3903
+ AND ((SELECT preferred_status FROM scoped) IS NULL
3904
+ OR _status = (SELECT preferred_status FROM scoped))
3644
3905
  ORDER BY _input_index ASC NULLS LAST, _created_at ASC, _key ASC
3645
3906
  LIMIT $2 OFFSET $3`,
3646
3907
  [input.runId, input.limit, input.offset],
@@ -4104,15 +4365,29 @@ async function prepareRuntimeSheetDatasetRows(
4104
4365
  );
4105
4366
  const { rows } = await client.query(
4106
4367
  `
4107
- WITH input_rows AS (
4108
- SELECT DISTINCT ON (key_values._key)
4109
- key_values._key, payload_values.payload, index_values._input_index
4110
- FROM unnest($1::text[]) WITH ORDINALITY AS key_values(_key, ord)
4111
- JOIN unnest($2::jsonb[]) WITH ORDINALITY AS payload_values(payload, ord)
4112
- ON payload_values.ord = key_values.ord
4113
- JOIN unnest($3::bigint[]) WITH ORDINALITY AS index_values(_input_index, ord)
4114
- ON index_values.ord = key_values.ord
4115
- ORDER BY key_values._key, key_values.ord
4368
+ WITH input_rows AS MATERIALIZED (
4369
+ SELECT DISTINCT ON (input_values._key)
4370
+ input_values._key, input_values._input_index, input_values.ord
4371
+ FROM unnest($1::text[], $3::bigint[])
4372
+ WITH ORDINALITY AS input_values(_key, _input_index, ord)
4373
+ WHERE input_values.ord <= LEAST(
4374
+ COALESCE(cardinality($1::text[]), 0),
4375
+ COALESCE(cardinality($2::jsonb[]), 0),
4376
+ COALESCE(cardinality($3::bigint[]), 0)
4377
+ )
4378
+ ORDER BY input_values._key, input_values.ord
4379
+ ),
4380
+ input_payloads AS MATERIALIZED (
4381
+ SELECT payload_values.payload, payload_values.ord
4382
+ FROM unnest($2::jsonb[]) WITH ORDINALITY AS payload_values(payload, ord)
4383
+ ),
4384
+ physical_input_rows AS NOT MATERIALIZED (
4385
+ -- Repeated admission/fence scans retain only narrow identities.
4386
+ -- Keep one unsorted payload spool and join its original ordinal only
4387
+ -- in physical write paths; do not materialize this wide join again.
4388
+ SELECT input_rows._key, input_rows._input_index, input_payloads.payload
4389
+ FROM input_rows
4390
+ JOIN input_payloads ON input_payloads.ord = input_rows.ord
4116
4391
  ),
4117
4392
  preexisting_rows AS (
4118
4393
  -- Force is new intent across runs, not across scheduler attempts of
@@ -4134,7 +4409,7 @@ async function prepareRuntimeSheetDatasetRows(
4134
4409
  _input_index = input_rows._input_index,
4135
4410
  _updated_at = now(),
4136
4411
  _version = ${nextRuntimeSheetVersionExpression(session)}${input.physicalRefreshSetSql}
4137
- FROM input_rows
4412
+ FROM physical_input_rows AS input_rows
4138
4413
  WHERE target._key = input_rows._key
4139
4414
  AND target._status = 'enriched'
4140
4415
  AND COALESCE(target._write_version, 0) < $12::bigint
@@ -4155,7 +4430,7 @@ async function prepareRuntimeSheetDatasetRows(
4155
4430
  _attempt_seq = $10::integer,
4156
4431
  _updated_at = now(),
4157
4432
  _version = ${nextRuntimeSheetVersionExpression(session)}${input.physicalRefreshSetSql}
4158
- FROM input_rows
4433
+ FROM physical_input_rows AS input_rows
4159
4434
  WHERE target._key = input_rows._key
4160
4435
  AND target._status = 'failed'
4161
4436
  AND COALESCE(target._write_version, 0) < $12::bigint
@@ -4166,7 +4441,7 @@ async function prepareRuntimeSheetDatasetRows(
4166
4441
  SET _input_index = input_rows._input_index,
4167
4442
  _updated_at = now(),
4168
4443
  _version = ${nextRuntimeSheetVersionExpression(session)}${input.physicalRefreshSetSql}
4169
- FROM input_rows
4444
+ FROM physical_input_rows AS input_rows
4170
4445
  WHERE target._key = input_rows._key
4171
4446
  AND target._write_version = $12::bigint
4172
4447
  AND target._status <> 'stale'
@@ -4200,7 +4475,7 @@ async function prepareRuntimeSheetDatasetRows(
4200
4475
  inserted_rows AS (
4201
4476
  INSERT INTO ${sheetTable(session)} (_key, _status, _run_id, _input_index, _attempt_id, _attempt_owner_run_id, _attempt_expires_at, _attempt_seq, _write_version, _writer_run_id, _created_at${input.physicalInsertColumnsSql})
4202
4477
  SELECT input_rows._key, 'pending', $4::text, input_rows._input_index, $7::text, $8::text, $9::timestamptz, $10::integer, $12::bigint, $4::text, now()${input.physicalInsertValuesSql}
4203
- FROM input_rows
4478
+ FROM physical_input_rows AS input_rows
4204
4479
  LEFT JOIN ${sheetTable(session)} AS existing_input ON existing_input._key = input_rows._key
4205
4480
  ${input.physicalInsertJoinSql}
4206
4481
  ON CONFLICT (_key) DO UPDATE SET
@@ -4251,7 +4526,7 @@ async function prepareRuntimeSheetDatasetRows(
4251
4526
  _error = NULL,
4252
4527
  _updated_at = now(),
4253
4528
  _version = ${nextRuntimeSheetVersionExpression(session)}${input.physicalRefreshSetSql}
4254
- FROM input_rows
4529
+ FROM physical_input_rows AS input_rows
4255
4530
  WHERE target._key = input_rows._key
4256
4531
  AND target._status IN ('pending', 'running')
4257
4532
  AND COALESCE(target._write_version, 0) < $12::bigint
@@ -4965,7 +5240,9 @@ async function getRuntimeWorkReceiptSession(
4965
5240
  playName: string;
4966
5241
  key: string;
4967
5242
  },
5243
+ access: 'read' | 'mutate' = 'mutate',
4968
5244
  ): Promise<RuntimePostgresSession> {
5245
+ if (access === 'mutate') assertWorkReceiptKeyMutable(input.key);
4969
5246
  const playName = context.playName?.trim() || input.playName;
4970
5247
  if (!playName) {
4971
5248
  throw new Error('Runtime work receipts require a playName.');
@@ -5033,15 +5310,23 @@ async function getRuntimeWorkReceiptSessionForKeys(
5033
5310
  playName: string;
5034
5311
  keys: string[];
5035
5312
  },
5313
+ access: 'read' | 'mutate' = 'mutate',
5036
5314
  ): Promise<RuntimePostgresSession> {
5315
+ if (access === 'mutate') {
5316
+ for (const key of input.keys) assertWorkReceiptKeyMutable(key);
5317
+ }
5037
5318
  const firstKey = input.keys.find((key) => key.trim());
5038
5319
  if (!firstKey) {
5039
5320
  throw new Error('Runtime work receipt batch requires at least one key.');
5040
5321
  }
5041
- const session = await getRuntimeWorkReceiptSession(context, {
5042
- playName: input.playName,
5043
- key: firstKey,
5044
- });
5322
+ const session = await getRuntimeWorkReceiptSession(
5323
+ context,
5324
+ {
5325
+ playName: input.playName,
5326
+ key: firstKey,
5327
+ },
5328
+ access,
5329
+ );
5045
5330
  for (const key of input.keys) {
5046
5331
  validateRuntimeWorkReceiptKeyScope(session, { key });
5047
5332
  }
@@ -5073,6 +5358,121 @@ async function readRuntimeWorkReceipt(
5073
5358
  return rows[0] ? mapRuntimeWorkReceiptRow(rows[0]) : null;
5074
5359
  }
5075
5360
 
5361
+ async function withRuntimeArrayInputReceiptPort<T>(
5362
+ context: RuntimeApiContext,
5363
+ input: RuntimeArrayInputScope,
5364
+ operation: (port: RuntimeArrayInputReceiptPort) => Promise<T>,
5365
+ ): Promise<T> {
5366
+ const orgId = context.orgId?.trim();
5367
+ if (!orgId || !input.playName?.trim() || !input.runId?.trim()) {
5368
+ throw new Error('Runtime array input requires exact org/play/run scope.');
5369
+ }
5370
+ if (
5371
+ (context.playName && context.playName !== input.playName) ||
5372
+ (context.runId && context.runId !== input.runId)
5373
+ ) {
5374
+ throw new Error('Runtime array input is outside the execution scope.');
5375
+ }
5376
+ const session = await getRuntimeWorkReceiptSessionForRun(context, {
5377
+ playName: input.playName,
5378
+ });
5379
+ if (session.target.orgId !== orgId || session.playName !== input.playName) {
5380
+ throw new Error('Runtime array input session is outside execution scope.');
5381
+ }
5382
+ return await operation({
5383
+ orgId,
5384
+ playName: input.playName,
5385
+ runId: input.runId,
5386
+ table: workReceiptTable(session),
5387
+ outerTransaction:
5388
+ Boolean(session.executionRole) &&
5389
+ !isRuntimeOneShotQueryFactoryRegistered(),
5390
+ withClient: (callback) =>
5391
+ withRuntimeWorkReceiptClient(context, session, callback),
5392
+ });
5393
+ }
5394
+
5395
+ export async function getRuntimeArrayInputManifest(
5396
+ context: RuntimeApiContext,
5397
+ input: RuntimeArrayInputScope,
5398
+ ): Promise<RuntimeArrayInputManifest> {
5399
+ if (context.dbSessionStrategy === 'gateway_only') {
5400
+ return await postRuntimeApi<RuntimeArrayInputManifest>(context, {
5401
+ action: 'runtime_array_input_get_manifest',
5402
+ input,
5403
+ });
5404
+ }
5405
+ return await withRuntimeArrayInputReceiptPort(context, input, (port) =>
5406
+ getRuntimeArrayInputManifestReceipt(port, input),
5407
+ );
5408
+ }
5409
+
5410
+ export async function beginRuntimeArrayInput(
5411
+ context: RuntimeApiContext,
5412
+ input: RuntimeArrayInputBegin,
5413
+ ): Promise<void> {
5414
+ if (context.dbSessionStrategy === 'gateway_only') {
5415
+ await postRuntimeApi(context, {
5416
+ action: 'runtime_array_input_begin',
5417
+ input,
5418
+ });
5419
+ return;
5420
+ }
5421
+ await withRuntimeArrayInputReceiptPort(context, input, (port) =>
5422
+ beginRuntimeArrayInputReceipt(port, input),
5423
+ );
5424
+ }
5425
+
5426
+ export async function putRuntimeArrayInputPage(
5427
+ context: RuntimeApiContext,
5428
+ input: RuntimeArrayInputPutPage,
5429
+ ): Promise<void> {
5430
+ if (context.dbSessionStrategy === 'gateway_only') {
5431
+ await postRuntimeApi(context, {
5432
+ action: 'runtime_array_input_put_page',
5433
+ input,
5434
+ });
5435
+ return;
5436
+ }
5437
+ await withRuntimeArrayInputReceiptPort(context, input, (port) =>
5438
+ putRuntimeArrayInputPageReceipt(port, input),
5439
+ );
5440
+ }
5441
+
5442
+ export async function sealRuntimeArrayInput(
5443
+ context: RuntimeApiContext,
5444
+ input: RuntimeArrayInputSeal,
5445
+ ): Promise<void> {
5446
+ if (context.dbSessionStrategy === 'gateway_only') {
5447
+ await postRuntimeApi(context, {
5448
+ action: 'runtime_array_input_seal',
5449
+ input,
5450
+ });
5451
+ return;
5452
+ }
5453
+ await withRuntimeArrayInputReceiptPort(context, input, (port) =>
5454
+ sealRuntimeArrayInputReceipt(port, input),
5455
+ );
5456
+ }
5457
+
5458
+ export async function readRuntimeArrayInputPage(
5459
+ context: RuntimeApiContext,
5460
+ input: RuntimeArrayInputScope & { ordinal: number },
5461
+ ): Promise<{ offset: number; rows: RuntimeArrayInputOccurrence[] }> {
5462
+ if (context.dbSessionStrategy === 'gateway_only') {
5463
+ return await postRuntimeApi<{
5464
+ offset: number;
5465
+ rows: RuntimeArrayInputOccurrence[];
5466
+ }>(context, {
5467
+ action: 'runtime_array_input_read_page',
5468
+ input,
5469
+ });
5470
+ }
5471
+ return await withRuntimeArrayInputReceiptPort(context, input, (port) =>
5472
+ readRuntimeArrayInputPageReceipt(port, input),
5473
+ );
5474
+ }
5475
+
5076
5476
  export async function getRuntimeWorkReceipt(
5077
5477
  context: RuntimeApiContext,
5078
5478
  input: {
@@ -5080,10 +5480,14 @@ export async function getRuntimeWorkReceipt(
5080
5480
  key: string;
5081
5481
  },
5082
5482
  ): Promise<WorkReceipt | null> {
5083
- const session = await getRuntimeWorkReceiptSession(context, {
5084
- playName: input.playName,
5085
- key: input.key,
5086
- });
5483
+ const session = await getRuntimeWorkReceiptSession(
5484
+ context,
5485
+ {
5486
+ playName: input.playName,
5487
+ key: input.key,
5488
+ },
5489
+ 'read',
5490
+ );
5087
5491
  return await withRuntimeWorkReceiptClient(context, session, async (client) =>
5088
5492
  readRuntimeWorkReceipt(client, session, input.key),
5089
5493
  );
@@ -5100,10 +5504,14 @@ export async function getRuntimeWorkReceipts(
5100
5504
  ...new Set(input.keys.map((key) => key.trim()).filter(Boolean)),
5101
5505
  ];
5102
5506
  if (keys.length === 0) return [];
5103
- const session = await getRuntimeWorkReceiptSessionForKeys(context, {
5104
- playName: input.playName,
5105
- keys,
5106
- });
5507
+ const session = await getRuntimeWorkReceiptSessionForKeys(
5508
+ context,
5509
+ {
5510
+ playName: input.playName,
5511
+ keys,
5512
+ },
5513
+ 'read',
5514
+ );
5107
5515
  return await withRuntimeWorkReceiptClient(
5108
5516
  context,
5109
5517
  session,
@@ -6206,6 +6614,8 @@ export async function completeRuntimeWorkReceipts(
6206
6614
  returnOutput?: boolean;
6207
6615
  },
6208
6616
  ): Promise<Array<WorkReceipt | null>> {
6617
+ for (const receipt of input.receipts)
6618
+ assertWorkReceiptKeyMutable(receipt.key);
6209
6619
  const receipts = input.receipts.filter((receipt) => receipt.key.trim());
6210
6620
  if (receipts.length === 0) return [];
6211
6621
  if (
@@ -6989,33 +7399,501 @@ export async function skipRuntimeWorkReceipt(
6989
7399
  );
6990
7400
  }
6991
7401
 
6992
- /**
6993
- * Prepare one dataset page and return its row dispositions and write version.
6994
- * With stableWriteIntent, the first admission for a run/table is durable in
6995
- * the same transaction as its rows; subsequent pages and wakes reuse it even
6996
- * after losing all runner-local state. A new run admits a newer intent.
6997
- * Gateway transport uses a named operation; an older receiver cannot silently
6998
- * ignore this opt-in or trigger unsafe legacy admission.
6999
- */
7000
- export async function startRuntimeSheetDataset(
7402
+ type RuntimeSheetDatasetStartInput = Parameters<
7403
+ typeof startRuntimeSheetDataset
7404
+ >[1];
7405
+
7406
+ /** Leave 50 KB of headroom below the 4.5 MB Vercel Function body ceiling. */
7407
+ const RUNTIME_SHEET_START_REQUEST_MAX_BYTES = 4_450_000;
7408
+
7409
+ function runtimeSheetStartIntentId(
7410
+ input: RuntimeSheetDatasetStartInput,
7411
+ ): string {
7412
+ const inputOffset = Math.max(0, Math.floor(input.inputOffset ?? 0));
7413
+ const digest = createHash('sha256')
7414
+ .update(input.playName)
7415
+ .update('\0')
7416
+ .update(input.runId)
7417
+ .update('\0')
7418
+ .update(input.tableNamespace)
7419
+ .update('\0')
7420
+ .update(String(inputOffset))
7421
+ .update('\0');
7422
+ for (const row of input.rows) {
7423
+ const serializedRow = JSON.stringify(row);
7424
+ digest.update(serializedRow ?? 'undefined').update('\0');
7425
+ }
7426
+ const value = digest.digest('hex').slice(0, 32);
7427
+ return `sheet-attempt:${value}`;
7428
+ }
7429
+
7430
+ function runtimeSheetStartRequestBytes(input: {
7431
+ request: RuntimeApiActionRequest;
7432
+ limitBytes: number;
7433
+ }): { bytes: number; exceeded: boolean } {
7434
+ const body = prepareRetainedPlayPipelinesForTransport(input.request);
7435
+ const measurement = jsonByteLengthUpTo(body, input.limitBytes);
7436
+ return { bytes: measurement.bytes, exceeded: measurement.exceeded };
7437
+ }
7438
+
7439
+ async function startRuntimeSheetDatasetViaGateway(
7001
7440
  context: RuntimeApiContext,
7002
- input: {
7003
- playName: string;
7004
- tableNamespace: string;
7005
- playInput?: Record<string, unknown> | null;
7006
- sheetContract: PlaySheetContract;
7007
- rows: Record<string, unknown>[];
7008
- runId: string;
7009
- attemptId?: string | null;
7010
- attemptOwnerRunId?: string | null;
7011
- attemptExpiresAt?: string | null;
7012
- attemptSeq?: number | null;
7013
- attemptLeaseTtlMs?: number | null;
7014
- writeVersion?: number | null;
7015
- /** Retain this run/table's first admission across pages, retries and wakes. Absence keeps legacy allocation. */
7016
- stableWriteIntent?: boolean;
7017
- /** Return completed durable cells separately from merged source rows. */
7018
- includePreviousCells?: boolean;
7441
+ input: RuntimeSheetDatasetStartInput,
7442
+ ): Promise<PrepareRuntimeSheetResult> {
7443
+ const request = (
7444
+ pageInput: RuntimeSheetDatasetStartInput,
7445
+ ): RuntimeApiActionRequest => ({
7446
+ action:
7447
+ pageInput.stableWriteIntent === true
7448
+ ? 'runtime_sheet_start_with_intent'
7449
+ : 'runtime_sheet_start',
7450
+ // Sheet admission consumes rows, not the full authored Play input. Exclude
7451
+ // that duplicate payload from both sizing and every actual page request.
7452
+ input: { ...pageInput, playInput: undefined, pendingRerunAdmission: true },
7453
+ });
7454
+ const completeRequest = request(input);
7455
+ if (
7456
+ !runtimeSheetStartRequestBytes({
7457
+ request: completeRequest,
7458
+ limitBytes: RUNTIME_SHEET_START_REQUEST_MAX_BYTES,
7459
+ }).exceeded
7460
+ ) {
7461
+ return await postRuntimeApi<PrepareRuntimeSheetResult>(
7462
+ context,
7463
+ completeRequest,
7464
+ );
7465
+ }
7466
+
7467
+ const requestInput: RuntimeSheetDatasetStartInput = {
7468
+ ...input,
7469
+ attemptId: input.attemptId?.trim() || runtimeSheetStartIntentId(input),
7470
+ pendingRerunAdmission: true,
7471
+ };
7472
+ // Keep every page under the app-function ingress ceiling while preserving
7473
+ // the stable write intent, rerun admission, and attempt fence.
7474
+ const inputOffset = Math.max(0, Math.floor(input.inputOffset ?? 0));
7475
+ const continuationInput: RuntimeSheetDatasetStartInput = {
7476
+ ...requestInput,
7477
+ inputOffset,
7478
+ rows: [],
7479
+ };
7480
+ const emptyRequestBytes = runtimeSheetStartRequestBytes({
7481
+ request: request(continuationInput),
7482
+ limitBytes: RUNTIME_SHEET_START_REQUEST_MAX_BYTES,
7483
+ });
7484
+ if (emptyRequestBytes.exceeded) {
7485
+ throw new Error(
7486
+ `Runtime sheet start metadata exceeds the ${RUNTIME_SHEET_START_REQUEST_MAX_BYTES}-byte app request budget before row data is included.`,
7487
+ );
7488
+ }
7489
+ // Leave space for fields returned by the first page and sent on continuation.
7490
+ const rowPageBudgetBytes =
7491
+ RUNTIME_SHEET_START_REQUEST_MAX_BYTES - emptyRequestBytes.bytes - 4 * 1024;
7492
+ if (rowPageBudgetBytes < 1) {
7493
+ throw new Error(
7494
+ 'Runtime sheet start metadata leaves no room for a row in the app request budget.',
7495
+ );
7496
+ }
7497
+
7498
+ const pageResults: PrepareRuntimeSheetResult[] = [];
7499
+ let pageInput = continuationInput;
7500
+ const startedAt = Date.now();
7501
+ for await (const page of iteratePlayDatasetInputPages(input.rows, {
7502
+ maxPageBytes: rowPageBudgetBytes,
7503
+ estimateRowBytes: (row) => {
7504
+ const measured = jsonByteLengthUpTo(
7505
+ row,
7506
+ RUNTIME_SHEET_START_REQUEST_MAX_BYTES,
7507
+ );
7508
+ return measured.bytes + 1;
7509
+ },
7510
+ })) {
7511
+ const nextInput: RuntimeSheetDatasetStartInput = {
7512
+ ...pageInput,
7513
+ rows: page.rows,
7514
+ inputOffset: inputOffset + page.offset,
7515
+ };
7516
+ const pageRequest = request(nextInput);
7517
+ const pageBytes = runtimeSheetStartRequestBytes({
7518
+ request: pageRequest,
7519
+ limitBytes: RUNTIME_SHEET_START_REQUEST_MAX_BYTES,
7520
+ });
7521
+ if (pageBytes.exceeded) {
7522
+ throw new Error(
7523
+ `A runtime sheet start page exceeds the ${RUNTIME_SHEET_START_REQUEST_MAX_BYTES}-byte app request budget.`,
7524
+ );
7525
+ }
7526
+ const result = await postRuntimeApi<PrepareRuntimeSheetResult>(
7527
+ context,
7528
+ pageRequest,
7529
+ );
7530
+ pageResults.push(result);
7531
+ pageInput = {
7532
+ ...nextInput,
7533
+ attemptId: result.attemptId ?? nextInput.attemptId,
7534
+ attemptOwnerRunId:
7535
+ result.attemptOwnerRunId ?? nextInput.attemptOwnerRunId,
7536
+ attemptExpiresAt: result.attemptExpiresAt ?? nextInput.attemptExpiresAt,
7537
+ attemptSeq: result.attemptSeq ?? nextInput.attemptSeq,
7538
+ writeVersion: result.writeVersion ?? nextInput.writeVersion,
7539
+ };
7540
+ }
7541
+
7542
+ if (pageResults.length === 0) {
7543
+ return await postRuntimeApi<PrepareRuntimeSheetResult>(
7544
+ context,
7545
+ request(continuationInput),
7546
+ );
7547
+ }
7548
+ if (pageResults.length === 1) return pageResults[0]!;
7549
+
7550
+ const latest = pageResults.at(-1)!;
7551
+ const pendingRows = pageResults.flatMap((result) => result.pendingRows);
7552
+ const completedRows = pageResults.flatMap((result) => result.completedRows);
7553
+ const blockedRows = pageResults.flatMap((result) => result.blockedRows);
7554
+ const inserted = pageResults.reduce(
7555
+ (total, result) => total + result.inserted,
7556
+ 0,
7557
+ );
7558
+ const skipped = pageResults.reduce(
7559
+ (total, result) => total + result.skipped,
7560
+ 0,
7561
+ );
7562
+ const lastDefined = <T>(
7563
+ select: (result: PrepareRuntimeSheetResult) => T | null | undefined,
7564
+ ) => {
7565
+ for (let index = pageResults.length - 1; index >= 0; index -= 1) {
7566
+ const value = select(pageResults[index]!);
7567
+ if (value != null) return value;
7568
+ }
7569
+ return undefined;
7570
+ };
7571
+ return {
7572
+ ...(input.includePreviousCells
7573
+ ? {
7574
+ previousCells: pageResults.flatMap(
7575
+ (result) => result.previousCells ?? [],
7576
+ ),
7577
+ }
7578
+ : {}),
7579
+ inserted,
7580
+ skipped,
7581
+ pendingRows,
7582
+ completedRows,
7583
+ blockedRows,
7584
+ tableNamespace: latest.tableNamespace,
7585
+ attemptId:
7586
+ lastDefined((result) => result.attemptId) ??
7587
+ requestInput.attemptId ??
7588
+ undefined,
7589
+ attemptOwnerRunId:
7590
+ lastDefined((result) => result.attemptOwnerRunId) ??
7591
+ requestInput.attemptOwnerRunId ??
7592
+ undefined,
7593
+ attemptExpiresAt:
7594
+ lastDefined((result) => result.attemptExpiresAt) ??
7595
+ requestInput.attemptExpiresAt ??
7596
+ undefined,
7597
+ attemptSeq:
7598
+ lastDefined((result) => result.attemptSeq) ??
7599
+ requestInput.attemptSeq ??
7600
+ undefined,
7601
+ writeVersion:
7602
+ lastDefined((result) => result.writeVersion) ??
7603
+ requestInput.writeVersion ??
7604
+ undefined,
7605
+ timings: [
7606
+ {
7607
+ phase: 'gateway_page_batch',
7608
+ ms: Date.now() - startedAt,
7609
+ rows: input.rows.length,
7610
+ chunks: pageResults.length,
7611
+ inserted,
7612
+ skipped,
7613
+ pending: pendingRows.length,
7614
+ completed: completedRows.length,
7615
+ },
7616
+ ],
7617
+ };
7618
+ }
7619
+
7620
+ /**
7621
+ * Prepare one dataset page and return its row dispositions and write version.
7622
+ * With stableWriteIntent, the first admission for a run/table is durable in
7623
+ * the same transaction as its rows; subsequent pages and wakes reuse it even
7624
+ * after losing all runner-local state. A new run admits a newer intent.
7625
+ * Gateway transport uses a named operation; an older receiver cannot silently
7626
+ * ignore this opt-in or trigger unsafe legacy admission.
7627
+ */
7628
+ function runtimeSheetInputRowIdentity(
7629
+ row: Record<string, unknown>,
7630
+ tableNamespace: string,
7631
+ ): { key: string; cleanedRow: Record<string, unknown> } {
7632
+ // Use the same identity before transport paging and inside the receiver.
7633
+ const cleanedRow = toSerializableCsvAliasedRow(row);
7634
+ return {
7635
+ key:
7636
+ resolveMapRowOutcomeKey(row) ??
7637
+ derivePlayRowIdentity(cleanedRow, tableNamespace),
7638
+ cleanedRow,
7639
+ };
7640
+ }
7641
+
7642
+ type RuntimeSheetStartInput = Parameters<typeof startRuntimeSheetDataset>[1];
7643
+ type RuntimeSheetStartMetadata = Omit<
7644
+ RuntimeSheetStartInput,
7645
+ 'rows' | 'playInput'
7646
+ >;
7647
+ type RuntimeSheetStartAction =
7648
+ | 'runtime_sheet_start'
7649
+ | 'runtime_sheet_start_with_intent';
7650
+ type RuntimeSheetStartTransportPage = {
7651
+ rowsJson: string[];
7652
+ inputOffset?: number;
7653
+ };
7654
+
7655
+ function serializeRuntimeSheetStartRequest(
7656
+ action: RuntimeSheetStartAction,
7657
+ metadata: RuntimeSheetStartMetadata,
7658
+ rowsJson: readonly string[],
7659
+ ): string {
7660
+ const prepared = prepareRetainedPlayPipelinesForTransport({
7661
+ action,
7662
+ input: metadata,
7663
+ });
7664
+ const inputJson = JSON.stringify(prepared.input);
7665
+ return `{"action":${JSON.stringify(prepared.action)},"input":${inputJson.slice(0, -1)}${inputJson.length > 2 ? ',' : ''}"rows":[${rowsJson.join(',')}]}}`;
7666
+ }
7667
+
7668
+ /**
7669
+ * Local admission pages, not a new wire response. Consumers must retain each
7670
+ * page's history/row-memory checks rather than merge a source-sized response.
7671
+ * The runner supplies its existing persistence retry around each fixed page.
7672
+ */
7673
+ export async function* startRuntimeSheetDatasetPages(
7674
+ context: RuntimeApiContext,
7675
+ input: RuntimeSheetStartInput,
7676
+ options?: {
7677
+ runPage?: (
7678
+ operation: () => Promise<PrepareRuntimeSheetResult>,
7679
+ ) => Promise<PrepareRuntimeSheetResult>;
7680
+ },
7681
+ ): AsyncGenerator<PrepareRuntimeSheetResult> {
7682
+ const runPage =
7683
+ options?.runPage ??
7684
+ ((operation: () => Promise<PrepareRuntimeSheetResult>) => operation());
7685
+ if (context.dbSessionStrategy !== 'gateway_only') {
7686
+ yield await runPage(() => startRuntimeSheetDataset(context, input));
7687
+ return;
7688
+ }
7689
+ const action: RuntimeSheetStartAction =
7690
+ input.stableWriteIntent === true
7691
+ ? 'runtime_sheet_start_with_intent'
7692
+ : 'runtime_sheet_start';
7693
+ // playInput is not read by sheet admission. Repeating the entire authored
7694
+ // input beside every bounded row page would defeat the transport boundary.
7695
+ const frozenMetadata = JSON.parse(
7696
+ JSON.stringify({
7697
+ ...input,
7698
+ rows: undefined,
7699
+ playInput: undefined,
7700
+ pendingRerunAdmission: true,
7701
+ }),
7702
+ ) as RuntimeSheetStartMetadata;
7703
+ const tableNamespace = frozenMetadata.tableNamespace;
7704
+ const includePreviousCells = frozenMetadata.includePreviousCells === true;
7705
+ const previousCellsBudgetBytes = frozenMetadata.previousCellsBudgetBytes;
7706
+ const attemptOwnerRunId =
7707
+ normalizeRuntimeSheetAttemptOwnerRunId(frozenMetadata);
7708
+ const attemptSeq = normalizeRuntimeRunAttempt(frozenMetadata.attemptSeq);
7709
+ // Array JSON encodes holes/undefined as null; preserve that wire behavior on
7710
+ // the unchanged single-page path as well as ordinary JSON value snapshots.
7711
+ const rowsJson = Array.from(
7712
+ input.rows,
7713
+ (row) => JSON.stringify(row) ?? 'null',
7714
+ );
7715
+ const rowBytes = rowsJson.reduce(
7716
+ (total, row) => total + Buffer.byteLength(row, 'utf8'),
7717
+ Math.max(0, rowsJson.length - 1),
7718
+ );
7719
+ const fullBytes =
7720
+ Buffer.byteLength(
7721
+ serializeRuntimeSheetStartRequest(action, frozenMetadata, []),
7722
+ 'utf8',
7723
+ ) + rowBytes;
7724
+ const pages: RuntimeSheetStartTransportPage[] = [];
7725
+ let duplicateRows = 0;
7726
+ if (fullBytes <= RUNNER_TERMINAL_PUSH_MAX_BODY_BYTES) {
7727
+ // Keep every previously transportable admission as one transaction, with
7728
+ // its existing receiver-side deduplication and optional response fields.
7729
+ pages.push({ rowsJson });
7730
+ } else {
7731
+ frozenMetadata.attemptId = normalizeRuntimeSheetAttemptId(
7732
+ frozenMetadata.attemptId,
7733
+ );
7734
+ const inputOffset = Math.max(
7735
+ 0,
7736
+ Math.floor(frozenMetadata.inputOffset ?? 0),
7737
+ );
7738
+ if (!Number.isSafeInteger(inputOffset + rowsJson.length)) {
7739
+ throw new Error('Runtime sheet start requires safe global row indexes.');
7740
+ }
7741
+ // Reserve the longest valid version/index before ANY page is admitted.
7742
+ // A late oversized row or contract must not leave earlier pages pending.
7743
+ const framingBytes = Buffer.byteLength(
7744
+ serializeRuntimeSheetStartRequest(
7745
+ action,
7746
+ {
7747
+ ...frozenMetadata,
7748
+ writeVersion: Number.MAX_SAFE_INTEGER,
7749
+ inputOffset: Number.MAX_SAFE_INTEGER,
7750
+ },
7751
+ [],
7752
+ ),
7753
+ 'utf8',
7754
+ );
7755
+ const seen = new Set<string>();
7756
+ let page: RuntimeSheetStartTransportPage = { rowsJson: [] };
7757
+ let pageBytes = framingBytes;
7758
+ for (let index = 0; index < rowsJson.length; index += 1) {
7759
+ const row: unknown = JSON.parse(rowsJson[index]!);
7760
+ rowsJson[index] = '';
7761
+ if (!row || typeof row !== 'object' || Array.isArray(row)) {
7762
+ throw new Error(
7763
+ `Runtime sheet start row ${index} must encode an object.`,
7764
+ );
7765
+ }
7766
+ const sourceRow = row as Record<string, unknown>;
7767
+ const { key } = runtimeSheetInputRowIdentity(sourceRow, tableNamespace);
7768
+ if (seen.has(key)) {
7769
+ duplicateRows += 1;
7770
+ continue;
7771
+ }
7772
+ seen.add(key);
7773
+ const serialized = JSON.stringify({
7774
+ ...sourceRow,
7775
+ ...mapRowOutcomeRuntimeFields({
7776
+ key,
7777
+ inputIndex:
7778
+ normalizeRuntimeMapInputIndex(
7779
+ sourceRow[MAP_ROW_OUTCOME_RUNTIME_FIELDS.inputIndex],
7780
+ ) ?? inputOffset + index,
7781
+ }),
7782
+ });
7783
+ const bytes = Buffer.byteLength(serialized, 'utf8');
7784
+ if (framingBytes + bytes > RUNNER_TERMINAL_PUSH_MAX_BODY_BYTES) {
7785
+ throw new Error(
7786
+ `Runtime sheet start row ${index} exceeds the ${RUNNER_TERMINAL_PUSH_MAX_BODY_BYTES} byte request-body limit.`,
7787
+ );
7788
+ }
7789
+ const separatorBytes = page.rowsJson.length > 0 ? 1 : 0;
7790
+ if (
7791
+ page.rowsJson.length > 0 &&
7792
+ pageBytes + separatorBytes + bytes >
7793
+ RUNTIME_SHEET_START_TARGET_BODY_BYTES
7794
+ ) {
7795
+ pages.push(page);
7796
+ page = { rowsJson: [] };
7797
+ pageBytes = framingBytes;
7798
+ }
7799
+ if (page.rowsJson.length === 0) page.inputOffset = inputOffset + index;
7800
+ pageBytes += bytes + (page.rowsJson.length > 0 ? 1 : 0);
7801
+ page.rowsJson.push(serialized);
7802
+ }
7803
+ if (page.rowsJson.length > 0) pages.push(page);
7804
+ if (pages.length === 0) {
7805
+ throw new Error(
7806
+ 'Runtime sheet start metadata exceeds the request-body limit.',
7807
+ );
7808
+ }
7809
+ }
7810
+
7811
+ let writeVersion = frozenMetadata.writeVersion;
7812
+ let membershipCapture: boolean | undefined;
7813
+ for (const [index, page] of pages.entries()) {
7814
+ const pageMetadata = {
7815
+ ...frozenMetadata,
7816
+ ...(page.inputOffset !== undefined
7817
+ ? { inputOffset: page.inputOffset }
7818
+ : {}),
7819
+ ...(writeVersion != null ? { writeVersion } : {}),
7820
+ };
7821
+ const preparedBodyJson = serializeRuntimeSheetStartRequest(
7822
+ action,
7823
+ pageMetadata,
7824
+ page.rowsJson,
7825
+ );
7826
+ if (
7827
+ Buffer.byteLength(preparedBodyJson, 'utf8') >
7828
+ RUNNER_TERMINAL_PUSH_MAX_BODY_BYTES
7829
+ ) {
7830
+ throw new Error(
7831
+ 'Runtime sheet start page exceeded its preflight body budget.',
7832
+ );
7833
+ }
7834
+ const request = JSON.parse(preparedBodyJson) as Extract<
7835
+ RuntimeApiActionRequest,
7836
+ { action: RuntimeSheetStartAction }
7837
+ >;
7838
+ const result = await runPage(() =>
7839
+ postRuntimeApi<PrepareRuntimeSheetResult>(
7840
+ context,
7841
+ request,
7842
+ preparedBodyJson,
7843
+ ),
7844
+ );
7845
+ page.rowsJson.length = 0;
7846
+ if (includePreviousCells) {
7847
+ assertDatasetAdmissionHistory(result, previousCellsBudgetBytes);
7848
+ }
7849
+ if (pages.length > 1) {
7850
+ const admittedVersion = result.writeVersion;
7851
+ if (
7852
+ typeof admittedVersion !== 'number' ||
7853
+ !Number.isSafeInteger(admittedVersion) ||
7854
+ admittedVersion <= 0 ||
7855
+ (writeVersion != null && result.writeVersion !== writeVersion) ||
7856
+ normalizeTableNamespace(result.tableNamespace) !==
7857
+ normalizeTableNamespace(tableNamespace) ||
7858
+ (result.attemptId !== undefined &&
7859
+ result.attemptId !== frozenMetadata.attemptId) ||
7860
+ (result.attemptOwnerRunId !== undefined &&
7861
+ result.attemptOwnerRunId !== attemptOwnerRunId) ||
7862
+ (result.attemptSeq !== undefined && result.attemptSeq !== attemptSeq) ||
7863
+ (index > 0 && result.membershipCapture !== membershipCapture)
7864
+ ) {
7865
+ throw new Error(
7866
+ 'Runtime sheet admission pages disagree on their write intent or identity.',
7867
+ );
7868
+ }
7869
+ writeVersion = result.writeVersion;
7870
+ }
7871
+ membershipCapture = result.membershipCapture;
7872
+ yield index === 0 && duplicateRows > 0
7873
+ ? { ...result, skipped: result.skipped + duplicateRows }
7874
+ : result;
7875
+ }
7876
+ }
7877
+
7878
+ export async function startRuntimeSheetDataset(
7879
+ context: RuntimeApiContext,
7880
+ input: {
7881
+ playName: string;
7882
+ tableNamespace: string;
7883
+ playInput?: Record<string, unknown> | null;
7884
+ sheetContract: PlaySheetContract;
7885
+ rows: Record<string, unknown>[];
7886
+ runId: string;
7887
+ attemptId?: string | null;
7888
+ attemptOwnerRunId?: string | null;
7889
+ attemptExpiresAt?: string | null;
7890
+ attemptSeq?: number | null;
7891
+ attemptLeaseTtlMs?: number | null;
7892
+ writeVersion?: number | null;
7893
+ /** Retain this run/table's first admission across pages, retries and wakes. Absence keeps legacy allocation. */
7894
+ stableWriteIntent?: boolean;
7895
+ /** Return completed durable cells separately from merged source rows. */
7896
+ includePreviousCells?: boolean;
7019
7897
  previousCellsBudgetBytes?: number;
7020
7898
  inputOffset?: number;
7021
7899
  force?: boolean;
@@ -7033,15 +7911,7 @@ export async function startRuntimeSheetDataset(
7033
7911
  };
7034
7912
  if (context.dbSessionStrategy === 'gateway_only') {
7035
7913
  return checkedResult(
7036
- await postRuntimeApi<PrepareRuntimeSheetResult>(context, {
7037
- // A named operation prevents an old gateway from silently ignoring the
7038
- // intent opt-in and re-admitting this run under a new version.
7039
- action:
7040
- input.stableWriteIntent === true
7041
- ? 'runtime_sheet_start_with_intent'
7042
- : 'runtime_sheet_start',
7043
- input: { ...input, pendingRerunAdmission: true },
7044
- }),
7914
+ await startRuntimeSheetDatasetViaGateway(context, input),
7045
7915
  );
7046
7916
  }
7047
7917
  const totalStartedAt = Date.now();
@@ -7103,10 +7973,10 @@ export async function startRuntimeSheetDataset(
7103
7973
  // Materializes projected CSV aliases as visible cells and drops internal
7104
7974
  // __deepline* keys — a plain spread would silently lose the
7105
7975
  // non-enumerable alias fields on the JSON payload boundary.
7106
- const cleanedRow = toSerializableCsvAliasedRow(row);
7107
- const key =
7108
- resolveMapRowOutcomeKey(row) ??
7109
- derivePlayRowIdentity(cleanedRow, input.tableNamespace);
7976
+ const { cleanedRow, key } = runtimeSheetInputRowIdentity(
7977
+ row,
7978
+ input.tableNamespace,
7979
+ );
7110
7980
  const source =
7111
7981
  row.__deeplineSourceRow && typeof row.__deeplineSourceRow === 'object'
7112
7982
  ? (sanitizePostgresJsonValue(row.__deeplineSourceRow) as Record<
@@ -7210,9 +8080,10 @@ export async function startRuntimeSheetDataset(
7210
8080
  tableNamespace: input.tableNamespace,
7211
8081
  sheetContract: input.sheetContract,
7212
8082
  transactional: needsTransaction,
8083
+ concurrentColumnPreparation: true,
7213
8084
  timings,
7214
8085
  },
7215
- async (client) => {
8086
+ async (client, columnLockMode) => {
7216
8087
  const attemptExpiryStartedAt = Date.now();
7217
8088
  const attemptExpiresAt = await mintRuntimeSheetAttemptExpiresAt(
7218
8089
  client,
@@ -7224,6 +8095,7 @@ export async function startRuntimeSheetDataset(
7224
8095
  input,
7225
8096
  );
7226
8097
  const columnTypes = await RuntimeSheetColumns.prepareWrite(client, {
8098
+ lockMode: columnLockMode,
7227
8099
  schema: session.postgres.schema,
7228
8100
  table: session.postgres.sheetTable,
7229
8101
  columns: physicalSheetColumnProjections(input.sheetContract).map(
@@ -7781,6 +8653,7 @@ function physicalRuntimeSheetUpdateSql(
7781
8653
  }
7782
8654
 
7783
8655
  type CompleteRuntimeMapRowChunksInput = {
8656
+ columnLockMode: 'shared' | 'exclusive';
7784
8657
  chunks: RuntimePreparedCompletedRow[][];
7785
8658
  physicalColumnProjections: PhysicalSheetColumnProjection[];
7786
8659
  runId: string;
@@ -7951,45 +8824,57 @@ function writableEnrichedRuntimeSheetAttemptSql(
7951
8824
  async function completeRuntimeMapRowChunks(
7952
8825
  client: RuntimeQueryClient,
7953
8826
  session: RuntimePostgresSession,
7954
- input: CompleteRuntimeMapRowChunksInput,
7955
- ): Promise<RuntimeMapRowsMutationResult> {
8827
+ input: CompleteRuntimeMapRowChunksInput & {
8828
+ exactKeyChunks: RuntimePreparedCompletedRow[][][];
8829
+ },
8830
+ ): Promise<RuntimeMapRowsMutationResult & { allExactRowsUpdated: boolean }> {
7956
8831
  let updated = 0;
8832
+ // Exact submitted keys are deduplicated before chunking. A physical update
8833
+ // locks each existing key until this transaction commits; only that proof
8834
+ // permits skipping missing-row discovery. Repair counts are not key coverage.
8835
+ let allExactRowsUpdated = true;
7957
8836
  const fencedRows: RuntimeMapRowsStaleRow[] = [];
7958
- for (const chunk of input.chunks) {
7959
- const chunkKeys = chunk.map((row) => row.key);
7960
- const chunkInputIndexes = chunk.map((row) => row.input_index);
7961
- const chunkDataPatches = chunk.map((row) => row.data_patch_json);
7962
- const chunkCellMetaPatches = chunk.map((row) => row.cell_meta_patch_json);
7963
- const targetChangedPatchedCellSql = changedPatchedCellSql(
7964
- 'target',
7965
- 'updates.data_patch',
7966
- input.physicalColumnProjections,
7967
- );
7968
- const writableEnrichedAttemptSql = writableEnrichedRuntimeSheetAttemptSql(
7969
- 'target',
7970
- '$10::text',
7971
- '$11::timestamptz',
7972
- '$12',
7973
- );
7974
- const writableNonTerminalAttemptSql = `(
8837
+ // Preserve the original row-count units: every exact write precedes all
8838
+ // input-index repairs within that unit, regardless of payload subdivision.
8839
+ for (const exactKeyChunks of input.exactKeyChunks) {
8840
+ const repairChunk: RuntimePreparedCompletedRow[] = [];
8841
+ for (const chunk of exactKeyChunks) {
8842
+ const chunkKeys = chunk.map((row) => row.key);
8843
+ const chunkInputIndexes = chunk.map((row) => row.input_index);
8844
+ const chunkDataPatches = chunk.map((row) => row.data_patch_json);
8845
+ const chunkCellMetaPatches = chunk.map((row) => row.cell_meta_patch_json);
8846
+ const targetChangedPatchedCellSql = changedPatchedCellSql(
8847
+ 'target',
8848
+ 'updates.data_patch',
8849
+ input.physicalColumnProjections,
8850
+ );
8851
+ const writableEnrichedAttemptSql = writableEnrichedRuntimeSheetAttemptSql(
8852
+ 'target',
8853
+ '$10::text',
8854
+ '$11::timestamptz',
8855
+ '$12',
8856
+ );
8857
+ const writableNonTerminalAttemptSql = `(
7975
8858
  target._status <> 'enriched'
7976
8859
  AND (
7977
8860
  ${activeRuntimeSheetAttemptFenceSql('target', '$9::text', '$10::text', '$11::timestamptz', '$12::integer')}
7978
8861
  OR coalesce(target._attempt_owner_run_id, target._run_id) IS DISTINCT FROM $10::text
7979
8862
  )
7980
8863
  )`;
7981
- const matchingUpdatesSql = `WITH mutation_context AS (SELECT $6::text AS play_name, $7::text AS table_namespace, $11::timestamptz AS attempt_expires_at, $12::integer AS attempt_seq), updates AS (
7982
- SELECT key_values._key,
7983
- input_index_values.input_index,
7984
- data_values.data_patch,
7985
- cell_meta_values.cell_meta_patch
7986
- FROM unnest($1::text[]) WITH ORDINALITY AS key_values(_key, ord)
7987
- JOIN unnest($2::bigint[]) WITH ORDINALITY AS input_index_values(input_index, ord)
7988
- ON input_index_values.ord = key_values.ord
7989
- JOIN unnest($3::jsonb[]) WITH ORDINALITY AS data_values(data_patch, ord)
7990
- ON data_values.ord = key_values.ord
7991
- JOIN unnest($4::jsonb[]) WITH ORDINALITY AS cell_meta_values(cell_meta_patch, ord)
7992
- ON cell_meta_values.ord = key_values.ord
8864
+ const matchingUpdatesSql = `WITH mutation_context AS (SELECT $6::text AS play_name, $7::text AS table_namespace, $11::timestamptz AS attempt_expires_at, $12::integer AS attempt_seq), updates AS (
8865
+ SELECT input_values._key,
8866
+ input_values.input_index,
8867
+ input_values.ord AS submitted_ordinal,
8868
+ input_values.data_patch,
8869
+ input_values.cell_meta_patch
8870
+ FROM unnest($1::text[], $2::bigint[], $3::jsonb[], $4::jsonb[])
8871
+ WITH ORDINALITY AS input_values(_key, input_index, data_patch, cell_meta_patch, ord)
8872
+ WHERE input_values.ord <= LEAST(
8873
+ COALESCE(cardinality($1::text[]), 0),
8874
+ COALESCE(cardinality($2::bigint[]), 0),
8875
+ COALESCE(cardinality($3::jsonb[]), 0),
8876
+ COALESCE(cardinality($4::jsonb[]), 0)
8877
+ )
7993
8878
  ),
7994
8879
  matched_updates AS (
7995
8880
  SELECT target._key AS matched_key,
@@ -7997,18 +8882,21 @@ async function completeRuntimeMapRowChunks(
7997
8882
  target._status AS prev_status,
7998
8883
  target._cell_meta AS prev_cell_meta,
7999
8884
  updates.input_index,
8885
+ updates.submitted_ordinal,
8000
8886
  updates.data_patch,
8001
8887
  updates.cell_meta_patch
8002
8888
  FROM updates
8003
8889
  JOIN ${sheetTable(session)} AS target
8004
8890
  ON target._key = updates._key
8005
8891
  )`;
8006
- const writableRowsSql = `target._key = updates.matched_key
8892
+ // Membership must not promote a run-wide partial index over exact keys
8893
+ // when statistics underestimate a fresh run. Keep negated fencing intact.
8894
+ const writableRowsSql = `target._key = updates.matched_key
8007
8895
  AND ($13::bigint IS NULL OR target._write_version = $13::bigint)
8008
8896
  AND ($13::bigint IS NOT NULL OR target._status <> 'enriched')
8009
8897
  AND NOT (${newerTerminalRuntimeSheetRowSql('target', '$11::timestamptz', '$12::integer', '$9::text', '$10::text')})
8010
8898
  AND (
8011
- ($9::text IS NULL AND target._run_id = $5::text)
8899
+ ($9::text IS NULL AND coalesce(target._run_id = $5::text, false))
8012
8900
  OR ($9::text IS NOT NULL AND ${writableNonTerminalAttemptSql})
8013
8901
  OR ($9::text IS NOT NULL AND ${activeRuntimeSheetAttemptFenceSql('target', '$9::text', '$10::text', '$11::timestamptz', '$12::integer')})
8014
8902
  OR ($9::text IS NOT NULL AND ${writableEnrichedAttemptSql})
@@ -8045,46 +8933,76 @@ async function completeRuntimeMapRowChunks(
8045
8933
  WHERE coalesce(target._cell_meta -> field_values.field ->> 'status', '') <> 'completed'
8046
8934
  )
8047
8935
  )`;
8048
- const mutationParams = [
8049
- chunkKeys,
8050
- chunkInputIndexes,
8051
- chunkDataPatches,
8052
- chunkCellMetaPatches,
8053
- input.runId,
8054
- input.normalizedPlayName,
8055
- input.normalizedTableNamespace,
8056
- [...new Set(input.outputFields)],
8057
- input.attemptId,
8058
- input.attemptOwnerRunId,
8059
- input.attemptExpiresAt,
8060
- input.attemptSeq,
8061
- input.writeVersion,
8062
- ];
8063
- const columnTypes = await RuntimeSheetColumns.prepareWrite(client, {
8064
- schema: session.postgres.schema,
8065
- table: session.postgres.sheetTable,
8066
- columns: input.physicalColumnProjections,
8067
- rows: async () => {
8068
- const accepted = await client.query<{
8069
- data_patch: Record<string, unknown>;
8070
- }>(
8071
- `${matchingUpdatesSql}
8072
- SELECT updates.data_patch
8936
+ const mutationParams = [
8937
+ chunkKeys,
8938
+ chunkInputIndexes,
8939
+ chunkDataPatches,
8940
+ chunkCellMetaPatches,
8941
+ input.runId,
8942
+ input.normalizedPlayName,
8943
+ input.normalizedTableNamespace,
8944
+ [...new Set(input.outputFields)],
8945
+ input.attemptId,
8946
+ input.attemptOwnerRunId,
8947
+ input.attemptExpiresAt,
8948
+ input.attemptSeq,
8949
+ input.writeVersion,
8950
+ ];
8951
+ const columnTypes = await RuntimeSheetColumns.prepareWrite(client, {
8952
+ lockMode: input.columnLockMode,
8953
+ schema: session.postgres.schema,
8954
+ table: session.postgres.sheetTable,
8955
+ columns: input.physicalColumnProjections,
8956
+ rows: async () => {
8957
+ const accepted = await client.query<{
8958
+ submitted_ordinal: number | string;
8959
+ }>(
8960
+ `${matchingUpdatesSql}
8961
+ SELECT updates.submitted_ordinal
8073
8962
  FROM matched_updates AS updates
8074
8963
  JOIN ${sheetTable(session)} AS target ON target._key = updates.matched_key
8075
8964
  WHERE ${writableRowsSql}
8076
8965
  FOR UPDATE OF target`,
8077
- mutationParams,
8078
- );
8079
- return accepted.rows.map((row) => row.data_patch);
8080
- },
8081
- });
8082
- const { rows } = await client.query<{
8083
- updated: number;
8084
- matched_keys: string[];
8085
- fenced_rows: unknown;
8086
- }>(
8087
- `${matchingUpdatesSql},
8966
+ mutationParams,
8967
+ );
8968
+ // Ordinality identifies the original parameter even if UTF8 text
8969
+ // transport replaces a lone surrogate or makes two keys collide.
8970
+ // Decode only the already-sanitized JSON parameter; never re-run
8971
+ // authored toJSON or deduplicate accepted physical-key collisions.
8972
+ const seen = new Set<number>();
8973
+ return accepted.rows.map((row) => {
8974
+ const raw = row.submitted_ordinal;
8975
+ const ordinal =
8976
+ typeof raw === 'number'
8977
+ ? raw
8978
+ : typeof raw === 'string'
8979
+ ? Number(raw)
8980
+ : NaN;
8981
+ if (
8982
+ !Number.isSafeInteger(ordinal) ||
8983
+ ordinal < 1 ||
8984
+ ordinal > chunk.length ||
8985
+ (typeof raw === 'string' && String(ordinal) !== raw) ||
8986
+ seen.has(ordinal)
8987
+ ) {
8988
+ throw new Error(
8989
+ 'Runtime Sheet accepted-ordinal evidence is invalid.',
8990
+ );
8991
+ }
8992
+ seen.add(ordinal);
8993
+ return JSON.parse(chunk[ordinal - 1]!.data_patch_json) as Record<
8994
+ string,
8995
+ unknown
8996
+ >;
8997
+ });
8998
+ },
8999
+ });
9000
+ const { rows } = await client.query<{
9001
+ updated: number;
9002
+ matched_keys: string[];
9003
+ fenced_rows: unknown;
9004
+ }>(
9005
+ `${matchingUpdatesSql},
8088
9006
  applied_rows AS (
8089
9007
  UPDATE ${sheetTable(session)} AS target
8090
9008
  SET _status = 'enriched',
@@ -8224,23 +9142,31 @@ async function completeRuntimeMapRowChunks(
8224
9142
  coalesce((SELECT jsonb_agg(jsonb_build_object('key', _key, 'reason', reason, 'rowVersionStamp', row_write_version)) FROM fenced_rows), '[]'::jsonb) AS fenced_rows,
8225
9143
  (SELECT count(*)::int FROM summary_delta) AS summary_delta_count,
8226
9144
  (SELECT count(*)::int FROM column_delta) AS column_delta_count`,
8227
- mutationParams,
8228
- );
8229
- const matchedKeys = new Set(rows[0]?.matched_keys ?? []);
8230
- const chunkFencedRows = parseRuntimeMapRowsFencedRows(rows[0]?.fenced_rows);
8231
- // An exact-version replay that already produced the same terminal value is
8232
- // accepted idempotently even when SQL has no physical mutation to apply.
8233
- // Stale versions remain excluded through fenced_rows.
8234
- const appliedCount = Number(rows[0]?.updated ?? 0);
8235
- updated += appliedCount;
8236
- fencedRows.push(...chunkFencedRows);
8237
- if (matchedKeys.size === chunk.length) {
8238
- continue;
8239
- }
9145
+ mutationParams,
9146
+ );
9147
+ const matchedKeys = new Set(rows[0]?.matched_keys ?? []);
9148
+ const chunkFencedRows = parseRuntimeMapRowsFencedRows(
9149
+ rows[0]?.fenced_rows,
9150
+ );
9151
+ // An exact-version replay that already produced the same terminal value is
9152
+ // accepted idempotently even when SQL has no physical mutation to apply.
9153
+ // Stale versions remain excluded through fenced_rows.
9154
+ const appliedCount = Number(rows[0]?.updated ?? 0);
9155
+ if (matchedKeys.size !== chunk.length || appliedCount !== chunk.length) {
9156
+ allExactRowsUpdated = false;
9157
+ }
9158
+ updated += appliedCount;
9159
+ fencedRows.push(...chunkFencedRows);
9160
+ if (matchedKeys.size === chunk.length) {
9161
+ continue;
9162
+ }
8240
9163
 
8241
- const repairChunk = chunk.filter(
8242
- (row) => !matchedKeys.has(row.key) && row.input_index !== null,
8243
- );
9164
+ repairChunk.push(
9165
+ ...chunk.filter(
9166
+ (row) => !matchedKeys.has(row.key) && row.input_index !== null,
9167
+ ),
9168
+ );
9169
+ }
8244
9170
  if (repairChunk.length === 0) {
8245
9171
  continue;
8246
9172
  }
@@ -8252,7 +9178,7 @@ async function completeRuntimeMapRowChunks(
8252
9178
  updated += repaired.updated;
8253
9179
  fencedRows.push(...repaired.fencedRows);
8254
9180
  }
8255
- return { updated, fencedRows };
9181
+ return { updated, fencedRows, allExactRowsUpdated };
8256
9182
  }
8257
9183
 
8258
9184
  async function completeRuntimeMapRowChunksWithInputIndexRepair(
@@ -8360,6 +9286,7 @@ async function completeRuntimeMapRowChunksWithInputIndexRepair(
8360
9286
  input.writeVersion,
8361
9287
  ];
8362
9288
  const columnTypes = await RuntimeSheetColumns.prepareWrite(client, {
9289
+ lockMode: input.columnLockMode,
8363
9290
  schema: session.postgres.schema,
8364
9291
  table: session.postgres.sheetTable,
8365
9292
  columns: input.physicalColumnProjections,
@@ -8546,6 +9473,7 @@ async function insertMissingCompletedMapRowChunks(
8546
9473
  const chunkDataPatches = chunk.map((row) => row.data_patch_json);
8547
9474
  const chunkCellMetaPatches = chunk.map((row) => row.cell_meta_patch_json);
8548
9475
  const columnTypes = await RuntimeSheetColumns.prepareWrite(client, {
9476
+ lockMode: input.columnLockMode,
8549
9477
  schema: session.postgres.schema,
8550
9478
  table: session.postgres.sheetTable,
8551
9479
  columns: input.physicalColumnProjections,
@@ -8553,13 +9481,19 @@ async function insertMissingCompletedMapRowChunks(
8553
9481
  const accepted = await client.query<{
8554
9482
  data_patch: Record<string, unknown>;
8555
9483
  }>(
8556
- `SELECT incoming.data_patch FROM jsonb_to_recordset($1::jsonb)
8557
- AS incoming(key text, input_index bigint, data_patch jsonb)
9484
+ `WITH exact_key_missing AS MATERIALIZED (
9485
+ SELECT incoming.* FROM jsonb_to_recordset($1::jsonb)
9486
+ AS incoming(key text, input_index bigint, data_patch jsonb)
9487
+ WHERE NOT EXISTS (
9488
+ SELECT 1 FROM ${sheetTable(session)} AS target
9489
+ WHERE target._key = incoming.key
9490
+ )
9491
+ )
9492
+ SELECT incoming.data_patch FROM exact_key_missing AS incoming
8558
9493
  WHERE NOT EXISTS (
8559
9494
  SELECT 1 FROM ${sheetTable(session)} AS target
8560
- WHERE target._key = incoming.key
8561
- OR (incoming.input_index IS NOT NULL AND target._run_id = $2::text
8562
- AND target._input_index = incoming.input_index)
9495
+ WHERE incoming.input_index IS NOT NULL AND target._run_id = $2::text
9496
+ AND target._input_index = incoming.input_index
8563
9497
  )`,
8564
9498
  [
8565
9499
  stringifyPostgresJson(
@@ -8602,20 +9536,28 @@ async function insertMissingCompletedMapRowChunks(
8602
9536
  JOIN unnest($4::jsonb[]) WITH ORDINALITY AS cell_meta_values(cell_meta_patch, ord)
8603
9537
  ON cell_meta_values.ord = key_values.ord
8604
9538
  ),
8605
- -- Decode only accepted rows: a rejected replay may contain values
8606
- -- incompatible with the stored native types.
8607
- missing_rows AS MATERIALIZED (
9539
+ -- Keep input-index repair behind exact-key misses: otherwise the OR
9540
+ -- can read a whole run's index entries for every existing key.
9541
+ exact_key_missing AS MATERIALIZED (
8608
9542
  SELECT input_rows.*
8609
9543
  FROM input_rows
8610
9544
  WHERE NOT EXISTS (
8611
9545
  SELECT 1
8612
9546
  FROM ${sheetTable(session)} AS target
8613
9547
  WHERE target._key = input_rows._key
8614
- OR (
8615
- input_rows.input_index IS NOT NULL
8616
- AND target._run_id = $5::text
8617
- AND target._input_index = input_rows.input_index
8618
- )
9548
+ )
9549
+ ),
9550
+ -- Decode only accepted rows: a rejected replay may contain values
9551
+ -- incompatible with the stored native types.
9552
+ missing_rows AS MATERIALIZED (
9553
+ SELECT exact_key_missing.*
9554
+ FROM exact_key_missing
9555
+ WHERE NOT EXISTS (
9556
+ SELECT 1
9557
+ FROM ${sheetTable(session)} AS target
9558
+ WHERE exact_key_missing.input_index IS NOT NULL
9559
+ AND target._run_id = $5::text
9560
+ AND target._input_index = exact_key_missing.input_index
8619
9561
  )
8620
9562
  ),
8621
9563
  inserted_rows AS (
@@ -8741,6 +9683,7 @@ async function failRuntimeMapRowChunks(
8741
9683
  client: RuntimeQueryClient,
8742
9684
  session: RuntimePostgresSession,
8743
9685
  input: {
9686
+ columnLockMode: 'shared' | 'exclusive';
8744
9687
  chunks: RuntimePreparedFailedRow[][];
8745
9688
  physicalColumnProjections: PhysicalSheetColumnProjection[];
8746
9689
  forceTerminal?: boolean;
@@ -8767,24 +9710,10 @@ async function failRuntimeMapRowChunks(
8767
9710
  'updates.data_patch',
8768
9711
  input.physicalColumnProjections,
8769
9712
  );
8770
- // Per-field failed-cell counts for the column summary, computed from the
8771
- // cell meta patches (a failed row records exactly which cell failed).
8772
- const failedCellCounts = new Map<string, number>();
8773
- for (const row of chunk) {
8774
- for (const [field, meta] of Object.entries(row.cell_meta_patch)) {
8775
- if (
8776
- meta &&
8777
- typeof meta === 'object' &&
8778
- (meta as { status?: unknown }).status === 'failed'
8779
- ) {
8780
- failedCellCounts.set(field, (failedCellCounts.get(field) ?? 0) + 1);
8781
- }
8782
- }
8783
- }
8784
- const failedCellFields = [...failedCellCounts.keys()];
8785
- const failedCellTotals = failedCellFields.map(
8786
- (field) => failedCellCounts.get(field) ?? 0,
8787
- );
9713
+ // Keep legacy parameter positions stable; deltas now come from applied
9714
+ // physical winners and their previous cells, never submitted aliases.
9715
+ const failedCellFields: string[] = [];
9716
+ const failedCellTotals: number[] = [];
8788
9717
  const matchingUpdatesSql = `WITH mutation_context AS (SELECT $7::text AS play_name, $8::text AS table_namespace, $9::text[] AS failed_fields, $10::int[] AS failed_totals, $12::text AS attempt_owner_run_id, $13::timestamptz AS attempt_expires_at, $14::integer AS attempt_seq), updates AS (
8789
9718
  SELECT key_values._key,
8790
9719
  input_index_values.input_index,
@@ -8856,6 +9785,7 @@ async function failRuntimeMapRowChunks(
8856
9785
  input.writeVersion,
8857
9786
  ];
8858
9787
  const columnTypes = await RuntimeSheetColumns.prepareWrite(client, {
9788
+ lockMode: input.columnLockMode,
8859
9789
  schema: session.postgres.schema,
8860
9790
  table: session.postgres.sheetTable,
8861
9791
  columns: input.physicalColumnProjections,
@@ -8896,7 +9826,10 @@ async function failRuntimeMapRowChunks(
8896
9826
  FROM matched_updates AS updates, ${sheetTable(session)} AS prev
8897
9827
  WHERE ${writableRowsSql}
8898
9828
  AND prev._key = target._key
8899
- RETURNING target._key, prev._status AS prev_status
9829
+ RETURNING target._key, prev._status AS prev_status,
9830
+ prev._cell_meta AS prev_cell_meta,
9831
+ target._cell_meta AS next_cell_meta,
9832
+ updates.cell_meta_patch
8900
9833
  ),
8901
9834
  applied_count AS (
8902
9835
  SELECT count(*)::bigint AS c,
@@ -8972,20 +9905,45 @@ async function failRuntimeMapRowChunks(
8972
9905
  _updated_at = now()
8973
9906
  RETURNING 1
8974
9907
  ),
9908
+ cell_status_delta AS (
9909
+ SELECT field_values.field,
9910
+ sum((coalesce(applied_rows.next_cell_meta -> field_values.field ->> 'status', '') = 'failed')::int
9911
+ - (coalesce(applied_rows.prev_cell_meta -> field_values.field ->> 'status', '') = 'failed')::int)::int AS failed,
9912
+ -- Completed output counts are admitted by the completed-row
9913
+ -- writer. Partial failed rows may carry uncounted completed
9914
+ -- cells; only remove a cell counted on an enriched row.
9915
+ -count(*) FILTER (
9916
+ WHERE applied_rows.prev_status = 'enriched'
9917
+ AND coalesce(applied_rows.prev_cell_meta -> field_values.field ->> 'status', '') = 'completed'
9918
+ AND coalesce(applied_rows.next_cell_meta -> field_values.field ->> 'status', '') <> 'completed'
9919
+ )::int AS completed
9920
+ FROM applied_rows
9921
+ CROSS JOIN LATERAL jsonb_object_keys(
9922
+ CASE WHEN jsonb_typeof(applied_rows.cell_meta_patch) = 'object'
9923
+ THEN applied_rows.cell_meta_patch ELSE '{}'::jsonb END
9924
+ ) AS field_values(field)
9925
+ GROUP BY field_values.field
9926
+ ),
8975
9927
  column_delta AS (
8976
9928
  INSERT INTO ${columnSummaryTable(session)} AS target (
8977
9929
  play_name,
8978
9930
  table_namespace,
8979
9931
  field,
8980
- failed
9932
+ failed,
9933
+ completed
8981
9934
  )
8982
- SELECT $7::text, $8::text, field_values.field, count_values.c
8983
- FROM unnest($9::text[]) WITH ORDINALITY AS field_values(field, ord)
8984
- JOIN unnest($10::int[]) WITH ORDINALITY AS count_values(c, ord)
8985
- ON count_values.ord = field_values.ord
8986
- WHERE EXISTS (SELECT 1 FROM applied_rows)
9935
+ SELECT $7::text, $8::text, field, GREATEST(failed, 0), GREATEST(completed, 0)
9936
+ FROM cell_status_delta
9937
+ WHERE failed <> 0 OR completed <> 0
8987
9938
  ON CONFLICT (play_name, table_namespace, field) DO UPDATE SET
8988
- failed = GREATEST(target.failed + EXCLUDED.failed, 0),
9939
+ failed = GREATEST(target.failed + (
9940
+ SELECT delta.failed FROM cell_status_delta AS delta
9941
+ WHERE delta.field = EXCLUDED.field
9942
+ ), 0),
9943
+ completed = GREATEST(target.completed + (
9944
+ SELECT delta.completed FROM cell_status_delta AS delta
9945
+ WHERE delta.field = EXCLUDED.field
9946
+ ), 0),
8989
9947
  _updated_at = now()
8990
9948
  RETURNING 1
8991
9949
  )
@@ -9002,6 +9960,40 @@ async function failRuntimeMapRowChunks(
9002
9960
  return { updated, fencedRows };
9003
9961
  }
9004
9962
 
9963
+ /** Post only rows already canonicalized by this module's owning operation. */
9964
+ async function postPreparedRuntimeMapRows(
9965
+ context: RuntimeApiContext & { playName: string },
9966
+ input: Parameters<typeof completeRuntimeMapRows>[1],
9967
+ submittedKeys: string[],
9968
+ ): Promise<RuntimeMapRowsWriteResult> {
9969
+ const result = await postRuntimeApi<
9970
+ Partial<RuntimeMapRowsWriteResult> &
9971
+ Pick<RuntimeMapRowsWriteResult, 'updated' | 'fencedKeys'>
9972
+ >(context, {
9973
+ action: 'runtime_sheet_complete_map_rows',
9974
+ input,
9975
+ });
9976
+ const staleKeys =
9977
+ result.staleKeys ?? result.staleDroppedKeys ?? result.fencedKeys;
9978
+ return {
9979
+ ...classifyRuntimeMapRowsWrite({
9980
+ submittedKeys,
9981
+ updated: result.updated,
9982
+ fencedKeys: staleKeys,
9983
+ // A gateway that predates stale reasons omits staleRows; keep it
9984
+ // absent so the caller classifies those keys as unreported.
9985
+ fencedRows: Array.isArray(result.staleRows)
9986
+ ? parseRuntimeMapRowsFencedRows(result.staleRows)
9987
+ : undefined,
9988
+ }),
9989
+ // Preserve the gateway's compatibility classification during rollout.
9990
+ fencedKeys: result.fencedKeys,
9991
+ ...(typeof result.runtimeTestPageTailHoldMs === 'number'
9992
+ ? { runtimeTestPageTailHoldMs: result.runtimeTestPageTailHoldMs }
9993
+ : {}),
9994
+ };
9995
+ }
9996
+
9005
9997
  /**
9006
9998
  * Mark map rows terminal in the per-run scoped Postgres sheet table by key.
9007
9999
  * Writes enriched output values into materialized physical columns, flips
@@ -9052,35 +10044,13 @@ export async function completeRuntimeMapRows(
9052
10044
  runId: input.runId,
9053
10045
  outputFields: input.outputFields ?? [],
9054
10046
  });
9055
- const result = await postRuntimeApi<
9056
- Partial<RuntimeMapRowsWriteResult> &
9057
- Pick<RuntimeMapRowsWriteResult, 'updated' | 'fencedKeys'>
9058
- >(context, {
9059
- action: 'runtime_sheet_complete_map_rows',
9060
- input: { ...input, rows },
9061
- });
9062
- const submittedKeys = keyedRows
9063
- .map((row) => row.key)
9064
- .filter((key): key is string => Boolean(key));
9065
- const staleKeys =
9066
- result.staleKeys ?? result.staleDroppedKeys ?? result.fencedKeys;
9067
- return {
9068
- ...classifyRuntimeMapRowsWrite({
9069
- submittedKeys,
9070
- updated: result.updated,
9071
- fencedKeys: staleKeys,
9072
- // A gateway that predates stale reasons omits staleRows; keep it
9073
- // absent so the caller classifies those keys as unreported.
9074
- fencedRows: Array.isArray(result.staleRows)
9075
- ? parseRuntimeMapRowsFencedRows(result.staleRows)
9076
- : undefined,
9077
- }),
9078
- // Preserve the gateway's compatibility classification during rollout.
9079
- fencedKeys: result.fencedKeys,
9080
- ...(typeof result.runtimeTestPageTailHoldMs === 'number'
9081
- ? { runtimeTestPageTailHoldMs: result.runtimeTestPageTailHoldMs }
9082
- : {}),
9083
- };
10047
+ return postPreparedRuntimeMapRows(
10048
+ context,
10049
+ { ...input, rows },
10050
+ keyedRows
10051
+ .map((row) => row.key)
10052
+ .filter((key): key is string => Boolean(key)),
10053
+ );
9084
10054
  }
9085
10055
  const sheetContract = augmentSheetContractWithDatasetFields({
9086
10056
  contract: input.sheetContract,
@@ -9097,22 +10067,35 @@ export async function completeRuntimeMapRows(
9097
10067
  let updatedRowCount = 0;
9098
10068
  let fencedRowCount = 0;
9099
10069
  const logCompletion = () => {
9100
- console.info(
9101
- '[runtime-sheet-map-completion]',
9102
- JSON.stringify({
9103
- event: 'finished',
9104
- outcome: completionOutcome,
9105
- inputRows: input.rows.length,
9106
- uniqueRows: uniqueRowCount,
9107
- completedRows: completedRowCount,
9108
- failedRows: failedRowCount,
9109
- chunks: chunkCount,
9110
- updatedRows: updatedRowCount,
9111
- fencedRows: fencedRowCount,
9112
- ...completionDiagnostics,
9113
- totalMs: Date.now() - completionStartedAt,
9114
- }),
9115
- );
10070
+ const diagnostics: RuntimeSheetMapCompletionDiagnostics = {
10071
+ event: 'finished',
10072
+ outcome: completionOutcome,
10073
+ inputRows: input.rows.length,
10074
+ uniqueRows: uniqueRowCount,
10075
+ completedRows: completedRowCount,
10076
+ failedRows: failedRowCount,
10077
+ chunks: chunkCount,
10078
+ updatedRows: updatedRowCount,
10079
+ fencedRows: fencedRowCount,
10080
+ ...completionDiagnostics,
10081
+ totalMs: Date.now() - completionStartedAt,
10082
+ };
10083
+ try {
10084
+ if (context.onSheetMapCompletion) {
10085
+ // Delivery is observational: neither a synchronous throw nor a rejected
10086
+ // asynchronous sink may replace the write's result or delay its return.
10087
+ void Promise.resolve(context.onSheetMapCompletion(diagnostics)).catch(
10088
+ () => {},
10089
+ );
10090
+ } else {
10091
+ console.info(
10092
+ '[runtime-sheet-map-completion]',
10093
+ JSON.stringify(diagnostics),
10094
+ );
10095
+ }
10096
+ } catch {
10097
+ // Product execution does not depend on operational telemetry delivery.
10098
+ }
9116
10099
  };
9117
10100
  let session: RuntimePostgresSession;
9118
10101
  try {
@@ -9175,11 +10158,20 @@ export async function completeRuntimeMapRows(
9175
10158
  runId: input.runId,
9176
10159
  outputFields: input.outputFields ?? [],
9177
10160
  });
10161
+ const sqlBatchBudget = {
10162
+ maxRows: DIRECT_POSTGRES_BATCH_SIZE,
10163
+ maxBytes: RUNTIME_MAP_SQL_BATCH_INPUT_BYTES,
10164
+ };
9178
10165
  const chunks = chunkValues(completedRows, DIRECT_POSTGRES_BATCH_SIZE);
10166
+ const exactKeyChunks = chunks.map((chunk) =>
10167
+ chunkPreparedRuntimeSheetRows(chunk, sqlBatchBudget),
10168
+ );
9179
10169
  const failedChunks = chunkValues(failedRows, DIRECT_POSTGRES_BATCH_SIZE);
9180
10170
  completedRowCount = completedRows.length;
9181
10171
  failedRowCount = failedRows.length;
9182
- chunkCount = chunks.length + failedChunks.length;
10172
+ chunkCount =
10173
+ exactKeyChunks.reduce((total, group) => total + group.length, 0) +
10174
+ failedChunks.length;
9183
10175
  const needsTransaction = true;
9184
10176
  const outputFields = [...new Set(input.outputFields ?? [])];
9185
10177
  const attemptId = input.attemptId?.trim() || null;
@@ -9205,15 +10197,18 @@ export async function completeRuntimeMapRows(
9205
10197
  tableNamespace: input.tableNamespace,
9206
10198
  sheetContract,
9207
10199
  transactional: needsTransaction,
10200
+ concurrentColumnPreparation: true,
9208
10201
  statementTimeoutMs: RUNTIME_SHEET_COMPLETION_STATEMENT_TIMEOUT_MS,
9209
10202
  completionDiagnostics,
9210
10203
  },
9211
- async (client) => {
10204
+ async (client, columnLockMode) => {
9212
10205
  const completed =
9213
10206
  completedRows.length > 0
9214
10207
  ? await (async () => {
9215
10208
  const chunkInput = {
10209
+ columnLockMode,
9216
10210
  chunks,
10211
+ exactKeyChunks,
9217
10212
  physicalColumnProjections: projections,
9218
10213
  runId: input.runId,
9219
10214
  attemptId,
@@ -9232,16 +10227,19 @@ export async function completeRuntimeMapRows(
9232
10227
  'mapRowsUpdate',
9233
10228
  () => completeRuntimeMapRowChunks(client, session, chunkInput),
9234
10229
  );
9235
- const inserted = await measureRuntimeSheetPhase(
9236
- completionDiagnostics,
9237
- 'missingRowsInsert',
9238
- () =>
9239
- insertMissingCompletedMapRowChunks(
9240
- client,
9241
- session,
9242
- chunkInput,
9243
- ),
9244
- );
10230
+ const inserted =
10231
+ updated.allExactRowsUpdated === true
10232
+ ? { inserted: 0 }
10233
+ : await measureRuntimeSheetPhase(
10234
+ completionDiagnostics,
10235
+ 'missingRowsInsert',
10236
+ () =>
10237
+ insertMissingCompletedMapRowChunks(
10238
+ client,
10239
+ session,
10240
+ chunkInput,
10241
+ ),
10242
+ );
9245
10243
  return {
9246
10244
  updated: updated.updated + inserted.inserted,
9247
10245
  fencedRows: updated.fencedRows,
@@ -9255,6 +10253,7 @@ export async function completeRuntimeMapRows(
9255
10253
  'failedRowsUpdate',
9256
10254
  () =>
9257
10255
  failRuntimeMapRowChunks(client, session, {
10256
+ columnLockMode,
9258
10257
  chunks: failedChunks,
9259
10258
  physicalColumnProjections: projections,
9260
10259
  forceTerminal: input.forceFailedRows === true,
@@ -9278,14 +10277,22 @@ export async function completeRuntimeMapRows(
9278
10277
  updated: completed.updated + failed.updated,
9279
10278
  fencedRows: [...completed.fencedRows, ...failed.fencedRows],
9280
10279
  });
9281
- if (await runtimeSheetMembershipCaptureAvailable(client, session)) {
9282
- await captureRuntimeSheetTerminalMembership(client, session, {
9283
- runId: input.runId,
9284
- tableNamespace: input.tableNamespace,
9285
- sheetContract,
9286
- keys: classified.committedKeys,
9287
- });
9288
- }
10280
+ // This phase includes the availability probe and, when available, the
10281
+ // terminal membership capture. It is nested inside queryClientMs.
10282
+ await measureRuntimeSheetPhase(
10283
+ completionDiagnostics,
10284
+ 'membershipCapture',
10285
+ async () => {
10286
+ if (await runtimeSheetMembershipCaptureAvailable(client, session)) {
10287
+ await captureRuntimeSheetTerminalMembership(client, session, {
10288
+ runId: input.runId,
10289
+ tableNamespace: input.tableNamespace,
10290
+ sheetContract,
10291
+ keys: classified.committedKeys,
10292
+ });
10293
+ }
10294
+ },
10295
+ );
9289
10296
  return classified;
9290
10297
  },
9291
10298
  )
@@ -9318,7 +10325,15 @@ export function createCompleteRuntimeMapRowsOperation(
9318
10325
  outputFields: input.outputFields ?? [],
9319
10326
  }),
9320
10327
  };
9321
- return () => completeRuntimeMapRows(context, frozenInput);
10328
+ const submittedKeys = frozenInput.rows
10329
+ .map((row) => row.key)
10330
+ .filter((key): key is string => Boolean(key));
10331
+ // The operation owns this JSON-safe snapshot. Re-preparing it on each
10332
+ // capacity retry repeats full data/meta stringify+parse without changing
10333
+ // its canonical value. Ordinary public calls still prepare above.
10334
+ return frozenInput.rows.length === 0
10335
+ ? () => completeRuntimeMapRows(context, frozenInput)
10336
+ : () => postPreparedRuntimeMapRows(context, frozenInput, submittedKeys);
9322
10337
  }
9323
10338
 
9324
10339
  export type RuntimeSheetDatasetReadInput = {