@smthrs/engine-store 0.0.0-stage → 1.0.0-rc.4

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 (787) hide show
  1. package/CHANGELOG.md +247 -0
  2. package/LICENSE +21 -0
  3. package/README.md +146 -2
  4. package/dist/cjs/ArtifactGc.d.ts +235 -0
  5. package/dist/cjs/ArtifactGc.d.ts.map +1 -0
  6. package/dist/cjs/ArtifactGc.js +183 -0
  7. package/dist/cjs/ArtifactGc.js.map +7 -0
  8. package/dist/cjs/ArtifactSync.d.ts +164 -0
  9. package/dist/cjs/ArtifactSync.d.ts.map +1 -0
  10. package/dist/cjs/ArtifactSync.js +144 -0
  11. package/dist/cjs/ArtifactSync.js.map +7 -0
  12. package/dist/cjs/CacheSync.d.ts +112 -0
  13. package/dist/cjs/CacheSync.d.ts.map +1 -0
  14. package/dist/cjs/CacheSync.js +64 -0
  15. package/dist/cjs/CacheSync.js.map +7 -0
  16. package/dist/cjs/DeferredClockFold.d.ts +354 -0
  17. package/dist/cjs/DeferredClockFold.d.ts.map +1 -0
  18. package/dist/cjs/DeferredClockFold.js +335 -0
  19. package/dist/cjs/DeferredClockFold.js.map +7 -0
  20. package/dist/cjs/DisasterRecovery.d.ts +431 -0
  21. package/dist/cjs/DisasterRecovery.d.ts.map +1 -0
  22. package/dist/cjs/DisasterRecovery.js +453 -0
  23. package/dist/cjs/DisasterRecovery.js.map +7 -0
  24. package/dist/cjs/DurableEngineState.d.ts +776 -0
  25. package/dist/cjs/DurableEngineState.d.ts.map +1 -0
  26. package/dist/cjs/DurableEngineState.js +1536 -0
  27. package/dist/cjs/DurableEngineState.js.map +7 -0
  28. package/dist/cjs/EngineStore.d.ts +158 -0
  29. package/dist/cjs/EngineStore.d.ts.map +1 -0
  30. package/dist/cjs/EngineStore.js +398 -0
  31. package/dist/cjs/EngineStore.js.map +7 -0
  32. package/dist/cjs/EngineStoreMetrics.d.ts +276 -0
  33. package/dist/cjs/EngineStoreMetrics.d.ts.map +1 -0
  34. package/dist/cjs/EngineStoreMetrics.js +135 -0
  35. package/dist/cjs/EngineStoreMetrics.js.map +7 -0
  36. package/dist/cjs/Errors.d.ts +22 -0
  37. package/dist/cjs/Errors.d.ts.map +1 -0
  38. package/dist/cjs/Errors.js +39 -0
  39. package/dist/cjs/Errors.js.map +7 -0
  40. package/dist/cjs/EventTypes.d.ts +27 -0
  41. package/dist/cjs/EventTypes.d.ts.map +1 -0
  42. package/dist/cjs/EventTypes.js +37 -0
  43. package/dist/cjs/EventTypes.js.map +7 -0
  44. package/dist/cjs/ExecutionFacts.d.ts +59 -0
  45. package/dist/cjs/ExecutionFacts.d.ts.map +1 -0
  46. package/dist/cjs/ExecutionFacts.js +123 -0
  47. package/dist/cjs/ExecutionFacts.js.map +7 -0
  48. package/dist/cjs/ExecutionSnapshot.d.ts +117 -0
  49. package/dist/cjs/ExecutionSnapshot.d.ts.map +1 -0
  50. package/dist/cjs/ExecutionSnapshot.js +111 -0
  51. package/dist/cjs/ExecutionSnapshot.js.map +7 -0
  52. package/dist/cjs/Inconsistency.d.ts +154 -0
  53. package/dist/cjs/Inconsistency.d.ts.map +1 -0
  54. package/dist/cjs/Inconsistency.js +135 -0
  55. package/dist/cjs/Inconsistency.js.map +7 -0
  56. package/dist/cjs/Migrations.d.ts +56 -0
  57. package/dist/cjs/Migrations.d.ts.map +1 -0
  58. package/dist/cjs/Migrations.js +78 -0
  59. package/dist/cjs/Migrations.js.map +7 -0
  60. package/dist/cjs/OwnerIdentity.d.ts +91 -0
  61. package/dist/cjs/OwnerIdentity.d.ts.map +1 -0
  62. package/dist/cjs/OwnerIdentity.js +65 -0
  63. package/dist/cjs/OwnerIdentity.js.map +7 -0
  64. package/dist/cjs/PlanInputStore.d.ts +110 -0
  65. package/dist/cjs/PlanInputStore.d.ts.map +1 -0
  66. package/dist/cjs/PlanInputStore.js +224 -0
  67. package/dist/cjs/PlanInputStore.js.map +7 -0
  68. package/dist/cjs/PlanMergeStore.d.ts +113 -0
  69. package/dist/cjs/PlanMergeStore.d.ts.map +1 -0
  70. package/dist/cjs/PlanMergeStore.js +255 -0
  71. package/dist/cjs/PlanMergeStore.js.map +7 -0
  72. package/dist/cjs/PlanScheduler.d.ts +363 -0
  73. package/dist/cjs/PlanScheduler.d.ts.map +1 -0
  74. package/dist/cjs/PlanScheduler.js +1090 -0
  75. package/dist/cjs/PlanScheduler.js.map +7 -0
  76. package/dist/cjs/Reconciliation.d.ts +175 -0
  77. package/dist/cjs/Reconciliation.d.ts.map +1 -0
  78. package/dist/cjs/Reconciliation.js +119 -0
  79. package/dist/cjs/Reconciliation.js.map +7 -0
  80. package/dist/cjs/ReplayOnly.d.ts +91 -0
  81. package/dist/cjs/ReplayOnly.d.ts.map +1 -0
  82. package/dist/cjs/ReplayOnly.js +52 -0
  83. package/dist/cjs/ReplayOnly.js.map +7 -0
  84. package/dist/cjs/Retention.d.ts +113 -0
  85. package/dist/cjs/Retention.d.ts.map +1 -0
  86. package/dist/cjs/Retention.js +99 -0
  87. package/dist/cjs/Retention.js.map +7 -0
  88. package/dist/cjs/RunCatalogRead.d.ts +17 -0
  89. package/dist/cjs/RunCatalogRead.d.ts.map +1 -0
  90. package/dist/cjs/RunCatalogRead.js +33 -0
  91. package/dist/cjs/RunCatalogRead.js.map +7 -0
  92. package/dist/cjs/RunChangeFeed.d.ts +75 -0
  93. package/dist/cjs/RunChangeFeed.d.ts.map +1 -0
  94. package/dist/cjs/RunChangeFeed.js +96 -0
  95. package/dist/cjs/RunChangeFeed.js.map +7 -0
  96. package/dist/cjs/RunState.d.ts +116 -0
  97. package/dist/cjs/RunState.d.ts.map +1 -0
  98. package/dist/cjs/RunState.js +76 -0
  99. package/dist/cjs/RunState.js.map +7 -0
  100. package/dist/cjs/Selection.d.ts +495 -0
  101. package/dist/cjs/Selection.d.ts.map +1 -0
  102. package/dist/cjs/Selection.js +288 -0
  103. package/dist/cjs/Selection.js.map +7 -0
  104. package/dist/cjs/SelectionStore.d.ts +141 -0
  105. package/dist/cjs/SelectionStore.d.ts.map +1 -0
  106. package/dist/cjs/SelectionStore.js +229 -0
  107. package/dist/cjs/SelectionStore.js.map +7 -0
  108. package/dist/cjs/StepBoundary.d.ts +501 -0
  109. package/dist/cjs/StepBoundary.d.ts.map +1 -0
  110. package/dist/cjs/StepBoundary.js +619 -0
  111. package/dist/cjs/StepBoundary.js.map +7 -0
  112. package/dist/cjs/StepCacheFold.d.ts +148 -0
  113. package/dist/cjs/StepCacheFold.d.ts.map +1 -0
  114. package/dist/cjs/StepCacheFold.js +131 -0
  115. package/dist/cjs/StepCacheFold.js.map +7 -0
  116. package/dist/cjs/StepSandbox.d.ts +78 -0
  117. package/dist/cjs/StepSandbox.d.ts.map +1 -0
  118. package/dist/cjs/StepSandbox.js +75 -0
  119. package/dist/cjs/StepSandbox.js.map +7 -0
  120. package/dist/cjs/WakeBus.d.ts +116 -0
  121. package/dist/cjs/WakeBus.d.ts.map +1 -0
  122. package/dist/cjs/WakeBus.js +89 -0
  123. package/dist/cjs/WakeBus.js.map +7 -0
  124. package/dist/cjs/WorkspaceSandbox.d.ts +619 -0
  125. package/dist/cjs/WorkspaceSandbox.d.ts.map +1 -0
  126. package/dist/cjs/WorkspaceSandbox.js +711 -0
  127. package/dist/cjs/WorkspaceSandbox.js.map +7 -0
  128. package/dist/cjs/index.d.ts +157 -0
  129. package/dist/cjs/index.d.ts.map +1 -0
  130. package/dist/cjs/index.js +95 -0
  131. package/dist/cjs/index.js.map +7 -0
  132. package/dist/cjs/internal/ActionPersistence.d.ts +276 -0
  133. package/dist/cjs/internal/ActionPersistence.d.ts.map +1 -0
  134. package/dist/cjs/internal/ActionPersistence.js +1395 -0
  135. package/dist/cjs/internal/ActionPersistence.js.map +7 -0
  136. package/dist/cjs/internal/ArtifactRoots.d.ts +38 -0
  137. package/dist/cjs/internal/ArtifactRoots.d.ts.map +1 -0
  138. package/dist/cjs/internal/ArtifactRoots.js +89 -0
  139. package/dist/cjs/internal/ArtifactRoots.js.map +7 -0
  140. package/dist/cjs/internal/AttemptAdmission.d.ts +53 -0
  141. package/dist/cjs/internal/AttemptAdmission.d.ts.map +1 -0
  142. package/dist/cjs/internal/AttemptAdmission.js +52 -0
  143. package/dist/cjs/internal/AttemptAdmission.js.map +7 -0
  144. package/dist/cjs/internal/AttemptLifecycle.d.ts +245 -0
  145. package/dist/cjs/internal/AttemptLifecycle.d.ts.map +1 -0
  146. package/dist/cjs/internal/AttemptLifecycle.js +112 -0
  147. package/dist/cjs/internal/AttemptLifecycle.js.map +7 -0
  148. package/dist/cjs/internal/AttemptProbe.d.ts +53 -0
  149. package/dist/cjs/internal/AttemptProbe.d.ts.map +1 -0
  150. package/dist/cjs/internal/AttemptProbe.js +66 -0
  151. package/dist/cjs/internal/AttemptProbe.js.map +7 -0
  152. package/dist/cjs/internal/CacheAdmission.d.ts +79 -0
  153. package/dist/cjs/internal/CacheAdmission.d.ts.map +1 -0
  154. package/dist/cjs/internal/CacheAdmission.js +71 -0
  155. package/dist/cjs/internal/CacheAdmission.js.map +7 -0
  156. package/dist/cjs/internal/CacheAgeHistory.d.ts +27 -0
  157. package/dist/cjs/internal/CacheAgeHistory.d.ts.map +1 -0
  158. package/dist/cjs/internal/CacheAgeHistory.js +61 -0
  159. package/dist/cjs/internal/CacheAgeHistory.js.map +7 -0
  160. package/dist/cjs/internal/CacheAgeVerdicts.d.ts +15 -0
  161. package/dist/cjs/internal/CacheAgeVerdicts.d.ts.map +1 -0
  162. package/dist/cjs/internal/CacheAgeVerdicts.js +73 -0
  163. package/dist/cjs/internal/CacheAgeVerdicts.js.map +7 -0
  164. package/dist/cjs/internal/CacheOutputPolicy.d.ts +24 -0
  165. package/dist/cjs/internal/CacheOutputPolicy.d.ts.map +1 -0
  166. package/dist/cjs/internal/CacheOutputPolicy.js +75 -0
  167. package/dist/cjs/internal/CacheOutputPolicy.js.map +7 -0
  168. package/dist/cjs/internal/CachePublication.d.ts +97 -0
  169. package/dist/cjs/internal/CachePublication.d.ts.map +1 -0
  170. package/dist/cjs/internal/CachePublication.js +80 -0
  171. package/dist/cjs/internal/CachePublication.js.map +7 -0
  172. package/dist/cjs/internal/CallFacts.d.ts +15 -0
  173. package/dist/cjs/internal/CallFacts.d.ts.map +1 -0
  174. package/dist/cjs/internal/CallFacts.js +97 -0
  175. package/dist/cjs/internal/CallFacts.js.map +7 -0
  176. package/dist/cjs/internal/CopiedRecord.d.ts +61 -0
  177. package/dist/cjs/internal/CopiedRecord.d.ts.map +1 -0
  178. package/dist/cjs/internal/CopiedRecord.js +134 -0
  179. package/dist/cjs/internal/CopiedRecord.js.map +7 -0
  180. package/dist/cjs/internal/DeferredPersistence.d.ts +108 -0
  181. package/dist/cjs/internal/DeferredPersistence.d.ts.map +1 -0
  182. package/dist/cjs/internal/DeferredPersistence.js +270 -0
  183. package/dist/cjs/internal/DeferredPersistence.js.map +7 -0
  184. package/dist/cjs/internal/EffectRecords.d.ts +92 -0
  185. package/dist/cjs/internal/EffectRecords.d.ts.map +1 -0
  186. package/dist/cjs/internal/EffectRecords.js +63 -0
  187. package/dist/cjs/internal/EffectRecords.js.map +7 -0
  188. package/dist/cjs/internal/EngineJj.d.ts +30 -0
  189. package/dist/cjs/internal/EngineJj.d.ts.map +1 -0
  190. package/dist/cjs/internal/EngineJj.js +42 -0
  191. package/dist/cjs/internal/EngineJj.js.map +7 -0
  192. package/dist/cjs/internal/EngineStateSchema.d.ts +68 -0
  193. package/dist/cjs/internal/EngineStateSchema.d.ts.map +1 -0
  194. package/dist/cjs/internal/EngineStateSchema.js +58 -0
  195. package/dist/cjs/internal/EngineStateSchema.js.map +7 -0
  196. package/dist/cjs/internal/ExecutionSnapshotRead.d.ts +162 -0
  197. package/dist/cjs/internal/ExecutionSnapshotRead.d.ts.map +1 -0
  198. package/dist/cjs/internal/ExecutionSnapshotRead.js +201 -0
  199. package/dist/cjs/internal/ExecutionSnapshotRead.js.map +7 -0
  200. package/dist/cjs/internal/ExecutionSnapshotRelations.d.ts +39 -0
  201. package/dist/cjs/internal/ExecutionSnapshotRelations.d.ts.map +1 -0
  202. package/dist/cjs/internal/ExecutionSnapshotRelations.js +105 -0
  203. package/dist/cjs/internal/ExecutionSnapshotRelations.js.map +7 -0
  204. package/dist/cjs/internal/ExitEncoding.d.ts +180 -0
  205. package/dist/cjs/internal/ExitEncoding.d.ts.map +1 -0
  206. package/dist/cjs/internal/ExitEncoding.js +236 -0
  207. package/dist/cjs/internal/ExitEncoding.js.map +7 -0
  208. package/dist/cjs/internal/FileBoundarySnapshot.d.ts +14 -0
  209. package/dist/cjs/internal/FileBoundarySnapshot.d.ts.map +1 -0
  210. package/dist/cjs/internal/FileBoundarySnapshot.js +51 -0
  211. package/dist/cjs/internal/FileBoundarySnapshot.js.map +7 -0
  212. package/dist/cjs/internal/FileEnumeration.d.ts +127 -0
  213. package/dist/cjs/internal/FileEnumeration.d.ts.map +1 -0
  214. package/dist/cjs/internal/FileEnumeration.js +194 -0
  215. package/dist/cjs/internal/FileEnumeration.js.map +7 -0
  216. package/dist/cjs/internal/HostReflection.d.ts +80 -0
  217. package/dist/cjs/internal/HostReflection.d.ts.map +1 -0
  218. package/dist/cjs/internal/HostReflection.js +43 -0
  219. package/dist/cjs/internal/HostReflection.js.map +7 -0
  220. package/dist/cjs/internal/JournalRecords.d.ts +325 -0
  221. package/dist/cjs/internal/JournalRecords.d.ts.map +1 -0
  222. package/dist/cjs/internal/JournalRecords.js +122 -0
  223. package/dist/cjs/internal/JournalRecords.js.map +7 -0
  224. package/dist/cjs/internal/NodeJournal.d.ts +157 -0
  225. package/dist/cjs/internal/NodeJournal.d.ts.map +1 -0
  226. package/dist/cjs/internal/NodeJournal.js +192 -0
  227. package/dist/cjs/internal/NodeJournal.js.map +7 -0
  228. package/dist/cjs/internal/Ordering.d.ts +14 -0
  229. package/dist/cjs/internal/Ordering.d.ts.map +1 -0
  230. package/dist/cjs/internal/Ordering.js +25 -0
  231. package/dist/cjs/internal/Ordering.js.map +7 -0
  232. package/dist/cjs/internal/PlanInputs.d.ts +14 -0
  233. package/dist/cjs/internal/PlanInputs.d.ts.map +1 -0
  234. package/dist/cjs/internal/PlanInputs.js +78 -0
  235. package/dist/cjs/internal/PlanInputs.js.map +7 -0
  236. package/dist/cjs/internal/PlanMerges.d.ts +178 -0
  237. package/dist/cjs/internal/PlanMerges.d.ts.map +1 -0
  238. package/dist/cjs/internal/PlanMerges.js +125 -0
  239. package/dist/cjs/internal/PlanMerges.js.map +7 -0
  240. package/dist/cjs/internal/ProvenanceSlot.d.ts +30 -0
  241. package/dist/cjs/internal/ProvenanceSlot.d.ts.map +1 -0
  242. package/dist/cjs/internal/ProvenanceSlot.js +53 -0
  243. package/dist/cjs/internal/ProvenanceSlot.js.map +7 -0
  244. package/dist/cjs/internal/ResultEnvelope.d.ts +88 -0
  245. package/dist/cjs/internal/ResultEnvelope.d.ts.map +1 -0
  246. package/dist/cjs/internal/ResultEnvelope.js +90 -0
  247. package/dist/cjs/internal/ResultEnvelope.js.map +7 -0
  248. package/dist/cjs/internal/RetentionOps.d.ts +414 -0
  249. package/dist/cjs/internal/RetentionOps.d.ts.map +1 -0
  250. package/dist/cjs/internal/RetentionOps.js +354 -0
  251. package/dist/cjs/internal/RetentionOps.js.map +7 -0
  252. package/dist/cjs/internal/RunCatalogOps.d.ts +118 -0
  253. package/dist/cjs/internal/RunCatalogOps.d.ts.map +1 -0
  254. package/dist/cjs/internal/RunCatalogOps.js +85 -0
  255. package/dist/cjs/internal/RunCatalogOps.js.map +7 -0
  256. package/dist/cjs/internal/RunCoordinator.d.ts +78 -0
  257. package/dist/cjs/internal/RunCoordinator.d.ts.map +1 -0
  258. package/dist/cjs/internal/RunCoordinator.js +123 -0
  259. package/dist/cjs/internal/RunCoordinator.js.map +7 -0
  260. package/dist/cjs/internal/RunDriver.d.ts +192 -0
  261. package/dist/cjs/internal/RunDriver.d.ts.map +1 -0
  262. package/dist/cjs/internal/RunDriver.js +1586 -0
  263. package/dist/cjs/internal/RunDriver.js.map +7 -0
  264. package/dist/cjs/internal/RunLifecycle.d.ts +22 -0
  265. package/dist/cjs/internal/RunLifecycle.d.ts.map +1 -0
  266. package/dist/cjs/internal/RunLifecycle.js +63 -0
  267. package/dist/cjs/internal/RunLifecycle.js.map +7 -0
  268. package/dist/cjs/internal/RunListing.d.ts +78 -0
  269. package/dist/cjs/internal/RunListing.d.ts.map +1 -0
  270. package/dist/cjs/internal/RunListing.js +171 -0
  271. package/dist/cjs/internal/RunListing.js.map +7 -0
  272. package/dist/cjs/internal/RunListingError.d.ts +35 -0
  273. package/dist/cjs/internal/RunListingError.d.ts.map +1 -0
  274. package/dist/cjs/internal/RunListingError.js +47 -0
  275. package/dist/cjs/internal/RunListingError.js.map +7 -0
  276. package/dist/cjs/internal/RuntimeGraph.d.ts +73 -0
  277. package/dist/cjs/internal/RuntimeGraph.d.ts.map +1 -0
  278. package/dist/cjs/internal/RuntimeGraph.js +180 -0
  279. package/dist/cjs/internal/RuntimeGraph.js.map +7 -0
  280. package/dist/cjs/internal/SandboxedExecution.d.ts +80 -0
  281. package/dist/cjs/internal/SandboxedExecution.d.ts.map +1 -0
  282. package/dist/cjs/internal/SandboxedExecution.js +104 -0
  283. package/dist/cjs/internal/SandboxedExecution.js.map +7 -0
  284. package/dist/cjs/internal/StateTransaction.d.ts +44 -0
  285. package/dist/cjs/internal/StateTransaction.d.ts.map +1 -0
  286. package/dist/cjs/internal/StateTransaction.js +52 -0
  287. package/dist/cjs/internal/StateTransaction.js.map +7 -0
  288. package/dist/cjs/internal/StepFacts.d.ts +14 -0
  289. package/dist/cjs/internal/StepFacts.d.ts.map +1 -0
  290. package/dist/cjs/internal/StepFacts.js +58 -0
  291. package/dist/cjs/internal/StepFacts.js.map +7 -0
  292. package/dist/cjs/internal/TypedEvents.d.ts +14 -0
  293. package/dist/cjs/internal/TypedEvents.d.ts.map +1 -0
  294. package/dist/cjs/internal/TypedEvents.js +37 -0
  295. package/dist/cjs/internal/TypedEvents.js.map +7 -0
  296. package/dist/cjs/migrations/0001_initial.d.ts +17 -0
  297. package/dist/cjs/migrations/0001_initial.d.ts.map +1 -0
  298. package/dist/cjs/migrations/0001_initial.js +61 -0
  299. package/dist/cjs/migrations/0001_initial.js.map +7 -0
  300. package/dist/cjs/migrations/0002_selection_store.d.ts +15 -0
  301. package/dist/cjs/migrations/0002_selection_store.d.ts.map +1 -0
  302. package/dist/cjs/migrations/0002_selection_store.js +56 -0
  303. package/dist/cjs/migrations/0002_selection_store.js.map +7 -0
  304. package/dist/cjs/migrations/0003_plan_inputs.d.ts +13 -0
  305. package/dist/cjs/migrations/0003_plan_inputs.d.ts.map +1 -0
  306. package/dist/cjs/migrations/0003_plan_inputs.js +90 -0
  307. package/dist/cjs/migrations/0003_plan_inputs.js.map +7 -0
  308. package/dist/cjs/migrations/0004_plan_environment.d.ts +14 -0
  309. package/dist/cjs/migrations/0004_plan_environment.d.ts.map +1 -0
  310. package/dist/cjs/migrations/0004_plan_environment.js +56 -0
  311. package/dist/cjs/migrations/0004_plan_environment.js.map +7 -0
  312. package/dist/cjs/migrations/0005_plan_merges.d.ts +14 -0
  313. package/dist/cjs/migrations/0005_plan_merges.d.ts.map +1 -0
  314. package/dist/cjs/migrations/0005_plan_merges.js +82 -0
  315. package/dist/cjs/migrations/0005_plan_merges.js.map +7 -0
  316. package/dist/cjs/migrations/0006_execution_listing.d.ts +15 -0
  317. package/dist/cjs/migrations/0006_execution_listing.d.ts.map +1 -0
  318. package/dist/cjs/migrations/0006_execution_listing.js +101 -0
  319. package/dist/cjs/migrations/0006_execution_listing.js.map +7 -0
  320. package/dist/cjs/migrations/0007_run_parent_sequence.d.ts +15 -0
  321. package/dist/cjs/migrations/0007_run_parent_sequence.d.ts.map +1 -0
  322. package/dist/cjs/migrations/0007_run_parent_sequence.js +40 -0
  323. package/dist/cjs/migrations/0007_run_parent_sequence.js.map +7 -0
  324. package/dist/cjs/migrations/0008_deferred_consumption.d.ts +16 -0
  325. package/dist/cjs/migrations/0008_deferred_consumption.d.ts.map +1 -0
  326. package/dist/cjs/migrations/0008_deferred_consumption.js +45 -0
  327. package/dist/cjs/migrations/0008_deferred_consumption.js.map +7 -0
  328. package/dist/cjs/migrations/0009_deferred_clock_facts.d.ts +24 -0
  329. package/dist/cjs/migrations/0009_deferred_clock_facts.d.ts.map +1 -0
  330. package/dist/cjs/migrations/0009_deferred_clock_facts.js +141 -0
  331. package/dist/cjs/migrations/0009_deferred_clock_facts.js.map +7 -0
  332. package/dist/cjs/migrations/0010_pending_cancellation.d.ts +15 -0
  333. package/dist/cjs/migrations/0010_pending_cancellation.d.ts.map +1 -0
  334. package/dist/cjs/migrations/0010_pending_cancellation.js +41 -0
  335. package/dist/cjs/migrations/0010_pending_cancellation.js.map +7 -0
  336. package/dist/cjs/package.json +1 -0
  337. package/dist/cjs/test/TestStores.d.ts +89 -0
  338. package/dist/cjs/test/TestStores.d.ts.map +1 -0
  339. package/dist/cjs/test/TestStores.js +98 -0
  340. package/dist/cjs/test/TestStores.js.map +7 -0
  341. package/dist/esm/ArtifactGc.d.ts +235 -0
  342. package/dist/esm/ArtifactGc.d.ts.map +1 -0
  343. package/dist/esm/ArtifactGc.js +268 -0
  344. package/dist/esm/ArtifactGc.js.map +1 -0
  345. package/dist/esm/ArtifactSync.d.ts +164 -0
  346. package/dist/esm/ArtifactSync.d.ts.map +1 -0
  347. package/dist/esm/ArtifactSync.js +176 -0
  348. package/dist/esm/ArtifactSync.js.map +1 -0
  349. package/dist/esm/CacheSync.d.ts +112 -0
  350. package/dist/esm/CacheSync.d.ts.map +1 -0
  351. package/dist/esm/CacheSync.js +94 -0
  352. package/dist/esm/CacheSync.js.map +1 -0
  353. package/dist/esm/DeferredClockFold.d.ts +354 -0
  354. package/dist/esm/DeferredClockFold.d.ts.map +1 -0
  355. package/dist/esm/DeferredClockFold.js +472 -0
  356. package/dist/esm/DeferredClockFold.js.map +1 -0
  357. package/dist/esm/DisasterRecovery.d.ts +431 -0
  358. package/dist/esm/DisasterRecovery.d.ts.map +1 -0
  359. package/dist/esm/DisasterRecovery.js +523 -0
  360. package/dist/esm/DisasterRecovery.js.map +1 -0
  361. package/dist/esm/DurableEngineState.d.ts +776 -0
  362. package/dist/esm/DurableEngineState.d.ts.map +1 -0
  363. package/dist/esm/DurableEngineState.js +1648 -0
  364. package/dist/esm/DurableEngineState.js.map +1 -0
  365. package/dist/esm/EngineStore.d.ts +158 -0
  366. package/dist/esm/EngineStore.d.ts.map +1 -0
  367. package/dist/esm/EngineStore.js +403 -0
  368. package/dist/esm/EngineStore.js.map +1 -0
  369. package/dist/esm/EngineStoreMetrics.d.ts +276 -0
  370. package/dist/esm/EngineStoreMetrics.d.ts.map +1 -0
  371. package/dist/esm/EngineStoreMetrics.js +260 -0
  372. package/dist/esm/EngineStoreMetrics.js.map +1 -0
  373. package/dist/esm/Errors.d.ts +22 -0
  374. package/dist/esm/Errors.d.ts.map +1 -0
  375. package/dist/esm/Errors.js +22 -0
  376. package/dist/esm/Errors.js.map +1 -0
  377. package/dist/esm/EventTypes.d.ts +27 -0
  378. package/dist/esm/EventTypes.d.ts.map +1 -0
  379. package/dist/esm/EventTypes.js +27 -0
  380. package/dist/esm/EventTypes.js.map +1 -0
  381. package/dist/esm/ExecutionFacts.d.ts +59 -0
  382. package/dist/esm/ExecutionFacts.d.ts.map +1 -0
  383. package/dist/esm/ExecutionFacts.js +96 -0
  384. package/dist/esm/ExecutionFacts.js.map +1 -0
  385. package/dist/esm/ExecutionSnapshot.d.ts +117 -0
  386. package/dist/esm/ExecutionSnapshot.d.ts.map +1 -0
  387. package/dist/esm/ExecutionSnapshot.js +95 -0
  388. package/dist/esm/ExecutionSnapshot.js.map +1 -0
  389. package/dist/esm/Inconsistency.d.ts +154 -0
  390. package/dist/esm/Inconsistency.d.ts.map +1 -0
  391. package/dist/esm/Inconsistency.js +133 -0
  392. package/dist/esm/Inconsistency.js.map +1 -0
  393. package/dist/esm/Migrations.d.ts +56 -0
  394. package/dist/esm/Migrations.d.ts.map +1 -0
  395. package/dist/esm/Migrations.js +91 -0
  396. package/dist/esm/Migrations.js.map +1 -0
  397. package/dist/esm/OwnerIdentity.d.ts +91 -0
  398. package/dist/esm/OwnerIdentity.d.ts.map +1 -0
  399. package/dist/esm/OwnerIdentity.js +99 -0
  400. package/dist/esm/OwnerIdentity.js.map +1 -0
  401. package/dist/esm/PlanInputStore.d.ts +110 -0
  402. package/dist/esm/PlanInputStore.d.ts.map +1 -0
  403. package/dist/esm/PlanInputStore.js +207 -0
  404. package/dist/esm/PlanInputStore.js.map +1 -0
  405. package/dist/esm/PlanMergeStore.d.ts +113 -0
  406. package/dist/esm/PlanMergeStore.d.ts.map +1 -0
  407. package/dist/esm/PlanMergeStore.js +244 -0
  408. package/dist/esm/PlanMergeStore.js.map +1 -0
  409. package/dist/esm/PlanScheduler.d.ts +363 -0
  410. package/dist/esm/PlanScheduler.d.ts.map +1 -0
  411. package/dist/esm/PlanScheduler.js +1321 -0
  412. package/dist/esm/PlanScheduler.js.map +1 -0
  413. package/dist/esm/Reconciliation.d.ts +175 -0
  414. package/dist/esm/Reconciliation.d.ts.map +1 -0
  415. package/dist/esm/Reconciliation.js +175 -0
  416. package/dist/esm/Reconciliation.js.map +1 -0
  417. package/dist/esm/ReplayOnly.d.ts +91 -0
  418. package/dist/esm/ReplayOnly.d.ts.map +1 -0
  419. package/dist/esm/ReplayOnly.js +59 -0
  420. package/dist/esm/ReplayOnly.js.map +1 -0
  421. package/dist/esm/Retention.d.ts +113 -0
  422. package/dist/esm/Retention.d.ts.map +1 -0
  423. package/dist/esm/Retention.js +139 -0
  424. package/dist/esm/Retention.js.map +1 -0
  425. package/dist/esm/RunCatalogRead.d.ts +17 -0
  426. package/dist/esm/RunCatalogRead.d.ts.map +1 -0
  427. package/dist/esm/RunCatalogRead.js +17 -0
  428. package/dist/esm/RunCatalogRead.js.map +1 -0
  429. package/dist/esm/RunChangeFeed.d.ts +75 -0
  430. package/dist/esm/RunChangeFeed.d.ts.map +1 -0
  431. package/dist/esm/RunChangeFeed.js +81 -0
  432. package/dist/esm/RunChangeFeed.js.map +1 -0
  433. package/dist/esm/RunState.d.ts +116 -0
  434. package/dist/esm/RunState.d.ts.map +1 -0
  435. package/dist/esm/RunState.js +109 -0
  436. package/dist/esm/RunState.js.map +1 -0
  437. package/dist/esm/Selection.d.ts +495 -0
  438. package/dist/esm/Selection.d.ts.map +1 -0
  439. package/dist/esm/Selection.js +482 -0
  440. package/dist/esm/Selection.js.map +1 -0
  441. package/dist/esm/SelectionStore.d.ts +141 -0
  442. package/dist/esm/SelectionStore.d.ts.map +1 -0
  443. package/dist/esm/SelectionStore.js +215 -0
  444. package/dist/esm/SelectionStore.js.map +1 -0
  445. package/dist/esm/StepBoundary.d.ts +501 -0
  446. package/dist/esm/StepBoundary.d.ts.map +1 -0
  447. package/dist/esm/StepBoundary.js +919 -0
  448. package/dist/esm/StepBoundary.js.map +1 -0
  449. package/dist/esm/StepCacheFold.d.ts +148 -0
  450. package/dist/esm/StepCacheFold.d.ts.map +1 -0
  451. package/dist/esm/StepCacheFold.js +201 -0
  452. package/dist/esm/StepCacheFold.js.map +1 -0
  453. package/dist/esm/StepSandbox.d.ts +78 -0
  454. package/dist/esm/StepSandbox.d.ts.map +1 -0
  455. package/dist/esm/StepSandbox.js +70 -0
  456. package/dist/esm/StepSandbox.js.map +1 -0
  457. package/dist/esm/WakeBus.d.ts +116 -0
  458. package/dist/esm/WakeBus.d.ts.map +1 -0
  459. package/dist/esm/WakeBus.js +121 -0
  460. package/dist/esm/WakeBus.js.map +1 -0
  461. package/dist/esm/WorkspaceSandbox.d.ts +619 -0
  462. package/dist/esm/WorkspaceSandbox.d.ts.map +1 -0
  463. package/dist/esm/WorkspaceSandbox.js +985 -0
  464. package/dist/esm/WorkspaceSandbox.js.map +1 -0
  465. package/dist/esm/index.d.ts +157 -0
  466. package/dist/esm/index.d.ts.map +1 -0
  467. package/dist/esm/index.js +157 -0
  468. package/dist/esm/index.js.map +1 -0
  469. package/dist/esm/internal/ActionPersistence.d.ts +276 -0
  470. package/dist/esm/internal/ActionPersistence.d.ts.map +1 -0
  471. package/dist/esm/internal/ActionPersistence.js +2306 -0
  472. package/dist/esm/internal/ActionPersistence.js.map +1 -0
  473. package/dist/esm/internal/ArtifactRoots.d.ts +38 -0
  474. package/dist/esm/internal/ArtifactRoots.d.ts.map +1 -0
  475. package/dist/esm/internal/ArtifactRoots.js +76 -0
  476. package/dist/esm/internal/ArtifactRoots.js.map +1 -0
  477. package/dist/esm/internal/AttemptAdmission.d.ts +53 -0
  478. package/dist/esm/internal/AttemptAdmission.d.ts.map +1 -0
  479. package/dist/esm/internal/AttemptAdmission.js +52 -0
  480. package/dist/esm/internal/AttemptAdmission.js.map +1 -0
  481. package/dist/esm/internal/AttemptLifecycle.d.ts +245 -0
  482. package/dist/esm/internal/AttemptLifecycle.d.ts.map +1 -0
  483. package/dist/esm/internal/AttemptLifecycle.js +104 -0
  484. package/dist/esm/internal/AttemptLifecycle.js.map +1 -0
  485. package/dist/esm/internal/AttemptProbe.d.ts +53 -0
  486. package/dist/esm/internal/AttemptProbe.d.ts.map +1 -0
  487. package/dist/esm/internal/AttemptProbe.js +83 -0
  488. package/dist/esm/internal/AttemptProbe.js.map +1 -0
  489. package/dist/esm/internal/CacheAdmission.d.ts +79 -0
  490. package/dist/esm/internal/CacheAdmission.d.ts.map +1 -0
  491. package/dist/esm/internal/CacheAdmission.js +72 -0
  492. package/dist/esm/internal/CacheAdmission.js.map +1 -0
  493. package/dist/esm/internal/CacheAgeHistory.d.ts +27 -0
  494. package/dist/esm/internal/CacheAgeHistory.d.ts.map +1 -0
  495. package/dist/esm/internal/CacheAgeHistory.js +49 -0
  496. package/dist/esm/internal/CacheAgeHistory.js.map +1 -0
  497. package/dist/esm/internal/CacheAgeVerdicts.d.ts +15 -0
  498. package/dist/esm/internal/CacheAgeVerdicts.d.ts.map +1 -0
  499. package/dist/esm/internal/CacheAgeVerdicts.js +54 -0
  500. package/dist/esm/internal/CacheAgeVerdicts.js.map +1 -0
  501. package/dist/esm/internal/CacheOutputPolicy.d.ts +24 -0
  502. package/dist/esm/internal/CacheOutputPolicy.d.ts.map +1 -0
  503. package/dist/esm/internal/CacheOutputPolicy.js +73 -0
  504. package/dist/esm/internal/CacheOutputPolicy.js.map +1 -0
  505. package/dist/esm/internal/CachePublication.d.ts +97 -0
  506. package/dist/esm/internal/CachePublication.d.ts.map +1 -0
  507. package/dist/esm/internal/CachePublication.js +116 -0
  508. package/dist/esm/internal/CachePublication.js.map +1 -0
  509. package/dist/esm/internal/CallFacts.d.ts +15 -0
  510. package/dist/esm/internal/CallFacts.d.ts.map +1 -0
  511. package/dist/esm/internal/CallFacts.js +83 -0
  512. package/dist/esm/internal/CallFacts.js.map +1 -0
  513. package/dist/esm/internal/CopiedRecord.d.ts +61 -0
  514. package/dist/esm/internal/CopiedRecord.d.ts.map +1 -0
  515. package/dist/esm/internal/CopiedRecord.js +165 -0
  516. package/dist/esm/internal/CopiedRecord.js.map +1 -0
  517. package/dist/esm/internal/DeferredPersistence.d.ts +108 -0
  518. package/dist/esm/internal/DeferredPersistence.d.ts.map +1 -0
  519. package/dist/esm/internal/DeferredPersistence.js +240 -0
  520. package/dist/esm/internal/DeferredPersistence.js.map +1 -0
  521. package/dist/esm/internal/EffectRecords.d.ts +92 -0
  522. package/dist/esm/internal/EffectRecords.d.ts.map +1 -0
  523. package/dist/esm/internal/EffectRecords.js +94 -0
  524. package/dist/esm/internal/EffectRecords.js.map +1 -0
  525. package/dist/esm/internal/EngineJj.d.ts +30 -0
  526. package/dist/esm/internal/EngineJj.d.ts.map +1 -0
  527. package/dist/esm/internal/EngineJj.js +29 -0
  528. package/dist/esm/internal/EngineJj.js.map +1 -0
  529. package/dist/esm/internal/EngineStateSchema.d.ts +68 -0
  530. package/dist/esm/internal/EngineStateSchema.d.ts.map +1 -0
  531. package/dist/esm/internal/EngineStateSchema.js +53 -0
  532. package/dist/esm/internal/EngineStateSchema.js.map +1 -0
  533. package/dist/esm/internal/ExecutionSnapshotRead.d.ts +162 -0
  534. package/dist/esm/internal/ExecutionSnapshotRead.d.ts.map +1 -0
  535. package/dist/esm/internal/ExecutionSnapshotRead.js +218 -0
  536. package/dist/esm/internal/ExecutionSnapshotRead.js.map +1 -0
  537. package/dist/esm/internal/ExecutionSnapshotRelations.d.ts +39 -0
  538. package/dist/esm/internal/ExecutionSnapshotRelations.d.ts.map +1 -0
  539. package/dist/esm/internal/ExecutionSnapshotRelations.js +81 -0
  540. package/dist/esm/internal/ExecutionSnapshotRelations.js.map +1 -0
  541. package/dist/esm/internal/ExitEncoding.d.ts +180 -0
  542. package/dist/esm/internal/ExitEncoding.d.ts.map +1 -0
  543. package/dist/esm/internal/ExitEncoding.js +361 -0
  544. package/dist/esm/internal/ExitEncoding.js.map +1 -0
  545. package/dist/esm/internal/FileBoundarySnapshot.d.ts +14 -0
  546. package/dist/esm/internal/FileBoundarySnapshot.d.ts.map +1 -0
  547. package/dist/esm/internal/FileBoundarySnapshot.js +29 -0
  548. package/dist/esm/internal/FileBoundarySnapshot.js.map +1 -0
  549. package/dist/esm/internal/FileEnumeration.d.ts +127 -0
  550. package/dist/esm/internal/FileEnumeration.d.ts.map +1 -0
  551. package/dist/esm/internal/FileEnumeration.js +236 -0
  552. package/dist/esm/internal/FileEnumeration.js.map +1 -0
  553. package/dist/esm/internal/HostReflection.d.ts +80 -0
  554. package/dist/esm/internal/HostReflection.d.ts.map +1 -0
  555. package/dist/esm/internal/HostReflection.js +98 -0
  556. package/dist/esm/internal/HostReflection.js.map +1 -0
  557. package/dist/esm/internal/JournalRecords.d.ts +325 -0
  558. package/dist/esm/internal/JournalRecords.d.ts.map +1 -0
  559. package/dist/esm/internal/JournalRecords.js +306 -0
  560. package/dist/esm/internal/JournalRecords.js.map +1 -0
  561. package/dist/esm/internal/NodeJournal.d.ts +157 -0
  562. package/dist/esm/internal/NodeJournal.d.ts.map +1 -0
  563. package/dist/esm/internal/NodeJournal.js +298 -0
  564. package/dist/esm/internal/NodeJournal.js.map +1 -0
  565. package/dist/esm/internal/Ordering.d.ts +14 -0
  566. package/dist/esm/internal/Ordering.d.ts.map +1 -0
  567. package/dist/esm/internal/Ordering.js +14 -0
  568. package/dist/esm/internal/Ordering.js.map +1 -0
  569. package/dist/esm/internal/PlanInputs.d.ts +14 -0
  570. package/dist/esm/internal/PlanInputs.d.ts.map +1 -0
  571. package/dist/esm/internal/PlanInputs.js +57 -0
  572. package/dist/esm/internal/PlanInputs.js.map +1 -0
  573. package/dist/esm/internal/PlanMerges.d.ts +178 -0
  574. package/dist/esm/internal/PlanMerges.d.ts.map +1 -0
  575. package/dist/esm/internal/PlanMerges.js +113 -0
  576. package/dist/esm/internal/PlanMerges.js.map +1 -0
  577. package/dist/esm/internal/ProvenanceSlot.d.ts +30 -0
  578. package/dist/esm/internal/ProvenanceSlot.d.ts.map +1 -0
  579. package/dist/esm/internal/ProvenanceSlot.js +39 -0
  580. package/dist/esm/internal/ProvenanceSlot.js.map +1 -0
  581. package/dist/esm/internal/ResultEnvelope.d.ts +88 -0
  582. package/dist/esm/internal/ResultEnvelope.d.ts.map +1 -0
  583. package/dist/esm/internal/ResultEnvelope.js +83 -0
  584. package/dist/esm/internal/ResultEnvelope.js.map +1 -0
  585. package/dist/esm/internal/RetentionOps.d.ts +414 -0
  586. package/dist/esm/internal/RetentionOps.d.ts.map +1 -0
  587. package/dist/esm/internal/RetentionOps.js +576 -0
  588. package/dist/esm/internal/RetentionOps.js.map +1 -0
  589. package/dist/esm/internal/RunCatalogOps.d.ts +118 -0
  590. package/dist/esm/internal/RunCatalogOps.d.ts.map +1 -0
  591. package/dist/esm/internal/RunCatalogOps.js +120 -0
  592. package/dist/esm/internal/RunCatalogOps.js.map +1 -0
  593. package/dist/esm/internal/RunCoordinator.d.ts +78 -0
  594. package/dist/esm/internal/RunCoordinator.d.ts.map +1 -0
  595. package/dist/esm/internal/RunCoordinator.js +115 -0
  596. package/dist/esm/internal/RunCoordinator.js.map +1 -0
  597. package/dist/esm/internal/RunDriver.d.ts +192 -0
  598. package/dist/esm/internal/RunDriver.d.ts.map +1 -0
  599. package/dist/esm/internal/RunDriver.js +2334 -0
  600. package/dist/esm/internal/RunDriver.js.map +1 -0
  601. package/dist/esm/internal/RunLifecycle.d.ts +22 -0
  602. package/dist/esm/internal/RunLifecycle.d.ts.map +1 -0
  603. package/dist/esm/internal/RunLifecycle.js +49 -0
  604. package/dist/esm/internal/RunLifecycle.js.map +1 -0
  605. package/dist/esm/internal/RunListing.d.ts +78 -0
  606. package/dist/esm/internal/RunListing.d.ts.map +1 -0
  607. package/dist/esm/internal/RunListing.js +146 -0
  608. package/dist/esm/internal/RunListing.js.map +1 -0
  609. package/dist/esm/internal/RunListingError.d.ts +35 -0
  610. package/dist/esm/internal/RunListingError.d.ts.map +1 -0
  611. package/dist/esm/internal/RunListingError.js +27 -0
  612. package/dist/esm/internal/RunListingError.js.map +1 -0
  613. package/dist/esm/internal/RuntimeGraph.d.ts +73 -0
  614. package/dist/esm/internal/RuntimeGraph.d.ts.map +1 -0
  615. package/dist/esm/internal/RuntimeGraph.js +182 -0
  616. package/dist/esm/internal/RuntimeGraph.js.map +1 -0
  617. package/dist/esm/internal/SandboxedExecution.d.ts +80 -0
  618. package/dist/esm/internal/SandboxedExecution.d.ts.map +1 -0
  619. package/dist/esm/internal/SandboxedExecution.js +107 -0
  620. package/dist/esm/internal/SandboxedExecution.js.map +1 -0
  621. package/dist/esm/internal/StateTransaction.d.ts +44 -0
  622. package/dist/esm/internal/StateTransaction.d.ts.map +1 -0
  623. package/dist/esm/internal/StateTransaction.js +54 -0
  624. package/dist/esm/internal/StateTransaction.js.map +1 -0
  625. package/dist/esm/internal/StepFacts.d.ts +14 -0
  626. package/dist/esm/internal/StepFacts.d.ts.map +1 -0
  627. package/dist/esm/internal/StepFacts.js +35 -0
  628. package/dist/esm/internal/StepFacts.js.map +1 -0
  629. package/dist/esm/internal/TypedEvents.d.ts +14 -0
  630. package/dist/esm/internal/TypedEvents.d.ts.map +1 -0
  631. package/dist/esm/internal/TypedEvents.js +14 -0
  632. package/dist/esm/internal/TypedEvents.js.map +1 -0
  633. package/dist/esm/migrations/0001_initial.d.ts +17 -0
  634. package/dist/esm/migrations/0001_initial.d.ts.map +1 -0
  635. package/dist/esm/migrations/0001_initial.js +41 -0
  636. package/dist/esm/migrations/0001_initial.js.map +1 -0
  637. package/dist/esm/migrations/0002_selection_store.d.ts +15 -0
  638. package/dist/esm/migrations/0002_selection_store.d.ts.map +1 -0
  639. package/dist/esm/migrations/0002_selection_store.js +34 -0
  640. package/dist/esm/migrations/0002_selection_store.js.map +1 -0
  641. package/dist/esm/migrations/0003_plan_inputs.d.ts +13 -0
  642. package/dist/esm/migrations/0003_plan_inputs.d.ts.map +1 -0
  643. package/dist/esm/migrations/0003_plan_inputs.js +70 -0
  644. package/dist/esm/migrations/0003_plan_inputs.js.map +1 -0
  645. package/dist/esm/migrations/0004_plan_environment.d.ts +14 -0
  646. package/dist/esm/migrations/0004_plan_environment.d.ts.map +1 -0
  647. package/dist/esm/migrations/0004_plan_environment.js +33 -0
  648. package/dist/esm/migrations/0004_plan_environment.js.map +1 -0
  649. package/dist/esm/migrations/0005_plan_merges.d.ts +14 -0
  650. package/dist/esm/migrations/0005_plan_merges.d.ts.map +1 -0
  651. package/dist/esm/migrations/0005_plan_merges.js +59 -0
  652. package/dist/esm/migrations/0005_plan_merges.js.map +1 -0
  653. package/dist/esm/migrations/0006_execution_listing.d.ts +15 -0
  654. package/dist/esm/migrations/0006_execution_listing.d.ts.map +1 -0
  655. package/dist/esm/migrations/0006_execution_listing.js +81 -0
  656. package/dist/esm/migrations/0006_execution_listing.js.map +1 -0
  657. package/dist/esm/migrations/0007_run_parent_sequence.d.ts +15 -0
  658. package/dist/esm/migrations/0007_run_parent_sequence.d.ts.map +1 -0
  659. package/dist/esm/migrations/0007_run_parent_sequence.js +18 -0
  660. package/dist/esm/migrations/0007_run_parent_sequence.js.map +1 -0
  661. package/dist/esm/migrations/0008_deferred_consumption.d.ts +16 -0
  662. package/dist/esm/migrations/0008_deferred_consumption.d.ts.map +1 -0
  663. package/dist/esm/migrations/0008_deferred_consumption.js +24 -0
  664. package/dist/esm/migrations/0008_deferred_consumption.js.map +1 -0
  665. package/dist/esm/migrations/0009_deferred_clock_facts.d.ts +24 -0
  666. package/dist/esm/migrations/0009_deferred_clock_facts.d.ts.map +1 -0
  667. package/dist/esm/migrations/0009_deferred_clock_facts.js +127 -0
  668. package/dist/esm/migrations/0009_deferred_clock_facts.js.map +1 -0
  669. package/dist/esm/migrations/0010_pending_cancellation.d.ts +15 -0
  670. package/dist/esm/migrations/0010_pending_cancellation.d.ts.map +1 -0
  671. package/dist/esm/migrations/0010_pending_cancellation.js +19 -0
  672. package/dist/esm/migrations/0010_pending_cancellation.js.map +1 -0
  673. package/dist/esm/test/TestStores.d.ts +89 -0
  674. package/dist/esm/test/TestStores.d.ts.map +1 -0
  675. package/dist/esm/test/TestStores.js +103 -0
  676. package/dist/esm/test/TestStores.js.map +1 -0
  677. package/docs/README.md +113 -0
  678. package/docs/api.md +1494 -0
  679. package/docs/concepts/attempts-and-replay.md +149 -0
  680. package/docs/concepts/cache-admission.md +265 -0
  681. package/docs/concepts/call-facts.md +86 -0
  682. package/docs/concepts/durable-waits.md +175 -0
  683. package/docs/concepts/execution-facts.md +90 -0
  684. package/docs/concepts/ownership-and-fencing.md +128 -0
  685. package/docs/concepts/step-boundaries.md +148 -0
  686. package/docs/concepts/workspace-transactions.md +199 -0
  687. package/docs/event-types.md +14 -0
  688. package/docs/guides/back-up-and-restore.md +164 -0
  689. package/docs/guides/collect-unreferenced-artifacts.md +128 -0
  690. package/docs/guides/compose-a-durable-engine.md +146 -0
  691. package/docs/guides/coordinate-two-processes.md +120 -0
  692. package/docs/guides/defer-work-with-selection.md +171 -0
  693. package/docs/guides/delete-old-run-history.md +154 -0
  694. package/docs/guides/drive-a-plan.md +314 -0
  695. package/docs/guides/observe-engine-metrics.md +98 -0
  696. package/docs/guides/observe-executions.md +187 -0
  697. package/docs/guides/reclaim-runs-from-a-dead-host.md +133 -0
  698. package/docs/guides/share-a-cache-across-machines.md +134 -0
  699. package/docs/guides/testing.md +123 -0
  700. package/docs/installation.md +102 -0
  701. package/docs/quickstart.md +215 -0
  702. package/docs/troubleshooting.md +347 -0
  703. package/package.json +438 -3
  704. package/src/ArtifactGc.ts +408 -0
  705. package/src/ArtifactSync.ts +278 -0
  706. package/src/CacheSync.ts +131 -0
  707. package/src/DeferredClockFold.ts +586 -0
  708. package/src/DisasterRecovery.ts +888 -0
  709. package/src/DurableEngineState.ts +2656 -0
  710. package/src/EngineStore.ts +656 -0
  711. package/src/EngineStoreMetrics.ts +331 -0
  712. package/src/Errors.ts +28 -0
  713. package/src/EventTypes.ts +27 -0
  714. package/src/ExecutionFacts.ts +125 -0
  715. package/src/ExecutionSnapshot.ts +186 -0
  716. package/src/Inconsistency.ts +253 -0
  717. package/src/Migrations.ts +95 -0
  718. package/src/OwnerIdentity.ts +125 -0
  719. package/src/PlanInputStore.ts +292 -0
  720. package/src/PlanMergeStore.ts +330 -0
  721. package/src/PlanScheduler.ts +1806 -0
  722. package/src/Reconciliation.ts +224 -0
  723. package/src/ReplayOnly.ts +95 -0
  724. package/src/Retention.ts +214 -0
  725. package/src/RunCatalogRead.ts +27 -0
  726. package/src/RunChangeFeed.ts +125 -0
  727. package/src/RunState.ts +137 -0
  728. package/src/Selection.ts +683 -0
  729. package/src/SelectionStore.ts +353 -0
  730. package/src/StepBoundary.ts +1165 -0
  731. package/src/StepCacheFold.ts +244 -0
  732. package/src/StepSandbox.ts +110 -0
  733. package/src/WakeBus.ts +167 -0
  734. package/src/WorkspaceSandbox.ts +1604 -0
  735. package/src/index.ts +185 -0
  736. package/src/internal/ActionPersistence.ts +2687 -0
  737. package/src/internal/ArtifactRoots.ts +92 -0
  738. package/src/internal/AttemptAdmission.ts +76 -0
  739. package/src/internal/AttemptLifecycle.ts +163 -0
  740. package/src/internal/AttemptProbe.ts +90 -0
  741. package/src/internal/CacheAdmission.ts +133 -0
  742. package/src/internal/CacheAgeHistory.ts +58 -0
  743. package/src/internal/CacheAgeVerdicts.ts +61 -0
  744. package/src/internal/CacheOutputPolicy.ts +82 -0
  745. package/src/internal/CachePublication.ts +153 -0
  746. package/src/internal/CallFacts.ts +87 -0
  747. package/src/internal/CopiedRecord.ts +201 -0
  748. package/src/internal/DeferredPersistence.ts +459 -0
  749. package/src/internal/EffectRecords.ts +136 -0
  750. package/src/internal/EngineJj.ts +30 -0
  751. package/src/internal/EngineStateSchema.ts +92 -0
  752. package/src/internal/ExecutionSnapshotRead.ts +300 -0
  753. package/src/internal/ExecutionSnapshotRelations.ts +121 -0
  754. package/src/internal/ExitEncoding.ts +453 -0
  755. package/src/internal/FileBoundarySnapshot.ts +39 -0
  756. package/src/internal/FileEnumeration.ts +334 -0
  757. package/src/internal/HostReflection.ts +128 -0
  758. package/src/internal/JournalRecords.ts +396 -0
  759. package/src/internal/NodeJournal.ts +340 -0
  760. package/src/internal/Ordering.ts +14 -0
  761. package/src/internal/PlanInputs.ts +62 -0
  762. package/src/internal/PlanMerges.ts +123 -0
  763. package/src/internal/ProvenanceSlot.ts +52 -0
  764. package/src/internal/ResultEnvelope.ts +118 -0
  765. package/src/internal/RetentionOps.ts +865 -0
  766. package/src/internal/RunCatalogOps.ts +174 -0
  767. package/src/internal/RunCoordinator.ts +215 -0
  768. package/src/internal/RunDriver.ts +3154 -0
  769. package/src/internal/RunLifecycle.ts +53 -0
  770. package/src/internal/RunListing.ts +210 -0
  771. package/src/internal/RunListingError.ts +37 -0
  772. package/src/internal/RuntimeGraph.ts +253 -0
  773. package/src/internal/SandboxedExecution.ts +165 -0
  774. package/src/internal/StateTransaction.ts +71 -0
  775. package/src/internal/StepFacts.ts +36 -0
  776. package/src/internal/TypedEvents.ts +29 -0
  777. package/src/migrations/0001_initial.ts +51 -0
  778. package/src/migrations/0002_selection_store.ts +38 -0
  779. package/src/migrations/0003_plan_inputs.ts +75 -0
  780. package/src/migrations/0004_plan_environment.ts +34 -0
  781. package/src/migrations/0005_plan_merges.ts +60 -0
  782. package/src/migrations/0006_execution_listing.ts +86 -0
  783. package/src/migrations/0007_run_parent_sequence.ts +19 -0
  784. package/src/migrations/0008_deferred_consumption.ts +27 -0
  785. package/src/migrations/0009_deferred_clock_facts.ts +163 -0
  786. package/src/migrations/0010_pending_cancellation.ts +20 -0
  787. package/src/test/TestStores.ts +145 -0
