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

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 +199 -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 +1584 -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 +298 -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 +3152 -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,2656 @@
1
+ /**
2
+ * Durable deferred-completion and clock-deadline state used by the flow
3
+ * engine adapter.
4
+ *
5
+ * A parked run keeps the `suspended` status the run store admits; there is
6
+ * no `waiting` status. The waiting taxonomy lives beside it on the run row
7
+ * in the `waiting_reason`, `waiting_wake_at_ms`, `waiting_token`, and
8
+ * `waiting_request` columns, written by `park` and cleared by `wake`. See
9
+ * `docs/concepts/durable-waits.md`.
10
+ *
11
+ * @since 0.1.0
12
+ */
13
+
14
+ import * as Dialect from "@smthrs/database/Dialect"
15
+ import { afterCommit, DatabaseError, DurableWriter } from "@smthrs/database/DurableWriter"
16
+ import { contentDigest } from "@smthrs/journal/JournalEvent"
17
+ import type { OwnerId } from "@smthrs/run-store/Ownership"
18
+ import * as Cause from "effect/Cause"
19
+ import * as Context from "effect/Context"
20
+ import * as Effect from "effect/Effect"
21
+ import * as Exit from "effect/Exit"
22
+ import * as Layer from "effect/Layer"
23
+ import * as Option from "effect/Option"
24
+ import * as Result from "effect/Result"
25
+ import * as Schema from "effect/Schema"
26
+ import * as Semaphore from "effect/Semaphore"
27
+ import * as SqlClient from "effect/unstable/sql/SqlClient"
28
+ import * as EngineStateSchema from "./internal/EngineStateSchema.ts"
29
+ import { compareText } from "./internal/Ordering.ts"
30
+ import type { OnParentExit } from "./RunState.ts"
31
+
32
+ /**
33
+ * A position in pending admission creation order.
34
+ *
35
+ * @since 1.0.0
36
+ * @category models
37
+ */
38
+ export interface PendingRunCursor {
39
+ readonly runId: string
40
+ readonly createdAtMs: number
41
+ }
42
+
43
+ const pendingCancellationLimit = (limit: number): Effect.Effect<void> =>
44
+ Number.isSafeInteger(limit) && limit >= 0
45
+ ? Effect.void
46
+ : Effect.die(new RangeError("Pending cancellation limit must be a non-negative safe integer"))
47
+
48
+ /** JSON text carrying an arbitrary decoded value. */
49
+ const UnknownFromJsonString = Schema.fromJsonString(Schema.Unknown)
50
+
51
+ /** @private */
52
+ const NonNegativeSafeInt = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))
53
+
54
+ /**
55
+ * The durable address of a deferred result.
56
+ *
57
+ * @since 0.1.0
58
+ * @category models
59
+ */
60
+ export const DeferredAddress = Schema.Struct({
61
+ flowName: Schema.NonEmptyString,
62
+ executionId: Schema.NonEmptyString,
63
+ deferredName: Schema.NonEmptyString
64
+ })
65
+
66
+ /**
67
+ * The durable address of a deferred result.
68
+ *
69
+ * @category models
70
+ * @since 0.1.0
71
+ */
72
+ export type DeferredAddress = typeof DeferredAddress.Type
73
+
74
+ /**
75
+ * The first durable completion recorded for a deferred.
76
+ *
77
+ * Correlation data remains opaque at this boundary. A future external-event
78
+ * layer owns its schema and authorization policy.
79
+ *
80
+ * @since 0.1.0
81
+ * @category models
82
+ */
83
+ export const DeferredRow = Schema.Struct({
84
+ ...DeferredAddress.fields,
85
+ exit: Schema.Unknown,
86
+ metadata: Schema.optionalKey(Schema.Unknown),
87
+ completedAtMs: NonNegativeSafeInt
88
+ })
89
+
90
+ /**
91
+ * The first durable completion recorded for a deferred.
92
+ *
93
+ * @category models
94
+ * @since 0.1.0
95
+ */
96
+ export type DeferredRow = typeof DeferredRow.Type
97
+
98
+ /**
99
+ * Result of a first-writer-wins deferred completion.
100
+ *
101
+ * @since 0.1.0
102
+ * @category models
103
+ */
104
+ export type CompleteDeferredOutcome =
105
+ | { readonly _tag: "Completed"; readonly row: DeferredRow; readonly digest: DeferredDigest }
106
+ | { readonly _tag: "Existing"; readonly row: DeferredRow; readonly digest: DeferredDigest }
107
+
108
+ /**
109
+ * The `JournalEvent.contentDigest` of a stored completion's encoded `exit`
110
+ * and `metadata` text. The journal fact that announces a completion carries
111
+ * these digests instead of the values, which stay in the row.
112
+ *
113
+ * @since 1.0.0
114
+ * @category models
115
+ */
116
+ export interface DeferredDigest {
117
+ readonly exit: string
118
+ readonly metadata: string | null
119
+ }
120
+
121
+ /**
122
+ * The durable address of a clock.
123
+ *
124
+ * @since 0.1.0
125
+ * @category models
126
+ */
127
+ export const ClockAddress = Schema.Struct({
128
+ flowName: Schema.NonEmptyString,
129
+ executionId: Schema.NonEmptyString,
130
+ clockName: Schema.NonEmptyString
131
+ })
132
+
133
+ /**
134
+ * The durable address of a clock.
135
+ *
136
+ * @category models
137
+ * @since 0.1.0
138
+ */
139
+ export type ClockAddress = typeof ClockAddress.Type
140
+
141
+ /**
142
+ * A durable absolute clock deadline.
143
+ *
144
+ * @since 0.1.0
145
+ * @category models
146
+ */
147
+ export const ClockRow = Schema.Struct({
148
+ ...ClockAddress.fields,
149
+ deferredName: Schema.NonEmptyString,
150
+ dueAtMs: NonNegativeSafeInt,
151
+ completedAtMs: Schema.NullOr(NonNegativeSafeInt)
152
+ })
153
+
154
+ /**
155
+ * A durable absolute clock deadline.
156
+ *
157
+ * @category models
158
+ * @since 0.1.0
159
+ */
160
+ export type ClockRow = typeof ClockRow.Type
161
+
162
+ /**
163
+ * Result of scheduling a durable clock.
164
+ *
165
+ * @since 0.1.0
166
+ * @category models
167
+ */
168
+ export type ScheduleClockOutcome =
169
+ | { readonly _tag: "Scheduled"; readonly row: ClockRow }
170
+ | { readonly _tag: "Existing"; readonly row: ClockRow }
171
+
172
+ /**
173
+ * Result of completing a durable clock.
174
+ *
175
+ * @since 0.1.0
176
+ * @category models
177
+ */
178
+ export type CompleteClockOutcome =
179
+ | { readonly _tag: "Completed"; readonly row: ClockRow }
180
+ | { readonly _tag: "AlreadyCompleted"; readonly row: ClockRow }
181
+ | { readonly _tag: "NotFound" }
182
+
183
+ /**
184
+ * The core wait-reason vocabulary a supervisor understands for wake policy.
185
+ *
186
+ * - `approval` — parked until a human (or policy) approves; woken by token.
187
+ * - `event` — parked on an external event / deferred completion.
188
+ * - `timer` — parked until `wakeAt`; woken by the durable clock sweep.
189
+ * - `quota` — parked on provider quota; woken at `wakeAt` when known.
190
+ * - `released` — the owning process released the run without settling it
191
+ * (shutdown, heartbeat self-interrupt; issue #39). A released row has no
192
+ * held lease and no `wakeAt`: any supervisor/sweeper MUST scan for this
193
+ * reason explicitly and re-drive the run through the claim path, or
194
+ * released runs are stranded forever (issue #67).
195
+ * - `quarantine` — parked because a SUCCEEDED attempt row's recorded
196
+ * evidence measured corrupt under the strict verdict (issue #171). The
197
+ * row cannot be evicted/re-executed like a cache row (its side effects
198
+ * already ran), so the corrupt boundary evidence is quarantined off the
199
+ * row while its durable outcome stays intact. No sweeper auto-wakes this
200
+ * reason: the strict integrity verdict remains visible until an explicit
201
+ * resume, which completes from the recorded outcome without re-execution.
202
+ *
203
+ * Left open (`string & {}`) so a plugin can park on a reason the core
204
+ * taxonomy has not named yet — the store persists whatever it is given
205
+ * rather than rejecting unknown reasons.
206
+ *
207
+ * @since 0.1.0
208
+ * @category models
209
+ */
210
+ export const WaitingReason = Schema.NonEmptyString
211
+
212
+ /**
213
+ * The open wait-reason vocabulary.
214
+ *
215
+ * @category models
216
+ * @since 0.1.0
217
+ */
218
+ export type WaitingReason = typeof WaitingReason.Type
219
+
220
+ /**
221
+ * The payload recorded when a run parks.
222
+ *
223
+ * `reason` and `wakeAt` earn columns because a supervisor sweeper queries
224
+ * them (`WHERE waiting_reason = 'quota' AND waiting_wake_at_ms <= ?`);
225
+ * `token` is compare-and-swap/lookup material a wake handler matches
226
+ * against.
227
+ *
228
+ * @since 0.1.0
229
+ * @category models
230
+ */
231
+ export const Waiting = Schema.Struct({
232
+ reason: WaitingReason,
233
+ wakeAt: Schema.optional(NonNegativeSafeInt),
234
+ token: Schema.optional(Schema.NonEmptyString),
235
+ /**
236
+ * What the wait declared about itself: for a `HumanTask` park, the kind of
237
+ * answer it wants, the prompt, and the attempt budget.
238
+ *
239
+ * `reason` and `token` say that a person owes the run an answer and which
240
+ * wait point holds it. Neither says what was asked, and the question lives
241
+ * in an action payload of an execution that has released its owner, so a
242
+ * reader had nothing to render but the wait's own name. This is the question
243
+ * itself, as the JSON TEXT the declaring flow wrote, stored and cleared with
244
+ * the other waiting columns.
245
+ */
246
+ request: Schema.optional(Schema.String)
247
+ })
248
+
249
+ /**
250
+ * The payload recorded when a run parks.
251
+ *
252
+ * @category models
253
+ * @since 0.1.0
254
+ */
255
+ export type Waiting = typeof Waiting.Type
256
+
257
+ /**
258
+ * A decoded waiting row for a parked run.
259
+ *
260
+ * @since 0.1.0
261
+ * @category models
262
+ */
263
+ export const WaitingRow = Schema.Struct({
264
+ runId: Schema.NonEmptyString,
265
+ reason: WaitingReason,
266
+ wakeAt: Schema.NullOr(NonNegativeSafeInt),
267
+ token: Schema.NullOr(Schema.NonEmptyString),
268
+ /** The question {@link Waiting} declared, absent on a park that declared none. */
269
+ request: Schema.optionalKey(Schema.Json)
270
+ })
271
+
272
+ /**
273
+ * A decoded waiting row for a parked run.
274
+ *
275
+ * @category models
276
+ * @since 0.1.0
277
+ */
278
+ export type WaitingRow = typeof WaitingRow.Type
279
+
280
+ /**
281
+ * A `running` row whose owner stopped heartbeating, and the lease it froze on.
282
+ *
283
+ * The heartbeat identifies the lease: a sweep that remembers a refusal against
284
+ * one heartbeat must not apply it to a later stall of the same run.
285
+ *
286
+ * @category models
287
+ * @since 1.0.0
288
+ */
289
+ export interface StaleRunningRow {
290
+ readonly runId: string
291
+ readonly heartbeatAtMs: number
292
+ }
293
+
294
+ /**
295
+ * A predicate over `waitingRuns` — omitted fields are unconstrained.
296
+ *
297
+ * @since 0.1.0
298
+ * @category models
299
+ */
300
+ export interface WaitingRunsFilter {
301
+ readonly reason?: string
302
+ readonly dueBeforeMs?: number
303
+ /**
304
+ * When `true`, only parked runs whose cancellation was durably requested
305
+ * (`flows_runs.cancel_requested_at_ms IS NOT NULL`). Lets the parked-run
306
+ * sweep fetch actionable rows instead of scanning every parked run and
307
+ * probing each with a `store.get` (issue #68). The in-memory
308
+ * implementation reads the flag from the `runs` view
309
+ * (`MemoryRunView.cancelRequestedAtMs`); without a view it stays
310
+ * permissive and the sweeper's own per-row guard decides.
311
+ */
312
+ readonly cancelRequested?: boolean
313
+ }
314
+
315
+ /**
316
+ * Result of recording that a run parked on a waiting reason.
317
+ *
318
+ * @since 0.1.0
319
+ * @category models
320
+ */
321
+ export type ParkOutcome =
322
+ | { readonly _tag: "Parked"; readonly row: WaitingRow }
323
+ | { readonly _tag: "NotFound" }
324
+
325
+ /**
326
+ * Result of clearing a run's waiting payload on wake.
327
+ *
328
+ * @since 0.1.0
329
+ * @category models
330
+ */
331
+ export type WakeOutcome =
332
+ | { readonly _tag: "Woken"; readonly row: WaitingRow }
333
+ | { readonly _tag: "NotWaiting" }
334
+ | { readonly _tag: "NotFound" }
335
+
336
+ /**
337
+ * A durable parent edge in the run DAG.
338
+ *
339
+ * The run row's `state_json` carries only the first (creating) parent; a
340
+ * diamond gives a run a second parent the row cannot express. Cycle
341
+ * detection must see every edge from every owner process and across
342
+ * restarts (issues #40/#41), so edges are persisted here. `seq` is a
343
+ * store-global insertion order used only to list a run's parents oldest
344
+ * first (and to make the cycle walk deterministic); it no longer arbitrates
345
+ * anything, so duplicate values would be harmless (issues #54/#66).
346
+ *
347
+ * @since 0.1.0
348
+ * @category models
349
+ */
350
+ export const RunParentEdge = Schema.Struct({
351
+ childId: Schema.NonEmptyString,
352
+ parentId: Schema.NonEmptyString,
353
+ seq: NonNegativeSafeInt
354
+ })
355
+
356
+ /**
357
+ * A durable parent edge in the run DAG.
358
+ *
359
+ * @category models
360
+ * @since 0.1.0
361
+ */
362
+ export type RunParentEdge = typeof RunParentEdge.Type
363
+
364
+ /**
365
+ * The surviving attempt rows of one action key: the earliest surviving
366
+ * row's start time and the highest attempt number contiguous from it.
367
+ *
368
+ * @since 0.1.0
369
+ * @category models
370
+ */
371
+ export const AttemptSurvivors = Schema.Struct({
372
+ /**
373
+ * The lowest surviving attempt number. Anything above 1 means the prune
374
+ * job removed a leading run of attempts; the engine store applies the
375
+ * pruned-prefix tolerance to it so both survivor paths agree (issue #96).
376
+ */
377
+ earliestAttempt: NonNegativeSafeInt,
378
+ earliestStartedAtMs: NonNegativeSafeInt,
379
+ latest: NonNegativeSafeInt
380
+ })
381
+
382
+ /**
383
+ * The surviving attempt range of one action key.
384
+ *
385
+ * @category models
386
+ * @since 0.1.0
387
+ */
388
+ export type AttemptSurvivors = typeof AttemptSurvivors.Type
389
+
390
+ /**
391
+ * Recording a parent edge would close a cycle in the run DAG.
392
+ *
393
+ * Raised from inside the same transaction that inserted the edge, which is
394
+ * rolled back: a rejected edge leaves no durable trace anywhere (issues
395
+ * #54/#55/#56). `path` lists execution ids from the child back to itself
396
+ * through its would-be ancestors, matching the engine's
397
+ * `FlowCycleDetected.path` shape.
398
+ *
399
+ * @since 0.1.0
400
+ * @category errors
401
+ */
402
+ export class RunParentCycleError extends Schema.TaggedError<RunParentCycleError>()(
403
+ "@smthrs/engine-store/RunParentCycleError",
404
+ {
405
+ path: Schema.Array(Schema.String)
406
+ }
407
+ ) {}
408
+
409
+ /**
410
+ * A durable engine-state invariant broke: a defect (a bug), never a typed
411
+ * failure.
412
+ *
413
+ * `value_not_serializable` and `value_not_decodable` name the JSON column
414
+ * (`field`) whose value could not be written or read back.
415
+ * `deferred_completion_missing` and `run_parent_edge_missing` mean a row a
416
+ * first-writer transaction just lost to was gone when read.
417
+ *
418
+ * @since 1.0.0
419
+ * @category errors
420
+ */
421
+ export class EngineStateFault extends Schema.TaggedError<EngineStateFault>()(
422
+ "@smthrs/engine-store/EngineStateFault",
423
+ {
424
+ reason: Schema.Literals([
425
+ "value_not_serializable",
426
+ "value_not_decodable",
427
+ "deferred_completion_missing",
428
+ "run_parent_edge_missing"
429
+ ]),
430
+ field: Schema.optional(Schema.String),
431
+ cause: Schema.optional(Schema.Defect())
432
+ }
433
+ ) {
434
+ override get message(): string {
435
+ switch (this.reason) {
436
+ case "value_not_serializable":
437
+ return `${this.field} must be JSON-serializable`
438
+ case "value_not_decodable":
439
+ return `could not decode ${this.field}`
440
+ case "deferred_completion_missing":
441
+ return "deferred completion disappeared during first-writer transaction"
442
+ case "run_parent_edge_missing":
443
+ return "run parent edge disappeared during first-writer transaction"
444
+ }
445
+ }
446
+ }
447
+
448
+ /**
449
+ * Result of recording a durable parent edge (idempotent).
450
+ *
451
+ * @since 0.1.0
452
+ * @category models
453
+ */
454
+ export type RecordRunParentOutcome =
455
+ | { readonly _tag: "Recorded"; readonly edge: RunParentEdge }
456
+ | { readonly _tag: "Existing"; readonly edge: RunParentEdge }
457
+
458
+ /**
459
+ * Minimal durable state missing from the current `@smthrs/journal` contract.
460
+ *
461
+ * A successful mutation means the row is durable. Callers may therefore
462
+ * journal and schedule a wake only after the mutation returns. Deferred and
463
+ * clock state belong to this package for 1.0.
464
+ *
465
+ * @since 0.1.0
466
+ * @category models
467
+ */
468
+ export interface Service {
469
+ readonly deferred: (address: DeferredAddress) => Effect.Effect<Option.Option<DeferredRow>>
470
+ /**
471
+ * Marks an existing result observed without removing its replay evidence.
472
+ * Returns whether this call consumed it: false when there is no result or
473
+ * it was already consumed.
474
+ */
475
+ readonly consumeDeferred: (address: DeferredAddress, consumedAtMs: number) => Effect.Effect<boolean>
476
+ readonly completeDeferred: (row: DeferredRow) => Effect.Effect<CompleteDeferredOutcome>
477
+ readonly clock: (address: ClockAddress) => Effect.Effect<Option.Option<ClockRow>>
478
+ /**
479
+ * Schedules a clock while the execution is running under the supplied owner.
480
+ * An existing clock wins unchanged, even after ownership is lost. If no
481
+ * clock exists and the ownership fence fails, the effect self-interrupts.
482
+ */
483
+ readonly scheduleClock: (row: ClockRow, owner: OwnerId) => Effect.Effect<ScheduleClockOutcome>
484
+ readonly completeClock: (
485
+ address: ClockAddress,
486
+ completedAtMs: number
487
+ ) => Effect.Effect<CompleteClockOutcome>
488
+ readonly dueClocks: (nowMs: number) => Effect.Effect<ReadonlyArray<ClockRow>>
489
+ /**
490
+ * Completes every uncompleted clock row of one run in a single statement.
491
+ *
492
+ * A run that settled has no future: a timer that fires against it can only
493
+ * schedule a resume nothing will drive. Terminal transitions call this in
494
+ * their own transaction, so the rows stop being pending with the run rather
495
+ * than lingering as durable work no sweep will ever finish.
496
+ *
497
+ * It takes the run id rather than a clock address because the caller is
498
+ * closing a RUN, and it does not read the run row, so it composes with a
499
+ * terminal transition in either order.
500
+ */
501
+ readonly completeRunClocks: (
502
+ executionId: string,
503
+ completedAtMs: number
504
+ ) => Effect.Effect<ReadonlyArray<ClockRow>>
505
+ /**
506
+ * Lists uncompleted clock rows scoped to an execution or flow, with no
507
+ * due-time bound. Suspension-reason derivation and registration-time
508
+ * recovery use this instead of abusing `dueClocks` as an all-clocks
509
+ * listing (issue #35): `dueClocks` is a due-timer sweeper query and may
510
+ * grow a result cap without affecting park correctness.
511
+ *
512
+ * Rows belonging to a settled run are never listed. Registration sweeps
513
+ * this to re-arm timers, so without the run-status join every restart armed
514
+ * one timer per clock row a flow ever wrote, for runs that ended long ago.
515
+ */
516
+ readonly pendingClocks: (scope: {
517
+ readonly executionId?: string
518
+ readonly flowName?: string
519
+ }) => Effect.Effect<ReadonlyArray<ClockRow>>
520
+ /**
521
+ * Lists unconsumed completed deferred addresses for registration-time wake recovery.
522
+ * Observed results remain readable through `deferred` but cannot re-drive a
523
+ * run that has moved on to a later wait.
524
+ *
525
+ * Rows belonging to a settled run are never listed, for the reason
526
+ * {@link Service.pendingClocks} gives: this is swept on every registration,
527
+ * and one resume per completion in a flow's whole history is unbounded work
528
+ * that can wake nothing.
529
+ */
530
+ readonly completedDeferreds: (
531
+ flowName: string
532
+ ) => Effect.Effect<ReadonlyArray<DeferredAddress>>
533
+ /**
534
+ * Records the waiting-reason payload for a parked run, fenced to the
535
+ * current owner so a stale process cannot park a run it no longer runs.
536
+ */
537
+ readonly park: (
538
+ runId: string,
539
+ waiting: Waiting,
540
+ owner: OwnerId
541
+ ) => Effect.Effect<ParkOutcome>
542
+ /**
543
+ * Clears a run's waiting payload on wake or resume. Idempotent: waking a
544
+ * run that is not waiting reports `NotWaiting` rather than failing.
545
+ */
546
+ readonly wake: (runId: string) => Effect.Effect<WakeOutcome>
547
+ /**
548
+ * Reads the current waiting payload for a run, if any.
549
+ */
550
+ readonly waiting: (runId: string) => Effect.Effect<Option.Option<WaitingRow>>
551
+ /**
552
+ * Reads the waiting payloads of a run AND of every execution beneath it.
553
+ *
554
+ * A flow that calls another flow parks the CHILD execution, not the run an
555
+ * operator named: `coding/request` sat at `waiting_reason = 'event'` while
556
+ * the `HumanTask` a person actually had to answer was three executions down,
557
+ * on `coding/PreparePlan`. Reading one row therefore answered "nothing is
558
+ * waiting on you" about a run tree that was waiting on a person, which is
559
+ * how run-3 parked forever with an empty approvals inbox.
560
+ *
561
+ * The walk follows every `flows_run_parents` spawn edge and `parent_run_id`
562
+ * link, so it sees shared children, forks, and trampoline rounds alike.
563
+ * Each run appears once, ordered by minimum depth, creation time, then run
564
+ * ID; the named run comes first. Rows of a settled execution are never
565
+ * listed — a wait a finished execution left behind is not one a person
566
+ * can answer.
567
+ *
568
+ * A DETACHED child and everything under it are skipped. `onParentExit:
569
+ * "detach"` is a fire-and-forget spawn whose whole point is to outlive the
570
+ * run that started it, so its question is not one the run that started it is
571
+ * waiting on, and reporting it here would tell a reader that a run which can
572
+ * proceed cannot. Every other edge is attached: the run is waiting for that
573
+ * child's value, whether its own row has flipped to `suspended` yet or not.
574
+ * Trampoline links continue the same run, so an inherited detach policy
575
+ * does not hide later rounds from that run's own tree.
576
+ *
577
+ * `{ detached: "include" }` walks detached subtrees too, for a caller asking
578
+ * what an explicit resume of the run would restart rather than what the run
579
+ * itself waits on.
580
+ */
581
+ readonly waitingTree: (
582
+ runId: string,
583
+ options?: WaitingTreeOptions | undefined
584
+ ) => Effect.Effect<ReadonlyArray<WaitingRow>>
585
+ /**
586
+ * Lists parked runs matching an optional reason/due-before filter, ordered
587
+ * for sweeper consumption (earliest wake first, untimed waits last, then
588
+ * run id to break ties).
589
+ */
590
+ readonly waitingRuns: (
591
+ filter?: WaitingRunsFilter
592
+ ) => Effect.Effect<ReadonlyArray<WaitingRow>>
593
+ /** Pending admissions ordered by creation, strictly after and through optional cursors. */
594
+ readonly pendingRuns: (
595
+ flowName: string,
596
+ limit?: number | undefined,
597
+ after?: PendingRunCursor | undefined,
598
+ through?: PendingRunCursor | undefined,
599
+ eligibleBeforeMs?: number | undefined
600
+ ) => Effect.Effect<ReadonlyArray<PendingRunCursor>>
601
+ /** Latest pending admission, bounding one finite recovery scan cycle. */
602
+ readonly pendingRunTail: (
603
+ flowName: string,
604
+ eligibleBeforeMs?: number | undefined
605
+ ) => Effect.Effect<Option.Option<PendingRunCursor>>
606
+ /** Pending cancellation requests in bounded creation order; limit is zero or a positive safe integer. */
607
+ readonly pendingCancellationRuns: (
608
+ limit: number,
609
+ after?: PendingRunCursor | undefined,
610
+ through?: PendingRunCursor | undefined
611
+ ) => Effect.Effect<ReadonlyArray<PendingRunCursor>>
612
+ /** Latest pending cancellation, bounding one finite cleanup scan cycle. */
613
+ readonly pendingCancellationTail: () => Effect.Effect<Option.Option<PendingRunCursor>>
614
+ /**
615
+ * Lists the runs whose row is `running` with a heartbeat strictly older
616
+ * than `staleBeforeMs`, each with that heartbeat — an owner that stopped heartbeating without
617
+ * releasing the run (SIGKILL, OOM, power loss). Nothing else ever
618
+ * revisits such a run: it has no waiting row for the parked-run sweep,
619
+ * no pending clock, and no future deferred completion. A periodic
620
+ * sweeper re-drives these through the ordinary claim/steal path — the
621
+ * analog of Temporal's task-timeout re-dispatch (issue #53).
622
+ *
623
+ * `limit` caps one enumeration (issue #79): the rows come back oldest
624
+ * heartbeat first, so a capped sweep drains a mass owner death across
625
+ * ticks instead of waking every stale run in every driver every tick.
626
+ */
627
+ readonly staleRunningRuns: (
628
+ staleBeforeMs: number,
629
+ limit?: number | undefined
630
+ ) => Effect.Effect<ReadonlyArray<StaleRunningRow>>
631
+ /**
632
+ * The surviving attempt rows for an action key, in one range read: the
633
+ * earliest surviving row's start time (the durable retry origin when
634
+ * attempt 1 itself was pruned, issue #69) and the highest attempt number
635
+ * contiguous from it (the resumed attempt counter, issue #59).
636
+ * `Option.none()` means no attempt row survives.
637
+ *
638
+ * Optional because only storage that can range-scan `flows_attempts`
639
+ * implements it; when absent the engine store falls back to per-attempt
640
+ * point reads against `AttemptStore` (issue #77).
641
+ */
642
+ readonly attemptSurvivors?:
643
+ | ((
644
+ runId: string,
645
+ stepKeyDigest: string
646
+ ) => Effect.Effect<Option.Option<AttemptSurvivors>>)
647
+ | undefined
648
+ /**
649
+ * Durably records a parent edge in the run DAG, first-writer-wins per
650
+ * `(child, parent)` pair.
651
+ *
652
+ * The cycle check happens inside the same transaction as the insert: the
653
+ * edge is inserted, the child's ancestor chain is walked over the durable
654
+ * edges, and on a hit the transaction rolls back and the call fails with
655
+ * `RunParentCycleError`. There is no window between insert and check, no
656
+ * arbitration, and no withdrawal protocol — concurrent writers that
657
+ * jointly close a cycle serialize on the write transaction and exactly
658
+ * one of them fails (issues #29/#40/#54/#55/#56).
659
+ */
660
+ readonly recordRunParent: (
661
+ childId: string,
662
+ parentId: string
663
+ ) => Effect.Effect<RecordRunParentOutcome, RunParentCycleError>
664
+ /**
665
+ * Removes every parent edge touching a run — both the edges naming it as
666
+ * child and the edges naming it as parent. The cleanup half of run
667
+ * deletion (issue #66): a lane that deletes run rows (time-travel,
668
+ * retention) must call this so `runParents` and the cycle walk stop
669
+ * seeing edges of runs that no longer exist.
670
+ */
671
+ readonly removeRunParentsForRun: (runId: string) => Effect.Effect<void>
672
+ /**
673
+ * Lists the durably recorded parent edges of a run, oldest first.
674
+ */
675
+ readonly runParents: (childId: string) => Effect.Effect<ReadonlyArray<RunParentEdge>>
676
+ /**
677
+ * Lists the durably recorded child edges of a run, oldest first.
678
+ *
679
+ * The reverse direction of `runParents`, and the only instance-independent
680
+ * way to find the runs a cancelled parent linked to itself. Cancellation
681
+ * cascade reads it rather than an in-process instance map, so a
682
+ * cross-process cancel observed by a driver that never spawned the children
683
+ * still reaches them (`docs/concepts/durable-waits.md`, "The run DAG").
684
+ * Served by the
685
+ * `flows_run_parents_parent_idx` index.
686
+ */
687
+ readonly runChildren: (parentId: string) => Effect.Effect<ReadonlyArray<RunParentEdge>>
688
+ /**
689
+ * Runs `effect` inside one storage write transaction, so a caller can make
690
+ * several store operations atomic — the run driver wraps the parent-edge
691
+ * record and the run-row creation it guards, closing the crash window that
692
+ * left a durable orphan edge for a run that was never created (issue #80).
693
+ * Nested store writes become savepoints of this transaction. Storage
694
+ * failures are defects, matching the store methods' own posture.
695
+ *
696
+ * The in-memory twin honors the same contract: it serializes every store
697
+ * operation behind one permit, snapshots its state maps on entry, restores
698
+ * them when the effect fails or dies, and treats a nested call on the same
699
+ * fiber as a savepoint of the outer one. What it cannot offer is
700
+ * durability: a process crash mid-transaction loses the whole state, not
701
+ * only the uncommitted part.
702
+ *
703
+ * **Lock order.** A caller that needs this transaction AND the journal's
704
+ * write transaction opens this one OUTSIDE. The SQL implementation cannot
705
+ * tell the difference, because both are the same `DurableWriter`
706
+ * transaction and either nests as a savepoint of the other; the memory twin
707
+ * has a permit of its own, and a fiber that holds the journal's writer
708
+ * while waiting for that permit deadlocks against a fiber doing the
709
+ * opposite. Interruption is not a way out of that cycle: both boundaries
710
+ * are acquired at the caller's own interruptibility, so a settlement that
711
+ * masked interruption to finish its write keeps waiting. The same
712
+ * rule applies to every single operation above, each of which takes the
713
+ * permit for itself: inside an open journal transaction, call them only
714
+ * under an enclosing `transaction` on the same fiber.
715
+ */
716
+ readonly transaction: <A, E, R>(
717
+ effect: Effect.Effect<A, E, R>,
718
+ options?: {
719
+ /**
720
+ * Receives an idempotent memory commit effect. A coordinating durable
721
+ * transaction may run it after COMMIT, before publishing or capturing
722
+ * state. This accepts the snapshot and releases its gate; subsequent
723
+ * publication failures cannot restore the old snapshot. Without an
724
+ * external boundary, returning successfully commits as usual. SQL owns
725
+ * its commit boundary and does not need this callback.
726
+ */
727
+ readonly onCommit: (commit: Effect.Effect<void>) => void
728
+ }
729
+ ) => Effect.Effect<A, E, R>
730
+ }
731
+
732
+ /**
733
+ * Service tag for durable deferred completions and absolute clock deadlines.
734
+ *
735
+ * @since 0.1.0
736
+ * @category services
737
+ */
738
+ export class DurableEngineState extends Context.Service<DurableEngineState, Service>()(
739
+ "@smthrs/engine-store/DurableEngineState"
740
+ ) {}
741
+
742
+ /** @private */
743
+ const deferredKey = (address: DeferredAddress): string =>
744
+ JSON.stringify([address.flowName, address.executionId, address.deferredName])
745
+
746
+ /** @private */
747
+ const clockKey = (address: ClockAddress): string =>
748
+ JSON.stringify([address.flowName, address.executionId, address.clockName])
749
+
750
+ /** @private */
751
+ const DeferredDatabaseRow = Schema.Struct({
752
+ ...DeferredAddress.fields,
753
+ exitJson: Schema.String,
754
+ metadataJson: Schema.NullOr(Schema.String),
755
+ completedAtMs: NonNegativeSafeInt
756
+ })
757
+
758
+ type DeferredDatabaseRow = typeof DeferredDatabaseRow.Type
759
+
760
+ /** @private */
761
+ const ClockDatabaseRow = Schema.Struct({
762
+ ...ClockAddress.fields,
763
+ deferredName: Schema.NonEmptyString,
764
+ dueAtMs: NonNegativeSafeInt,
765
+ completedAtMs: Schema.NullOr(NonNegativeSafeInt)
766
+ })
767
+
768
+ type ClockDatabaseRow = typeof ClockDatabaseRow.Type
769
+
770
+ /** @private */
771
+ const WaitingDatabaseRow = Schema.Struct({
772
+ runId: Schema.NonEmptyString,
773
+ waitingReason: WaitingReason,
774
+ waitingWakeAtMs: Schema.NullOr(NonNegativeSafeInt),
775
+ waitingToken: Schema.NullOr(Schema.NonEmptyString),
776
+ /** Absent rather than null on a database written before `0004_waiting_request`. */
777
+ waitingRequest: Schema.optional(Schema.NullOr(Schema.String))
778
+ })
779
+
780
+ type WaitingDatabaseRow = typeof WaitingDatabaseRow.Type
781
+
782
+ /**
783
+ * The declared question of a waiting row, or absence.
784
+ *
785
+ * The column is the wait's own JSON, so text that does not parse is text the
786
+ * writer never wrote: it is reported as no declared question rather than
787
+ * failing the read of a park that is otherwise intact.
788
+ */
789
+ const decodeWaitingRequest = (value: string | null | undefined): { readonly request?: unknown } => {
790
+ if (value === null || value === undefined) return {}
791
+ try {
792
+ return { request: JSON.parse(value) as unknown }
793
+ } catch {
794
+ return {}
795
+ }
796
+ }
797
+
798
+ const decodeWaitingRow = (input: unknown): Effect.Effect<WaitingRow> => decodeWaitingRowResult(input).pipe(Effect.orDie)
799
+
800
+ /** {@link decodeWaitingRow} with its parse failure left in the error channel. */
801
+ const decodeWaitingRowResult = (input: unknown): Effect.Effect<WaitingRow, unknown> =>
802
+ Schema.decodeUnknownEffect(WaitingDatabaseRow)(input).pipe(
803
+ Effect.map((row) => ({
804
+ runId: row.runId,
805
+ reason: row.waitingReason,
806
+ wakeAt: row.waitingWakeAtMs,
807
+ token: row.waitingToken,
808
+ ...decodeWaitingRequest(row.waitingRequest)
809
+ } as WaitingRow))
810
+ )
811
+
812
+ /** @private */
813
+ const RunParentDatabaseRow = Schema.Struct({
814
+ childId: Schema.NonEmptyString,
815
+ parentId: Schema.NonEmptyString,
816
+ seq: NonNegativeSafeInt
817
+ })
818
+
819
+ type RunParentDatabaseRow = typeof RunParentDatabaseRow.Type
820
+
821
+ const decodeRunParentEdge = (input: unknown): Effect.Effect<RunParentEdge> =>
822
+ decodeRunParentEdgeResult(input).pipe(Effect.orDie)
823
+
824
+ /** {@link decodeRunParentEdge} with its parse failure left in the error channel. */
825
+ const decodeRunParentEdgeResult = (input: unknown): Effect.Effect<RunParentEdge, unknown> =>
826
+ Schema.decodeUnknownEffect(RunParentDatabaseRow)(input)
827
+
828
+ /**
829
+ * Walks the parent chain upward from `parentId` over the durable edges,
830
+ * looking for `childId`. Returns the cycle path the new `(childId,
831
+ * parentId)` edge would close — ids from the child back to itself, matching
832
+ * the engine's `FlowCycleDetected.path` shape — or `undefined` when the
833
+ * edge is safe. A repeated id (pre-existing corrupt cycle written outside
834
+ * this API) terminates that branch instead of looping forever.
835
+ */
836
+ const findCyclePath = (
837
+ childId: string,
838
+ parentId: string,
839
+ parentsOf: (id: string) => Effect.Effect<ReadonlyArray<string>>
840
+ ): Effect.Effect<ReadonlyArray<string> | undefined> =>
841
+ Effect.suspend(() => {
842
+ if (parentId === childId) {
843
+ return Effect.succeed<ReadonlyArray<string> | undefined>([childId])
844
+ }
845
+ const seen = new Set<string>([parentId])
846
+ const walk = (
847
+ current: string,
848
+ chain: ReadonlyArray<string>
849
+ ): Effect.Effect<ReadonlyArray<string> | undefined> =>
850
+ Effect.gen(function*() {
851
+ const parents = yield* parentsOf(current)
852
+ for (const parent of parents) {
853
+ if (parent === childId) return [...chain, parent].reverse()
854
+ if (seen.has(parent)) continue
855
+ seen.add(parent)
856
+ const found = yield* walk(parent, [...chain, parent])
857
+ if (found !== undefined) return found
858
+ }
859
+ return undefined
860
+ })
861
+ return walk(parentId, [parentId])
862
+ })
863
+
864
+ /** @private */
865
+ const DeferredAddressDatabaseRow = DeferredAddress
866
+
867
+ /** The digests of a stored completion row's encoded text. */
868
+ const deferredDigest = (row: {
869
+ readonly exitJson: string
870
+ readonly metadataJson: string | null
871
+ }): DeferredDigest => ({
872
+ exit: contentDigest(row.exitJson),
873
+ metadata: row.metadataJson === null ? null : contentDigest(row.metadataJson)
874
+ })
875
+
876
+ /** The digests of a memory completion row, over the text the SQL store would keep. */
877
+ const memoryDigest = (row: DeferredRow): Effect.Effect<DeferredDigest> =>
878
+ Effect.gen(function*() {
879
+ const exitJson = yield* encodeJson(row.exit, "exit")
880
+ const metadataJson = row.metadata === undefined ? null : yield* encodeJson(row.metadata, "metadata")
881
+ return deferredDigest({ exitJson, metadataJson })
882
+ })
883
+
884
+ const encodeJson = (value: unknown, field: string): Effect.Effect<string> =>
885
+ Schema.encodeEffect(UnknownFromJsonString)(value).pipe(
886
+ Effect.mapError((cause) => new EngineStateFault({ reason: "value_not_serializable", field, cause })),
887
+ Effect.orDie
888
+ )
889
+
890
+ const decodeJson = (value: string, field: string): Effect.Effect<unknown> =>
891
+ Schema.decodeUnknownEffect(UnknownFromJsonString)(value).pipe(
892
+ Effect.mapError((cause) => new EngineStateFault({ reason: "value_not_decodable", field, cause })),
893
+ Effect.orDie
894
+ )
895
+
896
+ /**
897
+ * Rehydrates Effect's JSON representation after a storage round-trip.
898
+ *
899
+ * `Exit` and `Cause` deliberately serialize as plain JSON objects. Leaving a
900
+ * failure in that form is unsafe: the success branch happens to be rebuilt by
901
+ * `Exit.map`, while a plain failure is not an Exit and the deferred's schema
902
+ * rejects it after restart. Unknown non-Exit payloads remain untouched for
903
+ * the generic durable-state contract.
904
+ */
905
+ const restoreJsonExit = (value: unknown): unknown => {
906
+ if (typeof value !== "object" || value === null) return value
907
+ const record = value as Record<string, unknown>
908
+ if (record["_id"] !== "Exit") return value
909
+ if (record["_tag"] === "Success") return Exit.succeed(record["value"])
910
+ if (record["_tag"] !== "Failure") return value
911
+ const cause = record["cause"]
912
+ if (typeof cause !== "object" || cause === null) return value
913
+ const failures = (cause as Record<string, unknown>)["failures"]
914
+ if (!Array.isArray(failures)) return value
915
+ const reasons: Array<Cause.Reason<unknown>> = []
916
+ for (const failure of failures) {
917
+ if (typeof failure !== "object" || failure === null) return value
918
+ const reason = failure as Record<string, unknown>
919
+ switch (reason["_tag"]) {
920
+ case "Fail":
921
+ reasons.push(Cause.makeFailReason(reason["error"]))
922
+ break
923
+ case "Die":
924
+ reasons.push(Cause.makeDieReason(reason["defect"]))
925
+ break
926
+ case "Interrupt":
927
+ reasons.push(Cause.makeInterruptReason(
928
+ typeof reason["fiberId"] === "number" ? reason["fiberId"] : undefined
929
+ ))
930
+ break
931
+ default:
932
+ return value
933
+ }
934
+ }
935
+ return Exit.failCause(Cause.fromReasons(reasons))
936
+ }
937
+
938
+ const decodeDeferredRow = (input: unknown): Effect.Effect<DeferredRow> =>
939
+ Schema.decodeUnknownEffect(DeferredDatabaseRow)(input).pipe(
940
+ Effect.orDie,
941
+ Effect.flatMap((row) =>
942
+ Effect.all({
943
+ exit: decodeJson(row.exitJson, "exit_json").pipe(Effect.map(restoreJsonExit)),
944
+ metadata: row.metadataJson === null
945
+ ? Effect.succeed(undefined)
946
+ : decodeJson(row.metadataJson, "metadata_json")
947
+ }).pipe(
948
+ Effect.map(({ exit, metadata }) => ({
949
+ flowName: row.flowName,
950
+ executionId: row.executionId,
951
+ deferredName: row.deferredName,
952
+ exit,
953
+ ...(metadata === undefined ? {} : { metadata }),
954
+ completedAtMs: row.completedAtMs
955
+ }))
956
+ )
957
+ )
958
+ )
959
+
960
+ const decodeClockRow = (input: unknown): Effect.Effect<ClockRow> => decodeClockRowResult(input).pipe(Effect.orDie)
961
+
962
+ /** {@link decodeClockRow} with its parse failure left in the error channel. */
963
+ const decodeClockRowResult = (input: unknown): Effect.Effect<ClockRow, unknown> =>
964
+ Schema.decodeUnknownEffect(ClockDatabaseRow)(input).pipe(
965
+ Effect.map((row) => ({
966
+ flowName: row.flowName,
967
+ executionId: row.executionId,
968
+ clockName: row.clockName,
969
+ deferredName: row.deferredName,
970
+ dueAtMs: row.dueAtMs,
971
+ completedAtMs: row.completedAtMs
972
+ }))
973
+ )
974
+
975
+ /** The primary key of a clock row, read defensively off a row that may not decode. */
976
+ const clockRowKey = (row: Record<string, unknown>): string =>
977
+ JSON.stringify([row["flowName"], row["executionId"], row["clockName"]])
978
+
979
+ /** The primary key of a deferred completion, read the same way. */
980
+ const deferredAddressRowKey = (row: Record<string, unknown>): string =>
981
+ JSON.stringify([row["flowName"], row["executionId"], row["deferredName"]])
982
+
983
+ /** The primary key of a run-parent edge, read the same way. */
984
+ const runParentRowKey = (row: Record<string, unknown>): string => JSON.stringify([row["childId"], row["parentId"]])
985
+
986
+ /**
987
+ * How far {@link Service.waitingTree} walks: detached subtrees are skipped
988
+ * unless `detached` is `"include"`.
989
+ *
990
+ * @since 1.0.0
991
+ * @category models
992
+ */
993
+ export interface WaitingTreeOptions {
994
+ readonly detached?: "skip" | "include" | undefined
995
+ }
996
+
997
+ /**
998
+ * The deepest nesting {@link Service.waitingTree} walks.
999
+ *
1000
+ * A run tree is authored, not generated: `agent/run` → `coding/request` →
1001
+ * `coding/Request` → `coding/PrepareWithWiki` → `coding/PreparePlan` is five,
1002
+ * and the deepest composition in this repository is well inside this. The cap
1003
+ * exists so a tree whose edges were corrupted outside the engine costs a
1004
+ * bounded read rather than the whole table.
1005
+ *
1006
+ * @since 1.0.0
1007
+ * @category constants
1008
+ */
1009
+ export const waitingTreeMaxDepth = 64
1010
+
1011
+ /** The primary key of a parked run's waiting payload, read the same way. */
1012
+ const waitingRowKey = (row: Record<string, unknown>): string => JSON.stringify([row["runId"]])
1013
+
1014
+ /**
1015
+ * Decodes the rows one durable-state sweep read, skipping — with a
1016
+ * storage-integrity warning naming the row's key — any row that will not
1017
+ * decode.
1018
+ *
1019
+ * Decoding the batch through `Effect.orDie` made one malformed row fatal for
1020
+ * every row beside it: `completedDeferreds` and `pendingClocks` are what
1021
+ * `register` sweeps, so a single unreadable row anywhere in a flow's history
1022
+ * killed every registration of that flow, and no run of it ever resumed again.
1023
+ * The blast radius of a corrupt row must be that row. The key is logged
1024
+ * because it is what an operator needs to find and repair it, and it is read
1025
+ * field by field off the raw row so a row that failed to decode can still name
1026
+ * itself.
1027
+ *
1028
+ * Every enumeration goes through here for that reason, not just the two the
1029
+ * outage named: `dueClocks`, `waitingRuns`, `runParents`, and `runChildren`
1030
+ * each had their own version of it — no timer in the store fires, no parked
1031
+ * run is ever swept, no cycle check can read a run's ancestry, no
1032
+ * cancellation reaches a child.
1033
+ *
1034
+ * The point reads (`deferred`, `clock`, `waiting`) deliberately stay on
1035
+ * `Effect.orDie`. They answer a question about ONE row, and their callers
1036
+ * read "no row" as "this never happened": silently reporting that for a
1037
+ * completion or deadline that is durably recorded but unreadable would
1038
+ * re-run work whose side effects already ran. There the corrupt row IS the
1039
+ * blast radius, so dying is the correct-sized failure.
1040
+ */
1041
+ const decodeSweep = <A>(
1042
+ rows: ReadonlyArray<Record<string, unknown>>,
1043
+ kind: string,
1044
+ keyOf: (row: Record<string, unknown>) => string,
1045
+ decode: (row: Record<string, unknown>) => Effect.Effect<A, unknown>
1046
+ ): Effect.Effect<ReadonlyArray<A>> =>
1047
+ Effect.gen(function*() {
1048
+ const decoded: Array<A> = []
1049
+ for (const row of rows) {
1050
+ const result = yield* Effect.result(decode(row))
1051
+ if (Result.isSuccess(result)) {
1052
+ decoded.push(result.success)
1053
+ continue
1054
+ }
1055
+ yield* Effect.logWarning(
1056
+ `engine-store: skipping a malformed ${kind} row ${keyOf(row)}`,
1057
+ result.failure
1058
+ )
1059
+ }
1060
+ return decoded
1061
+ })
1062
+
1063
+ /**
1064
+ * Demotes a write boundary's storage failures to defects, reason by reason.
1065
+ *
1066
+ * `transaction` declares the BODY's error type, and the shared writer adds
1067
+ * `DatabaseError` to it. A sole storage failure is already a defect; a
1068
+ * composite one used to be cast back through the declared channel, which let
1069
+ * a raw `DatabaseError` surface as a typed failure of a type that never
1070
+ * admits one. Re-channelling keeps the error object, its annotations, the
1071
+ * other reasons and the interruptions intact while making the declared type
1072
+ * true. Every other reason is passed through UNCHANGED rather than rebuilt,
1073
+ * so a body failure keeps its own identity and spans.
1074
+ */
1075
+ const dieOnDatabaseError = <E>(cause: Cause.Cause<E | DatabaseError>): Cause.Cause<E> =>
1076
+ Cause.fromReasons(
1077
+ cause.reasons.map((reason) =>
1078
+ Cause.isFailReason(reason) && reason.error instanceof DatabaseError
1079
+ ? Cause.makeDieReason(reason.error).annotate(Cause.reasonAnnotations(reason))
1080
+ // Narrowing a generic union by `instanceof` is beyond the checker, but
1081
+ // the guard above is the proof: what is left is a defect, an
1082
+ // interruption, or a failure whose error is not a `DatabaseError`.
1083
+ : reason as Cause.Reason<E>
1084
+ )
1085
+ )
1086
+
1087
+ /**
1088
+ * Constructs the database-backed durable-state implementation.
1089
+ *
1090
+ * Clock creation is fenced against the current run owner. Deferred completion
1091
+ * and clock firing are external trigger admissions protected by first-writer
1092
+ * and compare-and-set semantics; execution remains claim-gated by `RunStore`.
1093
+ *
1094
+ * @since 0.1.0
1095
+ * @category constructors
1096
+ */
1097
+ export const make: Effect.Effect<Service, never, DurableWriter | SqlClient.SqlClient> = Effect.gen(function*() {
1098
+ const sql = yield* Effect.service(SqlClient.SqlClient)
1099
+ const writer = yield* DurableWriter
1100
+
1101
+ // Engine-store-owned storage created outside the journal migration
1102
+ // (issues #40/#41/#79/#81). The statements, their rationale, and
1103
+ // the dialects each is known to accept live in one machine-readable
1104
+ // inventory so the pg-porting plan cannot omit them (issue #92).
1105
+ yield* EngineStateSchema.apply(sql, writer)
1106
+
1107
+ const selectDeferred = (address: DeferredAddress) =>
1108
+ sql<DeferredDatabaseRow>`
1109
+ SELECT
1110
+ flow_name AS "flowName",
1111
+ execution_id AS "executionId",
1112
+ deferred_name AS "deferredName",
1113
+ exit_json AS "exitJson",
1114
+ metadata_json AS "metadataJson",
1115
+ completed_at_ms AS "completedAtMs"
1116
+ FROM flows_deferred_completions
1117
+ WHERE flow_name = ${address.flowName}
1118
+ AND execution_id = ${address.executionId}
1119
+ AND deferred_name = ${address.deferredName}
1120
+ `
1121
+
1122
+ const selectClock = (address: ClockAddress) =>
1123
+ sql<ClockDatabaseRow>`
1124
+ SELECT
1125
+ flow_name AS "flowName",
1126
+ execution_id AS "executionId",
1127
+ clock_name AS "clockName",
1128
+ deferred_name AS "deferredName",
1129
+ due_at_ms AS "dueAtMs",
1130
+ completed_at_ms AS "completedAtMs"
1131
+ FROM flows_clock_deadlines
1132
+ WHERE flow_name = ${address.flowName}
1133
+ AND execution_id = ${address.executionId}
1134
+ AND clock_name = ${address.clockName}
1135
+ `
1136
+
1137
+ const deferred: Service["deferred"] = Effect.fn("DurableEngineState.deferred")((address) =>
1138
+ selectDeferred(address).pipe(
1139
+ Effect.orDie,
1140
+ Effect.flatMap((rows) =>
1141
+ rows[0] === undefined
1142
+ ? Effect.succeedNone
1143
+ : Effect.map(decodeDeferredRow(rows[0]), Option.some)
1144
+ )
1145
+ )
1146
+ )
1147
+
1148
+ const consumeDeferred: Service["consumeDeferred"] = Effect.fn("DurableEngineState.consumeDeferred")((
1149
+ address,
1150
+ consumedAtMs
1151
+ ) =>
1152
+ writer.write(sql<{ readonly flowName: string }>`
1153
+ UPDATE flows_deferred_completions
1154
+ SET consumed_at_ms = ${consumedAtMs}
1155
+ WHERE flow_name = ${address.flowName}
1156
+ AND execution_id = ${address.executionId}
1157
+ AND deferred_name = ${address.deferredName}
1158
+ AND consumed_at_ms IS NULL
1159
+ RETURNING flow_name AS "flowName"
1160
+ `).pipe(Effect.orDie, Effect.map((rows) => rows.length > 0))
1161
+ )
1162
+
1163
+ const completeDeferred: Service["completeDeferred"] = Effect.fn(
1164
+ "DurableEngineState.completeDeferred"
1165
+ )((row) =>
1166
+ Effect.gen(function*() {
1167
+ const exitJson = yield* encodeJson(row.exit, "exit")
1168
+ const metadataJson = row.metadata === undefined
1169
+ ? null
1170
+ : yield* encodeJson(row.metadata, "metadata")
1171
+ return yield* writer.write(
1172
+ Effect.gen(function*() {
1173
+ const inserted = yield* sql<DeferredDatabaseRow>`
1174
+ INSERT INTO flows_deferred_completions (
1175
+ flow_name,
1176
+ execution_id,
1177
+ deferred_name,
1178
+ exit_json,
1179
+ metadata_json,
1180
+ completed_at_ms
1181
+ ) VALUES (
1182
+ ${row.flowName},
1183
+ ${row.executionId},
1184
+ ${row.deferredName},
1185
+ ${exitJson},
1186
+ ${metadataJson},
1187
+ ${row.completedAtMs}
1188
+ )
1189
+ ON CONFLICT (flow_name, execution_id, deferred_name) DO NOTHING
1190
+ RETURNING
1191
+ flow_name AS "flowName",
1192
+ execution_id AS "executionId",
1193
+ deferred_name AS "deferredName",
1194
+ exit_json AS "exitJson",
1195
+ metadata_json AS "metadataJson",
1196
+ completed_at_ms AS "completedAtMs"
1197
+ `
1198
+ if (inserted[0] !== undefined) {
1199
+ return {
1200
+ _tag: "Completed" as const,
1201
+ row: yield* decodeDeferredRow(inserted[0]),
1202
+ digest: deferredDigest(inserted[0])
1203
+ }
1204
+ }
1205
+ const existing = yield* selectDeferred(row)
1206
+ if (existing[0] === undefined) {
1207
+ return yield* Effect.die(new EngineStateFault({ reason: "deferred_completion_missing" }))
1208
+ }
1209
+ return {
1210
+ _tag: "Existing" as const,
1211
+ row: yield* decodeDeferredRow(existing[0]),
1212
+ digest: deferredDigest(existing[0])
1213
+ }
1214
+ })
1215
+ ).pipe(Effect.orDie)
1216
+ })
1217
+ )
1218
+
1219
+ const clock: Service["clock"] = Effect.fn("DurableEngineState.clock")((address) =>
1220
+ selectClock(address).pipe(
1221
+ Effect.orDie,
1222
+ Effect.flatMap((rows) =>
1223
+ rows[0] === undefined
1224
+ ? Effect.succeedNone
1225
+ : Effect.map(decodeClockRow(rows[0]), Option.some)
1226
+ )
1227
+ )
1228
+ )
1229
+
1230
+ const scheduleClock: Service["scheduleClock"] = Effect.fn("DurableEngineState.scheduleClock")((row, owner) =>
1231
+ writer.write(
1232
+ Effect.gen(function*() {
1233
+ const inserted = yield* sql<ClockDatabaseRow>`
1234
+ INSERT INTO flows_clock_deadlines (
1235
+ flow_name,
1236
+ execution_id,
1237
+ clock_name,
1238
+ deferred_name,
1239
+ due_at_ms,
1240
+ completed_at_ms
1241
+ )
1242
+ SELECT
1243
+ ${row.flowName},
1244
+ ${row.executionId},
1245
+ ${row.clockName},
1246
+ ${row.deferredName},
1247
+ ${row.dueAtMs},
1248
+ ${row.completedAtMs}
1249
+ WHERE EXISTS (
1250
+ SELECT 1
1251
+ FROM flows_runs
1252
+ WHERE run_id = ${row.executionId}
1253
+ AND status = 'running'
1254
+ AND owner_host_id = ${owner.hostId}
1255
+ AND owner_pid = ${owner.pid}
1256
+ AND owner_nonce = ${owner.nonce}
1257
+ )
1258
+ ON CONFLICT (flow_name, execution_id, clock_name) DO NOTHING
1259
+ RETURNING
1260
+ flow_name AS "flowName",
1261
+ execution_id AS "executionId",
1262
+ clock_name AS "clockName",
1263
+ deferred_name AS "deferredName",
1264
+ due_at_ms AS "dueAtMs",
1265
+ completed_at_ms AS "completedAtMs"
1266
+ `
1267
+ if (inserted[0] !== undefined) {
1268
+ return {
1269
+ _tag: "Scheduled" as const,
1270
+ row: yield* decodeClockRow(inserted[0])
1271
+ }
1272
+ }
1273
+ const existing = yield* selectClock(row)
1274
+ if (existing[0] !== undefined) {
1275
+ return {
1276
+ _tag: "Existing" as const,
1277
+ row: yield* decodeClockRow(existing[0])
1278
+ }
1279
+ }
1280
+ return yield* Effect.interrupt
1281
+ })
1282
+ ).pipe(Effect.orDie)
1283
+ )
1284
+
1285
+ const completeClock: Service["completeClock"] = Effect.fn("DurableEngineState.completeClock")((
1286
+ address,
1287
+ completedAtMs
1288
+ ) =>
1289
+ writer.write(
1290
+ Effect.gen(function*() {
1291
+ const updated = yield* sql<ClockDatabaseRow>`
1292
+ UPDATE flows_clock_deadlines
1293
+ SET completed_at_ms = ${completedAtMs}
1294
+ WHERE flow_name = ${address.flowName}
1295
+ AND execution_id = ${address.executionId}
1296
+ AND clock_name = ${address.clockName}
1297
+ AND completed_at_ms IS NULL
1298
+ RETURNING
1299
+ flow_name AS "flowName",
1300
+ execution_id AS "executionId",
1301
+ clock_name AS "clockName",
1302
+ deferred_name AS "deferredName",
1303
+ due_at_ms AS "dueAtMs",
1304
+ completed_at_ms AS "completedAtMs"
1305
+ `
1306
+ if (updated[0] !== undefined) {
1307
+ return {
1308
+ _tag: "Completed" as const,
1309
+ row: yield* decodeClockRow(updated[0])
1310
+ }
1311
+ }
1312
+ const existing = yield* selectClock(address)
1313
+ return existing[0] === undefined
1314
+ ? { _tag: "NotFound" as const }
1315
+ : {
1316
+ _tag: "AlreadyCompleted" as const,
1317
+ row: yield* decodeClockRow(existing[0])
1318
+ }
1319
+ })
1320
+ ).pipe(Effect.orDie)
1321
+ )
1322
+
1323
+ const dueClocks: Service["dueClocks"] = Effect.fn("DurableEngineState.dueClocks")((nowMs) =>
1324
+ sql<ClockDatabaseRow>`
1325
+ SELECT
1326
+ flow_name AS "flowName",
1327
+ execution_id AS "executionId",
1328
+ clock_name AS "clockName",
1329
+ deferred_name AS "deferredName",
1330
+ due_at_ms AS "dueAtMs",
1331
+ completed_at_ms AS "completedAtMs"
1332
+ FROM flows_clock_deadlines
1333
+ WHERE completed_at_ms IS NULL
1334
+ AND due_at_ms <= ${nowMs}
1335
+ ORDER BY due_at_ms, execution_id, clock_name
1336
+ `.pipe(
1337
+ Effect.orDie,
1338
+ // This is the whole store's due-timer sweep: dying on one unreadable
1339
+ // deadline stopped every timer in every flow from firing.
1340
+ Effect.flatMap((rows) => decodeSweep(rows, "clock deadline", clockRowKey, decodeClockRowResult))
1341
+ )
1342
+ )
1343
+
1344
+ const pendingClocks: Service["pendingClocks"] = Effect.fn("DurableEngineState.pendingClocks")((scope) =>
1345
+ (scope.executionId !== undefined
1346
+ ? sql<ClockDatabaseRow>`
1347
+ SELECT
1348
+ flow_name AS "flowName",
1349
+ execution_id AS "executionId",
1350
+ clock_name AS "clockName",
1351
+ deferred_name AS "deferredName",
1352
+ due_at_ms AS "dueAtMs",
1353
+ completed_at_ms AS "completedAtMs"
1354
+ FROM flows_clock_deadlines
1355
+ WHERE completed_at_ms IS NULL
1356
+ AND execution_id = ${scope.executionId}
1357
+ AND ${scope.flowName === undefined ? sql`1 = 1` : sql`flow_name = ${scope.flowName}`}
1358
+ AND EXISTS (
1359
+ SELECT 1 FROM flows_runs
1360
+ WHERE flows_runs.run_id = flows_clock_deadlines.execution_id
1361
+ AND flows_runs.status NOT IN ('completed', 'failed', 'cancelled')
1362
+ )
1363
+ ORDER BY due_at_ms, execution_id, clock_name
1364
+ `
1365
+ : scope.flowName !== undefined
1366
+ ? sql<ClockDatabaseRow>`
1367
+ SELECT
1368
+ flow_name AS "flowName",
1369
+ execution_id AS "executionId",
1370
+ clock_name AS "clockName",
1371
+ deferred_name AS "deferredName",
1372
+ due_at_ms AS "dueAtMs",
1373
+ completed_at_ms AS "completedAtMs"
1374
+ FROM flows_clock_deadlines
1375
+ WHERE completed_at_ms IS NULL
1376
+ AND flow_name = ${scope.flowName}
1377
+ AND EXISTS (
1378
+ SELECT 1 FROM flows_runs
1379
+ WHERE flows_runs.run_id = flows_clock_deadlines.execution_id
1380
+ AND flows_runs.status NOT IN ('completed', 'failed', 'cancelled')
1381
+ )
1382
+ ORDER BY due_at_ms, execution_id, clock_name
1383
+ `
1384
+ : sql<ClockDatabaseRow>`
1385
+ SELECT
1386
+ flow_name AS "flowName",
1387
+ execution_id AS "executionId",
1388
+ clock_name AS "clockName",
1389
+ deferred_name AS "deferredName",
1390
+ due_at_ms AS "dueAtMs",
1391
+ completed_at_ms AS "completedAtMs"
1392
+ FROM flows_clock_deadlines
1393
+ WHERE completed_at_ms IS NULL
1394
+ AND EXISTS (
1395
+ SELECT 1 FROM flows_runs
1396
+ WHERE flows_runs.run_id = flows_clock_deadlines.execution_id
1397
+ AND flows_runs.status NOT IN ('completed', 'failed', 'cancelled')
1398
+ )
1399
+ ORDER BY due_at_ms, execution_id, clock_name
1400
+ `).pipe(
1401
+ Effect.orDie,
1402
+ // A row that will not decode is a storage-integrity event about that
1403
+ // row. Dying here made one of them fatal for the whole sweep, and so
1404
+ // for `register` itself (issue B-08).
1405
+ Effect.flatMap((rows) => decodeSweep(rows, "clock deadline", clockRowKey, decodeClockRowResult))
1406
+ )
1407
+ )
1408
+
1409
+ const completedDeferreds: Service["completedDeferreds"] = Effect.fn(
1410
+ "DurableEngineState.completedDeferreds"
1411
+ )((flowName) =>
1412
+ sql<Record<string, unknown>>`
1413
+ SELECT
1414
+ flow_name AS "flowName",
1415
+ execution_id AS "executionId",
1416
+ deferred_name AS "deferredName"
1417
+ FROM flows_deferred_completions
1418
+ WHERE flow_name = ${flowName}
1419
+ AND consumed_at_ms IS NULL
1420
+ AND EXISTS (
1421
+ SELECT 1 FROM flows_runs
1422
+ WHERE flows_runs.run_id = flows_deferred_completions.execution_id
1423
+ AND flows_runs.status NOT IN ('completed', 'failed', 'cancelled')
1424
+ )
1425
+ ORDER BY execution_id, deferred_name
1426
+ `.pipe(
1427
+ Effect.orDie,
1428
+ Effect.flatMap((rows) =>
1429
+ decodeSweep(
1430
+ rows,
1431
+ "deferred completion",
1432
+ deferredAddressRowKey,
1433
+ (row) => Schema.decodeUnknownEffect(DeferredAddressDatabaseRow)(row)
1434
+ )
1435
+ )
1436
+ )
1437
+ )
1438
+
1439
+ const completeRunClocks: Service["completeRunClocks"] = Effect.fn(
1440
+ "DurableEngineState.completeRunClocks"
1441
+ )((executionId, completedAtMs) =>
1442
+ writer.write(sql<ClockDatabaseRow>`
1443
+ UPDATE flows_clock_deadlines
1444
+ SET completed_at_ms = ${completedAtMs}
1445
+ WHERE execution_id = ${executionId}
1446
+ AND completed_at_ms IS NULL
1447
+ RETURNING
1448
+ flow_name AS "flowName",
1449
+ execution_id AS "executionId",
1450
+ clock_name AS "clockName",
1451
+ deferred_name AS "deferredName",
1452
+ due_at_ms AS "dueAtMs",
1453
+ completed_at_ms AS "completedAtMs"
1454
+ `).pipe(
1455
+ Effect.orDie,
1456
+ Effect.flatMap((rows) => Effect.forEach(rows, decodeClockRow)),
1457
+ Effect.map((rows) =>
1458
+ [...rows].sort((left, right) =>
1459
+ compareText(left.flowName, right.flowName) || compareText(left.clockName, right.clockName)
1460
+ )
1461
+ )
1462
+ )
1463
+ )
1464
+
1465
+ const selectWaiting = (runId: string) =>
1466
+ sql<WaitingDatabaseRow>`
1467
+ SELECT
1468
+ run_id AS "runId",
1469
+ waiting_reason AS "waitingReason",
1470
+ waiting_wake_at_ms AS "waitingWakeAtMs",
1471
+ waiting_token AS "waitingToken",
1472
+ waiting_request AS "waitingRequest"
1473
+ FROM flows_runs
1474
+ WHERE run_id = ${runId}
1475
+ AND waiting_reason IS NOT NULL
1476
+ `
1477
+
1478
+ const park: Service["park"] = Effect.fn("DurableEngineState.park")((runId, waiting, owner) =>
1479
+ writer.write(
1480
+ Effect.gen(function*() {
1481
+ const updated = yield* sql<WaitingDatabaseRow>`
1482
+ UPDATE flows_runs
1483
+ SET
1484
+ waiting_reason = ${waiting.reason},
1485
+ waiting_wake_at_ms = ${waiting.wakeAt ?? null},
1486
+ waiting_token = ${waiting.token ?? null},
1487
+ waiting_request = ${waiting.request ?? null}
1488
+ WHERE run_id = ${runId}
1489
+ AND owner_host_id = ${owner.hostId}
1490
+ AND owner_pid = ${owner.pid}
1491
+ AND owner_nonce = ${owner.nonce}
1492
+ RETURNING
1493
+ run_id AS "runId",
1494
+ waiting_reason AS "waitingReason",
1495
+ waiting_wake_at_ms AS "waitingWakeAtMs",
1496
+ waiting_token AS "waitingToken",
1497
+ waiting_request AS "waitingRequest"
1498
+ `
1499
+ if (updated[0] === undefined) {
1500
+ return { _tag: "NotFound" as const }
1501
+ }
1502
+ return { _tag: "Parked" as const, row: yield* decodeWaitingRow(updated[0]) }
1503
+ })
1504
+ ).pipe(Effect.orDie)
1505
+ )
1506
+
1507
+ const wake: Service["wake"] = Effect.fn("DurableEngineState.wake")((runId) =>
1508
+ writer.write(
1509
+ Effect.gen(function*() {
1510
+ const before = yield* selectWaiting(runId)
1511
+ if (before[0] === undefined) {
1512
+ const existing = yield* sql<{ runId: string }>`
1513
+ SELECT run_id AS "runId" FROM flows_runs WHERE run_id = ${runId}
1514
+ `
1515
+ return existing[0] === undefined ? { _tag: "NotFound" as const } : { _tag: "NotWaiting" as const }
1516
+ }
1517
+ const row = yield* decodeWaitingRow(before[0])
1518
+ yield* sql`
1519
+ UPDATE flows_runs
1520
+ SET
1521
+ waiting_reason = NULL,
1522
+ waiting_wake_at_ms = NULL,
1523
+ waiting_token = NULL,
1524
+ waiting_request = NULL
1525
+ WHERE run_id = ${runId}
1526
+ AND waiting_reason IS NOT NULL
1527
+ `
1528
+ return { _tag: "Woken" as const, row }
1529
+ })
1530
+ ).pipe(Effect.orDie)
1531
+ )
1532
+
1533
+ const waiting: Service["waiting"] = Effect.fn("DurableEngineState.waiting")((runId) =>
1534
+ selectWaiting(runId).pipe(
1535
+ Effect.orDie,
1536
+ Effect.flatMap((rows) =>
1537
+ rows[0] === undefined
1538
+ ? Effect.succeedNone
1539
+ : Effect.map(decodeWaitingRow(rows[0]), Option.some)
1540
+ )
1541
+ )
1542
+ )
1543
+
1544
+ /**
1545
+ * Every open wait in one run tree, the named run's own first.
1546
+ *
1547
+ * Walk the authoritative spawn and trampoline edges. UNION visits each
1548
+ * (run, depth) pair once; the depth cap bounds even externally corrupted
1549
+ * cycles. Grouping by run afterwards collapses unequal-depth DAG paths
1550
+ * and orders each wait at its shortest attached distance from the root.
1551
+ * Correlated edge lookups seek each parent's indexes instead of building
1552
+ * an edge set from the database's entire execution history.
1553
+ */
1554
+ const waitingTree: Service["waitingTree"] = Effect.fn("DurableEngineState.waitingTree")((runId, options) =>
1555
+ sql<WaitingDatabaseRow>`
1556
+ WITH RECURSIVE tree(run_id, depth) AS (
1557
+ SELECT run_id, 0 FROM flows_runs WHERE run_id = ${runId}
1558
+ UNION
1559
+ SELECT child.run_id, tree.depth + 1
1560
+ FROM tree
1561
+ JOIN flows_runs child ON child.run_id IN (
1562
+ SELECT child_id FROM flows_run_parents WHERE parent_id = tree.run_id
1563
+ UNION ALL
1564
+ SELECT run_id FROM flows_runs WHERE parent_run_id = tree.run_id
1565
+ )
1566
+ WHERE tree.depth < ${waitingTreeMaxDepth}
1567
+ AND (${options?.detached === "include" ? 1 : 0} = 1
1568
+ OR child.parent_run_id = tree.run_id
1569
+ OR COALESCE(${Dialect.jsonText(sql, sql`child.state_json`, "$.onParentExit")}, 'cancel') <> 'detach')
1570
+ ), reachable(run_id, depth) AS (
1571
+ SELECT run_id, MIN(depth) FROM tree GROUP BY run_id
1572
+ )
1573
+ SELECT
1574
+ parked.run_id AS "runId",
1575
+ parked.waiting_reason AS "waitingReason",
1576
+ parked.waiting_wake_at_ms AS "waitingWakeAtMs",
1577
+ parked.waiting_token AS "waitingToken",
1578
+ parked.waiting_request AS "waitingRequest"
1579
+ FROM reachable
1580
+ JOIN flows_runs parked ON parked.run_id = reachable.run_id
1581
+ WHERE parked.waiting_reason IS NOT NULL
1582
+ AND parked.status NOT IN ('completed', 'failed', 'cancelled')
1583
+ ORDER BY reachable.depth, parked.created_at_ms, parked.run_id
1584
+ `.pipe(
1585
+ Effect.orDie,
1586
+ Effect.flatMap((rows) => decodeSweep(rows, "waiting run", waitingRowKey, decodeWaitingRowResult))
1587
+ )
1588
+ )
1589
+
1590
+ const waitingRuns: Service["waitingRuns"] = Effect.fn("DurableEngineState.waitingRuns")((filter) => {
1591
+ // `1 = 0` short-circuits the cancel predicate away when the caller did
1592
+ // not ask for it, so the reason/wake filters (and the 0004 index) stay
1593
+ // the leading predicates (issue #68).
1594
+ const cancelRequestedOnly = filter?.cancelRequested === true ? 1 : 0
1595
+ return (filter?.reason !== undefined && filter.dueBeforeMs !== undefined
1596
+ ? sql<WaitingDatabaseRow>`
1597
+ SELECT
1598
+ run_id AS "runId",
1599
+ waiting_reason AS "waitingReason",
1600
+ waiting_wake_at_ms AS "waitingWakeAtMs",
1601
+ waiting_token AS "waitingToken",
1602
+ waiting_request AS "waitingRequest"
1603
+ FROM flows_runs
1604
+ WHERE waiting_reason = ${filter.reason}
1605
+ AND waiting_wake_at_ms IS NOT NULL
1606
+ AND waiting_wake_at_ms <= ${filter.dueBeforeMs}
1607
+ AND (${cancelRequestedOnly} = 0 OR cancel_requested_at_ms IS NOT NULL)
1608
+ AND status NOT IN ('completed', 'failed', 'cancelled')
1609
+ ORDER BY waiting_wake_at_ms, run_id
1610
+ `
1611
+ : filter?.reason !== undefined
1612
+ ? sql<WaitingDatabaseRow>`
1613
+ SELECT
1614
+ run_id AS "runId",
1615
+ waiting_reason AS "waitingReason",
1616
+ waiting_wake_at_ms AS "waitingWakeAtMs",
1617
+ waiting_token AS "waitingToken",
1618
+ waiting_request AS "waitingRequest"
1619
+ FROM flows_runs
1620
+ WHERE waiting_reason = ${filter.reason}
1621
+ AND (${cancelRequestedOnly} = 0 OR cancel_requested_at_ms IS NOT NULL)
1622
+ AND status NOT IN ('completed', 'failed', 'cancelled')
1623
+ ORDER BY waiting_wake_at_ms IS NULL, waiting_wake_at_ms, run_id
1624
+ `
1625
+ : filter?.dueBeforeMs !== undefined
1626
+ ? sql<WaitingDatabaseRow>`
1627
+ SELECT
1628
+ run_id AS "runId",
1629
+ waiting_reason AS "waitingReason",
1630
+ waiting_wake_at_ms AS "waitingWakeAtMs",
1631
+ waiting_token AS "waitingToken",
1632
+ waiting_request AS "waitingRequest"
1633
+ FROM flows_runs
1634
+ WHERE waiting_reason IS NOT NULL
1635
+ AND waiting_wake_at_ms IS NOT NULL
1636
+ AND waiting_wake_at_ms <= ${filter.dueBeforeMs}
1637
+ AND (${cancelRequestedOnly} = 0 OR cancel_requested_at_ms IS NOT NULL)
1638
+ AND status NOT IN ('completed', 'failed', 'cancelled')
1639
+ ORDER BY waiting_wake_at_ms, run_id
1640
+ `
1641
+ : sql<WaitingDatabaseRow>`
1642
+ SELECT
1643
+ run_id AS "runId",
1644
+ waiting_reason AS "waitingReason",
1645
+ waiting_wake_at_ms AS "waitingWakeAtMs",
1646
+ waiting_token AS "waitingToken",
1647
+ waiting_request AS "waitingRequest"
1648
+ FROM flows_runs
1649
+ WHERE waiting_reason IS NOT NULL
1650
+ AND (${cancelRequestedOnly} = 0 OR cancel_requested_at_ms IS NOT NULL)
1651
+ AND status NOT IN ('completed', 'failed', 'cancelled')
1652
+ ORDER BY waiting_wake_at_ms IS NULL, waiting_wake_at_ms, run_id
1653
+ `).pipe(
1654
+ Effect.orDie,
1655
+ // The parked-run sweeper's only enumeration: one unreadable waiting
1656
+ // payload stranded every parked run, including the ones whose
1657
+ // cancellation had already been requested.
1658
+ Effect.flatMap((rows) => decodeSweep(rows, "waiting run", waitingRowKey, decodeWaitingRowResult))
1659
+ )
1660
+ })
1661
+
1662
+ const pendingRows = (
1663
+ flowName: string,
1664
+ limit?: number,
1665
+ after?: PendingRunCursor,
1666
+ through?: PendingRunCursor,
1667
+ descending = false,
1668
+ eligibleBeforeMs?: number
1669
+ ): Effect.Effect<ReadonlyArray<PendingRunCursor>> =>
1670
+ sql<{ readonly runId: string; readonly createdAtMs: number | bigint | string }>`
1671
+ SELECT run_id AS "runId", created_at_ms AS "createdAtMs" FROM flows_runs
1672
+ ${Dialect.isPostgres(sql) ? sql`` : sql`INDEXED BY flows_runs_listing_3`}
1673
+ WHERE status = 'pending'
1674
+ AND ${eligibleBeforeMs === undefined ? sql`TRUE` : sql`created_at_ms < ${eligibleBeforeMs}`}
1675
+ AND execution_flow = ${flowName}
1676
+ AND ${
1677
+ after === undefined ? sql`TRUE` : sql`(
1678
+ created_at_ms > ${after.createdAtMs}
1679
+ OR (created_at_ms = ${after.createdAtMs} AND run_id > ${after.runId})
1680
+ )`
1681
+ }
1682
+ AND ${
1683
+ through === undefined ? sql`TRUE` : sql`(
1684
+ created_at_ms < ${through.createdAtMs}
1685
+ OR (created_at_ms = ${through.createdAtMs} AND run_id <= ${through.runId})
1686
+ )`
1687
+ }
1688
+ ORDER BY ${descending ? sql`created_at_ms DESC, run_id DESC` : sql`created_at_ms, run_id`}
1689
+ LIMIT ${limit ?? (Dialect.isPostgres(sql) ? null : -1)}
1690
+ `.pipe(
1691
+ Effect.orDie,
1692
+ Effect.map((rows) => rows.map((row) => ({ runId: row.runId, createdAtMs: Number(row.createdAtMs) })))
1693
+ )
1694
+ const pendingRuns: Service["pendingRuns"] = Effect.fn("DurableEngineState.pendingRuns")((
1695
+ flowName,
1696
+ limit,
1697
+ after,
1698
+ through,
1699
+ eligibleBeforeMs
1700
+ ) => pendingRows(flowName, limit, after, through, false, eligibleBeforeMs))
1701
+ const pendingRunTail: Service["pendingRunTail"] = Effect.fn("DurableEngineState.pendingRunTail")((
1702
+ flowName,
1703
+ eligibleBeforeMs
1704
+ ) =>
1705
+ pendingRows(flowName, 1, undefined, undefined, true, eligibleBeforeMs).pipe(
1706
+ Effect.map((rows) => Option.fromNullishOr(rows[0]))
1707
+ )
1708
+ )
1709
+
1710
+ const pendingCancellationRows = (
1711
+ limit: number,
1712
+ after?: PendingRunCursor,
1713
+ through?: PendingRunCursor,
1714
+ descending = false
1715
+ ): Effect.Effect<ReadonlyArray<PendingRunCursor>> =>
1716
+ pendingCancellationLimit(limit).pipe(
1717
+ Effect.andThen(sql<{ readonly runId: string; readonly createdAtMs: number | bigint | string }>`
1718
+ SELECT run_id AS "runId", created_at_ms AS "createdAtMs" FROM flows_runs
1719
+ ${Dialect.isPostgres(sql) ? sql`` : sql`INDEXED BY flows_runs_pending_cancel_idx`}
1720
+ WHERE status = 'pending' AND cancel_requested_at_ms IS NOT NULL
1721
+ AND ${after === undefined ? sql`TRUE` : sql`(created_at_ms, run_id) > (${after.createdAtMs}, ${after.runId})`}
1722
+ AND ${
1723
+ through === undefined ? sql`TRUE` : sql`(created_at_ms, run_id) <= (${through.createdAtMs}, ${through.runId})`
1724
+ }
1725
+ ORDER BY ${descending ? sql`created_at_ms DESC, run_id DESC` : sql`created_at_ms, run_id`}
1726
+ LIMIT ${limit}
1727
+ `.pipe(
1728
+ Effect.orDie,
1729
+ Effect.map((rows) => rows.map((row) => ({ runId: row.runId, createdAtMs: Number(row.createdAtMs) })))
1730
+ ))
1731
+ )
1732
+ const pendingCancellationRuns: Service["pendingCancellationRuns"] = Effect.fn(
1733
+ "DurableEngineState.pendingCancellationRuns"
1734
+ )((limit, after, through) => pendingCancellationRows(limit, after, through))
1735
+ const pendingCancellationTail: Service["pendingCancellationTail"] = Effect.fn(
1736
+ "DurableEngineState.pendingCancellationTail"
1737
+ )(() =>
1738
+ pendingCancellationRows(1, undefined, undefined, true).pipe(
1739
+ Effect.map((rows) => Option.fromNullishOr(rows[0]))
1740
+ )
1741
+ )
1742
+
1743
+ const staleRunningRuns: Service["staleRunningRuns"] = Effect.fn(
1744
+ "DurableEngineState.staleRunningRuns"
1745
+ )((staleBeforeMs, limit) =>
1746
+ // `LIMIT -1` is SQLite's explicit "no limit"; the partial index created
1747
+ // in `make` serves the predicate, so the per-tick sweep is an index
1748
+ // range read instead of a full table scan (issue #79).
1749
+ sql<{ runId: string; heartbeatAtMs: number | bigint | string }>`
1750
+ SELECT run_id AS "runId", heartbeat_at_ms AS "heartbeatAtMs"
1751
+ FROM flows_runs
1752
+ WHERE status = 'running'
1753
+ AND heartbeat_at_ms IS NOT NULL
1754
+ AND heartbeat_at_ms < ${staleBeforeMs}
1755
+ ORDER BY heartbeat_at_ms, run_id
1756
+ LIMIT ${limit ?? (Dialect.isPostgres(sql) ? null : -1)}
1757
+ `.pipe(
1758
+ Effect.orDie,
1759
+ Effect.map((rows) => rows.map((row) => ({ runId: String(row.runId), heartbeatAtMs: Number(row.heartbeatAtMs) })))
1760
+ )
1761
+ )
1762
+
1763
+ /**
1764
+ * One ordered range read replaces the engine store's per-attempt point
1765
+ * probes (issue #77): a fresh key costs one empty SELECT instead of 32
1766
+ * sequential gets, and a resumed key one SELECT instead of one per
1767
+ * surviving attempt. The earliest surviving row is the durable retry
1768
+ * origin (issue #69) and the highest attempt contiguous from it the
1769
+ * resumed counter (issue #59). This read is deliberately unbounded; the
1770
+ * pruned-prefix tolerance that makes it agree with the point-probe
1771
+ * fallback is applied once, by the caller, in
1772
+ * `internal/AttemptProbe.probeAttempts` (issue #96).
1773
+ */
1774
+ const attemptSurvivors: NonNullable<Service["attemptSurvivors"]> = Effect.fn(
1775
+ "DurableEngineState.attemptSurvivors"
1776
+ )((runId, stepKeyDigest) =>
1777
+ sql<{ readonly attempt: number; readonly startedAtMs: number }>`
1778
+ SELECT attempt AS "attempt", started_at_ms AS "startedAtMs"
1779
+ FROM flows_attempts
1780
+ WHERE run_id = ${runId}
1781
+ AND step_key_digest = ${stepKeyDigest}
1782
+ ORDER BY attempt
1783
+ `.pipe(
1784
+ Effect.orDie,
1785
+ Effect.map((rows) => {
1786
+ const first = rows[0]
1787
+ if (first === undefined) return Option.none()
1788
+ let latest = Number(first.attempt)
1789
+ for (let index = 1; index < rows.length; index++) {
1790
+ const next = Number(rows[index]!.attempt)
1791
+ if (next !== latest + 1) break
1792
+ latest = next
1793
+ }
1794
+ return Option.some({
1795
+ earliestAttempt: Number(first.attempt),
1796
+ earliestStartedAtMs: Number(first.startedAtMs),
1797
+ latest
1798
+ })
1799
+ })
1800
+ )
1801
+ )
1802
+
1803
+ const selectRunParent = (childId: string, parentId: string) =>
1804
+ sql<RunParentDatabaseRow>`
1805
+ SELECT
1806
+ child_id AS "childId",
1807
+ parent_id AS "parentId",
1808
+ seq AS "seq"
1809
+ FROM flows_run_parents
1810
+ WHERE child_id = ${childId}
1811
+ AND parent_id = ${parentId}
1812
+ `
1813
+
1814
+ const parentIdsOf = (id: string): Effect.Effect<ReadonlyArray<string>> =>
1815
+ sql<RunParentDatabaseRow>`
1816
+ SELECT
1817
+ child_id AS "childId",
1818
+ parent_id AS "parentId",
1819
+ seq AS "seq"
1820
+ FROM flows_run_parents
1821
+ WHERE child_id = ${id}
1822
+ ORDER BY seq
1823
+ `.pipe(
1824
+ Effect.orDie,
1825
+ Effect.map((rows) => rows.map((row) => String(row.parentId)))
1826
+ )
1827
+
1828
+ const recordRunParent: Service["recordRunParent"] = Effect.fn(
1829
+ "DurableEngineState.recordRunParent"
1830
+ )((childId, parentId) =>
1831
+ writer.write(
1832
+ Effect.gen(function*() {
1833
+ // MAX(seq)+1 and the insert share one write transaction, so the
1834
+ // assigned seq is a total insertion order over the store.
1835
+ const inserted = yield* sql<RunParentDatabaseRow>`
1836
+ INSERT INTO flows_run_parents (child_id, parent_id, seq)
1837
+ SELECT ${childId}, ${parentId}, COALESCE(MAX(seq), 0) + 1
1838
+ FROM flows_run_parents
1839
+ WHERE TRUE
1840
+ ON CONFLICT (child_id, parent_id) DO NOTHING
1841
+ RETURNING
1842
+ child_id AS "childId",
1843
+ parent_id AS "parentId",
1844
+ seq AS "seq"
1845
+ `
1846
+ if (inserted[0] !== undefined) {
1847
+ // The cycle check shares the insert's transaction: a hit fails the
1848
+ // effect, rolling the insert back, so a rejected edge is never
1849
+ // durable and there is no check-then-record window for another
1850
+ // writer to slip through (issues #54/#55/#56). The insert
1851
+ // precedes the walk, so the transaction holds the write lock
1852
+ // while walking: under `DurableWriter.write`'s documented contract —
1853
+ // write transactions are mutually serialized (issue #74) — of two
1854
+ // edges that jointly close a cycle, exactly the later one fails.
1855
+ // This is a contract requirement, not a SQLite artifact; a
1856
+ // backend that ran writes at READ COMMITTED would break it and is
1857
+ // excluded by the DurableWriter contract.
1858
+ const cycle = yield* findCyclePath(childId, parentId, parentIdsOf)
1859
+ if (cycle !== undefined) {
1860
+ return yield* Effect.fail(new RunParentCycleError({ path: cycle }))
1861
+ }
1862
+ return {
1863
+ _tag: "Recorded" as const,
1864
+ edge: yield* decodeRunParentEdge(inserted[0])
1865
+ }
1866
+ }
1867
+ const existing = yield* selectRunParent(childId, parentId)
1868
+ if (existing[0] === undefined) {
1869
+ return yield* Effect.die(new EngineStateFault({ reason: "run_parent_edge_missing" }))
1870
+ }
1871
+ return {
1872
+ _tag: "Existing" as const,
1873
+ edge: yield* decodeRunParentEdge(existing[0])
1874
+ }
1875
+ })
1876
+ ).pipe(
1877
+ Effect.catch((error) => error instanceof RunParentCycleError ? Effect.fail(error) : Effect.die(error))
1878
+ )
1879
+ )
1880
+
1881
+ const removeRunParentsForRun: Service["removeRunParentsForRun"] = Effect.fn(
1882
+ "DurableEngineState.removeRunParentsForRun"
1883
+ )((runId) =>
1884
+ writer.write(sql`
1885
+ DELETE FROM flows_run_parents
1886
+ WHERE child_id = ${runId}
1887
+ OR parent_id = ${runId}
1888
+ `).pipe(Effect.orDie, Effect.asVoid)
1889
+ )
1890
+
1891
+ // Keep state-first ordering. This gate and DurableWriter's shared permit are
1892
+ // both taken at the caller's interruptibility, so an interruptible caller can
1893
+ // still be cancelled while queued, including behind a journal-only write that
1894
+ // skips this gate.
1895
+ // A nested SQL transaction already owns the connection and bypasses this gate.
1896
+ const transactionGate = Semaphore.makeUnsafe(1)
1897
+ const transaction: Service["transaction"] = <A, E, R>(
1898
+ effect: Effect.Effect<A, E, R>
1899
+ ): Effect.Effect<A, E, R> =>
1900
+ Effect.flatMap(Effect.serviceOption(sql.transactionService), (enclosing) => {
1901
+ const write = (body: Effect.Effect<A, E, R>) =>
1902
+ writer.write(body).pipe(
1903
+ Effect.catchCause((cause) => {
1904
+ // Only a sole storage failure is normalized. A composite keeps every
1905
+ // reason for the caller to diagnose, and its storage failures are
1906
+ // demoted to defects one by one rather than cast through the
1907
+ // declared error type: `transaction` promises the BODY's failures,
1908
+ // so a `DatabaseError` arriving as a typed failure would be a lie
1909
+ // the compiler could not catch. Defect identity is unchanged.
1910
+ if (cause.reasons.length === 1) {
1911
+ const reason = cause.reasons[0]!
1912
+ if (Cause.isFailReason(reason) && reason.error instanceof DatabaseError) {
1913
+ return Effect.die(reason.error)
1914
+ }
1915
+ }
1916
+ return Effect.failCause(dieOnDatabaseError<E>(cause))
1917
+ })
1918
+ )
1919
+ if (Option.isSome(enclosing)) return write(effect)
1920
+ return Effect.uninterruptibleMask((restore) =>
1921
+ Effect.gen(function*() {
1922
+ yield* restore(transactionGate.take(1))
1923
+ let released = false
1924
+ const release = Effect.suspend(() => {
1925
+ if (released) return Effect.void
1926
+ released = true
1927
+ return transactionGate.release(1).pipe(Effect.asVoid)
1928
+ })
1929
+ return yield* restore(write(Effect.gen(function*() {
1930
+ // Release at COMMIT, before publication can read through another
1931
+ // transaction. A failed write instead releases in ensuring below.
1932
+ yield* afterCommit(release, sql)
1933
+ return yield* effect
1934
+ }))).pipe(Effect.ensuring(release))
1935
+ })
1936
+ )
1937
+ })
1938
+
1939
+ const runParents: Service["runParents"] = Effect.fn("DurableEngineState.runParents")((childId) =>
1940
+ sql<RunParentDatabaseRow>`
1941
+ SELECT
1942
+ child_id AS "childId",
1943
+ parent_id AS "parentId",
1944
+ seq AS "seq"
1945
+ FROM flows_run_parents
1946
+ WHERE child_id = ${childId}
1947
+ ORDER BY seq
1948
+ `.pipe(
1949
+ Effect.orDie,
1950
+ // One unreadable edge made a run's whole ancestry unreadable, so no
1951
+ // subflow under it could be linked at all.
1952
+ Effect.flatMap((rows) => decodeSweep(rows, "run parent", runParentRowKey, decodeRunParentEdgeResult))
1953
+ )
1954
+ )
1955
+
1956
+ const runChildren: Service["runChildren"] = Effect.fn("DurableEngineState.runChildren")((parentId) =>
1957
+ sql<RunParentDatabaseRow>`
1958
+ SELECT
1959
+ child_id AS "childId",
1960
+ parent_id AS "parentId",
1961
+ seq AS "seq"
1962
+ FROM flows_run_parents
1963
+ WHERE parent_id = ${parentId}
1964
+ ORDER BY seq
1965
+ `.pipe(
1966
+ Effect.orDie,
1967
+ // The cancellation cascade's only cross-process way to find a run's
1968
+ // children: one unreadable sibling edge left every child running after
1969
+ // its parent was cancelled.
1970
+ Effect.flatMap((rows) => decodeSweep(rows, "run parent", runParentRowKey, decodeRunParentEdgeResult))
1971
+ )
1972
+ )
1973
+
1974
+ return DurableEngineState.of({
1975
+ deferred,
1976
+ consumeDeferred,
1977
+ completeDeferred,
1978
+ clock,
1979
+ scheduleClock,
1980
+ completeClock,
1981
+ completeRunClocks,
1982
+ dueClocks,
1983
+ pendingClocks,
1984
+ completedDeferreds,
1985
+ park,
1986
+ wake,
1987
+ waiting,
1988
+ waitingTree,
1989
+ waitingRuns,
1990
+ pendingRuns,
1991
+ pendingRunTail,
1992
+ pendingCancellationRuns,
1993
+ pendingCancellationTail,
1994
+ staleRunningRuns,
1995
+ attemptSurvivors,
1996
+ recordRunParent,
1997
+ removeRunParentsForRun,
1998
+ runParents,
1999
+ runChildren,
2000
+ transaction
2001
+ })
2002
+ })
2003
+
2004
+ /**
2005
+ * Provides database-backed durable engine state.
2006
+ *
2007
+ * @since 0.1.0
2008
+ * @category layers
2009
+ */
2010
+ export const layer: Layer.Layer<DurableEngineState, never, DurableWriter | SqlClient.SqlClient> = Layer.effect(
2011
+ DurableEngineState,
2012
+ make
2013
+ )
2014
+
2015
+ /**
2016
+ * A run's ownership view as the in-memory implementation needs it for the
2017
+ * same fences the SQL implementation reads from `flows_runs`.
2018
+ *
2019
+ * @since 0.1.0
2020
+ * @category models
2021
+ */
2022
+ export interface MemoryRunView {
2023
+ readonly flowName?: string | undefined
2024
+ readonly createdAtMs?: number | undefined
2025
+ readonly status: "pending" | "running" | "suspended" | "completed" | "failed" | "cancelled"
2026
+ readonly owner: OwnerId | null
2027
+ /**
2028
+ * The run's last heartbeat instant, mirroring `flows_runs.heartbeat_at_ms`.
2029
+ * `undefined` means unknown — such a run never surfaces from
2030
+ * `staleRunningRuns` (issue #53).
2031
+ */
2032
+ readonly heartbeatAtMs?: number | null | undefined
2033
+ /**
2034
+ * The run's durable cancel request, mirroring
2035
+ * `flows_runs.cancel_requested_at_ms`. `undefined`/`null` means not
2036
+ * requested — such a run never surfaces from a
2037
+ * `waitingRuns({ cancelRequested: true })` query (issue #68).
2038
+ */
2039
+ readonly cancelRequestedAtMs?: number | null | undefined
2040
+ /**
2041
+ * What this run does when the run that spawned it ends, mirroring
2042
+ * `RunState.onParentExit`. `undefined` reads as `cancel`, the default and
2043
+ * the shape a `.child()` boundary takes, so a view that does not record it
2044
+ * describes an attached child.
2045
+ */
2046
+ readonly onParentExit?: OnParentExit | undefined
2047
+ }
2048
+
2049
+ /**
2050
+ * Options for the in-memory durable-state implementation.
2051
+ *
2052
+ * @since 0.1.0
2053
+ * @category models
2054
+ */
2055
+ export interface MemoryOptions {
2056
+ /**
2057
+ * Resolves a run's existence, status, and owner — the in-memory analogue
2058
+ * of the `flows_runs` lookups the SQL implementation performs for its
2059
+ * `park`/`wake`/`scheduleClock` fences. `Option.none()` means the run does
2060
+ * not exist. When omitted, every run is treated as running and owned by
2061
+ * whichever owner is presented (a deliberately permissive default for tests
2062
+ * that exercise only deferred/clock state without a run table).
2063
+ */
2064
+ readonly runs?: (runId: string) => Option.Option<MemoryRunView>
2065
+ /**
2066
+ * Enumerates every known run — the in-memory analogue of scanning
2067
+ * `flows_runs`, which `staleRunningRuns` needs (the `runs` lookup alone
2068
+ * cannot enumerate). When omitted, `staleRunningRuns` reports no stale
2069
+ * rows.
2070
+ */
2071
+ readonly listRuns?: () => Iterable<readonly [string, MemoryRunView]>
2072
+ }
2073
+
2074
+ /**
2075
+ * Constructs a deterministic in-memory durable-state implementation.
2076
+ *
2077
+ * The returned service can be shared by multiple fresh engine instances in a
2078
+ * test to model process restart over the same storage. With `runs` supplied
2079
+ * it enforces the same ownership fences as the SQL implementation and is
2080
+ * held to the same contract suite
2081
+ * (`test/contract/DurableEngineStateContract.ts`).
2082
+ *
2083
+ * @since 0.1.0
2084
+ * @category constructors
2085
+ */
2086
+ export const makeMemory = (options: MemoryOptions = {}): Service => {
2087
+ const deferreds = new Map<string, DeferredRow>()
2088
+ const consumedDeferreds = new Map<string, number>()
2089
+ const clocks = new Map<string, ClockRow>()
2090
+ const waitingRows = new Map<string, WaitingRow>()
2091
+ // childId -> (parentId -> seq); `parentSeq` mirrors the SQL MAX(seq)+1.
2092
+ const parentEdges = new Map<string, Map<string, number>>()
2093
+ let parentSeq = 0
2094
+
2095
+ const snapshotDeferred = (row: DeferredRow): Effect.Effect<DeferredRow> =>
2096
+ Schema.decodeUnknownEffect(DeferredRow)(row).pipe(
2097
+ Effect.orDie,
2098
+ Effect.flatMap((decoded) =>
2099
+ Effect.all({
2100
+ exitJson: encodeJson(decoded.exit, "exit"),
2101
+ metadataJson: decoded.metadata === undefined
2102
+ ? Effect.succeed(null)
2103
+ : encodeJson(decoded.metadata, "metadata")
2104
+ }).pipe(
2105
+ Effect.flatMap(({ exitJson, metadataJson }) =>
2106
+ decodeDeferredRow({
2107
+ flowName: decoded.flowName,
2108
+ executionId: decoded.executionId,
2109
+ deferredName: decoded.deferredName,
2110
+ exitJson,
2111
+ metadataJson,
2112
+ completedAtMs: decoded.completedAtMs
2113
+ })
2114
+ )
2115
+ )
2116
+ )
2117
+ )
2118
+
2119
+ const snapshotClock = (row: ClockRow): Effect.Effect<ClockRow> =>
2120
+ Schema.decodeUnknownEffect(ClockRow)(row).pipe(
2121
+ Effect.orDie,
2122
+ Effect.map((decoded) => ({ ...decoded }))
2123
+ )
2124
+
2125
+ const snapshotWaiting = (row: WaitingRow): WaitingRow => ({ ...row })
2126
+
2127
+ /** The synchronous twin of `findCyclePath` over the in-memory edge map. */
2128
+ const findCyclePathSync = (
2129
+ childId: string,
2130
+ parentId: string
2131
+ ): ReadonlyArray<string> | undefined => {
2132
+ if (parentId === childId) return [childId]
2133
+ const seen = new Set<string>([parentId])
2134
+ const walk = (
2135
+ current: string,
2136
+ chain: ReadonlyArray<string>
2137
+ ): ReadonlyArray<string> | undefined => {
2138
+ for (const parent of parentEdges.get(current)?.keys() ?? []) {
2139
+ if (parent === childId) return [...chain, parent].reverse()
2140
+ if (seen.has(parent)) continue
2141
+ seen.add(parent)
2142
+ const found = walk(parent, [...chain, parent])
2143
+ if (found !== undefined) return found
2144
+ }
2145
+ return undefined
2146
+ }
2147
+ return walk(parentId, [parentId])
2148
+ }
2149
+
2150
+ const sameOwner = (left: OwnerId, right: OwnerId): boolean =>
2151
+ left.hostId === right.hostId && left.pid === right.pid && left.nonce === right.nonce
2152
+
2153
+ /** Mirrors the SQL `flows_runs` owner-match predicate for a run. */
2154
+ const runView = (runId: string, presented: OwnerId | undefined): {
2155
+ readonly exists: boolean
2156
+ readonly running: boolean
2157
+ readonly owned: boolean
2158
+ } => {
2159
+ if (options.runs === undefined) {
2160
+ return { exists: true, running: true, owned: true }
2161
+ }
2162
+ const view = options.runs(runId)
2163
+ if (Option.isNone(view)) {
2164
+ return { exists: false, running: false, owned: false }
2165
+ }
2166
+ const owned = presented !== undefined &&
2167
+ view.value.owner !== null &&
2168
+ sameOwner(view.value.owner, presented)
2169
+ return { exists: true, running: view.value.status === "running", owned }
2170
+ }
2171
+
2172
+ /**
2173
+ * Mirrors the SQL sweeps' run-status join: a row whose run has settled is
2174
+ * never swept, and `Option.none()` from a supplied `runs` view means the run
2175
+ * does not exist, so its orphaned engine-state rows stay hidden. The
2176
+ * permissive default (no `runs` view) treats every run as live because a
2177
+ * standalone memory store has no run table to consult.
2178
+ */
2179
+ const isLive = (runId: string): boolean => {
2180
+ if (options.runs === undefined) return true
2181
+ const view = options.runs(runId)
2182
+ return Option.isSome(view) && (
2183
+ view.value.status === "pending" ||
2184
+ view.value.status === "running" ||
2185
+ view.value.status === "suspended"
2186
+ )
2187
+ }
2188
+
2189
+ const pendingMemoryRows = (
2190
+ flowName: string | undefined,
2191
+ after?: PendingRunCursor,
2192
+ through?: PendingRunCursor,
2193
+ eligibleBeforeMs?: number,
2194
+ cancellationOnly = false
2195
+ ) =>
2196
+ Array.from(options.listRuns?.() ?? [])
2197
+ .filter(([runId, view]) =>
2198
+ view.status === "pending" && (flowName === undefined || view.flowName === flowName) &&
2199
+ (!cancellationOnly || view.cancelRequestedAtMs != null) &&
2200
+ (eligibleBeforeMs === undefined || (view.createdAtMs ?? 0) < eligibleBeforeMs) &&
2201
+ (after === undefined || (view.createdAtMs ?? 0) > after.createdAtMs ||
2202
+ ((view.createdAtMs ?? 0) === after.createdAtMs && compareText(runId, after.runId) > 0)) &&
2203
+ (through === undefined || (view.createdAtMs ?? 0) < through.createdAtMs ||
2204
+ ((view.createdAtMs ?? 0) === through.createdAtMs && compareText(runId, through.runId) <= 0))
2205
+ )
2206
+ .sort(([leftId, left], [rightId, right]) =>
2207
+ (left.createdAtMs ?? 0) - (right.createdAtMs ?? 0) || compareText(leftId, rightId)
2208
+ )
2209
+ .map(([runId, view]) => ({ runId, createdAtMs: view.createdAtMs ?? 0 }))
2210
+
2211
+ const unguarded: Omit<Service, "transaction"> = {
2212
+ deferred: Effect.fn("DurableEngineState.deferred")((address) =>
2213
+ Effect.gen(function*() {
2214
+ const row = deferreds.get(deferredKey(address))
2215
+ return row === undefined ? Option.none() : Option.some(yield* snapshotDeferred(row))
2216
+ })
2217
+ ),
2218
+ consumeDeferred: Effect.fn("DurableEngineState.consumeDeferred")((address, consumedAtMs) =>
2219
+ Effect.sync(() => {
2220
+ const key = deferredKey(address)
2221
+ if (!deferreds.has(key) || consumedDeferreds.has(key)) return false
2222
+ consumedDeferreds.set(key, consumedAtMs)
2223
+ return true
2224
+ })
2225
+ ),
2226
+ completeDeferred: Effect.fn("DurableEngineState.completeDeferred")((row) =>
2227
+ Effect.gen(function*() {
2228
+ const stored = yield* snapshotDeferred(row)
2229
+ const key = deferredKey(stored)
2230
+ const existing = deferreds.get(key)
2231
+ if (existing !== undefined) {
2232
+ return {
2233
+ _tag: "Existing" as const,
2234
+ row: yield* snapshotDeferred(existing),
2235
+ digest: yield* memoryDigest(existing)
2236
+ }
2237
+ }
2238
+ deferreds.set(key, stored)
2239
+ return {
2240
+ _tag: "Completed" as const,
2241
+ row: yield* snapshotDeferred(stored),
2242
+ digest: yield* memoryDigest(stored)
2243
+ }
2244
+ })
2245
+ ),
2246
+ clock: Effect.fn("DurableEngineState.clock")((address) =>
2247
+ Effect.gen(function*() {
2248
+ const row = clocks.get(clockKey(address))
2249
+ return row === undefined ? Option.none() : Option.some(yield* snapshotClock(row))
2250
+ })
2251
+ ),
2252
+ scheduleClock: Effect.fn("DurableEngineState.scheduleClock")((row, owner) =>
2253
+ Effect.gen(function*(): Generator<Effect.Effect<unknown>, ScheduleClockOutcome, never> {
2254
+ // Mirrors the SQL fence: creation requires the presented owner to
2255
+ // currently run the execution; a lost fence surfaces as
2256
+ // self-interruption, an existing row wins regardless.
2257
+ const stored = yield* snapshotClock(row)
2258
+ const key = clockKey(stored)
2259
+ const existing = clocks.get(key)
2260
+ if (existing !== undefined) {
2261
+ return { _tag: "Existing" as const, row: yield* snapshotClock(existing) }
2262
+ }
2263
+ const view = runView(stored.executionId, owner)
2264
+ if (!view.exists || !view.running || !view.owned) {
2265
+ return yield* Effect.interrupt
2266
+ }
2267
+ clocks.set(key, stored)
2268
+ return { _tag: "Scheduled" as const, row: yield* snapshotClock(stored) }
2269
+ })
2270
+ ),
2271
+ completeClock: Effect.fn("DurableEngineState.completeClock")((address, completedAtMs) =>
2272
+ Effect.sync(() => {
2273
+ const key = clockKey(address)
2274
+ const existing = clocks.get(key)
2275
+ if (existing === undefined) {
2276
+ return { _tag: "NotFound" as const }
2277
+ }
2278
+ if (existing.completedAtMs !== null) {
2279
+ return { _tag: "AlreadyCompleted" as const, row: { ...existing } }
2280
+ }
2281
+ const row = { ...existing, completedAtMs }
2282
+ clocks.set(key, row)
2283
+ return { _tag: "Completed" as const, row: { ...row } }
2284
+ })
2285
+ ),
2286
+ dueClocks: Effect.fn("DurableEngineState.dueClocks")((nowMs) =>
2287
+ Effect.forEach(
2288
+ Array.from(clocks.values())
2289
+ .filter((row) => row.completedAtMs === null && row.dueAtMs <= nowMs)
2290
+ .sort((left, right) =>
2291
+ left.dueAtMs - right.dueAtMs ||
2292
+ compareText(left.executionId, right.executionId) ||
2293
+ compareText(left.clockName, right.clockName)
2294
+ ),
2295
+ snapshotClock
2296
+ )
2297
+ ),
2298
+ completeRunClocks: Effect.fn("DurableEngineState.completeRunClocks")((executionId, completedAtMs) =>
2299
+ Effect.sync(() => {
2300
+ const completed: Array<ClockRow> = []
2301
+ for (const [key, row] of clocks) {
2302
+ if (row.executionId !== executionId || row.completedAtMs !== null) continue
2303
+ const next = { ...row, completedAtMs }
2304
+ clocks.set(key, next)
2305
+ completed.push({ ...next })
2306
+ }
2307
+ return completed.sort((left, right) =>
2308
+ compareText(left.flowName, right.flowName) || compareText(left.clockName, right.clockName)
2309
+ )
2310
+ })
2311
+ ),
2312
+ pendingClocks: Effect.fn("DurableEngineState.pendingClocks")((scope) =>
2313
+ Effect.flatMap(
2314
+ Effect.sync(() =>
2315
+ Array.from(clocks.values())
2316
+ .filter((row) =>
2317
+ row.completedAtMs === null &&
2318
+ isLive(row.executionId) &&
2319
+ (scope.executionId === undefined || row.executionId === scope.executionId) &&
2320
+ (scope.flowName === undefined || row.flowName === scope.flowName)
2321
+ )
2322
+ .sort((left, right) =>
2323
+ left.dueAtMs - right.dueAtMs ||
2324
+ compareText(left.executionId, right.executionId) ||
2325
+ compareText(left.clockName, right.clockName)
2326
+ )
2327
+ ),
2328
+ (rows) => Effect.forEach(rows, snapshotClock)
2329
+ )
2330
+ ),
2331
+ completedDeferreds: Effect.fn("DurableEngineState.completedDeferreds")((flowName) =>
2332
+ Effect.sync(() =>
2333
+ Array.from(deferreds.values())
2334
+ .filter((row) =>
2335
+ row.flowName === flowName && isLive(row.executionId) && !consumedDeferreds.has(deferredKey(row))
2336
+ )
2337
+ .map(({ flowName, executionId, deferredName }) => ({
2338
+ flowName,
2339
+ executionId,
2340
+ deferredName
2341
+ }))
2342
+ .sort((left, right) =>
2343
+ compareText(left.executionId, right.executionId) ||
2344
+ compareText(left.deferredName, right.deferredName)
2345
+ )
2346
+ )
2347
+ ),
2348
+ park: Effect.fn("DurableEngineState.park")((runId, waitingPayload, owner) =>
2349
+ Effect.sync(() => {
2350
+ // Mirrors the SQL fence: only the current owner of an existing run
2351
+ // may park it; anything else reports NotFound, exactly like the
2352
+ // owner-guarded UPDATE matching no row.
2353
+ const view = runView(runId, owner)
2354
+ if (!view.exists || !view.owned) {
2355
+ return { _tag: "NotFound" as const }
2356
+ }
2357
+ // The declared question is stored as JSON TEXT and read back decoded,
2358
+ // exactly as the column and its SELECT do it.
2359
+ const row: WaitingRow = {
2360
+ runId,
2361
+ reason: waitingPayload.reason,
2362
+ wakeAt: waitingPayload.wakeAt ?? null,
2363
+ token: waitingPayload.token ?? null,
2364
+ ...decodeWaitingRequest(waitingPayload.request)
2365
+ } as WaitingRow
2366
+ waitingRows.set(runId, row)
2367
+ return { _tag: "Parked" as const, row: snapshotWaiting(row) }
2368
+ })
2369
+ ),
2370
+ wake: Effect.fn("DurableEngineState.wake")((runId) =>
2371
+ Effect.sync(() => {
2372
+ const row = waitingRows.get(runId)
2373
+ if (row === undefined) {
2374
+ // Mirrors SQL: an unknown run is NotFound, an existing unparked
2375
+ // run is NotWaiting.
2376
+ return runView(runId, undefined).exists
2377
+ ? { _tag: "NotWaiting" as const }
2378
+ : { _tag: "NotFound" as const }
2379
+ }
2380
+ waitingRows.delete(runId)
2381
+ return { _tag: "Woken" as const, row: snapshotWaiting(row) }
2382
+ })
2383
+ ),
2384
+ waiting: Effect.fn("DurableEngineState.waiting")((runId) =>
2385
+ Effect.sync(() => {
2386
+ const row = waitingRows.get(runId)
2387
+ return row === undefined ? Option.none() : Option.some(snapshotWaiting(row))
2388
+ })
2389
+ ),
2390
+ waitingTree: Effect.fn("DurableEngineState.waitingTree")((runId, walk) =>
2391
+ Effect.sync(() => {
2392
+ // Breadth-first over recorded spawn edges: the named run first, then
2393
+ // each generation beneath it. This memory view does not model the
2394
+ // parent_run_id trampoline links that the SQL walk also follows.
2395
+ const found: Array<WaitingRow> = []
2396
+ const seen = new Set<string>([runId])
2397
+ let frontier: ReadonlyArray<string> = [runId]
2398
+ for (let depth = 0; depth <= waitingTreeMaxDepth && frontier.length > 0; depth++) {
2399
+ const next: Array<string> = []
2400
+ for (const current of frontier) {
2401
+ const row = waitingRows.get(current)
2402
+ if (row !== undefined && isLive(current)) found.push(snapshotWaiting(row))
2403
+ for (const [childId, parents] of parentEdges) {
2404
+ if (!parents.has(current) || seen.has(childId)) continue
2405
+ // The SQL walk skips a detached subtree; so does this one, for a
2406
+ // view that records the policy. A view that does not describes an
2407
+ // attached child, which is the default.
2408
+ const view = options.runs?.(childId)
2409
+ if (
2410
+ walk?.detached !== "include" && view !== undefined && Option.isSome(view) &&
2411
+ view.value.onParentExit === "detach"
2412
+ ) continue
2413
+ seen.add(childId)
2414
+ next.push(childId)
2415
+ }
2416
+ }
2417
+ frontier = next
2418
+ }
2419
+ return found
2420
+ })
2421
+ ),
2422
+ waitingRuns: Effect.fn("DurableEngineState.waitingRuns")((filter) =>
2423
+ Effect.sync(() =>
2424
+ Array.from(waitingRows.values())
2425
+ // Mirrors the SQL status predicate: a terminally closed run's
2426
+ // stale waiting row never surfaces to a sweeper (issue #28). The
2427
+ // permissive default (no `runs` view) treats every run as live.
2428
+ .filter((row) => isLive(row.runId))
2429
+ .filter((row) => filter?.reason === undefined || row.reason === filter.reason)
2430
+ .filter((row) =>
2431
+ filter?.dueBeforeMs === undefined ||
2432
+ (row.wakeAt !== null && row.wakeAt <= filter.dueBeforeMs)
2433
+ )
2434
+ // Mirrors the SQL cancel predicate over
2435
+ // `flows_runs.cancel_requested_at_ms` (issue #68). Without a
2436
+ // `runs` view the flag is unknowable, so the query stays
2437
+ // permissive and the sweeper's own per-row guard decides.
2438
+ .filter((row) => {
2439
+ if (filter?.cancelRequested !== true) return true
2440
+ if (options.runs === undefined) return true
2441
+ const view = options.runs(row.runId)
2442
+ return Option.isSome(view) && view.value.cancelRequestedAtMs != null
2443
+ })
2444
+ .sort((left, right) =>
2445
+ Number(left.wakeAt === null) - Number(right.wakeAt === null) ||
2446
+ (left.wakeAt ?? 0) - (right.wakeAt ?? 0) ||
2447
+ compareText(left.runId, right.runId)
2448
+ )
2449
+ .map(snapshotWaiting)
2450
+ )
2451
+ ),
2452
+ pendingRuns: Effect.fn("DurableEngineState.pendingRuns")((flowName, limit, after, through, eligibleBeforeMs) =>
2453
+ Effect.sync(() => {
2454
+ const ordered = pendingMemoryRows(flowName, after, through, eligibleBeforeMs)
2455
+ return limit === undefined ? ordered : ordered.slice(0, limit)
2456
+ })
2457
+ ),
2458
+ pendingRunTail: Effect.fn("DurableEngineState.pendingRunTail")((flowName, eligibleBeforeMs) =>
2459
+ Effect.sync(() =>
2460
+ Option.fromNullishOr(pendingMemoryRows(flowName, undefined, undefined, eligibleBeforeMs).at(-1))
2461
+ )
2462
+ ),
2463
+ pendingCancellationRuns: Effect.fn("DurableEngineState.pendingCancellationRuns")((limit, after, through) =>
2464
+ pendingCancellationLimit(limit).pipe(Effect.andThen(
2465
+ Effect.sync(() => pendingMemoryRows(undefined, after, through, undefined, true).slice(0, limit))
2466
+ ))
2467
+ ),
2468
+ pendingCancellationTail: Effect.fn("DurableEngineState.pendingCancellationTail")(() =>
2469
+ Effect.sync(() =>
2470
+ Option.fromNullishOr(
2471
+ pendingMemoryRows(undefined, undefined, undefined, undefined, true).at(-1)
2472
+ )
2473
+ )
2474
+ ),
2475
+ staleRunningRuns: Effect.fn("DurableEngineState.staleRunningRuns")((staleBeforeMs, limit) =>
2476
+ Effect.sync(() => {
2477
+ // Mirrors the SQL scan of `flows_runs`: without an enumerator there
2478
+ // is nothing to scan, so no stale rows exist.
2479
+ if (options.listRuns === undefined) return []
2480
+ const stale: Array<StaleRunningRow> = []
2481
+ for (const [runId, view] of options.listRuns()) {
2482
+ const heartbeatAtMs = view.heartbeatAtMs
2483
+ if (
2484
+ view.status === "running" &&
2485
+ typeof heartbeatAtMs === "number" &&
2486
+ heartbeatAtMs < staleBeforeMs
2487
+ ) {
2488
+ stale.push({ runId, heartbeatAtMs })
2489
+ }
2490
+ }
2491
+ const ordered = stale.sort((left, right) =>
2492
+ left.heartbeatAtMs - right.heartbeatAtMs ||
2493
+ compareText(left.runId, right.runId)
2494
+ )
2495
+ // Mirrors the SQL LIMIT: oldest heartbeats first, capped per sweep
2496
+ // (issue #79).
2497
+ return limit === undefined ? ordered : ordered.slice(0, limit)
2498
+ })
2499
+ ),
2500
+ recordRunParent: Effect.fn("DurableEngineState.recordRunParent")((childId, parentId) =>
2501
+ // A single synchronous step mirrors the SQL transaction: the cycle
2502
+ // check and the insert are atomic — no fiber can interleave between
2503
+ // them, and a rejected edge is never observable (issues #54/#55/#56).
2504
+ Effect.suspend((): Effect.Effect<RecordRunParentOutcome, RunParentCycleError> => {
2505
+ const parents = parentEdges.get(childId) ?? new Map<string, number>()
2506
+ const existing = parents.get(parentId)
2507
+ if (existing !== undefined) {
2508
+ return Effect.succeed({
2509
+ _tag: "Existing" as const,
2510
+ edge: { childId, parentId, seq: existing }
2511
+ })
2512
+ }
2513
+ const cycle = findCyclePathSync(childId, parentId)
2514
+ if (cycle !== undefined) {
2515
+ return Effect.fail(new RunParentCycleError({ path: cycle }))
2516
+ }
2517
+ parentSeq += 1
2518
+ parents.set(parentId, parentSeq)
2519
+ parentEdges.set(childId, parents)
2520
+ return Effect.succeed({
2521
+ _tag: "Recorded" as const,
2522
+ edge: { childId, parentId, seq: parentSeq }
2523
+ })
2524
+ })
2525
+ ),
2526
+ removeRunParentsForRun: Effect.fn("DurableEngineState.removeRunParentsForRun")((runId) =>
2527
+ Effect.sync(() => {
2528
+ parentEdges.delete(runId)
2529
+ for (const [childId, parents] of parentEdges) {
2530
+ parents.delete(runId)
2531
+ if (parents.size === 0) parentEdges.delete(childId)
2532
+ }
2533
+ })
2534
+ ),
2535
+ runParents: Effect.fn("DurableEngineState.runParents")((childId) =>
2536
+ Effect.sync(() =>
2537
+ Array.from(parentEdges.get(childId) ?? [])
2538
+ .map(([parentId, seq]) => ({ childId, parentId, seq }))
2539
+ .sort((left, right) => left.seq - right.seq)
2540
+ )
2541
+ ),
2542
+ runChildren: Effect.fn("DurableEngineState.runChildren")((parentId) =>
2543
+ Effect.sync(() => {
2544
+ const edges: Array<RunParentEdge> = []
2545
+ for (const [childId, parents] of parentEdges) {
2546
+ const seq = parents.get(parentId)
2547
+ if (seq !== undefined) edges.push({ childId, parentId, seq })
2548
+ }
2549
+ return edges.sort((left, right) => left.seq - right.seq)
2550
+ })
2551
+ )
2552
+ }
2553
+
2554
+ const gate = Semaphore.makeUnsafe(1)
2555
+ const transactionDepth = new Map<number, number>()
2556
+ const guard = <A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>
2557
+ Effect.flatMap(
2558
+ Effect.fiberId,
2559
+ (fiberId) => (transactionDepth.get(fiberId) ?? 0) > 0 ? effect : gate.withPermit(effect)
2560
+ )
2561
+
2562
+ const restoreMap = <K, V>(target: Map<K, V>, source: ReadonlyMap<K, V>): void => {
2563
+ target.clear()
2564
+ for (const [key, value] of source) target.set(key, value)
2565
+ }
2566
+
2567
+ const transaction: Service["transaction"] = <A, E, R>(
2568
+ effect: Effect.Effect<A, E, R>,
2569
+ options?: { readonly onCommit: (commit: Effect.Effect<void>) => void }
2570
+ ): Effect.Effect<A, E, R> =>
2571
+ Effect.flatMap(Effect.fiberId, (fiberId) =>
2572
+ Effect.uninterruptibleMask((restore) =>
2573
+ Effect.gen(function*() {
2574
+ const nested = (transactionDepth.get(fiberId) ?? 0) > 0
2575
+ if (!nested) yield* restore(gate.take(1))
2576
+ const before = {
2577
+ deferreds: new Map(deferreds),
2578
+ consumedDeferreds: new Map(consumedDeferreds),
2579
+ clocks: new Map(clocks),
2580
+ waitingRows: new Map(waitingRows),
2581
+ parentEdges: new Map([...parentEdges].map(([child, parents]) => [child, new Map(parents)])),
2582
+ parentSeq
2583
+ }
2584
+ transactionDepth.set(fiberId, (transactionDepth.get(fiberId) ?? 0) + 1)
2585
+ let committed = false
2586
+ let closed = false
2587
+ const close = Effect.suspend(() => {
2588
+ if (closed) return Effect.void
2589
+ closed = true
2590
+ const depth = transactionDepth.get(fiberId)! - 1
2591
+ if (depth === 0) transactionDepth.delete(fiberId)
2592
+ else transactionDepth.set(fiberId, depth)
2593
+ return nested ? Effect.void : gate.release(1).pipe(Effect.asVoid)
2594
+ })
2595
+ const commit = Effect.sync(() => {
2596
+ committed = true
2597
+ }).pipe(Effect.andThen(close))
2598
+ return yield* Effect.sync(() => options?.onCommit(commit)).pipe(
2599
+ Effect.andThen(restore(effect)),
2600
+ Effect.catchCause((cause) =>
2601
+ Effect.sync(() => {
2602
+ if (committed) return
2603
+ restoreMap(deferreds, before.deferreds)
2604
+ restoreMap(consumedDeferreds, before.consumedDeferreds)
2605
+ restoreMap(clocks, before.clocks)
2606
+ restoreMap(waitingRows, before.waitingRows)
2607
+ restoreMap(parentEdges, before.parentEdges)
2608
+ parentSeq = before.parentSeq
2609
+ }).pipe(Effect.andThen(Effect.failCause(cause)))
2610
+ ),
2611
+ Effect.ensuring(close)
2612
+ )
2613
+ })
2614
+ ))
2615
+
2616
+ return DurableEngineState.of({
2617
+ deferred: (address) => guard(unguarded.deferred(address)),
2618
+ consumeDeferred: (address, consumedAtMs) => guard(unguarded.consumeDeferred(address, consumedAtMs)),
2619
+ completeDeferred: (row) => guard(unguarded.completeDeferred(row)),
2620
+ clock: (address) => guard(unguarded.clock(address)),
2621
+ scheduleClock: (row, owner) => guard(unguarded.scheduleClock(row, owner)),
2622
+ completeClock: (address, completedAtMs) => guard(unguarded.completeClock(address, completedAtMs)),
2623
+ dueClocks: (nowMs) => guard(unguarded.dueClocks(nowMs)),
2624
+ completeRunClocks: (executionId, completedAtMs) => guard(unguarded.completeRunClocks(executionId, completedAtMs)),
2625
+ pendingClocks: (scope) => guard(unguarded.pendingClocks(scope)),
2626
+ completedDeferreds: (flowName) => guard(unguarded.completedDeferreds(flowName)),
2627
+ park: (runId, waiting, owner) => guard(unguarded.park(runId, waiting, owner)),
2628
+ wake: (runId) => guard(unguarded.wake(runId)),
2629
+ waiting: (runId) => guard(unguarded.waiting(runId)),
2630
+ waitingTree: (runId, options) => guard(unguarded.waitingTree(runId, options)),
2631
+ waitingRuns: (filter) => guard(unguarded.waitingRuns(filter)),
2632
+ pendingRuns: (flowName, limit, after, through, eligibleBeforeMs) =>
2633
+ guard(unguarded.pendingRuns(flowName, limit, after, through, eligibleBeforeMs)),
2634
+ pendingRunTail: (flowName, eligibleBeforeMs) => guard(unguarded.pendingRunTail(flowName, eligibleBeforeMs)),
2635
+ pendingCancellationRuns: (limit, after, through) => guard(unguarded.pendingCancellationRuns(limit, after, through)),
2636
+ pendingCancellationTail: () => guard(unguarded.pendingCancellationTail()),
2637
+ staleRunningRuns: (staleBeforeMs, limit) => guard(unguarded.staleRunningRuns(staleBeforeMs, limit)),
2638
+ attemptSurvivors: undefined,
2639
+ recordRunParent: (childId, parentId) => guard(unguarded.recordRunParent(childId, parentId)),
2640
+ removeRunParentsForRun: (runId) => guard(unguarded.removeRunParentsForRun(runId)),
2641
+ runParents: (childId) => guard(unguarded.runParents(childId)),
2642
+ runChildren: (parentId) => guard(unguarded.runChildren(parentId)),
2643
+ transaction
2644
+ })
2645
+ }
2646
+
2647
+ /**
2648
+ * Provides deterministic in-memory durable engine state.
2649
+ *
2650
+ * @since 0.1.0
2651
+ * @category layers
2652
+ */
2653
+ export const layerMemory: Layer.Layer<DurableEngineState> = Layer.sync(
2654
+ DurableEngineState,
2655
+ makeMemory
2656
+ )