@@ -0,0 +1,2334 @@
1
+ /**
2
+ * Claim-gated durable flow run lifecycle.
3
+ *
4
+ * @since 0.1.0
5
+ */
6
+ import * as CapabilitySet from "@smthrs/capability/CapabilitySet";
7
+ import { DatabaseError, DurableWriter } from "@smthrs/database/DurableWriter";
8
+ import { FlowEngine } from "@smthrs/engine";
9
+ import { Flow, FlowRuntime } from "@smthrs/flow";
10
+ import { Journal } from "@smthrs/journal";
11
+ import { Ownership, RunStore } from "@smthrs/run-store";
12
+ import * as Cause from "effect/Cause";
13
+ import * as Clock from "effect/Clock";
14
+ import * as Deferred from "effect/Deferred";
15
+ import * as Duration from "effect/Duration";
16
+ import * as Effect from "effect/Effect";
17
+ import * as Exit from "effect/Exit";
18
+ import * as Metric from "effect/Metric";
19
+ import * as Option from "effect/Option";
20
+ import * as Result from "effect/Result";
21
+ import * as Schema from "effect/Schema";
22
+ import * as Scope from "effect/Scope";
23
+ import * as DurableEngineState from "../DurableEngineState.js";
24
+ import * as EngineStoreMetrics from "../EngineStoreMetrics.js";
25
+ import { EventTypes } from "../EventTypes.js";
26
+ import * as ExecutionFacts from "../ExecutionFacts.js";
27
+ import { RunState } from "../RunState.js";
28
+ import * as WakeBus from "../WakeBus.js";
29
+ import * as ActionPersistence from "./ActionPersistence.js";
30
+ import * as EffectRecords from "./EffectRecords.js";
31
+ import * as ExitEncoding from "./ExitEncoding.js";
32
+ import * as JournalRecords from "./JournalRecords.js";
33
+ import * as RunCoordinator from "./RunCoordinator.js";
34
+ import * as RunLifecycle from "./RunLifecycle.js";
35
+ import * as StateTransaction from "./StateTransaction.js";
36
+ const RunStateJson = Schema.fromJsonString(RunState);
37
+ /**
38
+ * Re-channels every typed reason of a cause as a defect, identity intact.
39
+ *
40
+ * A boundary whose declared error type cannot describe what happened has two
41
+ * honest options: widen the type, or stop calling the failure typed. Casting
42
+ * the cause is the third, dishonest one — it left a raw `DatabaseError`
43
+ * escaping an `Effect<void, CancelRequestFailed>`. Demotion keeps the error
44
+ * object, every other reason and every interruption, and no caller can
45
+ * mistake the result for a failure its handler declared.
46
+ */
47
+ const asDefects = (cause) => Cause.fromReasons(cause.reasons.map((reason) => Cause.isFailReason(reason)
48
+ ? Cause.makeDieReason(reason.error).annotate(Cause.reasonAnnotations(reason))
49
+ : reason));
50
+ /**
51
+ * Raised when a flow (directly or through mutual ancestry) attempts to
52
+ * execute an execution id that already appears in its own persisted
53
+ * `parentExecutionId` chain.
54
+ *
55
+ * Detection walks the already-persisted parent chain from the requesting
56
+ * parent upward — an O(depth) check, not a dependency-graph DFS — because
57
+ * `parentExecutionId` is the only edge our runtime model can express.
58
+ *
59
+ * The class is declared by `@smthrs/flow` (it is part of the `execute`
60
+ * contract) and re-exported here for the detector's callers.
61
+ *
62
+ * @since 0.1.0
63
+ * @category errors
64
+ */
65
+ export const FlowCycleDetected = FlowRuntime.FlowCycleDetected;
66
+ /**
67
+ * The effect kind a detached child spawn is journaled under, and therefore the
68
+ * kind an engine composition registers a spawn-compensation handler for.
69
+ *
70
+ * @since 0.1.0
71
+ * @category constants
72
+ */
73
+ export const spawnEffectKind = EventTypes.childSpawnKind;
74
+ const snapshot = (row) => ({
75
+ status: row.status,
76
+ owner: row.owner,
77
+ heartbeatAtMs: row.heartbeatAtMs
78
+ });
79
+ const isPlainObject = (value) => {
80
+ if (value === null || typeof value !== "object")
81
+ return false;
82
+ const prototype = Object.getPrototypeOf(value);
83
+ return prototype === Object.prototype || prototype === null;
84
+ };
85
+ /**
86
+ * Compares encoded JSON payloads structurally. Object key order is not part
87
+ * of JSON identity, while array order and array length are.
88
+ *
89
+ * Values outside JSON compare unequal unless both arguments are the same
90
+ * reference.
91
+ *
92
+ * @since 1.0.0
93
+ * @category guards
94
+ * @private
95
+ */
96
+ export const samePayload = (left, right) => {
97
+ if (Object.is(left, right))
98
+ return true;
99
+ if (typeof left === "number" && typeof right === "number") {
100
+ return left === 0 && right === 0;
101
+ }
102
+ if (Array.isArray(left)) {
103
+ if (!Array.isArray(right) || left.length !== right.length)
104
+ return false;
105
+ for (let index = 0; index < left.length; index++) {
106
+ if (!samePayload(left[index], right[index]))
107
+ return false;
108
+ }
109
+ return true;
110
+ }
111
+ if (Array.isArray(right) || !isPlainObject(left) || !isPlainObject(right))
112
+ return false;
113
+ const leftKeys = Object.keys(left);
114
+ const rightKeys = Object.keys(right);
115
+ if (leftKeys.length !== rightKeys.length)
116
+ return false;
117
+ for (const key of leftKeys) {
118
+ if (!Object.hasOwn(right, key) || !samePayload(left[key], right[key]))
119
+ return false;
120
+ }
121
+ return true;
122
+ };
123
+ /**
124
+ * Sentinel produced when the cancel-request poll observes a durable
125
+ * cancellation before the flow settles.
126
+ */
127
+ const cancelRequested = { _tag: "CancelRequested" };
128
+ /**
129
+ * How many stale-running rows one heartbeat tick may wake (issue #79).
130
+ * Oldest heartbeats surface first, so a backlog larger than the batch
131
+ * drains across ticks; each successful steal removes the row from the
132
+ * stale window, and losing drivers see a shrinking batch next tick.
133
+ */
134
+ const staleRunningSweepBatch = 64;
135
+ /**
136
+ * How long a run waits before its live owner is probed again, and how far
137
+ * that wait doubles.
138
+ *
139
+ * A refusal is evidence: the recorded owner answered, so the one thing the
140
+ * sweep knows is that re-asking immediately is waste. Without a wait the
141
+ * oldest batch is re-probed every heartbeat forever and nothing behind it is
142
+ * ever reached (issue #79 follow-up). The first wait is two ticks, which is
143
+ * already enough for the batch to move on, and it doubles up to the lease
144
+ * window: an owner that stops heartbeating is therefore re-probed within one
145
+ * `heartbeatStaleAfter` of the last refusal, which is the same window every
146
+ * other reclaim decision is made against.
147
+ */
148
+ const stealRefusalBackoffFirst = Duration.times(Ownership.heartbeatInterval, 2);
149
+ const stealRefusalBackoffMax = Ownership.heartbeatStaleAfter;
150
+ const withoutResult = (state) => {
151
+ const { cancellation: _, result: __, ...rest } = state;
152
+ return rest;
153
+ };
154
+ /**
155
+ * Constructs a scoped run driver.
156
+ *
157
+ * Every start and wake enters the same keyed coordinator and then the same
158
+ * exact-snapshot claim/activation path.
159
+ *
160
+ * @since 0.1.0
161
+ * @category constructors
162
+ */
163
+ export const make = (dependencies) => Effect.gen(function* () {
164
+ const journal = yield* Journal.Journal;
165
+ const writer = yield* DurableWriter;
166
+ const store = yield* RunStore.RunStore;
167
+ const engineState = yield* DurableEngineState.DurableEngineState;
168
+ const lifecycle = RunLifecycle.make(store, engineState);
169
+ const wakeBus = dependencies.wakeBus ?? WakeBus.makeNoop();
170
+ /**
171
+ * The deployment's own liveness answer, if it gave one.
172
+ *
173
+ * Kept separate from {@link isAlive} because the two differ in what they
174
+ * PROVE, and the journal says which was used: a supplied probe reports on
175
+ * the process (or the host it lives on), while the default reports only
176
+ * that the lease it stopped renewing has expired.
177
+ */
178
+ const suppliedIsAlive = dependencies.isAlive;
179
+ const isAlive = suppliedIsAlive ??
180
+ Ownership.leaseLiveness(Ownership.heartbeatStaleAfter);
181
+ /**
182
+ * The evidence kind a steal admitted by {@link isAlive} carries.
183
+ *
184
+ * `steal` is only reached for a run whose lease has already expired, so
185
+ * `lease-expired` is always true here; what the supplied-probe kinds add
186
+ * is WHY the deployment believes the owner is gone, which is the
187
+ * distinction `RunStore` binds to the host relation.
188
+ */
189
+ const livenessEvidenceKind = (expectedOwner) => suppliedIsAlive === undefined
190
+ ? "lease-expired"
191
+ : Ownership.sameHostIncarnation(expectedOwner, dependencies.owner)
192
+ ? "same-host-pid-dead"
193
+ : "cross-host-unreachable-stale";
194
+ /** Owner identity equality, the fence's own notion of "the same process". */
195
+ const sameOwner = (left, right) => left.hostId === right.hostId && left.pid === right.pid && left.nonce === right.nonce;
196
+ const registrations = new Map();
197
+ /**
198
+ * Runs already warned about waking without a registered flow (issue
199
+ * #62): the sweep retries every heartbeat, so the warning is emitted
200
+ * once per run, not once per tick. Cleared on every registration — a
201
+ * newly registered flow makes previously dropped runs drivable again.
202
+ */
203
+ const warnedUnregistered = new Set();
204
+ /**
205
+ * Runs whose owner answered a probe (or held a fresh lease) and are
206
+ * therefore not worth re-arbitrating yet. See {@link StealRefusal}.
207
+ *
208
+ * Bounded by the stale set the sweep can see: every tick drops the entries
209
+ * whose rows are no longer in the window it read, so a run that is stolen,
210
+ * released, or settled takes its entry with it.
211
+ */
212
+ const stealRefusals = new Map();
213
+ /**
214
+ * Records that a run's recorded owner was found alive, and when it may be
215
+ * asked about again.
216
+ */
217
+ const deferSteal = (runId, heartbeatAtMs, nowMs) => {
218
+ const previous = stealRefusals.get(runId);
219
+ // A moved lease is new evidence about a different state of the world, so
220
+ // it starts the ladder over instead of inheriting the old wait.
221
+ const attempt = previous !== undefined && previous.heartbeatAtMs === heartbeatAtMs ? previous.attempt + 1 : 1;
222
+ const waitMs = Math.min(Duration.toMillis(stealRefusalBackoffFirst) * Math.pow(2, attempt - 1), Duration.toMillis(stealRefusalBackoffMax));
223
+ stealRefusals.set(runId, { heartbeatAtMs, nextProbeAtMs: nowMs + waitMs, attempt });
224
+ };
225
+ /**
226
+ * The journal address of one refusal.
227
+ *
228
+ * Refusals were emitted through the driver's default source id with no
229
+ * `sourceSeq`, so nothing deduplicated them: a live owner cost one durable
230
+ * journal row per refused run per heartbeat, indefinitely. Addressing the
231
+ * record by the run, the owner, and the lease it was refused against makes
232
+ * the journal's own first-writer admission the deduplication — the same
233
+ * mechanism the registration-time wake records use — so a repeated refusal
234
+ * about an unchanged lease says nothing twice.
235
+ *
236
+ * The evidence is part of the address because one lease can be refused for
237
+ * two different reasons, and they are different records. A wake that
238
+ * arrives while the owner is still pulsing is refused by the lease alone
239
+ * (`lease-fresh`); the same owner, still alive but stalled past the
240
+ * window, is refused by the probe (`probe`). Addressing both to one key
241
+ * made the second write a different payload under the first one's address,
242
+ * which the journal rejects as `idempotency_conflict` — a defect that
243
+ * killed the drive fiber, dropped the probe refusal, and took down any
244
+ * `execute` joined to that run. One address per (run, owner, lease,
245
+ * evidence) keeps every reason sayable and still admits each of them once.
246
+ *
247
+ * That makes this driver's own payload a pure function of its address, so
248
+ * it can no longer conflict with itself. It is not the only writer at
249
+ * these addresses, and a conflict raised by another one is no longer fatal
250
+ * either: refusals are emitted through {@link emitEvidence}.
251
+ */
252
+ const stealRefusedSourceId = (owner, heartbeatAtMs, evidence) => `${dependencies.journalSource}:steal-refused:${evidence}:${JSON.stringify([owner.hostId, owner.pid, owner.nonce])}:${heartbeatAtMs}`;
253
+ const liveInstances = new Map();
254
+ /**
255
+ * Flow scopes retained past a round's settlement.
256
+ *
257
+ * `Flow.intoResult` closes the flow scope for every settlement EXCEPT a
258
+ * suspension — a parked flow keeps its scope so the resources its body
259
+ * acquired outlive the park (`packages/smithers/flows/flow/src/Flow/Runtime.ts`). Nothing
260
+ * then owned that scope: the instance left `liveInstances` on the way out
261
+ * and the `Scope.Closeable` became unreachable, so a cancelled parked flow
262
+ * never ran its finalizers and every park leaked one scope for the
263
+ * process's lifetime.
264
+ *
265
+ * The map is that owner. It holds at most one scope per run — an entry is
266
+ * a state, not a queue — and every removal goes through
267
+ * `releaseRetainedScope`, which deletes before it closes. That makes the
268
+ * close idempotent by construction rather than by a flag: a second caller
269
+ * finds no entry and does nothing, so concurrent cancel, resume, and
270
+ * shutdown paths cannot double-finalize.
271
+ */
272
+ const retainedScopes = new Map();
273
+ /**
274
+ * Closes a run's retained flow scope exactly once, if it has one.
275
+ *
276
+ * `exit` decides what the finalizers see. A cancellation closes with an
277
+ * interrupt exit so `Flow.withRollback` compensations run; a supersede
278
+ * (the run re-entered execution under a fresh instance and scope) closes
279
+ * with `Exit.void` so ordinary finalizers release resources while
280
+ * rollbacks correctly stay out — a re-driven round is not an unsuccessful
281
+ * exit.
282
+ */
283
+ const releaseRetainedScope = (runId, exit) => Effect.suspend(() => {
284
+ const scope = retainedScopes.get(runId);
285
+ if (scope === undefined)
286
+ return Effect.void;
287
+ retainedScopes.delete(runId);
288
+ return Scope.close(scope, exit).pipe(Effect.catchCause((cause) => Effect.logWarning(`engine-store: finalizers of parked run ${runId} failed while closing its retained flow scope`, cause)));
289
+ });
290
+ /**
291
+ * Snapshots the runs whose flow scope this driver is currently holding.
292
+ * Exposed for the leak assertions in `packages/smithers/flows/engine-store/test`, which
293
+ * would otherwise have to reach into driver internals.
294
+ */
295
+ const retainedRuns = Effect.sync(() => new Set(retainedScopes.keys()));
296
+ // Process shutdown must not strand parked scopes either: closing the
297
+ // driver's own scope releases every scope still retained. `Exit.void`,
298
+ // because a shutdown is a reclaimable release (issue #26) and not a
299
+ // cancellation, so compensations must not fire.
300
+ yield* Effect.addFinalizer(() => Effect.forEach(Array.from(retainedScopes.keys()), (runId) => releaseRetainedScope(runId, Exit.void), { discard: true }));
301
+ const encodeState = (state) => Schema.encodeEffect(RunStateJson)(state).pipe(Effect.orDie);
302
+ const decodeState = (stateJson) => Schema.decodeUnknownEffect(RunStateJson)(stateJson).pipe(Effect.orDie);
303
+ /**
304
+ * Tells the host that this driver is asking a PARKED execution to resume.
305
+ *
306
+ * Only a parked one. A wake against a run that is still running is
307
+ * already covered by the round it is in, and a request recorded for it
308
+ * would be a delegation no park ever takes up: the host polls it for as
309
+ * long as the run lives, and every poll pays a park wait for a run that
310
+ * is not parked.
311
+ *
312
+ * See {@link Dependencies.requestResume} for which wakes reach here.
313
+ */
314
+ const recordResume = (executionId, parked, reason) => dependencies.requestResume === undefined || !parked
315
+ ? Effect.void
316
+ : dependencies.requestResume(executionId, reason);
317
+ /**
318
+ * {@link recordResume} for the parent a settling child is waking.
319
+ *
320
+ * The waiting row is the park: it is written in the same transaction as
321
+ * the suspended transition and cleared by the claim that ends it, so a
322
+ * parent that has one is parked and a parent mid-round has none. It is
323
+ * read only for a host that records, so a composition without one pays
324
+ * nothing per child settlement.
325
+ */
326
+ const recordParentResume = (parentExecutionId) => dependencies.requestResume === undefined ? Effect.void : Effect.flatMap(engineState.waiting(parentExecutionId), (waiting) => recordResume(parentExecutionId, Option.isSome(waiting) && waiting.value.reason !== "quarantine", "parent"));
327
+ /**
328
+ * Run decisions are lifecycle records: they take the journal's durable
329
+ * channel so a saturated lossy queue can never drop them (issue #10).
330
+ * They stay ownerless because several decisions — `claim-lost`,
331
+ * `steal-refused-owner-alive`, post-transition `transitioned` — are
332
+ * legitimately recorded by a process that does not (or no longer does)
333
+ * own the run; the ownership fence for these paths is the run-row CAS
334
+ * that precedes each emit.
335
+ *
336
+ * `meta.lineageId` is a JOURNAL lineage id (`FlowEngine.Lineage`,
337
+ * `<runId>/root`), because that is the space a time-travel frame addresses:
338
+ * time travel (`packages/smithers/flows/time-travel/docs/README.md`) makes a frame `(lineageId, seq)`, and
339
+ * replay skips an entry whose `meta.lineageId` names a different lineage.
340
+ * The run row's `lineageId` column is a different space — the TRAMPOLINE
341
+ * lineage of `packages/smithers/flows/engine/docs/concepts/trampoline-rounds.md`, round 0's execution
342
+ * id.
343
+ *
344
+ * DECIDED (2026-08-12): decisions address the journal lineage of the run
345
+ * that records them, one per round, the same lineage that run's attempt
346
+ * and snapshot records already carry. Reading the run row's trampoline
347
+ * lineage here put a run's decisions in a lineage its own attempts do not
348
+ * address, so a rewind of that run skipped them. The trampoline lineage is
349
+ * not lost: `created` and `handed-off` carry `lineageId` and
350
+ * `roundOrdinal` in the decision payload, which is what walks a whole
351
+ * trampoline chain.
352
+ */
353
+ const executionFacts = ExecutionFacts.make({
354
+ runs: store,
355
+ state: engineState,
356
+ journal,
357
+ sourceId: dependencies.journalSource
358
+ });
359
+ const requestCancellation = (runId, at) => executionFacts.requestCancel(runId, at).pipe(Effect.catchIf((error) => error._tag === "@smthrs/journal/JournalError" || error instanceof DatabaseError, Effect.die));
360
+ const decisionRecord = (runId, payload, sourceId) =>
361
+ // Lifecycle decisions normally use the unfenced channel: they commit in the SAME transaction
362
+ // as the store-level owner CAS that is its fence (`transitionOwned`,
363
+ // `activate`, `claim`/`steal` outcomes), and by then the run is often no
364
+ // longer `running` under this owner, which is the exact predicate the
365
+ // journal fence asserts. Several call sites (claim-lost,
366
+ // steal-refused-owner-alive, activation-lost) also record decisions for
367
+ // runs this driver never owned at all — first-writer-wins evidence,
368
+ // which is what the unfenced channel exists for. Lease reconfirmation
369
+ // retains ownership and emits this record through the fenced channel.
370
+ JournalRecords.runDecision({
371
+ runId,
372
+ lineageId: FlowEngine.Lineage.root(runId),
373
+ sourceId,
374
+ ...(sourceId === dependencies.journalSource ? {} : { sourceSeq: 0 })
375
+ }, payload);
376
+ /**
377
+ * Completes every pending clock of a settled run and records each
378
+ * completion, in the caller's transaction. A run that settles before its
379
+ * timer fires still has to leave the journal saying the clock is done, or
380
+ * a rebuilt deadline index would arm it again (issue #2037).
381
+ */
382
+ const completeRunClocks = (runId, completedAtMs) => Effect.flatMap(engineState.completeRunClocks(runId, completedAtMs), (completed) => Effect.forEach(completed, (clock) => journal.emitDurableUnfenced(JournalRecords.clockCompletedFor(dependencies.journalSource, FlowEngine.Lineage.root(runId), clock, completedAtMs)), { discard: true }));
383
+ /**
384
+ * Records a decision this driver's own write transaction is responsible
385
+ * for: the `transitioned` record that commits with its run-row CAS, and
386
+ * the `claimed-and-activated` / `stolen-and-activated` record that commits
387
+ * with the activation CAS.
388
+ *
389
+ * These stay fatal. They take the driver's default source id, so the
390
+ * journal allocates them a fresh `sourceSeq` and no address can collide;
391
+ * a failure here is the storage layer failing under a transaction whose
392
+ * other half is a run-row write, and the transaction has to take the
393
+ * driver down with it rather than commit a terminal row whose decision
394
+ * never reached the journal.
395
+ */
396
+ const emitDecision = (runId, payload, sourceId = dependencies.journalSource) => Effect.gen(function* () {
397
+ const decision = payload;
398
+ const authoritative = sourceId === dependencies.journalSource && decision.decision !== "wake-scheduled" &&
399
+ decision.decision !== "child-policy-applied";
400
+ const recorded = authoritative
401
+ ? {
402
+ ...decision,
403
+ executionFact: yield* executionFacts.fact(runId, decision.decision === "created" ? "created" : "legacy")
404
+ }
405
+ : payload;
406
+ yield* journal.emitDurableUnfenced(decisionRecord(runId, recorded, sourceId));
407
+ }).pipe(Effect.orDie);
408
+ /**
409
+ * Records evidence ABOUT a run this driver does not own, and never raises
410
+ * on the drive fiber.
411
+ *
412
+ * `claim-lost`, `activation-lost` and `steal-refused-owner-alive` are the
413
+ * three decisions taken about a run another process holds. They are
414
+ * observations, not commitments: the arbitration they describe already
415
+ * happened in the store, the caller has already lost, and the very next
416
+ * thing every one of these call sites does is answer `false`. Routing them
417
+ * through {@link emitDecision} put an audit write on the failure path of
418
+ * the loop that drives every run in the process — an `idempotency_conflict`
419
+ * on a shared refusal address killed the drive fiber, dropped the record
420
+ * anyway, and took down any `execute` joined to that run.
421
+ *
422
+ * So the receipt is folded into a log, the shape `ActionPersistence`'s
423
+ * `emitConverging` uses for the same reason on the replay branches:
424
+ *
425
+ * - `Accepted`: the observation is durable. Nothing to say.
426
+ * - `Duplicate`: an identical observation is already durable — the
427
+ * per-heartbeat deduplication the refusal address exists for. Debug.
428
+ * - `idempotency_conflict`: this address already holds a DIFFERENT
429
+ * observation, so someone (another process, an earlier evidence shape,
430
+ * a copied journal after a time-travel fork) said something else about
431
+ * the same (run, owner, lease, evidence). The durable record wins and
432
+ * the divergence is worth a warning, but it is still only evidence.
433
+ *
434
+ * Any other journal failure is folded the same way and for the same
435
+ * reason: an audit record must never be the thing that stops a driver
436
+ * from driving. A journal that is genuinely broken still fails the very
437
+ * next decision write that IS a fence, which is where it belongs.
438
+ */
439
+ const emitEvidence = (runId, payload, sourceId = dependencies.journalSource) => journal.emitDurableUnfenced(decisionRecord(runId, payload, sourceId)).pipe(Effect.flatMap((receipt) => receipt._tag === "Duplicate"
440
+ ? Effect.logDebug(`engine-store: run ${runId} already recorded this evidence`, { runId, sourceId, seq: receipt.seq })
441
+ : Effect.void), Effect.catch((error) => Effect.logWarning(`engine-store: evidence about run ${runId} was not admitted to the journal (${error.code}); the run is unaffected`, { runId, sourceId, code: error.code, payload })));
442
+ /**
443
+ * The package's ordered state-and-journal boundary: engine state first,
444
+ * the journal's write transaction inside it. See
445
+ * {@link StateTransaction.make} for the order and what it guarantees.
446
+ */
447
+ const transactState = StateTransaction.make(engineState, journal);
448
+ /**
449
+ * Commits a run-row transition and the decision describing it in ONE write
450
+ * transaction, reporting the store outcome.
451
+ *
452
+ * `RunStore` and the journal write through the same `DurableWriter`, so the CAS
453
+ * becomes a savepoint of this transaction: a crash can no longer leave a
454
+ * terminal run row whose `transitioned` decision never reached the
455
+ * journal, nor a decision for a CAS that lost. Temporal commits mutable
456
+ * state and its history events as one persistence request
457
+ * (`reference/temporal/service/history/workflow/transaction_impl.go`);
458
+ * this is the same unit of work for a run transition.
459
+ *
460
+ * The decision is emitted only for a `Transitioned` outcome: a lost CAS
461
+ * changed nothing, and its `claim-lost`/`activation-lost` records are
462
+ * emitted by the caller, outside the transaction, so they survive.
463
+ *
464
+ * `waiting` parks before the CAS releases ownership, inside this same
465
+ * transaction. A refused park or transition FAILS the transaction, so SQL
466
+ * rolls back and the memory state's serialized snapshot is restored too.
467
+ * Only outside both boundaries is the refusal returned as an outcome.
468
+ * No former owner reads or clears a replacement's waiting marker.
469
+ *
470
+ * `afterTransitioned` is the seam for work that must commit WITH the
471
+ * transition or not at all: today, ending the run's linked children on a
472
+ * terminal exit. It runs only after a successful CAS, so a lost fence
473
+ * touches nothing, and inside the transaction, so a crash between the two
474
+ * writes rolls both back.
475
+ */
476
+ const transitionAndRecord = (runId, toStatus, stateJson, decision, guard, afterTransitioned, waiting) => transactState(Effect.gen(function* () {
477
+ if (waiting !== undefined && waiting !== null) {
478
+ const parked = yield* engineState.park(runId, waiting, dependencies.owner);
479
+ if (parked._tag === "NotFound") {
480
+ return yield* Effect.fail({ _tag: "TransitionRefused", outcome: parked });
481
+ }
482
+ }
483
+ const transitioned = yield* store.transitionOwned(runId, dependencies.owner, toStatus, stateJson, guard).pipe(Effect.orDie);
484
+ if (transitioned._tag !== "Transitioned") {
485
+ return yield* Effect.fail({ _tag: "TransitionRefused", outcome: transitioned });
486
+ }
487
+ if (waiting === null) {
488
+ yield* engineState.wake(runId);
489
+ }
490
+ if (afterTransitioned !== undefined) {
491
+ yield* afterTransitioned;
492
+ }
493
+ // Every terminal round closes its own deadlines, including a
494
+ // handoff whose successor and linked children continue separately.
495
+ if (toStatus === "completed" || toStatus === "failed") {
496
+ yield* completeRunClocks(runId, yield* Clock.currentTimeMillis.pipe(Effect.map(Math.floor)));
497
+ }
498
+ // The decision carries the state it committed, so run state at a
499
+ // frame is DERIVED by replaying decisions rather than read off the
500
+ // run row's current `state_json`
501
+ // (Temporal's
502
+ // `ndc/state_rebuilder.go` is the model). Without it a fork at an
503
+ // early frame silently inherited the parent's *latest* state.
504
+ yield* emitDecision(runId, { ...decision, state: JSON.parse(stateJson) });
505
+ return transitioned;
506
+ })).pipe(Effect.catchTag("TransitionRefused", ({ outcome }) => Effect.succeed(outcome)), Effect.orDie);
507
+ const abandon = (runId, claimedAtMs) => store.abandonClaim(runId, dependencies.owner, claimedAtMs).pipe(Effect.asVoid, Effect.orDie);
508
+ const claimAndActivate = Effect.fn("RunDriver.claimAndActivate")(function* (row) {
509
+ return yield* Effect.gen(function* () {
510
+ yield* Effect.annotateCurrentSpan({ runId: row.runId, status: row.status });
511
+ if (dependencies.canExecute !== undefined && !(yield* dependencies.canExecute(row))) {
512
+ return false;
513
+ }
514
+ if (row.status === "completed" || row.status === "failed" || row.status === "cancelled") {
515
+ yield* Effect.annotateCurrentSpan({ outcome: "terminal" });
516
+ yield* Metric.update(EngineStoreMetrics.claim.Terminal, 1);
517
+ return false;
518
+ }
519
+ // Every drive, including a fresh execute or a queued wake, must
520
+ // preserve quarantine. Cancellation still claims the row to close it.
521
+ if (row.cancelRequestedAtMs === null) {
522
+ const waiting = yield* engineState.waiting(row.runId);
523
+ if (Option.isSome(waiting) && waiting.value.reason === "quarantine") {
524
+ return false;
525
+ }
526
+ }
527
+ const expected = snapshot(row);
528
+ const nowMs = yield* Clock.currentTimeMillis.pipe(Effect.map(Math.floor));
529
+ let claim;
530
+ let stealEvidenceKind;
531
+ let recoveredClaim;
532
+ if (row.status === "running") {
533
+ if (row.owner === null ||
534
+ row.heartbeatAtMs === null ||
535
+ row.heartbeatAtMs >= nowMs - Duration.toMillis(Ownership.heartbeatStaleAfter)) {
536
+ // The lease is the refusal, and no probe is consulted for it: a
537
+ // probe may be a remote call, and a fresh heartbeat already
538
+ // answers the question. It is journaled anyway — but only when
539
+ // another owner holds the run, because refusing to steal from
540
+ // ourselves is routine re-entry rather than arbitration — so an
541
+ // operator whose `resume` did nothing can see that a live owner
542
+ // was in the way (issue #53 follow-up).
543
+ if (row.owner !== null && !sameOwner(row.owner, dependencies.owner)) {
544
+ deferSteal(row.runId, row.heartbeatAtMs, nowMs);
545
+ yield* emitEvidence(row.runId, {
546
+ decision: "steal-refused-owner-alive",
547
+ evidence: "lease-fresh",
548
+ expectedOwner: row.owner,
549
+ heartbeatAtMs: row.heartbeatAtMs
550
+ }, stealRefusedSourceId(row.owner, row.heartbeatAtMs, "lease-fresh"));
551
+ }
552
+ yield* Effect.annotateCurrentSpan({ outcome: "heartbeat_fresh" });
553
+ yield* Metric.update(EngineStoreMetrics.claim.HeartbeatFresh, 1);
554
+ return false;
555
+ }
556
+ if (yield* isAlive(row.owner, { claimant: dependencies.owner, heartbeatAtMs: row.heartbeatAtMs, nowMs })) {
557
+ deferSteal(row.runId, row.heartbeatAtMs, nowMs);
558
+ yield* emitEvidence(row.runId, {
559
+ decision: "steal-refused-owner-alive",
560
+ // Always a supplied probe. The default check is the lease, and
561
+ // the lease already refused above for every row that could still
562
+ // be fresh, so reaching this arm means the deployment answered
563
+ // for the owner with something the store cannot see.
564
+ evidence: "probe",
565
+ expectedOwner: row.owner,
566
+ heartbeatAtMs: row.heartbeatAtMs
567
+ }, stealRefusedSourceId(row.owner, row.heartbeatAtMs, "probe"));
568
+ yield* Effect.annotateCurrentSpan({ outcome: "steal_refused_owner_alive" });
569
+ yield* Metric.update(EngineStoreMetrics.claim.StealRefusedOwnerAlive, 1);
570
+ yield* Effect.logDebug("run steal refused, recorded owner is alive", {
571
+ runId: row.runId,
572
+ heartbeatAtMs: row.heartbeatAtMs
573
+ });
574
+ return false;
575
+ }
576
+ stealEvidenceKind = livenessEvidenceKind(row.owner);
577
+ claim = yield* store.steal(row.runId, expected, dependencies.owner, nowMs, {
578
+ expectedOwner: row.owner,
579
+ checkedAtMs: nowMs,
580
+ kind: stealEvidenceKind
581
+ }).pipe(Effect.orDie);
582
+ }
583
+ else {
584
+ // A process that died between `claim` and `activate` leaves its
585
+ // claim on a parked or pending row, and every later claim loses to
586
+ // it: the run could never be driven again. A claim older than the
587
+ // lease whose claimant is gone is released first, by the same
588
+ // liveness rule a steal applies to a running owner.
589
+ if (row.claim !== null && row.claimedAtMs !== null && !sameOwner(row.claim, dependencies.owner) &&
590
+ row.claimedAtMs < nowMs - Duration.toMillis(Ownership.heartbeatStaleAfter) &&
591
+ !(yield* isAlive(row.claim, { claimant: dependencies.owner, heartbeatAtMs: row.claimedAtMs, nowMs }))) {
592
+ const recovered = yield* store.recoverClaim(row.runId, row.claim, row.claimedAtMs, dependencies.owner, nowMs, { expectedOwner: row.claim, checkedAtMs: nowMs, kind: livenessEvidenceKind(row.claim) }).pipe(Effect.orDie);
593
+ if (recovered._tag === "Recovered") {
594
+ recoveredClaim = { claimant: row.claim, claimedAtMs: row.claimedAtMs };
595
+ }
596
+ }
597
+ claim = yield* store.claim(row.runId, expected, dependencies.owner, nowMs).pipe(Effect.orDie);
598
+ }
599
+ if (claim._tag !== "Claimed") {
600
+ yield* emitEvidence(row.runId, {
601
+ decision: "claim-lost",
602
+ outcome: claim._tag,
603
+ expected
604
+ });
605
+ yield* Effect.annotateCurrentSpan({ outcome: "claim_lost" });
606
+ yield* Metric.update(EngineStoreMetrics.claim.ClaimLost, 1);
607
+ yield* Effect.logDebug("run claim lost", { runId: row.runId, outcome: claim._tag });
608
+ return false;
609
+ }
610
+ // The activation CAS and the decision recording it commit together:
611
+ // a crash between them left a run durably running under this owner
612
+ // with no journal entry saying who took it.
613
+ const activation = yield* transactState(Effect.gen(function* () {
614
+ // Admission can change without changing the ownership snapshot
615
+ // (a released→running→released ABA, rewind, or consent change).
616
+ // Recheck its read-only native fence under the writer transaction;
617
+ // loading modules and probing external hosts remain preflight work.
618
+ if (dependencies.canActivate !== undefined &&
619
+ !(yield* dependencies.canActivate(yield* store.get(row.runId).pipe(Effect.orDie)))) {
620
+ return { _tag: "SnapshotChanged" };
621
+ }
622
+ // Another owner can quarantine and return to the same suspended
623
+ // snapshot between preflight and claim. Fence that park in the
624
+ // activation transaction too; the rejected claim is abandoned below.
625
+ if (row.cancelRequestedAtMs === null) {
626
+ const waiting = yield* engineState.waiting(row.runId);
627
+ if (Option.isSome(waiting) && waiting.value.reason === "quarantine") {
628
+ return { _tag: "SnapshotChanged" };
629
+ }
630
+ }
631
+ const activation = yield* store.activate(row.runId, dependencies.owner, claim.claimedAtMs, expected).pipe(Effect.orDie);
632
+ if (activation._tag !== "Activated") {
633
+ return activation;
634
+ }
635
+ yield* emitDecision(row.runId, {
636
+ decision: row.status === "running" ? "stolen-and-activated" : "claimed-and-activated",
637
+ previousStatus: row.status,
638
+ owner: dependencies.owner,
639
+ // What admitted the takeover, so an operator reading the journal
640
+ // can tell a lease that simply expired from a probe that
641
+ // positively reported the owner gone.
642
+ ...(stealEvidenceKind === undefined ? {} : { evidence: stealEvidenceKind }),
643
+ ...(recoveredClaim === undefined ? {} : { recoveredClaim })
644
+ });
645
+ return activation;
646
+ })).pipe(Effect.orDie);
647
+ if (activation._tag !== "Activated") {
648
+ yield* abandon(row.runId, claim.claimedAtMs);
649
+ yield* emitEvidence(row.runId, {
650
+ decision: "activation-lost",
651
+ outcome: activation._tag,
652
+ expected
653
+ });
654
+ yield* Effect.annotateCurrentSpan({ outcome: "activation_lost" });
655
+ yield* Metric.update(EngineStoreMetrics.claim.ActivationLost, 1);
656
+ yield* Effect.logDebug("run activation lost", { runId: row.runId, outcome: activation._tag });
657
+ return false;
658
+ }
659
+ yield* Effect.annotateCurrentSpan({ outcome: "activated" });
660
+ yield* Metric.update(EngineStoreMetrics.claim.Activated, 1);
661
+ // The run is this driver's now, so whatever it was told about the
662
+ // previous owner is spent.
663
+ stealRefusals.delete(row.runId);
664
+ return true;
665
+ });
666
+ });
667
+ /**
668
+ * Collects other rounds and transitive descendants of the logical run.
669
+ *
670
+ * Round membership and the edge table represent the logical job that every
671
+ * owner process and every restart can see (issues #40/#41), which is
672
+ * exactly why the cascade reads it instead of an in-process instance map:
673
+ * a cross-process cancellation is observed by a driver that never spawned
674
+ * the children and holds no `FlowInstance` for any of them.
675
+ *
676
+ * Deduplicated by run id, so the diamond the edge set permits is visited
677
+ * once and a (rejected, but defensively handled) cycle terminates. Work and
678
+ * memory are bounded by the finite durable edge set; there is deliberately
679
+ * no arbitrary fan-out cap, because truncating a wide generation would
680
+ * leave linked children uncancelled forever.
681
+ */
682
+ const descendantsOf = lifecycle.descendants;
683
+ /**
684
+ * Records a durable cancellation request against every linked descendant
685
+ * of a cancelled run.
686
+ *
687
+ * `requestCancel` is unfenced and first-writer-wins, so this is idempotent
688
+ * by construction: re-running the cascade — a re-drive, a second operator
689
+ * cancel, a child that also cascades from its own cancellation — writes
690
+ * nothing the second time and answers `AlreadyRequested`. A row that no
691
+ * longer exists answers `NotFound`, which is equally fine.
692
+ *
693
+ * `cancelOwned` calls it inside the same transaction as the parent's
694
+ * terminal `cancelled` transition, so a crash cannot commit a cancelled
695
+ * parent whose children were never asked to stop.
696
+ *
697
+ * Both callers record the run's OWN cancellation BEFORE this walk —
698
+ * `interrupt` writes the request, `cancelOwned` commits the terminal
699
+ * `cancelled` transition — and that order is load-bearing rather than
700
+ * incidental. A late admission inherits by reading the parent row after it
701
+ * has created the child row (`inheritParentCancellation`), so a walk that
702
+ * ran before the parent was marked could miss an edge whose admission also
703
+ * missed the mark. Both the SQL engine state and
704
+ * `DurableEngineState.makeMemory` serialize their transactions. The memory
705
+ * implementation snapshots and restores its maps on failure; the shared
706
+ * writer transaction supplies the run-store write boundary. The ordering
707
+ * also lets a child admitted within the cascade inherit cancellation.
708
+ */
709
+ const requestCancelDescendants = (runId, nowMs) => Effect.gen(function* () {
710
+ const descendants = yield* descendantsOf(runId);
711
+ yield* Effect.forEach(descendants, (childId) => requestCancellation(childId, nowMs), { discard: true });
712
+ return descendants;
713
+ });
714
+ /** The exit policy a child recorded for itself when it was spawned. */
715
+ const exitPolicyOf = (row) => decodeState(row.stateJson).pipe(
716
+ // A row written before the policy existed, or created outside
717
+ // `ensureRun` (a seeded edge, an external admission), names none. It
718
+ // is a LINKED child either way, and the contract for a linked child is
719
+ // that it ends with the run it belongs to; `detach` is the deliberate
720
+ // exception and has to be recorded to hold.
721
+ Effect.map((state) => state.onParentExit ?? "cancel"));
722
+ /**
723
+ * Applies the per-child exit policy when a run ends on its own terms.
724
+ *
725
+ * Cancellation is not this path. A cancelled run cascades to every linked
726
+ * descendant unconditionally (`requestCancelDescendants`): an operator who
727
+ * cancels a run is asking for the subtree to stop, and a child cannot opt
728
+ * out of that. What this closes is the other half — a parent that
729
+ * COMPLETED or FAILED used to leave every linked child running, so a
730
+ * `.child()` boundary whose parent died on an unrelated branch left a
731
+ * durable orphan nothing would ever wait for or cancel.
732
+ *
733
+ * Three shapes are left alone, and each for its own reason:
734
+ *
735
+ * - A child that recorded `detach` was spawned fire-and-forget. Outliving
736
+ * the parent is the whole point, so it is reported, not touched, and its
737
+ * own subtree is not walked — those runs hang off a run that is still
738
+ * going.
739
+ * - A child that already settled has nothing to cancel, and it applied
740
+ * this same policy to its own children when it settled.
741
+ * - A child whose row is gone leaves only an edge.
742
+ *
743
+ * The requests are written by the caller's transaction — the parent's
744
+ * terminal compare-and-swap — so a crash cannot commit a completed parent
745
+ * whose children were never asked to stop.
746
+ */
747
+ const endLinkedChildren = (runId, nowMs) => Effect.gen(function* () {
748
+ const roots = (yield* lifecycle.rounds(runId)).map((row) => row.runId);
749
+ const seen = new Set(roots);
750
+ const cancelled = [];
751
+ const detached = [];
752
+ let frontier = roots;
753
+ while (frontier.length > 0) {
754
+ const next = [];
755
+ for (const parentId of frontier) {
756
+ for (const edge of yield* engineState.runChildren(parentId)) {
757
+ if (seen.has(edge.childId))
758
+ continue;
759
+ const rounds = yield* lifecycle.rounds(edge.childId);
760
+ for (const round of rounds)
761
+ seen.add(round.runId);
762
+ seen.add(edge.childId);
763
+ const row = rounds.find((round) => round.status !== "completed" && round.status !== "failed" && round.status !== "cancelled");
764
+ if (row === undefined)
765
+ continue;
766
+ // The edge belongs to the creating round. Its ownership policy
767
+ // survives the child's handoffs; do not infer it from a later
768
+ // round written by a version that omitted onParentExit.
769
+ // A nonempty lineage read includes its requested member.
770
+ if ((yield* exitPolicyOf(rounds.find((round) => round.runId === edge.childId))) === "detach") {
771
+ detached.push(edge.childId);
772
+ continue;
773
+ }
774
+ for (const round of rounds) {
775
+ if (round.status !== "completed" && round.status !== "failed" && round.status !== "cancelled") {
776
+ cancelled.push(round.runId);
777
+ }
778
+ next.push(round.runId);
779
+ }
780
+ }
781
+ }
782
+ frontier = next;
783
+ }
784
+ yield* Effect.forEach(cancelled, (childId) => requestCancellation(childId, nowMs), { discard: true });
785
+ return { cancelled, detached };
786
+ });
787
+ /**
788
+ * The effect a terminal transition runs inside its own transaction to end
789
+ * the children it is responsible for, journaling what it decided.
790
+ *
791
+ * The record is emitted only when there was a linked child to decide
792
+ * about, so an ordinary childless run adds nothing to its journal.
793
+ */
794
+ const applyChildExitPolicy = (runId) => Effect.gen(function* () {
795
+ const nowMs = yield* Clock.currentTimeMillis.pipe(Effect.map(Math.floor));
796
+ const outcome = yield* endLinkedChildren(runId, nowMs).pipe(Effect.orDie);
797
+ if (outcome.cancelled.length === 0 && outcome.detached.length === 0)
798
+ return;
799
+ yield* emitDecision(runId, {
800
+ decision: "child-policy-applied",
801
+ appliedAtMs: nowMs,
802
+ cancelled: outcome.cancelled,
803
+ detached: outcome.detached
804
+ });
805
+ });
806
+ /**
807
+ * Carries a parent's durable cancellation onto a child that was linked
808
+ * AFTER the parent's cascade had already run.
809
+ *
810
+ * Admission and cancellation are two separate serialized write
811
+ * transactions over the same store, so they interleave in both orders and
812
+ * neither order may leak a live child (issue #83):
813
+ *
814
+ * - Admission first. The edge is committed before the cancellation
815
+ * transaction opens, so `requestCancelDescendants` sees the child and
816
+ * requests it. Nothing more is needed here.
817
+ * - Cancellation first. The cascade walked `flows_run_parents` while the
818
+ * edge did not exist yet, so it could not have seen this child — and
819
+ * the parent's drive may already be gone, so nothing will walk the
820
+ * edge table again on its behalf. The child would be created fresh and
821
+ * uncancelled, and would run to completion under a cancelled parent.
822
+ *
823
+ * The second order is closed by reading the parent row inside the SAME
824
+ * outer storage transaction that recorded the edge and created the child
825
+ * row, after the child row exists. Either the cancellation transaction
826
+ * committed before this read, in which case the request is visible here
827
+ * and is inherited atomically with the child's own creation, or it has
828
+ * not committed yet, in which case it commits after this transaction and
829
+ * its own cascade sees the edge. There is no third interleaving, so no
830
+ * ephemeral hand-off flag and no timing-based retry is involved. The
831
+ * argument rests on the store's serialized writes and the order inside
832
+ * `requestCancelDescendants`: a cancellation marks its own run before it
833
+ * walks the edge table. Memory durable-state transactions also serialize
834
+ * access and restore their snapshots on failure; they do not provide
835
+ * persistence across process crashes.
836
+ *
837
+ * Nesting falls out of this: the inherited request is written to the
838
+ * child's own row, so a grandchild admitted later reads a cancel-requested
839
+ * parent and inherits in turn.
840
+ *
841
+ * `requestCancel` is unfenced and first-writer-wins, so a repeated
842
+ * admission writes nothing the second time; a parent row that is gone,
843
+ * or one with no rounds at all, inherits nothing. Both ways a parent can
844
+ * exit are inherited here, and they differ in whether the child's own
845
+ * `onParentExit` can spare it. An explicit cancellation in ANY round is
846
+ * inherited unconditionally, whether it is a pending
847
+ * `cancel_requested_at_ms` or a round already settled terminally
848
+ * `cancelled` whose request column a compaction could have cleared. A
849
+ * parent that instead stopped normally, with EVERY round `completed` or
850
+ * `failed`, cancels a late child that was admitted attached and leaves an
851
+ * `onParentExit === "detach"` child to outlive it.
852
+ *
853
+ * The child does not need its own cascade here: it was just created, so
854
+ * it has no descendants of its own, and once it observes the inherited
855
+ * request `cancelOwned` cascades from it like any other cancellation.
856
+ */
857
+ const inheritParentCancellation = (parentId, childId) => Effect.gen(function* () {
858
+ const rounds = yield* lifecycle.rounds(parentId).pipe(Effect.orDie);
859
+ if (rounds.length === 0)
860
+ return;
861
+ const cancelled = rounds.find((round) => round.cancelRequestedAtMs !== null || round.status === "cancelled");
862
+ if (cancelled !== undefined) {
863
+ // The parent's own request timestamp, so the inherited request reads
864
+ // as the same operator intent rather than as a later, independent one.
865
+ const nowMs = cancelled.cancelRequestedAtMs ?? (yield* Clock.currentTimeMillis.pipe(Effect.map(Math.floor)));
866
+ yield* requestCancellation(childId, nowMs).pipe(Effect.orDie);
867
+ return;
868
+ }
869
+ // The same interleaving, for the other way a parent stops: a parent
870
+ // that had already COMPLETED or FAILED ran `applyChildExitPolicy`
871
+ // before this edge existed, so its walk could not have seen this
872
+ // child and nothing will walk again on its behalf. The child applies
873
+ // its own recorded policy here instead, in the transaction that made
874
+ // it a child, so an attached child admitted a moment too late cannot
875
+ // run on under a parent that is already gone.
876
+ if (rounds.some((round) => round.status !== "completed" && round.status !== "failed"))
877
+ return;
878
+ // Admission reads the child's recorded policy in this transaction.
879
+ // A later caller awaiting a detached execution cannot change the
880
+ // policy chosen by its original spawn.
881
+ if ((yield* exitPolicyOf(yield* store.get(childId).pipe(Effect.orDie))) === "detach") {
882
+ return;
883
+ }
884
+ yield* requestCancellation(childId, yield* Clock.currentTimeMillis.pipe(Effect.map(Math.floor))).pipe(Effect.orDie);
885
+ });
886
+ const cancelOwned = (runId, state) => Effect.gen(function* () {
887
+ const interruptedAtMs = yield* Clock.currentTimeMillis.pipe(Effect.map(Math.floor));
888
+ const stateJson = yield* encodeState({
889
+ ...withoutResult(state),
890
+ cancellation: { interruptedAtMs }
891
+ });
892
+ let cascaded = [];
893
+ let settled = false;
894
+ yield* transactState(Effect.gen(function* () {
895
+ yield* store.acknowledgeCancel(runId, dependencies.owner, interruptedAtMs).pipe(Effect.orDie);
896
+ const transitioned = yield* store.transitionOwned(runId, dependencies.owner, "cancelled", stateJson).pipe(Effect.orDie);
897
+ if (transitioned._tag !== "Transitioned")
898
+ return;
899
+ settled = true;
900
+ // Cancellation cascades to linked children (`Flow.interrupt` is
901
+ // documented to preserve "child-flow handling"). It is recorded
902
+ // durably, in this transaction, off the durable edge table, so the
903
+ // cascade is identical whether the cancellation was requested in
904
+ // this process or observed from another one — the pre-existing
905
+ // in-process linkage in `FlowEngine.make` fires off the ephemeral
906
+ // `instance.interrupted` flag, which a durable observation never
907
+ // sets and a second driver instance never has.
908
+ cascaded = yield* requestCancelDescendants(runId, interruptedAtMs);
909
+ // The run has no future, so neither do its timers. Completing the
910
+ // clock rows in this transaction is what stops a durable timer
911
+ // from outliving the run it belongs to: the sweep queries already
912
+ // skip a settled run's rows, and this stops them being pending at
913
+ // all — including under an in-memory state with no run view to
914
+ // join against.
915
+ yield* completeRunClocks(runId, interruptedAtMs);
916
+ // A cancel can race the final poll after the run already parked
917
+ // (park precedes the guarded terminal CAS). Clear the waiting row so
918
+ // the terminally cancelled run never surfaces to a sweeper again
919
+ // (issue #28). It shares the transition's transaction, so a crash
920
+ // can no longer land between them.
921
+ yield* engineState.wake(runId).pipe(Effect.asVoid);
922
+ // Durable channel (issue #10): the interruption record must survive
923
+ // the process exiting right after cancellation. Unfenced because the
924
+ // `cancelled` transition above has already released ownership; the
925
+ // fence is the transition CAS itself, which now commits with this
926
+ // record rather than before it.
927
+ yield* journal.emitDurableUnfenced(JournalRecords.interrupted({
928
+ runId,
929
+ lineageId: FlowEngine.Lineage.root(runId),
930
+ sourceId: dependencies.journalSource
931
+ }, {
932
+ outcome: "cancelled",
933
+ interruptedAtMs,
934
+ owner: dependencies.owner,
935
+ executionFact: yield* executionFacts.fact(runId),
936
+ ...(cascaded.length === 0 ? {} : { cascadedTo: cascaded })
937
+ })).pipe(Effect.orDie);
938
+ })).pipe(Effect.orDie);
939
+ // Post-commit, and only reachable after a transition that actually
940
+ // happened. Closing the parked flow scope runs the finalizers the
941
+ // suspension retained, with an interrupt exit so `Flow.withRollback`
942
+ // compensations fire — cancellation is exactly the unsuccessful exit
943
+ // they exist for. The run is durably cancelled first, so a crash
944
+ // inside a finalizer cannot resurrect it.
945
+ yield* releaseRetainedScope(runId, Exit.failCause(Cause.interrupt()));
946
+ // Waking a cascaded child is in-process scheduling: a child this
947
+ // process owns or has parked re-enters claim/activate now and the
948
+ // activation cancel guard closes it, instead of waiting out a poll
949
+ // interval. A child owned elsewhere ignores the wake and is reached by
950
+ // its own owner's cancel poll or by that owner's parked-run sweep.
951
+ if (cascaded.length > 0) {
952
+ const activeCoordinator = yield* Deferred.await(coordinatorDeferred);
953
+ yield* Effect.forEach(cascaded, (childId) => activeCoordinator.wake(childId), { discard: true });
954
+ }
955
+ // A cancelled round is a terminal settlement like a completed or
956
+ // failed one, so every parent already parked on it — through any round
957
+ // of its lineage — is woken to read the cancellation (#2758). Only
958
+ // after the transition this call committed: a lost fence woke nobody.
959
+ if (settled) {
960
+ yield* announceSettled(runId, state);
961
+ }
962
+ });
963
+ /**
964
+ * Releases an interrupted run reclaimably instead of closing it
965
+ * (issue #26). A drive-fiber interruption is not evidence of operator
966
+ * cancellation — process shutdown closes the coordinator scope, and the
967
+ * heartbeat loop self-interrupts on any heartbeat error — so the run
968
+ * transitions back to `suspended` while the fence is still validly held,
969
+ * leaving it claimable by any worker (Temporal worker-shutdown
970
+ * semantics). On genuine fence loss the owned transition fails
971
+ * harmlessly.
972
+ */
973
+ /**
974
+ * The waiting reason an interrupted round parks under.
975
+ *
976
+ * A flow that has ALREADY asked to suspend is parked, not released. The
977
+ * round's own settlement derives the same three answers a few statements
978
+ * later ({@link drive}'s `settleRound`), and losing the race to a
979
+ * shutdown must not lose the classification with it: a `released` row
980
+ * carries no wake time and no token, so a run that suspended on a
981
+ * 150-second clock came back as a run whose owner merely went away, and
982
+ * the sweeper had to re-drive it blind instead of waking it when its
983
+ * deadline fell due (release rehearsal).
984
+ *
985
+ * A flow that has NOT asked to suspend is a genuine shutdown or heartbeat
986
+ * self-interrupt, and `released` is exactly what it is (issue #39).
987
+ */
988
+ const interruptedWaiting = (runId, instance) => Effect.gen(function* () {
989
+ if (instance === undefined || !instance.suspended)
990
+ return { reason: "released" };
991
+ const declared = instance.waiting;
992
+ if (declared !== undefined)
993
+ return declared;
994
+ const pendingClocks = yield* engineState.pendingClocks({ executionId: runId });
995
+ return pendingClocks.length > 0
996
+ ? {
997
+ reason: "timer",
998
+ wakeAt: Math.min(...pendingClocks.map((clock) => clock.dueAtMs))
999
+ }
1000
+ : { reason: "event" };
1001
+ });
1002
+ const releaseOwned = (runId, state, cause, round) => Effect.gen(function* () {
1003
+ // Park atomically with releasing ownership (`park` is owner-fenced). The
1004
+ // durable waiting row is what makes a released run visible to the
1005
+ // parked-run sweeper: without it nothing ever re-drives the run and
1006
+ // a durable `requestCancel` against it is write-only forever
1007
+ // (issue #39). On genuine fence loss the park reports NotFound
1008
+ // harmlessly, exactly like the transition below.
1009
+ const waiting = yield* interruptedWaiting(runId, round?.instance);
1010
+ // A run whose flow asked to suspend carries the suspension on its row,
1011
+ // exactly as the round's own settlement writes it. `poll` publishes a
1012
+ // state with no `result` as "not settled yet", so a park recorded
1013
+ // without one was invisible to every resumer: `AgentSession`'s
1014
+ // `awaitParked` polls `poll` and answers `unknown` for anything but a
1015
+ // published `Suspended`, so `smithers approve` and `smithers run
1016
+ // --resume` accepted the request and then drove nothing in the release rehearsal.
1017
+ const suspension = round !== undefined && waiting.reason !== "released"
1018
+ ? (yield* encodeResult(round.flow, new Flow.Suspended({ cause: round.instance.cause }))).encoded
1019
+ : undefined;
1020
+ yield* transitionAndRecord(runId, "suspended", yield* encodeState(suspension === undefined
1021
+ ? withoutResult(state)
1022
+ : { ...withoutResult(state), result: suspension }),
1023
+ // Why the run was released (#3328): an operator reading a parked run
1024
+ // must be able to tell a lapsed lease on a live host from a shutdown.
1025
+ { decision: "interrupt-released", owner: dependencies.owner, cause }, undefined, undefined, waiting);
1026
+ });
1027
+ /**
1028
+ * Discriminates an interruption cause by durable state: only an
1029
+ * interruption backed by a recorded cancel request closes the run;
1030
+ * anything else releases it for reclaim (issue #26), under the waiting
1031
+ * reason the flow had already declared for itself when it has one.
1032
+ */
1033
+ const settleInterrupted = (runId, state, cause, round) => store.get(runId).pipe(Effect.flatMap((row) => {
1034
+ // Shutdown can arrive after the claim CAS but before activation, or
1035
+ // after activation but before/after the execution race. Release only
1036
+ // this incarnation's persisted fence; a settled or foreign row stays
1037
+ // untouched, including its waiting reason.
1038
+ if (row.claim !== null && row.claimedAtMs !== null && sameOwner(row.claim, dependencies.owner)) {
1039
+ return abandon(runId, row.claimedAtMs);
1040
+ }
1041
+ if (row.status !== "running" || row.owner === null || !sameOwner(row.owner, dependencies.owner)) {
1042
+ return Effect.void;
1043
+ }
1044
+ return row.cancelRequestedAtMs !== null ? cancelOwned(runId, state) : releaseOwned(runId, state, cause, round);
1045
+ }), Effect.catch((error) => error.code === "not_found_row" ? Effect.void : Effect.die(error)));
1046
+ /**
1047
+ * Encodes a settlement for the row, and never fails.
1048
+ *
1049
+ * The encode used to be guarded with `Effect.orDie`, which made a
1050
+ * settlement the flow's own codec rejects fatal for the drain rather than
1051
+ * terminal for the run. `agent/run` declares both channels
1052
+ * `Schema.Unknown`, so every `Data.TaggedError` an agent body fails with
1053
+ * is such a settlement, and the release validation watched the whole
1054
+ * chain: the drain died with `SchemaError: Expected JSON value at
1055
+ * ["exit"]["cause"][0]["error"]`, `engine.db` kept the run `running`
1056
+ * under a pid that had exited while `control.db` said `failed`, and the
1057
+ * next process's stale-running sweep stole the row and billed the
1058
+ * provider seat a second time. `ExitEncoding.encode` fails closed
1059
+ * instead: on rejection it answers a JSON projection of the settlement
1060
+ * and a `note`, and a noted settlement is written `failed`.
1061
+ */
1062
+ const encodeResult = (flow, result) => ExitEncoding.encode(flow, result);
1063
+ /**
1064
+ * Re-encodes a handoff payload through the target flow's own codec, so the
1065
+ * bytes the next round's row holds are the bytes `ensureRun` would write
1066
+ * for the same invocation.
1067
+ */
1068
+ const normalizePayload = (flow, payload) => {
1069
+ const codec = Schema.toCodecJson(flow.payloadSchema);
1070
+ return Schema.decodeUnknownEffect(codec)(payload).pipe(Effect.orDie, Effect.flatMap((decoded) => Schema.encodeEffect(codec)(decoded)), Effect.orDie);
1071
+ };
1072
+ /**
1073
+ * Creates one run row, tolerating only an existing row with identical
1074
+ * execution identity and encoded invocation data.
1075
+ */
1076
+ const ensureCreatedRun = (options) => Effect.gen(function* () {
1077
+ const created = yield* store.create(options.executionId, options.stateJson, {
1078
+ ...(options.parentRunId === undefined ? {} : { parentRunId: options.parentRunId }),
1079
+ lineageId: options.lineageId,
1080
+ roundOrdinal: options.roundOrdinal
1081
+ }).pipe(Effect.exit);
1082
+ if (Exit.isSuccess(created)) {
1083
+ return yield* options.onCreated;
1084
+ }
1085
+ // A cause carrying no `Fail` reason — an interrupt-only cause, or a
1086
+ // bare defect — used to reach `Option.getOrThrow(Exit.findErrorOption(...))`,
1087
+ // which threw a raw `NoSuchElementError` defect and discarded the
1088
+ // original cause. A caller that interrupted the fiber while
1089
+ // `store.create` was inside its write transaction saw a crash rather
1090
+ // than the cancellation it asked for. `Cause.findFail` hands back
1091
+ // that residual cause typed `Cause<never>`, so re-raising it verbatim
1092
+ // keeps an interrupt an interrupt (issue #151).
1093
+ const failure = Cause.findFail(created.cause);
1094
+ if (Result.isFailure(failure)) {
1095
+ return yield* Effect.failCause(failure.failure);
1096
+ }
1097
+ const error = failure.success.error;
1098
+ if (!(error instanceof RunStore.RunStoreError) || error.code !== "constraint") {
1099
+ return yield* Effect.die(error);
1100
+ }
1101
+ const existing = yield* store.get(options.executionId).pipe(Effect.orDie);
1102
+ const persisted = yield* decodeState(existing.stateJson);
1103
+ const sameParent = options.parentRunId === undefined ||
1104
+ existing.parentRunId === options.parentRunId;
1105
+ const conflict = persisted.flowName !== options.flowName
1106
+ ? {
1107
+ field: "flow",
1108
+ expected: persisted.flowName,
1109
+ actual: options.flowName
1110
+ }
1111
+ : !samePayload(persisted.payload, options.payload)
1112
+ ? {
1113
+ field: "payload",
1114
+ expected: "the encoded payload the execution was admitted with",
1115
+ actual: "a different encoded payload"
1116
+ }
1117
+ : !FlowEngine.joinable(
1118
+ // A row with no persisted authority ran unrestricted, and is
1119
+ // refused further dispatch; only an unrestricted caller joins it.
1120
+ persisted.capabilityCeilings ?? [], options.capabilityCeilings)
1121
+ ? {
1122
+ // A join answers the result the admitted authority produced.
1123
+ field: "capabilities",
1124
+ expected: "a capability ceiling covering the one the execution was admitted with",
1125
+ actual: "a capability ceiling that does not cover it"
1126
+ }
1127
+ : existing.lineageId !== options.lineageId
1128
+ ? {
1129
+ field: "lineage",
1130
+ expected: existing.lineageId ?? "no lineage",
1131
+ actual: options.lineageId
1132
+ }
1133
+ : existing.roundOrdinal !== options.roundOrdinal
1134
+ ? {
1135
+ field: "round",
1136
+ expected: String(existing.roundOrdinal),
1137
+ actual: String(options.roundOrdinal)
1138
+ }
1139
+ : !sameParent
1140
+ ? {
1141
+ field: "parent",
1142
+ expected: existing.parentRunId ?? "no parent",
1143
+ // `!sameParent` implies the requested parent is present: an
1144
+ // omitted parent is deliberately compatible with any persisted
1145
+ // parent when joining an already-created root.
1146
+ actual: options.parentRunId
1147
+ }
1148
+ : undefined;
1149
+ if (conflict !== undefined) {
1150
+ return yield* Effect.die(new FlowEngine.ExecutionIdentityConflict({
1151
+ executionId: options.executionId,
1152
+ status: existing.status,
1153
+ ...conflict,
1154
+ message: `execution ${options.executionId} already belongs to ${conflict.field} identity ` +
1155
+ `${conflict.expected}; it cannot be reused for ${conflict.actual}`
1156
+ }));
1157
+ }
1158
+ });
1159
+ const coordinatorDeferred = yield* Deferred.make();
1160
+ /**
1161
+ * Observes a durably recorded cancellation request
1162
+ * (`RunStore.requestCancel` / `cancel_requested_at_ms`) from another
1163
+ * process. Polls on the heartbeat cadence — the request is unfenced, so
1164
+ * only the owner can act on it, and it must act within a poll interval
1165
+ * (issue #11). Completes when a request is observed; races against the
1166
+ * flow like the heartbeat loop.
1167
+ */
1168
+ const cancelPollLoop = (executionId) => Effect.gen(function* () {
1169
+ // Check-first: a request that raced in just before activation is
1170
+ // observed without waiting out a full heartbeat (issue #27).
1171
+ while (true) {
1172
+ const requested = yield* store.get(executionId).pipe(Effect.map((row) => row.cancelRequestedAtMs !== null), Effect.catch(() => Effect.succeed(false)));
1173
+ if (requested) {
1174
+ yield* store.acknowledgeCancel(executionId, dependencies.owner, yield* Clock.currentTimeMillis.pipe(Effect.map(Math.floor))).pipe(Effect.orDie);
1175
+ return cancelRequested;
1176
+ }
1177
+ yield* Effect.sleep(Ownership.heartbeatInterval);
1178
+ }
1179
+ });
1180
+ /**
1181
+ * Opens the next round, and closes this one, in ONE transaction.
1182
+ *
1183
+ * `ensureRun`'s parent-edge/run-row pairing (issue #80) is the precedent
1184
+ * and the reason: a crash between the two writes would leave a terminal
1185
+ * round whose successor was never created, and nothing would ever look for
1186
+ * it again — the lineage would end silently at a round that says it handed
1187
+ * off. The stores' own writes become savepoints of this transaction, so
1188
+ * either both commit or neither.
1189
+ *
1190
+ * The next round is a run row chained through the RESERVED
1191
+ * `parent_run_id` column, not a `flows_run_parents` edge: that table is
1192
+ * the subflow DAG cycle detection walks, and a round is the same run
1193
+ * continuing rather than a child being spawned
1194
+ * (`packages/smithers/flows/engine/docs/concepts/trampoline-rounds.md`).
1195
+ */
1196
+ const continueLineage = (seam, advanced) => Effect.gen(function* () {
1197
+ // The handoff payload travels encoded, and the next round's row has to
1198
+ // hold the same value `ensureRun` would write for it: the root caller
1199
+ // re-enters `execute` for this round, and its identical-create check
1200
+ // compares encoded payloads structurally, so object key order cannot
1201
+ // separate two encodings of the same value. The execution id itself
1202
+ // uses canonical RFC 8785 JSON, so the identity and payload checks
1203
+ // agree by construction.
1204
+ //
1205
+ // A target this process does not run has no codec to normalize
1206
+ // through, and the round is still created with the payload verbatim —
1207
+ // the same posture the wake path takes for an unregistered flow, and
1208
+ // the reason a lineage survives a worker that only knows some of its
1209
+ // legs.
1210
+ const target = registrations.get(seam.handoff.flow)?.at(-1);
1211
+ const payload = target === undefined
1212
+ ? seam.handoff.payload
1213
+ : yield* normalizePayload(target.flow, seam.handoff.payload);
1214
+ const nextCeilings = [
1215
+ // The execution guard rejects missing persisted authority before a handler can hand off.
1216
+ ...seam.state.capabilityCeilings,
1217
+ ...(seam.handoff.capabilityCeilings ?? [[]]),
1218
+ // The successor records its own declaration, as admission would, so
1219
+ // a later join compares against the authority it actually runs
1220
+ // under. A target this process does not run has none here; the root
1221
+ // caller's re-entry then carries a declaration the row lacks and is
1222
+ // refused rather than trusted.
1223
+ ...(target === undefined
1224
+ ? []
1225
+ : Flow.parseCapabilityCeilings(Flow.capabilityCeilings(target.flow.annotations)))
1226
+ ];
1227
+ const nextStateJson = yield* encodeState({
1228
+ version: 1,
1229
+ flowName: seam.handoff.flow,
1230
+ payload,
1231
+ capabilityCeilings: nextCeilings,
1232
+ ...(seam.state.parentExecutionId === undefined
1233
+ ? {}
1234
+ : { parentExecutionId: seam.state.parentExecutionId }),
1235
+ ...(seam.state.onParentExit === undefined ? {} : { onParentExit: seam.state.onParentExit }),
1236
+ ...(seam.state.maxRounds === undefined ? {} : { maxRounds: seam.state.maxRounds }),
1237
+ ...(seam.handoff.deadline === undefined ? {} : { deadline: seam.handoff.deadline })
1238
+ });
1239
+ const settledStateJson = yield* encodeState({
1240
+ ...seam.state,
1241
+ result: (yield* encodeResult(seam.flow, seam.handoff)).encoded
1242
+ });
1243
+ const cancelled = { _tag: "HandoffCancelled" };
1244
+ const fenceLost = { _tag: "HandoffFenceLost" };
1245
+ const committed = yield* Effect.result(transactState(Effect.gen(function* () {
1246
+ // The next round's id is DERIVED from (lineage, ordinal), so a
1247
+ // re-drive finds the exact row it already opened. Only an
1248
+ // identical create is tolerated; a collision in flow, payload,
1249
+ // parent, lineage, or ordinal is a defect.
1250
+ yield* ensureCreatedRun({
1251
+ flowName: seam.handoff.flow,
1252
+ executionId: advanced.executionId,
1253
+ stateJson: nextStateJson,
1254
+ payload,
1255
+ lineageId: advanced.round.rootExecutionId,
1256
+ roundOrdinal: advanced.round.ordinal,
1257
+ parentRunId: seam.executionId,
1258
+ capabilityCeilings: nextCeilings,
1259
+ onCreated: emitDecision(advanced.executionId, {
1260
+ decision: "created",
1261
+ state: JSON.parse(nextStateJson),
1262
+ lineageId: advanced.round.rootExecutionId,
1263
+ roundOrdinal: advanced.round.ordinal,
1264
+ parentExecutionId: seam.executionId
1265
+ })
1266
+ });
1267
+ // DECIDED (2026-08-11, pending review): a handed-off round settles
1268
+ // `completed`. The round did finish, and adding a `Continued`
1269
+ // status would widen every status reader for a distinction the
1270
+ // `handed-off` decision and lineage columns already record.
1271
+ //
1272
+ // DECIDED (2026-08-11, pending review): cancellation guards the
1273
+ // handoff transition. If it raced the last poll, failing the outer
1274
+ // transaction rolls successor creation back before the ordinary
1275
+ // cancellation path closes the owned round.
1276
+ const transitioned = yield* transitionAndRecord(seam.executionId, "completed", settledStateJson, {
1277
+ decision: "handed-off",
1278
+ status: "completed",
1279
+ flow: seam.handoff.flow,
1280
+ lineageId: advanced.round.rootExecutionId,
1281
+ roundOrdinal: advanced.round.ordinal,
1282
+ nextExecutionId: advanced.executionId,
1283
+ owner: dependencies.owner
1284
+ }, { cancelRequested: "absent" });
1285
+ if (transitioned._tag === "Transitioned")
1286
+ return;
1287
+ return yield* Effect.fail(transitioned._tag === "GuardFailed" ? cancelled : fenceLost);
1288
+ })).pipe(Effect.catchTag("@smthrs/journal/JournalError", Effect.die)));
1289
+ if (Result.isFailure(committed)) {
1290
+ if (committed.failure._tag === "HandoffCancelled") {
1291
+ yield* cancelOwned(seam.executionId, seam.state);
1292
+ }
1293
+ return;
1294
+ }
1295
+ // The root caller follows the lineage itself, but a discarded (or
1296
+ // orphaned) one does not exist to follow it, so the successor is woken
1297
+ // here rather than left for a sweep that has no reason to look at it.
1298
+ const activeCoordinator = yield* Deferred.await(coordinatorDeferred);
1299
+ yield* activeCoordinator.wake(advanced.executionId);
1300
+ });
1301
+ /**
1302
+ * Announces a committed terminal settlement: to any in-process caller
1303
+ * parked on this run's poll loop, and to every parent attached to the
1304
+ * logical run, which is runnable now. That wake is the only thing that
1305
+ * says so: a child settlement never passes through `scheduleResume`.
1306
+ *
1307
+ * A diamond's second parent lives only in its durable edge (issue #2500),
1308
+ * and `ensureRun` records an edge under the round the parent joined, so
1309
+ * the edges are gathered from every round of the lineage, deduplicated
1310
+ * with the creating parent `state_json` carries. Each is recorded for the
1311
+ * same reason `scheduleResume` records: the round this wake schedules is
1312
+ * the one a host's park guard has to recognize.
1313
+ */
1314
+ const announceSettled = (executionId, state) => Effect.gen(function* () {
1315
+ yield* wakeBus.wake(executionId);
1316
+ const parents = new Set();
1317
+ if (state.parentExecutionId !== undefined)
1318
+ parents.add(state.parentExecutionId);
1319
+ const rounds = yield* lifecycle.rounds(executionId).pipe(Effect.orDie);
1320
+ for (const roundId of new Set([executionId, ...rounds.map((round) => round.runId)])) {
1321
+ for (const edge of yield* engineState.runParents(roundId))
1322
+ parents.add(edge.parentId);
1323
+ }
1324
+ if (parents.size === 0)
1325
+ return;
1326
+ const activeCoordinator = yield* Deferred.await(coordinatorDeferred);
1327
+ for (const parentId of parents) {
1328
+ yield* recordParentResume(parentId);
1329
+ yield* activeCoordinator.wake(parentId);
1330
+ yield* wakeBus.wake(parentId);
1331
+ }
1332
+ });
1333
+ /**
1334
+ * Ends a lineage that asked for one round past its declared budget.
1335
+ *
1336
+ * The round itself ran to completion; what is refused is the handoff, so
1337
+ * the round settles `failed` carrying the typed refusal and no successor
1338
+ * is created (`packages/smithers/flows/engine/docs/concepts/trampoline-rounds.md`).
1339
+ */
1340
+ const endLineage = (seam, error) => Effect.gen(function* () {
1341
+ const stateJson = yield* encodeState({
1342
+ ...seam.state,
1343
+ result: (yield* encodeResult(seam.flow, new Flow.Complete({ exit: Exit.die(error) }))).encoded
1344
+ });
1345
+ const transitioned = yield* transitionAndRecord(seam.executionId, "failed", stateJson, {
1346
+ decision: "lineage-exhausted",
1347
+ status: "failed",
1348
+ lineageId: error.lineageId,
1349
+ maxRounds: error.maxRounds,
1350
+ owner: dependencies.owner
1351
+ }, { cancelRequested: "absent" },
1352
+ // The lineage is over, not continuing: this round is the last one,
1353
+ // so it ends its children like any other terminal exit. A round that
1354
+ // HANDS OFF settles through `continueLineage` instead and deliberately
1355
+ // does not, because the lineage — and therefore the parent the
1356
+ // children are linked to — carries on.
1357
+ applyChildExitPolicy(seam.executionId));
1358
+ if (transitioned._tag === "GuardFailed") {
1359
+ yield* cancelOwned(seam.executionId, seam.state);
1360
+ }
1361
+ else if (transitioned._tag === "Transitioned") {
1362
+ yield* announceSettled(seam.executionId, seam.state);
1363
+ }
1364
+ });
1365
+ /** Fails a run whose persisted round identity or budget is malformed. */
1366
+ const endInvalidRound = (seam, error) => Effect.gen(function* () {
1367
+ const stateJson = yield* encodeState({
1368
+ ...seam.state,
1369
+ result: (yield* encodeResult(seam.flow, new Flow.Complete({ exit: Exit.die(error) }))).encoded
1370
+ });
1371
+ const transitioned = yield* transitionAndRecord(seam.executionId, "failed", stateJson, {
1372
+ decision: "round-invalid",
1373
+ status: "failed",
1374
+ message: error.message,
1375
+ owner: dependencies.owner
1376
+ }, { cancelRequested: "absent" }, applyChildExitPolicy(seam.executionId));
1377
+ if (transitioned._tag === "GuardFailed") {
1378
+ yield* cancelOwned(seam.executionId, seam.state);
1379
+ }
1380
+ else if (transitioned._tag === "Transitioned") {
1381
+ yield* announceSettled(seam.executionId, seam.state);
1382
+ }
1383
+ });
1384
+ /**
1385
+ * The handoff seam: the one place a round's terminal settlement and its
1386
+ * successor are decided together.
1387
+ */
1388
+ const handOff = (seam) => FlowEngine.Round.next({
1389
+ // The row's lineage_id was written from a round's rootExecutionId,
1390
+ // so this rehydrates the brand; `Round.next` still validates it.
1391
+ rootExecutionId: seam.row.lineageId,
1392
+ ordinal: seam.row.roundOrdinal
1393
+ },
1394
+ // The origin persisted its budget into every round's state, so a
1395
+ // multi-flow handoff cannot reset the lineage by changing targets.
1396
+ { flowName: seam.flow._tag, maxRounds: seam.state.maxRounds }).pipe(Effect.flatMap((advanced) => continueLineage(seam, advanced)), Effect.catch((error) => error instanceof FlowEngine.Round.InvalidRound
1397
+ ? endInvalidRound(seam, error)
1398
+ : endLineage(seam, error)));
1399
+ const drive = (executionId) => Effect.suspend(() => {
1400
+ let interruptState;
1401
+ let round;
1402
+ let durableParkAccepted = false;
1403
+ // Set by the heartbeat loop when this round's lease lapses, so the
1404
+ // release it causes records why (#3328).
1405
+ let lapsedMs;
1406
+ return Effect.gen(function* () {
1407
+ const initial = yield* store.get(executionId).pipe(Effect.catch((error) => error.code === "not_found_row"
1408
+ ? Effect.succeed(undefined)
1409
+ : Effect.die(error)));
1410
+ if (initial === undefined)
1411
+ return;
1412
+ const state = yield* decodeState(initial.stateJson);
1413
+ interruptState = state;
1414
+ const registration = registrations.get(state.flowName)?.at(-1);
1415
+ if (registration === undefined) {
1416
+ // A coalesced wake can outlive terminal settlement. There is no
1417
+ // parked work left for a handler to reclaim.
1418
+ if (initial.status === "cancelled" || initial.status === "completed" || initial.status === "failed")
1419
+ return;
1420
+ // Cancelling needs no handler. A parked run whose cancellation was
1421
+ // durably requested used to be dropped here with everything else,
1422
+ // and nothing else could ever reach it: the sweep woke the row every
1423
+ // heartbeat and every wake bailed at this line, so a cancel written
1424
+ // by a control-only process — the operator CLI, a second engine that
1425
+ // registers other flows — stayed write-only forever while the run's
1426
+ // linked children kept going. The terminal transition, the cascade
1427
+ // over the durable edge table, and the interruption record are all
1428
+ // written from the run ROW, so a process that could never execute
1429
+ // this flow can still close it. Claiming first is what makes the
1430
+ // close fenced: `cancelOwned` is owner-fenced, so exactly one of the
1431
+ // sweeping processes wins and the rest see a lost CAS.
1432
+ //
1433
+ // `running` is here for the same reason `suspended` is. The row a
1434
+ // hard-killed owner leaves behind is `running` with a frozen
1435
+ // heartbeat and no waiting row, so the stale-running sweep is the
1436
+ // only thing that reaches it, and it arrives through this same
1437
+ // function: without this arm a control-only process could close a
1438
+ // PARKED run of an unregistered flow and not a hard-killed one,
1439
+ // though the two closes are the identical durable write. Nothing is
1440
+ // weakened by admitting it, because the arbitration is
1441
+ // `claimAndActivate`'s and not this guard's: a fresh lease or a live
1442
+ // owner refuses the takeover and the run is left exactly as it was.
1443
+ if ((initial.status === "pending" || initial.status === "suspended" || initial.status === "running") &&
1444
+ initial.cancelRequestedAtMs !== null) {
1445
+ if (!(yield* claimAndActivate(initial))) {
1446
+ return;
1447
+ }
1448
+ return yield* cancelOwned(executionId, withoutResult(state));
1449
+ }
1450
+ // A wake for a flow this process has not registered — after a full
1451
+ // restart the sweep re-drives released rows before (or without)
1452
+ // the flow ever registering here. Dropping the wake silently made
1453
+ // the #39 reclaim guarantee invisibly conditional on registration
1454
+ // (issue #62): warn (once per run, the sweep retries every
1455
+ // heartbeat) and leave the durable waiting row untouched so any
1456
+ // process that does register the flow still reclaims the run.
1457
+ if (!warnedUnregistered.has(executionId)) {
1458
+ warnedUnregistered.add(executionId);
1459
+ yield* Effect.logWarning(`engine-store: run ${executionId} woke for flow ${state.flowName}, which is not registered in this process; leaving it parked for a worker that registers the flow`, { runId: executionId, flowName: state.flowName });
1460
+ }
1461
+ return;
1462
+ }
1463
+ if (!(yield* claimAndActivate(initial))) {
1464
+ return;
1465
+ }
1466
+ yield* Effect.logDebug("run activated", { flowName: state.flowName });
1467
+ const activeState = withoutResult(state);
1468
+ // The activation transition carries the cancel guard: a run whose
1469
+ // cancellation was durably requested while it was parked must cancel
1470
+ // here instead of re-executing flow side effects (issue #27).
1471
+ const cleared = yield* transitionAndRecord(executionId, "running", yield* encodeState(activeState), { decision: "resumed", status: "running" }, { cancelRequested: "absent" }, undefined, null);
1472
+ if (cleared._tag === "GuardFailed") {
1473
+ return yield* cancelOwned(executionId, withoutResult(state));
1474
+ }
1475
+ if (cleared._tag !== "Transitioned")
1476
+ return;
1477
+ const payload = yield* Schema.decodeUnknownEffect(Schema.toCodecJson(registration.flow.payloadSchema))(activeState.payload).pipe(Effect.orDie);
1478
+ // A previous round of this run may have parked and retained its flow
1479
+ // scope. That instance is now superseded — this round runs under the
1480
+ // fresh instance below, with a scope of its own — so release the old
1481
+ // one instead of orphaning it. `Exit.void`: a re-drive is a successful
1482
+ // continuation, so ordinary finalizers release resources while
1483
+ // `Flow.withRollback` compensations correctly stay out.
1484
+ yield* releaseRetainedScope(executionId, Exit.void);
1485
+ const instance = FlowEngine.makeInstance(registration.flow, executionId, activeState.deadline);
1486
+ round = { flow: registration.flow, instance };
1487
+ const flowEngine = yield* dependencies.engine;
1488
+ // ONE cleanup region, registered before the race starts and spanning
1489
+ // everything that can retain a scope or make its park durable: the
1490
+ // suspension surfacing, the retention insertion in the race's exit,
1491
+ // `encodeResult`, the pending-clock read, `engineState.park`, the
1492
+ // guarded transition and the journal write it commits with, and the
1493
+ // durable-park decision itself. Registering it here is what closes the
1494
+ // last window: retention is inserted by an uninterruptible finalizer,
1495
+ // and the fiber becomes interruptible again the instant that finalizer
1496
+ // returns, so any cleanup piped onto the settlement alone would never
1497
+ // be registered when an interrupt is already pending at that boundary —
1498
+ // the scope would stay retained until the driver's own scope closed,
1499
+ // which for a long-lived driver is not a release at all.
1500
+ //
1501
+ // `Exit.void` is the release exit, for the reason shutdown and a
1502
+ // supersede use it (issue #26): a park that never committed leaves the
1503
+ // run owned-but-stale or already re-owned, so it is reclaimed and
1504
+ // replayed, and the replay re-registers its `Flow.withRollback`
1505
+ // compensations under the reclaiming owner. Compensating here would
1506
+ // undo work the journal still reports as done — and would then do it
1507
+ // twice. Only a cancellation closes with an interrupt exit, and only
1508
+ // `cancelOwned` does that.
1509
+ yield* Effect.gen(function* () {
1510
+ // Published inside the region for the same reason: the entry used to
1511
+ // be written before `dependencies.engine` was awaited, one
1512
+ // interruptible step ahead of the `ensuring` that removes it, so an
1513
+ // interrupt landing there left this round's instance in the map for
1514
+ // the driver's lifetime — a slow leak, and a stale instance for
1515
+ // `interrupt` to mark interrupted.
1516
+ liveInstances.set(executionId, instance);
1517
+ const result = yield* Effect.scoped(Effect.raceFirst(Effect.raceFirst(Effect.suspend(() => activeState.capabilityCeilings === undefined
1518
+ ? Effect.die(new Error("Missing persisted capabilityCeilings; execution cannot safely resume"))
1519
+ : registration.execute(payload, executionId)).pipe(CapabilitySet.attenuateGroups(activeState.capabilityCeilings ?? [[]]), Flow.intoResult, Effect.provideService(FlowRuntime.FlowInstance, instance), Effect.provideService(FlowRuntime.FlowRuntime, flowEngine)), Ownership.heartbeatLoop(executionId, dependencies.owner, {
1520
+ onReconfirm: (unconfirmedMs) => {
1521
+ const record = decisionRecord(executionId, {
1522
+ decision: "lease-reconfirmed",
1523
+ detail: { unconfirmedMs }
1524
+ }, dependencies.journalSource);
1525
+ return journal.emitDurable(record, dependencies.owner).pipe(Effect.asVoid, Effect.orDie);
1526
+ },
1527
+ onLapse: (unconfirmedMs) => Effect.sync(() => {
1528
+ lapsedMs = unconfirmedMs;
1529
+ })
1530
+ }).pipe(Effect.provideService(RunStore.RunStore, store))), cancelPollLoop(executionId))).pipe(Effect.ensuring(Effect.sync(() => liveInstances.delete(executionId))),
1531
+ // A suspension is the ONE settlement `Flow.intoResult` leaves the
1532
+ // flow scope open for, so from the instant it surfaces the scope
1533
+ // needs an owner. Retention is taken HERE, inside the race's own
1534
+ // exit processing — `Effect.onExit` finalizers run uninterruptibly —
1535
+ // because taking it any later crosses an interruptible boundary: an
1536
+ // interrupt landing there would end the drive with the scope never
1537
+ // retained, unreachable, and its finalizers never run at all. The
1538
+ // entry is inserted into a cleanup region that was registered before
1539
+ // this race started, so it is owned from this instant on: every
1540
+ // ending, including an interrupt observed at the boundary between
1541
+ // this finalizer and the settlement below, releases it there.
1542
+ Effect.onExit((exit) => Effect.suspend(() => {
1543
+ if (!(Exit.isSuccess(exit) && exit.value._tag === "Suspended"))
1544
+ return Effect.void;
1545
+ retainedScopes.set(executionId, instance.scope);
1546
+ return dependencies.unsafeOnScopeRetained?.(executionId) ?? Effect.void;
1547
+ })));
1548
+ /**
1549
+ * Settles this round durably.
1550
+ *
1551
+ * A suspension that the guarded terminal transition durably accepts
1552
+ * sets `durableParkAccepted`, and that flag is the only thing that lets
1553
+ * a retained flow scope outlive this round. Every other ending — an
1554
+ * alternative settlement, a cancellation, a lost fence, a defect, an
1555
+ * interruption — leaves it unset, so the cleanup region registered
1556
+ * around the round releases the scope.
1557
+ */
1558
+ const settleRound = Effect.gen(function* () {
1559
+ if (result._tag === "CancelRequested") {
1560
+ yield* cancelOwned(executionId, activeState);
1561
+ return;
1562
+ }
1563
+ // Corrupt evidence on a SUCCEEDED attempt row is an operator-visible
1564
+ // event, not a terminal run failure (issue #171): the row cannot be
1565
+ // evicted and re-executed like a corrupt cache row (#164) without
1566
+ // breaking exactly-once. ActionPersistence has already journalled
1567
+ // the corruption and quarantined only its boundary evidence off the
1568
+ // row. Park this first strict detection so it remains visible; the
1569
+ // next explicit resume returns the durable outcome without replaying
1570
+ // the poison or re-executing the action.
1571
+ const quarantine = result._tag === "Complete" && Exit.isFailure(result.exit)
1572
+ ? ActionPersistence.evidenceQuarantined(result.exit.cause)
1573
+ : undefined;
1574
+ if (quarantine !== undefined) {
1575
+ const parked = yield* transitionAndRecord(executionId, "suspended", yield* encodeState(withoutResult(activeState)), {
1576
+ decision: "quarantined",
1577
+ status: "suspended",
1578
+ keyDigest: quarantine.keyDigest,
1579
+ owner: dependencies.owner
1580
+ }, { cancelRequested: "absent" }, undefined, { reason: "quarantine", token: quarantine.keyDigest });
1581
+ if (parked._tag === "GuardFailed") {
1582
+ yield* cancelOwned(executionId, activeState);
1583
+ }
1584
+ return;
1585
+ }
1586
+ // A round that handed off settles through the seam instead: its
1587
+ // terminal transition and its successor's creation are one write, so
1588
+ // it cannot share the ordinary terminal path below.
1589
+ if (result._tag === "Handoff") {
1590
+ yield* handOff({
1591
+ executionId,
1592
+ row: initial,
1593
+ state: activeState,
1594
+ flow: registration.flow,
1595
+ handoff: result
1596
+ });
1597
+ return;
1598
+ }
1599
+ const encodedResult = yield* encodeResult(registration.flow, result);
1600
+ const nextState = { ...activeState, result: encodedResult.encoded };
1601
+ // A settlement the flow's codec rejected is written `failed`
1602
+ // whatever it claimed to be: the row must reach a terminal status
1603
+ // in this process, because the alternative — the `running` row the
1604
+ // release validation found — is re-executed by the next one.
1605
+ const status = encodedResult.note !== undefined
1606
+ ? "failed"
1607
+ : result._tag === "Suspended"
1608
+ ? "suspended"
1609
+ : Exit.isSuccess(result.exit)
1610
+ ? "completed"
1611
+ : "failed";
1612
+ let waiting = null;
1613
+ if (status === "suspended") {
1614
+ // Park atomically with the suspended transition while this
1615
+ // process still owns the row (`park` is owner-fenced). The
1616
+ // reason is derived from durable state: a pending
1617
+ // clock row means a timer wake with a known deadline; anything
1618
+ // else waits on an external event (deferred completion). This is
1619
+ // what makes `waitingRuns` sweepers and the 0004 partial index
1620
+ // match real suspensions (issue #12).
1621
+ // A flow-declared classification (FlowRuntime.annotateWaiting) wins:
1622
+ // it is the only way an approval or quota wait — and its wake
1623
+ // token — reaches the parked row (issue #31). The durable-state
1624
+ // derivation stays the fallback.
1625
+ const declared = instance.waiting;
1626
+ const pendingClocks = yield* engineState.pendingClocks({ executionId });
1627
+ waiting = declared !== undefined
1628
+ ? declared
1629
+ : pendingClocks.length > 0
1630
+ ? {
1631
+ reason: "timer",
1632
+ wakeAt: Math.min(...pendingClocks.map((clock) => clock.dueAtMs))
1633
+ }
1634
+ : { reason: "event" };
1635
+ }
1636
+ // Finalize is guarded on `cancel_requested_at_ms` inside the same
1637
+ // CAS: a cancellation request that raced past the last poll turns
1638
+ // the terminal transition into GuardFailed, and the run cancels
1639
+ // instead of finalizing (issue #11).
1640
+ const transitioned = yield* transitionAndRecord(executionId, status, yield* encodeState(nextState), { decision: "transitioned", status, owner: dependencies.owner }, { cancelRequested: "absent" },
1641
+ // A suspension is not an exit: the run is parked and will settle
1642
+ // later, so its children keep going and its timers stay armed. A
1643
+ // `completed` or `failed` run is done, so its attached children
1644
+ // end with it. The transition closes its own clock rows.
1645
+ status === "suspended" ? undefined : applyChildExitPolicy(executionId), waiting).pipe(
1646
+ // The park becomes durable the instant this transition commits, so
1647
+ // the flag that records it is set in the transition's own exit
1648
+ // processing. `Effect.onExit` finalizers run uninterruptibly and
1649
+ // before the fiber can observe an interruption again, so no
1650
+ // interrupt can land between the durable commit and the flag and
1651
+ // make the cleanup region discard a scope the park still needs.
1652
+ Effect.onExit((exit) => Effect.sync(() => {
1653
+ if (status === "suspended" && Exit.isSuccess(exit) && exit.value._tag === "Transitioned") {
1654
+ durableParkAccepted = true;
1655
+ }
1656
+ })));
1657
+ if (transitioned._tag === "GuardFailed") {
1658
+ // `cancelOwned` closes the retained scope itself, with the
1659
+ // interrupt exit cancellation compensations exist for. Leaving
1660
+ // `durableParkAccepted` unset therefore releases nothing a second
1661
+ // time: the entry is already gone from the map.
1662
+ yield* cancelOwned(executionId, activeState);
1663
+ return;
1664
+ }
1665
+ // A lost fence (`NotFound`, a mismatched CAS) settles nothing: the
1666
+ // run is someone else's now, so a suspension holds no durable park
1667
+ // and the cleanup region releases the scope rather than holding it
1668
+ // until shutdown.
1669
+ if (transitioned._tag !== "Transitioned")
1670
+ return;
1671
+ if (status !== "suspended") {
1672
+ // The settle is durable; tell any in-process caller parked on this
1673
+ // run's poll loop, so a run driven to completion by a sweep or a
1674
+ // coordinator wake is observed now rather than on the next tick.
1675
+ yield* announceSettled(executionId, activeState);
1676
+ return;
1677
+ }
1678
+ });
1679
+ yield* settleRound;
1680
+ });
1681
+ }).pipe(
1682
+ // Install ownership cleanup before the claim itself. Limiting it to
1683
+ // the execution race strands an activated row if shutdown lands in
1684
+ // setup or settlement; a replacement host in the same process then
1685
+ // correctly refuses to steal from the still-live PID forever.
1686
+ Effect.onInterrupt(() => interruptState === undefined
1687
+ ? Effect.void
1688
+ : settleInterrupted(executionId, interruptState, lapsedMs === undefined
1689
+ ? { kind: "interrupted" }
1690
+ : { kind: "lease-lapsed", unconfirmedMs: Math.max(0, Math.floor(lapsedMs)) }, round)), Effect.onExit(() => Effect.suspend(() => {
1691
+ liveInstances.delete(executionId);
1692
+ // Cancellation settles first so its compensations still see the
1693
+ // retained scope. A successful durable park alone retains it.
1694
+ return round === undefined || durableParkAccepted
1695
+ ? Effect.void
1696
+ : releaseRetainedScope(executionId, Exit.void);
1697
+ })), Effect.annotateLogs({ runId: executionId }));
1698
+ });
1699
+ const coordinator = yield* RunCoordinator.make({
1700
+ drain: drive
1701
+ });
1702
+ yield* Deferred.succeed(coordinatorDeferred, coordinator);
1703
+ /**
1704
+ * Delivers cancellation to parked runs (issue #27). A suspended run has
1705
+ * no owner and therefore no cancel poll, so `requestCancel` against it
1706
+ * is write-only until something re-drives the run — a run parked on a
1707
+ * deferred that never completes could otherwise never be cancelled. The
1708
+ * sweep lists parked rows, and wakes any whose cancel was durably
1709
+ * requested; the re-activation cancel guard then closes the run without
1710
+ * re-executing the flow.
1711
+ *
1712
+ * The same sweep reclaims interrupt-released runs (issue #39): a run
1713
+ * parked with reason `released` was interrupted mid-action by shutdown
1714
+ * or a heartbeat self-interrupt, has no pending clock and no completed
1715
+ * deferred, and would otherwise never be re-driven. Waking it re-enters
1716
+ * the ordinary claim/activate path, which also delivers any pending
1717
+ * cancel via the activation guard.
1718
+ */
1719
+ const sweepCancelRequested = Effect.gen(function* () {
1720
+ // Fetch only actionable rows (issue #68): the sweep acts solely on
1721
+ // released rows and rows whose cancellation was durably requested, so
1722
+ // a large quota/event-parked fleet must cost it nothing per tick. The
1723
+ // per-row `store.get` below is a status guard over the (small)
1724
+ // actionable set, not a probe over every parked run — the in-memory
1725
+ // implementation without a `runs` view stays permissive on the cancel
1726
+ // predicate and relies on exactly this guard.
1727
+ const released = yield* engineState.waitingRuns({ reason: "released" });
1728
+ const cancelRequestedRows = yield* engineState.waitingRuns({ cancelRequested: true });
1729
+ const candidates = new Map();
1730
+ for (const waiting of released)
1731
+ candidates.set(waiting.runId, waiting);
1732
+ for (const waiting of cancelRequestedRows)
1733
+ candidates.set(waiting.runId, waiting);
1734
+ for (const waiting of candidates.values()) {
1735
+ const row = yield* store.get(waiting.runId).pipe(Effect.catch(() => Effect.succeed(undefined)));
1736
+ if (row !== undefined &&
1737
+ row.status === "suspended" &&
1738
+ (row.cancelRequestedAtMs !== null || waiting.reason === "released")) {
1739
+ yield* coordinator.wake(row.runId);
1740
+ }
1741
+ }
1742
+ });
1743
+ /**
1744
+ * A timer can fire while a dead control owner is still inside its stale
1745
+ * cutoff. The clock completion is durable, but that first coordinator
1746
+ * wake cannot claim yet (#3408). Revisit only due timer waits with an
1747
+ * unconsumed completion; the normal drive still checks admission and CAS.
1748
+ * Released actions, future timers and ordinary event waits are excluded.
1749
+ */
1750
+ const sweepReadyTimers = Effect.gen(function* () {
1751
+ const nowMs = yield* Clock.currentTimeMillis;
1752
+ const due = yield* engineState.waitingRuns({ reason: "timer", dueBeforeMs: nowMs });
1753
+ const completions = new Map();
1754
+ for (const waiting of due) {
1755
+ const row = yield* store.get(waiting.runId).pipe(Effect.catch(() => Effect.succeed(undefined)));
1756
+ if (row === undefined || row.status !== "suspended")
1757
+ continue;
1758
+ const state = yield* decodeState(row.stateJson);
1759
+ if (!registrations.has(state.flowName))
1760
+ continue;
1761
+ let ready = completions.get(state.flowName);
1762
+ if (ready === undefined) {
1763
+ ready = new Set((yield* engineState.completedDeferreds(state.flowName)).map((address) => address.executionId));
1764
+ completions.set(state.flowName, ready);
1765
+ }
1766
+ if (!ready.has(row.runId))
1767
+ continue;
1768
+ yield* coordinator.schedule(row.runId);
1769
+ }
1770
+ });
1771
+ // Admissions committed before a host crash have no lease to go stale.
1772
+ // Reuse the normal coordinator and claim/activate path for these rows.
1773
+ const pendingCycles = new Map();
1774
+ let pendingCancellationCycle;
1775
+ // Explicit cancellation needs no registration or admission grace. A finite
1776
+ // tail and cursor keep a denied prefix from starving eligible later rows,
1777
+ // even when new cancellations arrive continuously.
1778
+ const sweepPendingCancellation = () => Effect.gen(function* () {
1779
+ if (pendingCancellationCycle === undefined) {
1780
+ const tail = yield* engineState.pendingCancellationTail();
1781
+ if (Option.isNone(tail))
1782
+ return;
1783
+ pendingCancellationCycle = { through: tail.value };
1784
+ }
1785
+ const cycle = pendingCancellationCycle;
1786
+ const candidates = yield* engineState.pendingCancellationRuns(staleRunningSweepBatch, cycle.after, cycle.through);
1787
+ for (const cursor of candidates) {
1788
+ const row = yield* store.get(cursor.runId).pipe(Effect.catch((error) => error.code === "not_found_row" ? Effect.succeed(undefined) : error.code === "decode_failed"
1789
+ ? Effect.logWarning("engine-store: unreadable pending cancellation skipped", {
1790
+ runId: cursor.runId,
1791
+ error
1792
+ })
1793
+ .pipe(Effect.as(undefined))
1794
+ : Effect.die(error)));
1795
+ // Permanent misses advance; a transient read failure retries this slot.
1796
+ cycle.after = cursor;
1797
+ if (row === undefined || row.status !== "pending" || row.cancelRequestedAtMs === null ||
1798
+ (yield* coordinator.active).has(cursor.runId))
1799
+ continue;
1800
+ if (dependencies.canExecute !== undefined && !(yield* dependencies.canExecute(row)))
1801
+ continue;
1802
+ if (!(yield* coordinator.active).has(cursor.runId))
1803
+ yield* coordinator.schedule(cursor.runId);
1804
+ }
1805
+ if (candidates.length < staleRunningSweepBatch ||
1806
+ candidates.at(-1)?.runId === cycle.through.runId)
1807
+ pendingCancellationCycle = undefined;
1808
+ });
1809
+ let pendingFlowOffset = 0;
1810
+ const sweepPending = () => Effect.gen(function* () {
1811
+ let remaining = staleRunningSweepBatch;
1812
+ const allNames = Array.from(registrations.keys());
1813
+ const offset = allNames.length === 0 ? 0 : pendingFlowOffset % allNames.length;
1814
+ const names = [...allNames.slice(offset), ...allNames.slice(0, offset)];
1815
+ pendingFlowOffset++;
1816
+ for (const name of names) {
1817
+ const registration = registrations.get(name)?.at(-1);
1818
+ if (registration === undefined || remaining === 0)
1819
+ continue;
1820
+ let cycle = pendingCycles.get(name);
1821
+ if (cycle === undefined) {
1822
+ const eligibleBeforeMs = (yield* Clock.currentTimeMillis) - Duration.toMillis(Ownership.heartbeatStaleAfter);
1823
+ const tail = yield* engineState.pendingRunTail(name, eligibleBeforeMs);
1824
+ if (Option.isNone(tail))
1825
+ continue;
1826
+ cycle = { through: tail.value, eligibleBeforeMs };
1827
+ pendingCycles.set(name, cycle);
1828
+ }
1829
+ const requested = remaining;
1830
+ const pending = yield* engineState.pendingRuns(name, requested, cycle.after, cycle.through, cycle.eligibleBeforeMs);
1831
+ for (const cursor of pending) {
1832
+ if (remaining === 0 || registrations.get(name)?.at(-1) !== registration)
1833
+ break;
1834
+ remaining--;
1835
+ const row = yield* store.get(cursor.runId).pipe(Effect.catch((error) => error.code === "not_found_row" ? Effect.succeed(undefined) : error.code === "decode_failed"
1836
+ ? Effect.logWarning("engine-store: unreadable pending admission skipped", {
1837
+ runId: cursor.runId,
1838
+ flowName: name,
1839
+ error
1840
+ }).pipe(Effect.as(undefined))
1841
+ : Effect.die(error)));
1842
+ // Advance after successful reads or permanent row errors; transient failures retry this
1843
+ // position, while a retained query cursor skips deleted rows safely.
1844
+ cycle.after = cursor;
1845
+ if (row === undefined || row.status !== "pending" || (yield* coordinator.active).has(cursor.runId)) {
1846
+ continue;
1847
+ }
1848
+ if (dependencies.canExecute !== undefined && !(yield* dependencies.canExecute(row))) {
1849
+ continue;
1850
+ }
1851
+ if (registrations.get(name)?.at(-1) !== registration || (yield* coordinator.active).has(cursor.runId)) {
1852
+ continue;
1853
+ }
1854
+ yield* coordinator.schedule(cursor.runId);
1855
+ }
1856
+ const last = pending.at(-1);
1857
+ if (pending.length < requested || (last !== undefined && last.runId === cycle.through.runId)) {
1858
+ pendingCycles.delete(name);
1859
+ }
1860
+ }
1861
+ });
1862
+ /**
1863
+ * Reclaims hard-killed runs (issue #53). An owner that dies without
1864
+ * releasing (SIGKILL, OOM, power loss) leaves a `running` row with a
1865
+ * frozen heartbeat and no waiting row, so the parked-run sweep above
1866
+ * never sees it, and the steal path — reachable only through `drive()` —
1867
+ * is never entered. Enumerate stale-running rows and re-drive them: the
1868
+ * ordinary claim/steal path (liveness check, exact-snapshot steal CAS)
1869
+ * then decides whether the owner is genuinely dead — the analog of
1870
+ * Temporal's task-timeout re-dispatch. A pending durable cancel is
1871
+ * delivered by the re-activation guard, same as for parked runs.
1872
+ */
1873
+ const sweepStaleRunning = Effect.gen(function* () {
1874
+ const nowMs = yield* Clock.currentTimeMillis.pipe(Effect.map(Math.floor));
1875
+ // Rows this driver has already arbitrated and been refused. They are the
1876
+ // oldest heartbeats in the window, so they sort first and would fill the
1877
+ // batch every tick; the read asks for enough extra rows to see past
1878
+ // exactly the ones it is going to skip.
1879
+ const deferred = Array.from(stealRefusals.values()).filter((entry) => entry.nextProbeAtMs > nowMs).length;
1880
+ // Capped per tick (issue #79): oldest heartbeats come back first, so a
1881
+ // mass owner death drains across successive ticks — batch after batch
1882
+ // as each stolen run's heartbeat leaves the stale window — instead of
1883
+ // every surviving driver waking every stale run every second and
1884
+ // contending N-drivers × M-runs on the claim/steal CAS.
1885
+ const stale = yield* engineState.staleRunningRuns(nowMs - Duration.toMillis(Ownership.heartbeatStaleAfter), staleRunningSweepBatch + deferred);
1886
+ // A refusal is remembered only while its row is still in the window this
1887
+ // read covers. A run that was stolen, released, or settled leaves the
1888
+ // stale set, and its entry leaves with it, so the map cannot outgrow the
1889
+ // backlog it describes.
1890
+ const visible = new Set(stale.map((row) => row.runId));
1891
+ for (const runId of stealRefusals.keys()) {
1892
+ if (!visible.has(runId))
1893
+ stealRefusals.delete(runId);
1894
+ }
1895
+ let woken = 0;
1896
+ for (const { heartbeatAtMs, runId } of stale) {
1897
+ if (woken >= staleRunningSweepBatch)
1898
+ break;
1899
+ // A refusal defers only the lease it was refused against. An owner
1900
+ // that pulsed and stalled again between two ticks never left this
1901
+ // read's window, so the lease is what says the stall is a new one.
1902
+ const refusal = stealRefusals.get(runId);
1903
+ if (refusal !== undefined && refusal.heartbeatAtMs === heartbeatAtMs && refusal.nextProbeAtMs > nowMs)
1904
+ continue;
1905
+ woken = woken + 1;
1906
+ yield* coordinator.wake(runId);
1907
+ }
1908
+ });
1909
+ const resilientSweep = (sweep, name) => sweep.pipe(Effect.sandbox, Effect.catchCause((cause) => Effect.logWarning(`engine-store: ${name} sweep failed; retrying next tick`, cause)));
1910
+ yield* Effect.forkScoped(Effect.forever(Effect.sleep(Ownership.heartbeatInterval).pipe(Effect.andThen(
1911
+ // One transient defect (a `SQLITE_BUSY` escaping `waitingRuns()`'s
1912
+ // `orDie`, a wake failure) must not kill the sweeper for the rest
1913
+ // of the process lifetime (issue #44) — that would silently revert
1914
+ // to pre-#27 behavior where cancel of parked runs is never
1915
+ // delivered. Mirror `armClock`'s hardening: expose the full cause,
1916
+ // log it, and keep ticking.
1917
+ resilientSweep(sweepCancelRequested, "parked-run cancel").pipe(Effect.andThen(resilientSweep(sweepStaleRunning, "stale-running")), Effect.andThen(resilientSweep(sweepReadyTimers, "ready timer")), Effect.andThen(resilientSweep(sweepPendingCancellation(), "pending cancellation")), Effect.andThen(resilientSweep(sweepPending(), "pending admission")))))));
1918
+ const ensureRun = (flow, options) => Effect.gen(function* () {
1919
+ const payload = yield* Schema.encodeEffect(Schema.toCodecJson(flow.payloadSchema))(options.payload).pipe(Effect.orDie);
1920
+ // Every requested parent — first (creating) parent and a diamond's
1921
+ // second parent alike — is recorded as a durable edge BEFORE the run
1922
+ // row is created. `recordRunParent` inserts the edge and checks for
1923
+ // a cycle inside one storage transaction (issues #29/#40/#54/#56),
1924
+ // so a rejected request fails here and never creates a run row — no
1925
+ // `state_json` parent can outlive a rejected edge (issue #55). The
1926
+ // edge table is the single source of truth for cycle detection in
1927
+ // every owner process and across restarts (issues #40/#41).
1928
+ //
1929
+ // The edge record and the run-row creation share one outer storage
1930
+ // transaction (issue #80): a crash between them used to commit a
1931
+ // durable orphan edge for a child run that never existed — never
1932
+ // GC'd, permanently walked by cycle detection, and able to force a
1933
+ // false FlowCycleDetected if the execution id was later reused
1934
+ // under an inverted topology. The stores' own writes become
1935
+ // savepoints of this transaction, so either both commit or neither.
1936
+ yield* transactState(Effect.gen(function* () {
1937
+ let parentRecorded = false;
1938
+ if (options.parent !== undefined) {
1939
+ const parentEdge = yield* engineState.recordRunParent(options.executionId, options.parent.executionId).pipe(Effect.catch((error) => Effect.fail(new FlowCycleDetected({ code: "flow_cycle_detected", path: error.path }))));
1940
+ parentRecorded = parentEdge._tag === "Recorded";
1941
+ }
1942
+ const round = options.round ?? FlowEngine.Round.initial(options.executionId);
1943
+ const previousExecutionId = options.round?.previousExecutionId;
1944
+ // Decided once, at spawn, from what the caller did with the result:
1945
+ // a caller waiting for the value keeps the child attached, and a
1946
+ // caller that discarded it detached the child on purpose. Nothing
1947
+ // later rewrites this.
1948
+ const onParentExit = options.discard === true ? "detach" : "cancel";
1949
+ const state = {
1950
+ version: 1,
1951
+ flowName: flow._tag,
1952
+ payload,
1953
+ capabilityCeilings: yield* FlowEngine.requestedAuthority(flow),
1954
+ onParentExit,
1955
+ ...(options.parent === undefined
1956
+ ? {}
1957
+ : { parentExecutionId: options.parent.executionId }),
1958
+ ...(flow.maxRounds === undefined ? {} : { maxRounds: flow.maxRounds })
1959
+ };
1960
+ const createdStateJson = yield* encodeState(state);
1961
+ yield* ensureCreatedRun({
1962
+ flowName: flow._tag,
1963
+ capabilityCeilings: state.capabilityCeilings,
1964
+ executionId: options.executionId,
1965
+ stateJson: createdStateJson,
1966
+ payload,
1967
+ lineageId: round.rootExecutionId,
1968
+ roundOrdinal: round.ordinal,
1969
+ ...(previousExecutionId === undefined
1970
+ ? {}
1971
+ : { parentRunId: previousExecutionId }),
1972
+ onCreated: Effect.gen(function* () {
1973
+ // The run's opening frame. `store.create` used to be the one
1974
+ // durable write with no journal record at all, which left the
1975
+ // replay-derived state projection with no base to fold onto —
1976
+ // `flowName` and `payload` exist nowhere else in the journal.
1977
+ // The state travels ENCODED, exactly as every later decision
1978
+ // carries it: `stateAt` folds these payloads and hands the winner
1979
+ // back as the run row's own `state_json`, so a base recorded in the
1980
+ // decoded shape would be a schema the caller cannot decode.
1981
+ yield* emitDecision(options.executionId, {
1982
+ decision: "created",
1983
+ state: JSON.parse(createdStateJson),
1984
+ ...(options.parent === undefined ? {} : { parentExecutionId: options.parent.executionId })
1985
+ });
1986
+ if (options.parent !== undefined) {
1987
+ // A spawn is a lineage edge, and a DETACHED spawn is a tier-3
1988
+ // effect: nothing the parent's rewind can undo, because the child
1989
+ // is its own claim and its own journal. The record
1990
+ // is boundary-shaped so the same assessment that classifies a
1991
+ // sent webhook classifies an orphaned child, and it is emitted at
1992
+ // `succeeded` because by this point the child run durably exists.
1993
+ // Unfenced: the creator of a detached child is not necessarily
1994
+ // the parent's current owner (an external spawn admission
1995
+ // creates child rows without holding the parent's fence), so
1996
+ // this lineage-edge record is first-writer-wins evidence keyed
1997
+ // by its spawn identity.
1998
+ yield* journal.emitDurableUnfenced(EffectRecords.boundary({
1999
+ id: `${options.parent.executionId}:spawn:${options.executionId}`,
2000
+ kind: spawnEffectKind,
2001
+ tier: "irreversible",
2002
+ runId: options.parent.executionId,
2003
+ lineageId: FlowEngine.Lineage.root(options.parent.executionId),
2004
+ sourceId: dependencies.journalSource,
2005
+ attempt: 1,
2006
+ residue: `Child run ${options.executionId} exists and keeps its own journal; rewinding past its spawn orphans it.`
2007
+ }, "succeeded",
2008
+ // `attached` is written even though it is always false: the
2009
+ // lineage-tree bridge in `@smthrs/time-travel` reads it off
2010
+ // this payload, and an absent field there would make "this
2011
+ // spawn is detached" indistinguishable from "this producer
2012
+ // predates the field". A run created with a parent is a
2013
+ // separate run row with its own claim, which is what detached
2014
+ // means; attached nesting
2015
+ // never reaches `create` because it is one journal.
2016
+ { childRunId: options.executionId, flowName: flow._tag, attached: false })).pipe(Effect.orDie);
2017
+ }
2018
+ })
2019
+ });
2020
+ // The row already exists; the durable edge recorded above is the
2021
+ // only place a diamond's second parent lives (issues #41/#48): a
2022
+ // driver-local side table would be invisible to other owners over
2023
+ // the same store, lost across restart, and would grow without
2024
+ // bound for the driver's lifetime.
2025
+ //
2026
+ // Which is also why the parent's cancellation is inherited HERE,
2027
+ // last and inside this same transaction: the child row must already
2028
+ // exist for `requestCancel` to have anything to write to, and the
2029
+ // inheritance must commit or roll back with the edge that made this
2030
+ // run a child in the first place. The inherited request is what the
2031
+ // activation guard reads, so the child cancels instead of executing
2032
+ // the flow body.
2033
+ if (options.parent !== undefined) {
2034
+ yield* inheritParentCancellation(options.parent.executionId, options.executionId);
2035
+ if (parentRecorded) {
2036
+ yield* emitDecision(options.executionId, {
2037
+ decision: "parent-recorded",
2038
+ parentExecutionId: options.parent.executionId
2039
+ });
2040
+ }
2041
+ }
2042
+ })).pipe(Effect.catchIf((error) => error instanceof Journal.JournalError || error instanceof DatabaseError, Effect.die));
2043
+ });
2044
+ const readResult = (flow, executionId, followLineage = false) => Effect.annotateCurrentSpan({ executionId, flow: flow._tag }).pipe(Effect.andThen(FlowEngine.requestedAuthority(flow)), Effect.flatMap((requested) => Effect.map(followLineage ? store.latestRound(executionId) : store.get(executionId), (row) => ({
2045
+ row,
2046
+ requested
2047
+ }))), Effect.catch((error) => error.code === "not_found_row"
2048
+ ? Effect.succeed(undefined)
2049
+ : Effect.die(error)), Effect.flatMap((found) => {
2050
+ if (found === undefined) {
2051
+ // No run row at all is a typed not-found; `Option.none` is
2052
+ // reserved for a known run that has not settled yet.
2053
+ return Effect.fail(new FlowRuntime.FlowExecutionNotFound({
2054
+ code: "execution_not_found",
2055
+ executionId
2056
+ }));
2057
+ }
2058
+ const { row, requested } = found;
2059
+ return decodeState(row.stateJson).pipe(Effect.flatMap((state) => {
2060
+ if ((!followLineage && state.flowName !== flow._tag) ||
2061
+ state.result === undefined) {
2062
+ return Effect.succeedNone;
2063
+ }
2064
+ // A result is what the admitted authority produced; a caller
2065
+ // whose authority does not cover it never reads it.
2066
+ if (!FlowEngine.joinable(state.capabilityCeilings ?? [], requested)) {
2067
+ return Effect.die(FlowEngine.capabilityConflict(executionId));
2068
+ }
2069
+ const resultFlow = state.flowName === flow._tag ? flow : registrations.get(state.flowName)?.at(-1)?.flow;
2070
+ if (resultFlow === undefined) {
2071
+ // Direct nested callers must register the terminal flow's
2072
+ // codec here, even when another worker executed that flow.
2073
+ // This is the typed Trampoline's fail-fast wiring contract:
2074
+ // terminal work has no future wake to justify Suspended.
2075
+ return Effect.die(new FlowEngine.FlowNotRegistered({
2076
+ flowName: state.flowName,
2077
+ message: `Flow ${state.flowName} is not registered`
2078
+ }));
2079
+ }
2080
+ return Schema.decodeUnknownEffect(Schema.toCodecJson(Flow.Result({
2081
+ success: resultFlow.successSchema,
2082
+ error: resultFlow.errorSchema
2083
+ })))(state.result).pipe(Effect.orDie, Effect.map(Option.some));
2084
+ }), Effect.map((result) => ({ result, status: row.status })));
2085
+ }));
2086
+ /**
2087
+ * Whether a resume of `executionId` for `flow` may drive anything: only an
2088
+ * admitted row of this declaration whose recorded authority the caller's
2089
+ * covers. An unknown id or a row another declaration owns drives nothing,
2090
+ * so a row admitted after this read is never woken on this request; a
2091
+ * caller whose authority does not cover the row is refused.
2092
+ */
2093
+ const authorizeResume = (flow, executionId) => Effect.gen(function* () {
2094
+ const row = yield* store.get(executionId).pipe(Effect.catch((error) => error.code === "not_found_row" ? Effect.succeed(undefined) : Effect.die(error)));
2095
+ if (row === undefined)
2096
+ return false;
2097
+ const state = yield* decodeState(row.stateJson);
2098
+ if (state.flowName !== flow._tag)
2099
+ return false;
2100
+ if (!FlowEngine.joinable(state.capabilityCeilings ?? [], yield* FlowEngine.requestedAuthority(flow))) {
2101
+ return yield* Effect.die(FlowEngine.capabilityConflict(executionId));
2102
+ }
2103
+ return true;
2104
+ });
2105
+ const poll = Effect.fn("FlowEngine.poll")((flow, executionId) => Effect.map(readResult(flow, executionId), (observed) => observed.result));
2106
+ const execute = Effect.fn("FlowEngine.execute")(function* (flow, options) {
2107
+ yield* Effect.annotateCurrentSpan({ executionId: options.executionId, flow: flow._tag });
2108
+ if (!registrations.has(flow._tag)) {
2109
+ return yield* Effect.die(new FlowEngine.FlowNotRegistered({
2110
+ flowName: flow._tag,
2111
+ message: `Flow ${flow._tag} is not registered`
2112
+ }));
2113
+ }
2114
+ // Cycle rejection happens atomically inside `ensureRun`'s call to
2115
+ // `DurableEngineState.recordRunParent`: the storage transaction that
2116
+ // inserts the edge also walks the parent chain and rolls back on a
2117
+ // hit, so no in-process gate, cross-owner arbitration, or withdrawal
2118
+ // protocol is needed here (issues #29/#40/#54/#55/#56) and the
2119
+ // mutual `coordinator.run` deadlock cannot form.
2120
+ if (options.discard) {
2121
+ // Keep durable admission and scheduling together across caller cancellation.
2122
+ // The coordinator owns the drive in the engine scope, so a
2123
+ // detached child outlives the caller's action and execution.
2124
+ yield* Effect.uninterruptible(ensureRun(flow, options).pipe(Effect.andThen(coordinator.schedule(options.executionId))));
2125
+ return undefined;
2126
+ }
2127
+ yield* ensureRun(flow, options);
2128
+ // Admission, handoff, or a wake normally scheduled this drive. A
2129
+ // replacement host may instead find an admitted pending row with no
2130
+ // drive, so schedule that row before joining. An idle suspended row
2131
+ // stays passive until its actual wake or elapsed poll resumes it.
2132
+ if (options.follow === true) {
2133
+ yield* coordinator.scheduleIf(options.executionId, store.get(options.executionId).pipe(Effect.orDie, Effect.map((row) => row.status === "pending")));
2134
+ yield* coordinator.join(options.executionId);
2135
+ }
2136
+ else {
2137
+ yield* coordinator.run(options.executionId);
2138
+ }
2139
+ // `ensureRun` created the row above, so a not-found here is a broken
2140
+ // store invariant, not a caller-recoverable state.
2141
+ // The trampoline supplies `round` and consumes each raw handoff itself.
2142
+ // A nested direct caller instead needs the child's current lineage row,
2143
+ // including its own codec and cancellation status. `follow` schedules an
2144
+ // undriven pending row before joining; it does not select the result's scope.
2145
+ const observed = yield* Effect.orDie(readResult(flow, options.executionId, options.parent !== undefined && options.round === undefined));
2146
+ if (Option.isSome(observed.result)) {
2147
+ return observed.result.value;
2148
+ }
2149
+ // A cancelled run has no `Flow.Result` on its row — cancellation is
2150
+ // recorded as `cancellation`, not as a settlement — so answering the
2151
+ // absence with `Suspended` told the caller to wait for a run that was
2152
+ // over. The engine's suspended-retry loop then did what that answer
2153
+ // means: slept, resumed, re-drove, and asked again, for as long as the
2154
+ // caller lived. A run cancelled underneath its caller interrupts the
2155
+ // caller instead, which is what the memory engine already does
2156
+ // (`layerMemory.ts` `interrupt` settles the round `Complete` with an
2157
+ // interrupt cause) and what a cancellation means: the work the caller
2158
+ // asked for is not going to happen.
2159
+ // Result and cancellation status come from the same row snapshot.
2160
+ return (observed.status === "cancelled"
2161
+ ? new Flow.Complete({ exit: Exit.failCause(Cause.interrupt()) })
2162
+ : new Flow.Suspended({}));
2163
+ });
2164
+ const interrupt = Effect.fn("FlowEngine.interrupt")((_flow, executionId) => Effect.gen(function* () {
2165
+ yield* Effect.annotateCurrentSpan({ executionId });
2166
+ // Operator intent is recorded durably before the fiber interrupt so
2167
+ // the interruption handler can tell cancellation apart from shutdown
2168
+ // (issue #26), and so the request survives if this process dies
2169
+ // before the interrupt lands.
2170
+ //
2171
+ // The write used to be `Effect.ignore`d, which answered success for a
2172
+ // cancellation that was never recorded: the fiber interrupt that
2173
+ // follows is then read as a shutdown by `settleInterrupted`, the run
2174
+ // is RELEASED for reclaim rather than cancelled, another worker picks
2175
+ // it up, and the caller was told the run was cancelled. The failure is
2176
+ // typed instead, so the caller can retry against a state that is still
2177
+ // truthful — the run is still running and still cancellable.
2178
+ const nowMs = yield* Clock.currentTimeMillis.pipe(Effect.map(Math.floor));
2179
+ // The package's one lock order, the same one every settlement takes:
2180
+ // engine state first, then the journal. The descendant walk reads
2181
+ // engine state while holding the SQL writer, so the alternative would
2182
+ // deadlock against any fiber that ordered the two boundaries the
2183
+ // other way.
2184
+ const requested = yield* transactState(Effect.gen(function* () {
2185
+ yield* requestCancellation(executionId, nowMs);
2186
+ return [executionId, ...yield* requestCancelDescendants(executionId, nowMs)];
2187
+ }),
2188
+ // Capture the shared writer directly. SqlJournal.transact catches a
2189
+ // selected failure and can discard the rest of a composite cause.
2190
+ (write) => Effect.flatMap(Effect.exit(writer.write(write)), (exit) => exit)).pipe(Effect.catchCause((cause) => {
2191
+ // A composite is never a retryable CancelRequestFailed: retain
2192
+ // every original reason, including defect identity and interruption.
2193
+ const failure = (error) => new FlowRuntime.CancelRequestFailed({
2194
+ code: "cancel_request_failed",
2195
+ executionId,
2196
+ reason: error.message
2197
+ });
2198
+ if (cause.reasons.length === 1) {
2199
+ const reason = cause.reasons[0];
2200
+ if (Cause.isDieReason(reason) && reason.defect instanceof DatabaseError) {
2201
+ return Effect.fail(failure(reason.defect));
2202
+ }
2203
+ // The typed channel is the normalized writer/store/journal union.
2204
+ if (Cause.isFailReason(reason)) {
2205
+ return Effect.fail(failure(reason.error));
2206
+ }
2207
+ }
2208
+ // A composite is not the retryable request failure the typed
2209
+ // channel promises, so every typed reason is re-channelled as a
2210
+ // defect rather than cast through `CancelRequestFailed`. The error
2211
+ // objects, the accompanying defects and the interruptions all
2212
+ // survive; only the channel changes, and it changes to the one the
2213
+ // declared type actually admits.
2214
+ return Effect.failCause(asDefects(cause));
2215
+ }));
2216
+ // Only after the durable write succeeds may the ephemeral surface say
2217
+ // this instance was interrupted or stop its drive fiber. If the write
2218
+ // failed, changing either would make a typed failure observably mutate
2219
+ // the run (and could spuriously trigger the in-process child path).
2220
+ // The transaction above records the parent and every durable
2221
+ // descendant together. A run this process does not drive — parked
2222
+ // here, owned elsewhere, or already settled — therefore receives the
2223
+ // same child-flow handling without a partial-cascade state if any
2224
+ // request write fails. `requestCancel` is first-writer-wins, so the
2225
+ // owning driver's own cascade later writes nothing.
2226
+ yield* journal.whenCommitted(Effect.gen(function* () {
2227
+ for (const targetId of requested) {
2228
+ const instance = liveInstances.get(targetId);
2229
+ if (instance !== undefined)
2230
+ instance.interrupted = true;
2231
+ // Publication only sends the request. Awaiting user finalizers
2232
+ // here would make an outer commit uninterruptibly wait on them.
2233
+ yield* coordinator.requestInterrupt(targetId);
2234
+ }
2235
+ }));
2236
+ }));
2237
+ const scheduleResume = Effect.fn("FlowEngine.scheduleResume")((flowName, executionId, reason, sourceId) => transactState(Effect.gen(function* () {
2238
+ yield* Effect.annotateCurrentSpan({ executionId, flow: flowName, reason });
2239
+ const row = yield* store.get(executionId).pipe(Effect.catch((error) => error.code === "not_found_row"
2240
+ ? Effect.succeed(undefined)
2241
+ : Effect.die(error)));
2242
+ if (row === undefined)
2243
+ return;
2244
+ // A settled run is not woken and then refused: it is not woken. The
2245
+ // terminal reject used to live one layer down, in `claimAndActivate`,
2246
+ // so a timer armed before its run was cancelled and an external
2247
+ // trigger arriving after the run finished both wrote a
2248
+ // `wake-scheduled` decision into the journal of a run that can never
2249
+ // wake again — one per event, forever, on a durable channel.
2250
+ if (row.status === "completed" || row.status === "failed" || row.status === "cancelled")
2251
+ return;
2252
+ const state = yield* decodeState(row.stateJson);
2253
+ if (state.flowName !== flowName)
2254
+ return;
2255
+ const waiting = yield* engineState.waiting(executionId);
2256
+ if (Option.isSome(waiting) && waiting.value.reason === "quarantine") {
2257
+ if (reason !== "operator")
2258
+ return;
2259
+ // Recovery consent and its journal receipt commit together. A
2260
+ // queued drive cannot consume quarantined evidence before consent.
2261
+ yield* engineState.wake(executionId);
2262
+ }
2263
+ yield* emitDecision(executionId, {
2264
+ decision: "wake-scheduled",
2265
+ reason
2266
+ }, sourceId);
2267
+ return { parked: row.status === "suspended" };
2268
+ })).pipe(Effect.orDie,
2269
+ // A host can write a separate database. Release the engine transaction
2270
+ // before calling it, but record its request before publishing the wake.
2271
+ Effect.flatMap((scheduled) => scheduled === undefined
2272
+ ? Effect.void
2273
+ : (reason === "operator" || reason === "delegated"
2274
+ ? Effect.void
2275
+ : recordResume(executionId, scheduled.parked, reason)).pipe(Effect.andThen(coordinator.wake(executionId)), Effect.andThen(wakeBus.wake(executionId))))));
2276
+ return {
2277
+ register: Effect.fn("FlowEngine.register")((flow, handler) => Effect.acquireRelease(Effect.sync(() => {
2278
+ const registration = { flow, execute: handler };
2279
+ const entries = registrations.get(flow._tag) ?? [];
2280
+ entries.push(registration);
2281
+ registrations.set(flow._tag, entries);
2282
+ pendingCycles.delete(flow._tag);
2283
+ warnedUnregistered.clear();
2284
+ return { registration, entries };
2285
+ }), ({ registration, entries }) => Effect.sync(() => {
2286
+ const wasLatest = entries.at(-1) === registration;
2287
+ entries.splice(entries.indexOf(registration), 1);
2288
+ if (entries.length === 0)
2289
+ registrations.delete(flow._tag);
2290
+ if (wasLatest) {
2291
+ pendingCycles.delete(flow._tag);
2292
+ // Restoring a live predecessor changes which handler may
2293
+ // drive pending work, just like a fresh registration does.
2294
+ if (entries.length > 0)
2295
+ warnedUnregistered.clear();
2296
+ }
2297
+ })).pipe(Effect.asVoid)),
2298
+ execute,
2299
+ poll,
2300
+ interrupt,
2301
+ // The durable engine has ONE cancellation path (the release policy). The
2302
+ // port promises forced cancellation without cleanup; nothing here can
2303
+ // deliver that — a durable run is closed by a fenced transition whose
2304
+ // finalizers, cascade, and interruption record are the point — and
2305
+ // answering with `interrupt` silently reinterpreted the caller's
2306
+ // request as the safe one. Refusing is what keeps the excluded feature
2307
+ // from appearing to half-work; `interrupt` is the supported request and
2308
+ // is still available to the same caller. The memory engine, which does
2309
+ // have two behaviors, is unchanged.
2310
+ //
2311
+ // The reason is the release policy "Durable interruptUnsafe" verbatim, not a
2312
+ // paraphrase of it: the release note and the failure a caller catches
2313
+ // are the same statement, so an operator who reads one and a developer
2314
+ // who logs the other are reading the same sentence.
2315
+ interruptUnsafe: Effect.fn("FlowEngine.interruptUnsafe")((_flow, executionId) => Effect.fail(new FlowRuntime.CancelRequestFailed({
2316
+ code: "unsafe_interrupt_unsupported",
2317
+ executionId,
2318
+ reason: "The durable engine has one cancellation path, interrupt, which is durable and cascades to linked children. FlowRuntime.interruptUnsafe on the durable engine fails with unsafe_interrupt_unsupported instead of forcing cancellation without cleanup."
2319
+ }))),
2320
+ resume: Effect.fn("FlowEngine.resume")((flow, executionId, options) => Effect.annotateCurrentSpan({ executionId, flow: flow._tag }).pipe(
2321
+ // A resume drives the run under its admitted authority, so a caller
2322
+ // whose authority does not cover it may not wake or join it, and a
2323
+ // request that names no admitted run of this flow drives nothing.
2324
+ Effect.andThen(authorizeResume(flow, executionId)), Effect.flatMap((authorized) => authorized
2325
+ ? Effect.andThen(options?.poll === true
2326
+ ? Effect.void
2327
+ : scheduleResume(flow._tag, executionId, options?.delegated === true ? "delegated" : "operator"), coordinator.run(executionId))
2328
+ : Effect.void))),
2329
+ scheduleResume,
2330
+ active: Effect.fn("FlowEngine.active")(() => coordinator.active)(),
2331
+ retainedRuns
2332
+ };
2333
+ });
2334
+ //# sourceMappingURL=RunDriver.js.map